Compare commits
164 Commits
v13.12.1
...
fix-error-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a3a0a83b7 | ||
|
|
b8d75ff241 | ||
|
|
a3b7682935 | ||
|
|
200f6da8b2 | ||
|
|
7c9018f401 | ||
|
|
ffadd671b7 | ||
|
|
47befa697d | ||
|
|
676c5280cc | ||
|
|
e0decb0ae2 | ||
|
|
b906cc20ae | ||
|
|
c98421c69a | ||
|
|
d7afb9ef65 | ||
|
|
a915b9cf72 | ||
|
|
a022e01d3f | ||
|
|
867cfa04b2 | ||
|
|
6e63dc1360 | ||
|
|
698214bd59 | ||
|
|
873d166a4e | ||
|
|
9166d58717 | ||
|
|
7895d2a048 | ||
|
|
5a06ee9230 | ||
|
|
b6609d1649 | ||
|
|
9431bb9466 | ||
|
|
fdd9cc76be | ||
|
|
f8348ab681 | ||
|
|
eecfb25c90 | ||
|
|
41a0e12954 | ||
|
|
f0383289d8 | ||
|
|
46209023ce | ||
|
|
e69bd39cdd | ||
|
|
0fcb3cd918 | ||
|
|
44ab131792 | ||
|
|
b648d77316 | ||
|
|
0012f0b2da | ||
|
|
1796f09c0f | ||
|
|
64b58b148f | ||
|
|
4415bf9968 | ||
|
|
1b7d94d70d | ||
|
|
952c60b3f5 | ||
|
|
8c33103838 | ||
|
|
a8c966eb25 | ||
|
|
88b1c1c87e | ||
|
|
771213c415 | ||
|
|
d86f5ec1ba | ||
|
|
a568fc7924 | ||
|
|
c040256793 | ||
|
|
29996ee726 | ||
|
|
1120506e11 | ||
|
|
e64751e3a2 | ||
|
|
bf47c6836e | ||
|
|
e80192e2da | ||
|
|
b2f1b02e34 | ||
|
|
ed090f2e3e | ||
|
|
d796172249 | ||
|
|
ff9f6366ad | ||
|
|
b8683d5532 | ||
|
|
4a156cdc2e | ||
|
|
771b076448 | ||
|
|
e6346ac982 | ||
|
|
547e173fe0 | ||
|
|
37bd0ecf87 | ||
|
|
8244980d79 | ||
|
|
10b239ec50 | ||
|
|
c9c4a9995b | ||
|
|
b6dc71679e | ||
|
|
5bdb6041b9 | ||
|
|
89828defc5 | ||
|
|
816236b587 | ||
|
|
69f17721ef | ||
|
|
125bb1f99a | ||
|
|
6f786b42a9 | ||
|
|
fed80177de | ||
|
|
2ce36d1edc | ||
|
|
7e44c30404 | ||
|
|
47ced6810f | ||
|
|
6c3f5687f2 | ||
|
|
1b632b683f | ||
|
|
c5660e8511 | ||
|
|
c4338d184e | ||
|
|
d598a61556 | ||
|
|
d262d0ac27 | ||
|
|
7dc2f95932 | ||
|
|
cf87c9138e | ||
|
|
1a42f82d14 | ||
|
|
2c5a0bff47 | ||
|
|
a831e6b552 | ||
|
|
f844c36ab2 | ||
|
|
091c2f3023 | ||
|
|
119c2f01e1 | ||
|
|
40aac908d1 | ||
|
|
4e6d588ae1 | ||
|
|
504f2f06d3 | ||
|
|
d96fd60878 | ||
|
|
d9a219850a | ||
|
|
cb6d884058 | ||
|
|
4102f799dc | ||
|
|
3c53c5b660 | ||
|
|
0a4b3d8129 | ||
|
|
bad489426a | ||
|
|
729e29d268 | ||
|
|
8108d4761b | ||
|
|
00cb04df84 | ||
|
|
dd0cefbeb9 | ||
|
|
4da5cb36e7 | ||
|
|
56b58cbeea | ||
|
|
71f676eedd | ||
|
|
e57037b4cf | ||
|
|
2cbd5a9fcf | ||
|
|
47a7eeca54 | ||
|
|
62bbf0fe45 | ||
|
|
ae8c1ae311 | ||
|
|
8f5ab94b70 | ||
|
|
9507b2d752 | ||
|
|
7e018f94ce | ||
|
|
2cfafede44 | ||
|
|
df1f8fddf6 | ||
|
|
a17fed9cd9 | ||
|
|
d8479a41e5 | ||
|
|
d2f5d31f98 | ||
|
|
44ee44dec5 | ||
|
|
bebd77c27d | ||
|
|
0c55a98190 | ||
|
|
32d72fdecb | ||
|
|
046ec928e0 | ||
|
|
0660d6ed01 | ||
|
|
877820b902 | ||
|
|
ab0e381cfc | ||
|
|
5e34cdf00f | ||
|
|
4ecb798585 | ||
|
|
91d269fe1a | ||
|
|
4535a9415f | ||
|
|
e17713c9d6 | ||
|
|
21a5498d5d | ||
|
|
c0b17edbbf | ||
|
|
e9ed379b57 | ||
|
|
4d0d642db7 | ||
|
|
f02438eb54 | ||
|
|
62fa1f0305 | ||
|
|
77d4849ce8 | ||
|
|
fe4df3a14a | ||
|
|
ff570f48a0 | ||
|
|
e4b89d2fcd | ||
|
|
3529622a0d | ||
|
|
8f98238114 | ||
|
|
86e3adf344 | ||
|
|
d6152df3b4 | ||
|
|
4837238f3d | ||
|
|
e4f12f0458 | ||
|
|
23431cf261 | ||
|
|
57e66f958c | ||
|
|
05374cb8b2 | ||
|
|
36b519c962 | ||
|
|
15c9c08261 | ||
|
|
5d1de91b68 | ||
|
|
cf6e10ac7b | ||
|
|
6d99bb5ce6 | ||
|
|
6b38778dcb | ||
|
|
7c47f36a4c | ||
|
|
6ce2111b6d | ||
|
|
f0af24fc6d | ||
|
|
6eb9a114be | ||
|
|
8c01ae952b | ||
|
|
e8cf32e1c8 | ||
|
|
8dfdab9dc1 |
2
.github/helper/.flake8_strict
vendored
2
.github/helper/.flake8_strict
vendored
@@ -1,6 +1,8 @@
|
||||
[flake8]
|
||||
ignore =
|
||||
B007,
|
||||
B009,
|
||||
B010,
|
||||
B950,
|
||||
E101,
|
||||
E111,
|
||||
|
||||
@@ -131,3 +131,21 @@ rules:
|
||||
key `$X` is uselessly assigned twice. This could be a potential bug.
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
|
||||
- id: frappe-manual-commit
|
||||
patterns:
|
||||
- pattern: frappe.db.commit()
|
||||
- pattern-not-inside: |
|
||||
try:
|
||||
...
|
||||
except ...:
|
||||
...
|
||||
message: |
|
||||
Manually commiting a transaction is highly discouraged. Read about the transaction model implemented by Frappe Framework before adding manual commits: https://frappeframework.com/docs/user/en/api/database#database-transaction-model If you think manual commit is required then add a comment explaining why and `// nosemgrep` on the same line.
|
||||
paths:
|
||||
exclude:
|
||||
- "**/patches/**"
|
||||
- "**/demo/**"
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
2
.github/workflows/ui-tests.yml
vendored
2
.github/workflows/ui-tests.yml
vendored
@@ -99,6 +99,8 @@ jobs:
|
||||
|
||||
- name: Build Assets
|
||||
run: cd ~/frappe-bench/ && bench build
|
||||
env:
|
||||
CI: Yes
|
||||
|
||||
- name: UI Tests
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless
|
||||
|
||||
58
.mergify.yml
Normal file
58
.mergify.yml
Normal file
@@ -0,0 +1,58 @@
|
||||
pull_request_rules:
|
||||
- name: Auto-close PRs on stable branch
|
||||
conditions:
|
||||
- and:
|
||||
- and:
|
||||
- author!=surajshetty3416
|
||||
- author!=gavindsouza
|
||||
- author!=rohitwaghchaure
|
||||
- author!=nabinhait
|
||||
- or:
|
||||
- base=version-13
|
||||
- base=version-12
|
||||
actions:
|
||||
close:
|
||||
comment:
|
||||
message: |
|
||||
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
|
||||
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
|
||||
|
||||
- name: backport to version-13-hotfix
|
||||
conditions:
|
||||
- label="backport version-13-hotfix"
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- version-13-hotfix
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
|
||||
- name: backport to version-13-pre-release
|
||||
conditions:
|
||||
- label="backport version-13-pre-release"
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- version-13-pre-release
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
|
||||
- name: backport to version-12-hotfix
|
||||
conditions:
|
||||
- label="backport version-12-hotfix"
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- version-12-hotfix
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
|
||||
- name: backport to version-12-pre-release
|
||||
conditions:
|
||||
- label="backport version-12-pre-release"
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- version-12-pre-release
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
@@ -20,6 +20,9 @@ repos:
|
||||
rev: 3.9.2
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [
|
||||
'flake8-bugbear',
|
||||
]
|
||||
args: ['--config', '.github/helper/.flake8_strict']
|
||||
exclude: ".*setup.py$"
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ context('Organizational Chart', () => {
|
||||
cy.get('.frappe-control[data-fieldname=company] input').focus().as('input');
|
||||
cy.get('@input')
|
||||
.clear({ force: true })
|
||||
.type('Test Org Chart{enter}', { force: true })
|
||||
.type('Test Org Chart{downarrow}{enter}', { force: true })
|
||||
.blur({ force: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ context('Organizational Chart Mobile', () => {
|
||||
cy.get('.frappe-control[data-fieldname=company] input').focus().as('input');
|
||||
cy.get('@input')
|
||||
.clear({ force: true })
|
||||
.type('Test Org Chart{enter}', { force: true })
|
||||
.type('Test Org Chart{downarrow}{enter}', { force: true })
|
||||
.blur({ force: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import frappe
|
||||
|
||||
from erpnext.hooks import regional_overrides
|
||||
|
||||
__version__ = '13.12.1'
|
||||
__version__ = '13.13.0'
|
||||
|
||||
def get_default_company(user=None):
|
||||
'''Get default company for user'''
|
||||
|
||||
@@ -8,6 +8,8 @@ from frappe import _, throw
|
||||
from frappe.utils import cint, cstr
|
||||
from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_of
|
||||
|
||||
import erpnext
|
||||
|
||||
|
||||
class RootNotEditable(frappe.ValidationError): pass
|
||||
class BalanceMismatchError(frappe.ValidationError): pass
|
||||
@@ -196,7 +198,7 @@ class Account(NestedSet):
|
||||
"company": company,
|
||||
# parent account's currency should be passed down to child account's curreny
|
||||
# if it is None, it picks it up from default company currency, which might be unintended
|
||||
"account_currency": self.account_currency,
|
||||
"account_currency": erpnext.get_company_currency(company),
|
||||
"parent_account": parent_acc_name_map[company]
|
||||
})
|
||||
|
||||
@@ -207,8 +209,7 @@ class Account(NestedSet):
|
||||
# update the parent company's value in child companies
|
||||
doc = frappe.get_doc("Account", child_account)
|
||||
parent_value_changed = False
|
||||
for field in ['account_type', 'account_currency',
|
||||
'freeze_account', 'balance_must_be']:
|
||||
for field in ['account_type', 'freeze_account', 'balance_must_be']:
|
||||
if doc.get(field) != self.get(field):
|
||||
parent_value_changed = True
|
||||
doc.set(field, self.get(field))
|
||||
|
||||
@@ -45,6 +45,49 @@ frappe.treeview_settings["Account"] = {
|
||||
],
|
||||
root_label: "Accounts",
|
||||
get_tree_nodes: 'erpnext.accounts.utils.get_children',
|
||||
on_get_node: function(nodes, deep=false) {
|
||||
if (frappe.boot.user.can_read.indexOf("GL Entry") == -1) return;
|
||||
|
||||
let accounts = [];
|
||||
if (deep) {
|
||||
// in case of `get_all_nodes`
|
||||
accounts = nodes.reduce((acc, node) => [...acc, ...node.data], []);
|
||||
} else {
|
||||
accounts = nodes;
|
||||
}
|
||||
|
||||
const get_balances = frappe.call({
|
||||
method: 'erpnext.accounts.utils.get_account_balances',
|
||||
args: {
|
||||
accounts: accounts,
|
||||
company: cur_tree.args.company
|
||||
},
|
||||
});
|
||||
|
||||
get_balances.then(r => {
|
||||
if (!r.message || r.message.length == 0) return;
|
||||
|
||||
for (let account of r.message) {
|
||||
|
||||
const node = cur_tree.nodes && cur_tree.nodes[account.value];
|
||||
if (!node || node.is_root) continue;
|
||||
|
||||
// show Dr if positive since balance is calculated as debit - credit else show Cr
|
||||
const balance = account.balance_in_account_currency || account.balance;
|
||||
const dr_or_cr = balance > 0 ? "Dr": "Cr";
|
||||
const format = (value, currency) => format_currency(Math.abs(value), currency);
|
||||
|
||||
if (account.balance!==undefined) {
|
||||
$('<span class="balance-area pull-right">'
|
||||
+ (account.balance_in_account_currency ?
|
||||
(format(account.balance_in_account_currency, account.account_currency) + " / ") : "")
|
||||
+ format(account.balance, account.company_currency)
|
||||
+ " " + dr_or_cr
|
||||
+ '</span>').insertBefore(node.$ul);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
add_tree_node: 'erpnext.accounts.utils.add_ac',
|
||||
menu_items:[
|
||||
{
|
||||
@@ -122,24 +165,6 @@ frappe.treeview_settings["Account"] = {
|
||||
}
|
||||
}, "add");
|
||||
},
|
||||
onrender: function(node) {
|
||||
if (frappe.boot.user.can_read.indexOf("GL Entry") !== -1) {
|
||||
|
||||
// show Dr if positive since balance is calculated as debit - credit else show Cr
|
||||
let balance = node.data.balance_in_account_currency || node.data.balance;
|
||||
let dr_or_cr = balance > 0 ? "Dr": "Cr";
|
||||
|
||||
if (node.data && node.data.balance!==undefined) {
|
||||
$('<span class="balance-area pull-right">'
|
||||
+ (node.data.balance_in_account_currency ?
|
||||
(format_currency(Math.abs(node.data.balance_in_account_currency),
|
||||
node.data.account_currency) + " / ") : "")
|
||||
+ format_currency(Math.abs(node.data.balance), node.data.company_currency)
|
||||
+ " " + dr_or_cr
|
||||
+ '</span>').insertBefore(node.$ul);
|
||||
}
|
||||
}
|
||||
},
|
||||
toolbar: [
|
||||
{
|
||||
label:__("Add Child"),
|
||||
|
||||
@@ -12,7 +12,7 @@ from six import iteritems
|
||||
from unidecode import unidecode
|
||||
|
||||
|
||||
def create_charts(company, chart_template=None, existing_company=None, custom_chart=None):
|
||||
def create_charts(company, chart_template=None, existing_company=None, custom_chart=None, from_coa_importer=None):
|
||||
chart = custom_chart or get_chart(chart_template, existing_company)
|
||||
if chart:
|
||||
accounts = []
|
||||
@@ -22,7 +22,7 @@ def create_charts(company, chart_template=None, existing_company=None, custom_ch
|
||||
if root_account:
|
||||
root_type = child.get("root_type")
|
||||
|
||||
if account_name not in ["account_number", "account_type",
|
||||
if account_name not in ["account_name", "account_number", "account_type",
|
||||
"root_type", "is_group", "tax_rate"]:
|
||||
|
||||
account_number = cstr(child.get("account_number")).strip()
|
||||
@@ -35,7 +35,7 @@ def create_charts(company, chart_template=None, existing_company=None, custom_ch
|
||||
|
||||
account = frappe.get_doc({
|
||||
"doctype": "Account",
|
||||
"account_name": account_name,
|
||||
"account_name": child.get('account_name') if from_coa_importer else account_name,
|
||||
"company": company,
|
||||
"parent_account": parent,
|
||||
"is_group": is_group,
|
||||
@@ -213,7 +213,7 @@ def validate_bank_account(coa, bank_account):
|
||||
return (bank_account in accounts)
|
||||
|
||||
@frappe.whitelist()
|
||||
def build_tree_from_json(chart_template, chart_data=None):
|
||||
def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=False):
|
||||
''' get chart template from its folder and parse the json to be rendered as tree '''
|
||||
chart = chart_data or get_chart(chart_template)
|
||||
|
||||
@@ -226,9 +226,12 @@ def build_tree_from_json(chart_template, chart_data=None):
|
||||
''' recursively called to form a parent-child based list of dict from chart template '''
|
||||
for account_name, child in iteritems(children):
|
||||
account = {}
|
||||
if account_name in ["account_number", "account_type",\
|
||||
if account_name in ["account_name", "account_number", "account_type",\
|
||||
"root_type", "is_group", "tax_rate"]: continue
|
||||
|
||||
if from_coa_importer:
|
||||
account_name = child['account_name']
|
||||
|
||||
account['parent_account'] = parent
|
||||
account['expandable'] = True if identify_is_group(child) else False
|
||||
account['value'] = (cstr(child.get('account_number')).strip() + ' - ' + account_name) \
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
"default": "0",
|
||||
"fieldname": "automatically_fetch_payment_terms",
|
||||
"fieldtype": "Check",
|
||||
"label": "Automatically Fetch Payment Terms"
|
||||
"label": "Automatically Fetch Payment Terms from Order"
|
||||
},
|
||||
{
|
||||
"description": "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ",
|
||||
@@ -283,7 +283,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-19 11:17:38.788054",
|
||||
"modified": "2021-10-11 17:42:36.427699",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -69,7 +69,7 @@ def import_coa(file_name, company):
|
||||
|
||||
frappe.local.flags.ignore_root_company_validation = True
|
||||
forest = build_forest(data)
|
||||
create_charts(company, custom_chart=forest)
|
||||
create_charts(company, custom_chart=forest, from_coa_importer=True)
|
||||
|
||||
# trigger on_update for company to reset default accounts
|
||||
set_default_accounts(company)
|
||||
@@ -148,7 +148,7 @@ def get_coa(doctype, parent, is_root=False, file_name=None, for_validate=0):
|
||||
|
||||
if not for_validate:
|
||||
forest = build_forest(data)
|
||||
accounts = build_tree_from_json("", chart_data=forest) # returns a list of dict in a tree render-able form
|
||||
accounts = build_tree_from_json("", chart_data=forest, from_coa_importer=True) # returns a list of dict in a tree render-able form
|
||||
|
||||
# filter out to show data for the selected node only
|
||||
accounts = [d for d in accounts if d['parent_account']==parent]
|
||||
@@ -212,11 +212,14 @@ def build_forest(data):
|
||||
if not account_name:
|
||||
error_messages.append("Row {0}: Please enter Account Name".format(line_no))
|
||||
|
||||
name = account_name
|
||||
if account_number:
|
||||
account_number = cstr(account_number).strip()
|
||||
account_name = "{} - {}".format(account_number, account_name)
|
||||
|
||||
charts_map[account_name] = {}
|
||||
charts_map[account_name]['account_name'] = name
|
||||
if account_number: charts_map[account_name]["account_number"] = account_number
|
||||
if cint(is_group) == 1: charts_map[account_name]["is_group"] = is_group
|
||||
if account_type: charts_map[account_name]["account_type"] = account_type
|
||||
if root_type: charts_map[account_name]["root_type"] = root_type
|
||||
|
||||
@@ -16,7 +16,7 @@ class LoyaltyPointEntry(Document):
|
||||
|
||||
def get_loyalty_point_entries(customer, loyalty_program, company, expiry_date=None):
|
||||
if not expiry_date:
|
||||
date = today()
|
||||
expiry_date = today()
|
||||
|
||||
return frappe.db.sql('''
|
||||
select name, loyalty_points, expiry_date, loyalty_program_tier, invoice_type, invoice
|
||||
|
||||
@@ -390,6 +390,9 @@ class PaymentEntry(AccountsController):
|
||||
invoice_paid_amount_map[invoice_key]['discounted_amt'] = ref.total_amount * (term.discount / 100)
|
||||
|
||||
for key, allocated_amount in iteritems(invoice_payment_amount_map):
|
||||
if not invoice_paid_amount_map.get(key):
|
||||
frappe.throw(_('Payment term {0} not used in {1}').format(key[0], key[1]))
|
||||
|
||||
outstanding = flt(invoice_paid_amount_map.get(key, {}).get('outstanding'))
|
||||
discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get('discounted_amt'))
|
||||
|
||||
@@ -502,12 +505,13 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def validate_received_amount(self):
|
||||
if self.paid_from_account_currency == self.paid_to_account_currency:
|
||||
if self.paid_amount != self.received_amount:
|
||||
frappe.throw(_("Received Amount should be same as Paid Amount"))
|
||||
if self.paid_amount < self.received_amount:
|
||||
frappe.throw(_("Received Amount cannot be greater than Paid Amount"))
|
||||
|
||||
def set_received_amount(self):
|
||||
self.base_received_amount = self.base_paid_amount
|
||||
if self.paid_from_account_currency == self.paid_to_account_currency:
|
||||
if self.paid_from_account_currency == self.paid_to_account_currency \
|
||||
and not self.payment_type == 'Internal Transfer':
|
||||
self.received_amount = self.paid_amount
|
||||
|
||||
def set_amounts_after_tax(self):
|
||||
@@ -709,10 +713,14 @@ class PaymentEntry(AccountsController):
|
||||
dr_or_cr = "credit" if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit"
|
||||
|
||||
for d in self.get("references"):
|
||||
cost_center = self.cost_center
|
||||
if d.reference_doctype == "Sales Invoice" and not cost_center:
|
||||
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
|
||||
gle = party_gl_dict.copy()
|
||||
gle.update({
|
||||
"against_voucher_type": d.reference_doctype,
|
||||
"against_voucher": d.reference_name
|
||||
"against_voucher": d.reference_name,
|
||||
"cost_center": cost_center
|
||||
})
|
||||
|
||||
allocated_amount_in_company_currency = flt(flt(d.allocated_amount) * flt(d.exchange_rate),
|
||||
|
||||
@@ -52,21 +52,35 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
|
||||
|
||||
refresh: function() {
|
||||
this.frm.disable_save();
|
||||
this.frm.set_df_property('invoices', 'cannot_delete_rows', true);
|
||||
this.frm.set_df_property('payments', 'cannot_delete_rows', true);
|
||||
this.frm.set_df_property('allocation', 'cannot_delete_rows', true);
|
||||
|
||||
this.frm.set_df_property('invoices', 'cannot_add_rows', true);
|
||||
this.frm.set_df_property('payments', 'cannot_add_rows', true);
|
||||
this.frm.set_df_property('allocation', 'cannot_add_rows', true);
|
||||
|
||||
|
||||
if (this.frm.doc.receivable_payable_account) {
|
||||
this.frm.add_custom_button(__('Get Unreconciled Entries'), () =>
|
||||
this.frm.trigger("get_unreconciled_entries")
|
||||
);
|
||||
this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'primary');
|
||||
}
|
||||
if (this.frm.doc.invoices.length && this.frm.doc.payments.length) {
|
||||
this.frm.add_custom_button(__('Allocate'), () =>
|
||||
this.frm.trigger("allocate")
|
||||
);
|
||||
this.frm.change_custom_button_type('Allocate', null, 'primary');
|
||||
this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'default');
|
||||
}
|
||||
if (this.frm.doc.allocation.length) {
|
||||
this.frm.add_custom_button(__('Reconcile'), () =>
|
||||
this.frm.trigger("reconcile")
|
||||
);
|
||||
this.frm.change_custom_button_type('Reconcile', null, 'primary');
|
||||
this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'default');
|
||||
this.frm.change_custom_button_type('Allocate', null, 'default');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -12,15 +12,16 @@
|
||||
"receivable_payable_account",
|
||||
"col_break1",
|
||||
"from_invoice_date",
|
||||
"to_invoice_date",
|
||||
"minimum_invoice_amount",
|
||||
"maximum_invoice_amount",
|
||||
"invoice_limit",
|
||||
"column_break_13",
|
||||
"from_payment_date",
|
||||
"to_payment_date",
|
||||
"minimum_invoice_amount",
|
||||
"minimum_payment_amount",
|
||||
"column_break_11",
|
||||
"to_invoice_date",
|
||||
"to_payment_date",
|
||||
"maximum_invoice_amount",
|
||||
"maximum_payment_amount",
|
||||
"column_break_13",
|
||||
"invoice_limit",
|
||||
"payment_limit",
|
||||
"bank_cash_account",
|
||||
"sec_break1",
|
||||
@@ -79,6 +80,7 @@
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.payments).length || (doc.invoices).length",
|
||||
"description": "If you need to reconcile particular transactions against each other, then please select accordingly. If not, all the transactions will be allocated in FIFO order.",
|
||||
"fieldname": "sec_break1",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Unreconciled Entries"
|
||||
@@ -163,6 +165,7 @@
|
||||
"label": "Maximum Payment Amount"
|
||||
},
|
||||
{
|
||||
"description": "System will fetch all the entries if limit value is zero.",
|
||||
"fieldname": "payment_limit",
|
||||
"fieldtype": "Int",
|
||||
"label": "Payment Limit"
|
||||
@@ -171,13 +174,17 @@
|
||||
"fieldname": "maximum_invoice_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Maximum Invoice Amount"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"icon": "icon-resize-horizontal",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-30 13:05:51.977861",
|
||||
"modified": "2021-10-04 20:27:11.114194",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation",
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
"section_break_6",
|
||||
"allocated_amount",
|
||||
"unreconciled_amount",
|
||||
"amount",
|
||||
"column_break_8",
|
||||
"amount",
|
||||
"is_advance",
|
||||
"section_break_5",
|
||||
"difference_amount",
|
||||
@@ -127,12 +127,13 @@
|
||||
"fieldname": "reference_row",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Reference Row"
|
||||
"label": "Reference Row",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-20 17:23:09.455803",
|
||||
"modified": "2021-10-06 11:48:59.616562",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation Allocation",
|
||||
|
||||
@@ -33,7 +33,9 @@ class TestPOSProfile(unittest.TestCase):
|
||||
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
|
||||
def get_customers_list(pos_profile={}):
|
||||
def get_customers_list(pos_profile=None):
|
||||
if pos_profile is None:
|
||||
pos_profile = {}
|
||||
cond = "1=1"
|
||||
customer_groups = []
|
||||
if pos_profile.get('customer_groups'):
|
||||
|
||||
@@ -398,7 +398,9 @@ def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules):
|
||||
pricing_rules[0].apply_rule_on_other_items = items
|
||||
return pricing_rules
|
||||
|
||||
def get_qty_amount_data_for_cumulative(pr_doc, doc, items=[]):
|
||||
def get_qty_amount_data_for_cumulative(pr_doc, doc, items=None):
|
||||
if items is None:
|
||||
items = []
|
||||
sum_qty, sum_amt = [0, 0]
|
||||
doctype = doc.get('parenttype') or doc.doctype
|
||||
|
||||
|
||||
@@ -69,7 +69,9 @@ class PromotionalScheme(Document):
|
||||
{'promotional_scheme': self.name}):
|
||||
frappe.delete_doc('Pricing Rule', rule.name)
|
||||
|
||||
def get_pricing_rules(doc, rules = {}):
|
||||
def get_pricing_rules(doc, rules=None):
|
||||
if rules is None:
|
||||
rules = {}
|
||||
new_doc = []
|
||||
for child_doc, fields in {'price_discount_slabs': price_discount_fields,
|
||||
'product_discount_slabs': product_discount_fields}.items():
|
||||
@@ -78,7 +80,9 @@ def get_pricing_rules(doc, rules = {}):
|
||||
|
||||
return new_doc
|
||||
|
||||
def _get_pricing_rules(doc, child_doc, discount_fields, rules = {}):
|
||||
def _get_pricing_rules(doc, child_doc, discount_fields, rules=None):
|
||||
if rules is None:
|
||||
rules = {}
|
||||
new_doc = []
|
||||
args = get_args_for_pricing_rule(doc)
|
||||
applicable_for = frappe.scrub(doc.get('applicable_for'))
|
||||
|
||||
@@ -149,16 +149,18 @@
|
||||
"cb_17",
|
||||
"hold_comment",
|
||||
"more_info",
|
||||
"status",
|
||||
"inter_company_invoice_reference",
|
||||
"represents_company",
|
||||
"column_break_147",
|
||||
"is_internal_supplier",
|
||||
"accounting_details_section",
|
||||
"credit_to",
|
||||
"party_account_currency",
|
||||
"is_opening",
|
||||
"against_expense_account",
|
||||
"column_break_63",
|
||||
"unrealized_profit_loss_account",
|
||||
"status",
|
||||
"inter_company_invoice_reference",
|
||||
"is_internal_supplier",
|
||||
"represents_company",
|
||||
"remarks",
|
||||
"subscription_section",
|
||||
"from_date",
|
||||
@@ -1171,6 +1173,15 @@
|
||||
"options": "fa fa-file-text",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "supplier.is_internal_supplier",
|
||||
"fieldname": "is_internal_supplier",
|
||||
"fieldtype": "Check",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Is Internal Supplier",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "credit_to",
|
||||
"fieldtype": "Link",
|
||||
@@ -1196,7 +1207,7 @@
|
||||
"default": "No",
|
||||
"fieldname": "is_opening",
|
||||
"fieldtype": "Select",
|
||||
"label": "Is Opening",
|
||||
"label": "Is Opening Entry",
|
||||
"oldfieldname": "is_opening",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "No\nYes",
|
||||
@@ -1298,15 +1309,6 @@
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "supplier.is_internal_supplier",
|
||||
"fieldname": "is_internal_supplier",
|
||||
"fieldtype": "Check",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Is Internal Supplier",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_category",
|
||||
"fieldtype": "Link",
|
||||
@@ -1395,13 +1397,24 @@
|
||||
"hidden": 1,
|
||||
"label": "Ignore Default Payment Terms Template",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Details",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_147",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-28 13:10:28.351810",
|
||||
"modified": "2021-10-12 20:55:16.145651",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -15,6 +15,7 @@ from erpnext.accounts.deferred_revenue import validate_service_stop_date
|
||||
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
check_if_return_invoice_linked_with_payment_entry,
|
||||
get_total_in_party_account_currency,
|
||||
is_overdue,
|
||||
unlink_inter_company_doc,
|
||||
update_linked_doc,
|
||||
@@ -1147,6 +1148,7 @@ class PurchaseInvoice(BuyingController):
|
||||
return
|
||||
|
||||
outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount"))
|
||||
total = get_total_in_party_account_currency(self)
|
||||
|
||||
if not status:
|
||||
if self.docstatus == 2:
|
||||
@@ -1154,9 +1156,9 @@ class PurchaseInvoice(BuyingController):
|
||||
elif self.docstatus == 1:
|
||||
if self.is_internal_transfer():
|
||||
self.status = 'Internal Transfer'
|
||||
elif is_overdue(self):
|
||||
elif is_overdue(self, total):
|
||||
self.status = "Overdue"
|
||||
elif 0 < outstanding_amount < flt(self.grand_total, self.precision("grand_total")):
|
||||
elif 0 < outstanding_amount < total:
|
||||
self.status = "Partly Paid"
|
||||
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
|
||||
self.status = "Unpaid"
|
||||
|
||||
@@ -446,12 +446,15 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
|
||||
},
|
||||
|
||||
currency() {
|
||||
var me = this;
|
||||
this._super();
|
||||
$.each(cur_frm.doc.timesheets, function(i, d) {
|
||||
let row = frappe.get_doc(d.doctype, d.name)
|
||||
set_timesheet_detail_rate(row.doctype, row.name, cur_frm.doc.currency, row.timesheet_detail)
|
||||
});
|
||||
calculate_total_billing_amount(cur_frm)
|
||||
if (this.frm.doc.timesheets) {
|
||||
this.frm.doc.timesheets.forEach((d) => {
|
||||
let row = frappe.get_doc(d.doctype, d.name)
|
||||
set_timesheet_detail_rate(row.doctype, row.name, me.frm.doc.currency, row.timesheet_detail)
|
||||
});
|
||||
frm.trigger("calculate_timesheet_totals");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -999,7 +1002,7 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
|
||||
|
||||
frappe.ui.form.on("Sales Invoice Timesheet", {
|
||||
timesheets_remove(frm, cdt, cdn) {
|
||||
timesheets_remove(frm) {
|
||||
frm.trigger("calculate_timesheet_totals");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -124,6 +124,13 @@
|
||||
"total_advance",
|
||||
"outstanding_amount",
|
||||
"disable_rounded_total",
|
||||
"column_break4",
|
||||
"write_off_amount",
|
||||
"base_write_off_amount",
|
||||
"write_off_outstanding_amount_automatically",
|
||||
"column_break_74",
|
||||
"write_off_account",
|
||||
"write_off_cost_center",
|
||||
"advances_section",
|
||||
"allocate_advances_automatically",
|
||||
"get_advances",
|
||||
@@ -144,13 +151,6 @@
|
||||
"column_break_90",
|
||||
"change_amount",
|
||||
"account_for_change_amount",
|
||||
"column_break4",
|
||||
"write_off_amount",
|
||||
"base_write_off_amount",
|
||||
"write_off_outstanding_amount_automatically",
|
||||
"column_break_74",
|
||||
"write_off_account",
|
||||
"write_off_cost_center",
|
||||
"terms_section_break",
|
||||
"tc_name",
|
||||
"terms",
|
||||
@@ -161,14 +161,14 @@
|
||||
"column_break_84",
|
||||
"language",
|
||||
"more_information",
|
||||
"status",
|
||||
"inter_company_invoice_reference",
|
||||
"is_internal_customer",
|
||||
"represents_company",
|
||||
"customer_group",
|
||||
"campaign",
|
||||
"is_discounted",
|
||||
"col_break23",
|
||||
"status",
|
||||
"is_internal_customer",
|
||||
"is_discounted",
|
||||
"source",
|
||||
"more_info",
|
||||
"debit_to",
|
||||
@@ -1990,16 +1990,6 @@
|
||||
"label": "Additional Discount Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ignore_default_payment_terms_template",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Ignore Default Payment Terms Template",
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "dispatch_address_name",
|
||||
@@ -2015,6 +2005,14 @@
|
||||
"label": "Dispatch Address",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ignore_default_payment_terms_template",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Ignore Default Payment Terms Template",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_billing_hours",
|
||||
"fieldtype": "Float",
|
||||
@@ -2033,7 +2031,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2021-09-28 13:09:34.391799",
|
||||
"modified": "2021-10-11 20:19:38.667508",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
@@ -2088,4 +2086,4 @@
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
@@ -1296,12 +1296,20 @@ class SalesInvoice(SellingController):
|
||||
|
||||
serial_nos = item.serial_no or ""
|
||||
si_serial_nos = set(get_serial_nos(serial_nos))
|
||||
serial_no_diff = si_serial_nos - dn_serial_nos
|
||||
|
||||
if si_serial_nos - dn_serial_nos:
|
||||
frappe.throw(_("Serial Numbers in row {0} does not match with Delivery Note").format(item.idx))
|
||||
if serial_no_diff:
|
||||
dn_link = frappe.utils.get_link_to_form("Delivery Note", item.delivery_note)
|
||||
serial_no_msg = ", ".join(frappe.bold(d) for d in serial_no_diff)
|
||||
|
||||
msg = _("Row #{0}: The following Serial Nos are not present in Delivery Note {1}:").format(
|
||||
item.idx, dn_link)
|
||||
msg += " " + serial_no_msg
|
||||
|
||||
frappe.throw(msg=msg, title=_("Serial Nos Mismatch"))
|
||||
|
||||
if item.serial_no and cint(item.qty) != len(si_serial_nos):
|
||||
frappe.throw(_("Row {0}: {1} Serial numbers required for Item {2}. You have provided {3}.").format(
|
||||
frappe.throw(_("Row #{0}: {1} Serial numbers required for Item {2}. You have provided {3}.").format(
|
||||
item.idx, item.qty, item.item_code, len(si_serial_nos)))
|
||||
|
||||
def update_project(self):
|
||||
@@ -1470,6 +1478,7 @@ class SalesInvoice(SellingController):
|
||||
return
|
||||
|
||||
outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount"))
|
||||
total = get_total_in_party_account_currency(self)
|
||||
|
||||
if not status:
|
||||
if self.docstatus == 2:
|
||||
@@ -1477,9 +1486,9 @@ class SalesInvoice(SellingController):
|
||||
elif self.docstatus == 1:
|
||||
if self.is_internal_transfer():
|
||||
self.status = 'Internal Transfer'
|
||||
elif is_overdue(self):
|
||||
elif is_overdue(self, total):
|
||||
self.status = "Overdue"
|
||||
elif 0 < outstanding_amount < flt(self.grand_total, self.precision("grand_total")):
|
||||
elif 0 < outstanding_amount < total:
|
||||
self.status = "Partly Paid"
|
||||
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
|
||||
self.status = "Unpaid"
|
||||
@@ -1506,27 +1515,42 @@ class SalesInvoice(SellingController):
|
||||
if update:
|
||||
self.db_set('status', self.status, update_modified = update_modified)
|
||||
|
||||
def is_overdue(doc):
|
||||
outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount"))
|
||||
|
||||
def get_total_in_party_account_currency(doc):
|
||||
total_fieldname = (
|
||||
"grand_total"
|
||||
if doc.disable_rounded_total
|
||||
else "rounded_total"
|
||||
)
|
||||
if doc.party_account_currency != doc.currency:
|
||||
total_fieldname = "base_" + total_fieldname
|
||||
|
||||
return flt(doc.get(total_fieldname), doc.precision(total_fieldname))
|
||||
|
||||
def is_overdue(doc, total):
|
||||
outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount"))
|
||||
if outstanding_amount <= 0:
|
||||
return
|
||||
|
||||
grand_total = flt(doc.grand_total, doc.precision("grand_total"))
|
||||
nowdate = getdate()
|
||||
if doc.payment_schedule:
|
||||
# calculate payable amount till date
|
||||
payable_amount = sum(
|
||||
payment.payment_amount
|
||||
for payment in doc.payment_schedule
|
||||
if getdate(payment.due_date) < nowdate
|
||||
)
|
||||
today = getdate()
|
||||
if doc.get('is_pos') or not doc.get('payment_schedule'):
|
||||
return getdate(doc.due_date) < today
|
||||
|
||||
if (grand_total - outstanding_amount) < payable_amount:
|
||||
return True
|
||||
# calculate payable amount till date
|
||||
payment_amount_field = (
|
||||
"base_payment_amount"
|
||||
if doc.party_account_currency != doc.currency
|
||||
else "payment_amount"
|
||||
)
|
||||
|
||||
payable_amount = sum(
|
||||
payment.get(payment_amount_field)
|
||||
for payment in doc.payment_schedule
|
||||
if getdate(payment.due_date) < today
|
||||
)
|
||||
|
||||
return (total - outstanding_amount) < payable_amount
|
||||
|
||||
elif getdate(doc.due_date) < nowdate:
|
||||
return True
|
||||
|
||||
def get_discounting_status(sales_invoice):
|
||||
status = None
|
||||
|
||||
@@ -1085,8 +1085,6 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
actual_qty_1 = get_qty_after_transaction(item_code = "_Test Item", warehouse = "Stores - TCP1")
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
self.assertEqual(actual_qty_0 - 5, actual_qty_1)
|
||||
|
||||
# outgoing_rate
|
||||
@@ -2341,6 +2339,18 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
|
||||
def test_sales_invoice_submission_post_account_freezing_date(self):
|
||||
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', add_days(getdate(), 1))
|
||||
si = create_sales_invoice(do_not_save=True)
|
||||
si.posting_date = add_days(getdate(), 1)
|
||||
si.save()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, si.submit)
|
||||
si.posting_date = getdate()
|
||||
si.submit()
|
||||
|
||||
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
|
||||
|
||||
def get_sales_invoice_for_e_invoice():
|
||||
si = make_sales_invoice_for_ewaybill()
|
||||
si.naming_series = 'INV-2020-.#####'
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
"column_break_9",
|
||||
"billing_amount",
|
||||
"section_break_11",
|
||||
"timesheet_detail",
|
||||
"column_break_5",
|
||||
"time_sheet",
|
||||
"timesheet_detail",
|
||||
"column_break_13",
|
||||
"project_name"
|
||||
],
|
||||
"fields": [
|
||||
@@ -91,7 +91,6 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Totals"
|
||||
@@ -110,11 +109,15 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Project Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-15 18:37:08.084930",
|
||||
"modified": "2021-10-02 03:48:44.979777",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Timesheet",
|
||||
|
||||
@@ -33,7 +33,7 @@ class Subscription(Document):
|
||||
# update start just before the subscription doc is created
|
||||
self.update_subscription_period(self.start_date)
|
||||
|
||||
def update_subscription_period(self, date=None):
|
||||
def update_subscription_period(self, date=None, return_date=False):
|
||||
"""
|
||||
Subscription period is the period to be billed. This method updates the
|
||||
beginning of the billing period and end of the billing period.
|
||||
@@ -41,28 +41,41 @@ class Subscription(Document):
|
||||
The beginning of the billing period is represented in the doctype as
|
||||
`current_invoice_start` and the end of the billing period is represented
|
||||
as `current_invoice_end`.
|
||||
"""
|
||||
self.set_current_invoice_start(date)
|
||||
self.set_current_invoice_end()
|
||||
|
||||
def set_current_invoice_start(self, date=None):
|
||||
If return_date is True, it wont update the start and end dates.
|
||||
This is implemented to get the dates to check if is_current_invoice_generated
|
||||
"""
|
||||
This sets the date of the beginning of the current billing period.
|
||||
_current_invoice_start = self.get_current_invoice_start(date)
|
||||
_current_invoice_end = self.get_current_invoice_end(_current_invoice_start)
|
||||
|
||||
if return_date:
|
||||
return _current_invoice_start, _current_invoice_end
|
||||
|
||||
self.current_invoice_start = _current_invoice_start
|
||||
self.current_invoice_end = _current_invoice_end
|
||||
|
||||
def get_current_invoice_start(self, date=None):
|
||||
"""
|
||||
This returns the date of the beginning of the current billing period.
|
||||
If the `date` parameter is not given , it will be automatically set as today's
|
||||
date.
|
||||
"""
|
||||
if self.is_new_subscription() and self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date):
|
||||
self.current_invoice_start = add_days(self.trial_period_end, 1)
|
||||
elif self.trial_period_start and self.is_trialling():
|
||||
self.current_invoice_start = self.trial_period_start
|
||||
elif date:
|
||||
self.current_invoice_start = date
|
||||
else:
|
||||
self.current_invoice_start = nowdate()
|
||||
_current_invoice_start = None
|
||||
|
||||
def set_current_invoice_end(self):
|
||||
if self.is_new_subscription() and self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date):
|
||||
_current_invoice_start = add_days(self.trial_period_end, 1)
|
||||
elif self.trial_period_start and self.is_trialling():
|
||||
_current_invoice_start = self.trial_period_start
|
||||
elif date:
|
||||
_current_invoice_start = date
|
||||
else:
|
||||
_current_invoice_start = nowdate()
|
||||
|
||||
return _current_invoice_start
|
||||
|
||||
def get_current_invoice_end(self, date=None):
|
||||
"""
|
||||
This sets the date of the end of the current billing period.
|
||||
This returns the date of the end of the current billing period.
|
||||
|
||||
If the subscription is in trial period, it will be set as the end of the
|
||||
trial period.
|
||||
@@ -71,44 +84,47 @@ class Subscription(Document):
|
||||
current billing period where `x` is the billing interval from the
|
||||
`Subscription Plan` in the `Subscription`.
|
||||
"""
|
||||
if self.is_trialling() and getdate(self.current_invoice_start) < getdate(self.trial_period_end):
|
||||
self.current_invoice_end = self.trial_period_end
|
||||
_current_invoice_end = None
|
||||
|
||||
if self.is_trialling() and getdate(date) < getdate(self.trial_period_end):
|
||||
_current_invoice_end = self.trial_period_end
|
||||
else:
|
||||
billing_cycle_info = self.get_billing_cycle_data()
|
||||
if billing_cycle_info:
|
||||
if self.is_new_subscription() and getdate(self.start_date) < getdate(self.current_invoice_start):
|
||||
self.current_invoice_end = add_to_date(self.start_date, **billing_cycle_info)
|
||||
if self.is_new_subscription() and getdate(self.start_date) < getdate(date):
|
||||
_current_invoice_end = add_to_date(self.start_date, **billing_cycle_info)
|
||||
|
||||
# For cases where trial period is for an entire billing interval
|
||||
if getdate(self.current_invoice_end) < getdate(self.current_invoice_start):
|
||||
self.current_invoice_end = add_to_date(self.current_invoice_start, **billing_cycle_info)
|
||||
if getdate(self.current_invoice_end) < getdate(date):
|
||||
_current_invoice_end = add_to_date(date, **billing_cycle_info)
|
||||
else:
|
||||
self.current_invoice_end = add_to_date(self.current_invoice_start, **billing_cycle_info)
|
||||
_current_invoice_end = add_to_date(date, **billing_cycle_info)
|
||||
else:
|
||||
self.current_invoice_end = get_last_day(self.current_invoice_start)
|
||||
_current_invoice_end = get_last_day(date)
|
||||
|
||||
if self.follow_calendar_months:
|
||||
billing_info = self.get_billing_cycle_and_interval()
|
||||
billing_interval_count = billing_info[0]['billing_interval_count']
|
||||
calendar_months = get_calendar_months(billing_interval_count)
|
||||
calendar_month = 0
|
||||
current_invoice_end_month = getdate(self.current_invoice_end).month
|
||||
current_invoice_end_year = getdate(self.current_invoice_end).year
|
||||
current_invoice_end_month = getdate(_current_invoice_end).month
|
||||
current_invoice_end_year = getdate(_current_invoice_end).year
|
||||
|
||||
for month in calendar_months:
|
||||
if month <= current_invoice_end_month:
|
||||
calendar_month = month
|
||||
|
||||
if cint(calendar_month - billing_interval_count) <= 0 and \
|
||||
getdate(self.current_invoice_start).month != 1:
|
||||
getdate(date).month != 1:
|
||||
calendar_month = 12
|
||||
current_invoice_end_year -= 1
|
||||
|
||||
self.current_invoice_end = get_last_day(cstr(current_invoice_end_year) + '-' \
|
||||
+ cstr(calendar_month) + '-01')
|
||||
_current_invoice_end = get_last_day(cstr(current_invoice_end_year) + '-' + cstr(calendar_month) + '-01')
|
||||
|
||||
if self.end_date and getdate(self.current_invoice_end) > getdate(self.end_date):
|
||||
self.current_invoice_end = self.end_date
|
||||
if self.end_date and getdate(_current_invoice_end) > getdate(self.end_date):
|
||||
_current_invoice_end = self.end_date
|
||||
|
||||
return _current_invoice_end
|
||||
|
||||
@staticmethod
|
||||
def validate_plans_billing_cycle(billing_cycle_data):
|
||||
@@ -484,8 +500,9 @@ class Subscription(Document):
|
||||
|
||||
def is_current_invoice_generated(self):
|
||||
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 invoice and getdate(self.current_invoice_start) <= getdate(invoice.posting_date) <= getdate(self.current_invoice_end):
|
||||
if invoice and getdate(_current_start_date) <= getdate(invoice.posting_date) <= getdate(_current_end_date):
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -538,15 +555,15 @@ class Subscription(Document):
|
||||
else:
|
||||
self.set_status_grace_period()
|
||||
|
||||
if getdate() > getdate(self.current_invoice_end):
|
||||
self.update_subscription_period(add_days(self.current_invoice_end, 1))
|
||||
|
||||
# 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()
|
||||
or self.is_prepaid_to_invoice()):
|
||||
prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
|
||||
self.generate_invoice(prorate)
|
||||
|
||||
if getdate() > getdate(self.current_invoice_end):
|
||||
self.update_subscription_period(add_days(self.current_invoice_end, 1))
|
||||
|
||||
@staticmethod
|
||||
def is_paid(invoice):
|
||||
"""
|
||||
|
||||
@@ -18,6 +18,7 @@ from frappe.utils.data import (
|
||||
|
||||
from erpnext.accounts.doctype.subscription.subscription import get_prorata_factor
|
||||
|
||||
test_dependencies = ("UOM", "Item Group", "Item")
|
||||
|
||||
def create_plan():
|
||||
if not frappe.db.exists('Subscription Plan', '_Test Plan Name'):
|
||||
@@ -68,7 +69,6 @@ def create_plan():
|
||||
supplier.insert()
|
||||
|
||||
class TestSubscription(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
create_plan()
|
||||
|
||||
|
||||
@@ -203,6 +203,9 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
# then chargeable value is "prev invoices + advances" value which cross the threshold
|
||||
tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
|
||||
|
||||
if cint(tax_details.round_off_tax_amount):
|
||||
tax_amount = round(tax_amount)
|
||||
|
||||
return tax_amount, tax_deducted
|
||||
|
||||
def get_invoice_vouchers(parties, tax_details, company, party_type='Supplier'):
|
||||
@@ -322,9 +325,6 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
|
||||
else:
|
||||
tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0
|
||||
|
||||
if cint(tax_details.round_off_tax_amount):
|
||||
tds_amount = round(tds_amount)
|
||||
|
||||
return tds_amount
|
||||
|
||||
def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
||||
|
||||
@@ -293,7 +293,7 @@ def check_freezing_date(posting_date, adv_adj=False):
|
||||
if acc_frozen_upto:
|
||||
frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None,'frozen_accounts_modifier')
|
||||
if getdate(posting_date) <= getdate(acc_frozen_upto) \
|
||||
and not frozen_accounts_modifier in frappe.get_roles() or frappe.session.user == 'Administrator':
|
||||
and (frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == 'Administrator'):
|
||||
frappe.throw(_("You are not authorized to add or update entries before {0}").format(formatdate(acc_frozen_upto)))
|
||||
|
||||
def set_as_cancel(voucher_type, voucher_no):
|
||||
|
||||
@@ -139,9 +139,9 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_
|
||||
data["total"] = total
|
||||
return data
|
||||
|
||||
def get_account_type_based_gl_data(company, start_date, end_date, account_type, filters={}):
|
||||
def get_account_type_based_gl_data(company, start_date, end_date, account_type, filters=None):
|
||||
cond = ""
|
||||
filters = frappe._dict(filters)
|
||||
filters = frappe._dict(filters or {})
|
||||
|
||||
if filters.include_default_book_entries:
|
||||
company_fb = frappe.db.get_value("Company", company, 'default_finance_book')
|
||||
|
||||
@@ -103,8 +103,11 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
|
||||
column.is_tree = true;
|
||||
}
|
||||
|
||||
value = default_formatter(value, row, column, data);
|
||||
if (data && data.account && column.apply_currency_formatter) {
|
||||
data.currency = erpnext.get_currency(column.company_name);
|
||||
}
|
||||
|
||||
value = default_formatter(value, row, column, data);
|
||||
if (!data.parent_account) {
|
||||
value = $(`<span>${value}</span>`);
|
||||
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint, flt, getdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.report.balance_sheet.balance_sheet import (
|
||||
check_opening_balance,
|
||||
get_chart_data,
|
||||
get_provisional_profit_loss,
|
||||
)
|
||||
@@ -31,7 +33,7 @@ from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement
|
||||
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import (
|
||||
get_report_summary as get_pl_summary,
|
||||
)
|
||||
from erpnext.accounts.report.utils import convert_to_presentation_currency
|
||||
from erpnext.accounts.report.utils import convert, convert_to_presentation_currency
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -42,7 +44,7 @@ def execute(filters=None):
|
||||
|
||||
fiscal_year = get_fiscal_year_data(filters.get('from_fiscal_year'), filters.get('to_fiscal_year'))
|
||||
companies_column, companies = get_companies(filters)
|
||||
columns = get_columns(companies_column)
|
||||
columns = get_columns(companies_column, filters)
|
||||
|
||||
if filters.get('report') == "Balance Sheet":
|
||||
data, message, chart, report_summary = get_balance_sheet_data(fiscal_year, companies, columns, filters)
|
||||
@@ -73,21 +75,24 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
|
||||
provisional_profit_loss, total_credit = get_provisional_profit_loss(asset, liability, equity,
|
||||
companies, filters.get('company'), company_currency, True)
|
||||
|
||||
message, opening_balance = check_opening_balance(asset, liability, equity)
|
||||
message, opening_balance = prepare_companywise_opening_balance(asset, liability, equity, companies)
|
||||
|
||||
if opening_balance and round(opening_balance,2) !=0:
|
||||
unclosed ={
|
||||
if opening_balance:
|
||||
unclosed = {
|
||||
"account_name": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'",
|
||||
"account": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'",
|
||||
"warn_if_negative": True,
|
||||
"currency": company_currency
|
||||
}
|
||||
for company in companies:
|
||||
unclosed[company] = opening_balance
|
||||
if provisional_profit_loss:
|
||||
provisional_profit_loss[company] = provisional_profit_loss[company] - opening_balance
|
||||
|
||||
unclosed["total"]=opening_balance
|
||||
for company in companies:
|
||||
unclosed[company] = opening_balance.get(company)
|
||||
if provisional_profit_loss and provisional_profit_loss.get(company):
|
||||
provisional_profit_loss[company] = (
|
||||
flt(provisional_profit_loss[company]) - flt(opening_balance.get(company))
|
||||
)
|
||||
|
||||
unclosed["total"] = opening_balance.get(company)
|
||||
data.append(unclosed)
|
||||
|
||||
if provisional_profit_loss:
|
||||
@@ -102,6 +107,37 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
|
||||
|
||||
return data, message, chart, report_summary
|
||||
|
||||
def prepare_companywise_opening_balance(asset_data, liability_data, equity_data, companies):
|
||||
opening_balance = {}
|
||||
for company in companies:
|
||||
opening_value = 0
|
||||
|
||||
# opening_value = Aseet - liability - equity
|
||||
for data in [asset_data, liability_data, equity_data]:
|
||||
account_name = get_root_account_name(data[0].root_type, company)
|
||||
opening_value += get_opening_balance(account_name, data, company)
|
||||
|
||||
opening_balance[company] = opening_value
|
||||
|
||||
if opening_balance:
|
||||
return _("Previous Financial Year is not closed"), opening_balance
|
||||
|
||||
return '', {}
|
||||
|
||||
def get_opening_balance(account_name, data, company):
|
||||
for row in data:
|
||||
if row.get('account_name') == account_name:
|
||||
return row.get('company_wise_opening_bal', {}).get(company, 0.0)
|
||||
|
||||
def get_root_account_name(root_type, company):
|
||||
return frappe.get_all(
|
||||
'Account',
|
||||
fields=['account_name'],
|
||||
filters = {'root_type': root_type, 'is_group': 1,
|
||||
'company': company, 'parent_account': ('is', 'not set')},
|
||||
as_list=1
|
||||
)[0][0]
|
||||
|
||||
def get_profit_loss_data(fiscal_year, companies, columns, filters):
|
||||
income, expense, net_profit_loss = get_income_expense_data(companies, fiscal_year, filters)
|
||||
company_currency = get_company_currency(filters)
|
||||
@@ -193,30 +229,37 @@ def get_account_type_based_data(account_type, companies, fiscal_year, filters):
|
||||
data["total"] = total
|
||||
return data
|
||||
|
||||
def get_columns(companies):
|
||||
columns = [{
|
||||
"fieldname": "account",
|
||||
"label": _("Account"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Account",
|
||||
"width": 300
|
||||
}]
|
||||
|
||||
columns.append({
|
||||
"fieldname": "currency",
|
||||
"label": _("Currency"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Currency",
|
||||
"hidden": 1
|
||||
})
|
||||
def get_columns(companies, filters):
|
||||
columns = [
|
||||
{
|
||||
"fieldname": "account",
|
||||
"label": _("Account"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Account",
|
||||
"width": 300
|
||||
}, {
|
||||
"fieldname": "currency",
|
||||
"label": _("Currency"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Currency",
|
||||
"hidden": 1
|
||||
}
|
||||
]
|
||||
|
||||
for company in companies:
|
||||
apply_currency_formatter = 1 if not filters.presentation_currency else 0
|
||||
currency = filters.presentation_currency
|
||||
if not currency:
|
||||
currency = erpnext.get_company_currency(company)
|
||||
|
||||
columns.append({
|
||||
"fieldname": company,
|
||||
"label": company,
|
||||
"label": f'{company} ({currency})',
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 150
|
||||
"width": 150,
|
||||
"apply_currency_formatter": apply_currency_formatter,
|
||||
"company_name": company
|
||||
})
|
||||
|
||||
return columns
|
||||
@@ -236,6 +279,8 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
|
||||
start_date = filters.period_start_date if filters.report != 'Balance Sheet' else None
|
||||
end_date = filters.period_end_date
|
||||
|
||||
filters.end_date = end_date
|
||||
|
||||
gl_entries_by_account = {}
|
||||
for root in frappe.db.sql("""select lft, rgt from tabAccount
|
||||
where root_type=%s and ifnull(parent_account, '') = ''""", root_type, as_dict=1):
|
||||
@@ -244,9 +289,10 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
|
||||
end_date, root.lft, root.rgt, filters,
|
||||
gl_entries_by_account, accounts_by_name, accounts, ignore_closing_entries=False)
|
||||
|
||||
calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters)
|
||||
calculate_values(accounts_by_name, gl_entries_by_account, companies, filters, fiscal_year)
|
||||
accumulate_values_into_parents(accounts, accounts_by_name, companies)
|
||||
out = prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency)
|
||||
|
||||
out = prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters)
|
||||
|
||||
if out:
|
||||
add_total_row(out, root_type, balance_must_be, companies, company_currency)
|
||||
@@ -257,7 +303,10 @@ def get_company_currency(filters=None):
|
||||
return (filters.get('presentation_currency')
|
||||
or frappe.get_cached_value('Company', filters.company, "default_currency"))
|
||||
|
||||
def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters):
|
||||
def calculate_values(accounts_by_name, gl_entries_by_account, companies, filters, fiscal_year):
|
||||
start_date = (fiscal_year.year_start_date
|
||||
if filters.filter_based_on == 'Fiscal Year' else filters.period_start_date)
|
||||
|
||||
for entries in gl_entries_by_account.values():
|
||||
for entry in entries:
|
||||
if entry.account_number:
|
||||
@@ -266,15 +315,32 @@ def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_d
|
||||
account_name = entry.account_name
|
||||
|
||||
d = accounts_by_name.get(account_name)
|
||||
|
||||
if d:
|
||||
debit, credit = 0, 0
|
||||
for company in companies:
|
||||
# check if posting date is within the period
|
||||
if (entry.company == company or (filters.get('accumulated_in_group_company'))
|
||||
and entry.company in companies.get(company)):
|
||||
d[company] = d.get(company, 0.0) + flt(entry.debit) - flt(entry.credit)
|
||||
parent_company_currency = erpnext.get_company_currency(d.company)
|
||||
child_company_currency = erpnext.get_company_currency(entry.company)
|
||||
|
||||
debit, credit = flt(entry.debit), flt(entry.credit)
|
||||
|
||||
if (not filters.get('presentation_currency')
|
||||
and entry.company != company
|
||||
and parent_company_currency != child_company_currency
|
||||
and filters.get('accumulated_in_group_company')):
|
||||
debit = convert(debit, parent_company_currency, child_company_currency, filters.end_date)
|
||||
credit = convert(credit, parent_company_currency, child_company_currency, filters.end_date)
|
||||
|
||||
d[company] = d.get(company, 0.0) + flt(debit) - flt(credit)
|
||||
|
||||
if entry.posting_date < getdate(start_date):
|
||||
d['company_wise_opening_bal'][company] += (flt(debit) - flt(credit))
|
||||
|
||||
if entry.posting_date < getdate(start_date):
|
||||
d["opening_balance"] = d.get("opening_balance", 0.0) + flt(entry.debit) - flt(entry.credit)
|
||||
d["opening_balance"] = d.get("opening_balance", 0.0) + flt(debit) - flt(credit)
|
||||
|
||||
def accumulate_values_into_parents(accounts, accounts_by_name, companies):
|
||||
"""accumulate children's values in parent accounts"""
|
||||
@@ -282,17 +348,18 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
|
||||
if d.parent_account:
|
||||
account = d.parent_account_name
|
||||
|
||||
if not accounts_by_name.get(account):
|
||||
continue
|
||||
# if not accounts_by_name.get(account):
|
||||
# continue
|
||||
|
||||
for company in companies:
|
||||
accounts_by_name[account][company] = \
|
||||
accounts_by_name[account].get(company, 0.0) + d.get(company, 0.0)
|
||||
|
||||
accounts_by_name[account]['company_wise_opening_bal'][company] += d.get('company_wise_opening_bal', {}).get(company, 0.0)
|
||||
|
||||
accounts_by_name[account]["opening_balance"] = \
|
||||
accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0)
|
||||
|
||||
|
||||
def get_account_heads(root_type, companies, filters):
|
||||
accounts = get_accounts(root_type, filters)
|
||||
|
||||
@@ -353,7 +420,7 @@ def get_accounts(root_type, filters):
|
||||
`tabAccount` where company = %s and root_type = %s
|
||||
""" , (filters.get('company'), root_type), as_dict=1)
|
||||
|
||||
def prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency):
|
||||
def prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters):
|
||||
data = []
|
||||
|
||||
for d in accounts:
|
||||
@@ -367,10 +434,13 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com
|
||||
"parent_account": _(d.parent_account),
|
||||
"indent": flt(d.indent),
|
||||
"year_start_date": start_date,
|
||||
"root_type": d.root_type,
|
||||
"year_end_date": end_date,
|
||||
"currency": company_currency,
|
||||
"currency": filters.presentation_currency,
|
||||
"company_wise_opening_bal": d.company_wise_opening_bal,
|
||||
"opening_balance": d.get("opening_balance", 0.0) * (1 if balance_must_be == "Debit" else -1)
|
||||
})
|
||||
|
||||
for company in companies:
|
||||
if d.get(company) and balance_must_be == "Credit":
|
||||
# change sign based on Debit or Credit, since calculation is done using (debit - credit)
|
||||
@@ -385,6 +455,7 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com
|
||||
|
||||
row["has_value"] = has_value
|
||||
row["total"] = total
|
||||
|
||||
data.append(row)
|
||||
|
||||
return data
|
||||
@@ -447,6 +518,7 @@ def get_account_details(account):
|
||||
'is_group', 'account_name', 'account_number', 'parent_account', 'lft', 'rgt'], as_dict=1)
|
||||
|
||||
def validate_entries(key, entry, accounts_by_name, accounts):
|
||||
# If an account present in the child company and not in the parent company
|
||||
if key not in accounts_by_name:
|
||||
args = get_account_details(entry.account)
|
||||
|
||||
@@ -456,12 +528,23 @@ def validate_entries(key, entry, accounts_by_name, accounts):
|
||||
args.update({
|
||||
'lft': parent_args.lft + 1,
|
||||
'rgt': parent_args.rgt - 1,
|
||||
'indent': 3,
|
||||
'root_type': parent_args.root_type,
|
||||
'report_type': parent_args.report_type
|
||||
'report_type': parent_args.report_type,
|
||||
'parent_account_name': parent_args.account_name,
|
||||
'company_wise_opening_bal': defaultdict(float)
|
||||
})
|
||||
|
||||
accounts_by_name.setdefault(key, args)
|
||||
accounts.append(args)
|
||||
|
||||
idx = len(accounts)
|
||||
# To identify parent account index
|
||||
for index, row in enumerate(accounts):
|
||||
if row.parent_account_name == args.parent_account_name:
|
||||
idx = index
|
||||
break
|
||||
|
||||
accounts.insert(idx+1, args)
|
||||
|
||||
def get_additional_conditions(from_date, ignore_closing_entries, filters):
|
||||
additional_conditions = []
|
||||
@@ -491,7 +574,6 @@ def add_total_row(out, root_type, balance_must_be, companies, company_currency):
|
||||
for company in companies:
|
||||
total_row.setdefault(company, 0.0)
|
||||
total_row[company] += row.get(company, 0.0)
|
||||
row[company] = 0.0
|
||||
|
||||
total_row.setdefault("total", 0.0)
|
||||
total_row["total"] += flt(row["total"])
|
||||
@@ -511,6 +593,7 @@ def filter_accounts(accounts, depth=10):
|
||||
account_name = d.account_number + ' - ' + d.account_name
|
||||
else:
|
||||
account_name = d.account_name
|
||||
d['company_wise_opening_bal'] = defaultdict(float)
|
||||
accounts_by_name[account_name] = d
|
||||
|
||||
parent_children_map.setdefault(d.parent_account or None, []).append(d)
|
||||
|
||||
@@ -421,8 +421,6 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
update_value_in_dict(totals, 'closing', gle)
|
||||
|
||||
elif gle.posting_date <= to_date:
|
||||
update_value_in_dict(gle_map[gle.get(group_by)].totals, 'total', gle)
|
||||
update_value_in_dict(totals, 'total', gle)
|
||||
if filters.get("group_by") != 'Group by Voucher (Consolidated)':
|
||||
gle_map[gle.get(group_by)].entries.append(gle)
|
||||
elif filters.get("group_by") == 'Group by Voucher (Consolidated)':
|
||||
@@ -436,10 +434,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
else:
|
||||
update_value_in_dict(consolidated_gle, key, gle)
|
||||
|
||||
update_value_in_dict(gle_map[gle.get(group_by)].totals, 'closing', gle)
|
||||
update_value_in_dict(totals, 'closing', gle)
|
||||
|
||||
for key, value in consolidated_gle.items():
|
||||
update_value_in_dict(gle_map[value.get(group_by)].totals, 'total', value)
|
||||
update_value_in_dict(totals, 'total', value)
|
||||
update_value_in_dict(gle_map[value.get(group_by)].totals, 'closing', value)
|
||||
update_value_in_dict(totals, 'closing', value)
|
||||
entries.append(value)
|
||||
|
||||
return totals, entries
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from json import loads
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
from frappe import _, throw
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.utils import cint, cstr, flt, formatdate, get_number_format_info, getdate, now, nowdate
|
||||
from six import string_types
|
||||
|
||||
import erpnext
|
||||
|
||||
@@ -787,16 +790,28 @@ def get_children(doctype, parent, company, is_root=False):
|
||||
|
||||
if doctype == 'Account':
|
||||
sort_accounts(acc, is_root, key="value")
|
||||
company_currency = frappe.get_cached_value('Company', company, "default_currency")
|
||||
for each in acc:
|
||||
each["company_currency"] = company_currency
|
||||
each["balance"] = flt(get_balance_on(each.get("value"), in_account_currency=False, company=company))
|
||||
|
||||
if each.account_currency != company_currency:
|
||||
each["balance_in_account_currency"] = flt(get_balance_on(each.get("value"), company=company))
|
||||
|
||||
return acc
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_account_balances(accounts, company):
|
||||
|
||||
if isinstance(accounts, string_types):
|
||||
accounts = loads(accounts)
|
||||
|
||||
if not accounts:
|
||||
return []
|
||||
|
||||
company_currency = frappe.get_cached_value("Company", company, "default_currency")
|
||||
|
||||
for account in accounts:
|
||||
account["company_currency"] = company_currency
|
||||
account["balance"] = flt(get_balance_on(account["value"], in_account_currency=False, company=company))
|
||||
if account["account_currency"] and account["account_currency"] != company_currency:
|
||||
account["balance_in_account_currency"] = flt(get_balance_on(account["value"], company=company))
|
||||
|
||||
return accounts
|
||||
|
||||
def create_payment_gateway_account(gateway, payment_channel="Email"):
|
||||
from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ class Asset(AccountsController):
|
||||
start = self.clear_depreciation_schedule()
|
||||
|
||||
# value_after_depreciation - current Asset value
|
||||
if d.value_after_depreciation:
|
||||
if self.docstatus == 1 and d.value_after_depreciation:
|
||||
value_after_depreciation = (flt(d.value_after_depreciation) -
|
||||
flt(self.opening_accumulated_depreciation))
|
||||
else:
|
||||
|
||||
@@ -682,6 +682,27 @@ class TestAsset(unittest.TestCase):
|
||||
# reset indian company
|
||||
frappe.flags.company = company_flag
|
||||
|
||||
def test_expected_value_change(self):
|
||||
"""
|
||||
tests if changing `expected_value_after_useful_life`
|
||||
affects `value_after_depreciation`
|
||||
"""
|
||||
|
||||
asset = create_asset(calculate_depreciation=1)
|
||||
asset.opening_accumulated_depreciation = 2000
|
||||
asset.number_of_depreciations_booked = 1
|
||||
|
||||
asset.finance_books[0].expected_value_after_useful_life = 100
|
||||
asset.save()
|
||||
asset.reload()
|
||||
self.assertEquals(asset.finance_books[0].value_after_depreciation, 98000.0)
|
||||
|
||||
# changing expected_value_after_useful_life shouldn't affect value_after_depreciation
|
||||
asset.finance_books[0].expected_value_after_useful_life = 200
|
||||
asset.save()
|
||||
asset.reload()
|
||||
self.assertEquals(asset.finance_books[0].value_after_depreciation, 98000.0)
|
||||
|
||||
def create_asset_data():
|
||||
if not frappe.db.exists("Asset Category", "Computers"):
|
||||
create_asset_category()
|
||||
|
||||
@@ -22,7 +22,7 @@ class TestAssetRepair(unittest.TestCase):
|
||||
frappe.db.sql("delete from `tabTax Rule`")
|
||||
|
||||
def test_update_status(self):
|
||||
asset = create_asset()
|
||||
asset = create_asset(submit=1)
|
||||
initial_status = asset.status
|
||||
asset_repair = create_asset_repair(asset = asset)
|
||||
|
||||
@@ -76,7 +76,7 @@ class TestAssetRepair(unittest.TestCase):
|
||||
self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity)
|
||||
|
||||
def test_increase_in_asset_value_due_to_stock_consumption(self):
|
||||
asset = create_asset(calculate_depreciation = 1)
|
||||
asset = create_asset(calculate_depreciation = 1, submit=1)
|
||||
initial_asset_value = get_asset_value(asset)
|
||||
asset_repair = create_asset_repair(asset= asset, stock_consumption = 1, submit = 1)
|
||||
asset.reload()
|
||||
@@ -85,7 +85,7 @@ class TestAssetRepair(unittest.TestCase):
|
||||
self.assertEqual(asset_repair.stock_items[0].total_value, increase_in_asset_value)
|
||||
|
||||
def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self):
|
||||
asset = create_asset(calculate_depreciation = 1)
|
||||
asset = create_asset(calculate_depreciation = 1, submit=1)
|
||||
initial_asset_value = get_asset_value(asset)
|
||||
asset_repair = create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1)
|
||||
asset.reload()
|
||||
@@ -103,7 +103,7 @@ class TestAssetRepair(unittest.TestCase):
|
||||
self.assertEqual(asset_repair.name, gl_entry.voucher_no)
|
||||
|
||||
def test_increase_in_asset_life(self):
|
||||
asset = create_asset(calculate_depreciation = 1)
|
||||
asset = create_asset(calculate_depreciation = 1, submit=1)
|
||||
initial_num_of_depreciations = num_of_depreciations(asset)
|
||||
create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1)
|
||||
asset.reload()
|
||||
@@ -126,7 +126,7 @@ def create_asset_repair(**args):
|
||||
if args.asset:
|
||||
asset = args.asset
|
||||
else:
|
||||
asset = create_asset(is_existing_asset = 1)
|
||||
asset = create_asset(is_existing_asset = 1, submit=1)
|
||||
asset_repair = frappe.new_doc("Asset Repair")
|
||||
asset_repair.update({
|
||||
"asset": asset.name,
|
||||
|
||||
@@ -11,7 +11,7 @@ frappe.tour['Buying Settings'] = [
|
||||
{
|
||||
fieldname: "supp_master_name",
|
||||
title: "Supplier Naming By",
|
||||
description: __("By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a ") + "<a href='https://docs.erpnext.com/docs/user/manual/en/setting-up/settings/naming-series' target='_blank'>Naming Series</a>" + __(" choose the 'Naming Series' option."),
|
||||
description: __("By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a <a href='https://docs.erpnext.com/docs/user/manual/en/setting-up/settings/naming-series' target='_blank'>Naming Series</a> choose the 'Naming Series' option."),
|
||||
},
|
||||
{
|
||||
fieldname: "buying_price_list",
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"creation": "2021-07-28 11:51:42.319984",
|
||||
"docstatus": 0,
|
||||
"doctype": "Form Tour",
|
||||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"modified": "2021-10-05 13:06:56.414584",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
"owner": "Administrator",
|
||||
"reference_doctype": "Buying Settings",
|
||||
"save_on_complete": 0,
|
||||
"steps": [
|
||||
{
|
||||
"description": "When a Supplier is saved, system generates a unique identity or name for that Supplier which can be used to refer the Supplier in various Buying transactions.",
|
||||
"field": "",
|
||||
"fieldname": "supp_master_name",
|
||||
"fieldtype": "Select",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Supplier Naming By",
|
||||
"parent_field": "",
|
||||
"position": "Bottom",
|
||||
"title": "Supplier Naming By"
|
||||
},
|
||||
{
|
||||
"description": "Configure what should be the default value of Supplier Group when creating a new Supplier.",
|
||||
"field": "",
|
||||
"fieldname": "supplier_group",
|
||||
"fieldtype": "Link",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Default Supplier Group",
|
||||
"parent_field": "",
|
||||
"position": "Right",
|
||||
"title": "Default Supplier Group"
|
||||
},
|
||||
{
|
||||
"description": "Item prices will be fetched from this Price List.",
|
||||
"field": "",
|
||||
"fieldname": "buying_price_list",
|
||||
"fieldtype": "Link",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Default Buying Price List",
|
||||
"parent_field": "",
|
||||
"position": "Bottom",
|
||||
"title": "Default Buying Price List"
|
||||
},
|
||||
{
|
||||
"description": "If this option is configured \"Yes\", ERPNext will prevent you from creating a Purchase Invoice or a Purchase Receipt directly without creating a Purchase Order first.",
|
||||
"field": "",
|
||||
"fieldname": "po_required",
|
||||
"fieldtype": "Select",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Is Purchase Order Required for Purchase Invoice & Receipt Creation?",
|
||||
"parent_field": "",
|
||||
"position": "Bottom",
|
||||
"title": "Purchase Order Required"
|
||||
},
|
||||
{
|
||||
"description": "If this option is configured \"Yes\", ERPNext will prevent you from creating a Purchase Invoice without creating a Purchase Receipt first.",
|
||||
"field": "",
|
||||
"fieldname": "pr_required",
|
||||
"fieldtype": "Select",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Is Purchase Receipt Required for Purchase Invoice Creation?",
|
||||
"parent_field": "",
|
||||
"position": "Bottom",
|
||||
"title": "Purchase Receipt Required"
|
||||
}
|
||||
],
|
||||
"title": "Buying Settings"
|
||||
}
|
||||
82
erpnext/buying/form_tour/purchase_order/purchase_order.json
Normal file
82
erpnext/buying/form_tour/purchase_order/purchase_order.json
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"creation": "2021-07-29 14:11:58.271113",
|
||||
"docstatus": 0,
|
||||
"doctype": "Form Tour",
|
||||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"modified": "2021-10-05 13:11:31.436135",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
"owner": "Administrator",
|
||||
"reference_doctype": "Purchase Order",
|
||||
"save_on_complete": 1,
|
||||
"steps": [
|
||||
{
|
||||
"description": "Select a Supplier",
|
||||
"field": "",
|
||||
"fieldname": "supplier",
|
||||
"fieldtype": "Link",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Supplier",
|
||||
"parent_field": "",
|
||||
"position": "Right",
|
||||
"title": "Supplier"
|
||||
},
|
||||
{
|
||||
"description": "Set the \"Required By\" date for the materials. This sets the \"Required By\" date for all the items.",
|
||||
"field": "",
|
||||
"fieldname": "schedule_date",
|
||||
"fieldtype": "Date",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Required By",
|
||||
"parent_field": "",
|
||||
"position": "Left",
|
||||
"title": "Required By"
|
||||
},
|
||||
{
|
||||
"description": "Items to be purchased can be added here.",
|
||||
"field": "",
|
||||
"fieldname": "items",
|
||||
"fieldtype": "Table",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Items",
|
||||
"parent_field": "",
|
||||
"position": "Bottom",
|
||||
"title": "Items Table"
|
||||
},
|
||||
{
|
||||
"child_doctype": "Purchase Order Item",
|
||||
"description": "Enter the Item Code.",
|
||||
"field": "",
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"has_next_condition": 1,
|
||||
"is_table_field": 1,
|
||||
"label": "Item Code",
|
||||
"next_step_condition": "eval: doc.item_code",
|
||||
"parent_field": "",
|
||||
"parent_fieldname": "items",
|
||||
"position": "Right",
|
||||
"title": "Item Code"
|
||||
},
|
||||
{
|
||||
"child_doctype": "Purchase Order Item",
|
||||
"description": "Enter the required quantity for the material.",
|
||||
"field": "",
|
||||
"fieldname": "qty",
|
||||
"fieldtype": "Float",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 1,
|
||||
"label": "Quantity",
|
||||
"parent_field": "",
|
||||
"parent_fieldname": "items",
|
||||
"position": "Bottom",
|
||||
"title": "Quantity"
|
||||
}
|
||||
],
|
||||
"title": "Purchase Order"
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/buying",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"modified": "2020-07-08 14:05:28.273641",
|
||||
"modified": "2021-08-24 18:13:42.463776",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying",
|
||||
@@ -28,23 +28,11 @@
|
||||
{
|
||||
"step": "Introduction to Buying"
|
||||
},
|
||||
{
|
||||
"step": "Create a Supplier"
|
||||
},
|
||||
{
|
||||
"step": "Setup your Warehouse"
|
||||
},
|
||||
{
|
||||
"step": "Create a Product"
|
||||
},
|
||||
{
|
||||
"step": "Create a Material Request"
|
||||
},
|
||||
{
|
||||
"step": "Create your first Purchase Order"
|
||||
},
|
||||
{
|
||||
"step": "Buying Settings"
|
||||
}
|
||||
],
|
||||
"subtitle": "Products, Purchases, Analysis, and more.",
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
{
|
||||
"action": "Create Entry",
|
||||
"action": "Show Form Tour",
|
||||
"action_label": "Let\u2019s create your first Material Request",
|
||||
"creation": "2020-05-15 14:39:09.818764",
|
||||
"description": "# Track Material Request\n\n\nAlso known as Purchase Request or an Indent, is a document identifying a requirement of a set of items (products or services) for various purposes like procurement, transfer, issue, or manufacturing. Once the Material Request is validated, a purchase manager can take the next actions for purchasing items like requesting RFQ from a supplier or directly placing an order with an identified Supplier.\n\n",
|
||||
"docstatus": 0,
|
||||
"doctype": "Onboarding Step",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"is_mandatory": 1,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2020-05-15 14:39:09.818764",
|
||||
"modified": "2021-08-24 18:08:08.347501",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Create a Material Request",
|
||||
"owner": "Administrator",
|
||||
"reference_document": "Material Request",
|
||||
"show_form_tour": 1,
|
||||
"show_full_form": 1,
|
||||
"title": "Create a Material Request",
|
||||
"title": "Track Material Request",
|
||||
"validate_action": 1
|
||||
}
|
||||
@@ -1,19 +1,21 @@
|
||||
{
|
||||
"action": "Create Entry",
|
||||
"action": "Show Form Tour",
|
||||
"action_label": "Let\u2019s create your first Purchase Order",
|
||||
"creation": "2020-05-12 18:17:49.976035",
|
||||
"description": "# Create first Purchase Order\n\nPurchase Order is at the heart of your buying transactions. In ERPNext, Purchase Order can can be created against a Purchase Material Request (indent) and Supplier Quotation as well. Purchase Orders is also linked to Purchase Receipt and Purchase Invoices, allowing you to keep a birds-eye view on your purchase deals.\n\n",
|
||||
"docstatus": 0,
|
||||
"doctype": "Onboarding Step",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"is_mandatory": 0,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2020-05-12 18:31:56.856112",
|
||||
"modified": "2021-08-24 18:08:08.936484",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Create your first Purchase Order",
|
||||
"owner": "Administrator",
|
||||
"reference_document": "Purchase Order",
|
||||
"show_form_tour": 0,
|
||||
"show_full_form": 0,
|
||||
"title": "Create your first Purchase Order",
|
||||
"title": "Create first Purchase Order",
|
||||
"validate_action": 1
|
||||
}
|
||||
@@ -1,19 +1,22 @@
|
||||
{
|
||||
"action": "Watch Video",
|
||||
"action": "Show Form Tour",
|
||||
"action_label": "Let\u2019s walk-through few Buying Settings",
|
||||
"creation": "2020-05-06 15:37:09.477765",
|
||||
"description": "# Buying Settings\n\n\nBuying module\u2019s features are highly configurable as per your business needs. Buying Settings is the place where you can set your preferences for:\n\n- Supplier naming and default values\n- Billing and shipping preference in buying transactions\n\n\n",
|
||||
"docstatus": 0,
|
||||
"doctype": "Onboarding Step",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"is_mandatory": 0,
|
||||
"is_single": 0,
|
||||
"is_single": 1,
|
||||
"is_skipped": 0,
|
||||
"modified": "2020-05-12 18:25:08.509900",
|
||||
"modified": "2021-08-24 18:08:08.345735",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Introduction to Buying",
|
||||
"owner": "Administrator",
|
||||
"show_full_form": 0,
|
||||
"title": "Introduction to Buying",
|
||||
"reference_document": "Buying Settings",
|
||||
"show_form_tour": 1,
|
||||
"show_full_form": 1,
|
||||
"title": "Buying Settings",
|
||||
"validate_action": 1,
|
||||
"video_url": "https://youtu.be/efFajTTQBa8"
|
||||
}
|
||||
@@ -45,7 +45,6 @@ class TestProcurementTracker(unittest.TestCase):
|
||||
pr = make_purchase_receipt(po.name)
|
||||
pr.get("items")[0].cost_center = "Main - _TPC"
|
||||
pr.submit()
|
||||
frappe.db.commit()
|
||||
date_obj = datetime.date(datetime.now())
|
||||
|
||||
po.load_from_db()
|
||||
|
||||
29
erpnext/change_log/v13/v13_13_0.md
Normal file
29
erpnext/change_log/v13/v13_13_0.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Version 13.13.0 Release Notes
|
||||
|
||||
### Features & Enhancements
|
||||
|
||||
- HR Module onboarding ([#25741](https://github.com/frappe/erpnext/pull/25741))
|
||||
- Tracking multiple rounds for the interview ([#25482](https://github.com/frappe/erpnext/pull/25482))
|
||||
- HSN based tax breakup table check in GST Settings (India Localization) ([#27907](https://github.com/frappe/erpnext/pull/27907))
|
||||
|
||||
### Fixes
|
||||
|
||||
- To improve stock transactions added indexes in stock queries and speed up bin updation ([#27758](https://github.com/frappe/erpnext/pull/27758))
|
||||
- Interstate internal transfer invoices not visible in GSTR-1 ([#27970](https://github.com/frappe/erpnext/pull/27970))
|
||||
- Account number and name incorrectly imported using COA importer ([#27967](https://github.com/frappe/erpnext/pull/27967))
|
||||
- Multiple fixes to timesheets ([#27775](https://github.com/frappe/erpnext/pull/27742))
|
||||
- Totals row incorrect value in GL Entry ([#27867](https://github.com/frappe/erpnext/pull/27867))
|
||||
- Sales Order delivery Date not getting set via data import ([#27862](https://github.com/frappe/erpnext/pull/27862))
|
||||
- Add cost center in gl entry for advance payment entry ([#27840](https://github.com/frappe/erpnext/pull/27840))
|
||||
- Item Variant selection empty popup on website ([#27924](https://github.com/frappe/erpnext/pull/27924))
|
||||
- Improve performance of fetching account balance in chart of accounts ([#27661](https://github.com/frappe/erpnext/pull/27661))
|
||||
- Chart Of Accounts import button not visible ([#27748](https://github.com/frappe/erpnext/pull/27748))
|
||||
- Website Items with same Item name unhandled, thumbnails missing ([#27720](https://github.com/frappe/erpnext/pull/27720))
|
||||
- Delete linked Transaction Deletion Record docs on deleting company ([#27785](https://github.com/frappe/erpnext/pull/27785))
|
||||
- Display appropriate message for Payment Term discrepancies in Payment Entry ([#27749](https://github.com/frappe/erpnext/pull/27749))
|
||||
- Updated buying onboarding tours. ([#27800](https://github.com/frappe/erpnext/pull/27800))
|
||||
- Fixed variant qty in BOM while making work order ([#27686](https://github.com/frappe/erpnext/pull/27686))
|
||||
- Availability slots display, disabled Practitioner Schedule ([#27812](https://github.com/frappe/erpnext/pull/27812))
|
||||
- Consolidated report not consider company currency ([#27863](https://github.com/frappe/erpnext/pull/27863))
|
||||
- Batch Number not copied from Purchase Receipt to Stock Entry ([#27794](https://github.com/frappe/erpnext/pull/27794))
|
||||
- Employee Leave Balance report should only consider ledgers of transaction type Leave Allocation ([#27728](https://github.com/frappe/erpnext/pull/27728))
|
||||
@@ -1691,17 +1691,58 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
|
||||
|
||||
def update_invoice_status():
|
||||
"""Updates status as Overdue for applicable invoices. Runs daily."""
|
||||
today = getdate()
|
||||
|
||||
for doctype in ("Sales Invoice", "Purchase Invoice"):
|
||||
frappe.db.sql("""
|
||||
update `tab{}` as dt set dt.status = 'Overdue'
|
||||
where dt.docstatus = 1
|
||||
and dt.status != 'Overdue'
|
||||
and dt.outstanding_amount > 0
|
||||
and (dt.grand_total - dt.outstanding_amount) <
|
||||
(select sum(payment_amount) from `tabPayment Schedule` as ps
|
||||
where ps.parent = dt.name and ps.due_date < %s)
|
||||
""".format(doctype), getdate())
|
||||
UPDATE `tab{doctype}` invoice SET invoice.status = 'Overdue'
|
||||
WHERE invoice.docstatus = 1
|
||||
AND invoice.status REGEXP '^Unpaid|^Partly Paid'
|
||||
AND invoice.outstanding_amount > 0
|
||||
AND (
|
||||
{or_condition}
|
||||
(
|
||||
(
|
||||
CASE
|
||||
WHEN invoice.party_account_currency = invoice.currency
|
||||
THEN (
|
||||
CASE
|
||||
WHEN invoice.disable_rounded_total
|
||||
THEN invoice.grand_total
|
||||
ELSE invoice.rounded_total
|
||||
END
|
||||
)
|
||||
ELSE (
|
||||
CASE
|
||||
WHEN invoice.disable_rounded_total
|
||||
THEN invoice.base_grand_total
|
||||
ELSE invoice.base_rounded_total
|
||||
END
|
||||
)
|
||||
END
|
||||
) - invoice.outstanding_amount
|
||||
) < (
|
||||
SELECT SUM(
|
||||
CASE
|
||||
WHEN invoice.party_account_currency = invoice.currency
|
||||
THEN ps.payment_amount
|
||||
ELSE ps.base_payment_amount
|
||||
END
|
||||
)
|
||||
FROM `tabPayment Schedule` ps
|
||||
WHERE ps.parent = invoice.name
|
||||
AND ps.due_date < %(today)s
|
||||
)
|
||||
)
|
||||
""".format(
|
||||
doctype=doctype,
|
||||
or_condition=(
|
||||
"invoice.is_pos AND invoice.due_date < %(today)s OR"
|
||||
if doctype == "Sales Invoice"
|
||||
else ""
|
||||
)
|
||||
), {"today": today}
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):
|
||||
|
||||
@@ -79,8 +79,15 @@ class StockController(AccountsController):
|
||||
def clean_serial_nos(self):
|
||||
for row in self.get("items"):
|
||||
if hasattr(row, "serial_no") and row.serial_no:
|
||||
# replace commas by linefeed and remove all spaces in string
|
||||
row.serial_no = row.serial_no.replace(",", "\n").replace(" ", "")
|
||||
# replace commas by linefeed
|
||||
row.serial_no = row.serial_no.replace(",", "\n")
|
||||
|
||||
# strip preceeding and succeeding spaces for each SN
|
||||
# (SN could have valid spaces in between e.g. SN - 123 - 2021)
|
||||
serial_no_list = row.serial_no.split("\n")
|
||||
serial_no_list = [sn.strip() for sn in serial_no_list]
|
||||
|
||||
row.serial_no = "\n".join(serial_no_list)
|
||||
|
||||
def get_gl_entries(self, warehouse_account=None, default_expense_account=None,
|
||||
default_cost_center=None):
|
||||
@@ -591,7 +598,7 @@ def future_sle_exists(args, sl_entries=None):
|
||||
|
||||
data = frappe.db.sql("""
|
||||
select item_code, warehouse, count(name) as total_row
|
||||
from `tabStock Ledger Entry`
|
||||
from `tabStock Ledger Entry` force index (item_warehouse)
|
||||
where
|
||||
({})
|
||||
and timestamp(posting_date, posting_time)
|
||||
|
||||
@@ -34,6 +34,7 @@ class Opportunity(TransactionBase):
|
||||
self.validate_item_details()
|
||||
self.validate_uom_is_integer("uom", "qty")
|
||||
self.validate_cust_name()
|
||||
self.map_fields()
|
||||
|
||||
if not self.title:
|
||||
self.title = self.customer_name
|
||||
@@ -41,6 +42,15 @@ class Opportunity(TransactionBase):
|
||||
if not self.with_items:
|
||||
self.items = []
|
||||
|
||||
def map_fields(self):
|
||||
for field in self.meta.fields:
|
||||
if not self.get(field.fieldname):
|
||||
try:
|
||||
value = frappe.db.get_value(self.opportunity_from, self.party_name, field.fieldname)
|
||||
frappe.db.set(self, field.fieldname, value)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
def make_new_lead_if_required(self):
|
||||
"""Set lead against new opportunity"""
|
||||
if (not self.get("party_name")) and self.contact_email:
|
||||
|
||||
@@ -41,7 +41,6 @@ class TestECommerceSettings(unittest.TestCase):
|
||||
|
||||
def test_tax_rule_validation(self):
|
||||
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")
|
||||
frappe.db.commit()
|
||||
|
||||
cart_settings = self.get_cart_settings()
|
||||
cart_settings.enabled = 1
|
||||
|
||||
@@ -147,7 +147,7 @@ class WebsiteItem(WebsiteGenerator):
|
||||
|
||||
def make_thumbnail(self):
|
||||
"""Make a thumbnail of `website_image`"""
|
||||
if frappe.flags.in_import:
|
||||
if frappe.flags.in_import or frappe.flags.in_migrate:
|
||||
return
|
||||
|
||||
import requests.exceptions
|
||||
|
||||
@@ -138,7 +138,9 @@ class Student(Document):
|
||||
enrollment.submit()
|
||||
return enrollment
|
||||
|
||||
def enroll_in_course(self, course_name, program_enrollment, enrollment_date=frappe.utils.datetime.datetime.now()):
|
||||
def enroll_in_course(self, course_name, program_enrollment, enrollment_date=None):
|
||||
if enrollment_date is None:
|
||||
enrollment_date = frappe.utils.datetime.datetime.now()
|
||||
try:
|
||||
enrollment = frappe.get_doc({
|
||||
"doctype": "Course Enrollment",
|
||||
|
||||
@@ -62,7 +62,9 @@ class InpatientRecord(Document):
|
||||
admit_patient(self, service_unit, check_in, expected_discharge)
|
||||
|
||||
@frappe.whitelist()
|
||||
def discharge(self, check_out=now_datetime()):
|
||||
def discharge(self, check_out=None):
|
||||
if not check_out:
|
||||
check_out = now_datetime()
|
||||
if (getdate(check_out) < getdate(self.admitted_datetime)):
|
||||
frappe.throw(_('Discharge date cannot be less than Admission date'))
|
||||
discharge_patient(self, check_out)
|
||||
|
||||
@@ -433,11 +433,12 @@ let check_and_set_availability = function(frm) {
|
||||
slot_html += `<br><span> <b> ${__('Maximum Capacity:')} </b> ${slot_info.service_unit_capacity} </span>`;
|
||||
}
|
||||
|
||||
slot_html += '</div><br><br>';
|
||||
slot_html += '</div><br>';
|
||||
|
||||
slot_html += slot_info.avail_slot.map(slot => {
|
||||
appointment_count = 0;
|
||||
disabled = false;
|
||||
count_class = tool_tip = '';
|
||||
start_str = slot.from_time;
|
||||
slot_start_time = moment(slot.from_time, 'HH:mm:ss');
|
||||
slot_end_time = moment(slot.to_time, 'HH:mm:ss');
|
||||
@@ -486,10 +487,11 @@ let check_and_set_availability = function(frm) {
|
||||
data-duration=${interval}
|
||||
data-service-unit="${slot_info.service_unit || ''}"
|
||||
style="margin: 0 10px 10px 0; width: auto;" ${disabled ? 'disabled="disabled"' : ""}
|
||||
data-toggle="tooltip" title="${tool_tip}">
|
||||
${start_str.substring(0, start_str.length - 3)}<br>
|
||||
<span class='badge ${count_class}'> ${count} </span>
|
||||
data-toggle="tooltip" title="${tool_tip || ''}">
|
||||
${start_str.substring(0, start_str.length - 3)}
|
||||
${slot_info.service_unit_capacity ? `<br><span class='badge ${count_class}'> ${count} </span>` : ''}
|
||||
</button>`;
|
||||
|
||||
}).join("");
|
||||
|
||||
if (slot_info.service_unit_capacity) {
|
||||
|
||||
@@ -354,7 +354,7 @@ def get_available_slots(practitioner_doc, date):
|
||||
validate_practitioner_schedules(schedule_entry, practitioner)
|
||||
practitioner_schedule = frappe.get_doc('Practitioner Schedule', schedule_entry.schedule)
|
||||
|
||||
if practitioner_schedule:
|
||||
if practitioner_schedule and not practitioner_schedule.disabled:
|
||||
available_slots = []
|
||||
for time_slot in practitioner_schedule.time_slots:
|
||||
if weekday == time_slot.day:
|
||||
|
||||
@@ -21,6 +21,7 @@ class TestPatientMedicalRecord(unittest.TestCase):
|
||||
def setUp(self):
|
||||
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
|
||||
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
|
||||
frappe.db.sql('delete from `tabPatient Appointment`')
|
||||
make_pos_profile()
|
||||
|
||||
def test_medical_record(self):
|
||||
|
||||
@@ -6,7 +6,7 @@ from __future__ import unicode_literals
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import flt, getdate, nowdate
|
||||
from frappe.utils import add_days, flt, getdate, nowdate
|
||||
|
||||
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import (
|
||||
create_appointment,
|
||||
@@ -33,10 +33,12 @@ class TestTherapyPlan(unittest.TestCase):
|
||||
self.assertEqual(plan.status, 'Not Started')
|
||||
|
||||
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company')
|
||||
session.start_date = getdate()
|
||||
frappe.get_doc(session).submit()
|
||||
self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'In Progress')
|
||||
|
||||
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company')
|
||||
session.start_date = add_days(getdate(), 1)
|
||||
frappe.get_doc(session).submit()
|
||||
self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed')
|
||||
|
||||
@@ -44,6 +46,7 @@ class TestTherapyPlan(unittest.TestCase):
|
||||
appointment = create_appointment(patient, practitioner, nowdate())
|
||||
|
||||
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name)
|
||||
session.start_date = add_days(getdate(), 2)
|
||||
session = frappe.get_doc(session)
|
||||
session.submit()
|
||||
self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed')
|
||||
|
||||
@@ -6,7 +6,7 @@ from __future__ import unicode_literals
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import flt, today
|
||||
from frappe.utils import flt
|
||||
|
||||
|
||||
class TherapyPlan(Document):
|
||||
@@ -63,8 +63,6 @@ def make_therapy_session(therapy_plan, patient, therapy_type, company, appointme
|
||||
therapy_session.exercises = therapy_type.exercises
|
||||
therapy_session.appointment = appointment
|
||||
|
||||
if frappe.flags.in_test:
|
||||
therapy_session.start_date = today()
|
||||
return therapy_session.as_dict()
|
||||
|
||||
|
||||
|
||||
@@ -611,7 +611,7 @@ def render_docs_as_html(docs):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def render_doc_as_html(doctype, docname, exclude_fields = []):
|
||||
def render_doc_as_html(doctype, docname, exclude_fields = None):
|
||||
"""
|
||||
Render document as HTML
|
||||
"""
|
||||
@@ -622,6 +622,9 @@ def render_doc_as_html(doctype, docname, exclude_fields = []):
|
||||
sec_on = has_data = False
|
||||
col_on = 0
|
||||
|
||||
if exclude_fields is None:
|
||||
exclude_fields = []
|
||||
|
||||
for df in meta.fields:
|
||||
# on section break append previous section and html to doc html
|
||||
if df.fieldtype == "Section Break":
|
||||
|
||||
@@ -338,6 +338,7 @@ scheduler_events = {
|
||||
"all": [
|
||||
"erpnext.projects.doctype.project.project.project_status_update_reminder",
|
||||
"erpnext.healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder",
|
||||
"erpnext.hr.doctype.interview.interview.send_interview_reminder",
|
||||
"erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts"
|
||||
],
|
||||
"hourly": [
|
||||
@@ -383,6 +384,7 @@ scheduler_events = {
|
||||
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
|
||||
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
|
||||
"erpnext.non_profit.doctype.membership.membership.set_expired_status"
|
||||
"erpnext.hr.doctype.interview.interview.send_daily_feedback_reminder"
|
||||
],
|
||||
"daily_long": [
|
||||
"erpnext.setup.doctype.email_digest.email_digest.send",
|
||||
|
||||
@@ -9,83 +9,86 @@ frappe.listview_settings['Attendance'] = {
|
||||
return [__(doc.status), "orange", "status,=," + doc.status];
|
||||
}
|
||||
},
|
||||
|
||||
onload: function(list_view) {
|
||||
let me = this;
|
||||
const months = moment.months()
|
||||
list_view.page.add_inner_button( __("Mark Attendance"), function() {
|
||||
const months = moment.months();
|
||||
list_view.page.add_inner_button(__("Mark Attendance"), function() {
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __("Mark Attendance"),
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'employee',
|
||||
label: __('For Employee'),
|
||||
fieldtype: 'Link',
|
||||
options: 'Employee',
|
||||
get_query: () => {
|
||||
return {query: "erpnext.controllers.queries.employee_query"}
|
||||
},
|
||||
reqd: 1,
|
||||
onchange: function() {
|
||||
dialog.set_df_property("unmarked_days", "hidden", 1);
|
||||
dialog.set_df_property("status", "hidden", 1);
|
||||
dialog.set_df_property("month", "value", '');
|
||||
fields: [{
|
||||
fieldname: 'employee',
|
||||
label: __('For Employee'),
|
||||
fieldtype: 'Link',
|
||||
options: 'Employee',
|
||||
get_query: () => {
|
||||
return {query: "erpnext.controllers.queries.employee_query"};
|
||||
},
|
||||
reqd: 1,
|
||||
onchange: function() {
|
||||
dialog.set_df_property("unmarked_days", "hidden", 1);
|
||||
dialog.set_df_property("status", "hidden", 1);
|
||||
dialog.set_df_property("month", "value", '');
|
||||
dialog.set_df_property("unmarked_days", "options", []);
|
||||
dialog.no_unmarked_days_left = false;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: __("For Month"),
|
||||
fieldtype: "Select",
|
||||
fieldname: "month",
|
||||
options: months,
|
||||
reqd: 1,
|
||||
onchange: function() {
|
||||
if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
|
||||
dialog.set_df_property("status", "hidden", 0);
|
||||
dialog.set_df_property("unmarked_days", "options", []);
|
||||
dialog.no_unmarked_days_left = false;
|
||||
me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options => {
|
||||
if (options.length > 0) {
|
||||
dialog.set_df_property("unmarked_days", "hidden", 0);
|
||||
dialog.set_df_property("unmarked_days", "options", options);
|
||||
} else {
|
||||
dialog.no_unmarked_days_left = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
label: __("For Month"),
|
||||
fieldtype: "Select",
|
||||
fieldname: "month",
|
||||
options: months,
|
||||
reqd: 1,
|
||||
onchange: function() {
|
||||
if(dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
|
||||
dialog.set_df_property("status", "hidden", 0);
|
||||
dialog.set_df_property("unmarked_days", "options", []);
|
||||
dialog.no_unmarked_days_left = false;
|
||||
me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options =>{
|
||||
if (options.length > 0) {
|
||||
dialog.set_df_property("unmarked_days", "hidden", 0);
|
||||
dialog.set_df_property("unmarked_days", "options", options);
|
||||
} else {
|
||||
dialog.no_unmarked_days_left = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: __("Status"),
|
||||
fieldtype: "Select",
|
||||
fieldname: "status",
|
||||
options: ["Present", "Absent", "Half Day", "Work From Home"],
|
||||
hidden:1,
|
||||
reqd: 1,
|
||||
}
|
||||
},
|
||||
{
|
||||
label: __("Status"),
|
||||
fieldtype: "Select",
|
||||
fieldname: "status",
|
||||
options: ["Present", "Absent", "Half Day", "Work From Home"],
|
||||
hidden: 1,
|
||||
reqd: 1,
|
||||
|
||||
},
|
||||
{
|
||||
label: __("Unmarked Attendance for days"),
|
||||
fieldname: "unmarked_days",
|
||||
fieldtype: "MultiCheck",
|
||||
options: [],
|
||||
columns: 2,
|
||||
hidden: 1
|
||||
},
|
||||
],
|
||||
primary_action(data) {
|
||||
},
|
||||
{
|
||||
label: __("Unmarked Attendance for days"),
|
||||
fieldname: "unmarked_days",
|
||||
fieldtype: "MultiCheck",
|
||||
options: [],
|
||||
columns: 2,
|
||||
hidden: 1
|
||||
}],
|
||||
primary_action(data) {
|
||||
if (cur_dialog.no_unmarked_days_left) {
|
||||
frappe.msgprint(__("Attendance for the month of {0} , has already been marked for the Employee {1}",[dialog.fields_dict.month.value, dialog.fields_dict.employee.value]));
|
||||
frappe.msgprint(__("Attendance for the month of {0} , has already been marked for the Employee {1}",
|
||||
[dialog.fields_dict.month.value, dialog.fields_dict.employee.value]));
|
||||
} else {
|
||||
frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status,data.month]), () => {
|
||||
frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status, data.month]), () => {
|
||||
frappe.call({
|
||||
method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance",
|
||||
args: {
|
||||
data: data
|
||||
},
|
||||
callback: function(r) {
|
||||
callback: function (r) {
|
||||
if (r.message === 1) {
|
||||
frappe.show_alert({message: __("Attendance Marked"), indicator: 'blue'});
|
||||
frappe.show_alert({
|
||||
message: __("Attendance Marked"),
|
||||
indicator: 'blue'
|
||||
});
|
||||
cur_dialog.hide();
|
||||
}
|
||||
}
|
||||
@@ -101,21 +104,26 @@ frappe.listview_settings['Attendance'] = {
|
||||
dialog.show();
|
||||
});
|
||||
},
|
||||
get_multi_select_options: function(employee, month){
|
||||
|
||||
get_multi_select_options: function(employee, month) {
|
||||
return new Promise(resolve => {
|
||||
frappe.call({
|
||||
method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days',
|
||||
async: false,
|
||||
args:{
|
||||
args: {
|
||||
employee: employee,
|
||||
month: month,
|
||||
}
|
||||
}).then(r => {
|
||||
var options = [];
|
||||
for(var d in r.message){
|
||||
for (var d in r.message) {
|
||||
var momentObj = moment(r.message[d], 'YYYY-MM-DD');
|
||||
var date = momentObj.format('DD-MM-YYYY');
|
||||
options.push({ "label":date, "value": r.message[d] , "checked": 1});
|
||||
options.push({
|
||||
"label": date,
|
||||
"value": r.message[d],
|
||||
"checked": 1
|
||||
});
|
||||
}
|
||||
resolve(options);
|
||||
});
|
||||
|
||||
@@ -74,7 +74,6 @@ class TestDailyWorkSummary(unittest.TestCase):
|
||||
from `tabEmail Queue` as q, `tabEmail Queue Recipient` as r \
|
||||
where q.name = r.parent""", as_dict=1)
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
def setup_groups(self, hour=None):
|
||||
# setup email to trigger at this hour
|
||||
|
||||
@@ -4,40 +4,46 @@
|
||||
frappe.provide("erpnext.hr");
|
||||
erpnext.hr.EmployeeController = frappe.ui.form.Controller.extend({
|
||||
setup: function() {
|
||||
this.frm.fields_dict.user_id.get_query = function(doc, cdt, cdn) {
|
||||
this.frm.fields_dict.user_id.get_query = function() {
|
||||
return {
|
||||
query: "frappe.core.doctype.user.user.user_query",
|
||||
filters: {ignore_user_type: 1}
|
||||
}
|
||||
}
|
||||
this.frm.fields_dict.reports_to.get_query = function(doc, cdt, cdn) {
|
||||
return { query: "erpnext.controllers.queries.employee_query"} }
|
||||
filters: {
|
||||
ignore_user_type: 1
|
||||
}
|
||||
};
|
||||
};
|
||||
this.frm.fields_dict.reports_to.get_query = function() {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.employee_query"
|
||||
};
|
||||
};
|
||||
},
|
||||
|
||||
refresh: function() {
|
||||
var me = this;
|
||||
erpnext.toggle_naming_series();
|
||||
},
|
||||
|
||||
date_of_birth: function() {
|
||||
return cur_frm.call({
|
||||
method: "get_retirement_date",
|
||||
args: {date_of_birth: this.frm.doc.date_of_birth}
|
||||
args: {
|
||||
date_of_birth: this.frm.doc.date_of_birth
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
salutation: function() {
|
||||
if(this.frm.doc.salutation) {
|
||||
if (this.frm.doc.salutation) {
|
||||
this.frm.set_value("gender", {
|
||||
"Mr": "Male",
|
||||
"Ms": "Female"
|
||||
}[this.frm.doc.salutation]);
|
||||
} [this.frm.doc.salutation]);
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
frappe.ui.form.on('Employee',{
|
||||
setup: function(frm) {
|
||||
frappe.ui.form.on('Employee', {
|
||||
setup: function (frm) {
|
||||
frm.set_query("leave_policy", function() {
|
||||
return {
|
||||
"filters": {
|
||||
@@ -46,7 +52,7 @@ frappe.ui.form.on('Employee',{
|
||||
};
|
||||
});
|
||||
},
|
||||
onload:function(frm) {
|
||||
onload: function (frm) {
|
||||
frm.set_query("department", function() {
|
||||
return {
|
||||
"filters": {
|
||||
@@ -55,23 +61,28 @@ frappe.ui.form.on('Employee',{
|
||||
};
|
||||
});
|
||||
},
|
||||
prefered_contact_email:function(frm){
|
||||
frm.events.update_contact(frm)
|
||||
prefered_contact_email: function(frm) {
|
||||
frm.events.update_contact(frm);
|
||||
},
|
||||
personal_email:function(frm){
|
||||
frm.events.update_contact(frm)
|
||||
|
||||
personal_email: function(frm) {
|
||||
frm.events.update_contact(frm);
|
||||
},
|
||||
company_email:function(frm){
|
||||
frm.events.update_contact(frm)
|
||||
|
||||
company_email: function(frm) {
|
||||
frm.events.update_contact(frm);
|
||||
},
|
||||
user_id:function(frm){
|
||||
frm.events.update_contact(frm)
|
||||
|
||||
user_id: function(frm) {
|
||||
frm.events.update_contact(frm);
|
||||
},
|
||||
update_contact:function(frm){
|
||||
|
||||
update_contact: function(frm) {
|
||||
var prefered_email_fieldname = frappe.model.scrub(frm.doc.prefered_contact_email) || 'user_id';
|
||||
frm.set_value("prefered_email",
|
||||
frm.fields_dict[prefered_email_fieldname].value)
|
||||
frm.fields_dict[prefered_email_fieldname].value);
|
||||
},
|
||||
|
||||
status: function(frm) {
|
||||
return frm.call({
|
||||
method: "deactivate_sales_person",
|
||||
@@ -81,19 +92,63 @@ frappe.ui.form.on('Employee',{
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
create_user: function(frm) {
|
||||
if (!frm.doc.prefered_email)
|
||||
{
|
||||
frappe.throw(__("Please enter Preferred Contact Email"))
|
||||
if (!frm.doc.prefered_email) {
|
||||
frappe.throw(__("Please enter Preferred Contact Email"));
|
||||
}
|
||||
frappe.call({
|
||||
method: "erpnext.hr.doctype.employee.employee.create_user",
|
||||
args: { employee: frm.doc.name, email: frm.doc.prefered_email },
|
||||
callback: function(r)
|
||||
{
|
||||
frm.set_value("user_id", r.message)
|
||||
args: {
|
||||
employee: frm.doc.name,
|
||||
email: frm.doc.prefered_email
|
||||
},
|
||||
callback: function (r) {
|
||||
frm.set_value("user_id", r.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
cur_frm.cscript = new erpnext.hr.EmployeeController({frm: cur_frm});
|
||||
|
||||
cur_frm.cscript = new erpnext.hr.EmployeeController({
|
||||
frm: cur_frm
|
||||
});
|
||||
|
||||
|
||||
frappe.tour['Employee'] = [
|
||||
{
|
||||
fieldname: "first_name",
|
||||
title: "First Name",
|
||||
description: __("Enter First and Last name of Employee, based on Which Full Name will be updated. IN transactions, it will be Full Name which will be fetched.")
|
||||
},
|
||||
{
|
||||
fieldname: "company",
|
||||
title: "Company",
|
||||
description: __("Select a Company this Employee belongs to. Other HR features like Payroll. Expense Claims and Leaves for this Employee will be created for a given company only.")
|
||||
},
|
||||
{
|
||||
fieldname: "date_of_birth",
|
||||
title: "Date of Birth",
|
||||
description: __("Select Date of Birth. This will validate Employees age and prevent hiring of under-age staff.")
|
||||
},
|
||||
{
|
||||
fieldname: "date_of_joining",
|
||||
title: "Date of Joining",
|
||||
description: __("Select Date of joining. It will have impact on the first salary calculation, Leave allocation on pro-rata bases.")
|
||||
},
|
||||
{
|
||||
fieldname: "holiday_list",
|
||||
title: "Holiday List",
|
||||
description: __("Select a default Holiday List for this Employee. The days listed in Holiday List will not be counted in Leave Application.")
|
||||
},
|
||||
{
|
||||
fieldname: "reports_to",
|
||||
title: "Reports To",
|
||||
description: __("Here, you can select a senior of this Employee. Based on this, Organization Chart will be populated.")
|
||||
},
|
||||
{
|
||||
fieldname: "leave_approver",
|
||||
title: "Leave Approver",
|
||||
description: __("Select Leave Approver for an employee. The user one who will look after his/her Leave application")
|
||||
},
|
||||
];
|
||||
|
||||
@@ -72,6 +72,7 @@ def get_job_applicant():
|
||||
applicant = frappe.new_doc('Job Applicant')
|
||||
applicant.applicant_name = 'Test Researcher'
|
||||
applicant.email_id = 'test@researcher.com'
|
||||
applicant.designation = 'Researcher'
|
||||
applicant.status = 'Open'
|
||||
applicant.cover_letter = 'I am a great Researcher.'
|
||||
applicant.insert()
|
||||
|
||||
@@ -38,8 +38,10 @@ def create_job_applicant(source_name, target_doc=None):
|
||||
status = "Open"
|
||||
|
||||
job_applicant = frappe.new_doc("Job Applicant")
|
||||
job_applicant.source = "Employee Referral"
|
||||
job_applicant.employee_referral = emp_ref.name
|
||||
job_applicant.status = status
|
||||
job_applicant.designation = emp_ref.for_designation
|
||||
job_applicant.applicant_name = emp_ref.full_name
|
||||
job_applicant.email_id = emp_ref.email
|
||||
job_applicant.phone_number = emp_ref.contact_no
|
||||
|
||||
@@ -17,6 +17,11 @@ from erpnext.hr.doctype.employee_referral.employee_referral import (
|
||||
|
||||
|
||||
class TestEmployeeReferral(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
frappe.db.sql("DELETE FROM `tabJob Applicant`")
|
||||
frappe.db.sql("DELETE FROM `tabEmployee Referral`")
|
||||
|
||||
def test_workflow_and_status_sync(self):
|
||||
emp_ref = create_employee_referral()
|
||||
|
||||
@@ -50,6 +55,10 @@ class TestEmployeeReferral(unittest.TestCase):
|
||||
add_sal = create_additional_salary(emp_ref)
|
||||
self.assertTrue(add_sal.ref_docname, emp_ref.name)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.sql("DELETE FROM `tabJob Applicant`")
|
||||
frappe.db.sql("DELETE FROM `tabEmployee Referral`")
|
||||
|
||||
|
||||
def create_employee_referral():
|
||||
emp_ref = frappe.new_doc("Employee Referral")
|
||||
|
||||
0
erpnext/hr/doctype/expected_skill_set/__init__.py
Normal file
0
erpnext/hr/doctype/expected_skill_set/__init__.py
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2021-04-12 13:05:06.741330",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"skill",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "skill",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Skill",
|
||||
"options": "Skill",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "skill.description",
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Description"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-12 14:26:33.062549",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Expected Skill Set",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
12
erpnext/hr/doctype/expected_skill_set/expected_skill_set.py
Normal file
12
erpnext/hr/doctype/expected_skill_set/expected_skill_set.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ExpectedSkillSet(Document):
|
||||
pass
|
||||
@@ -10,6 +10,26 @@ frappe.ui.form.on('Expense Claim', {
|
||||
},
|
||||
company: function(frm) {
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
var expenses = frm.doc.expenses;
|
||||
for (var i = 0; i < expenses.length; i++) {
|
||||
var expense = expenses[i];
|
||||
if (!expense.expense_type) {
|
||||
continue;
|
||||
}
|
||||
frappe.call({
|
||||
method: "erpnext.hr.doctype.expense_claim.expense_claim.get_expense_claim_account_and_cost_center",
|
||||
args: {
|
||||
"expense_claim_type": expense.expense_type,
|
||||
"company": frm.doc.company
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
expense.default_account = r.message.account;
|
||||
expense.cost_center = r.message.cost_center;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ def generate_taxes():
|
||||
account = create_account(company=company_name, account_name="Output Tax CGST", account_type="Tax", parent_account=parent_account)
|
||||
return {'taxes':[{
|
||||
"account_head": account,
|
||||
"rate": 0,
|
||||
"rate": 9,
|
||||
"description": "CGST",
|
||||
"tax_amount": 10,
|
||||
"total": 210
|
||||
|
||||
@@ -56,8 +56,6 @@
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fetch_from": "account_head.tax_rate",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
@@ -111,4 +109,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Holiday List', {
|
||||
frappe.ui.form.on("Holiday List", {
|
||||
refresh: function(frm) {
|
||||
if (frm.doc.holidays) {
|
||||
frm.set_value('total_holidays', frm.doc.holidays.length);
|
||||
frm.set_value("total_holidays", frm.doc.holidays.length);
|
||||
}
|
||||
},
|
||||
from_date: function(frm) {
|
||||
@@ -14,3 +14,36 @@ frappe.ui.form.on('Holiday List', {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frappe.tour["Holiday List"] = [
|
||||
{
|
||||
fieldname: "holiday_list_name",
|
||||
title: "Holiday List Name",
|
||||
description: __("Enter a name for this Holiday List."),
|
||||
},
|
||||
{
|
||||
fieldname: "from_date",
|
||||
title: "From Date",
|
||||
description: __("Based on your HR Policy, select your leave allocation period's start date"),
|
||||
},
|
||||
{
|
||||
fieldname: "to_date",
|
||||
title: "To Date",
|
||||
description: __("Based on your HR Policy, select your leave allocation period's end date"),
|
||||
},
|
||||
{
|
||||
fieldname: "weekly_off",
|
||||
title: "Weekly Off",
|
||||
description: __("Select your weekly off day"),
|
||||
},
|
||||
{
|
||||
fieldname: "get_weekly_off_dates",
|
||||
title: "Add Holidays",
|
||||
description: __("Click on Add to Holidays. This will populate the holidays table with all the dates that fall on the selected weekly off. Repeat the process for populating the dates for all your weekly holidays"),
|
||||
},
|
||||
{
|
||||
fieldname: "holidays",
|
||||
title: "Holidays",
|
||||
description: __("Here, your weekly offs are pre-populated based on the previous selections. You can add more rows to also add public and national holidays individually.")
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
@@ -94,9 +93,11 @@ def get_events(start, end, filters=None):
|
||||
update={"allDay": 1})
|
||||
|
||||
|
||||
def is_holiday(holiday_list, date=today()):
|
||||
def is_holiday(holiday_list, date=None):
|
||||
"""Returns true if the given date is a holiday in the given holiday list
|
||||
"""
|
||||
if date is None:
|
||||
date = today()
|
||||
if holiday_list:
|
||||
return bool(frappe.get_all('Holiday List',
|
||||
dict(name=holiday_list, holiday_date=date)))
|
||||
|
||||
@@ -2,7 +2,22 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('HR Settings', {
|
||||
restrict_backdated_leave_application: function(frm) {
|
||||
frm.toggle_reqd("role_allowed_to_create_backdated_leave_application", frm.doc.restrict_backdated_leave_application);
|
||||
}
|
||||
});
|
||||
|
||||
frappe.tour['HR Settings'] = [
|
||||
{
|
||||
fieldname: 'emp_created_by',
|
||||
title: 'Employee Naming By',
|
||||
description: __('Employee can be named by Employee ID if you assign one, or via Naming Series. Select your preference here.'),
|
||||
},
|
||||
{
|
||||
fieldname: 'standard_working_hours',
|
||||
title: 'Standard Working Hours',
|
||||
description: __('Enter the Standard Working Hours for a normal work day. These hours will be used in calculations of reports such as Employee Hours Utilization and Project Profitability analysis.'),
|
||||
},
|
||||
{
|
||||
fieldname: 'leave_and_expense_claim_settings',
|
||||
title: 'Leave and Expense Clain Settings',
|
||||
description: __('Review various other settings related to Employee Leaves and Expense Claim')
|
||||
}
|
||||
];
|
||||
|
||||
@@ -7,30 +7,36 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"employee_settings",
|
||||
"retirement_age",
|
||||
"emp_created_by",
|
||||
"column_break_4",
|
||||
"standard_working_hours",
|
||||
"expense_approver_mandatory_in_expense_claim",
|
||||
"column_break_9",
|
||||
"retirement_age",
|
||||
"reminders_section",
|
||||
"send_birthday_reminders",
|
||||
"column_break_9",
|
||||
"send_work_anniversary_reminders",
|
||||
"column_break_11",
|
||||
"send_work_anniversary_reminders",
|
||||
"column_break_18",
|
||||
"send_holiday_reminders",
|
||||
"frequency",
|
||||
"leave_settings",
|
||||
"leave_and_expense_claim_settings",
|
||||
"send_leave_notification",
|
||||
"leave_approval_notification_template",
|
||||
"leave_status_notification_template",
|
||||
"role_allowed_to_create_backdated_leave_application",
|
||||
"column_break_18",
|
||||
"leave_approver_mandatory_in_leave_application",
|
||||
"restrict_backdated_leave_application",
|
||||
"role_allowed_to_create_backdated_leave_application",
|
||||
"column_break_29",
|
||||
"expense_approver_mandatory_in_expense_claim",
|
||||
"show_leaves_of_all_department_members_in_calendar",
|
||||
"auto_leave_encashment",
|
||||
"restrict_backdated_leave_application",
|
||||
"hiring_settings",
|
||||
"check_vacancies"
|
||||
"hiring_settings_section",
|
||||
"check_vacancies",
|
||||
"send_interview_reminder",
|
||||
"interview_reminder_template",
|
||||
"remind_before",
|
||||
"column_break_4",
|
||||
"send_interview_feedback_reminder",
|
||||
"feedback_reminder_notification_template"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -39,17 +45,16 @@
|
||||
"label": "Employee Settings"
|
||||
},
|
||||
{
|
||||
"description": "Enter retirement age in years",
|
||||
"fieldname": "retirement_age",
|
||||
"fieldtype": "Data",
|
||||
"label": "Retirement Age"
|
||||
"label": "Retirement Age (In Years)"
|
||||
},
|
||||
{
|
||||
"default": "Naming Series",
|
||||
"description": "Employee records are created using the selected field",
|
||||
"description": "Employee records are created using the selected option",
|
||||
"fieldname": "emp_created_by",
|
||||
"fieldtype": "Select",
|
||||
"label": "Employee Records to be created by",
|
||||
"label": "Employee Naming By",
|
||||
"options": "Naming Series\nEmployee Number\nFull Name"
|
||||
},
|
||||
{
|
||||
@@ -62,28 +67,6 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Expense Approver Mandatory In Expense Claim"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "leave_settings",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Leave Settings"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.send_leave_notification == 1",
|
||||
"fieldname": "leave_approval_notification_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Leave Approval Notification Template",
|
||||
"mandatory_depends_on": "eval: doc.send_leave_notification == 1",
|
||||
"options": "Email Template"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.send_leave_notification == 1",
|
||||
"fieldname": "leave_status_notification_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Leave Status Notification Template",
|
||||
"mandatory_depends_on": "eval: doc.send_leave_notification == 1",
|
||||
"options": "Email Template"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_18",
|
||||
"fieldtype": "Column Break"
|
||||
@@ -100,35 +83,18 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Leaves Of All Department Members In Calendar"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "hiring_settings",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Hiring Settings"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "check_vacancies",
|
||||
"fieldtype": "Check",
|
||||
"label": "Check Vacancies On Job Offer Creation"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "auto_leave_encashment",
|
||||
"fieldtype": "Check",
|
||||
"label": "Auto Leave Encashment"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "restrict_backdated_leave_application",
|
||||
"fieldtype": "Check",
|
||||
"label": "Restrict Backdated Leave Application"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.restrict_backdated_leave_application == 1",
|
||||
"fieldname": "role_allowed_to_create_backdated_leave_application",
|
||||
"fieldtype": "Link",
|
||||
"label": "Role Allowed to Create Backdated Leave Application",
|
||||
"mandatory_depends_on": "eval:doc.restrict_backdated_leave_application == 1",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
@@ -137,11 +103,40 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Send Leave Notification"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.send_leave_notification == 1",
|
||||
"fieldname": "leave_approval_notification_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Leave Approval Notification Template",
|
||||
"mandatory_depends_on": "eval: doc.send_leave_notification == 1",
|
||||
"options": "Email Template"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.send_leave_notification == 1",
|
||||
"fieldname": "leave_status_notification_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Leave Status Notification Template",
|
||||
"mandatory_depends_on": "eval: doc.send_leave_notification == 1",
|
||||
"options": "Email Template"
|
||||
},
|
||||
{
|
||||
"fieldname": "standard_working_hours",
|
||||
"fieldtype": "Int",
|
||||
"label": "Standard Working Hours"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "leave_and_expense_claim_settings",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Leave and Expense Claim Settings"
|
||||
},
|
||||
{
|
||||
"default": "00:15:00",
|
||||
"depends_on": "send_interview_reminder",
|
||||
"fieldname": "remind_before",
|
||||
"fieldtype": "Time",
|
||||
"label": "Remind Before"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "reminders_section",
|
||||
@@ -166,6 +161,7 @@
|
||||
"fieldname": "frequency",
|
||||
"fieldtype": "Select",
|
||||
"label": "Set the frequency for holiday reminders",
|
||||
"mandatory_depends_on": "send_holiday_reminders",
|
||||
"options": "Weekly\nMonthly"
|
||||
},
|
||||
{
|
||||
@@ -181,13 +177,62 @@
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "send_interview_reminder",
|
||||
"fieldtype": "Check",
|
||||
"label": "Send Interview Reminder"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "send_interview_feedback_reminder",
|
||||
"fieldtype": "Check",
|
||||
"label": "Send Interview Feedback Reminder"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_29",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "send_interview_feedback_reminder",
|
||||
"fieldname": "feedback_reminder_notification_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Feedback Reminder Notification Template",
|
||||
"mandatory_depends_on": "send_interview_feedback_reminder",
|
||||
"options": "Email Template"
|
||||
},
|
||||
{
|
||||
"depends_on": "send_interview_reminder",
|
||||
"fieldname": "interview_reminder_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Interview Reminder Notification Template",
|
||||
"mandatory_depends_on": "send_interview_reminder",
|
||||
"options": "Email Template"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "restrict_backdated_leave_application",
|
||||
"fieldtype": "Check",
|
||||
"label": "Restrict Backdated Leave Application"
|
||||
},
|
||||
{
|
||||
"fieldname": "hiring_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Hiring Settings"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "check_vacancies",
|
||||
"fieldtype": "Check",
|
||||
"label": "Check Vacancies On Job Offer Creation"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"idx": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-24 14:54:12.834162",
|
||||
"modified": "2021-10-01 23:46:11.098236",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "HR Settings",
|
||||
|
||||
0
erpnext/hr/doctype/interview/__init__.py
Normal file
0
erpnext/hr/doctype/interview/__init__.py
Normal file
237
erpnext/hr/doctype/interview/interview.js
Normal file
237
erpnext/hr/doctype/interview/interview.js
Normal file
@@ -0,0 +1,237 @@
|
||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Interview', {
|
||||
onload: function (frm) {
|
||||
frm.events.set_job_applicant_query(frm);
|
||||
|
||||
frm.set_query('interviewer', 'interview_details', function () {
|
||||
return {
|
||||
query: 'erpnext.hr.doctype.interview.interview.get_interviewer_list'
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.docstatus != 2 && !frm.doc.__islocal) {
|
||||
if (frm.doc.status === 'Pending') {
|
||||
frm.add_custom_button(__('Reschedule Interview'), function() {
|
||||
frm.events.show_reschedule_dialog(frm);
|
||||
frm.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
let allowed_interviewers = [];
|
||||
frm.doc.interview_details.forEach(values => {
|
||||
allowed_interviewers.push(values.interviewer);
|
||||
});
|
||||
|
||||
if ((allowed_interviewers.includes(frappe.session.user))) {
|
||||
frappe.db.get_value('Interview Feedback', {'interviewer': frappe.session.user, 'interview': frm.doc.name, 'docstatus': 1}, 'name', (r) => {
|
||||
if (Object.keys(r).length === 0) {
|
||||
frm.add_custom_button(__('Submit Feedback'), function () {
|
||||
frappe.call({
|
||||
method: 'erpnext.hr.doctype.interview.interview.get_expected_skill_set',
|
||||
args: {
|
||||
interview_round: frm.doc.interview_round
|
||||
},
|
||||
callback: function (r) {
|
||||
frm.events.show_feedback_dialog(frm, r.message);
|
||||
frm.refresh();
|
||||
}
|
||||
});
|
||||
}).addClass('btn-primary');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
show_reschedule_dialog: function (frm) {
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: 'Reschedule Interview',
|
||||
fields: [
|
||||
{
|
||||
label: 'Schedule On',
|
||||
fieldname: 'scheduled_on',
|
||||
fieldtype: 'Date',
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: 'From Time',
|
||||
fieldname: 'from_time',
|
||||
fieldtype: 'Time',
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: 'To Time',
|
||||
fieldname: 'to_time',
|
||||
fieldtype: 'Time',
|
||||
reqd: 1
|
||||
}
|
||||
],
|
||||
primary_action_label: 'Reschedule',
|
||||
primary_action(values) {
|
||||
frm.call({
|
||||
method: 'reschedule_interview',
|
||||
doc: frm.doc,
|
||||
args: {
|
||||
scheduled_on: values.scheduled_on,
|
||||
from_time: values.from_time,
|
||||
to_time: values.to_time
|
||||
}
|
||||
}).then(() => {
|
||||
frm.refresh();
|
||||
d.hide();
|
||||
});
|
||||
}
|
||||
});
|
||||
d.show();
|
||||
},
|
||||
|
||||
show_feedback_dialog: function (frm, data) {
|
||||
let fields = frm.events.get_fields_for_feedback();
|
||||
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __('Submit Feedback'),
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'skill_set',
|
||||
fieldtype: 'Table',
|
||||
label: __('Skill Assessment'),
|
||||
cannot_add_rows: false,
|
||||
in_editable_grid: true,
|
||||
reqd: 1,
|
||||
fields: fields,
|
||||
data: data
|
||||
},
|
||||
{
|
||||
fieldname: 'result',
|
||||
fieldtype: 'Select',
|
||||
options: ['', 'Cleared', 'Rejected'],
|
||||
label: __('Result')
|
||||
},
|
||||
{
|
||||
fieldname: 'feedback',
|
||||
fieldtype: 'Small Text',
|
||||
label: __('Feedback')
|
||||
}
|
||||
],
|
||||
size: 'large',
|
||||
minimizable: true,
|
||||
primary_action: function(values) {
|
||||
frappe.call({
|
||||
method: 'erpnext.hr.doctype.interview.interview.create_interview_feedback',
|
||||
args: {
|
||||
data: values,
|
||||
interview_name: frm.doc.name,
|
||||
interviewer: frappe.session.user,
|
||||
job_applicant: frm.doc.job_applicant
|
||||
}
|
||||
}).then(() => {
|
||||
frm.refresh();
|
||||
});
|
||||
d.hide();
|
||||
}
|
||||
});
|
||||
d.show();
|
||||
},
|
||||
|
||||
get_fields_for_feedback: function () {
|
||||
return [{
|
||||
fieldtype: 'Link',
|
||||
fieldname: 'skill',
|
||||
options: 'Skill',
|
||||
in_list_view: 1,
|
||||
label: __('Skill')
|
||||
}, {
|
||||
fieldtype: 'Rating',
|
||||
fieldname: 'rating',
|
||||
label: __('Rating'),
|
||||
in_list_view: 1,
|
||||
reqd: 1,
|
||||
}];
|
||||
},
|
||||
|
||||
set_job_applicant_query: function (frm) {
|
||||
frm.set_query('job_applicant', function () {
|
||||
let job_applicant_filters = {
|
||||
status: ['!=', 'Rejected']
|
||||
};
|
||||
if (frm.doc.designation) {
|
||||
job_applicant_filters.designation = frm.doc.designation;
|
||||
}
|
||||
return {
|
||||
filters: job_applicant_filters
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
interview_round: async function (frm) {
|
||||
frm.events.reset_values(frm);
|
||||
frm.set_value('job_applicant', '');
|
||||
|
||||
let round_data = (await frappe.db.get_value('Interview Round', frm.doc.interview_round, 'designation')).message;
|
||||
frm.set_value('designation', round_data.designation);
|
||||
frm.events.set_job_applicant_query(frm);
|
||||
|
||||
if (frm.doc.interview_round) {
|
||||
frm.events.set_interview_details(frm);
|
||||
} else {
|
||||
frm.set_value('interview_details', []);
|
||||
}
|
||||
},
|
||||
|
||||
set_interview_details: function (frm) {
|
||||
frappe.call({
|
||||
method: 'erpnext.hr.doctype.interview.interview.get_interviewers',
|
||||
args: {
|
||||
interview_round: frm.doc.interview_round
|
||||
},
|
||||
callback: function (data) {
|
||||
let interview_details = data.message;
|
||||
frm.set_value('interview_details', []);
|
||||
if (data.message.length) {
|
||||
frm.set_value('interview_details', interview_details);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
job_applicant: function (frm) {
|
||||
if (!frm.doc.interview_round) {
|
||||
frm.doc.job_applicant = '';
|
||||
frm.refresh();
|
||||
frappe.throw(__('Select Interview Round First'));
|
||||
}
|
||||
|
||||
if (frm.doc.job_applicant) {
|
||||
frm.events.set_designation_and_job_opening(frm);
|
||||
} else {
|
||||
frm.events.reset_values(frm);
|
||||
}
|
||||
},
|
||||
|
||||
set_designation_and_job_opening: async function (frm) {
|
||||
let round_data = (await frappe.db.get_value('Interview Round', frm.doc.interview_round, 'designation')).message;
|
||||
frm.set_value('designation', round_data.designation);
|
||||
frm.events.set_job_applicant_query(frm);
|
||||
|
||||
let job_applicant_data = (await frappe.db.get_value(
|
||||
'Job Applicant', frm.doc.job_applicant, ['designation', 'job_title', 'resume_link'],
|
||||
)).message;
|
||||
|
||||
if (!round_data.designation) {
|
||||
frm.set_value('designation', job_applicant_data.designation);
|
||||
}
|
||||
|
||||
frm.set_value('job_opening', job_applicant_data.job_title);
|
||||
frm.set_value('resume_link', job_applicant_data.resume_link);
|
||||
},
|
||||
|
||||
reset_values: function (frm) {
|
||||
frm.set_value('designation', '');
|
||||
frm.set_value('job_opening', '');
|
||||
frm.set_value('resume_link', '');
|
||||
}
|
||||
});
|
||||
254
erpnext/hr/doctype/interview/interview.json
Normal file
254
erpnext/hr/doctype/interview/interview.json
Normal file
@@ -0,0 +1,254 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "HR-INT-.YYYY.-.####",
|
||||
"creation": "2021-04-12 15:03:11.524090",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"interview_details_section",
|
||||
"interview_round",
|
||||
"job_applicant",
|
||||
"job_opening",
|
||||
"designation",
|
||||
"resume_link",
|
||||
"column_break_4",
|
||||
"status",
|
||||
"scheduled_on",
|
||||
"from_time",
|
||||
"to_time",
|
||||
"interview_feedback_section",
|
||||
"interview_details",
|
||||
"ratings_section",
|
||||
"expected_average_rating",
|
||||
"column_break_12",
|
||||
"average_rating",
|
||||
"section_break_13",
|
||||
"interview_summary",
|
||||
"reminded",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "job_applicant",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Job Applicant",
|
||||
"options": "Job Applicant",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "job_opening",
|
||||
"fieldtype": "Link",
|
||||
"label": "Job Opening",
|
||||
"options": "Job Opening",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "interview_round",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Interview Round",
|
||||
"options": "Interview Round",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "Pending",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"options": "Pending\nUnder Review\nCleared\nRejected",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "ratings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Ratings"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "average_rating",
|
||||
"fieldtype": "Rating",
|
||||
"in_list_view": 1,
|
||||
"label": "Obtained Average Rating",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "interview_summary",
|
||||
"fieldtype": "Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "resume_link",
|
||||
"fieldtype": "Data",
|
||||
"label": "Resume link"
|
||||
},
|
||||
{
|
||||
"fieldname": "interview_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Details"
|
||||
},
|
||||
{
|
||||
"fetch_from": "interview_round.expected_average_rating",
|
||||
"fieldname": "expected_average_rating",
|
||||
"fieldtype": "Rating",
|
||||
"label": "Expected Average Rating",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_break_13",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Interview Summary"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_12",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "interview_round.designation",
|
||||
"fieldname": "designation",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Designation",
|
||||
"options": "Designation",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Interview",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "scheduled_on",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Scheduled On",
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "reminded",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Reminded"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "interview_details",
|
||||
"fieldtype": "Table",
|
||||
"options": "Interview Detail"
|
||||
},
|
||||
{
|
||||
"fieldname": "interview_feedback_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Feedback"
|
||||
},
|
||||
{
|
||||
"fieldname": "from_time",
|
||||
"fieldtype": "Time",
|
||||
"in_list_view": 1,
|
||||
"label": "From Time",
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "to_time",
|
||||
"fieldtype": "Time",
|
||||
"in_list_view": 1,
|
||||
"label": "To Time",
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "Interview Feedback",
|
||||
"link_fieldname": "interview"
|
||||
}
|
||||
],
|
||||
"modified": "2021-09-30 13:30:05.421035",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Interview",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "HR Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Interviewer",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "HR User",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "job_applicant",
|
||||
"track_changes": 1
|
||||
}
|
||||
293
erpnext/hr/doctype/interview/interview.py
Normal file
293
erpnext/hr/doctype/interview/interview.py
Normal file
@@ -0,0 +1,293 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cstr, get_datetime, get_link_to_form
|
||||
|
||||
|
||||
class DuplicateInterviewRoundError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
class Interview(Document):
|
||||
def validate(self):
|
||||
self.validate_duplicate_interview()
|
||||
self.validate_designation()
|
||||
self.validate_overlap()
|
||||
|
||||
def on_submit(self):
|
||||
if self.status not in ['Cleared', 'Rejected']:
|
||||
frappe.throw(_('Only Interviews with Cleared or Rejected status can be submitted.'), title=_('Not Allowed'))
|
||||
|
||||
def validate_duplicate_interview(self):
|
||||
duplicate_interview = frappe.db.exists('Interview', {
|
||||
'job_applicant': self.job_applicant,
|
||||
'interview_round': self.interview_round,
|
||||
'docstatus': 1
|
||||
}
|
||||
)
|
||||
|
||||
if duplicate_interview:
|
||||
frappe.throw(_('Job Applicants are not allowed to appear twice for the same Interview round. Interview {0} already scheduled for Job Applicant {1}').format(
|
||||
frappe.bold(get_link_to_form('Interview', duplicate_interview)),
|
||||
frappe.bold(self.job_applicant)
|
||||
))
|
||||
|
||||
def validate_designation(self):
|
||||
applicant_designation = frappe.db.get_value('Job Applicant', self.job_applicant, 'designation')
|
||||
if self.designation :
|
||||
if self.designation != applicant_designation:
|
||||
frappe.throw(_('Interview Round {0} is only for Designation {1}. Job Applicant has applied for the role {2}').format(
|
||||
self.interview_round, frappe.bold(self.designation), applicant_designation),
|
||||
exc=DuplicateInterviewRoundError)
|
||||
else:
|
||||
self.designation = applicant_designation
|
||||
|
||||
def validate_overlap(self):
|
||||
interviewers = [entry.interviewer for entry in self.interview_details] or ['']
|
||||
|
||||
overlaps = frappe.db.sql("""
|
||||
SELECT interview.name
|
||||
FROM `tabInterview` as interview
|
||||
INNER JOIN `tabInterview Detail` as detail
|
||||
WHERE
|
||||
interview.scheduled_on = %s and interview.name != %s and interview.docstatus != 2
|
||||
and (interview.job_applicant = %s or detail.interviewer IN %s) and
|
||||
((from_time < %s and to_time > %s) or
|
||||
(from_time > %s and to_time < %s) or
|
||||
(from_time = %s))
|
||||
""", (self.scheduled_on, self.name, self.job_applicant, interviewers,
|
||||
self.from_time, self.to_time, self.from_time, self.to_time, self.from_time))
|
||||
|
||||
if overlaps:
|
||||
overlapping_details = _('Interview overlaps with {0}').format(get_link_to_form('Interview', overlaps[0][0]))
|
||||
frappe.throw(overlapping_details, title=_('Overlap'))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def reschedule_interview(self, scheduled_on, from_time, to_time):
|
||||
original_date = self.scheduled_on
|
||||
from_time = self.from_time
|
||||
to_time = self.to_time
|
||||
|
||||
self.db_set({
|
||||
'scheduled_on': scheduled_on,
|
||||
'from_time': from_time,
|
||||
'to_time': to_time
|
||||
})
|
||||
self.notify_update()
|
||||
|
||||
recipients = get_recipients(self.name)
|
||||
|
||||
try:
|
||||
frappe.sendmail(
|
||||
recipients= recipients,
|
||||
subject=_('Interview: {0} Rescheduled').format(self.name),
|
||||
message=_('Your Interview session is rescheduled from {0} {1} - {2} to {3} {4} - {5}').format(
|
||||
original_date, from_time, to_time, self.scheduled_on, self.from_time, self.to_time),
|
||||
reference_doctype=self.doctype,
|
||||
reference_name=self.name
|
||||
)
|
||||
except Exception:
|
||||
frappe.msgprint(_('Failed to send the Interview Reschedule notification. Please configure your email account.'))
|
||||
|
||||
frappe.msgprint(_('Interview Rescheduled successfully'), indicator='green')
|
||||
|
||||
|
||||
def get_recipients(name, for_feedback=0):
|
||||
interview = frappe.get_doc('Interview', name)
|
||||
|
||||
if for_feedback:
|
||||
recipients = [d.interviewer for d in interview.interview_details if not d.interview_feedback]
|
||||
else:
|
||||
recipients = [d.interviewer for d in interview.interview_details]
|
||||
recipients.append(frappe.db.get_value('Job Applicant', interview.job_applicant, 'email_id'))
|
||||
|
||||
return recipients
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_interviewers(interview_round):
|
||||
return frappe.get_all('Interviewer', filters={'parent': interview_round}, fields=['user as interviewer'])
|
||||
|
||||
|
||||
def send_interview_reminder():
|
||||
reminder_settings = frappe.db.get_value('HR Settings', 'HR Settings',
|
||||
['send_interview_reminder', 'interview_reminder_template'], as_dict=True)
|
||||
|
||||
if not reminder_settings.send_interview_reminder:
|
||||
return
|
||||
|
||||
remind_before = cstr(frappe.db.get_single_value('HR Settings', 'remind_before')) or '01:00:00'
|
||||
remind_before = datetime.datetime.strptime(remind_before, '%H:%M:%S')
|
||||
reminder_date_time = datetime.datetime.now() + datetime.timedelta(
|
||||
hours=remind_before.hour, minutes=remind_before.minute, seconds=remind_before.second)
|
||||
|
||||
interviews = frappe.get_all('Interview', filters={
|
||||
'scheduled_on': ['between', (datetime.datetime.now(), reminder_date_time)],
|
||||
'status': 'Pending',
|
||||
'reminded': 0,
|
||||
'docstatus': ['!=', 2]
|
||||
})
|
||||
|
||||
interview_template = frappe.get_doc('Email Template', reminder_settings.interview_reminder_template)
|
||||
|
||||
for d in interviews:
|
||||
doc = frappe.get_doc('Interview', d.name)
|
||||
context = doc.as_dict()
|
||||
message = frappe.render_template(interview_template.response, context)
|
||||
recipients = get_recipients(doc.name)
|
||||
|
||||
frappe.sendmail(
|
||||
recipients= recipients,
|
||||
subject=interview_template.subject,
|
||||
message=message,
|
||||
reference_doctype=doc.doctype,
|
||||
reference_name=doc.name
|
||||
)
|
||||
|
||||
doc.db_set('reminded', 1)
|
||||
|
||||
|
||||
def send_daily_feedback_reminder():
|
||||
reminder_settings = frappe.db.get_value('HR Settings', 'HR Settings',
|
||||
['send_interview_feedback_reminder', 'feedback_reminder_notification_template'], as_dict=True)
|
||||
|
||||
if not reminder_settings.send_interview_feedback_reminder:
|
||||
return
|
||||
|
||||
interview_feedback_template = frappe.get_doc('Email Template', reminder_settings.feedback_reminder_notification_template)
|
||||
interviews = frappe.get_all('Interview', filters={'status': ['in', ['Under Review', 'Pending']], 'docstatus': ['!=', 2]})
|
||||
|
||||
for entry in interviews:
|
||||
recipients = get_recipients(entry.name, for_feedback=1)
|
||||
|
||||
doc = frappe.get_doc('Interview', entry.name)
|
||||
context = doc.as_dict()
|
||||
|
||||
message = frappe.render_template(interview_feedback_template.response, context)
|
||||
|
||||
if len(recipients):
|
||||
frappe.sendmail(
|
||||
recipients= recipients,
|
||||
subject=interview_feedback_template.subject,
|
||||
message=message,
|
||||
reference_doctype='Interview',
|
||||
reference_name=entry.name
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_expected_skill_set(interview_round):
|
||||
return frappe.get_all('Expected Skill Set', filters ={'parent': interview_round}, fields=['skill'])
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_interview_feedback(data, interview_name, interviewer, job_applicant):
|
||||
import json
|
||||
|
||||
from six import string_types
|
||||
|
||||
if isinstance(data, string_types):
|
||||
data = frappe._dict(json.loads(data))
|
||||
|
||||
if frappe.session.user != interviewer:
|
||||
frappe.throw(_('Only Interviewer Are allowed to submit Interview Feedback'))
|
||||
|
||||
interview_feedback = frappe.new_doc('Interview Feedback')
|
||||
interview_feedback.interview = interview_name
|
||||
interview_feedback.interviewer = interviewer
|
||||
interview_feedback.job_applicant = job_applicant
|
||||
|
||||
for d in data.skill_set:
|
||||
d = frappe._dict(d)
|
||||
interview_feedback.append('skill_assessment', {'skill': d.skill, 'rating': d.rating})
|
||||
|
||||
interview_feedback.feedback = data.feedback
|
||||
interview_feedback.result = data.result
|
||||
|
||||
interview_feedback.save()
|
||||
interview_feedback.submit()
|
||||
|
||||
frappe.msgprint(_('Interview Feedback {0} submitted successfully').format(
|
||||
get_link_to_form('Interview Feedback', interview_feedback.name)))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_interviewer_list(doctype, txt, searchfield, start, page_len, filters):
|
||||
filters = [
|
||||
['Has Role', 'parent', 'like', '%{}%'.format(txt)],
|
||||
['Has Role', 'role', '=', 'interviewer'],
|
||||
['Has Role', 'parenttype', '=', 'User']
|
||||
]
|
||||
|
||||
if filters and isinstance(filters, list):
|
||||
filters.extend(filters)
|
||||
|
||||
return frappe.get_all('Has Role', limit_start=start, limit_page_length=page_len,
|
||||
filters=filters, fields = ['parent'], as_list=1)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_events(start, end, filters=None):
|
||||
"""Returns events for Gantt / Calendar view rendering.
|
||||
|
||||
:param start: Start date-time.
|
||||
:param end: End date-time.
|
||||
:param filters: Filters (JSON).
|
||||
"""
|
||||
from frappe.desk.calendar import get_event_conditions
|
||||
|
||||
events = []
|
||||
|
||||
event_color = {
|
||||
"Pending": "#fff4f0",
|
||||
"Under Review": "#d3e8fc",
|
||||
"Cleared": "#eaf5ed",
|
||||
"Rejected": "#fce7e7"
|
||||
}
|
||||
|
||||
conditions = get_event_conditions('Interview', filters)
|
||||
|
||||
interviews = frappe.db.sql("""
|
||||
SELECT DISTINCT
|
||||
`tabInterview`.name, `tabInterview`.job_applicant, `tabInterview`.interview_round,
|
||||
`tabInterview`.scheduled_on, `tabInterview`.status, `tabInterview`.from_time as from_time,
|
||||
`tabInterview`.to_time as to_time
|
||||
from
|
||||
`tabInterview`
|
||||
where
|
||||
(`tabInterview`.scheduled_on between %(start)s and %(end)s)
|
||||
and docstatus != 2
|
||||
{conditions}
|
||||
""".format(conditions=conditions), {
|
||||
"start": start,
|
||||
"end": end
|
||||
}, as_dict=True, update={"allDay": 0})
|
||||
|
||||
for d in interviews:
|
||||
subject_data = []
|
||||
for field in ["name", "job_applicant", "interview_round"]:
|
||||
if not d.get(field):
|
||||
continue
|
||||
subject_data.append(d.get(field))
|
||||
|
||||
color = event_color.get(d.status)
|
||||
interview_data = {
|
||||
'from': get_datetime('%s %s' % (d.scheduled_on, d.from_time or '00:00:00')),
|
||||
'to': get_datetime('%s %s' % (d.scheduled_on, d.to_time or '00:00:00')),
|
||||
'name': d.name,
|
||||
'subject': '\n'.join(subject_data),
|
||||
'color': color if color else "#89bcde"
|
||||
}
|
||||
|
||||
events.append(interview_data)
|
||||
|
||||
return events
|
||||
14
erpnext/hr/doctype/interview/interview_calendar.js
Normal file
14
erpnext/hr/doctype/interview/interview_calendar.js
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
frappe.views.calendar['Interview'] = {
|
||||
field_map: {
|
||||
'start': 'from',
|
||||
'end': 'to',
|
||||
'id': 'name',
|
||||
'title': 'subject',
|
||||
'allDay': 'allDay',
|
||||
'color': 'color'
|
||||
},
|
||||
order_by: 'scheduled_on',
|
||||
gantt: true,
|
||||
get_events_method: 'erpnext.hr.doctype.interview.interview.get_events'
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
<h1>Interview Feedback Reminder</h1>
|
||||
|
||||
<p>
|
||||
Interview Feedback for Interview {{ name }} is not submitted yet. Please submit your feedback. Thank you, good day!
|
||||
</p>
|
||||
12
erpnext/hr/doctype/interview/interview_list.js
Normal file
12
erpnext/hr/doctype/interview/interview_list.js
Normal file
@@ -0,0 +1,12 @@
|
||||
frappe.listview_settings['Interview'] = {
|
||||
has_indicator_for_draft: 1,
|
||||
get_indicator: function(doc) {
|
||||
let status_color = {
|
||||
'Pending': 'orange',
|
||||
'Under Review': 'blue',
|
||||
'Cleared': 'green',
|
||||
'Rejected': 'red',
|
||||
};
|
||||
return [__(doc.status), status_color[doc.status], 'status,=,'+doc.status];
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
<h1>Interview Reminder</h1>
|
||||
|
||||
<p>
|
||||
Interview: {{name}} is scheduled on {{scheduled_on}} from {{from_time}} to {{to_time}}
|
||||
</p>
|
||||
174
erpnext/hr/doctype/interview/test_interview.py
Normal file
174
erpnext/hr/doctype/interview/test_interview.py
Normal file
@@ -0,0 +1,174 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.core.doctype.user_permission.test_user_permission import create_user
|
||||
from frappe.utils import add_days, getdate, nowtime
|
||||
|
||||
from erpnext.hr.doctype.designation.test_designation import create_designation
|
||||
from erpnext.hr.doctype.interview.interview import DuplicateInterviewRoundError
|
||||
from erpnext.hr.doctype.job_applicant.test_job_applicant import create_job_applicant
|
||||
|
||||
|
||||
class TestInterview(unittest.TestCase):
|
||||
def test_validations_for_designation(self):
|
||||
job_applicant = create_job_applicant()
|
||||
interview = create_interview_and_dependencies(job_applicant.name, designation='_Test_Sales_manager', save=0)
|
||||
self.assertRaises(DuplicateInterviewRoundError, interview.save)
|
||||
|
||||
def test_notification_on_rescheduling(self):
|
||||
job_applicant = create_job_applicant()
|
||||
interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=add_days(getdate(), -4))
|
||||
|
||||
previous_scheduled_date = interview.scheduled_on
|
||||
frappe.db.sql("DELETE FROM `tabEmail Queue`")
|
||||
|
||||
interview.reschedule_interview(add_days(getdate(previous_scheduled_date), 2),
|
||||
from_time=nowtime(), to_time=nowtime())
|
||||
interview.reload()
|
||||
|
||||
self.assertEqual(interview.scheduled_on, add_days(getdate(previous_scheduled_date), 2))
|
||||
|
||||
notification = frappe.get_all("Email Queue", filters={"message": ("like", "%Your Interview session is rescheduled from%")})
|
||||
self.assertIsNotNone(notification)
|
||||
|
||||
def test_notification_for_scheduling(self):
|
||||
from erpnext.hr.doctype.interview.interview import send_interview_reminder
|
||||
|
||||
setup_reminder_settings()
|
||||
|
||||
job_applicant = create_job_applicant()
|
||||
scheduled_on = datetime.datetime.now() + datetime.timedelta(minutes=10)
|
||||
|
||||
interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=scheduled_on)
|
||||
|
||||
frappe.db.sql("DELETE FROM `tabEmail Queue`")
|
||||
send_interview_reminder()
|
||||
|
||||
interview.reload()
|
||||
|
||||
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
|
||||
self.assertTrue("Subject: Interview Reminder" in email_queue[0].message)
|
||||
|
||||
def test_notification_for_feedback_submission(self):
|
||||
from erpnext.hr.doctype.interview.interview import send_daily_feedback_reminder
|
||||
|
||||
setup_reminder_settings()
|
||||
|
||||
job_applicant = create_job_applicant()
|
||||
scheduled_on = add_days(getdate(), -4)
|
||||
create_interview_and_dependencies(job_applicant.name, scheduled_on=scheduled_on)
|
||||
|
||||
frappe.db.sql("DELETE FROM `tabEmail Queue`")
|
||||
send_daily_feedback_reminder()
|
||||
|
||||
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
|
||||
self.assertTrue("Subject: Interview Feedback Reminder" in email_queue[0].message)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
|
||||
def create_interview_and_dependencies(job_applicant, scheduled_on=None, from_time=None, to_time=None, designation=None, save=1):
|
||||
if designation:
|
||||
designation=create_designation(designation_name = "_Test_Sales_manager").name
|
||||
|
||||
interviewer_1 = create_user("test_interviewer1@example.com", "Interviewer")
|
||||
interviewer_2 = create_user("test_interviewer2@example.com", "Interviewer")
|
||||
|
||||
interview_round = create_interview_round(
|
||||
"Technical Round", ["Python", "JS"],
|
||||
designation=designation, save=True
|
||||
)
|
||||
|
||||
interview = frappe.new_doc("Interview")
|
||||
interview.interview_round = interview_round.name
|
||||
interview.job_applicant = job_applicant
|
||||
interview.scheduled_on = scheduled_on or getdate()
|
||||
interview.from_time = from_time or nowtime()
|
||||
interview.to_time = to_time or nowtime()
|
||||
|
||||
interview.append("interview_details", {"interviewer": interviewer_1.name})
|
||||
interview.append("interview_details", {"interviewer": interviewer_2.name})
|
||||
|
||||
if save:
|
||||
interview.save()
|
||||
|
||||
return interview
|
||||
|
||||
def create_interview_round(name, skill_set, interviewers=[], designation=None, save=True):
|
||||
create_skill_set(skill_set)
|
||||
interview_round = frappe.new_doc("Interview Round")
|
||||
interview_round.round_name = name
|
||||
interview_round.interview_type = create_interview_type()
|
||||
interview_round.expected_average_rating = 4
|
||||
if designation:
|
||||
interview_round.designation = designation
|
||||
|
||||
for skill in skill_set:
|
||||
interview_round.append("expected_skill_set", {"skill": skill})
|
||||
|
||||
for interviewer in interviewers:
|
||||
interview_round.append("interviewer", {
|
||||
"user": interviewer
|
||||
})
|
||||
|
||||
if save:
|
||||
interview_round.save()
|
||||
|
||||
return interview_round
|
||||
|
||||
def create_skill_set(skill_set):
|
||||
for skill in skill_set:
|
||||
if not frappe.db.exists("Skill", skill):
|
||||
doc = frappe.new_doc("Skill")
|
||||
doc.skill_name = skill
|
||||
doc.save()
|
||||
|
||||
def create_interview_type(name="test_interview_type"):
|
||||
if frappe.db.exists("Interview Type", name):
|
||||
return frappe.get_doc("Interview Type", name).name
|
||||
else:
|
||||
doc = frappe.new_doc("Interview Type")
|
||||
doc.name = name
|
||||
doc.description = "_Test_Description"
|
||||
doc.save()
|
||||
|
||||
return doc.name
|
||||
|
||||
def setup_reminder_settings():
|
||||
if not frappe.db.exists('Email Template', _('Interview Reminder')):
|
||||
base_path = frappe.get_app_path('erpnext', 'hr', 'doctype')
|
||||
response = frappe.read_file(os.path.join(base_path, 'interview/interview_reminder_notification_template.html'))
|
||||
|
||||
frappe.get_doc({
|
||||
'doctype': 'Email Template',
|
||||
'name': _('Interview Reminder'),
|
||||
'response': response,
|
||||
'subject': _('Interview Reminder'),
|
||||
'owner': frappe.session.user,
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
if not frappe.db.exists('Email Template', _('Interview Feedback Reminder')):
|
||||
base_path = frappe.get_app_path('erpnext', 'hr', 'doctype')
|
||||
response = frappe.read_file(os.path.join(base_path, 'interview/interview_feedback_reminder_template.html'))
|
||||
|
||||
frappe.get_doc({
|
||||
'doctype': 'Email Template',
|
||||
'name': _('Interview Feedback Reminder'),
|
||||
'response': response,
|
||||
'subject': _('Interview Feedback Reminder'),
|
||||
'owner': frappe.session.user,
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
hr_settings = frappe.get_doc('HR Settings')
|
||||
hr_settings.interview_reminder_template = _('Interview Reminder')
|
||||
hr_settings.feedback_reminder_notification_template = _('Interview Feedback Reminder')
|
||||
hr_settings.save()
|
||||
0
erpnext/hr/doctype/interview_detail/__init__.py
Normal file
0
erpnext/hr/doctype/interview_detail/__init__.py
Normal file
8
erpnext/hr/doctype/interview_detail/interview_detail.js
Normal file
8
erpnext/hr/doctype/interview_detail/interview_detail.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Interview Detail', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
74
erpnext/hr/doctype/interview_detail/interview_detail.json
Normal file
74
erpnext/hr/doctype/interview_detail/interview_detail.json
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2021-04-12 16:24:10.382863",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"interviewer",
|
||||
"interview_feedback",
|
||||
"average_rating",
|
||||
"result",
|
||||
"column_break_4",
|
||||
"comments"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "interviewer",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Interviewer",
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "interview_feedback",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Interview Feedback",
|
||||
"options": "Interview Feedback",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "average_rating",
|
||||
"fieldtype": "Rating",
|
||||
"in_list_view": 1,
|
||||
"label": "Average Rating",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fetch_from": "interview_feedback.feedback",
|
||||
"fieldname": "comments",
|
||||
"fieldtype": "Text",
|
||||
"label": "Comments",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "result",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Result",
|
||||
"options": "\nCleared\nRejected",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-29 13:13:25.865063",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Interview Detail",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
12
erpnext/hr/doctype/interview_detail/interview_detail.py
Normal file
12
erpnext/hr/doctype/interview_detail/interview_detail.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class InterviewDetail(Document):
|
||||
pass
|
||||
11
erpnext/hr/doctype/interview_detail/test_interview_detail.py
Normal file
11
erpnext/hr/doctype/interview_detail/test_interview_detail.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
class TestInterviewDetail(unittest.TestCase):
|
||||
pass
|
||||
0
erpnext/hr/doctype/interview_feedback/__init__.py
Normal file
0
erpnext/hr/doctype/interview_feedback/__init__.py
Normal file
54
erpnext/hr/doctype/interview_feedback/interview_feedback.js
Normal file
54
erpnext/hr/doctype/interview_feedback/interview_feedback.js
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Interview Feedback', {
|
||||
onload: function(frm) {
|
||||
frm.ignore_doctypes_on_cancel_all = ['Interview'];
|
||||
|
||||
frm.set_query('interview', function() {
|
||||
return {
|
||||
filters: {
|
||||
docstatus: ['!=', 2]
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
interview_round: function(frm) {
|
||||
frappe.call({
|
||||
method: 'erpnext.hr.doctype.interview.interview.get_expected_skill_set',
|
||||
args: {
|
||||
interview_round: frm.doc.interview_round
|
||||
},
|
||||
callback: function(r) {
|
||||
frm.set_value('skill_assessment', r.message);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
interview: function(frm) {
|
||||
frappe.call({
|
||||
method: 'erpnext.hr.doctype.interview_feedback.interview_feedback.get_applicable_interviewers',
|
||||
args: {
|
||||
interview: frm.doc.interview || ''
|
||||
},
|
||||
callback: function(r) {
|
||||
frm.set_query('interviewer', function() {
|
||||
return {
|
||||
filters: {
|
||||
name: ['in', r.message]
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
interviewer: function(frm) {
|
||||
if (!frm.doc.interview) {
|
||||
frappe.throw(__('Select Interview first'));
|
||||
frm.set_value('interviewer', '');
|
||||
}
|
||||
}
|
||||
});
|
||||
171
erpnext/hr/doctype/interview_feedback/interview_feedback.json
Normal file
171
erpnext/hr/doctype/interview_feedback/interview_feedback.json
Normal file
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "HR-INT-FEED-.####",
|
||||
"creation": "2021-04-12 17:03:13.833285",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"details_section",
|
||||
"interview",
|
||||
"interview_round",
|
||||
"job_applicant",
|
||||
"column_break_3",
|
||||
"interviewer",
|
||||
"result",
|
||||
"section_break_4",
|
||||
"skill_assessment",
|
||||
"average_rating",
|
||||
"section_break_7",
|
||||
"feedback",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"fieldname": "interview",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Interview",
|
||||
"options": "Interview",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"fetch_from": "interview.interview_round",
|
||||
"fieldname": "interview_round",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Interview Round",
|
||||
"options": "Interview Round",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"fieldname": "interviewer",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Interviewer",
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_4",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Skill Assessment"
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"fieldname": "skill_assessment",
|
||||
"fieldtype": "Table",
|
||||
"options": "Skill Assessment",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"fieldname": "average_rating",
|
||||
"fieldtype": "Rating",
|
||||
"in_list_view": 1,
|
||||
"label": "Average Rating",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Feedback"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Interview Feedback",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"fieldname": "feedback",
|
||||
"fieldtype": "Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "result",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Result",
|
||||
"options": "\nCleared\nRejected",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Details"
|
||||
},
|
||||
{
|
||||
"fetch_from": "interview.job_applicant",
|
||||
"fieldname": "job_applicant",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Job Applicant",
|
||||
"options": "Job Applicant",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-30 13:30:49.955352",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Interview Feedback",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "HR Manager",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Interviewer",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "HR User",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "interviewer",
|
||||
"track_changes": 1
|
||||
}
|
||||
88
erpnext/hr/doctype/interview_feedback/interview_feedback.py
Normal file
88
erpnext/hr/doctype/interview_feedback/interview_feedback.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import flt, get_link_to_form, getdate
|
||||
|
||||
|
||||
class InterviewFeedback(Document):
|
||||
def validate(self):
|
||||
self.validate_interviewer()
|
||||
self.validate_interview_date()
|
||||
self.validate_duplicate()
|
||||
self.calculate_average_rating()
|
||||
|
||||
def on_submit(self):
|
||||
self.update_interview_details()
|
||||
|
||||
def on_cancel(self):
|
||||
self.update_interview_details()
|
||||
|
||||
def validate_interviewer(self):
|
||||
applicable_interviewers = get_applicable_interviewers(self.interview)
|
||||
if self.interviewer not in applicable_interviewers:
|
||||
frappe.throw(_('{0} is not allowed to submit Interview Feedback for the Interview: {1}').format(
|
||||
frappe.bold(self.interviewer), frappe.bold(self.interview)))
|
||||
|
||||
def validate_interview_date(self):
|
||||
scheduled_date = frappe.db.get_value('Interview', self.interview, 'scheduled_on')
|
||||
|
||||
if getdate() < getdate(scheduled_date) and self.docstatus == 1:
|
||||
frappe.throw(_('{0} submission before {1} is not allowed').format(
|
||||
frappe.bold('Interview Feedback'),
|
||||
frappe.bold('Interview Scheduled Date')
|
||||
))
|
||||
|
||||
def validate_duplicate(self):
|
||||
duplicate_feedback = frappe.db.exists('Interview Feedback', {
|
||||
'interviewer': self.interviewer,
|
||||
'interview': self.interview,
|
||||
'docstatus': 1
|
||||
})
|
||||
|
||||
if duplicate_feedback:
|
||||
frappe.throw(_('Feedback already submitted for the Interview {0}. Please cancel the previous Interview Feedback {1} to continue.').format(
|
||||
self.interview, get_link_to_form('Interview Feedback', duplicate_feedback)))
|
||||
|
||||
def calculate_average_rating(self):
|
||||
total_rating = 0
|
||||
for d in self.skill_assessment:
|
||||
if d.rating:
|
||||
total_rating += d.rating
|
||||
|
||||
self.average_rating = flt(total_rating / len(self.skill_assessment) if len(self.skill_assessment) else 0)
|
||||
|
||||
def update_interview_details(self):
|
||||
doc = frappe.get_doc('Interview', self.interview)
|
||||
total_rating = 0
|
||||
|
||||
if self.docstatus == 2:
|
||||
for entry in doc.interview_details:
|
||||
if entry.interview_feedback == self.name:
|
||||
entry.average_rating = entry.interview_feedback = entry.comments = entry.result = None
|
||||
break
|
||||
else:
|
||||
for entry in doc.interview_details:
|
||||
if entry.interviewer == self.interviewer:
|
||||
entry.average_rating = self.average_rating
|
||||
entry.interview_feedback = self.name
|
||||
entry.comments = self.feedback
|
||||
entry.result = self.result
|
||||
|
||||
if entry.average_rating:
|
||||
total_rating += entry.average_rating
|
||||
|
||||
doc.average_rating = flt(total_rating / len(doc.interview_details) if len(doc.interview_details) else 0)
|
||||
doc.save()
|
||||
doc.notify_update()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_applicable_interviewers(interview):
|
||||
data = frappe.get_all('Interview Detail', filters={'parent': interview}, fields=['interviewer'])
|
||||
return [d.interviewer for d in data]
|
||||
103
erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
Normal file
103
erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
Normal file
@@ -0,0 +1,103 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, flt, getdate
|
||||
|
||||
from erpnext.hr.doctype.interview.test_interview import (
|
||||
create_interview_and_dependencies,
|
||||
create_skill_set,
|
||||
)
|
||||
from erpnext.hr.doctype.job_applicant.test_job_applicant import create_job_applicant
|
||||
|
||||
|
||||
class TestInterviewFeedback(unittest.TestCase):
|
||||
def test_validation_for_skill_set(self):
|
||||
frappe.set_user("Administrator")
|
||||
job_applicant = create_job_applicant()
|
||||
interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=add_days(getdate(), -1))
|
||||
skill_ratings = get_skills_rating(interview.interview_round)
|
||||
|
||||
interviewer = interview.interview_details[0].interviewer
|
||||
create_skill_set(['Leadership'])
|
||||
|
||||
interview_feedback = create_interview_feedback(interview.name, interviewer, skill_ratings)
|
||||
interview_feedback.append("skill_assessment", {"skill": 'Leadership', 'rating': 4})
|
||||
frappe.set_user(interviewer)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, interview_feedback.save)
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def test_average_ratings_on_feedback_submission_and_cancellation(self):
|
||||
job_applicant = create_job_applicant()
|
||||
interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=add_days(getdate(), -1))
|
||||
skill_ratings = get_skills_rating(interview.interview_round)
|
||||
|
||||
# For First Interviewer Feedback
|
||||
interviewer = interview.interview_details[0].interviewer
|
||||
frappe.set_user(interviewer)
|
||||
|
||||
# calculating Average
|
||||
feedback_1 = create_interview_feedback(interview.name, interviewer, skill_ratings)
|
||||
|
||||
total_rating = 0
|
||||
for d in feedback_1.skill_assessment:
|
||||
if d.rating:
|
||||
total_rating += d.rating
|
||||
|
||||
avg_rating = flt(total_rating / len(feedback_1.skill_assessment) if len(feedback_1.skill_assessment) else 0)
|
||||
|
||||
self.assertEqual(flt(avg_rating, 3), feedback_1.average_rating)
|
||||
|
||||
avg_on_interview_detail = frappe.db.get_value('Interview Detail', {
|
||||
'parent': feedback_1.interview,
|
||||
'interviewer': feedback_1.interviewer,
|
||||
'interview_feedback': feedback_1.name
|
||||
}, 'average_rating')
|
||||
|
||||
# 1. average should be reflected in Interview Detail.
|
||||
self.assertEqual(avg_on_interview_detail, round(feedback_1.average_rating))
|
||||
|
||||
'''For Second Interviewer Feedback'''
|
||||
interviewer = interview.interview_details[1].interviewer
|
||||
frappe.set_user(interviewer)
|
||||
|
||||
feedback_2 = create_interview_feedback(interview.name, interviewer, skill_ratings)
|
||||
interview.reload()
|
||||
|
||||
feedback_2.cancel()
|
||||
interview.reload()
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
|
||||
def create_interview_feedback(interview, interviewer, skills_ratings):
|
||||
interview_feedback = frappe.new_doc("Interview Feedback")
|
||||
interview_feedback.interview = interview
|
||||
interview_feedback.interviewer = interviewer
|
||||
interview_feedback.result = "Cleared"
|
||||
|
||||
for rating in skills_ratings:
|
||||
interview_feedback.append("skill_assessment", rating)
|
||||
|
||||
interview_feedback.save()
|
||||
interview_feedback.submit()
|
||||
|
||||
return interview_feedback
|
||||
|
||||
|
||||
def get_skills_rating(interview_round):
|
||||
import random
|
||||
|
||||
skills = frappe.get_all("Expected Skill Set", filters={"parent": interview_round}, fields = ["skill"])
|
||||
for d in skills:
|
||||
d["rating"] = random.randint(1, 5)
|
||||
return skills
|
||||
0
erpnext/hr/doctype/interview_round/__init__.py
Normal file
0
erpnext/hr/doctype/interview_round/__init__.py
Normal file
24
erpnext/hr/doctype/interview_round/interview_round.js
Normal file
24
erpnext/hr/doctype/interview_round/interview_round.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Interview Round", {
|
||||
refresh: function(frm) {
|
||||
if (!frm.doc.__islocal) {
|
||||
frm.add_custom_button(__("Create Interview"), function() {
|
||||
frm.events.create_interview(frm);
|
||||
});
|
||||
}
|
||||
},
|
||||
create_interview: function(frm) {
|
||||
frappe.call({
|
||||
method: "erpnext.hr.doctype.interview_round.interview_round.create_interview",
|
||||
args: {
|
||||
doc: frm.doc
|
||||
},
|
||||
callback: function (r) {
|
||||
var doclist = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user