Merge branch 'version-13-pre-release' into version-13

This commit is contained in:
Rohit Waghchaure
2021-11-09 20:16:01 +05:30
152 changed files with 2577 additions and 1112 deletions

View File

@@ -1,38 +0,0 @@
# Semgrep linting
## What is semgrep?
Semgrep or "semantic grep" is language agnostic static analysis tool. In simple terms semgrep is syntax-aware `grep`, so unlike regex it doesn't get confused by different ways of writing same thing or whitespaces or code split in multiple lines etc.
Example:
To check if a translate function is using f-string or not the regex would be `r"_\(\s*f[\"']"` while equivalent rule in semgrep would be `_(f"...")`. As semgrep knows grammer of language it takes care of unnecessary whitespace, type of quotation marks etc.
You can read more such examples in `.github/helper/semgrep_rules` directory.
# Why/when to use this?
We want to maintain quality of contributions, at the same time remembering all the good practices can be pain to deal with while evaluating contributions. Using semgrep if you can translate "best practice" into a rule then it can automate the task for us.
## Running locally
Install semgrep using homebrew `brew install semgrep` or pip `pip install semgrep`.
To run locally use following command:
`semgrep --config=.github/helper/semgrep_rules [file/folder names]`
## Testing
semgrep allows testing the tests. Refer to this page: https://semgrep.dev/docs/writing-rules/testing-rules/
When writing new rules you should write few positive and few negative cases as shown in the guide and current tests.
To run current tests: `semgrep --test --test-ignore-todo .github/helper/semgrep_rules`
## Reference
If you are new to Semgrep read following pages to get started on writing/modifying rules:
- https://semgrep.dev/docs/getting-started/
- https://semgrep.dev/docs/writing-rules/rule-syntax
- https://semgrep.dev/docs/writing-rules/pattern-examples/
- https://semgrep.dev/docs/writing-rules/rule-ideas/#common-use-cases

View File

@@ -1,34 +0,0 @@
rules:
- id: frappe-missing-translate-function-in-report-python
paths:
include:
- "**/report"
exclude:
- "**/regional"
pattern-either:
- patterns:
- pattern: |
{..., "label": "...", ...}
- pattern-not: |
{..., "label": _("..."), ...}
- patterns:
- pattern: dict(..., label="...", ...)
- pattern-not: dict(..., label=_("..."), ...)
message: |
All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
languages: [python]
severity: ERROR
- id: frappe-translated-values-in-business-logic
paths:
include:
- "**/report"
patterns:
- pattern-inside: |
{..., filters: [...], ...}
- pattern: |
{..., options: [..., __("..."), ...], ...}
message: |
Using translated values in options field will require you to translate the values while comparing in business logic. Instead of passing translated labels provide objects that contain both label and value. e.g. { label: __("Option value"), value: "Option value"}
languages: [javascript]
severity: ERROR

View File

@@ -1,6 +0,0 @@
def function_name(input):
# ruleid: frappe-codeinjection-eval
eval(input)
# ok: frappe-codeinjection-eval
eval("1 + 1")

View File

@@ -1,10 +0,0 @@
rules:
- id: frappe-codeinjection-eval
patterns:
- pattern-not: eval("...")
- pattern: eval(...)
message: |
Detected the use of eval(). eval() can be dangerous if used to evaluate
dynamic content. Avoid it or use safe_eval().
languages: [python]
severity: ERROR

View File

@@ -1,44 +0,0 @@
// ruleid: frappe-translation-empty-string
__("")
// ruleid: frappe-translation-empty-string
__('')
// ok: frappe-translation-js-formatting
__('Welcome {0}, get started with ERPNext in just a few clicks.', [full_name]);
// ruleid: frappe-translation-js-formatting
__(`Welcome ${full_name}, get started with ERPNext in just a few clicks.`);
// ok: frappe-translation-js-formatting
__('This is fine');
// ok: frappe-translation-trailing-spaces
__('This is fine');
// ruleid: frappe-translation-trailing-spaces
__(' this is not ok ');
// ruleid: frappe-translation-trailing-spaces
__('this is not ok ');
// ruleid: frappe-translation-trailing-spaces
__(' this is not ok');
// ok: frappe-translation-js-splitting
__('You have {0} subscribers in your mailing list.', [subscribers.length])
// todoruleid: frappe-translation-js-splitting
__('You have') + subscribers.length + __('subscribers in your mailing list.')
// ruleid: frappe-translation-js-splitting
__('You have' + 'subscribers in your mailing list.')
// ruleid: frappe-translation-js-splitting
__('You have {0} subscribers' +
'in your mailing list', [subscribers.length])
// ok: frappe-translation-js-splitting
__("Ctrl+Enter to add comment")
// ruleid: frappe-translation-js-splitting
__('You have {0} subscribers \
in your mailing list', [subscribers.length])

View File

@@ -1,61 +0,0 @@
# Examples taken from https://frappeframework.com/docs/user/en/translations
# This file is used for testing the tests.
from frappe import _
full_name = "Jon Doe"
# ok: frappe-translation-python-formatting
_('Welcome {0}, get started with ERPNext in just a few clicks.').format(full_name)
# ruleid: frappe-translation-python-formatting
_('Welcome %s, get started with ERPNext in just a few clicks.' % full_name)
# ruleid: frappe-translation-python-formatting
_('Welcome %(name)s, get started with ERPNext in just a few clicks.' % {'name': full_name})
# ruleid: frappe-translation-python-formatting
_('Welcome {0}, get started with ERPNext in just a few clicks.'.format(full_name))
subscribers = ["Jon", "Doe"]
# ok: frappe-translation-python-formatting
_('You have {0} subscribers in your mailing list.').format(len(subscribers))
# ruleid: frappe-translation-python-splitting
_('You have') + len(subscribers) + _('subscribers in your mailing list.')
# ruleid: frappe-translation-python-splitting
_('You have {0} subscribers \
in your mailing list').format(len(subscribers))
# ok: frappe-translation-python-splitting
_('You have {0} subscribers') \
+ 'in your mailing list'
# ruleid: frappe-translation-trailing-spaces
msg = _(" You have {0} pending invoice ")
# ruleid: frappe-translation-trailing-spaces
msg = _("You have {0} pending invoice ")
# ruleid: frappe-translation-trailing-spaces
msg = _(" You have {0} pending invoice")
# ok: frappe-translation-trailing-spaces
msg = ' ' + _("You have {0} pending invoices") + ' '
# ruleid: frappe-translation-python-formatting
_(f"can not format like this - {subscribers}")
# ruleid: frappe-translation-python-splitting
_(f"what" + f"this is also not cool")
# ruleid: frappe-translation-empty-string
_("")
# ruleid: frappe-translation-empty-string
_('')
class Test:
# ok: frappe-translation-python-splitting
def __init__(
args
):
pass

View File

@@ -1,64 +0,0 @@
rules:
- id: frappe-translation-empty-string
pattern-either:
- pattern: _("")
- pattern: __("")
message: |
Empty string is useless for translation.
Please refer: https://frappeframework.com/docs/user/en/translations
languages: [python, javascript, json]
severity: ERROR
- id: frappe-translation-trailing-spaces
pattern-either:
- pattern: _("=~/(^[ \t]+|[ \t]+$)/")
- pattern: __("=~/(^[ \t]+|[ \t]+$)/")
message: |
Trailing or leading whitespace not allowed in translate strings.
Please refer: https://frappeframework.com/docs/user/en/translations
languages: [python, javascript, json]
severity: ERROR
- id: frappe-translation-python-formatting
pattern-either:
- pattern: _("..." % ...)
- pattern: _("...".format(...))
- pattern: _(f"...")
message: |
Only positional formatters are allowed and formatting should not be done before translating.
Please refer: https://frappeframework.com/docs/user/en/translations
languages: [python]
severity: ERROR
- id: frappe-translation-js-formatting
patterns:
- pattern: __(`...`)
- pattern-not: __("...")
message: |
Template strings are not allowed for text formatting.
Please refer: https://frappeframework.com/docs/user/en/translations
languages: [javascript, json]
severity: ERROR
- id: frappe-translation-python-splitting
pattern-either:
- pattern: _(...) + _(...)
- pattern: _("..." + "...")
- pattern-regex: '[\s\.]_\([^\)]*\\\s*' # lines broken by `\`
- pattern-regex: '[\s\.]_\(\s*\n' # line breaks allowed by python for using ( )
message: |
Do not split strings inside translate function. Do not concatenate using translate functions.
Please refer: https://frappeframework.com/docs/user/en/translations
languages: [python]
severity: ERROR
- id: frappe-translation-js-splitting
pattern-either:
- pattern-regex: '__\([^\)]*[\\]\s+'
- pattern: __('...' + '...', ...)
- pattern: __('...') + __('...')
message: |
Do not split strings inside translate function. Do not concatenate using translate functions.
Please refer: https://frappeframework.com/docs/user/en/translations
languages: [javascript, json]
severity: ERROR

View File

@@ -1,9 +0,0 @@
// ok: frappe-missing-translate-function-js
frappe.msgprint('{{ _("Both login and password required") }}');
// ruleid: frappe-missing-translate-function-js
frappe.msgprint('What');
// ok: frappe-missing-translate-function-js
frappe.throw(' {{ _("Both login and password required") }}. ');

View File

@@ -1,30 +0,0 @@
rules:
- id: frappe-missing-translate-function-python
pattern-either:
- patterns:
- pattern: frappe.msgprint("...", ...)
- pattern-not: frappe.msgprint(_("..."), ...)
- patterns:
- pattern: frappe.throw("...", ...)
- pattern-not: frappe.throw(_("..."), ...)
message: |
All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
languages: [python]
severity: ERROR
- id: frappe-missing-translate-function-js
pattern-either:
- patterns:
- pattern: frappe.msgprint("...", ...)
- pattern-not: frappe.msgprint(__("..."), ...)
# ignore microtemplating e.g. msgprint("{{ _("server side translation") }}")
- pattern-not: frappe.msgprint("=~/\{\{.*\_.*\}\}/i", ...)
- patterns:
- pattern: frappe.throw("...", ...)
- pattern-not: frappe.throw(__("..."), ...)
# ignore microtemplating
- pattern-not: frappe.throw("=~/\{\{.*\_.*\}\}/i", ...)
message: |
All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
languages: [javascript]
severity: ERROR

View File

@@ -10,13 +10,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - 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 - name: Set up Python 3.8
uses: actions/setup-python@v2 uses: actions/setup-python@v2
@@ -24,4 +17,15 @@ jobs:
python-version: 3.8 python-version: 3.8
- name: Install and Run Pre-commit - name: Install and Run Pre-commit
uses: pre-commit/action@v2.0.0 uses: pre-commit/action@v2.0.3
- name: Download Semgrep rules
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
- uses: returntocorp/semgrep-action@v1
env:
SEMGREP_TIMEOUT: 120
with:
config: >-
r/python.lang.correctness
./frappe-semgrep-rules/rules

View File

@@ -7,7 +7,7 @@ import frappe
from erpnext.hooks import regional_overrides from erpnext.hooks import regional_overrides
__version__ = '13.13.0' __version__ = '13.14.0'
def get_default_company(user=None): def get_default_company(user=None):
'''Get default company for user''' '''Get default company for user'''

View File

@@ -81,7 +81,7 @@ def add_suffix_if_duplicate(account_name, account_number, accounts):
def identify_is_group(child): def identify_is_group(child):
if child.get("is_group"): if child.get("is_group"):
is_group = child.get("is_group") is_group = child.get("is_group")
elif len(set(child.keys()) - set(["account_type", "root_type", "is_group", "tax_rate", "account_number"])): elif len(set(child.keys()) - set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"])):
is_group = 1 is_group = 1
else: else:
is_group = 0 is_group = 0

View File

@@ -27,10 +27,12 @@
"payment_accounts_section", "payment_accounts_section",
"party_balance", "party_balance",
"paid_from", "paid_from",
"paid_from_account_type",
"paid_from_account_currency", "paid_from_account_currency",
"paid_from_account_balance", "paid_from_account_balance",
"column_break_18", "column_break_18",
"paid_to", "paid_to",
"paid_to_account_type",
"paid_to_account_currency", "paid_to_account_currency",
"paid_to_account_balance", "paid_to_account_balance",
"payment_amounts_section", "payment_amounts_section",
@@ -440,7 +442,8 @@
"depends_on": "eval:(doc.paid_from && doc.paid_to)", "depends_on": "eval:(doc.paid_from && doc.paid_to)",
"fieldname": "reference_no", "fieldname": "reference_no",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Cheque/Reference No" "label": "Cheque/Reference No",
"mandatory_depends_on": "eval:(doc.paid_from_account_type == 'Bank' || doc.paid_to_account_type == 'Bank')"
}, },
{ {
"fieldname": "column_break_23", "fieldname": "column_break_23",
@@ -452,6 +455,7 @@
"fieldname": "reference_date", "fieldname": "reference_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Cheque/Reference Date", "label": "Cheque/Reference Date",
"mandatory_depends_on": "eval:(doc.paid_from_account_type == 'Bank' || doc.paid_to_account_type == 'Bank')",
"search_index": 1 "search_index": 1
}, },
{ {
@@ -707,15 +711,30 @@
"label": "Received Amount After Tax (Company Currency)", "label": "Received Amount After Tax (Company Currency)",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "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, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-07-09 08:58:15.008761", "modified": "2021-10-22 17:50:24.632806",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry", "name": "Payment Entry",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {

View File

@@ -389,7 +389,7 @@ class PaymentEntry(AccountsController):
invoice_paid_amount_map[invoice_key]['outstanding'] = term.outstanding invoice_paid_amount_map[invoice_key]['outstanding'] = term.outstanding
invoice_paid_amount_map[invoice_key]['discounted_amt'] = ref.total_amount * (term.discount / 100) invoice_paid_amount_map[invoice_key]['discounted_amt'] = ref.total_amount * (term.discount / 100)
for key, allocated_amount in iteritems(invoice_payment_amount_map): for idx, (key, allocated_amount) in enumerate(iteritems(invoice_payment_amount_map), 1):
if not invoice_paid_amount_map.get(key): if not invoice_paid_amount_map.get(key):
frappe.throw(_('Payment term {0} not used in {1}').format(key[0], key[1])) 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])) (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]))
else: else:
if allocated_amount > outstanding: if allocated_amount > outstanding:
frappe.throw(_('Cannot allocate more than {0} against payment term {1}').format(outstanding, key[0])) frappe.throw(_('Row #{0}: Cannot allocate more than {1} against payment term {2}').format(idx, outstanding, key[0]))
if allocated_amount and outstanding: if allocated_amount and outstanding:
frappe.db.sql(""" frappe.db.sql("""
@@ -1053,12 +1053,6 @@ def get_outstanding_reference_documents(args):
party_account_currency = get_account_currency(args.get("party_account")) party_account_currency = get_account_currency(args.get("party_account"))
company_currency = frappe.get_cached_value('Company', args.get("company"), "default_currency") 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 # Get positive outstanding sales /purchase invoices/ Fees
condition = "" condition = ""
if args.get("voucher_type") and args.get("voucher_no"): if args.get("voucher_type") and args.get("voucher_no"):
@@ -1105,6 +1099,12 @@ def get_outstanding_reference_documents(args):
orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"), 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) 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 data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed
if not data: if not data:
@@ -1137,22 +1137,26 @@ def split_invoices_based_on_payment_terms(outstanding_invoices):
'invoice_amount': flt(d.invoice_amount), 'invoice_amount': flt(d.invoice_amount),
'outstanding_amount': flt(d.outstanding_amount), 'outstanding_amount': flt(d.outstanding_amount),
'payment_amount': payment_term.payment_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: if invoice_ref_based_on_payment_terms:
for idx, ref in invoice_ref_based_on_payment_terms.items(): for idx, ref in invoice_ref_based_on_payment_terms.items():
voucher_no = outstanding_invoices[idx]['voucher_no'] voucher_no = ref[0]['voucher_no']
voucher_type = outstanding_invoices[idx]['voucher_type'] voucher_type = ref[0]['voucher_type']
frappe.msgprint(_("Spliting {} {} into {} rows as per payment terms").format( frappe.msgprint(_("Spliting {} {} into {} row(s) as per Payment Terms").format(
voucher_type, voucher_no, len(ref)), alert=True) voucher_type, voucher_no, len(ref)), alert=True)
outstanding_invoices.pop(idx - 1) outstanding_invoices_after_split += invoice_ref_based_on_payment_terms[idx]
outstanding_invoices += invoice_ref_based_on_payment_terms[idx]
return outstanding_invoices 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
def get_orders_to_be_billed(posting_date, party_type, party, def get_orders_to_be_billed(posting_date, party_type, party,
company, party_account_currency, company_currency, cost_center=None, filters=None): company, party_account_currency, company_currency, cost_center=None, filters=None):
@@ -1219,7 +1223,7 @@ def get_orders_to_be_billed(posting_date, party_type, party,
return order_list return order_list
def get_negative_outstanding_invoices(party_type, party, party_account, def get_negative_outstanding_invoices(party_type, party, party_account,
company, party_account_currency, company_currency, cost_center=None): party_account_currency, company_currency, cost_center=None, condition=None):
voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice" voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice"
supplier_condition = "" supplier_condition = ""
if voucher_type == "Purchase Invoice": if voucher_type == "Purchase Invoice":
@@ -1241,19 +1245,21 @@ def get_negative_outstanding_invoices(party_type, party, party_account,
`tab{voucher_type}` `tab{voucher_type}`
where where
{party_type} = %s and {party_account} = %s and docstatus = 1 and {party_type} = %s and {party_account} = %s and docstatus = 1 and
company = %s and outstanding_amount < 0 outstanding_amount < 0
{supplier_condition} {supplier_condition}
{condition}
order by order by
posting_date, name posting_date, name
""".format(**{ """.format(**{
"supplier_condition": supplier_condition, "supplier_condition": supplier_condition,
"condition": condition,
"rounded_total_field": rounded_total_field, "rounded_total_field": rounded_total_field,
"grand_total_field": grand_total_field, "grand_total_field": grand_total_field,
"voucher_type": voucher_type, "voucher_type": voucher_type,
"party_type": scrub(party_type), "party_type": scrub(party_type),
"party_account": "debit_to" if party_type == "Customer" else "credit_to", "party_account": "debit_to" if party_type == "Customer" else "credit_to",
"cost_center": cost_center "cost_center": cost_center
}), (party, party_account, company), as_dict=True) }), (party, party_account), as_dict=True)
@frappe.whitelist() @frappe.whitelist()

View File

@@ -10,6 +10,9 @@ frappe.ui.form.on('Payment Order', {
} }
} }
}); });
frm.set_df_property('references', 'cannot_add_rows', true);
frm.set_df_property('references', 'cannot_delete_rows', true);
}, },
refresh: function(frm) { refresh: function(frm) {
if (frm.doc.docstatus == 0) { if (frm.doc.docstatus == 0) {

View File

@@ -4,9 +4,14 @@
frappe.provide("erpnext.accounts"); frappe.provide("erpnext.accounts");
erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.extend({ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.extend({
onload: function() { onload: function() {
var me = this; const default_company = frappe.defaults.get_default('company');
this.frm.set_value('company', default_company);
this.frm.set_query("party_type", function() { this.frm.set_value('party_type', '');
this.frm.set_value('party', '');
this.frm.set_value('receivable_payable_account', '');
this.frm.set_query("party_type", () => {
return { return {
"filters": { "filters": {
"name": ["in", Object.keys(frappe.boot.party_account_types)], "name": ["in", Object.keys(frappe.boot.party_account_types)],
@@ -14,44 +19,30 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
} }
}); });
this.frm.set_query('receivable_payable_account', function() { this.frm.set_query('receivable_payable_account', () => {
check_mandatory(me.frm);
return { return {
filters: { filters: {
"company": me.frm.doc.company, "company": this.frm.doc.company,
"is_group": 0, "is_group": 0,
"account_type": frappe.boot.party_account_types[me.frm.doc.party_type] "account_type": frappe.boot.party_account_types[this.frm.doc.party_type]
} }
}; };
}); });
this.frm.set_query('bank_cash_account', function() { this.frm.set_query('bank_cash_account', () => {
check_mandatory(me.frm, true);
return { return {
filters:[ filters:[
['Account', 'company', '=', me.frm.doc.company], ['Account', 'company', '=', this.frm.doc.company],
['Account', 'is_group', '=', 0], ['Account', 'is_group', '=', 0],
['Account', 'account_type', 'in', ['Bank', 'Cash']] ['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() { refresh: function() {
this.frm.disable_save(); this.frm.disable_save();
this.frm.set_df_property('invoices', 'cannot_delete_rows', true); 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('payments', 'cannot_delete_rows', true);
this.frm.set_df_property('allocation', 'cannot_delete_rows', true); this.frm.set_df_property('allocation', 'cannot_delete_rows', true);
@@ -85,76 +76,92 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
}, },
company: function() { company: function() {
var me = this; this.frm.set_value('party', '');
this.frm.set_value('receivable_payable_account', ''); this.frm.set_value('receivable_payable_account', '');
me.frm.clear_table("allocation"); },
me.frm.clear_table("invoices");
me.frm.clear_table("payments"); party_type: function() {
me.frm.refresh_fields(); this.frm.set_value('party', '');
me.frm.trigger('party');
}, },
party: function() { party: function() {
var me = this; this.frm.set_value('receivable_payable_account', '');
if (!me.frm.doc.receivable_payable_account && me.frm.doc.party_type && me.frm.doc.party) { this.frm.trigger("clear_child_tables");
if (!this.frm.doc.receivable_payable_account && this.frm.doc.party_type && this.frm.doc.party) {
return frappe.call({ return frappe.call({
method: "erpnext.accounts.party.get_party_account", method: "erpnext.accounts.party.get_party_account",
args: { args: {
company: me.frm.doc.company, company: this.frm.doc.company,
party_type: me.frm.doc.party_type, party_type: this.frm.doc.party_type,
party: me.frm.doc.party party: this.frm.doc.party
}, },
callback: function(r) { callback: (r) => {
if (!r.exc && r.message) { if (!r.exc && r.message) {
me.frm.set_value("receivable_payable_account", r.message); this.frm.set_value("receivable_payable_account", r.message);
} }
me.frm.refresh(); this.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() { get_unreconciled_entries: function() {
var me = this; this.frm.clear_table("allocation");
return this.frm.call({ return this.frm.call({
doc: me.frm.doc, doc: this.frm.doc,
method: 'get_unreconciled_entries', method: 'get_unreconciled_entries',
callback: function(r, rt) { callback: () => {
if (!(me.frm.doc.payments.length || me.frm.doc.invoices.length)) { if (!(this.frm.doc.payments.length || this.frm.doc.invoices.length)) {
frappe.throw({message: __("No invoice and payment records found for this party")}); 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")});
} }
me.frm.refresh(); this.frm.refresh();
} }
}); });
}, },
allocate: function() { allocate: function() {
var me = this; let payments = this.frm.fields_dict.payments.grid.get_selected_children();
let payments = me.frm.fields_dict.payments.grid.get_selected_children();
if (!(payments.length)) { if (!(payments.length)) {
payments = me.frm.doc.payments; payments = this.frm.doc.payments;
} }
let invoices = me.frm.fields_dict.invoices.grid.get_selected_children(); let invoices = this.frm.fields_dict.invoices.grid.get_selected_children();
if (!(invoices.length)) { if (!(invoices.length)) {
invoices = me.frm.doc.invoices; invoices = this.frm.doc.invoices;
} }
return me.frm.call({ return this.frm.call({
doc: me.frm.doc, doc: this.frm.doc,
method: 'allocate_entries', method: 'allocate_entries',
args: { args: {
payments: payments, payments: payments,
invoices: invoices invoices: invoices
}, },
callback: function() { callback: () => {
me.frm.refresh(); this.frm.refresh();
} }
}); });
}, },
reconcile: function() { reconcile: function() {
var me = this; var show_dialog = this.frm.doc.allocation.filter(d => d.difference_amount && !d.difference_account);
var show_dialog = me.frm.doc.allocation.filter(d => d.difference_amount && !d.difference_account);
if (show_dialog && show_dialog.length) { if (show_dialog && show_dialog.length) {
@@ -186,10 +193,10 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
label: __("Difference Account"), label: __("Difference Account"),
fieldname: 'difference_account', fieldname: 'difference_account',
reqd: 1, reqd: 1,
get_query: function() { get_query: () => {
return { return {
filters: { filters: {
company: me.frm.doc.company, company: this.frm.doc.company,
is_group: 0 is_group: 0
} }
} }
@@ -203,7 +210,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
}] }]
}, },
], ],
primary_action: function() { primary_action: () => {
const args = dialog.get_values()["allocation"]; const args = dialog.get_values()["allocation"];
args.forEach(d => { args.forEach(d => {
@@ -211,7 +218,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
"difference_account", d.difference_account); "difference_account", d.difference_account);
}); });
me.reconcile_payment_entries(); this.reconcile_payment_entries();
dialog.hide(); dialog.hide();
}, },
primary_action_label: __('Reconcile Entries') primary_action_label: __('Reconcile Entries')
@@ -237,15 +244,12 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
}, },
reconcile_payment_entries: function() { reconcile_payment_entries: function() {
var me = this;
return this.frm.call({ return this.frm.call({
doc: me.frm.doc, doc: this.frm.doc,
method: 'reconcile', method: 'reconcile',
callback: function(r, rt) { callback: () => {
me.frm.clear_table("allocation"); this.frm.clear_table("allocation");
me.frm.refresh_fields(); this.frm.refresh();
me.frm.refresh();
} }
}); });
} }

View File

@@ -180,8 +180,7 @@
"fieldname": "pos_transactions", "fieldname": "pos_transactions",
"fieldtype": "Table", "fieldtype": "Table",
"label": "POS Transactions", "label": "POS Transactions",
"options": "POS Invoice Reference", "options": "POS Invoice Reference"
"reqd": 1
}, },
{ {
"fieldname": "pos_opening_entry", "fieldname": "pos_opening_entry",
@@ -229,7 +228,7 @@
"link_fieldname": "pos_closing_entry" "link_fieldname": "pos_closing_entry"
} }
], ],
"modified": "2021-05-05 16:59:49.723261", "modified": "2021-10-20 16:19:25.340565",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Closing Entry", "name": "POS Closing Entry",

View File

@@ -114,6 +114,8 @@ class POSInvoiceMergeLog(Document):
def merge_pos_invoice_into(self, invoice, data): def merge_pos_invoice_into(self, invoice, data):
items, payments, taxes = [], [], [] items, payments, taxes = [], [], []
loyalty_amount_sum, loyalty_points_sum = 0, 0 loyalty_amount_sum, loyalty_points_sum = 0, 0
rounding_adjustment, base_rounding_adjustment = 0, 0
rounded_total, base_rounded_total = 0, 0
for doc in data: for doc in data:
map_doc(doc, invoice, table_map={ "doctype": invoice.doctype }) map_doc(doc, invoice, table_map={ "doctype": invoice.doctype })
@@ -162,6 +164,11 @@ class POSInvoiceMergeLog(Document):
found = True found = True
if not found: if not found:
payments.append(payment) payments.append(payment)
rounding_adjustment += doc.rounding_adjustment
rounded_total += doc.rounded_total
base_rounding_adjustment += doc.rounding_adjustment
base_rounded_total += doc.rounded_total
if loyalty_points_sum: if loyalty_points_sum:
invoice.redeem_loyalty_points = 1 invoice.redeem_loyalty_points = 1
@@ -171,6 +178,10 @@ class POSInvoiceMergeLog(Document):
invoice.set('items', items) invoice.set('items', items)
invoice.set('payments', payments) invoice.set('payments', payments)
invoice.set('taxes', taxes) invoice.set('taxes', taxes)
invoice.set('rounding_adjustment',rounding_adjustment)
invoice.set('rounding_adjustment',base_rounding_adjustment)
invoice.set('base_rounded_total',base_rounded_total)
invoice.set('rounded_total',rounded_total)
invoice.additional_discount_percentage = 0 invoice.additional_discount_percentage = 0
invoice.discount_amount = 0.0 invoice.discount_amount = 0.0
invoice.taxes_and_charges = None invoice.taxes_and_charges = None
@@ -246,7 +257,10 @@ def get_invoice_customer_map(pos_invoices):
return pos_invoice_customer_map return pos_invoice_customer_map
def consolidate_pos_invoices(pos_invoices=None, closing_entry=None): def consolidate_pos_invoices(pos_invoices=None, closing_entry=None):
invoices = pos_invoices or (closing_entry and closing_entry.get('pos_transactions')) or get_all_unconsolidated_invoices() invoices = pos_invoices or (closing_entry and closing_entry.get('pos_transactions'))
if frappe.flags.in_test and not invoices:
invoices = get_all_unconsolidated_invoices()
invoice_by_customer = get_invoice_customer_map(invoices) invoice_by_customer = get_invoice_customer_map(invoices)
if len(invoices) >= 10 and closing_entry: if len(invoices) >= 10 and closing_entry:

View File

@@ -120,6 +120,7 @@
{ {
"fieldname": "payments", "fieldname": "payments",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Payment Methods",
"options": "POS Payment Method", "options": "POS Payment Method",
"reqd": 1 "reqd": 1
}, },
@@ -377,7 +378,7 @@
"link_fieldname": "pos_profile" "link_fieldname": "pos_profile"
} }
], ],
"modified": "2021-02-01 13:52:51.081311", "modified": "2021-10-14 14:17:00.469298",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Profile", "name": "POS Profile",

View File

@@ -19,6 +19,7 @@ from erpnext.stock.get_item_details import get_item_details
class TestPricingRule(unittest.TestCase): class TestPricingRule(unittest.TestCase):
def setUp(self): def setUp(self):
delete_existing_pricing_rules() delete_existing_pricing_rules()
setup_pricing_rule_data()
def tearDown(self): def tearDown(self):
delete_existing_pricing_rules() delete_existing_pricing_rules()
@@ -561,6 +562,8 @@ class TestPricingRule(unittest.TestCase):
for doc in [si, si1]: for doc in [si, si1]:
doc.delete() doc.delete()
test_dependencies = ["Campaign"]
def make_pricing_rule(**args): def make_pricing_rule(**args):
args = frappe._dict(args) args = frappe._dict(args)
@@ -607,6 +610,13 @@ def make_pricing_rule(**args):
if args.get(applicable_for): if args.get(applicable_for):
doc.db_set(applicable_for, 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(): def delete_existing_pricing_rules():
for doctype in ["Pricing Rule", "Pricing Rule Item Code", for doctype in ["Pricing Rule", "Pricing Rule Item Code",

View File

@@ -29,6 +29,9 @@ def get_pricing_rules(args, doc=None):
pricing_rules = [] pricing_rules = []
values = {} values = {}
if not frappe.db.exists('Pricing Rule', {'disable': 0, args.transaction_type: 1}):
return
for apply_on in ['Item Code', 'Item Group', 'Brand']: for apply_on in ['Item Code', 'Item Group', 'Brand']:
pricing_rules.extend(_get_pricing_rules(apply_on, args, values)) pricing_rules.extend(_get_pricing_rules(apply_on, args, values))
if pricing_rules and not apply_multiple_pricing_rules(pricing_rules): if pricing_rules and not apply_multiple_pricing_rules(pricing_rules):

View File

@@ -590,5 +590,11 @@ frappe.ui.form.on("Purchase Invoice", {
company: function(frm) { company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
if (frm.doc.company) {
frappe.db.get_value('Company', frm.doc.company, 'default_payable_account', (r) => {
frm.set_value('credit_to', r.default_payable_account);
});
}
}, },
}) })

View File

@@ -10,9 +10,17 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
this.setup_posting_date_time_check(); this.setup_posting_date_time_check();
this._super(doc); this._super(doc);
}, },
company: function() { company: function() {
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype); erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
let me = this;
if (this.frm.doc.company) {
frappe.db.get_value('Company', this.frm.doc.company, 'default_receivable_account', (r) => {
me.frm.set_value('debit_to', r.default_receivable_account);
});
}
}, },
onload: function() { onload: function() {
var me = this; var me = this;
this._super(); this._super();

View File

@@ -2026,22 +2026,23 @@ def update_multi_mode_option(doc, pos_profile):
def append_payment(payment_mode): def append_payment(payment_mode):
payment = doc.append('payments', {}) payment = doc.append('payments', {})
payment.default = payment_mode.default payment.default = payment_mode.default
payment.mode_of_payment = payment_mode.parent payment.mode_of_payment = payment_mode.mop
payment.account = payment_mode.default_account payment.account = payment_mode.default_account
payment.type = payment_mode.type payment.type = payment_mode.type
doc.set('payments', []) doc.set('payments', [])
invalid_modes = [] invalid_modes = []
for pos_payment_method in pos_profile.get('payments'): mode_of_payments = [d.mode_of_payment for d in pos_profile.get('payments')]
pos_payment_method = pos_payment_method.as_dict() mode_of_payments_info = get_mode_of_payments_info(mode_of_payments, doc.company)
payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company) for row in pos_profile.get('payments'):
payment_mode = mode_of_payments_info.get(row.mode_of_payment)
if not payment_mode: if not payment_mode:
invalid_modes.append(get_link_to_form("Mode of Payment", pos_payment_method.mode_of_payment)) invalid_modes.append(get_link_to_form("Mode of Payment", row.mode_of_payment))
continue continue
payment_mode[0].default = pos_payment_method.default payment_mode.default = row.default
append_payment(payment_mode[0]) append_payment(payment_mode)
if invalid_modes: if invalid_modes:
if invalid_modes == 1: if invalid_modes == 1:
@@ -2057,6 +2058,24 @@ def get_all_mode_of_payments(doc):
where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""", where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""",
{'company': doc.company}, as_dict=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): def get_mode_of_payment_info(mode_of_payment, company):
return frappe.db.sql(""" return frappe.db.sql("""
select mpa.default_account, mpa.parent, mp.type as type select mpa.default_account, mpa.parent, mp.type as type

View File

@@ -498,9 +498,11 @@ class Subscription(Document):
# Check invoice dates and make sure it doesn't have outstanding invoices # Check invoice dates and make sure it doesn't have outstanding invoices
return getdate() >= getdate(self.current_invoice_start) return getdate() >= getdate(self.current_invoice_start)
def is_current_invoice_generated(self): def is_current_invoice_generated(self, _current_start_date=None, _current_end_date=None):
invoice = self.get_current_invoice() invoice = self.get_current_invoice()
_current_start_date, _current_end_date = self.update_subscription_period(date=add_days(self.current_invoice_end, 1), return_date=True)
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)
if invoice and getdate(_current_start_date) <= getdate(invoice.posting_date) <= getdate(_current_end_date): if invoice and getdate(_current_start_date) <= getdate(invoice.posting_date) <= getdate(_current_end_date):
return True return True
@@ -516,13 +518,16 @@ class Subscription(Document):
2. Change the `Subscription` status to 'Past Due Date' 2. Change the `Subscription` status to 'Past Due Date'
3. Change the `Subscription` status to 'Cancelled' 3. Change the `Subscription` status to 'Cancelled'
""" """
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()): 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') prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
self.generate_invoice(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 self.cancel_at_period_end and getdate() > getdate(self.current_invoice_end): if self.cancel_at_period_end and getdate() > getdate(self.current_invoice_end):
self.cancel_subscription_at_period_end() self.cancel_subscription_at_period_end()
@@ -556,8 +561,10 @@ class Subscription(Document):
self.set_status_grace_period() self.set_status_grace_period()
# Generate invoices periodically even if current invoice are unpaid # Generate invoices periodically even if current invoice are unpaid
if self.generate_new_invoices_past_due_date and not self.is_current_invoice_generated() and (self.is_postpaid_to_invoice() if self.generate_new_invoices_past_due_date and not \
or self.is_prepaid_to_invoice()): 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') prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
self.generate_invoice(prorate) self.generate_invoice(prorate)

View File

@@ -49,15 +49,24 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
pan_no = '' pan_no = ''
parties = [] parties = []
party_type, party = get_party_details(inv) party_type, party = get_party_details(inv)
has_pan_field = frappe.get_meta(party_type).has_field("pan")
if not tax_withholding_category: if not tax_withholding_category:
tax_withholding_category, pan_no = frappe.db.get_value(party_type, party, ['tax_withholding_category', 'pan']) 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')
if not tax_withholding_category: if not tax_withholding_category:
return return
# if tax_withholding_category passed as an argument but not pan_no # if tax_withholding_category passed as an argument but not pan_no
if not pan_no: if not pan_no and has_pan_field:
pan_no = frappe.db.get_value(party_type, party, 'pan') pan_no = frappe.db.get_value(party_type, party, 'pan')
# Get others suppliers with the same PAN No # Get others suppliers with the same PAN No
@@ -165,6 +174,7 @@ def get_lower_deduction_certificate(tax_details, pan_no):
ldc_name = frappe.db.get_value('Lower Deduction Certificate', ldc_name = frappe.db.get_value('Lower Deduction Certificate',
{ {
'pan_no': pan_no, 'pan_no': pan_no,
'tax_withholding_category': tax_details.tax_withholding_category,
'valid_from': ('>=', tax_details.from_date), 'valid_from': ('>=', tax_details.from_date),
'valid_upto': ('<=', tax_details.to_date) 'valid_upto': ('<=', tax_details.to_date)
}, 'name') }, 'name')

View File

@@ -68,7 +68,7 @@
{%- if einvoice.ShipDtls -%} {%- if einvoice.ShipDtls -%}
{%- set shipping = einvoice.ShipDtls -%} {%- set shipping = einvoice.ShipDtls -%}
<h5 style="margin-bottom: 5px;">Shipping</h5> <h5 style="margin-bottom: 5px;">Shipped From</h5>
<p>{{ shipping.Gstin }}</p> <p>{{ shipping.Gstin }}</p>
<p>{{ shipping.LglNm }}</p> <p>{{ shipping.LglNm }}</p>
<p>{{ shipping.Addr1 }}</p> <p>{{ shipping.Addr1 }}</p>
@@ -86,6 +86,17 @@
{%- if buyer.Addr2 -%} <p>{{ buyer.Addr2 }}</p> {% endif %} {%- if buyer.Addr2 -%} <p>{{ buyer.Addr2 }}</p> {% endif %}
<p>{{ buyer.Loc }}</p> <p>{{ buyer.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.customer_address, "gst_state") }} - {{ buyer.Pin }}</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> </div>
<div style="overflow-x: auto;"> <div style="overflow-x: auto;">

View File

@@ -114,8 +114,9 @@ def prepare_companywise_opening_balance(asset_data, liability_data, equity_data,
# opening_value = Aseet - liability - equity # opening_value = Aseet - liability - equity
for data in [asset_data, liability_data, equity_data]: for data in [asset_data, liability_data, equity_data]:
account_name = get_root_account_name(data[0].root_type, company) if data:
opening_value += get_opening_balance(account_name, data, company) account_name = get_root_account_name(data[0].root_type, company)
opening_value += (get_opening_balance(account_name, data, company) or 0.0)
opening_balance[company] = opening_value opening_balance[company] = opening_value

View File

@@ -155,6 +155,8 @@ def get_gl_entries(filters, accounting_dimensions):
if filters.get("group_by") == "Group by Voucher": if filters.get("group_by") == "Group by Voucher":
order_by_statement = "order by posting_date, voucher_type, voucher_no" 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"): if filters.get("include_default_book_entries"):
filters['company_fb'] = frappe.db.get_value("Company", filters['company_fb'] = frappe.db.get_value("Company",

View File

@@ -44,16 +44,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
if rate and tds_deducted: if rate and tds_deducted:
row = { row = {
'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier).pan, 'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'),
'supplier': supplier_map.get(supplier).name 'supplier': supplier_map.get(supplier, {}).get('name')
} }
if filters.naming_series == 'Naming Series': if filters.naming_series == 'Naming Series':
row.update({'supplier_name': supplier_map.get(supplier).supplier_name}) row.update({'supplier_name': supplier_map.get(supplier, {}).get('supplier_name')})
row.update({ row.update({
'section_code': tax_withholding_category, 'section_code': tax_withholding_category,
'entity_type': supplier_map.get(supplier).supplier_type, 'entity_type': supplier_map.get(supplier, {}).get('supplier_type'),
'tds_rate': rate, 'tds_rate': rate,
'total_amount_credited': total_amount_credited, 'total_amount_credited': total_amount_credited,
'tds_deducted': tds_deducted, 'tds_deducted': tds_deducted,

View File

@@ -450,7 +450,8 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
# new row with references # new row with references
new_row = journal_entry.append("accounts") new_row = journal_entry.append("accounts")
new_row.update(jv_detail.as_dict().copy())
new_row.update((frappe.copy_doc(jv_detail)).as_dict())
new_row.set(d["dr_or_cr"], d["allocated_amount"]) 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', new_row.set('debit' if d['dr_or_cr'] == 'debit_in_account_currency' else 'credit',
@@ -579,10 +580,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.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe)))
@frappe.whitelist() @frappe.whitelist()
def get_company_default(company, fieldname): def get_company_default(company, fieldname, ignore_validation=False):
value = frappe.get_cached_value('Company', company, fieldname) value = frappe.get_cached_value('Company', company, fieldname)
if not value: if not ignore_validation and not value:
throw(_("Please set default {0} in Company {1}") throw(_("Please set default {0} in Company {1}")
.format(frappe.get_meta("Company").get_label(fieldname), company)) .format(frappe.get_meta("Company").get_label(fieldname), company))

View File

@@ -454,6 +454,17 @@
"onboard": 0, "onboard": 0,
"type": "Link" "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, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
@@ -1034,6 +1045,16 @@
"onboard": 0, "onboard": 0,
"type": "Link" "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, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
@@ -1082,7 +1103,7 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2021-08-23 16:06:34.167267", "modified": "2021-08-26 13:15:52.872470",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounting", "name": "Accounting",

View File

@@ -16,9 +16,8 @@ frappe.query_reports["Fixed Asset Register"] = {
fieldname:"status", fieldname:"status",
label: __("Status"), label: __("Status"),
fieldtype: "Select", fieldtype: "Select",
options: "In Location\nDisposed", options: "\nIn Location\nDisposed",
default: 'In Location', default: 'In Location'
reqd: 1
}, },
{ {
"fieldname":"filter_based_on", "fieldname":"filter_based_on",

View File

@@ -45,12 +45,13 @@ def get_conditions(filters):
if filters.get('cost_center'): if filters.get('cost_center'):
conditions["cost_center"] = filters.get('cost_center') conditions["cost_center"] = filters.get('cost_center')
# In Store assets are those that are not sold or scrapped if status:
operand = 'not in' # In Store assets are those that are not sold or scrapped
if status not in 'In Location': operand = 'not in'
operand = 'in' if status not in 'In Location':
operand = 'in'
conditions['status'] = (operand, ['Sold', 'Scrapped']) conditions['status'] = (operand, ['Sold', 'Scrapped'])
return conditions return conditions

View File

@@ -0,0 +1,42 @@
# 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))

View File

@@ -820,6 +820,38 @@ class AccountsController(TransactionBase):
if frappe.db.get_single_value('Accounts Settings', 'unlink_advance_payment_on_cancelation_of_order'): if frappe.db.get_single_value('Accounts Settings', 'unlink_advance_payment_on_cancelation_of_order'):
unlink_ref_doc_from_payment_entries(self) 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): def get_tax_map(self):
tax_map = {} tax_map = {}
for tax in self.get('taxes'): for tax in self.get('taxes'):
@@ -1037,15 +1069,15 @@ class AccountsController(TransactionBase):
if role_allowed_to_over_bill in user_roles and total_overbilled_amt > 0.1: if role_allowed_to_over_bill in user_roles and total_overbilled_amt > 0.1:
frappe.msgprint(_("Overbilling of {} ignored because you have {} role.") frappe.msgprint(_("Overbilling of {} ignored because you have {} role.")
.format(total_overbilled_amt, role_allowed_to_over_bill), title=_("Warning"), indicator="orange") .format(total_overbilled_amt, role_allowed_to_over_bill), indicator="orange", alert=True)
def throw_overbill_exception(self, item, max_allowed_amt): 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") 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)) .format(item.item_code, item.idx, max_allowed_amt))
def get_company_default(self, fieldname): def get_company_default(self, fieldname, ignore_validation=False):
from erpnext.accounts.utils import get_company_default from erpnext.accounts.utils import get_company_default
return get_company_default(self.company, fieldname) return get_company_default(self.company, fieldname, ignore_validation=ignore_validation)
def get_stock_items(self): def get_stock_items(self):
stock_items = [] stock_items = []
@@ -1359,8 +1391,8 @@ class AccountsController(TransactionBase):
total = 0 total = 0
base_total = 0 base_total = 0
for d in self.get("payment_schedule"): for d in self.get("payment_schedule"):
total += flt(d.payment_amount) total += flt(d.payment_amount, d.precision("payment_amount"))
base_total += flt(d.base_payment_amount) base_total += flt(d.base_payment_amount, d.precision("base_payment_amount"))
base_grand_total = self.get("base_rounded_total") or self.base_grand_total base_grand_total = self.get("base_rounded_total") or self.base_grand_total
grand_total = self.get("rounded_total") or self.grand_total grand_total = self.get("rounded_total") or self.grand_total
@@ -1376,8 +1408,9 @@ class AccountsController(TransactionBase):
else: else:
grand_total -= self.get("total_advance") grand_total -= self.get("total_advance")
base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total")) base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total"))
if total != flt(grand_total, self.precision("grand_total")) or \
base_total != flt(base_grand_total, self.precision("base_grand_total")): if flt(total, self.precision("grand_total")) != flt(grand_total, self.precision("grand_total")) or \
flt(base_total, self.precision("base_grand_total")) != flt(base_grand_total, self.precision("base_grand_total")):
frappe.throw(_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total")) frappe.throw(_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total"))
def is_rounded_total_disabled(self): def is_rounded_total_disabled(self):

View File

@@ -132,7 +132,8 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql("""select {field} from `tabSupplier` return frappe.db.sql("""select {field} from `tabSupplier`
where docstatus < 2 where docstatus < 2
and ({key} like %(txt)s and ({key} like %(txt)s
or supplier_name like %(txt)s) and disabled=0 or supplier_name like %(txt)s) and disabled=0
and (on_hold = 0 or (on_hold = 1 and CURDATE() > release_date))
{mcond} {mcond}
order by order by
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
@@ -210,12 +211,15 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
meta = frappe.get_meta("Item", cached=True) meta = frappe.get_meta("Item", cached=True)
searchfields = meta.get_search_fields() searchfields = meta.get_search_fields()
if "description" in searchfields: # these are handled separately
searchfields.remove("description") ignored_search_fields = ("item_name", "description")
for ignored_field in ignored_search_fields:
if ignored_field in searchfields:
searchfields.remove(ignored_field)
columns = '' columns = ''
extra_searchfields = [field for field in searchfields extra_searchfields = [field for field in searchfields
if not field in ["name", "item_group", "description"]] if not field in ["name", "item_group", "description", "item_name"]]
if extra_searchfields: if extra_searchfields:
columns = ", " + ", ".join(extra_searchfields) columns = ", " + ", ".join(extra_searchfields)
@@ -252,10 +256,8 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
if frappe.db.count('Item', cache=True) < 50000: if frappe.db.count('Item', cache=True) < 50000:
# scan description only if items are less than 50000 # scan description only if items are less than 50000
description_cond = 'or tabItem.description LIKE %(txt)s' description_cond = 'or tabItem.description LIKE %(txt)s'
return frappe.db.sql("""select tabItem.name, return frappe.db.sql("""select
if(length(tabItem.item_name) > 40, tabItem.name, tabItem.item_name, tabItem.item_group,
concat(substr(tabItem.item_name, 1, 40), "..."), item_name) as item_name,
tabItem.item_group,
if(length(tabItem.description) > 40, \ if(length(tabItem.description) > 40, \
concat(substr(tabItem.description, 1, 40), "..."), description) as description concat(substr(tabItem.description, 1, 40), "..."), description) as description
{columns} {columns}
@@ -565,7 +567,7 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters)
query_filters.append(['name', query_selector, dimensions]) query_filters.append(['name', query_selector, dimensions])
output = frappe.get_all(doctype, filters=query_filters) output = frappe.get_list(doctype, filters=query_filters)
result = [d.name for d in output] result = [d.name for d in output]
return [(d,) for d in set(result)] return [(d,) for d in set(result)]

View File

@@ -216,11 +216,14 @@ class StatusUpdater(Document):
overflow_percent = ((item[args['target_field']] - item[args['target_ref_field']]) / overflow_percent = ((item[args['target_field']] - item[args['target_ref_field']]) /
item[args['target_ref_field']]) * 100 item[args['target_ref_field']]) * 100
if overflow_percent - allowance > 0.01 and role not in frappe.get_roles(): if overflow_percent - allowance > 0.01:
item['max_allowed'] = flt(item[args['target_ref_field']] * (100+allowance)/100) item['max_allowed'] = flt(item[args['target_ref_field']] * (100+allowance)/100)
item['reduce_by'] = item[args['target_field']] - item['max_allowed'] item['reduce_by'] = item[args['target_field']] - item['max_allowed']
self.limits_crossed_error(args, item, qty_or_amount) if role not in frappe.get_roles():
self.limits_crossed_error(args, item, qty_or_amount)
else:
self.warn_about_bypassing_with_role(item, qty_or_amount, role)
def limits_crossed_error(self, args, item, qty_or_amount): def limits_crossed_error(self, args, item, qty_or_amount):
'''Raise exception for limits crossed''' '''Raise exception for limits crossed'''
@@ -238,6 +241,19 @@ class StatusUpdater(Document):
frappe.bold(item.get('item_code')) frappe.bold(item.get('item_code'))
) + '<br><br>' + action_msg, OverAllowanceError, title = _('Limit Crossed')) ) + '<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): def update_qty(self, update_modified=True):
"""Updates qty or amount at row level """Updates qty or amount at row level

View File

@@ -260,7 +260,9 @@ class calculate_taxes_and_totals(object):
self.doc.round_floats_in(self.doc, ["total", "base_total", "net_total", "base_net_total"]) self.doc.round_floats_in(self.doc, ["total", "base_total", "net_total", "base_net_total"])
def calculate_taxes(self): def calculate_taxes(self):
self.doc.rounding_adjustment = 0 if not self.doc.get('is_consolidated'):
self.doc.rounding_adjustment = 0
# maintain actual tax rate based on idx # maintain actual tax rate based on idx
actual_tax_dict = dict([[tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))] 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"]) for tax in self.doc.get("taxes") if tax.charge_type == "Actual"])
@@ -312,7 +314,9 @@ class calculate_taxes_and_totals(object):
# adjust Discount Amount loss in last tax iteration # adjust Discount Amount loss in last tax iteration
if i == (len(self.doc.get("taxes")) - 1) and self.discount_amount_applied \ if i == (len(self.doc.get("taxes")) - 1) and self.discount_amount_applied \
and self.doc.discount_amount and self.doc.apply_discount_on == "Grand Total": and self.doc.discount_amount \
and self.doc.apply_discount_on == "Grand Total" \
and not self.doc.get('is_consolidated'):
self.doc.rounding_adjustment = flt(self.doc.grand_total self.doc.rounding_adjustment = flt(self.doc.grand_total
- flt(self.doc.discount_amount) - tax.total, - flt(self.doc.discount_amount) - tax.total,
self.doc.precision("rounding_adjustment")) self.doc.precision("rounding_adjustment"))
@@ -405,11 +409,16 @@ class calculate_taxes_and_totals(object):
self.doc.rounding_adjustment = diff self.doc.rounding_adjustment = diff
def calculate_totals(self): def calculate_totals(self):
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt(self.doc.rounding_adjustment) \ if self.doc.get("taxes"):
if self.doc.get("taxes") else flt(self.doc.net_total) self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt(self.doc.rounding_adjustment)
else:
self.doc.grand_total = flt(self.doc.net_total)
self.doc.total_taxes_and_charges = flt(self.doc.grand_total - self.doc.net_total if self.doc.get("taxes"):
self.doc.total_taxes_and_charges = flt(self.doc.grand_total - self.doc.net_total
- flt(self.doc.rounding_adjustment), self.doc.precision("total_taxes_and_charges")) - 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"]) self._set_in_company_currency(self.doc, ["total_taxes_and_charges", "rounding_adjustment"])
@@ -446,19 +455,20 @@ class calculate_taxes_and_totals(object):
self.doc.total_net_weight += d.total_weight self.doc.total_net_weight += d.total_weight
def set_rounded_total(self): def set_rounded_total(self):
if self.doc.meta.get_field("rounded_total"): if not self.doc.get('is_consolidated'):
if self.doc.is_rounded_total_disabled(): if self.doc.meta.get_field("rounded_total"):
self.doc.rounded_total = self.doc.base_rounded_total = 0 if self.doc.is_rounded_total_disabled():
return 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.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total,
self.doc.currency, self.doc.precision("rounded_total")) self.doc.currency, self.doc.precision("rounded_total"))
#if print_in_rate is set, we would have already calculated 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.rounding_adjustment += flt(self.doc.rounded_total - self.doc.grand_total,
self.doc.precision("rounding_adjustment")) 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): def _cleanup(self):
if not self.doc.get('is_consolidated'): if not self.doc.get('is_consolidated'):

View File

@@ -305,6 +305,8 @@ def make_request_for_quotation(source_name, target_doc=None):
@frappe.whitelist() @frappe.whitelist()
def make_customer(source_name, target_doc=None): def make_customer(source_name, target_doc=None):
def set_missing_values(source, target): def set_missing_values(source, target):
target.opportunity_name = source.name
if source.opportunity_from == "Lead": if source.opportunity_from == "Lead":
target.lead_name = source.party_name target.lead_name = source.party_name

View File

@@ -1,7 +1,6 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import frappe import frappe
from frappe import _dict
from frappe.utils import floor from frappe.utils import floor
@@ -96,38 +95,32 @@ class ProductFiltersBuilder:
return return
attributes = [row.attribute for row in self.doc.filter_attributes] attributes = [row.attribute for row in self.doc.filter_attributes]
attribute_docs = [
frappe.get_doc('Item Attribute', attribute) for attribute in attributes
]
valid_attributes = [] if not attributes:
return []
for attr_doc in attribute_docs: result = frappe.db.sql(
selected_attributes = [] """
for attr in attr_doc.item_attribute_values: select
or_filters = [] distinct attribute, attribute_value
filters= [ from
["Item Variant Attribute", "attribute", "=", attr.parent], `tabItem Variant Attribute`
["Item Variant Attribute", "attribute_value", "=", attr.attribute_value] where
] attribute in %(attributes)s
if self.item_group: and attribute_value is not null
or_filters.extend([ """,
["item_group", "=", self.item_group], {"attributes": attributes},
["Website Item Group", "item_group", "=", self.item_group] as_dict=1,
]) )
if frappe.db.get_all("Item", filters, or_filters=or_filters, limit=1): attribute_value_map = {}
selected_attributes.append(attr) for d in result:
attribute_value_map.setdefault(d.attribute, []).append(d.attribute_value)
if selected_attributes: out = []
valid_attributes.append( for name, values in attribute_value_map.items():
_dict( out.append(frappe._dict(name=name, item_attribute_values=values))
item_attribute_values=selected_attributes, return out
name=attr_doc.name
)
)
return valid_attributes
def get_discount_filters(self, discounts): def get_discount_filters(self, discounts):
discount_filters = [] discount_filters = []

View File

@@ -175,9 +175,7 @@ class TestProductDataEngine(unittest.TestCase):
filter_engine = ProductFiltersBuilder() filter_engine = ProductFiltersBuilder()
attribute_filter = filter_engine.get_attribute_filters()[0] attribute_filter = filter_engine.get_attribute_filters()[0]
attributes = attribute_filter.item_attribute_values attribute_values = attribute_filter.item_attribute_values
attribute_values = [d.attribute_value for d in attributes]
self.assertEqual(attribute_filter.name, "Test Size") self.assertEqual(attribute_filter.name, "Test Size")
self.assertGreater(len(attribute_values), 0) self.assertGreater(len(attribute_values), 0)

View File

@@ -1,8 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe import frappe
import frappe.defaults import frappe.defaults
@@ -17,10 +14,19 @@ def show_cart_count():
return False return False
def set_cart_count(login_manager): def set_cart_count(login_manager):
role, parties = check_customer_or_supplier() # since this is run only on hooks login event
if role == 'Supplier': return # make sure user is already a customer
# before trying to set cart count
user_is_customer = is_customer()
if not user_is_customer:
return
if show_cart_count(): if show_cart_count():
from erpnext.e_commerce.shopping_cart.cart import set_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() set_cart_count()
def clear_cart_count(login_manager): def clear_cart_count(login_manager):
@@ -31,13 +37,13 @@ def update_website_context(context):
cart_enabled = is_cart_enabled() cart_enabled = is_cart_enabled()
context["shopping_cart_enabled"] = cart_enabled context["shopping_cart_enabled"] = cart_enabled
def check_customer_or_supplier(): def is_customer():
if frappe.session.user: if frappe.session.user and frappe.session.user != "Guest":
contact_name = frappe.get_value("Contact", {"email_id": frappe.session.user}) contact_name = frappe.get_value("Contact", {"email_id": frappe.session.user})
if contact_name: if contact_name:
contact = frappe.get_doc('Contact', contact_name) contact = frappe.get_doc('Contact', contact_name)
for link in contact.links: for link in contact.links:
if link.link_doctype in ('Customer', 'Supplier'): if link.link_doctype == 'Customer':
return link.link_doctype, link.link_name return True
return 'Customer', None return False

View File

@@ -68,5 +68,8 @@ def dump_request_data(data, event="create/order"):
@frappe.whitelist() @frappe.whitelist()
def resync(method, name, request_data): def resync(method, name, request_data):
frappe.db.set_value("Shopify Log", name, "status", "Queued", update_modified=False) 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, frappe.enqueue(method=method, queue='short', timeout=300, is_async=True,
**{"order": json.loads(request_data), "request_id": name}) **{"order": json.loads(request_data), "request_id": name})

View File

@@ -0,0 +1,51 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2021-09-11 05:09:53.773838",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"region",
"region_code",
"country",
"country_code"
],
"fields": [
{
"fieldname": "region",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Region"
},
{
"fieldname": "region_code",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Region Code"
},
{
"fieldname": "country",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Country"
},
{
"fieldname": "country_code",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Country Code"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-09-14 05:33:06.444710",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "TaxJar Nexus",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class TaxJarNexus(Document):
pass

View File

@@ -5,5 +5,33 @@ frappe.ui.form.on('TaxJar Settings', {
is_sandbox: (frm) => { is_sandbox: (frm) => {
frm.toggle_reqd("api_key", !frm.doc.is_sandbox); frm.toggle_reqd("api_key", !frm.doc.is_sandbox);
frm.toggle_reqd("sandbox_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'
});
});
},
}); });

View File

@@ -6,17 +6,22 @@
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"is_sandbox",
"taxjar_calculate_tax", "taxjar_calculate_tax",
"is_sandbox",
"taxjar_create_transactions", "taxjar_create_transactions",
"credentials", "credentials",
"api_key", "api_key",
"cb_keys", "cb_keys",
"sandbox_api_key", "sandbox_api_key",
"configuration", "configuration",
"company",
"column_break_10",
"tax_account_head", "tax_account_head",
"configuration_cb", "configuration_cb",
"shipping_account_head" "shipping_account_head",
"section_break_12",
"nexus_address",
"nexus"
], ],
"fields": [ "fields": [
{ {
@@ -54,6 +59,7 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "taxjar_calculate_tax",
"fieldname": "is_sandbox", "fieldname": "is_sandbox",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Sandbox Mode" "label": "Sandbox Mode"
@@ -63,12 +69,9 @@
"fieldtype": "Password", "fieldtype": "Password",
"label": "Sandbox API Key" "label": "Sandbox API Key"
}, },
{
"fieldname": "configuration_cb",
"fieldtype": "Column Break"
},
{ {
"default": "0", "default": "0",
"depends_on": "taxjar_calculate_tax",
"fieldname": "taxjar_create_transactions", "fieldname": "taxjar_create_transactions",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Create TaxJar Transaction" "label": "Create TaxJar Transaction"
@@ -82,11 +85,42 @@
{ {
"fieldname": "cb_keys", "fieldname": "cb_keys",
"fieldtype": "Column Break" "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, "issingle": 1,
"links": [], "links": [],
"modified": "2020-04-30 04:38:03.311089", "modified": "2021-11-08 18:02:29.232090",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "ERPNext Integrations", "module": "ERPNext Integrations",
"name": "TaxJar Settings", "name": "TaxJar Settings",

View File

@@ -4,9 +4,98 @@
from __future__ import unicode_literals from __future__ import unicode_literals
# import frappe import json
import os
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.model.document import Document from frappe.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): class TaxJarSettings(Document):
pass
def on_update(self):
TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
TAXJAR_SANDBOX_MODE = frappe.db.get_single_value("TaxJar Settings", "is_sandbox")
fields_already_exist = frappe.db.exists('Custom Field', {'dt': ('in', ['Item','Sales Invoice Item']), 'fieldname':'product_tax_category'})
fields_hidden = frappe.get_value('Custom Field', {'dt': ('in', ['Sales Invoice Item'])}, 'hidden')
if (TAXJAR_CREATE_TRANSACTIONS or TAXJAR_CALCULATE_TAX or TAXJAR_SANDBOX_MODE):
if not fields_already_exist:
add_product_tax_categories()
make_custom_fields()
add_permissions()
frappe.enqueue('erpnext.regional.united_states.setup.add_product_tax_categories', now=False)
elif fields_already_exist and fields_hidden:
toggle_tax_category_fields(hidden='0')
elif fields_already_exist:
toggle_tax_category_fields(hidden='1')
def validate(self):
self.calculate_taxes_validation_for_create_transactions()
@frappe.whitelist()
def update_nexus_list(self):
client = get_client()
nexus = client.nexus_regions()
new_nexus_list = [frappe._dict(address) for address in nexus]
self.set('nexus', [])
self.set('nexus', new_nexus_list)
self.save()
def calculate_taxes_validation_for_create_transactions(self):
if not self.taxjar_calculate_tax and (self.taxjar_create_transactions or self.is_sandbox):
frappe.throw(frappe._('Before enabling <b>Create Transaction</b> or <b>Sandbox Mode</b>, you need to check the <b>Enable Tax Calculation</b> box'))
def toggle_tax_category_fields(hidden):
frappe.set_value('Custom Field', {'dt':'Sales Invoice Item', 'fieldname':'product_tax_category'}, 'hidden', hidden)
frappe.set_value('Custom Field', {'dt':'Item', 'fieldname':'product_tax_category'}, 'hidden', hidden)
def add_product_tax_categories():
with open(os.path.join(os.path.dirname(__file__), 'product_tax_category_data.json'), 'r') as f:
tax_categories = json.loads(f.read())
create_tax_categories(tax_categories['categories'])
def create_tax_categories(data):
for d in data:
if not frappe.db.exists('Product Tax Category',{'product_tax_code':d.get('product_tax_code')}):
tax_category = frappe.new_doc('Product Tax Category')
tax_category.description = d.get("description")
tax_category.product_tax_code = d.get("product_tax_code")
tax_category.category_name = d.get("name")
tax_category.db_insert()
def make_custom_fields(update=True):
custom_fields = {
'Sales Invoice Item': [
dict(fieldname='product_tax_category', fieldtype='Link', insert_after='description', options='Product Tax Category',
label='Product Tax Category', fetch_from='item_code.product_tax_category'),
dict(fieldname='tax_collectable', fieldtype='Currency', insert_after='net_amount',
label='Tax Collectable', read_only=1, 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)

View File

@@ -4,9 +4,9 @@ import frappe
import taxjar import taxjar
from frappe import _ from frappe import _
from frappe.contacts.doctype.address.address import get_company_address from frappe.contacts.doctype.address.address import get_company_address
from frappe.utils import cint from frappe.utils import cint, flt
from erpnext import get_default_company from erpnext import get_default_company, get_region
TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head") 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") SHIP_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "shipping_account_head")
@@ -21,6 +21,7 @@ SUPPORTED_STATE_CODES = ['AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'DC', '
'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY'] 'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY']
def get_client(): def get_client():
taxjar_settings = frappe.get_single("TaxJar Settings") taxjar_settings = frappe.get_single("TaxJar Settings")
@@ -103,7 +104,7 @@ def get_tax_data(doc):
shipping = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == SHIP_ACCOUNT_HEAD]) shipping = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == SHIP_ACCOUNT_HEAD])
line_items = [get_line_item_dict(item) for item in doc.items] line_items = [get_line_item_dict(item, doc.docstatus) for item in doc.items]
if from_shipping_state not in SUPPORTED_STATE_CODES: if from_shipping_state not in SUPPORTED_STATE_CODES:
from_shipping_state = get_state_code(from_address, 'Company') from_shipping_state = get_state_code(from_address, 'Company')
@@ -139,18 +140,28 @@ def get_state_code(address, location):
return state_code return state_code
def get_line_item_dict(item): def get_line_item_dict(item, docstatus):
return dict( tax_dict = dict(
id = item.get('idx'), id = item.get('idx'),
quantity = item.get('qty'), quantity = item.get('qty'),
unit_price = item.get('rate'), unit_price = item.get('rate'),
product_tax_code = item.get('product_tax_category') 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): def set_sales_tax(doc, method):
if not TAXJAR_CALCULATE_TAX: if not TAXJAR_CALCULATE_TAX:
return return
if get_region(doc.company) != 'United States':
return
if not doc.items: if not doc.items:
return return
@@ -164,6 +175,9 @@ def set_sales_tax(doc, method):
setattr(doc, "taxes", [tax for tax in doc.taxes if tax.account_head != TAX_ACCOUNT_HEAD]) setattr(doc, "taxes", [tax for tax in doc.taxes if tax.account_head != TAX_ACCOUNT_HEAD])
return return
# check if delivering within a nexus
check_for_nexus(doc, tax_dict)
tax_data = validate_tax_request(tax_dict) tax_data = validate_tax_request(tax_dict)
if tax_data is not None: if tax_data is not None:
if not tax_data.amount_to_collect: if not tax_data.amount_to_collect:
@@ -191,6 +205,17 @@ def set_sales_tax(doc, method):
doc.run_method("calculate_taxes_and_totals") 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): def check_sales_tax_exemption(doc):
# if the party is exempt from sales tax, then set all tax account heads to zero # 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 \ sales_tax_exempted = hasattr(doc, "exempt_from_sales_tax") and doc.exempt_from_sales_tax \
@@ -241,7 +266,7 @@ def get_shipping_address_details(doc):
if doc.shipping_address_name: if doc.shipping_address_name:
shipping_address = frappe.get_doc("Address", doc.shipping_address_name) shipping_address = frappe.get_doc("Address", doc.shipping_address_name)
elif doc.customer_address: elif doc.customer_address:
shipping_address = frappe.get_doc("Address", doc.customer_address_name) shipping_address = frappe.get_doc("Address", doc.customer_address)
else: else:
shipping_address = get_company_address_details(doc) shipping_address = get_company_address_details(doc)

View File

@@ -257,6 +257,7 @@ doc_events = {
"validate": "erpnext.regional.india.utils.validate_tax_category" "validate": "erpnext.regional.india.utils.validate_tax_category"
}, },
"Sales Invoice": { "Sales Invoice": {
"after_insert": "erpnext.regional.saudi_arabia.utils.create_qr_code",
"on_submit": [ "on_submit": [
"erpnext.regional.create_transaction_log", "erpnext.regional.create_transaction_log",
"erpnext.regional.italy.utils.sales_invoice_on_submit", "erpnext.regional.italy.utils.sales_invoice_on_submit",
@@ -266,7 +267,10 @@ doc_events = {
"erpnext.regional.italy.utils.sales_invoice_on_cancel", "erpnext.regional.italy.utils.sales_invoice_on_cancel",
"erpnext.erpnext_integrations.taxjar_integration.delete_transaction" "erpnext.erpnext_integrations.taxjar_integration.delete_transaction"
], ],
"on_trash": "erpnext.regional.check_deletion_permission", "on_trash": [
"erpnext.regional.check_deletion_permission",
"erpnext.regional.saudi_arabia.utils.delete_qr_code_file"
],
"validate": [ "validate": [
"erpnext.regional.india.utils.validate_document_name", "erpnext.regional.india.utils.validate_document_name",
"erpnext.regional.india.utils.update_taxable_values" "erpnext.regional.india.utils.update_taxable_values"

View File

@@ -156,6 +156,8 @@ def get_employees_having_an_event_today(event_type):
DAY({condition_column}) = DAY(%(today)s) DAY({condition_column}) = DAY(%(today)s)
AND AND
MONTH({condition_column}) = MONTH(%(today)s) MONTH({condition_column}) = MONTH(%(today)s)
AND
YEAR({condition_column}) < YEAR(%(today)s)
AND AND
`status` = 'Active' `status` = 'Active'
""", """,
@@ -166,6 +168,8 @@ def get_employees_having_an_event_today(event_type):
DATE_PART('day', {condition_column}) = date_part('day', %(today)s) DATE_PART('day', {condition_column}) = date_part('day', %(today)s)
AND AND
DATE_PART('month', {condition_column}) = date_part('month', %(today)s) DATE_PART('month', {condition_column}) = date_part('month', %(today)s)
AND
DATE_PART('year', {condition_column}) < date_part('year', %(today)s)
AND AND
"status" = 'Active' "status" = 'Active'
""", """,

View File

@@ -55,6 +55,7 @@ def make_employee(user, company=None, **kwargs):
"email": user, "email": user,
"first_name": user, "first_name": user,
"new_password": "password", "new_password": "password",
"send_welcome_email": 0,
"roles": [{"doctype": "Has Role", "role": "Employee"}] "roles": [{"doctype": "Has Role", "role": "Employee"}]
}).insert() }).insert()

View File

@@ -9,7 +9,7 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import getdate from frappe.utils import getdate
from erpnext.hr.utils import update_employee, validate_active_employee from erpnext.hr.utils import update_employee_work_history, validate_active_employee
class EmployeePromotion(Document): class EmployeePromotion(Document):
@@ -23,10 +23,10 @@ class EmployeePromotion(Document):
def on_submit(self): def on_submit(self):
employee = frappe.get_doc("Employee", self.employee) employee = frappe.get_doc("Employee", self.employee)
employee = update_employee(employee, self.promotion_details, date=self.promotion_date) employee = update_employee_work_history(employee, self.promotion_details, date=self.promotion_date)
employee.save() employee.save()
def on_cancel(self): def on_cancel(self):
employee = frappe.get_doc("Employee", self.employee) employee = frappe.get_doc("Employee", self.employee)
employee = update_employee(employee, self.promotion_details, cancel=True) employee = update_employee_work_history(employee, self.promotion_details, cancel=True)
employee.save() employee.save()

View File

@@ -9,7 +9,7 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import getdate from frappe.utils import getdate
from erpnext.hr.utils import update_employee from erpnext.hr.utils import update_employee_work_history
class EmployeeTransfer(Document): class EmployeeTransfer(Document):
@@ -24,7 +24,7 @@ class EmployeeTransfer(Document):
new_employee = frappe.copy_doc(employee) new_employee = frappe.copy_doc(employee)
new_employee.name = None new_employee.name = None
new_employee.employee_number = None new_employee.employee_number = None
new_employee = update_employee(new_employee, self.transfer_details, date=self.transfer_date) new_employee = update_employee_work_history(new_employee, self.transfer_details, date=self.transfer_date)
if self.new_company and self.company != self.new_company: if self.new_company and self.company != self.new_company:
new_employee.internal_work_history = [] new_employee.internal_work_history = []
new_employee.date_of_joining = self.transfer_date 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("relieving_date", self.transfer_date)
employee.db_set("status", "Left") employee.db_set("status", "Left")
else: else:
employee = update_employee(employee, self.transfer_details, date=self.transfer_date) employee = update_employee_work_history(employee, self.transfer_details, date=self.transfer_date)
if self.new_company and self.company != self.new_company: if self.new_company and self.company != self.new_company:
employee.company = self.new_company employee.company = self.new_company
employee.date_of_joining = self.transfer_date employee.date_of_joining = self.transfer_date
@@ -56,7 +56,7 @@ class EmployeeTransfer(Document):
employee.status = "Active" employee.status = "Active"
employee.relieving_date = '' employee.relieving_date = ''
else: else:
employee = update_employee(employee, self.transfer_details, cancel=True) employee = update_employee_work_history(employee, self.transfer_details, date=self.transfer_date, cancel=True)
if self.new_company != self.company: if self.new_company != self.company:
employee.company = self.company employee.company = self.company
employee.save() employee.save()

View File

@@ -4,6 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest import unittest
from datetime import date
import frappe import frappe
from frappe.utils import add_days, getdate from frappe.utils import add_days, getdate
@@ -15,7 +16,12 @@ class TestEmployeeTransfer(unittest.TestCase):
def setUp(self): def setUp(self):
make_employee("employee2@transfers.com") make_employee("employee2@transfers.com")
make_employee("employee3@transfers.com") make_employee("employee3@transfers.com")
frappe.db.sql("""delete from `tabEmployee Transfer`""") create_company()
create_employee()
create_employee_transfer()
def tearDown(self):
frappe.db.rollback()
def test_submit_before_transfer_date(self): def test_submit_before_transfer_date(self):
transfer_obj = frappe.get_doc({ transfer_obj = frappe.get_doc({
@@ -57,3 +63,77 @@ class TestEmployeeTransfer(unittest.TestCase):
self.assertTrue(transfer.new_employee_id) 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.new_employee_id, "status"), "Active")
self.assertEqual(frappe.get_value("Employee", transfer.employee, "status"), "Left") 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()

View File

@@ -100,7 +100,7 @@
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-09-23 20:27:36.027728", "modified": "2021-10-26 20:27:36.027728",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Expense Taxes and Charges", "name": "Expense Taxes and Charges",

View File

@@ -145,7 +145,15 @@ def set_employee_name(doc):
if doc.employee and not doc.employee_name: if doc.employee and not doc.employee_name:
doc.employee_name = frappe.db.get_value("Employee", doc.employee, "employee_name") doc.employee_name = frappe.db.get_value("Employee", doc.employee, "employee_name")
def update_employee(employee, details, date=None, cancel=False): def update_employee_work_history(employee, details, date=None, cancel=False):
if not employee.internal_work_history and not cancel:
employee.append("internal_work_history", {
"branch": employee.branch,
"designation": employee.designation,
"department": employee.department,
"from_date": employee.date_of_joining
})
internal_work_history = {} internal_work_history = {}
for item in details: for item in details:
field = frappe.get_meta("Employee").get_field(item.fieldname) field = frappe.get_meta("Employee").get_field(item.fieldname)
@@ -160,11 +168,35 @@ def update_employee(employee, details, date=None, cancel=False):
setattr(employee, item.fieldname, new_data) setattr(employee, item.fieldname, new_data)
if item.fieldname in ["department", "designation", "branch"]: if item.fieldname in ["department", "designation", "branch"]:
internal_work_history[item.fieldname] = item.new internal_work_history[item.fieldname] = item.new
if internal_work_history and not cancel: if internal_work_history and not cancel:
internal_work_history["from_date"] = date internal_work_history["from_date"] = date
employee.append("internal_work_history", internal_work_history) employee.append("internal_work_history", internal_work_history)
if cancel:
delete_employee_work_history(details, employee, date)
return employee 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() @frappe.whitelist()
def get_employee_fields_label(): def get_employee_fields_label():
fields = [] fields = []

View File

@@ -436,7 +436,7 @@
"description": "Item Image (if not slideshow)", "description": "Item Image (if not slideshow)",
"fieldname": "website_image", "fieldname": "website_image",
"fieldtype": "Attach Image", "fieldtype": "Attach Image",
"label": "Image" "label": "Website Image"
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
@@ -539,7 +539,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-05-16 12:25:09.081968", "modified": "2021-10-27 14:52:04.500251",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM", "name": "BOM",

View File

@@ -308,6 +308,9 @@ class BOM(WebsiteGenerator):
existing_bom_cost = self.total_cost existing_bom_cost = self.total_cost
for d in self.get("items"): for d in self.get("items"):
if not d.item_code:
continue
rate = self.get_rm_rate({ rate = self.get_rm_rate({
"company": self.company, "company": self.company,
"item_code": d.item_code, "item_code": d.item_code,
@@ -600,7 +603,7 @@ class BOM(WebsiteGenerator):
for d in self.get('items'): for d in self.get('items'):
if d.bom_no: if d.bom_no:
self.get_child_exploded_items(d.bom_no, d.stock_qty) self.get_child_exploded_items(d.bom_no, d.stock_qty)
else: elif d.item_code:
self.add_to_cur_exploded_items(frappe._dict({ self.add_to_cur_exploded_items(frappe._dict({
'item_code' : d.item_code, 'item_code' : d.item_code,
'item_name' : d.item_name, 'item_name' : d.item_name,

View File

@@ -28,6 +28,11 @@ frappe.ui.form.on('Job Card', {
frappe.flags.resume_job = 0; frappe.flags.resume_job = 0;
let has_items = frm.doc.items && frm.doc.items.length; 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) { if (!frm.doc.__islocal && has_items && frm.doc.docstatus < 2) {
let to_request = frm.doc.for_quantity > frm.doc.transferred_qty; let to_request = frm.doc.for_quantity > frm.doc.transferred_qty;
let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer; let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
@@ -70,6 +75,23 @@ frappe.ui.form.on('Job Card', {
&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { && (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
frm.trigger("prepare_timer_buttons"); 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) { setup_corrective_job_card: function(frm) {

View File

@@ -19,6 +19,7 @@
"serial_no", "serial_no",
"column_break_12", "column_break_12",
"wip_warehouse", "wip_warehouse",
"quality_inspection_template",
"quality_inspection", "quality_inspection",
"project", "project",
"batch_no", "batch_no",
@@ -407,11 +408,18 @@
"no_copy": 1, "no_copy": 1,
"options": "Job Card Scrap Item", "options": "Job Card Scrap Item",
"print_hide": 1 "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, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-09-14 00:38:46.873105", "modified": "2021-11-09 14:07:20.290306",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Job Card", "name": "Job Card",

View File

@@ -37,6 +37,7 @@ class JobCard(Document):
def onload(self): def onload(self):
excess_transfer = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer") 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("job_card_excess_transfer", excess_transfer)
self.set_onload("work_order_stopped", self.is_work_order_stopped())
def validate(self): def validate(self):
self.validate_time_logs() self.validate_time_logs()
@@ -45,6 +46,7 @@ class JobCard(Document):
self.validate_sequence_id() self.validate_sequence_id()
self.set_sub_operations() self.set_sub_operations()
self.update_sub_operation_status() self.update_sub_operation_status()
self.validate_work_order()
def set_sub_operations(self): def set_sub_operations(self):
if self.operation: if self.operation:
@@ -549,6 +551,18 @@ class JobCard(Document):
frappe.throw(_("{0}, complete the operation {1} before the operation {2}.") frappe.throw(_("{0}, complete the operation {1} before the operation {2}.")
.format(message, bold(row.operation), bold(self.operation)), OperationSequenceError) .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() @frappe.whitelist()
def make_time_log(args): def make_time_log(args):

View File

@@ -13,6 +13,7 @@
"is_corrective_operation", "is_corrective_operation",
"job_card_section", "job_card_section",
"create_job_card_based_on_batch_size", "create_job_card_based_on_batch_size",
"quality_inspection_template",
"column_break_6", "column_break_6",
"batch_size", "batch_size",
"sub_operations_section", "sub_operations_section",
@@ -92,12 +93,18 @@
"fieldname": "is_corrective_operation", "fieldname": "is_corrective_operation",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Corrective Operation" "label": "Is Corrective Operation"
},
{
"fieldname": "quality_inspection_template",
"fieldtype": "Link",
"label": "Quality Inspection Template",
"options": "Quality Inspection Template"
} }
], ],
"icon": "fa fa-wrench", "icon": "fa fa-wrench",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-01-12 15:09:23.593338", "modified": "2021-11-03 13:49:39.114976",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Operation", "name": "Operation",

View File

@@ -311,7 +311,7 @@ class ProductionPlan(Document):
if self.total_produced_qty > 0: if self.total_produced_qty > 0:
self.status = "In Process" self.status = "In Process"
if self.total_produced_qty >= self.total_planned_qty: if self.check_have_work_orders_completed():
self.status = "Completed" self.status = "Completed"
if self.status != 'Completed': if self.status != 'Completed':
@@ -424,7 +424,7 @@ class ProductionPlan(Document):
po = frappe.new_doc('Purchase Order') po = frappe.new_doc('Purchase Order')
po.supplier = supplier po.supplier = supplier
po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate() po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate()
po.is_subcontracted_item = 'Yes' po.is_subcontracted = 'Yes'
for row in po_list: for row in po_list:
args = { args = {
'item_code': row.production_item, 'item_code': row.production_item,
@@ -575,6 +575,15 @@ class ProductionPlan(Document):
self.append("sub_assembly_items", data) 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() @frappe.whitelist()
def download_raw_materials(doc, warehouses=None): def download_raw_materials(doc, warehouses=None):
if isinstance(doc, str): if isinstance(doc, str):

View File

@@ -12,6 +12,7 @@ from erpnext.manufacturing.doctype.work_order.work_order import (
ItemHasVariantError, ItemHasVariantError,
OverProductionError, OverProductionError,
StockOverProductionError, StockOverProductionError,
close_work_order,
make_stock_entry, make_stock_entry,
stop_unstop, stop_unstop,
) )
@@ -800,6 +801,46 @@ class TestWorkOrder(unittest.TestCase):
if row.is_scrap_item: if row.is_scrap_item:
self.assertEqual(row.qty, 1) 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): def update_job_card(job_card):
job_card_doc = frappe.get_doc('Job Card', job_card) job_card_doc = frappe.get_doc('Job Card', job_card)
job_card_doc.set('scrap_items', [ job_card_doc.set('scrap_items', [

View File

@@ -135,24 +135,26 @@ frappe.ui.form.on("Work Order", {
frm.set_intro(__("Submit this Work Order for further processing.")); frm.set_intro(__("Submit this Work Order for further processing."));
} }
if (frm.doc.docstatus===1) { if (frm.doc.status != "Closed") {
frm.trigger('show_progress_for_items'); if (frm.doc.docstatus===1) {
frm.trigger('show_progress_for_operations'); frm.trigger('show_progress_for_items');
} frm.trigger('show_progress_for_operations');
}
if (frm.doc.docstatus === 1 if (frm.doc.docstatus === 1
&& frm.doc.operations && frm.doc.operations.length) { && frm.doc.operations && frm.doc.operations.length) {
const not_completed = frm.doc.operations.filter(d => { const not_completed = frm.doc.operations.filter(d => {
if(d.status != 'Completed') { if (d.status != 'Completed') {
return true; return true;
}
});
if (not_completed && not_completed.length) {
frm.add_custom_button(__('Create Job Card'), () => {
frm.trigger("make_job_card");
}).addClass('btn-primary');
} }
});
if(not_completed && not_completed.length) {
frm.add_custom_button(__('Create Job Card'), () => {
frm.trigger("make_job_card");
}).addClass('btn-primary');
} }
} }
@@ -517,14 +519,22 @@ frappe.ui.form.on("Work Order Operation", {
erpnext.work_order = { erpnext.work_order = {
set_custom_buttons: function(frm) { set_custom_buttons: function(frm) {
var doc = frm.doc; var doc = frm.doc;
if (doc.docstatus === 1) { 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.status != 'Stopped' && doc.status != 'Completed') { if (doc.status != 'Stopped' && doc.status != 'Completed') {
frm.add_custom_button(__('Stop'), function() { frm.add_custom_button(__('Stop'), function() {
erpnext.work_order.stop_work_order(frm, "Stopped"); erpnext.work_order.change_work_order_status(frm, "Stopped");
}, __("Status")); }, __("Status"));
} else if (doc.status == 'Stopped') { } else if (doc.status == 'Stopped') {
frm.add_custom_button(__('Re-open'), function() { frm.add_custom_button(__('Re-open'), function() {
erpnext.work_order.stop_work_order(frm, "Resumed"); erpnext.work_order.change_work_order_status(frm, "Resumed");
}, __("Status")); }, __("Status"));
} }
@@ -713,9 +723,10 @@ erpnext.work_order = {
}); });
}, },
stop_work_order: function(frm, status) { change_work_order_status: function(frm, status) {
let method_name = status=="Closed" ? "close_work_order" : "stop_unstop";
frappe.call({ frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.stop_unstop", method: `erpnext.manufacturing.doctype.work_order.work_order.${method_name}`,
freeze: true, freeze: true,
freeze_message: __("Updating Work Order status"), freeze_message: __("Updating Work Order status"),
args: { args: {

View File

@@ -99,7 +99,7 @@
"no_copy": 1, "no_copy": 1,
"oldfieldname": "status", "oldfieldname": "status",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "\nDraft\nSubmitted\nNot Started\nIn Process\nCompleted\nStopped\nCancelled", "options": "\nDraft\nSubmitted\nNot Started\nIn Process\nCompleted\nStopped\nClosed\nCancelled",
"read_only": 1, "read_only": 1,
"reqd": 1, "reqd": 1,
"search_index": 1 "search_index": 1
@@ -182,6 +182,7 @@
"reqd": 1 "reqd": 1
}, },
{ {
"default": "1.0",
"fieldname": "qty", "fieldname": "qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Qty To Manufacture", "label": "Qty To Manufacture",
@@ -572,10 +573,12 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-08-24 15:14:03.844937", "migration_hash": "a18118963f4fcdb7f9d326de5f4063ba",
"modified": "2021-10-29 15:12:32.203605",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order", "name": "Work Order",
"naming_rule": "By \"Naming Series\" field",
"nsm_parent_field": "parent_work_order", "nsm_parent_field": "parent_work_order",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [

View File

@@ -175,7 +175,7 @@ class WorkOrder(Document):
def update_status(self, status=None): def update_status(self, status=None):
'''Update status of work order if unknown''' '''Update status of work order if unknown'''
if status != "Stopped": if status != "Stopped" and status != "Closed":
status = self.get_status(status) status = self.get_status(status)
if status != self.status: if status != self.status:
@@ -624,7 +624,6 @@ class WorkOrder(Document):
def validate_operation_time(self): def validate_operation_time(self):
for d in self.operations: for d in self.operations:
if not d.time_in_mins > 0: 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)) frappe.throw(_("Operation Time must be greater than 0 for Operation {0}").format(d.operation))
def update_required_items(self): def update_required_items(self):
@@ -685,9 +684,7 @@ class WorkOrder(Document):
if not d.operation: if not d.operation:
d.operation = operation d.operation = operation
else: else:
# Attribute a big number (999) to idx for sorting putpose in case idx is NULL for item in sorted(item_dict.values(), key=lambda d: d['idx'] or float('inf')):
# 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', { self.append('required_items', {
'rate': item.rate, 'rate': item.rate,
'amount': item.rate * item.qty, 'amount': item.rate * item.qty,
@@ -969,6 +966,10 @@ def stop_unstop(work_order, status):
frappe.throw(_("Not permitted"), frappe.PermissionError) frappe.throw(_("Not permitted"), frappe.PermissionError)
pro_order = frappe.get_doc("Work Order", work_order) 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_status(status)
pro_order.update_planned_qty() pro_order.update_planned_qty()
frappe.msgprint(_("Work Order has been {0}").format(status)) frappe.msgprint(_("Work Order has been {0}").format(status))
@@ -1003,6 +1004,29 @@ def make_job_card(work_order, operations):
if row.job_card_qty > 0: if row.job_card_qty > 0:
create_job_card(work_order, row, auto_create=True) 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): def split_qty_based_on_batch_size(wo_doc, row, qty):
if not cint(frappe.db.get_value("Operation", if not cint(frappe.db.get_value("Operation",
row.operation, "create_job_card_based_on_batch_size")): row.operation, "create_job_card_based_on_batch_size")):

View File

@@ -24,7 +24,7 @@ def get_data(filters):
} }
fields = ["name", "status", "work_order", "production_item", "item_name", "posting_date", fields = ["name", "status", "work_order", "production_item", "item_name", "posting_date",
"total_completed_qty", "workstation", "operation", "employee_name", "total_time_in_mins"] "total_completed_qty", "workstation", "operation", "total_time_in_mins"]
for field in ["work_order", "workstation", "operation", "company"]: for field in ["work_order", "workstation", "operation", "company"]:
if filters.get(field): if filters.get(field):
@@ -45,7 +45,7 @@ def get_data(filters):
job_card_time_details = {} job_card_time_details = {}
for job_card_data in frappe.get_all("Job Card Time Log", 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"], fields=["min(from_time) as from_time", "max(to_time) as to_time", "parent"],
filters=job_card_time_filter, group_by="parent", debug=1): filters=job_card_time_filter, group_by="parent"):
job_card_time_details[job_card_data.parent] = job_card_data job_card_time_details[job_card_data.parent] = job_card_data
res = [] res = []
@@ -172,12 +172,6 @@ def get_columns(filters):
"options": "Operation", "options": "Operation",
"width": 110 "width": 110
}, },
{
"label": _("Employee Name"),
"fieldname": "employee_name",
"fieldtype": "Data",
"width": 110
},
{ {
"label": _("Total Completed Qty"), "label": _("Total Completed Qty"),
"fieldname": "total_completed_qty", "fieldname": "total_completed_qty",

View File

@@ -9,9 +9,9 @@
"filters": [], "filters": [],
"idx": 0, "idx": 0,
"is_standard": "Yes", "is_standard": "Yes",
"modified": "2021-08-24 16:38:15.233395", "modified": "2021-10-20 22:03:57.606612",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Manufacturing",
"name": "Process Loss Report", "name": "Process Loss Report",
"owner": "Administrator", "owner": "Administrator",
"prepared_report": 0, "prepared_report": 0,
@@ -21,9 +21,6 @@
"roles": [ "roles": [
{ {
"role": "Manufacturing User" "role": "Manufacturing User"
},
{
"role": "Stock User"
} }
] ]
} }

View File

@@ -111,7 +111,7 @@ def run_query(query_args: QueryArgs) -> Data:
{work_order_filter} {work_order_filter}
GROUP BY GROUP BY
se.work_order se.work_order
""".format(**query_args), query_args, as_dict=1, debug=1) """.format(**query_args), query_args, as_dict=1)
def update_data_with_total_pl_value(data: Data) -> None: def update_data_with_total_pl_value(data: Data) -> None:
for row in data: for row in data:

View File

@@ -0,0 +1,64 @@
import unittest
from typing import List, Tuple
import frappe
from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report
DEFAULT_FILTERS = {
"company": "_Test Company",
"from_date": "2010-01-01",
"to_date": "2030-01-01",
"warehouse": "_Test Warehouse - _TC",
}
REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
("BOM Explorer", {"bom": frappe.get_last_doc("BOM").name}),
("BOM Operations Time", {}),
("BOM Stock Calculated", {"bom": frappe.get_last_doc("BOM").name, "qty_to_make": 2}),
("BOM Stock Report", {"bom": frappe.get_last_doc("BOM").name, "qty_to_produce": 2}),
("Cost of Poor Quality Report", {}),
("Downtime Analysis", {}),
(
"Exponential Smoothing Forecasting",
{
"based_on_document": "Sales Order",
"based_on_field": "Qty",
"no_of_years": 3,
"periodicity": "Yearly",
"smoothing_constant": 0.3,
},
),
("Job Card Summary", {"fiscal_year": "2021-2022"}),
("Production Analytics", {"range": "Monthly"}),
("Quality Inspection Summary", {}),
("Process Loss Report", {}),
("Work Order Stock Report", {}),
("Work Order Summary", {"fiscal_year": "2021-2022", "age": 0}),
]
if frappe.db.a_row_exists("Production Plan"):
REPORT_FILTER_TEST_CASES.append(
("Production Plan Summary", {"production_plan": frappe.get_last_doc("Production Plan").name})
)
OPTIONAL_FILTERS = {
"warehouse": "_Test Warehouse - _TC",
"item": "_Test Item",
"item_group": "_Test Item Group",
}
class TestManufacturingReports(unittest.TestCase):
def test_execute_all_manufacturing_reports(self):
"""Test that all script report in manufacturing modules are executable with supported filters"""
for report, filter in REPORT_FILTER_TEST_CASES:
execute_script_report(
report_name=report,
module="Manufacturing",
filters=filter,
default_filters=DEFAULT_FILTERS,
optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
)

View File

@@ -306,7 +306,9 @@ 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.rename_discharge_ordered_date_in_ip_record
erpnext.patches.v13_0.migrate_stripe_api erpnext.patches.v13_0.migrate_stripe_api
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
erpnext.patches.v13_0.custom_fields_for_taxjar_integration 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.set_operation_time_based_on_operating_cost erpnext.patches.v13_0.set_operation_time_based_on_operating_cost
erpnext.patches.v13_0.validate_options_for_data_field erpnext.patches.v13_0.validate_options_for_data_field
erpnext.patches.v13_0.create_website_items #30-09-2021 erpnext.patches.v13_0.create_website_items #30-09-2021
@@ -327,3 +329,6 @@ erpnext.patches.v13_0.trim_sales_invoice_custom_field_length
erpnext.patches.v13_0.enable_scheduler_job_for_item_reposting erpnext.patches.v13_0.enable_scheduler_job_for_item_reposting
erpnext.patches.v13_0.requeue_failed_reposts erpnext.patches.v13_0.requeue_failed_reposts
erpnext.patches.v13_0.fetch_thumbnail_in_website_items erpnext.patches.v13_0.fetch_thumbnail_in_website_items
erpnext.patches.v12_0.update_production_plan_status
erpnext.patches.v13_0.update_category_in_ltds_certificate
erpnext.patches.v13_0.create_ksa_vat_custom_fields

View File

@@ -0,0 +1,31 @@
# Copyright (c) 2021, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
def execute():
frappe.reload_doc("manufacturing", "doctype", "production_plan")
frappe.db.sql("""
UPDATE `tabProduction Plan` ppl
SET status = "Completed"
WHERE ppl.name IN (
SELECT ss.name FROM (
SELECT
(
count(wo.status = "Completed") =
count(pp.name)
) =
(
pp.status != "Completed"
AND pp.total_produced_qty >= pp.total_planned_qty
) AS should_set,
pp.name AS name
FROM
`tabWork Order` wo INNER JOIN`tabProduction Plan` pp
ON wo.production_plan = pp.name
GROUP BY pp.name
HAVING should_set = 1
) ss
)
""")

View File

@@ -0,0 +1,12 @@
import frappe
from erpnext.regional.saudi_arabia.setup import make_custom_fields
def execute():
company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'})
if not company:
return
make_custom_fields()

View File

@@ -3,7 +3,7 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from erpnext.regional.united_states.setup import add_permissions from erpnext.erpnext_integrations.doctype.taxjar_settings.taxjar_settings import add_permissions
def execute(): def execute():
@@ -11,22 +11,31 @@ def execute():
if not company: if not company:
return return
frappe.reload_doc("regional", "doctype", "product_tax_category") TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
TAXJAR_SANDBOX_MODE = frappe.db.get_single_value("TaxJar Settings", "is_sandbox")
if (not TAXJAR_CREATE_TRANSACTIONS and not TAXJAR_CALCULATE_TAX and not TAXJAR_SANDBOX_MODE):
return
custom_fields = { custom_fields = {
'Sales Invoice Item': [ 'Sales Invoice Item': [
dict(fieldname='product_tax_category', fieldtype='Link', insert_after='description', options='Product Tax Category', 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'), label='Product Tax Category', fetch_from='item_code.product_tax_category'),
dict(fieldname='tax_collectable', fieldtype='Currency', insert_after='net_amount', dict(fieldname='tax_collectable', fieldtype='Currency', insert_after='net_amount',
label='Tax Collectable', read_only=1), label='Tax Collectable', read_only=1, options='currency'),
dict(fieldname='taxable_amount', fieldtype='Currency', insert_after='tax_collectable', dict(fieldname='taxable_amount', fieldtype='Currency', insert_after='tax_collectable',
label='Taxable Amount', read_only=1) label='Taxable Amount', read_only=1, options='currency')
], ],
'Item': [ 'Item': [
dict(fieldname='product_tax_category', fieldtype='Link', insert_after='item_group', options='Product Tax Category', dict(fieldname='product_tax_category', fieldtype='Link', insert_after='item_group', options='Product Tax Category',
label='Product Tax Category') label='Product Tax Category')
],
'TaxJar Settings': [
dict(fieldname='company', fieldtype='Link', insert_after='configuration', options='Company',
label='Company')
] ]
} }
create_custom_fields(custom_fields, update=True) create_custom_fields(custom_fields, update=True)
add_permissions() add_permissions()
frappe.enqueue('erpnext.regional.united_states.setup.add_product_tax_categories', now=True) frappe.enqueue('erpnext.erpnext_integrations.doctype.taxjar_settings.taxjar_settings.add_product_tax_categories', now=True)

View File

@@ -0,0 +1,20 @@
import frappe
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
frappe.reload_doc('regional', 'doctype', 'lower_deduction_certificate')
ldc = frappe.qb.DocType("Lower Deduction Certificate").as_("ldc")
supplier = frappe.qb.DocType("Supplier")
frappe.qb.update(ldc).inner_join(supplier).on(
ldc.supplier == supplier.name
).set(
ldc.tax_withholding_category, supplier.tax_withholding_category
).where(
ldc.tax_withholding_category.isnull()
).run()

View File

@@ -125,27 +125,28 @@ class AdditionalSalary(Document):
no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1 no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1
return amount_per_day * no_of_days return amount_per_day * no_of_days
@frappe.whitelist()
def get_additional_salaries(employee, start_date, end_date, component_type): def get_additional_salaries(employee, start_date, end_date, component_type):
additional_salary_list = frappe.db.sql(""" comp_type = 'Earning' if component_type == 'earnings' else 'Deduction'
select name, salary_component as component, type, amount,
overwrite_salary_structure_amount as overwrite, additional_sal = frappe.qb.DocType('Additional Salary')
deduct_full_tax_on_selected_payroll_date component_field = additional_sal.salary_component.as_('component')
from `tabAdditional Salary` overwrite_field = additional_sal.overwrite_salary_structure_amount.as_('overwrite')
where employee=%(employee)s
and docstatus = 1 additional_salary_list = frappe.qb.from_(
and ( additional_sal
payroll_date between %(from_date)s and %(to_date)s ).select(
or additional_sal.name, component_field, additional_sal.type,
from_date <= %(to_date)s and to_date >= %(to_date)s additional_sal.amount, additional_sal.is_recurring, overwrite_field,
) additional_sal.deduct_full_tax_on_selected_payroll_date
and type = %(component_type)s ).where(
order by salary_component, overwrite ASC (additional_sal.employee == employee)
""", { & (additional_sal.docstatus == 1)
'employee': employee, & (additional_sal.type == comp_type)
'from_date': start_date, ).where(
'to_date': end_date, additional_sal.payroll_date[start_date: end_date]
'component_type': "Earning" if component_type == "earnings" else "Deduction" | ((additional_sal.from_date <= end_date) & (additional_sal.to_date >= end_date))
}, as_dict=1) ).run(as_dict=True)
additional_salaries = [] additional_salaries = []
components_to_overwrite = [] components_to_overwrite = []

View File

@@ -12,6 +12,7 @@
"year_to_date", "year_to_date",
"section_break_5", "section_break_5",
"additional_salary", "additional_salary",
"is_recurring_additional_salary",
"statistical_component", "statistical_component",
"depends_on_payment_days", "depends_on_payment_days",
"exempted_from_income_tax", "exempted_from_income_tax",
@@ -235,11 +236,19 @@
"label": "Year To Date", "label": "Year To Date",
"options": "currency", "options": "currency",
"read_only": 1 "read_only": 1
} },
{
"default": "0",
"depends_on": "eval:doc.parenttype=='Salary Slip' && doc.additional_salary",
"fieldname": "is_recurring_additional_salary",
"fieldtype": "Check",
"label": "Is Recurring Additional Salary",
"read_only": 1
}
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-01-14 13:39:15.847158", "modified": "2021-08-30 13:39:15.847158",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Salary Detail", "name": "Salary Detail",

View File

@@ -172,7 +172,6 @@ class SalarySlip(TransactionBase):
and employee = %s and name != %s {0}""".format(cond), and employee = %s and name != %s {0}""".format(cond),
(self.start_date, self.end_date, self.employee, self.name)) (self.start_date, self.end_date, self.employee, self.name))
if ret_exist: if ret_exist:
self.employee = ''
frappe.throw(_("Salary Slip of employee {0} already created for this period").format(self.employee)) frappe.throw(_("Salary Slip of employee {0} already created for this period").format(self.employee))
else: else:
for data in self.timesheets: for data in self.timesheets:
@@ -630,7 +629,8 @@ class SalarySlip(TransactionBase):
get_salary_component_data(additional_salary.component), get_salary_component_data(additional_salary.component),
additional_salary.amount, additional_salary.amount,
component_type, component_type,
additional_salary additional_salary,
is_recurring = additional_salary.is_recurring
) )
def add_tax_components(self, payroll_period): def add_tax_components(self, payroll_period):
@@ -651,7 +651,7 @@ class SalarySlip(TransactionBase):
tax_row = get_salary_component_data(d) tax_row = get_salary_component_data(d)
self.update_component_row(tax_row, tax_amount, "deductions") self.update_component_row(tax_row, tax_amount, "deductions")
def update_component_row(self, component_data, amount, component_type, additional_salary=None): def update_component_row(self, component_data, amount, component_type, additional_salary=None, is_recurring = 0):
component_row = None component_row = None
for d in self.get(component_type): for d in self.get(component_type):
if d.salary_component != component_data.salary_component: if d.salary_component != component_data.salary_component:
@@ -702,6 +702,7 @@ class SalarySlip(TransactionBase):
component_row.default_amount = 0 component_row.default_amount = 0
component_row.additional_amount = amount component_row.additional_amount = amount
component_row.is_recurring_additional_salary = is_recurring
component_row.additional_salary = additional_salary.name component_row.additional_salary = additional_salary.name
component_row.deduct_full_tax_on_selected_payroll_date = \ component_row.deduct_full_tax_on_selected_payroll_date = \
additional_salary.deduct_full_tax_on_selected_payroll_date additional_salary.deduct_full_tax_on_selected_payroll_date
@@ -898,25 +899,33 @@ class SalarySlip(TransactionBase):
amount, additional_amount = earning.default_amount, earning.additional_amount amount, additional_amount = earning.default_amount, earning.additional_amount
if earning.is_tax_applicable: if earning.is_tax_applicable:
if additional_amount:
taxable_earnings += (amount - additional_amount)
additional_income += additional_amount
if earning.deduct_full_tax_on_selected_payroll_date:
additional_income_with_full_tax += additional_amount
continue
if earning.is_flexible_benefit: if earning.is_flexible_benefit:
flexi_benefits += amount flexi_benefits += amount
else: else:
taxable_earnings += amount taxable_earnings += (amount - additional_amount)
additional_income += additional_amount
# Get additional amount based on future recurring additional salary
if additional_amount and earning.is_recurring_additional_salary:
additional_income += self.get_future_recurring_additional_amount(earning.additional_salary,
earning.additional_amount) # Used earning.additional_amount to consider the amount for the full month
if earning.deduct_full_tax_on_selected_payroll_date:
additional_income_with_full_tax += additional_amount
if allow_tax_exemption: if allow_tax_exemption:
for ded in self.deductions: for ded in self.deductions:
if ded.exempted_from_income_tax: if ded.exempted_from_income_tax:
amount = ded.amount amount, additional_amount = ded.amount, ded.additional_amount
if based_on_payment_days: if based_on_payment_days:
amount = self.get_amount_based_on_payment_days(ded, joining_date, relieving_date)[0] amount, additional_amount = self.get_amount_based_on_payment_days(ded, joining_date, relieving_date)
taxable_earnings -= flt(amount)
taxable_earnings -= flt(amount - additional_amount)
additional_income -= additional_amount
if additional_amount and ded.is_recurring_additional_salary:
additional_income -= self.get_future_recurring_additional_amount(ded.additional_salary,
ded.additional_amount) # Used ded.additional_amount to consider the amount for the full month
return frappe._dict({ return frappe._dict({
"taxable_earnings": taxable_earnings, "taxable_earnings": taxable_earnings,
@@ -925,11 +934,21 @@ class SalarySlip(TransactionBase):
"flexi_benefits": flexi_benefits "flexi_benefits": flexi_benefits
}) })
def get_future_recurring_additional_amount(self, additional_salary, monthly_additional_amount):
future_recurring_additional_amount = 0
to_date = frappe.db.get_value("Additional Salary", additional_salary, 'to_date')
# future month count excluding current
future_recurring_period = (getdate(to_date).month - getdate(self.start_date).month)
if future_recurring_period > 0:
future_recurring_additional_amount = monthly_additional_amount * future_recurring_period # Used earning.additional_amount to consider the amount for the full month
return future_recurring_additional_amount
def get_amount_based_on_payment_days(self, row, joining_date, relieving_date): def get_amount_based_on_payment_days(self, row, joining_date, relieving_date):
amount, additional_amount = row.amount, row.additional_amount amount, additional_amount = row.amount, row.additional_amount
if (self.salary_structure and if (self.salary_structure and
cint(row.depends_on_payment_days) and cint(self.total_working_days) and cint(row.depends_on_payment_days) and cint(self.total_working_days)
(not self.salary_slip_based_on_timesheet or and not (row.additional_salary and row.default_amount) # to identify overwritten additional salary
and (not self.salary_slip_based_on_timesheet or
getdate(self.start_date) < joining_date or getdate(self.start_date) < joining_date or
(relieving_date and getdate(self.end_date) > relieving_date) (relieving_date and getdate(self.end_date) > relieving_date)
)): )):
@@ -1248,7 +1267,7 @@ class SalarySlip(TransactionBase):
salary_slip_sum = frappe.get_list('Salary Slip', salary_slip_sum = frappe.get_list('Salary Slip',
fields = ['sum(net_pay) as net_sum', 'sum(gross_pay) as gross_sum'], fields = ['sum(net_pay) as net_sum', 'sum(gross_pay) as gross_sum'],
filters = {'employee_name' : self.employee_name, filters = {'employee' : self.employee,
'start_date' : ['>=', period_start_date], 'start_date' : ['>=', period_start_date],
'end_date' : ['<', period_end_date], 'end_date' : ['<', period_end_date],
'name': ['!=', self.name], 'name': ['!=', self.name],
@@ -1268,7 +1287,7 @@ class SalarySlip(TransactionBase):
first_day_of_the_month = get_first_day(self.start_date) first_day_of_the_month = get_first_day(self.start_date)
salary_slip_sum = frappe.get_list('Salary Slip', salary_slip_sum = frappe.get_list('Salary Slip',
fields = ['sum(net_pay) as sum'], fields = ['sum(net_pay) as sum'],
filters = {'employee_name' : self.employee_name, filters = {'employee' : self.employee,
'start_date' : ['>=', first_day_of_the_month], 'start_date' : ['>=', first_day_of_the_month],
'end_date' : ['<', self.start_date], 'end_date' : ['<', self.start_date],
'name': ['!=', self.name], 'name': ['!=', self.name],
@@ -1292,13 +1311,13 @@ class SalarySlip(TransactionBase):
INNER JOIN `tabSalary Slip` as salary_slip INNER JOIN `tabSalary Slip` as salary_slip
ON detail.parent = salary_slip.name ON detail.parent = salary_slip.name
WHERE WHERE
salary_slip.employee_name = %(employee_name)s salary_slip.employee = %(employee)s
AND detail.salary_component = %(component)s AND detail.salary_component = %(component)s
AND salary_slip.start_date >= %(period_start_date)s AND salary_slip.start_date >= %(period_start_date)s
AND salary_slip.end_date < %(period_end_date)s AND salary_slip.end_date < %(period_end_date)s
AND salary_slip.name != %(docname)s AND salary_slip.name != %(docname)s
AND salary_slip.docstatus = 1""", AND salary_slip.docstatus = 1""",
{'employee_name': self.employee_name, 'component': component.salary_component, 'period_start_date': period_start_date, {'employee': self.employee, 'component': component.salary_component, 'period_start_date': period_start_date,
'period_end_date': period_end_date, 'docname': self.name} 'period_end_date': period_end_date, 'docname': self.name}
) )

View File

@@ -540,6 +540,61 @@ class TestSalarySlip(unittest.TestCase):
# undelete fixture data # undelete fixture data
frappe.db.rollback() frappe.db.rollback()
def test_tax_for_recurring_additional_salary(self):
frappe.db.sql("""delete from `tabPayroll Period`""")
frappe.db.sql("""delete from `tabSalary Component`""")
payroll_period = create_payroll_period()
create_tax_slab(payroll_period, allow_tax_exemption=True)
employee = make_employee("test_tax@salary.slip")
delete_docs = [
"Salary Slip",
"Additional Salary",
"Employee Tax Exemption Declaration",
"Employee Tax Exemption Proof Submission",
"Employee Benefit Claim",
"Salary Structure Assignment"
]
for doc in delete_docs:
frappe.db.sql("delete from `tab%s` where employee='%s'" % (doc, employee))
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
salary_structure = make_salary_structure("Stucture to test tax", "Monthly",
other_details={"max_benefits": 100000}, test_tax=True,
employee=employee, payroll_period=payroll_period)
create_salary_slips_for_payroll_period(employee, salary_structure.name,
payroll_period, deduct_random=False, num=3)
tax_paid = get_tax_paid_in_period(employee)
annual_tax = 23196.0
self.assertEqual(tax_paid, annual_tax)
frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""", (employee))
#------------------------------------
# Recurring additional salary
start_date = add_months(payroll_period.start_date, 3)
end_date = add_months(payroll_period.start_date, 5)
create_recurring_additional_salary(employee, "Performance Bonus", 20000, start_date, end_date)
frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""", (employee))
create_salary_slips_for_payroll_period(employee, salary_structure.name,
payroll_period, deduct_random=False, num=4)
tax_paid = get_tax_paid_in_period(employee)
annual_tax = 32315.0
self.assertEqual(tax_paid, annual_tax)
frappe.db.rollback()
def make_activity_for_employee(self): def make_activity_for_employee(self):
activity_type = frappe.get_doc("Activity Type", "_Test Activity Type") activity_type = frappe.get_doc("Activity Type", "_Test Activity Type")
activity_type.billing_rate = 50 activity_type.billing_rate = 50
@@ -1011,3 +1066,17 @@ def make_salary_slip_for_payment_days_dependency_test(employee, salary_structure
salary_slip = frappe.get_doc("Salary Slip", salary_slip_name) salary_slip = frappe.get_doc("Salary Slip", salary_slip_name)
return salary_slip return salary_slip
def create_recurring_additional_salary(employee, salary_component, amount, from_date, to_date, company=None):
frappe.get_doc({
"doctype": "Additional Salary",
"employee": employee,
"company": company or erpnext.get_default_company(),
"salary_component": salary_component,
"is_recurring": 1,
"from_date": from_date,
"to_date": to_date,
"amount": amount,
"type": "Earning",
"currency": erpnext.get_default_currency()
}).submit()

View File

@@ -32,12 +32,12 @@ frappe.ui.form.on("Timesheet", {
}; };
}, },
onload: function(frm){ onload: function(frm) {
if (frm.doc.__islocal && frm.doc.time_logs) { if (frm.doc.__islocal && frm.doc.time_logs) {
calculate_time_and_amount(frm); calculate_time_and_amount(frm);
} }
if (frm.is_new()) { if (frm.is_new() && !frm.doc.employee) {
set_employee_and_company(frm); set_employee_and_company(frm);
} }
}, },
@@ -283,7 +283,9 @@ frappe.ui.form.on("Timesheet Detail", {
calculate_time_and_amount(frm); calculate_time_and_amount(frm);
}, },
activity_type: function(frm, cdt, cdn) { activity_type: function (frm, cdt, cdn) {
if (!frappe.get_doc(cdt, cdn).activity_type) return;
frappe.call({ frappe.call({
method: "erpnext.projects.doctype.timesheet.timesheet.get_activity_cost", method: "erpnext.projects.doctype.timesheet.timesheet.get_activity_cost",
args: { args: {
@@ -291,10 +293,10 @@ frappe.ui.form.on("Timesheet Detail", {
activity_type: frm.selected_doc.activity_type, activity_type: frm.selected_doc.activity_type,
currency: frm.doc.currency currency: frm.doc.currency
}, },
callback: function(r){ callback: function (r) {
if(r.message){ if (r.message) {
frappe.model.set_value(cdt, cdn, 'billing_rate', r.message['billing_rate']); frappe.model.set_value(cdt, cdn, "billing_rate", r.message["billing_rate"]);
frappe.model.set_value(cdt, cdn, 'costing_rate', r.message['costing_rate']); frappe.model.set_value(cdt, cdn, "costing_rate", r.message["costing_rate"]);
calculate_billing_costing_amount(frm, cdt, cdn); calculate_billing_costing_amount(frm, cdt, cdn);
} }
} }

View File

@@ -137,7 +137,9 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
var me = this; var me = this;
$.each(this.frm.doc["taxes"] || [], function(i, tax) { $.each(this.frm.doc["taxes"] || [], function(i, tax) {
tax.item_wise_tax_detail = {}; if (!tax.dont_recompute_tax) {
tax.item_wise_tax_detail = {};
}
var tax_fields = ["total", "tax_amount_after_discount_amount", var tax_fields = ["total", "tax_amount_after_discount_amount",
"tax_amount_for_current_item", "grand_total_for_current_item", "tax_amount_for_current_item", "grand_total_for_current_item",
"tax_fraction_for_current_item", "grand_total_fraction_for_current_item"]; "tax_fraction_for_current_item", "grand_total_fraction_for_current_item"];
@@ -419,7 +421,9 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
current_tax_amount = tax_rate * item.qty; current_tax_amount = tax_rate * item.qty;
} }
this.set_item_wise_tax(item, tax, tax_rate, current_tax_amount); if (!tax.dont_recompute_tax) {
this.set_item_wise_tax(item, tax, tax_rate, current_tax_amount);
}
return current_tax_amount; return current_tax_amount;
}, },
@@ -587,7 +591,9 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
delete tax[fieldname]; delete tax[fieldname];
}); });
tax.item_wise_tax_detail = JSON.stringify(tax.item_wise_tax_detail); if (!tax.dont_recompute_tax) {
tax.item_wise_tax_detail = JSON.stringify(tax.item_wise_tax_detail);
}
}); });
} }
}, },

View File

@@ -113,15 +113,15 @@ function get_filters() {
"fieldname":"period_start_date", "fieldname":"period_start_date",
"label": __("Start Date"), "label": __("Start Date"),
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 1, "reqd": 1,
"reqd": 1 "depends_on": "eval:doc.filter_based_on == 'Date Range'"
}, },
{ {
"fieldname":"period_end_date", "fieldname":"period_end_date",
"label": __("End Date"), "label": __("End Date"),
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 1, "reqd": 1,
"reqd": 1 "depends_on": "eval:doc.filter_based_on == 'Date Range'"
}, },
{ {
"fieldname":"from_fiscal_year", "fieldname":"from_fiscal_year",
@@ -129,7 +129,8 @@ function get_filters() {
"fieldtype": "Link", "fieldtype": "Link",
"options": "Fiscal Year", "options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"), "default": frappe.defaults.get_user_default("fiscal_year"),
"reqd": 1 "reqd": 1,
"depends_on": "eval:doc.filter_based_on == 'Fiscal Year'"
}, },
{ {
"fieldname":"to_fiscal_year", "fieldname":"to_fiscal_year",
@@ -137,7 +138,8 @@ function get_filters() {
"fieldtype": "Link", "fieldtype": "Link",
"options": "Fiscal Year", "options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"), "default": frappe.defaults.get_user_default("fiscal_year"),
"reqd": 1 "reqd": 1,
"depends_on": "eval:doc.filter_based_on == 'Fiscal Year'"
}, },
{ {
"fieldname": "periodicity", "fieldname": "periodicity",

View File

@@ -459,6 +459,7 @@ body.product-page {
min-height: 0px; min-height: 0px;
.r-item-image { .r-item-image {
min-height: 100px;
width: 40%; width: 40%;
.r-product-image { .r-product-image {
@@ -480,6 +481,7 @@ body.product-page {
.r-item-info { .r-item-info {
font-size: 14px; font-size: 14px;
padding-right: 0; padding-right: 0;
padding-left: 10px;
width: 60%; width: 60%;
a { a {
@@ -672,18 +674,6 @@ body.product-page {
img { img {
max-height: 112px; max-height: 112px;
} }
.no-image-cart-item {
max-height: 112px;
display: flex; justify-content: center;
background-color: var(--gray-200);
align-items: center;
color: var(--gray-400);
margin-top: .15rem;
border-radius: 6px;
height: 100%;
font-size: 24px;
}
} }
.cart-items { .cart-items {
@@ -862,6 +852,18 @@ body.product-page {
} }
} }
.no-image-cart-item {
max-height: 112px;
display: flex; justify-content: center;
background-color: var(--gray-200);
align-items: center;
color: var(--gray-400);
margin-top: .15rem;
border-radius: 6px;
height: 100%;
font-size: 24px;
}
.cart-empty.frappe-card { .cart-empty.frappe-card {
min-height: 76vh; min-height: 76vh;
@include flex(flex, center, center, column); @include flex(flex, center, center, column);

View File

@@ -31,3 +31,4 @@ def create_transaction_log(doc, method):
"document_name": doc.name, "document_name": doc.name,
"data": data "data": data
}).insert(ignore_permissions=True) }).insert(ignore_permissions=True)

View File

@@ -0,0 +1,49 @@
{
"actions": [],
"creation": "2021-07-13 09:17:09.862163",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"item_tax_template",
"account"
],
"fields": [
{
"fieldname": "account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "item_tax_template",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item Tax Template",
"options": "Item Tax Template",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-08-04 06:42:38.205597",
"modified_by": "Administrator",
"module": "Regional",
"name": "KSA VAT Purchase Account",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2021, Havenir Solutions and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class KSAVATPurchaseAccount(Document):
pass

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2021, Havenir Solutions and contributors
// For license information, please see license.txt
frappe.ui.form.on('KSA VAT Sales Account', {
// refresh: function(frm) {
// }
});

View File

@@ -0,0 +1,49 @@
{
"actions": [],
"creation": "2021-07-13 08:46:33.820968",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"item_tax_template",
"account"
],
"fields": [
{
"fieldname": "account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "item_tax_template",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item Tax Template",
"options": "Item Tax Template",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-08-04 06:42:00.081407",
"modified_by": "Administrator",
"module": "Regional",
"name": "KSA VAT Sales Account",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2021, Havenir Solutions and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class KSAVATSalesAccount(Document):
pass

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2021, Havenir Solutions and Contributors
# See license.txt
# import frappe
import unittest
class TestKSAVATSalesAccount(unittest.TestCase):
pass

Some files were not shown because too many files have changed in this diff Show More