Merge branch 'develop' into care
This commit is contained in:
commit
a1d3a09d3c
2
.github/helper/documentation.py
vendored
2
.github/helper/documentation.py
vendored
@ -24,6 +24,8 @@ def docs_link_exists(body):
|
|||||||
parts = parsed_url.path.split('/')
|
parts = parsed_url.path.split('/')
|
||||||
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
|
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
|
||||||
return True
|
return True
|
||||||
|
elif parsed_url.netloc == "docs.erpnext.com":
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
13
.github/helper/semgrep_rules/report.yml
vendored
13
.github/helper/semgrep_rules/report.yml
vendored
@ -19,3 +19,16 @@ rules:
|
|||||||
languages: [python]
|
languages: [python]
|
||||||
severity: ERROR
|
severity: ERROR
|
||||||
|
|
||||||
|
- id: frappe-translated-values-in-business-logic
|
||||||
|
paths:
|
||||||
|
include:
|
||||||
|
- "**/report"
|
||||||
|
patterns:
|
||||||
|
- pattern-inside: |
|
||||||
|
{..., filters: [...], ...}
|
||||||
|
- pattern: |
|
||||||
|
{..., options: [..., __("..."), ...], ...}
|
||||||
|
message: |
|
||||||
|
Using translated values in options field will require you to translate the values while comparing in business logic. Instead of passing translated labels provide objects that contain both label and value. e.g. { label: __("Option value"), value: "Option value"}
|
||||||
|
languages: [javascript]
|
||||||
|
severity: ERROR
|
||||||
|
38
.github/workflows/server-tests.yml
vendored
38
.github/workflows/server-tests.yml
vendored
@ -99,34 +99,10 @@ jobs:
|
|||||||
CI_BUILD_ID: ${{ github.run_id }}
|
CI_BUILD_ID: ${{ github.run_id }}
|
||||||
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
|
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
|
||||||
|
|
||||||
- name: Upload Coverage Data
|
- name: Upload coverage data
|
||||||
run: |
|
uses: codecov/codecov-action@v2
|
||||||
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
|
with:
|
||||||
cd ${GITHUB_WORKSPACE}
|
name: MariaDB
|
||||||
pip3 install coverage==5.5
|
fail_ci_if_error: true
|
||||||
pip3 install coveralls==3.0.1
|
files: /home/runner/frappe-bench/sites/coverage.xml
|
||||||
coveralls
|
verbose: true
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
|
|
||||||
COVERALLS_FLAG_NAME: run-${{ matrix.container }}
|
|
||||||
COVERALLS_SERVICE_NAME: ${{ github.event_name == 'pull_request' && 'github' || 'github-actions' }}
|
|
||||||
COVERALLS_PARALLEL: true
|
|
||||||
|
|
||||||
coveralls:
|
|
||||||
name: Coverage Wrap Up
|
|
||||||
needs: test
|
|
||||||
container: python:3-slim
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Clone
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Coveralls Finished
|
|
||||||
run: |
|
|
||||||
cd ${GITHUB_WORKSPACE}
|
|
||||||
pip3 install coverage==5.5
|
|
||||||
pip3 install coveralls==3.0.1
|
|
||||||
coveralls --finish
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
8
.snyk
8
.snyk
@ -1,8 +0,0 @@
|
|||||||
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
|
|
||||||
version: v1.14.0
|
|
||||||
ignore: {}
|
|
||||||
# patches apply the minimum changes required to fix a vulnerability
|
|
||||||
patch:
|
|
||||||
SNYK-JS-LODASH-450202:
|
|
||||||
- cypress > getos > async > lodash:
|
|
||||||
patched: '2020-01-31T01:35:12.802Z'
|
|
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
[](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml)
|
[](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml)
|
||||||
[](https://www.codetriage.com/frappe/erpnext)
|
[](https://www.codetriage.com/frappe/erpnext)
|
||||||
[](https://coveralls.io/github/frappe/erpnext?branch=develop)
|
[](https://codecov.io/gh/frappe/erpnext)
|
||||||
|
|
||||||
[https://erpnext.com](https://erpnext.com)
|
[https://erpnext.com](https://erpnext.com)
|
||||||
|
|
||||||
|
17
codecov.yml
Normal file
17
codecov.yml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
codecov:
|
||||||
|
require_ci_to_pass: yes
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
project:
|
||||||
|
default:
|
||||||
|
target: auto
|
||||||
|
threshold: 0.5%
|
||||||
|
|
||||||
|
comment:
|
||||||
|
layout: "diff, files"
|
||||||
|
require_changes: true
|
||||||
|
after_n_builds: 3
|
||||||
|
|
||||||
|
ignore:
|
||||||
|
- "erpnext/demo"
|
@ -6,7 +6,7 @@ context('Organizational Chart', () => {
|
|||||||
|
|
||||||
it('navigates to org chart', () => {
|
it('navigates to org chart', () => {
|
||||||
cy.visit('/app');
|
cy.visit('/app');
|
||||||
cy.awesomebar('Organizational Chart');
|
cy.visit('/app/organizational-chart');
|
||||||
cy.url().should('include', '/organizational-chart');
|
cy.url().should('include', '/organizational-chart');
|
||||||
|
|
||||||
cy.window().its('frappe.csrf_token').then(csrf_token => {
|
cy.window().its('frappe.csrf_token').then(csrf_token => {
|
||||||
|
@ -7,7 +7,7 @@ context('Organizational Chart Mobile', () => {
|
|||||||
it('navigates to org chart', () => {
|
it('navigates to org chart', () => {
|
||||||
cy.viewport(375, 667);
|
cy.viewport(375, 667);
|
||||||
cy.visit('/app');
|
cy.visit('/app');
|
||||||
cy.awesomebar('Organizational Chart');
|
cy.visit('/app/organizational-chart');
|
||||||
cy.url().should('include', '/organizational-chart');
|
cy.url().should('include', '/organizational-chart');
|
||||||
|
|
||||||
cy.window().its('frappe.csrf_token').then(csrf_token => {
|
cy.window().its('frappe.csrf_token').then(csrf_token => {
|
||||||
|
@ -374,12 +374,15 @@ def make_gl_entries(doc, credit_account, debit_account, against,
|
|||||||
try:
|
try:
|
||||||
make_gl_entries(gl_entries, cancel=(doc.docstatus == 2), merge_entries=True)
|
make_gl_entries(gl_entries, cancel=(doc.docstatus == 2), merge_entries=True)
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
frappe.db.rollback()
|
if frappe.flags.in_test:
|
||||||
traceback = frappe.get_traceback()
|
raise e
|
||||||
frappe.log_error(message=traceback)
|
else:
|
||||||
|
frappe.db.rollback()
|
||||||
|
traceback = frappe.get_traceback()
|
||||||
|
frappe.log_error(message=traceback)
|
||||||
|
|
||||||
frappe.flags.deferred_accounting_error = True
|
frappe.flags.deferred_accounting_error = True
|
||||||
|
|
||||||
def send_mail(deferred_process):
|
def send_mail(deferred_process):
|
||||||
title = _("Error while processing deferred accounting for {0}").format(deferred_process)
|
title = _("Error while processing deferred accounting for {0}").format(deferred_process)
|
||||||
|
@ -12,11 +12,6 @@ frappe.ui.form.on('Chart of Accounts Importer', {
|
|||||||
frm.set_df_property('import_file_section', 'hidden', frm.doc.company ? 0 : 1);
|
frm.set_df_property('import_file_section', 'hidden', frm.doc.company ? 0 : 1);
|
||||||
frm.set_df_property('chart_preview', 'hidden',
|
frm.set_df_property('chart_preview', 'hidden',
|
||||||
$(frm.fields_dict['chart_tree'].wrapper).html()!="" ? 0 : 1);
|
$(frm.fields_dict['chart_tree'].wrapper).html()!="" ? 0 : 1);
|
||||||
|
|
||||||
// Show import button when file is successfully attached
|
|
||||||
if (frm.page && frm.page.show_import_button) {
|
|
||||||
create_import_button(frm);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
download_template: function(frm) {
|
download_template: function(frm) {
|
||||||
@ -78,8 +73,12 @@ frappe.ui.form.on('Chart of Accounts Importer', {
|
|||||||
frm.page.set_indicator("");
|
frm.page.set_indicator("");
|
||||||
$(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper on removing file
|
$(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper on removing file
|
||||||
} else {
|
} else {
|
||||||
generate_tree_preview(frm);
|
frappe.run_serially([
|
||||||
validate_csv_data(frm);
|
() => validate_coa(frm),
|
||||||
|
() => generate_tree_preview(frm),
|
||||||
|
() => create_import_button(frm),
|
||||||
|
() => frm.set_df_property('chart_preview', 'hidden', 0),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -104,42 +103,27 @@ frappe.ui.form.on('Chart of Accounts Importer', {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var validate_csv_data = function(frm) {
|
|
||||||
frappe.call({
|
|
||||||
method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.validate_accounts",
|
|
||||||
args: {file_name: frm.doc.import_file},
|
|
||||||
callback: function(r) {
|
|
||||||
if(r.message && r.message[0]===true) {
|
|
||||||
frm.page["show_import_button"] = true;
|
|
||||||
frm.page["total_accounts"] = r.message[1];
|
|
||||||
frm.trigger("refresh");
|
|
||||||
} else {
|
|
||||||
frm.page.set_indicator(__('Resolve error and upload again.'), 'orange');
|
|
||||||
frappe.throw(__(r.message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var create_import_button = function(frm) {
|
var create_import_button = function(frm) {
|
||||||
frm.page.set_primary_action(__("Import"), function () {
|
if (frm.page.show_import_button) {
|
||||||
frappe.call({
|
frm.page.set_primary_action(__("Import"), function () {
|
||||||
method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.import_coa",
|
return frappe.call({
|
||||||
args: {
|
method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.import_coa",
|
||||||
file_name: frm.doc.import_file,
|
args: {
|
||||||
company: frm.doc.company
|
file_name: frm.doc.import_file,
|
||||||
},
|
company: frm.doc.company
|
||||||
freeze: true,
|
},
|
||||||
freeze_message: __("Creating Accounts..."),
|
freeze: true,
|
||||||
callback: function(r) {
|
freeze_message: __("Creating Accounts..."),
|
||||||
if(!r.exc) {
|
callback: function(r) {
|
||||||
clearInterval(frm.page["interval"]);
|
if (!r.exc) {
|
||||||
frm.page.set_indicator(__('Import Successful'), 'blue');
|
clearInterval(frm.page["interval"]);
|
||||||
create_reset_button(frm);
|
frm.page.set_indicator(__('Import Successful'), 'blue');
|
||||||
|
create_reset_button(frm);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
}).addClass('btn btn-primary');
|
||||||
}).addClass('btn btn-primary');
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var create_reset_button = function(frm) {
|
var create_reset_button = function(frm) {
|
||||||
@ -150,24 +134,48 @@ var create_reset_button = function(frm) {
|
|||||||
}).addClass('btn btn-primary');
|
}).addClass('btn btn-primary');
|
||||||
};
|
};
|
||||||
|
|
||||||
var generate_tree_preview = function(frm) {
|
var validate_coa = function(frm) {
|
||||||
let parent = __('All Accounts');
|
if (frm.doc.import_file) {
|
||||||
$(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper to load new data
|
let parent = __('All Accounts');
|
||||||
|
|
||||||
// generate tree structure based on the csv data
|
return frappe.call({
|
||||||
new frappe.ui.Tree({
|
'method': 'erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa',
|
||||||
parent: $(frm.fields_dict['chart_tree'].wrapper),
|
'args': {
|
||||||
label: parent,
|
file_name: frm.doc.import_file,
|
||||||
expandable: true,
|
parent: parent,
|
||||||
method: 'erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa',
|
doctype: 'Chart of Accounts Importer',
|
||||||
args: {
|
file_type: frm.doc.file_type,
|
||||||
file_name: frm.doc.import_file,
|
for_validate: 1
|
||||||
parent: parent,
|
},
|
||||||
doctype: 'Chart of Accounts Importer',
|
callback: function(r) {
|
||||||
file_type: frm.doc.file_type
|
if (r.message['show_import_button']) {
|
||||||
},
|
frm.page['show_import_button'] = Boolean(r.message['show_import_button']);
|
||||||
onclick: function(node) {
|
}
|
||||||
parent = node.value;
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var generate_tree_preview = function(frm) {
|
||||||
|
if (frm.doc.import_file) {
|
||||||
|
let parent = __('All Accounts');
|
||||||
|
$(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper to load new data
|
||||||
|
|
||||||
|
// generate tree structure based on the csv data
|
||||||
|
return new frappe.ui.Tree({
|
||||||
|
parent: $(frm.fields_dict['chart_tree'].wrapper),
|
||||||
|
label: parent,
|
||||||
|
expandable: true,
|
||||||
|
method: 'erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa',
|
||||||
|
args: {
|
||||||
|
file_name: frm.doc.import_file,
|
||||||
|
parent: parent,
|
||||||
|
doctype: 'Chart of Accounts Importer',
|
||||||
|
file_type: frm.doc.file_type
|
||||||
|
},
|
||||||
|
onclick: function(node) {
|
||||||
|
parent = node.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -25,8 +25,16 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import
|
|||||||
|
|
||||||
|
|
||||||
class ChartofAccountsImporter(Document):
|
class ChartofAccountsImporter(Document):
|
||||||
def validate(self):
|
pass
|
||||||
validate_accounts(self.import_file)
|
|
||||||
|
def validate_columns(data):
|
||||||
|
if not data:
|
||||||
|
frappe.throw(_('No data found. Seems like you uploaded a blank file'))
|
||||||
|
|
||||||
|
no_of_columns = max([len(d) for d in data])
|
||||||
|
|
||||||
|
if no_of_columns > 7:
|
||||||
|
frappe.throw(_('More columns found than expected. Please compare the uploaded file with standard template'))
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def validate_company(company):
|
def validate_company(company):
|
||||||
@ -56,6 +64,7 @@ def import_coa(file_name, company):
|
|||||||
else:
|
else:
|
||||||
data = generate_data_from_excel(file_doc, extension)
|
data = generate_data_from_excel(file_doc, extension)
|
||||||
|
|
||||||
|
frappe.local.flags.ignore_root_company_validation = True
|
||||||
forest = build_forest(data)
|
forest = build_forest(data)
|
||||||
create_charts(company, custom_chart=forest)
|
create_charts(company, custom_chart=forest)
|
||||||
|
|
||||||
@ -120,7 +129,7 @@ def generate_data_from_excel(file_doc, extension, as_dict=False):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_coa(doctype, parent, is_root=False, file_name=None):
|
def get_coa(doctype, parent, is_root=False, file_name=None, for_validate=0):
|
||||||
''' called by tree view (to fetch node's children) '''
|
''' called by tree view (to fetch node's children) '''
|
||||||
|
|
||||||
file_doc, extension = get_file(file_name)
|
file_doc, extension = get_file(file_name)
|
||||||
@ -131,13 +140,21 @@ def get_coa(doctype, parent, is_root=False, file_name=None):
|
|||||||
else:
|
else:
|
||||||
data = generate_data_from_excel(file_doc, extension)
|
data = generate_data_from_excel(file_doc, extension)
|
||||||
|
|
||||||
forest = build_forest(data)
|
validate_columns(data)
|
||||||
accounts = build_tree_from_json("", chart_data=forest) # returns alist of dict in a tree render-able form
|
validate_accounts(file_doc, extension)
|
||||||
|
|
||||||
# filter out to show data for the selected node only
|
if not for_validate:
|
||||||
accounts = [d for d in accounts if d['parent_account']==parent]
|
forest = build_forest(data)
|
||||||
|
accounts = build_tree_from_json("", chart_data=forest) # returns a list of dict in a tree render-able form
|
||||||
|
|
||||||
return accounts
|
# filter out to show data for the selected node only
|
||||||
|
accounts = [d for d in accounts if d['parent_account']==parent]
|
||||||
|
|
||||||
|
return accounts
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'show_import_button': 1
|
||||||
|
}
|
||||||
|
|
||||||
def build_forest(data):
|
def build_forest(data):
|
||||||
'''
|
'''
|
||||||
@ -294,10 +311,7 @@ def get_sample_template(writer):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def validate_accounts(file_name):
|
def validate_accounts(file_doc, extension):
|
||||||
|
|
||||||
file_doc, extension = get_file(file_name)
|
|
||||||
|
|
||||||
if extension == 'csv':
|
if extension == 'csv':
|
||||||
accounts = generate_data_from_csv(file_doc, as_dict=True)
|
accounts = generate_data_from_csv(file_doc, as_dict=True)
|
||||||
else:
|
else:
|
||||||
@ -316,15 +330,10 @@ def validate_accounts(file_name):
|
|||||||
|
|
||||||
validate_root(accounts_dict)
|
validate_root(accounts_dict)
|
||||||
|
|
||||||
validate_account_types(accounts_dict)
|
|
||||||
|
|
||||||
return [True, len(accounts)]
|
return [True, len(accounts)]
|
||||||
|
|
||||||
def validate_root(accounts):
|
def validate_root(accounts):
|
||||||
roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')]
|
roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')]
|
||||||
if len(roots) < 4:
|
|
||||||
frappe.throw(_("Number of root accounts cannot be less than 4"))
|
|
||||||
|
|
||||||
error_messages = []
|
error_messages = []
|
||||||
|
|
||||||
for account in roots:
|
for account in roots:
|
||||||
@ -333,9 +342,19 @@ def validate_root(accounts):
|
|||||||
elif account.get("root_type") not in get_root_types() and account.get("account_name"):
|
elif account.get("root_type") not in get_root_types() and account.get("account_name"):
|
||||||
error_messages.append(_("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(account.get("account_name")))
|
error_messages.append(_("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(account.get("account_name")))
|
||||||
|
|
||||||
|
validate_missing_roots(roots)
|
||||||
|
|
||||||
if error_messages:
|
if error_messages:
|
||||||
frappe.throw("<br>".join(error_messages))
|
frappe.throw("<br>".join(error_messages))
|
||||||
|
|
||||||
|
def validate_missing_roots(roots):
|
||||||
|
root_types_added = set(d.get('root_type') for d in roots)
|
||||||
|
|
||||||
|
missing = list(set(get_root_types()) - root_types_added)
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
frappe.throw(_("Please add Root Account for - {0}").format(' , '.join(missing)))
|
||||||
|
|
||||||
def get_root_types():
|
def get_root_types():
|
||||||
return ('Asset', 'Liability', 'Expense', 'Income', 'Equity')
|
return ('Asset', 'Liability', 'Expense', 'Income', 'Equity')
|
||||||
|
|
||||||
@ -361,23 +380,6 @@ def get_mandatory_account_types():
|
|||||||
{'account_type': 'Stock', 'root_type': 'Asset'}
|
{'account_type': 'Stock', 'root_type': 'Asset'}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def validate_account_types(accounts):
|
|
||||||
account_types_for_ledger = ["Cost of Goods Sold", "Depreciation", "Fixed Asset", "Payable", "Receivable", "Stock Adjustment"]
|
|
||||||
account_types = [accounts[d]["account_type"] for d in accounts if not accounts[d]['is_group'] == 1]
|
|
||||||
|
|
||||||
missing = list(set(account_types_for_ledger) - set(account_types))
|
|
||||||
if missing:
|
|
||||||
frappe.throw(_("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing)))
|
|
||||||
|
|
||||||
account_types_for_group = ["Bank", "Cash", "Stock"]
|
|
||||||
# fix logic bug
|
|
||||||
account_groups = [accounts[d]["account_type"] for d in accounts if accounts[d]['is_group'] == 1]
|
|
||||||
|
|
||||||
missing = list(set(account_types_for_group) - set(account_groups))
|
|
||||||
if missing:
|
|
||||||
frappe.throw(_("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing)))
|
|
||||||
|
|
||||||
def unset_existing_data(company):
|
def unset_existing_data(company):
|
||||||
linked = frappe.db.sql('''select fieldname from tabDocField
|
linked = frappe.db.sql('''select fieldname from tabDocField
|
||||||
where fieldtype="Link" and options="Account" and parent="Company"''', as_dict=True)
|
where fieldtype="Link" and options="Account" and parent="Company"''', as_dict=True)
|
||||||
|
@ -13,7 +13,7 @@ def get_data():
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': _('References'),
|
'label': _('References'),
|
||||||
'items': ['Period Closing Voucher', 'Tax Withholding Category']
|
'items': ['Period Closing Voucher']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': _('Target Details'),
|
'label': _('Target Details'),
|
||||||
|
@ -1,63 +1,33 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2014-10-02 13:35:44.155278",
|
"creation": "2014-10-02 13:35:44.155278",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "Setup",
|
"document_type": "Setup",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"company"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"fieldname": "company",
|
"fieldname": "company",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
"ignore_user_permissions": 1,
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Company",
|
"label": "Company",
|
||||||
"length": 0,
|
"options": "Company"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Company",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hide_heading": 0,
|
"index_web_pages_for_search": 1,
|
||||||
"hide_toolbar": 0,
|
|
||||||
"idx": 0,
|
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"max_attachments": 0,
|
"links": [],
|
||||||
"modified": "2016-07-11 03:28:00.505946",
|
"modified": "2021-09-28 18:01:53.495929",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Fiscal Year Company",
|
"name": "Fiscal Year Company",
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_seen": 0
|
"track_changes": 1
|
||||||
}
|
}
|
@ -13,10 +13,12 @@
|
|||||||
"voucher_type",
|
"voucher_type",
|
||||||
"naming_series",
|
"naming_series",
|
||||||
"finance_book",
|
"finance_book",
|
||||||
|
"tax_withholding_category",
|
||||||
"column_break1",
|
"column_break1",
|
||||||
"from_template",
|
"from_template",
|
||||||
"company",
|
"company",
|
||||||
"posting_date",
|
"posting_date",
|
||||||
|
"apply_tds",
|
||||||
"2_add_edit_gl_entries",
|
"2_add_edit_gl_entries",
|
||||||
"accounts",
|
"accounts",
|
||||||
"section_break99",
|
"section_break99",
|
||||||
@ -498,16 +500,32 @@
|
|||||||
"options": "Journal Entry Template",
|
"options": "Journal Entry Template",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"report_hide": 1
|
"report_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.apply_tds",
|
||||||
|
"fieldname": "tax_withholding_category",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Tax Withholding Category",
|
||||||
|
"mandatory_depends_on": "eval:doc.apply_tds",
|
||||||
|
"options": "Tax Withholding Category"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "eval:['Credit Note', 'Debit Note'].includes(doc.voucher_type)",
|
||||||
|
"fieldname": "apply_tds",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Apply Tax Withholding Amount "
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
"idx": 176,
|
"idx": 176,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-10-30 13:56:01.121995",
|
"modified": "2021-09-09 15:31:14.484029",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Journal Entry",
|
"name": "Journal Entry",
|
||||||
|
"naming_rule": "By \"Naming Series\" field",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
@ -15,6 +15,9 @@ from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts
|
|||||||
from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import (
|
from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import (
|
||||||
get_party_account_based_on_invoice_discounting,
|
get_party_account_based_on_invoice_discounting,
|
||||||
)
|
)
|
||||||
|
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||||
|
get_party_tax_withholding_details,
|
||||||
|
)
|
||||||
from erpnext.accounts.party import get_party_account
|
from erpnext.accounts.party import get_party_account
|
||||||
from erpnext.accounts.utils import (
|
from erpnext.accounts.utils import (
|
||||||
check_if_stock_and_account_balance_synced,
|
check_if_stock_and_account_balance_synced,
|
||||||
@ -57,7 +60,8 @@ class JournalEntry(AccountsController):
|
|||||||
|
|
||||||
self.validate_against_jv()
|
self.validate_against_jv()
|
||||||
self.validate_reference_doc()
|
self.validate_reference_doc()
|
||||||
self.set_against_account()
|
if self.docstatus == 0:
|
||||||
|
self.set_against_account()
|
||||||
self.create_remarks()
|
self.create_remarks()
|
||||||
self.set_print_format_fields()
|
self.set_print_format_fields()
|
||||||
self.validate_expense_claim()
|
self.validate_expense_claim()
|
||||||
@ -66,6 +70,10 @@ class JournalEntry(AccountsController):
|
|||||||
self.set_account_and_party_balance()
|
self.set_account_and_party_balance()
|
||||||
self.validate_inter_company_accounts()
|
self.validate_inter_company_accounts()
|
||||||
self.validate_stock_accounts()
|
self.validate_stock_accounts()
|
||||||
|
|
||||||
|
if self.docstatus == 0:
|
||||||
|
self.apply_tax_withholding()
|
||||||
|
|
||||||
if not self.title:
|
if not self.title:
|
||||||
self.title = self.get_title()
|
self.title = self.get_title()
|
||||||
|
|
||||||
@ -139,6 +147,72 @@ class JournalEntry(AccountsController):
|
|||||||
frappe.throw(_("Account: {0} can only be updated via Stock Transactions")
|
frappe.throw(_("Account: {0} can only be updated via Stock Transactions")
|
||||||
.format(account), StockAccountInvalidTransaction)
|
.format(account), StockAccountInvalidTransaction)
|
||||||
|
|
||||||
|
def apply_tax_withholding(self):
|
||||||
|
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
|
||||||
|
|
||||||
|
if not self.apply_tds or self.voucher_type not in ('Debit Note', 'Credit Note'):
|
||||||
|
return
|
||||||
|
|
||||||
|
parties = [d.party for d in self.get('accounts') if d.party]
|
||||||
|
parties = list(set(parties))
|
||||||
|
|
||||||
|
if len(parties) > 1:
|
||||||
|
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
|
||||||
|
|
||||||
|
account_type_map = get_account_type_map(self.company)
|
||||||
|
party_type = 'supplier' if self.voucher_type == 'Credit Note' else 'customer'
|
||||||
|
doctype = 'Purchase Invoice' if self.voucher_type == 'Credit Note' else 'Sales Invoice'
|
||||||
|
debit_or_credit = 'debit_in_account_currency' if self.voucher_type == 'Credit Note' else 'credit_in_account_currency'
|
||||||
|
rev_debit_or_credit = 'credit_in_account_currency' if debit_or_credit == 'debit_in_account_currency' else 'debit_in_account_currency'
|
||||||
|
|
||||||
|
party_account = get_party_account(party_type.title(), parties[0], self.company)
|
||||||
|
|
||||||
|
net_total = sum(d.get(debit_or_credit) for d in self.get('accounts') if account_type_map.get(d.account)
|
||||||
|
not in ('Tax', 'Chargeable'))
|
||||||
|
|
||||||
|
party_amount = sum(d.get(rev_debit_or_credit) for d in self.get('accounts') if d.account == party_account)
|
||||||
|
|
||||||
|
inv = frappe._dict({
|
||||||
|
party_type: parties[0],
|
||||||
|
'doctype': doctype,
|
||||||
|
'company': self.company,
|
||||||
|
'posting_date': self.posting_date,
|
||||||
|
'net_total': net_total
|
||||||
|
})
|
||||||
|
|
||||||
|
tax_withholding_details = get_party_tax_withholding_details(inv, self.tax_withholding_category)
|
||||||
|
|
||||||
|
if not tax_withholding_details:
|
||||||
|
return
|
||||||
|
|
||||||
|
accounts = []
|
||||||
|
for d in self.get('accounts'):
|
||||||
|
if d.get('account') == tax_withholding_details.get("account_head"):
|
||||||
|
d.update({
|
||||||
|
'account': tax_withholding_details.get("account_head"),
|
||||||
|
debit_or_credit: tax_withholding_details.get('tax_amount')
|
||||||
|
})
|
||||||
|
|
||||||
|
accounts.append(d.get('account'))
|
||||||
|
|
||||||
|
if d.get('account') == party_account:
|
||||||
|
d.update({
|
||||||
|
rev_debit_or_credit: party_amount - tax_withholding_details.get('tax_amount')
|
||||||
|
})
|
||||||
|
|
||||||
|
if not accounts or tax_withholding_details.get("account_head") not in accounts:
|
||||||
|
self.append("accounts", {
|
||||||
|
'account': tax_withholding_details.get("account_head"),
|
||||||
|
rev_debit_or_credit: tax_withholding_details.get('tax_amount'),
|
||||||
|
'against_account': parties[0]
|
||||||
|
})
|
||||||
|
|
||||||
|
to_remove = [d for d in self.get('accounts')
|
||||||
|
if not d.get(rev_debit_or_credit) and d.account == tax_withholding_details.get("account_head")]
|
||||||
|
|
||||||
|
for d in to_remove:
|
||||||
|
self.remove(d)
|
||||||
|
|
||||||
def update_inter_company_jv(self):
|
def update_inter_company_jv(self):
|
||||||
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
|
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
|
||||||
frappe.db.set_value("Journal Entry", self.inter_company_journal_entry_reference,\
|
frappe.db.set_value("Journal Entry", self.inter_company_journal_entry_reference,\
|
||||||
|
29
erpnext/accounts/doctype/payment_entry/regional/india.js
Normal file
29
erpnext/accounts/doctype/payment_entry/regional/india.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
frappe.ui.form.on("Payment Entry", {
|
||||||
|
company: function(frm) {
|
||||||
|
frappe.call({
|
||||||
|
'method': 'frappe.contacts.doctype.address.address.get_default_address',
|
||||||
|
'args': {
|
||||||
|
'doctype': 'Company',
|
||||||
|
'name': frm.doc.company
|
||||||
|
},
|
||||||
|
'callback': function(r) {
|
||||||
|
frm.set_value('company_address', r.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
party: function(frm) {
|
||||||
|
if (frm.doc.party_type == "Customer" && frm.doc.party) {
|
||||||
|
frappe.call({
|
||||||
|
'method': 'frappe.contacts.doctype.address.address.get_default_address',
|
||||||
|
'args': {
|
||||||
|
'doctype': 'Customer',
|
||||||
|
'name': frm.doc.party
|
||||||
|
},
|
||||||
|
'callback': function(r) {
|
||||||
|
frm.set_value('customer_address', r.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -93,6 +93,7 @@
|
|||||||
"options": "Payment Term"
|
"options": "Payment Term"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "exchange_gain_loss",
|
||||||
"fieldname": "exchange_gain_loss",
|
"fieldname": "exchange_gain_loss",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Exchange Gain/Loss",
|
"label": "Exchange Gain/Loss",
|
||||||
@ -103,7 +104,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-04-21 13:30:11.605388",
|
"modified": "2021-09-26 17:06:55.597389",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Entry Reference",
|
"name": "Payment Entry Reference",
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
"field_order": [
|
"field_order": [
|
||||||
"reference_type",
|
"reference_type",
|
||||||
"reference_name",
|
"reference_name",
|
||||||
|
"reference_row",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"invoice_type",
|
"invoice_type",
|
||||||
"invoice_number",
|
"invoice_number",
|
||||||
@ -121,11 +122,17 @@
|
|||||||
"label": "Amount",
|
"label": "Amount",
|
||||||
"options": "Currency",
|
"options": "Currency",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "reference_row",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Reference Row"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-08-30 10:58:42.665107",
|
"modified": "2021-09-20 17:23:09.455803",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Reconciliation Allocation",
|
"name": "Payment Reconciliation Allocation",
|
||||||
|
@ -549,3 +549,11 @@ def make_payment_order(source_name, target_doc=None):
|
|||||||
}, target_doc, set_missing_values)
|
}, target_doc, set_missing_values)
|
||||||
|
|
||||||
return doclist
|
return doclist
|
||||||
|
|
||||||
|
def validate_payment(doc, method=""):
|
||||||
|
if not frappe.db.has_column(doc.reference_doctype, 'status'):
|
||||||
|
return
|
||||||
|
|
||||||
|
status = frappe.db.get_value(doc.reference_doctype, doc.reference_docname, 'status')
|
||||||
|
if status == 'Paid':
|
||||||
|
frappe.throw(_("The Payment Request {0} is already paid, cannot process payment twice").format(doc.reference_docname))
|
@ -40,6 +40,7 @@ class POSInvoice(SalesInvoice):
|
|||||||
self.validate_change_amount()
|
self.validate_change_amount()
|
||||||
self.validate_change_account()
|
self.validate_change_account()
|
||||||
self.validate_item_cost_centers()
|
self.validate_item_cost_centers()
|
||||||
|
self.validate_warehouse()
|
||||||
self.validate_serialised_or_batched_item()
|
self.validate_serialised_or_batched_item()
|
||||||
self.validate_stock_availablility()
|
self.validate_stock_availablility()
|
||||||
self.validate_return_items_qty()
|
self.validate_return_items_qty()
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
frappe.ui.form.on('POS Invoice Merge Log', {
|
frappe.ui.form.on('POS Invoice Merge Log', {
|
||||||
setup: function(frm) {
|
setup: function(frm) {
|
||||||
frm.set_query("pos_invoice", "pos_invoices", doc => {
|
frm.set_query("pos_invoice", "pos_invoices", doc => {
|
||||||
return{
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
'docstatus': 1,
|
'docstatus': 1,
|
||||||
'customer': doc.customer,
|
'customer': doc.customer,
|
||||||
@ -12,5 +12,10 @@ frappe.ui.form.on('POS Invoice Merge Log', {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
merge_invoices_based_on: function(frm) {
|
||||||
|
frm.set_value('customer', '');
|
||||||
|
frm.set_value('customer_group', '');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -6,9 +6,11 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"posting_date",
|
"posting_date",
|
||||||
"customer",
|
"merge_invoices_based_on",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"pos_closing_entry",
|
"pos_closing_entry",
|
||||||
|
"customer",
|
||||||
|
"customer_group",
|
||||||
"section_break_3",
|
"section_break_3",
|
||||||
"pos_invoices",
|
"pos_invoices",
|
||||||
"references_section",
|
"references_section",
|
||||||
@ -88,12 +90,27 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "POS Closing Entry",
|
"label": "POS Closing Entry",
|
||||||
"options": "POS Closing Entry"
|
"options": "POS Closing Entry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "merge_invoices_based_on",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Merge Invoices Based On",
|
||||||
|
"options": "Customer\nCustomer Group",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.merge_invoices_based_on == 'Customer Group'",
|
||||||
|
"fieldname": "customer_group",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Customer Group",
|
||||||
|
"mandatory_depends_on": "eval:doc.merge_invoices_based_on == 'Customer Group'",
|
||||||
|
"options": "Customer Group"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-12-01 11:53:57.267579",
|
"modified": "2021-09-14 11:17:19.001142",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "POS Invoice Merge Log",
|
"name": "POS Invoice Merge Log",
|
||||||
|
@ -23,6 +23,9 @@ class POSInvoiceMergeLog(Document):
|
|||||||
self.validate_pos_invoice_status()
|
self.validate_pos_invoice_status()
|
||||||
|
|
||||||
def validate_customer(self):
|
def validate_customer(self):
|
||||||
|
if self.merge_invoices_based_on == 'Customer Group':
|
||||||
|
return
|
||||||
|
|
||||||
for d in self.pos_invoices:
|
for d in self.pos_invoices:
|
||||||
if d.customer != self.customer:
|
if d.customer != self.customer:
|
||||||
frappe.throw(_("Row #{}: POS Invoice {} is not against customer {}").format(d.idx, d.pos_invoice, self.customer))
|
frappe.throw(_("Row #{}: POS Invoice {} is not against customer {}").format(d.idx, d.pos_invoice, self.customer))
|
||||||
@ -124,7 +127,7 @@ class POSInvoiceMergeLog(Document):
|
|||||||
found = False
|
found = False
|
||||||
for i in items:
|
for i in items:
|
||||||
if (i.item_code == item.item_code and not i.serial_no and not i.batch_no and
|
if (i.item_code == item.item_code and not i.serial_no and not i.batch_no and
|
||||||
i.uom == item.uom and i.net_rate == item.net_rate):
|
i.uom == item.uom and i.net_rate == item.net_rate and i.warehouse == item.warehouse):
|
||||||
found = True
|
found = True
|
||||||
i.qty = i.qty + item.qty
|
i.qty = i.qty + item.qty
|
||||||
|
|
||||||
@ -172,6 +175,11 @@ class POSInvoiceMergeLog(Document):
|
|||||||
invoice.discount_amount = 0.0
|
invoice.discount_amount = 0.0
|
||||||
invoice.taxes_and_charges = None
|
invoice.taxes_and_charges = None
|
||||||
invoice.ignore_pricing_rule = 1
|
invoice.ignore_pricing_rule = 1
|
||||||
|
invoice.customer = self.customer
|
||||||
|
|
||||||
|
if self.merge_invoices_based_on == 'Customer Group':
|
||||||
|
invoice.flags.ignore_pos_profile = True
|
||||||
|
invoice.pos_profile = ''
|
||||||
|
|
||||||
return invoice
|
return invoice
|
||||||
|
|
||||||
@ -228,7 +236,7 @@ def get_all_unconsolidated_invoices():
|
|||||||
return pos_invoices
|
return pos_invoices
|
||||||
|
|
||||||
def get_invoice_customer_map(pos_invoices):
|
def get_invoice_customer_map(pos_invoices):
|
||||||
# pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Custoemr 2' : [{}] }
|
# pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Customer 2' : [{}] }
|
||||||
pos_invoice_customer_map = {}
|
pos_invoice_customer_map = {}
|
||||||
for invoice in pos_invoices:
|
for invoice in pos_invoices:
|
||||||
customer = invoice.get('customer')
|
customer = invoice.get('customer')
|
||||||
|
@ -219,6 +219,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "1",
|
"default": "1",
|
||||||
|
"description": "A customer must have primary contact email.",
|
||||||
"fieldname": "primary_mandatory",
|
"fieldname": "primary_mandatory",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Send To Primary Contact"
|
"label": "Send To Primary Contact"
|
||||||
@ -286,10 +287,11 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-05-21 11:14:22.426672",
|
"modified": "2021-09-06 21:00:45.732505",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Process Statement Of Accounts",
|
"name": "Process Statement Of Accounts",
|
||||||
|
"naming_rule": "Set by user",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
@ -196,7 +196,10 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
|
|||||||
primary_email = customer.get('email_id') or ''
|
primary_email = customer.get('email_id') or ''
|
||||||
billing_email = get_customer_emails(customer.name, 1, billing_and_primary=False)
|
billing_email = get_customer_emails(customer.name, 1, billing_and_primary=False)
|
||||||
|
|
||||||
if billing_email == '' or (primary_email == '' and int(primary_mandatory)):
|
if int(primary_mandatory):
|
||||||
|
if (primary_email == ''):
|
||||||
|
continue
|
||||||
|
elif (billing_email == '') and (primary_email == ''):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
customer_list.append({
|
customer_list.append({
|
||||||
@ -208,10 +211,29 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True):
|
def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True):
|
||||||
|
""" Returns first email from Contact Email table as a Billing email
|
||||||
|
when Is Billing Contact checked
|
||||||
|
and Primary email- email with Is Primary checked """
|
||||||
|
|
||||||
billing_email = frappe.db.sql("""
|
billing_email = frappe.db.sql("""
|
||||||
SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent
|
SELECT
|
||||||
WHERE l.link_doctype='Customer' and l.link_name=%s and c.is_billing_contact=1
|
email.email_id
|
||||||
order by c.creation desc""", customer_name)
|
FROM
|
||||||
|
`tabContact Email` AS email
|
||||||
|
JOIN
|
||||||
|
`tabDynamic Link` AS link
|
||||||
|
ON
|
||||||
|
email.parent=link.parent
|
||||||
|
JOIN
|
||||||
|
`tabContact` AS contact
|
||||||
|
ON
|
||||||
|
contact.name=link.parent
|
||||||
|
WHERE
|
||||||
|
link.link_doctype='Customer'
|
||||||
|
and link.link_name=%s
|
||||||
|
and contact.is_billing_contact=1
|
||||||
|
ORDER BY
|
||||||
|
contact.creation desc""", customer_name)
|
||||||
|
|
||||||
if len(billing_email) == 0 or (billing_email[0][0] is None):
|
if len(billing_email) == 0 or (billing_email[0][0] is None):
|
||||||
if billing_and_primary:
|
if billing_and_primary:
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -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.gl_entry.gl_entry import update_outstanding_amt
|
||||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||||
check_if_return_invoice_linked_with_payment_entry,
|
check_if_return_invoice_linked_with_payment_entry,
|
||||||
|
is_overdue,
|
||||||
unlink_inter_company_doc,
|
unlink_inter_company_doc,
|
||||||
update_linked_doc,
|
update_linked_doc,
|
||||||
validate_inter_company_party,
|
validate_inter_company_party,
|
||||||
@ -1145,6 +1146,12 @@ class PurchaseInvoice(BuyingController):
|
|||||||
if not self.apply_tds:
|
if not self.apply_tds:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self.apply_tds and not self.get('tax_withholding_category'):
|
||||||
|
self.tax_withholding_category = frappe.db.get_value('Supplier', self.supplier, 'tax_withholding_category')
|
||||||
|
|
||||||
|
if not self.tax_withholding_category:
|
||||||
|
return
|
||||||
|
|
||||||
tax_withholding_details = get_party_tax_withholding_details(self, self.tax_withholding_category)
|
tax_withholding_details = get_party_tax_withholding_details(self, self.tax_withholding_category)
|
||||||
|
|
||||||
if not tax_withholding_details:
|
if not tax_withholding_details:
|
||||||
@ -1175,10 +1182,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
self.status = 'Draft'
|
self.status = 'Draft'
|
||||||
return
|
return
|
||||||
|
|
||||||
precision = self.precision("outstanding_amount")
|
outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount"))
|
||||||
outstanding_amount = flt(self.outstanding_amount, precision)
|
|
||||||
due_date = getdate(self.due_date)
|
|
||||||
nowdate = getdate()
|
|
||||||
|
|
||||||
if not status:
|
if not status:
|
||||||
if self.docstatus == 2:
|
if self.docstatus == 2:
|
||||||
@ -1186,9 +1190,11 @@ class PurchaseInvoice(BuyingController):
|
|||||||
elif self.docstatus == 1:
|
elif self.docstatus == 1:
|
||||||
if self.is_internal_transfer():
|
if self.is_internal_transfer():
|
||||||
self.status = 'Internal Transfer'
|
self.status = 'Internal Transfer'
|
||||||
elif outstanding_amount > 0 and due_date < nowdate:
|
elif is_overdue(self):
|
||||||
self.status = "Overdue"
|
self.status = "Overdue"
|
||||||
elif outstanding_amount > 0 and due_date >= nowdate:
|
elif 0 < outstanding_amount < flt(self.grand_total, self.precision("grand_total")):
|
||||||
|
self.status = "Partly Paid"
|
||||||
|
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
|
||||||
self.status = "Unpaid"
|
self.status = "Unpaid"
|
||||||
#Check if outstanding amount is 0 due to debit note issued against invoice
|
#Check if outstanding amount is 0 due to debit note issued against invoice
|
||||||
elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Purchase Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}):
|
elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Purchase Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}):
|
||||||
|
@ -2,28 +2,58 @@
|
|||||||
// License: GNU General Public License v3. See license.txt
|
// License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
// render
|
// render
|
||||||
frappe.listview_settings['Purchase Invoice'] = {
|
frappe.listview_settings["Purchase Invoice"] = {
|
||||||
add_fields: ["supplier", "supplier_name", "base_grand_total", "outstanding_amount", "due_date", "company",
|
add_fields: [
|
||||||
"currency", "is_return", "release_date", "on_hold", "represents_company", "is_internal_supplier"],
|
"supplier",
|
||||||
get_indicator: function(doc) {
|
"supplier_name",
|
||||||
if ((flt(doc.outstanding_amount) <= 0) && doc.docstatus == 1 && doc.status == 'Debit Note Issued') {
|
"base_grand_total",
|
||||||
return [__("Debit Note Issued"), "darkgrey", "outstanding_amount,<=,0"];
|
"outstanding_amount",
|
||||||
} else if (flt(doc.outstanding_amount) > 0 && doc.docstatus==1) {
|
"due_date",
|
||||||
if(cint(doc.on_hold) && !doc.release_date) {
|
"company",
|
||||||
return [__("On Hold"), "darkgrey"];
|
"currency",
|
||||||
} else if (cint(doc.on_hold) && doc.release_date && frappe.datetime.get_diff(doc.release_date, frappe.datetime.nowdate()) > 0) {
|
"is_return",
|
||||||
return [__("Temporarily on Hold"), "darkgrey"];
|
"release_date",
|
||||||
} else if (frappe.datetime.get_diff(doc.due_date) < 0) {
|
"on_hold",
|
||||||
return [__("Overdue"), "red", "outstanding_amount,>,0|due_date,<,Today"];
|
"represents_company",
|
||||||
} else {
|
"is_internal_supplier",
|
||||||
return [__("Unpaid"), "orange", "outstanding_amount,>,0|due_date,>=,Today"];
|
],
|
||||||
}
|
get_indicator(doc) {
|
||||||
} else if (cint(doc.is_return)) {
|
if (doc.status == "Debit Note Issued") {
|
||||||
return [__("Return"), "gray", "is_return,=,Yes"];
|
return [__(doc.status), "darkgrey", "status,=," + doc.status];
|
||||||
} else if (doc.company == doc.represents_company && doc.is_internal_supplier) {
|
|
||||||
return [__("Internal Transfer"), "darkgrey", "outstanding_amount,=,0"];
|
|
||||||
} else if (flt(doc.outstanding_amount)==0 && doc.docstatus==1) {
|
|
||||||
return [__("Paid"), "green", "outstanding_amount,=,0"];
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if (
|
||||||
|
flt(doc.outstanding_amount) > 0 &&
|
||||||
|
doc.docstatus == 1 &&
|
||||||
|
cint(doc.on_hold)
|
||||||
|
) {
|
||||||
|
if (!doc.release_date) {
|
||||||
|
return [__("On Hold"), "darkgrey"];
|
||||||
|
} else if (
|
||||||
|
frappe.datetime.get_diff(
|
||||||
|
doc.release_date,
|
||||||
|
frappe.datetime.nowdate()
|
||||||
|
) > 0
|
||||||
|
) {
|
||||||
|
return [__("Temporarily on Hold"), "darkgrey"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const status_colors = {
|
||||||
|
"Unpaid": "orange",
|
||||||
|
"Paid": "green",
|
||||||
|
"Return": "gray",
|
||||||
|
"Overdue": "red",
|
||||||
|
"Partly Paid": "yellow",
|
||||||
|
"Internal Transfer": "darkgrey",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (status_colors[doc.status]) {
|
||||||
|
return [
|
||||||
|
__(doc.status),
|
||||||
|
status_colors[doc.status],
|
||||||
|
"status,=," + doc.status,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -1151,10 +1151,11 @@ class TestPurchaseInvoice(unittest.TestCase):
|
|||||||
tax_withholding_category = 'TDS - 194 - Dividends - Individual')
|
tax_withholding_category = 'TDS - 194 - Dividends - Individual')
|
||||||
|
|
||||||
# Update tax withholding category with current fiscal year and rate details
|
# Update tax withholding category with current fiscal year and rate details
|
||||||
update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate())
|
update_tax_witholding_category('_Test Company', 'TDS Payable - _TC')
|
||||||
|
|
||||||
# Create Purchase Order with TDS applied
|
# Create Purchase Order with TDS applied
|
||||||
po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item')
|
po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item',
|
||||||
|
posting_date='2021-09-15')
|
||||||
po.apply_tds = 1
|
po.apply_tds = 1
|
||||||
po.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
|
po.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
|
||||||
po.save()
|
po.save()
|
||||||
@ -1226,16 +1227,20 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
|||||||
doc.assertEqual(expected_gle[i][2], gle.credit)
|
doc.assertEqual(expected_gle[i][2], gle.credit)
|
||||||
doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
|
doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
|
||||||
|
|
||||||
def update_tax_witholding_category(company, account, date):
|
def update_tax_witholding_category(company, account):
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
|
|
||||||
fiscal_year = get_fiscal_year(date=date, company=company)
|
fiscal_year = get_fiscal_year(fiscal_year='2021')
|
||||||
|
|
||||||
if not frappe.db.get_value('Tax Withholding Rate',
|
if not frappe.db.get_value('Tax Withholding Rate',
|
||||||
{'parent': 'TDS - 194 - Dividends - Individual', 'fiscal_year': fiscal_year[0]}):
|
{'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]),
|
||||||
|
'to_date': ('<=', fiscal_year[2])}):
|
||||||
tds_category = frappe.get_doc('Tax Withholding Category', 'TDS - 194 - Dividends - Individual')
|
tds_category = frappe.get_doc('Tax Withholding Category', 'TDS - 194 - Dividends - Individual')
|
||||||
|
tds_category.set('rates', [])
|
||||||
|
|
||||||
tds_category.append('rates', {
|
tds_category.append('rates', {
|
||||||
'fiscal_year': fiscal_year[0],
|
'from_date': fiscal_year[1],
|
||||||
|
'to_date': fiscal_year[2],
|
||||||
'tax_withholding_rate': 10,
|
'tax_withholding_rate': 10,
|
||||||
'single_threshold': 2500,
|
'single_threshold': 2500,
|
||||||
'cumulative_threshold': 0
|
'cumulative_threshold': 0
|
||||||
|
@ -97,6 +97,7 @@
|
|||||||
"width": "100px"
|
"width": "100px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "exchange_gain_loss",
|
||||||
"fieldname": "exchange_gain_loss",
|
"fieldname": "exchange_gain_loss",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Exchange Gain/Loss",
|
"label": "Exchange Gain/Loss",
|
||||||
@ -104,6 +105,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "exchange_gain_loss",
|
||||||
"fieldname": "ref_exchange_rate",
|
"fieldname": "ref_exchange_rate",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Reference Exchange Rate",
|
"label": "Reference Exchange Rate",
|
||||||
@ -115,7 +117,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-04-20 16:26:53.820530",
|
"modified": "2021-09-26 15:47:28.167371",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice Advance",
|
"name": "Purchase Invoice Advance",
|
||||||
|
@ -445,15 +445,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
|||||||
this.frm.refresh_field("base_paid_amount");
|
this.frm.refresh_field("base_paid_amount");
|
||||||
}
|
}
|
||||||
|
|
||||||
currency() {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
currency() {
|
currency() {
|
||||||
var me = this;
|
var me = this;
|
||||||
super.currency();
|
super.currency();
|
||||||
|
@ -247,7 +247,7 @@
|
|||||||
"depends_on": "customer",
|
"depends_on": "customer",
|
||||||
"fetch_from": "customer.customer_name",
|
"fetch_from": "customer.customer_name",
|
||||||
"fieldname": "customer_name",
|
"fieldname": "customer_name",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Small Text",
|
||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
"hide_seconds": 1,
|
"hide_seconds": 1,
|
||||||
"in_global_search": 1,
|
"in_global_search": 1,
|
||||||
@ -1061,6 +1061,7 @@
|
|||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
"hide_seconds": 1,
|
"hide_seconds": 1,
|
||||||
"label": "Apply Additional Discount On",
|
"label": "Apply Additional Discount On",
|
||||||
|
"length": 15,
|
||||||
"options": "\nGrand Total\nNet Total",
|
"options": "\nGrand Total\nNet Total",
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
@ -1147,7 +1148,7 @@
|
|||||||
{
|
{
|
||||||
"description": "In Words will be visible once you save the Sales Invoice.",
|
"description": "In Words will be visible once you save the Sales Invoice.",
|
||||||
"fieldname": "base_in_words",
|
"fieldname": "base_in_words",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Small Text",
|
||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
"hide_seconds": 1,
|
"hide_seconds": 1,
|
||||||
"label": "In Words (Company Currency)",
|
"label": "In Words (Company Currency)",
|
||||||
@ -1207,7 +1208,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "in_words",
|
"fieldname": "in_words",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Small Text",
|
||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
"hide_seconds": 1,
|
"hide_seconds": 1,
|
||||||
"label": "In Words",
|
"label": "In Words",
|
||||||
@ -1560,6 +1561,7 @@
|
|||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
"hide_seconds": 1,
|
"hide_seconds": 1,
|
||||||
"label": "Print Language",
|
"label": "Print Language",
|
||||||
|
"length": 6,
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@ -1647,8 +1649,9 @@
|
|||||||
"hide_seconds": 1,
|
"hide_seconds": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Status",
|
"label": "Status",
|
||||||
|
"length": 30,
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer",
|
"options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nPartly Paid\nUnpaid\nUnpaid and Discounted\nPartly Paid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@ -1706,6 +1709,7 @@
|
|||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
"hide_seconds": 1,
|
"hide_seconds": 1,
|
||||||
"label": "Is Opening Entry",
|
"label": "Is Opening Entry",
|
||||||
|
"length": 4,
|
||||||
"oldfieldname": "is_opening",
|
"oldfieldname": "is_opening",
|
||||||
"oldfieldtype": "Select",
|
"oldfieldtype": "Select",
|
||||||
"options": "No\nYes",
|
"options": "No\nYes",
|
||||||
@ -1717,6 +1721,7 @@
|
|||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
"hide_seconds": 1,
|
"hide_seconds": 1,
|
||||||
"label": "C-Form Applicable",
|
"label": "C-Form Applicable",
|
||||||
|
"length": 4,
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "No\nYes",
|
"options": "No\nYes",
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
@ -1948,6 +1953,7 @@
|
|||||||
"fetch_from": "customer.represents_company",
|
"fetch_from": "customer.represents_company",
|
||||||
"fieldname": "represents_company",
|
"fieldname": "represents_company",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
|
"ignore_user_permissions": 1,
|
||||||
"label": "Represents Company",
|
"label": "Represents Company",
|
||||||
"options": "Company",
|
"options": "Company",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
@ -2017,11 +2023,12 @@
|
|||||||
"link_fieldname": "consolidated_invoice"
|
"link_fieldname": "consolidated_invoice"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2021-08-27 20:13:40.456462",
|
"modified": "2021-09-28 13:09:34.391799",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice",
|
"name": "Sales Invoice",
|
||||||
"name_case": "Title Case",
|
"name_case": "Title Case",
|
||||||
|
"naming_rule": "By \"Naming Series\" field",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
@ -485,7 +485,7 @@ class SalesInvoice(SellingController):
|
|||||||
self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
|
self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
|
||||||
|
|
||||||
from erpnext.stock.get_item_details import get_pos_profile, get_pos_profile_item_details
|
from erpnext.stock.get_item_details import get_pos_profile, get_pos_profile_item_details
|
||||||
if not self.pos_profile:
|
if not self.pos_profile and not self.flags.ignore_pos_profile:
|
||||||
pos_profile = get_pos_profile(self.company) or {}
|
pos_profile = get_pos_profile(self.company) or {}
|
||||||
if not pos_profile:
|
if not pos_profile:
|
||||||
return
|
return
|
||||||
@ -1422,14 +1422,7 @@ class SalesInvoice(SellingController):
|
|||||||
self.status = 'Draft'
|
self.status = 'Draft'
|
||||||
return
|
return
|
||||||
|
|
||||||
precision = self.precision("outstanding_amount")
|
outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount"))
|
||||||
outstanding_amount = flt(self.outstanding_amount, precision)
|
|
||||||
due_date = getdate(self.due_date)
|
|
||||||
nowdate = getdate()
|
|
||||||
|
|
||||||
discounting_status = None
|
|
||||||
if self.is_discounted:
|
|
||||||
discounting_status = get_discounting_status(self.name)
|
|
||||||
|
|
||||||
if not status:
|
if not status:
|
||||||
if self.docstatus == 2:
|
if self.docstatus == 2:
|
||||||
@ -1437,15 +1430,13 @@ class SalesInvoice(SellingController):
|
|||||||
elif self.docstatus == 1:
|
elif self.docstatus == 1:
|
||||||
if self.is_internal_transfer():
|
if self.is_internal_transfer():
|
||||||
self.status = 'Internal Transfer'
|
self.status = 'Internal Transfer'
|
||||||
elif outstanding_amount > 0 and due_date < nowdate and self.is_discounted and discounting_status=='Disbursed':
|
elif is_overdue(self):
|
||||||
self.status = "Overdue and Discounted"
|
|
||||||
elif outstanding_amount > 0 and due_date < nowdate:
|
|
||||||
self.status = "Overdue"
|
self.status = "Overdue"
|
||||||
elif outstanding_amount > 0 and due_date >= nowdate and self.is_discounted and discounting_status=='Disbursed':
|
elif 0 < outstanding_amount < flt(self.grand_total, self.precision("grand_total")):
|
||||||
self.status = "Unpaid and Discounted"
|
self.status = "Partly Paid"
|
||||||
elif outstanding_amount > 0 and due_date >= nowdate:
|
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
|
||||||
self.status = "Unpaid"
|
self.status = "Unpaid"
|
||||||
#Check if outstanding amount is 0 due to credit note issued against invoice
|
# Check if outstanding amount is 0 due to credit note issued against invoice
|
||||||
elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Sales Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}):
|
elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Sales Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}):
|
||||||
self.status = "Credit Note Issued"
|
self.status = "Credit Note Issued"
|
||||||
elif self.is_return == 1:
|
elif self.is_return == 1:
|
||||||
@ -1454,12 +1445,42 @@ class SalesInvoice(SellingController):
|
|||||||
self.status = "Paid"
|
self.status = "Paid"
|
||||||
else:
|
else:
|
||||||
self.status = "Submitted"
|
self.status = "Submitted"
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.status in ("Unpaid", "Partly Paid", "Overdue")
|
||||||
|
and self.is_discounted
|
||||||
|
and get_discounting_status(self.name) == "Disbursed"
|
||||||
|
):
|
||||||
|
self.status += " and Discounted"
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.status = "Draft"
|
self.status = "Draft"
|
||||||
|
|
||||||
if update:
|
if update:
|
||||||
self.db_set('status', self.status, update_modified = update_modified)
|
self.db_set('status', self.status, update_modified = update_modified)
|
||||||
|
|
||||||
|
def is_overdue(doc):
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
if (grand_total - outstanding_amount) < payable_amount:
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif getdate(doc.due_date) < nowdate:
|
||||||
|
return True
|
||||||
|
|
||||||
def get_discounting_status(sales_invoice):
|
def get_discounting_status(sales_invoice):
|
||||||
status = None
|
status = None
|
||||||
|
|
||||||
|
@ -6,18 +6,20 @@ frappe.listview_settings['Sales Invoice'] = {
|
|||||||
add_fields: ["customer", "customer_name", "base_grand_total", "outstanding_amount", "due_date", "company",
|
add_fields: ["customer", "customer_name", "base_grand_total", "outstanding_amount", "due_date", "company",
|
||||||
"currency", "is_return"],
|
"currency", "is_return"],
|
||||||
get_indicator: function(doc) {
|
get_indicator: function(doc) {
|
||||||
var status_color = {
|
const status_colors = {
|
||||||
"Draft": "grey",
|
"Draft": "grey",
|
||||||
"Unpaid": "orange",
|
"Unpaid": "orange",
|
||||||
"Paid": "green",
|
"Paid": "green",
|
||||||
"Return": "gray",
|
"Return": "gray",
|
||||||
"Credit Note Issued": "gray",
|
"Credit Note Issued": "gray",
|
||||||
"Unpaid and Discounted": "orange",
|
"Unpaid and Discounted": "orange",
|
||||||
|
"Partly Paid and Discounted": "yellow",
|
||||||
"Overdue and Discounted": "red",
|
"Overdue and Discounted": "red",
|
||||||
"Overdue": "red",
|
"Overdue": "red",
|
||||||
|
"Partly Paid": "yellow",
|
||||||
"Internal Transfer": "darkgrey"
|
"Internal Transfer": "darkgrey"
|
||||||
};
|
};
|
||||||
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
|
return [__(doc.status), status_colors[doc.status], "status,=,"+doc.status];
|
||||||
},
|
},
|
||||||
right_column: "grand_total"
|
right_column: "grand_total"
|
||||||
};
|
};
|
||||||
|
@ -133,6 +133,7 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
def test_payment_entry_unlink_against_invoice(self):
|
def test_payment_entry_unlink_against_invoice(self):
|
||||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||||
|
|
||||||
si = frappe.copy_doc(test_records[0])
|
si = frappe.copy_doc(test_records[0])
|
||||||
si.is_pos = 0
|
si.is_pos = 0
|
||||||
si.insert()
|
si.insert()
|
||||||
@ -156,6 +157,7 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
def test_payment_entry_unlink_against_standalone_credit_note(self):
|
def test_payment_entry_unlink_against_standalone_credit_note(self):
|
||||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||||
|
|
||||||
si1 = create_sales_invoice(rate=1000)
|
si1 = create_sales_invoice(rate=1000)
|
||||||
si2 = create_sales_invoice(rate=300)
|
si2 = create_sales_invoice(rate=300)
|
||||||
si3 = create_sales_invoice(qty=-1, rate=300, is_return=1)
|
si3 = create_sales_invoice(qty=-1, rate=300, is_return=1)
|
||||||
@ -1420,15 +1422,22 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(si)
|
itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(si)
|
||||||
|
|
||||||
expected_itemised_tax = {
|
expected_itemised_tax = {
|
||||||
"999800": {
|
"_Test Item": {
|
||||||
"Service Tax": {
|
"Service Tax": {
|
||||||
"tax_rate": 10.0,
|
"tax_rate": 10.0,
|
||||||
"tax_amount": 1500.0
|
"tax_amount": 1000.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_Test Item 2": {
|
||||||
|
"Service Tax": {
|
||||||
|
"tax_rate": 10.0,
|
||||||
|
"tax_amount": 500.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
expected_itemised_taxable_amount = {
|
expected_itemised_taxable_amount = {
|
||||||
"999800": 15000.0
|
"_Test Item": 10000.0,
|
||||||
|
"_Test Item 2": 5000.0
|
||||||
}
|
}
|
||||||
|
|
||||||
self.assertEqual(itemised_tax, expected_itemised_tax)
|
self.assertEqual(itemised_tax, expected_itemised_tax)
|
||||||
@ -1639,6 +1648,7 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
def test_credit_note(self):
|
def test_credit_note(self):
|
||||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||||
|
|
||||||
si = create_sales_invoice(item_code = "_Test Item", qty = (5 * -1), rate=500, is_return = 1)
|
si = create_sales_invoice(item_code = "_Test Item", qty = (5 * -1), rate=500, is_return = 1)
|
||||||
|
|
||||||
outstanding_amount = get_outstanding_amount(si.doctype,
|
outstanding_amount = get_outstanding_amount(si.doctype,
|
||||||
@ -1790,6 +1800,47 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
|
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
|
||||||
|
|
||||||
|
def test_deferred_revenue_post_account_freeze_upto_by_admin(self):
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
|
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
|
||||||
|
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
|
||||||
|
|
||||||
|
deferred_account = create_account(account_name="Deferred Revenue",
|
||||||
|
parent_account="Current Liabilities - _TC", company="_Test Company")
|
||||||
|
|
||||||
|
item = create_item("_Test Item for Deferred Accounting")
|
||||||
|
item.enable_deferred_revenue = 1
|
||||||
|
item.deferred_revenue_account = deferred_account
|
||||||
|
item.no_of_months = 12
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_save=True)
|
||||||
|
si.items[0].enable_deferred_revenue = 1
|
||||||
|
si.items[0].service_start_date = "2019-01-10"
|
||||||
|
si.items[0].service_end_date = "2019-03-15"
|
||||||
|
si.items[0].deferred_revenue_account = deferred_account
|
||||||
|
si.save()
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31'))
|
||||||
|
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', 'System Manager')
|
||||||
|
|
||||||
|
pda1 = frappe.get_doc(dict(
|
||||||
|
doctype='Process Deferred Accounting',
|
||||||
|
posting_date=nowdate(),
|
||||||
|
start_date="2019-01-01",
|
||||||
|
end_date="2019-03-31",
|
||||||
|
type="Income",
|
||||||
|
company="_Test Company"
|
||||||
|
))
|
||||||
|
|
||||||
|
pda1.insert()
|
||||||
|
self.assertRaises(frappe.ValidationError, pda1.submit)
|
||||||
|
|
||||||
|
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
|
||||||
|
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
|
||||||
|
|
||||||
def test_fixed_deferred_revenue(self):
|
def test_fixed_deferred_revenue(self):
|
||||||
deferred_account = create_account(account_name="Deferred Revenue",
|
deferred_account = create_account(account_name="Deferred Revenue",
|
||||||
parent_account="Current Liabilities - _TC", company="_Test Company")
|
parent_account="Current Liabilities - _TC", company="_Test Company")
|
||||||
@ -2262,6 +2313,54 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
party_link.delete()
|
party_link.delete()
|
||||||
frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 0)
|
frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 0)
|
||||||
|
|
||||||
|
def test_payment_statuses(self):
|
||||||
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||||
|
|
||||||
|
today = nowdate()
|
||||||
|
|
||||||
|
# Test Overdue
|
||||||
|
si = create_sales_invoice(do_not_submit=True)
|
||||||
|
si.payment_schedule = []
|
||||||
|
si.append("payment_schedule", {
|
||||||
|
"due_date": add_days(today, -5),
|
||||||
|
"invoice_portion": 50,
|
||||||
|
"payment_amount": si.grand_total / 2
|
||||||
|
})
|
||||||
|
si.append("payment_schedule", {
|
||||||
|
"due_date": add_days(today, 5),
|
||||||
|
"invoice_portion": 50,
|
||||||
|
"payment_amount": si.grand_total / 2
|
||||||
|
})
|
||||||
|
si.submit()
|
||||||
|
self.assertEqual(si.status, "Overdue")
|
||||||
|
|
||||||
|
# Test payment less than due amount
|
||||||
|
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
|
||||||
|
pe.reference_no = "1"
|
||||||
|
pe.reference_date = nowdate()
|
||||||
|
pe.paid_amount = 1
|
||||||
|
pe.references[0].allocated_amount = pe.paid_amount
|
||||||
|
pe.submit()
|
||||||
|
si.reload()
|
||||||
|
self.assertEqual(si.status, "Overdue")
|
||||||
|
|
||||||
|
# Test Partly Paid
|
||||||
|
pe = frappe.copy_doc(pe)
|
||||||
|
pe.paid_amount = si.grand_total / 2
|
||||||
|
pe.references[0].allocated_amount = pe.paid_amount
|
||||||
|
pe.submit()
|
||||||
|
si.reload()
|
||||||
|
self.assertEqual(si.status, "Partly Paid")
|
||||||
|
|
||||||
|
# Test Paid
|
||||||
|
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
|
||||||
|
pe.reference_no = "1"
|
||||||
|
pe.reference_date = nowdate()
|
||||||
|
pe.paid_amount = si.outstanding_amount
|
||||||
|
pe.submit()
|
||||||
|
si.reload()
|
||||||
|
self.assertEqual(si.status, "Paid")
|
||||||
|
|
||||||
def get_sales_invoice_for_e_invoice():
|
def get_sales_invoice_for_e_invoice():
|
||||||
si = make_sales_invoice_for_ewaybill()
|
si = make_sales_invoice_for_ewaybill()
|
||||||
si.naming_series = 'INV-2020-.#####'
|
si.naming_series = 'INV-2020-.#####'
|
||||||
@ -2294,6 +2393,7 @@ def make_test_address_for_ewaybill():
|
|||||||
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
|
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
|
||||||
address = frappe.get_doc({
|
address = frappe.get_doc({
|
||||||
"address_line1": "_Test Address Line 1",
|
"address_line1": "_Test Address Line 1",
|
||||||
|
"address_line2": "_Test Address Line 2",
|
||||||
"address_title": "_Test Address for Eway bill",
|
"address_title": "_Test Address for Eway bill",
|
||||||
"address_type": "Billing",
|
"address_type": "Billing",
|
||||||
"city": "_Test City",
|
"city": "_Test City",
|
||||||
@ -2315,11 +2415,12 @@ def make_test_address_for_ewaybill():
|
|||||||
|
|
||||||
address.save()
|
address.save()
|
||||||
|
|
||||||
if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'):
|
if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Billing'):
|
||||||
address = frappe.get_doc({
|
address = frappe.get_doc({
|
||||||
"address_line1": "_Test Address Line 1",
|
"address_line1": "_Test Address Line 1",
|
||||||
|
"address_line2": "_Test Address Line 2",
|
||||||
"address_title": "_Test Customer-Address for Eway bill",
|
"address_title": "_Test Customer-Address for Eway bill",
|
||||||
"address_type": "Shipping",
|
"address_type": "Billing",
|
||||||
"city": "_Test City",
|
"city": "_Test City",
|
||||||
"state": "Test State",
|
"state": "Test State",
|
||||||
"country": "India",
|
"country": "India",
|
||||||
@ -2339,9 +2440,34 @@ def make_test_address_for_ewaybill():
|
|||||||
|
|
||||||
address.save()
|
address.save()
|
||||||
|
|
||||||
|
if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'):
|
||||||
|
address = frappe.get_doc({
|
||||||
|
"address_line1": "_Test Address Line 1",
|
||||||
|
"address_line2": "_Test Address Line 2",
|
||||||
|
"address_title": "_Test Customer-Address for Eway bill",
|
||||||
|
"address_type": "Shipping",
|
||||||
|
"city": "_Test City",
|
||||||
|
"state": "Test State",
|
||||||
|
"country": "India",
|
||||||
|
"doctype": "Address",
|
||||||
|
"is_primary_address": 1,
|
||||||
|
"phone": "+910000000000",
|
||||||
|
"gst_state": "Maharashtra",
|
||||||
|
"gst_state_number": "27",
|
||||||
|
"pincode": "410098"
|
||||||
|
}).insert()
|
||||||
|
|
||||||
|
address.append("links", {
|
||||||
|
"link_doctype": "Customer",
|
||||||
|
"link_name": "_Test Customer"
|
||||||
|
})
|
||||||
|
|
||||||
|
address.save()
|
||||||
|
|
||||||
if not frappe.db.exists('Address', '_Test Dispatch-Address for Eway bill-Shipping'):
|
if not frappe.db.exists('Address', '_Test Dispatch-Address for Eway bill-Shipping'):
|
||||||
address = frappe.get_doc({
|
address = frappe.get_doc({
|
||||||
"address_line1": "_Test Dispatch Address Line 1",
|
"address_line1": "_Test Dispatch Address Line 1",
|
||||||
|
"address_line2": "_Test Dispatch Address Line 2",
|
||||||
"address_title": "_Test Dispatch-Address for Eway bill",
|
"address_title": "_Test Dispatch-Address for Eway bill",
|
||||||
"address_type": "Shipping",
|
"address_type": "Shipping",
|
||||||
"city": "_Test City",
|
"city": "_Test City",
|
||||||
@ -2356,11 +2482,6 @@ def make_test_address_for_ewaybill():
|
|||||||
"pincode": "1100101"
|
"pincode": "1100101"
|
||||||
}).insert()
|
}).insert()
|
||||||
|
|
||||||
address.append("links", {
|
|
||||||
"link_doctype": "Company",
|
|
||||||
"link_name": "_Test Company"
|
|
||||||
})
|
|
||||||
|
|
||||||
address.save()
|
address.save()
|
||||||
|
|
||||||
def make_test_transporter_for_ewaybill():
|
def make_test_transporter_for_ewaybill():
|
||||||
@ -2400,7 +2521,8 @@ def make_sales_invoice_for_ewaybill():
|
|||||||
|
|
||||||
si.distance = 2000
|
si.distance = 2000
|
||||||
si.company_address = "_Test Address for Eway bill-Billing"
|
si.company_address = "_Test Address for Eway bill-Billing"
|
||||||
si.customer_address = "_Test Customer-Address for Eway bill-Shipping"
|
si.customer_address = "_Test Customer-Address for Eway bill-Billing"
|
||||||
|
si.shipping_address_name = "_Test Customer-Address for Eway bill-Shipping"
|
||||||
si.dispatch_address_name = "_Test Dispatch-Address for Eway bill-Shipping"
|
si.dispatch_address_name = "_Test Dispatch-Address for Eway bill-Shipping"
|
||||||
si.vehicle_no = "KA12KA1234"
|
si.vehicle_no = "KA12KA1234"
|
||||||
si.gst_category = "Registered Regular"
|
si.gst_category = "Registered Regular"
|
||||||
|
@ -98,6 +98,7 @@
|
|||||||
"width": "120px"
|
"width": "120px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "exchange_gain_loss",
|
||||||
"fieldname": "exchange_gain_loss",
|
"fieldname": "exchange_gain_loss",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Exchange Gain/Loss",
|
"label": "Exchange Gain/Loss",
|
||||||
@ -105,6 +106,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "exchange_gain_loss",
|
||||||
"fieldname": "ref_exchange_rate",
|
"fieldname": "ref_exchange_rate",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Reference Exchange Rate",
|
"label": "Reference Exchange Rate",
|
||||||
@ -116,7 +118,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-06-04 20:25:49.832052",
|
"modified": "2021-09-26 15:47:46.911595",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice Advance",
|
"name": "Sales Invoice Advance",
|
||||||
|
@ -400,6 +400,7 @@ class Subscription(Document):
|
|||||||
|
|
||||||
invoice.flags.ignore_mandatory = True
|
invoice.flags.ignore_mandatory = True
|
||||||
|
|
||||||
|
invoice.set_missing_values()
|
||||||
invoice.save()
|
invoice.save()
|
||||||
|
|
||||||
if self.submit_invoice:
|
if self.submit_invoice:
|
||||||
|
@ -9,11 +9,26 @@ from frappe import _
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cint, getdate
|
from frappe.utils import cint, getdate
|
||||||
|
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
|
||||||
|
|
||||||
|
|
||||||
class TaxWithholdingCategory(Document):
|
class TaxWithholdingCategory(Document):
|
||||||
pass
|
def validate(self):
|
||||||
|
self.validate_dates()
|
||||||
|
self.validate_thresholds()
|
||||||
|
|
||||||
|
def validate_dates(self):
|
||||||
|
last_date = None
|
||||||
|
for d in self.get('rates'):
|
||||||
|
if getdate(d.from_date) >= getdate(d.to_date):
|
||||||
|
frappe.throw(_("Row #{0}: From Date cannot be before To Date").format(d.idx))
|
||||||
|
|
||||||
|
# validate overlapping of dates
|
||||||
|
if last_date and getdate(d.to_date) < getdate(last_date):
|
||||||
|
frappe.throw(_("Row #{0}: Dates overlapping with other row").format(d.idx))
|
||||||
|
|
||||||
|
def validate_thresholds(self):
|
||||||
|
for d in self.get('rates'):
|
||||||
|
if d.cumulative_threshold and d.cumulative_threshold < d.single_threshold:
|
||||||
|
frappe.throw(_("Row #{0}: Cumulative threshold cannot be less than Single Transaction threshold").format(d.idx))
|
||||||
|
|
||||||
def get_party_details(inv):
|
def get_party_details(inv):
|
||||||
party_type, party = '', ''
|
party_type, party = '', ''
|
||||||
@ -52,8 +67,8 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
|||||||
if not parties:
|
if not parties:
|
||||||
parties.append(party)
|
parties.append(party)
|
||||||
|
|
||||||
fiscal_year = get_fiscal_year(inv.get('posting_date') or inv.get('transaction_date'), company=inv.company)
|
posting_date = inv.get('posting_date') or inv.get('transaction_date')
|
||||||
tax_details = get_tax_withholding_details(tax_withholding_category, fiscal_year[0], inv.company)
|
tax_details = get_tax_withholding_details(tax_withholding_category, posting_date, inv.company)
|
||||||
|
|
||||||
if not tax_details:
|
if not tax_details:
|
||||||
frappe.throw(_('Please set associated account in Tax Withholding Category {0} against Company {1}')
|
frappe.throw(_('Please set associated account in Tax Withholding Category {0} against Company {1}')
|
||||||
@ -67,7 +82,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
|||||||
tax_amount, tax_deducted = get_tax_amount(
|
tax_amount, tax_deducted = get_tax_amount(
|
||||||
party_type, parties,
|
party_type, parties,
|
||||||
inv, tax_details,
|
inv, tax_details,
|
||||||
fiscal_year, pan_no
|
posting_date, pan_no
|
||||||
)
|
)
|
||||||
|
|
||||||
if party_type == 'Supplier':
|
if party_type == 'Supplier':
|
||||||
@ -77,16 +92,19 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
|||||||
|
|
||||||
return tax_row
|
return tax_row
|
||||||
|
|
||||||
def get_tax_withholding_details(tax_withholding_category, fiscal_year, company):
|
def get_tax_withholding_details(tax_withholding_category, posting_date, company):
|
||||||
tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category)
|
tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category)
|
||||||
|
|
||||||
tax_rate_detail = get_tax_withholding_rates(tax_withholding, fiscal_year)
|
tax_rate_detail = get_tax_withholding_rates(tax_withholding, posting_date)
|
||||||
|
|
||||||
for account_detail in tax_withholding.accounts:
|
for account_detail in tax_withholding.accounts:
|
||||||
if company == account_detail.company:
|
if company == account_detail.company:
|
||||||
return frappe._dict({
|
return frappe._dict({
|
||||||
|
"tax_withholding_category": tax_withholding_category,
|
||||||
"account_head": account_detail.account,
|
"account_head": account_detail.account,
|
||||||
"rate": tax_rate_detail.tax_withholding_rate,
|
"rate": tax_rate_detail.tax_withholding_rate,
|
||||||
|
"from_date": tax_rate_detail.from_date,
|
||||||
|
"to_date": tax_rate_detail.to_date,
|
||||||
"threshold": tax_rate_detail.single_threshold,
|
"threshold": tax_rate_detail.single_threshold,
|
||||||
"cumulative_threshold": tax_rate_detail.cumulative_threshold,
|
"cumulative_threshold": tax_rate_detail.cumulative_threshold,
|
||||||
"description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category,
|
"description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category,
|
||||||
@ -95,13 +113,13 @@ def get_tax_withholding_details(tax_withholding_category, fiscal_year, company):
|
|||||||
"round_off_tax_amount": tax_withholding.round_off_tax_amount
|
"round_off_tax_amount": tax_withholding.round_off_tax_amount
|
||||||
})
|
})
|
||||||
|
|
||||||
def get_tax_withholding_rates(tax_withholding, fiscal_year):
|
def get_tax_withholding_rates(tax_withholding, posting_date):
|
||||||
# returns the row that matches with the fiscal year from posting date
|
# returns the row that matches with the fiscal year from posting date
|
||||||
for rate in tax_withholding.rates:
|
for rate in tax_withholding.rates:
|
||||||
if rate.fiscal_year == fiscal_year:
|
if getdate(rate.from_date) <= getdate(posting_date) <= getdate(rate.to_date):
|
||||||
return rate
|
return rate
|
||||||
|
|
||||||
frappe.throw(_("No Tax Withholding data found for the current Fiscal Year."))
|
frappe.throw(_("No Tax Withholding data found for the current posting date."))
|
||||||
|
|
||||||
def get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted):
|
def get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted):
|
||||||
row = {
|
row = {
|
||||||
@ -143,38 +161,38 @@ def get_tax_row_for_tds(tax_details, tax_amount):
|
|||||||
"account_head": tax_details.account_head
|
"account_head": tax_details.account_head
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_lower_deduction_certificate(fiscal_year, pan_no):
|
def get_lower_deduction_certificate(tax_details, pan_no):
|
||||||
ldc_name = frappe.db.get_value('Lower Deduction Certificate', { 'pan_no': pan_no, 'fiscal_year': fiscal_year }, 'name')
|
ldc_name = frappe.db.get_value('Lower Deduction Certificate',
|
||||||
|
{
|
||||||
|
'pan_no': pan_no,
|
||||||
|
'valid_from': ('>=', tax_details.from_date),
|
||||||
|
'valid_upto': ('<=', tax_details.to_date)
|
||||||
|
}, 'name')
|
||||||
|
|
||||||
if ldc_name:
|
if ldc_name:
|
||||||
return frappe.get_doc('Lower Deduction Certificate', ldc_name)
|
return frappe.get_doc('Lower Deduction Certificate', ldc_name)
|
||||||
|
|
||||||
def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, pan_no=None):
|
def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=None):
|
||||||
fiscal_year = fiscal_year_details[0]
|
vouchers = get_invoice_vouchers(parties, tax_details, inv.company, party_type=party_type)
|
||||||
|
advance_vouchers = get_advance_vouchers(parties, company=inv.company, from_date=tax_details.from_date,
|
||||||
|
to_date=tax_details.to_date, party_type=party_type)
|
||||||
vouchers = get_invoice_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
|
|
||||||
advance_vouchers = get_advance_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
|
|
||||||
taxable_vouchers = vouchers + advance_vouchers
|
taxable_vouchers = vouchers + advance_vouchers
|
||||||
|
|
||||||
tax_deducted = 0
|
tax_deducted = 0
|
||||||
if taxable_vouchers:
|
if taxable_vouchers:
|
||||||
tax_deducted = get_deducted_tax(taxable_vouchers, fiscal_year, tax_details)
|
tax_deducted = get_deducted_tax(taxable_vouchers, tax_details)
|
||||||
|
|
||||||
tax_amount = 0
|
tax_amount = 0
|
||||||
posting_date = inv.get('posting_date') or inv.get('transaction_date')
|
|
||||||
if party_type == 'Supplier':
|
if party_type == 'Supplier':
|
||||||
ldc = get_lower_deduction_certificate(fiscal_year, pan_no)
|
ldc = get_lower_deduction_certificate(tax_details, pan_no)
|
||||||
if tax_deducted:
|
if tax_deducted:
|
||||||
net_total = inv.net_total
|
net_total = inv.net_total
|
||||||
if ldc:
|
if ldc:
|
||||||
tax_amount = get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total)
|
tax_amount = get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net_total)
|
||||||
else:
|
else:
|
||||||
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
|
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
|
||||||
else:
|
else:
|
||||||
tax_amount = get_tds_amount(
|
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers)
|
||||||
ldc, parties, inv, tax_details,
|
|
||||||
fiscal_year_details, tax_deducted, vouchers
|
|
||||||
)
|
|
||||||
|
|
||||||
elif party_type == 'Customer':
|
elif party_type == 'Customer':
|
||||||
if tax_deducted:
|
if tax_deducted:
|
||||||
@ -183,29 +201,47 @@ def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, p
|
|||||||
else:
|
else:
|
||||||
# if no TCS has been charged in FY,
|
# if no TCS has been charged in FY,
|
||||||
# then chargeable value is "prev invoices + advances" value which cross the threshold
|
# then chargeable value is "prev invoices + advances" value which cross the threshold
|
||||||
tax_amount = get_tcs_amount(
|
tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
|
||||||
parties, inv, tax_details,
|
|
||||||
fiscal_year_details, vouchers, advance_vouchers
|
|
||||||
)
|
|
||||||
|
|
||||||
return tax_amount, tax_deducted
|
return tax_amount, tax_deducted
|
||||||
|
|
||||||
def get_invoice_vouchers(parties, fiscal_year, company, party_type='Supplier'):
|
def get_invoice_vouchers(parties, tax_details, company, party_type='Supplier'):
|
||||||
dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit'
|
dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit'
|
||||||
|
doctype = 'Purchase Invoice' if party_type == 'Supplier' else 'Sales Invoice'
|
||||||
|
|
||||||
filters = {
|
filters = {
|
||||||
dr_or_cr: ['>', 0],
|
|
||||||
'company': company,
|
'company': company,
|
||||||
'party_type': party_type,
|
frappe.scrub(party_type): ['in', parties],
|
||||||
'party': ['in', parties],
|
'posting_date': ['between', (tax_details.from_date, tax_details.to_date)],
|
||||||
'fiscal_year': fiscal_year,
|
|
||||||
'is_opening': 'No',
|
'is_opening': 'No',
|
||||||
'is_cancelled': 0
|
'docstatus': 1
|
||||||
}
|
}
|
||||||
|
|
||||||
return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck="voucher_no") or [""]
|
if not tax_details.get('consider_party_ledger_amount') and doctype != "Sales Invoice":
|
||||||
|
filters.update({
|
||||||
|
'apply_tds': 1,
|
||||||
|
'tax_withholding_category': tax_details.get('tax_withholding_category')
|
||||||
|
})
|
||||||
|
|
||||||
def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None, to_date=None, party_type='Supplier'):
|
invoices = frappe.get_all(doctype, filters=filters, pluck="name") or [""]
|
||||||
|
|
||||||
|
journal_entries = frappe.db.sql("""
|
||||||
|
SELECT j.name
|
||||||
|
FROM `tabJournal Entry` j, `tabJournal Entry Account` ja
|
||||||
|
WHERE
|
||||||
|
j.docstatus = 1
|
||||||
|
AND j.is_opening = 'No'
|
||||||
|
AND j.posting_date between %s and %s
|
||||||
|
AND ja.{dr_or_cr} > 0
|
||||||
|
AND ja.party in %s
|
||||||
|
""".format(dr_or_cr=dr_or_cr), (tax_details.from_date, tax_details.to_date, tuple(parties)), as_list=1)
|
||||||
|
|
||||||
|
if journal_entries:
|
||||||
|
journal_entries = journal_entries[0]
|
||||||
|
|
||||||
|
return invoices + journal_entries
|
||||||
|
|
||||||
|
def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, party_type='Supplier'):
|
||||||
# for advance vouchers, debit and credit is reversed
|
# for advance vouchers, debit and credit is reversed
|
||||||
dr_or_cr = 'debit' if party_type == 'Supplier' else 'credit'
|
dr_or_cr = 'debit' if party_type == 'Supplier' else 'credit'
|
||||||
|
|
||||||
@ -218,8 +254,6 @@ def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None
|
|||||||
'against_voucher': ['is', 'not set']
|
'against_voucher': ['is', 'not set']
|
||||||
}
|
}
|
||||||
|
|
||||||
if fiscal_year:
|
|
||||||
filters['fiscal_year'] = fiscal_year
|
|
||||||
if company:
|
if company:
|
||||||
filters['company'] = company
|
filters['company'] = company
|
||||||
if from_date and to_date:
|
if from_date and to_date:
|
||||||
@ -227,20 +261,21 @@ def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None
|
|||||||
|
|
||||||
return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no') or [""]
|
return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no') or [""]
|
||||||
|
|
||||||
def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details):
|
def get_deducted_tax(taxable_vouchers, tax_details):
|
||||||
# check if TDS / TCS account is already charged on taxable vouchers
|
# check if TDS / TCS account is already charged on taxable vouchers
|
||||||
filters = {
|
filters = {
|
||||||
'is_cancelled': 0,
|
'is_cancelled': 0,
|
||||||
'credit': ['>', 0],
|
'credit': ['>', 0],
|
||||||
'fiscal_year': fiscal_year,
|
'posting_date': ['between', (tax_details.from_date, tax_details.to_date)],
|
||||||
'account': tax_details.account_head,
|
'account': tax_details.account_head,
|
||||||
'voucher_no': ['in', taxable_vouchers],
|
'voucher_no': ['in', taxable_vouchers],
|
||||||
}
|
}
|
||||||
field = "sum(credit)"
|
field = "credit"
|
||||||
|
|
||||||
return frappe.db.get_value('GL Entry', filters, field) or 0.0
|
entries = frappe.db.get_all('GL Entry', filters, pluck=field)
|
||||||
|
return sum(entries)
|
||||||
|
|
||||||
def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers):
|
def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
|
||||||
tds_amount = 0
|
tds_amount = 0
|
||||||
invoice_filters = {
|
invoice_filters = {
|
||||||
'name': ('in', vouchers),
|
'name': ('in', vouchers),
|
||||||
@ -264,7 +299,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
|
|||||||
supp_credit_amt += supp_jv_credit_amt
|
supp_credit_amt += supp_jv_credit_amt
|
||||||
supp_credit_amt += inv.net_total
|
supp_credit_amt += inv.net_total
|
||||||
|
|
||||||
debit_note_amount = get_debit_note_amount(parties, fiscal_year_details, inv.company)
|
debit_note_amount = get_debit_note_amount(parties, tax_details.from_date, tax_details.to_date, inv.company)
|
||||||
supp_credit_amt -= debit_note_amount
|
supp_credit_amt -= debit_note_amount
|
||||||
|
|
||||||
threshold = tax_details.get('threshold', 0)
|
threshold = tax_details.get('threshold', 0)
|
||||||
@ -292,9 +327,8 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
|
|||||||
|
|
||||||
return tds_amount
|
return tds_amount
|
||||||
|
|
||||||
def get_tcs_amount(parties, inv, tax_details, fiscal_year_details, vouchers, adv_vouchers):
|
def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
||||||
tcs_amount = 0
|
tcs_amount = 0
|
||||||
fiscal_year, _, _ = fiscal_year_details
|
|
||||||
|
|
||||||
# sum of debit entries made from sales invoices
|
# sum of debit entries made from sales invoices
|
||||||
invoiced_amt = frappe.db.get_value('GL Entry', {
|
invoiced_amt = frappe.db.get_value('GL Entry', {
|
||||||
@ -313,14 +347,14 @@ def get_tcs_amount(parties, inv, tax_details, fiscal_year_details, vouchers, adv
|
|||||||
}, 'sum(credit)') or 0.0
|
}, 'sum(credit)') or 0.0
|
||||||
|
|
||||||
# sum of credit entries made from sales invoice
|
# sum of credit entries made from sales invoice
|
||||||
credit_note_amt = frappe.db.get_value('GL Entry', {
|
credit_note_amt = sum(frappe.db.get_all('GL Entry', {
|
||||||
'is_cancelled': 0,
|
'is_cancelled': 0,
|
||||||
'credit': ['>', 0],
|
'credit': ['>', 0],
|
||||||
'party': ['in', parties],
|
'party': ['in', parties],
|
||||||
'fiscal_year': fiscal_year,
|
'posting_date': ['between', (tax_details.from_date, tax_details.to_date)],
|
||||||
'company': inv.company,
|
'company': inv.company,
|
||||||
'voucher_type': 'Sales Invoice',
|
'voucher_type': 'Sales Invoice',
|
||||||
}, 'sum(credit)') or 0.0
|
}, pluck='credit'))
|
||||||
|
|
||||||
cumulative_threshold = tax_details.get('cumulative_threshold', 0)
|
cumulative_threshold = tax_details.get('cumulative_threshold', 0)
|
||||||
|
|
||||||
@ -339,7 +373,7 @@ def get_invoice_total_without_tcs(inv, tax_details):
|
|||||||
|
|
||||||
return inv.grand_total - tcs_tax_row_amount
|
return inv.grand_total - tcs_tax_row_amount
|
||||||
|
|
||||||
def get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total):
|
def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net_total):
|
||||||
tds_amount = 0
|
tds_amount = 0
|
||||||
limit_consumed = frappe.db.get_value('Purchase Invoice', {
|
limit_consumed = frappe.db.get_value('Purchase Invoice', {
|
||||||
'supplier': ('in', parties),
|
'supplier': ('in', parties),
|
||||||
@ -356,14 +390,13 @@ def get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, post
|
|||||||
|
|
||||||
return tds_amount
|
return tds_amount
|
||||||
|
|
||||||
def get_debit_note_amount(suppliers, fiscal_year_details, company=None):
|
def get_debit_note_amount(suppliers, from_date, to_date, company=None):
|
||||||
_, year_start_date, year_end_date = fiscal_year_details
|
|
||||||
|
|
||||||
filters = {
|
filters = {
|
||||||
'supplier': ['in', suppliers],
|
'supplier': ['in', suppliers],
|
||||||
'is_return': 1,
|
'is_return': 1,
|
||||||
'docstatus': 1,
|
'docstatus': 1,
|
||||||
'posting_date': ['between', (year_start_date, year_end_date)]
|
'posting_date': ['between', (from_date, to_date)]
|
||||||
}
|
}
|
||||||
fields = ['abs(sum(net_total)) as net_total']
|
fields = ['abs(sum(net_total)) as net_total']
|
||||||
|
|
||||||
|
@ -176,6 +176,29 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
|||||||
for d in invoices:
|
for d in invoices:
|
||||||
d.cancel()
|
d.cancel()
|
||||||
|
|
||||||
|
def test_multi_category_single_supplier(self):
|
||||||
|
frappe.db.set_value("Supplier", "Test TDS Supplier5", "tax_withholding_category", "Test Service Category")
|
||||||
|
invoices = []
|
||||||
|
|
||||||
|
pi = create_purchase_invoice(supplier = "Test TDS Supplier5", rate = 500, do_not_save=True)
|
||||||
|
pi.tax_withholding_category = "Test Service Category"
|
||||||
|
pi.save()
|
||||||
|
pi.submit()
|
||||||
|
invoices.append(pi)
|
||||||
|
|
||||||
|
# Second Invoice will apply TDS checked
|
||||||
|
pi1 = create_purchase_invoice(supplier = "Test TDS Supplier5", rate = 2500, do_not_save=True)
|
||||||
|
pi1.tax_withholding_category = "Test Goods Category"
|
||||||
|
pi1.save()
|
||||||
|
pi1.submit()
|
||||||
|
invoices.append(pi1)
|
||||||
|
|
||||||
|
self.assertEqual(pi1.taxes[0].tax_amount, 250)
|
||||||
|
|
||||||
|
#delete invoices to avoid clashing
|
||||||
|
for d in invoices:
|
||||||
|
d.cancel()
|
||||||
|
|
||||||
def cancel_invoices():
|
def cancel_invoices():
|
||||||
purchase_invoices = frappe.get_all("Purchase Invoice", {
|
purchase_invoices = frappe.get_all("Purchase Invoice", {
|
||||||
'supplier': ['in', ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']],
|
'supplier': ['in', ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']],
|
||||||
@ -251,7 +274,8 @@ def create_sales_invoice(**args):
|
|||||||
|
|
||||||
def create_records():
|
def create_records():
|
||||||
# create a new suppliers
|
# create a new suppliers
|
||||||
for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3', 'Test TDS Supplier4']:
|
for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3',
|
||||||
|
'Test TDS Supplier4', 'Test TDS Supplier5']:
|
||||||
if frappe.db.exists('Supplier', name):
|
if frappe.db.exists('Supplier', name):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -313,16 +337,16 @@ def create_records():
|
|||||||
}).insert()
|
}).insert()
|
||||||
|
|
||||||
def create_tax_with_holding_category():
|
def create_tax_with_holding_category():
|
||||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")[0]
|
fiscal_year = get_fiscal_year(today(), company="_Test Company")
|
||||||
|
# Cumulative threshold
|
||||||
# Cummulative thresold
|
|
||||||
if not frappe.db.exists("Tax Withholding Category", "Cumulative Threshold TDS"):
|
if not frappe.db.exists("Tax Withholding Category", "Cumulative Threshold TDS"):
|
||||||
frappe.get_doc({
|
frappe.get_doc({
|
||||||
"doctype": "Tax Withholding Category",
|
"doctype": "Tax Withholding Category",
|
||||||
"name": "Cumulative Threshold TDS",
|
"name": "Cumulative Threshold TDS",
|
||||||
"category_name": "10% TDS",
|
"category_name": "10% TDS",
|
||||||
"rates": [{
|
"rates": [{
|
||||||
'fiscal_year': fiscal_year,
|
'from_date': fiscal_year[1],
|
||||||
|
'to_date': fiscal_year[2],
|
||||||
'tax_withholding_rate': 10,
|
'tax_withholding_rate': 10,
|
||||||
'single_threshold': 0,
|
'single_threshold': 0,
|
||||||
'cumulative_threshold': 30000.00
|
'cumulative_threshold': 30000.00
|
||||||
@ -339,7 +363,8 @@ def create_tax_with_holding_category():
|
|||||||
"name": "Cumulative Threshold TCS",
|
"name": "Cumulative Threshold TCS",
|
||||||
"category_name": "10% TCS",
|
"category_name": "10% TCS",
|
||||||
"rates": [{
|
"rates": [{
|
||||||
'fiscal_year': fiscal_year,
|
'from_date': fiscal_year[1],
|
||||||
|
'to_date': fiscal_year[2],
|
||||||
'tax_withholding_rate': 10,
|
'tax_withholding_rate': 10,
|
||||||
'single_threshold': 0,
|
'single_threshold': 0,
|
||||||
'cumulative_threshold': 30000.00
|
'cumulative_threshold': 30000.00
|
||||||
@ -357,7 +382,8 @@ def create_tax_with_holding_category():
|
|||||||
"name": "Single Threshold TDS",
|
"name": "Single Threshold TDS",
|
||||||
"category_name": "10% TDS",
|
"category_name": "10% TDS",
|
||||||
"rates": [{
|
"rates": [{
|
||||||
'fiscal_year': fiscal_year,
|
'from_date': fiscal_year[1],
|
||||||
|
'to_date': fiscal_year[2],
|
||||||
'tax_withholding_rate': 10,
|
'tax_withholding_rate': 10,
|
||||||
'single_threshold': 20000.00,
|
'single_threshold': 20000.00,
|
||||||
'cumulative_threshold': 0
|
'cumulative_threshold': 0
|
||||||
@ -377,7 +403,8 @@ def create_tax_with_holding_category():
|
|||||||
"consider_party_ledger_amount": 1,
|
"consider_party_ledger_amount": 1,
|
||||||
"tax_on_excess_amount": 1,
|
"tax_on_excess_amount": 1,
|
||||||
"rates": [{
|
"rates": [{
|
||||||
'fiscal_year': fiscal_year,
|
'from_date': fiscal_year[1],
|
||||||
|
'to_date': fiscal_year[2],
|
||||||
'tax_withholding_rate': 10,
|
'tax_withholding_rate': 10,
|
||||||
'single_threshold': 0,
|
'single_threshold': 0,
|
||||||
'cumulative_threshold': 30000
|
'cumulative_threshold': 30000
|
||||||
@ -387,3 +414,39 @@ def create_tax_with_holding_category():
|
|||||||
'account': 'TDS - _TC'
|
'account': 'TDS - _TC'
|
||||||
}]
|
}]
|
||||||
}).insert()
|
}).insert()
|
||||||
|
|
||||||
|
if not frappe.db.exists("Tax Withholding Category", "Test Service Category"):
|
||||||
|
frappe.get_doc({
|
||||||
|
"doctype": "Tax Withholding Category",
|
||||||
|
"name": "Test Service Category",
|
||||||
|
"category_name": "Test Service Category",
|
||||||
|
"rates": [{
|
||||||
|
'from_date': fiscal_year[1],
|
||||||
|
'to_date': fiscal_year[2],
|
||||||
|
'tax_withholding_rate': 10,
|
||||||
|
'single_threshold': 2000,
|
||||||
|
'cumulative_threshold': 2000
|
||||||
|
}],
|
||||||
|
"accounts": [{
|
||||||
|
'company': '_Test Company',
|
||||||
|
'account': 'TDS - _TC'
|
||||||
|
}]
|
||||||
|
}).insert()
|
||||||
|
|
||||||
|
if not frappe.db.exists("Tax Withholding Category", "Test Goods Category"):
|
||||||
|
frappe.get_doc({
|
||||||
|
"doctype": "Tax Withholding Category",
|
||||||
|
"name": "Test Goods Category",
|
||||||
|
"category_name": "Test Goods Category",
|
||||||
|
"rates": [{
|
||||||
|
'from_date': fiscal_year[1],
|
||||||
|
'to_date': fiscal_year[2],
|
||||||
|
'tax_withholding_rate': 10,
|
||||||
|
'single_threshold': 2000,
|
||||||
|
'cumulative_threshold': 2000
|
||||||
|
}],
|
||||||
|
"accounts": [{
|
||||||
|
'company': '_Test Company',
|
||||||
|
'account': 'TDS - _TC'
|
||||||
|
}]
|
||||||
|
}).insert()
|
||||||
|
@ -1,202 +1,72 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2018-07-17 16:53:13.716665",
|
"creation": "2018-07-17 16:53:13.716665",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "",
|
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"from_date",
|
||||||
|
"to_date",
|
||||||
|
"tax_withholding_rate",
|
||||||
|
"column_break_3",
|
||||||
|
"single_threshold",
|
||||||
|
"cumulative_threshold"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"columns": 1,
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 2,
|
|
||||||
"fieldname": "fiscal_year",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Fiscal Year",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Fiscal Year",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 2,
|
|
||||||
"fieldname": "tax_withholding_rate",
|
"fieldname": "tax_withholding_rate",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Tax Withholding Rate",
|
"label": "Tax Withholding Rate",
|
||||||
"length": 0,
|
"reqd": 1
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "column_break_3",
|
"fieldname": "column_break_3",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"columns": 2,
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 3,
|
|
||||||
"fieldname": "single_threshold",
|
"fieldname": "single_threshold",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
"label": "Single Transaction Threshold"
|
||||||
"label": "Single Transaction Threshold",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 3,
|
"columns": 3,
|
||||||
"fieldname": "cumulative_threshold",
|
"fieldname": "cumulative_threshold",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
"label": "Cumulative Transaction Threshold"
|
||||||
"label": "Cumulative Transaction Threshold",
|
},
|
||||||
"length": 0,
|
{
|
||||||
"no_copy": 0,
|
"columns": 2,
|
||||||
"permlevel": 0,
|
"fieldname": "from_date",
|
||||||
"precision": "",
|
"fieldtype": "Date",
|
||||||
"print_hide": 0,
|
"in_list_view": 1,
|
||||||
"print_hide_if_no_value": 0,
|
"label": "From Date",
|
||||||
"read_only": 0,
|
"reqd": 1
|
||||||
"remember_last_selected_value": 0,
|
},
|
||||||
"report_hide": 0,
|
{
|
||||||
"reqd": 0,
|
"columns": 2,
|
||||||
"search_index": 0,
|
"fieldname": "to_date",
|
||||||
"set_only_once": 0,
|
"fieldtype": "Date",
|
||||||
"translatable": 0,
|
"in_list_view": 1,
|
||||||
"unique": 0
|
"label": "To Date",
|
||||||
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
"index_web_pages_for_search": 1,
|
||||||
"hide_heading": 0,
|
|
||||||
"hide_toolbar": 0,
|
|
||||||
"idx": 0,
|
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"max_attachments": 0,
|
"links": [],
|
||||||
"modified": "2018-07-17 17:13:09.819580",
|
"modified": "2021-08-31 11:42:12.213977",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Tax Withholding Rate",
|
"name": "Tax Withholding Rate",
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1,
|
"track_changes": 1
|
||||||
"track_seen": 0,
|
|
||||||
"track_views": 0
|
|
||||||
}
|
}
|
@ -284,13 +284,16 @@ def check_freezing_date(posting_date, adv_adj=False):
|
|||||||
"""
|
"""
|
||||||
Nobody can do GL Entries where posting date is before freezing date
|
Nobody can do GL Entries where posting date is before freezing date
|
||||||
except authorized person
|
except authorized person
|
||||||
|
|
||||||
|
Administrator has all the roles so this check will be bypassed if any role is allowed to post
|
||||||
|
Hence stop admin to bypass if accounts are freezed
|
||||||
"""
|
"""
|
||||||
if not adv_adj:
|
if not adv_adj:
|
||||||
acc_frozen_upto = frappe.db.get_value('Accounts Settings', None, 'acc_frozen_upto')
|
acc_frozen_upto = frappe.db.get_value('Accounts Settings', None, 'acc_frozen_upto')
|
||||||
if acc_frozen_upto:
|
if acc_frozen_upto:
|
||||||
frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None,'frozen_accounts_modifier')
|
frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None,'frozen_accounts_modifier')
|
||||||
if getdate(posting_date) <= getdate(acc_frozen_upto) \
|
if getdate(posting_date) <= getdate(acc_frozen_upto) \
|
||||||
and not frozen_accounts_modifier in frappe.get_roles():
|
and not frozen_accounts_modifier 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)))
|
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):
|
def set_as_cancel(voucher_type, voucher_no):
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
frappe.query_reports["Accounts Payable"] = {
|
frappe.query_reports["Accounts Payable"] = {
|
||||||
"filters": [
|
"filters": [
|
||||||
{
|
{
|
||||||
"fieldname":"company",
|
"fieldname": "company",
|
||||||
"label": __("Company"),
|
"label": __("Company"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Company",
|
"options": "Company",
|
||||||
@ -12,19 +12,19 @@ frappe.query_reports["Accounts Payable"] = {
|
|||||||
"default": frappe.defaults.get_user_default("Company")
|
"default": frappe.defaults.get_user_default("Company")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"report_date",
|
"fieldname": "report_date",
|
||||||
"label": __("Posting Date"),
|
"label": __("Posting Date"),
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"default": frappe.datetime.get_today()
|
"default": frappe.datetime.get_today()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"finance_book",
|
"fieldname": "finance_book",
|
||||||
"label": __("Finance Book"),
|
"label": __("Finance Book"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Finance Book"
|
"options": "Finance Book"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"cost_center",
|
"fieldname": "cost_center",
|
||||||
"label": __("Cost Center"),
|
"label": __("Cost Center"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Cost Center",
|
"options": "Cost Center",
|
||||||
@ -38,7 +38,7 @@ frappe.query_reports["Accounts Payable"] = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"supplier",
|
"fieldname": "supplier",
|
||||||
"label": __("Supplier"),
|
"label": __("Supplier"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Supplier",
|
"options": "Supplier",
|
||||||
@ -54,48 +54,48 @@ frappe.query_reports["Accounts Payable"] = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"ageing_based_on",
|
"fieldname": "ageing_based_on",
|
||||||
"label": __("Ageing Based On"),
|
"label": __("Ageing Based On"),
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"options": 'Posting Date\nDue Date\nSupplier Invoice Date',
|
"options": 'Posting Date\nDue Date\nSupplier Invoice Date',
|
||||||
"default": "Due Date"
|
"default": "Due Date"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"range1",
|
"fieldname": "range1",
|
||||||
"label": __("Ageing Range 1"),
|
"label": __("Ageing Range 1"),
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"default": "30",
|
"default": "30",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"range2",
|
"fieldname": "range2",
|
||||||
"label": __("Ageing Range 2"),
|
"label": __("Ageing Range 2"),
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"default": "60",
|
"default": "60",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"range3",
|
"fieldname": "range3",
|
||||||
"label": __("Ageing Range 3"),
|
"label": __("Ageing Range 3"),
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"default": "90",
|
"default": "90",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"range4",
|
"fieldname": "range4",
|
||||||
"label": __("Ageing Range 4"),
|
"label": __("Ageing Range 4"),
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"default": "120",
|
"default": "120",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"payment_terms_template",
|
"fieldname": "payment_terms_template",
|
||||||
"label": __("Payment Terms Template"),
|
"label": __("Payment Terms Template"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Payment Terms Template"
|
"options": "Payment Terms Template"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"supplier_group",
|
"fieldname": "supplier_group",
|
||||||
"label": __("Supplier Group"),
|
"label": __("Supplier Group"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Supplier Group"
|
"options": "Supplier Group"
|
||||||
@ -106,12 +106,17 @@ frappe.query_reports["Accounts Payable"] = {
|
|||||||
"fieldtype": "Check"
|
"fieldtype": "Check"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"based_on_payment_terms",
|
"fieldname": "based_on_payment_terms",
|
||||||
"label": __("Based On Payment Terms"),
|
"label": __("Based On Payment Terms"),
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"tax_id",
|
"fieldname": "show_remarks",
|
||||||
|
"label": __("Show Remarks"),
|
||||||
|
"fieldtype": "Check",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "tax_id",
|
||||||
"label": __("Tax Id"),
|
"label": __("Tax Id"),
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 1
|
"hidden": 1
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
frappe.query_reports["Accounts Receivable"] = {
|
frappe.query_reports["Accounts Receivable"] = {
|
||||||
"filters": [
|
"filters": [
|
||||||
{
|
{
|
||||||
"fieldname":"company",
|
"fieldname": "company",
|
||||||
"label": __("Company"),
|
"label": __("Company"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Company",
|
"options": "Company",
|
||||||
@ -12,19 +12,19 @@ frappe.query_reports["Accounts Receivable"] = {
|
|||||||
"default": frappe.defaults.get_user_default("Company")
|
"default": frappe.defaults.get_user_default("Company")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"report_date",
|
"fieldname": "report_date",
|
||||||
"label": __("Posting Date"),
|
"label": __("Posting Date"),
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"default": frappe.datetime.get_today()
|
"default": frappe.datetime.get_today()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"finance_book",
|
"fieldname": "finance_book",
|
||||||
"label": __("Finance Book"),
|
"label": __("Finance Book"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Finance Book"
|
"options": "Finance Book"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"cost_center",
|
"fieldname": "cost_center",
|
||||||
"label": __("Cost Center"),
|
"label": __("Cost Center"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Cost Center",
|
"options": "Cost Center",
|
||||||
@ -38,7 +38,7 @@ frappe.query_reports["Accounts Receivable"] = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"customer",
|
"fieldname": "customer",
|
||||||
"label": __("Customer"),
|
"label": __("Customer"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Customer",
|
"options": "Customer",
|
||||||
@ -67,66 +67,66 @@ frappe.query_reports["Accounts Receivable"] = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"ageing_based_on",
|
"fieldname": "ageing_based_on",
|
||||||
"label": __("Ageing Based On"),
|
"label": __("Ageing Based On"),
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"options": 'Posting Date\nDue Date',
|
"options": 'Posting Date\nDue Date',
|
||||||
"default": "Due Date"
|
"default": "Due Date"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"range1",
|
"fieldname": "range1",
|
||||||
"label": __("Ageing Range 1"),
|
"label": __("Ageing Range 1"),
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"default": "30",
|
"default": "30",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"range2",
|
"fieldname": "range2",
|
||||||
"label": __("Ageing Range 2"),
|
"label": __("Ageing Range 2"),
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"default": "60",
|
"default": "60",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"range3",
|
"fieldname": "range3",
|
||||||
"label": __("Ageing Range 3"),
|
"label": __("Ageing Range 3"),
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"default": "90",
|
"default": "90",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"range4",
|
"fieldname": "range4",
|
||||||
"label": __("Ageing Range 4"),
|
"label": __("Ageing Range 4"),
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"default": "120",
|
"default": "120",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"customer_group",
|
"fieldname": "customer_group",
|
||||||
"label": __("Customer Group"),
|
"label": __("Customer Group"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Customer Group"
|
"options": "Customer Group"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"payment_terms_template",
|
"fieldname": "payment_terms_template",
|
||||||
"label": __("Payment Terms Template"),
|
"label": __("Payment Terms Template"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Payment Terms Template"
|
"options": "Payment Terms Template"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"sales_partner",
|
"fieldname": "sales_partner",
|
||||||
"label": __("Sales Partner"),
|
"label": __("Sales Partner"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Sales Partner"
|
"options": "Sales Partner"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"sales_person",
|
"fieldname": "sales_person",
|
||||||
"label": __("Sales Person"),
|
"label": __("Sales Person"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Sales Person"
|
"options": "Sales Person"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"territory",
|
"fieldname": "territory",
|
||||||
"label": __("Territory"),
|
"label": __("Territory"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Territory"
|
"options": "Territory"
|
||||||
@ -137,45 +137,50 @@ frappe.query_reports["Accounts Receivable"] = {
|
|||||||
"fieldtype": "Check"
|
"fieldtype": "Check"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"based_on_payment_terms",
|
"fieldname": "based_on_payment_terms",
|
||||||
"label": __("Based On Payment Terms"),
|
"label": __("Based On Payment Terms"),
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"show_future_payments",
|
"fieldname": "show_future_payments",
|
||||||
"label": __("Show Future Payments"),
|
"label": __("Show Future Payments"),
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"show_delivery_notes",
|
"fieldname": "show_delivery_notes",
|
||||||
"label": __("Show Linked Delivery Notes"),
|
"label": __("Show Linked Delivery Notes"),
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"show_sales_person",
|
"fieldname": "show_sales_person",
|
||||||
"label": __("Show Sales Person"),
|
"label": __("Show Sales Person"),
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"tax_id",
|
"fieldname": "show_remarks",
|
||||||
|
"label": __("Show Remarks"),
|
||||||
|
"fieldtype": "Check",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "tax_id",
|
||||||
"label": __("Tax Id"),
|
"label": __("Tax Id"),
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 1
|
"hidden": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"customer_name",
|
"fieldname": "customer_name",
|
||||||
"label": __("Customer Name"),
|
"label": __("Customer Name"),
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 1
|
"hidden": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"payment_terms",
|
"fieldname": "payment_terms",
|
||||||
"label": __("Payment Tems"),
|
"label": __("Payment Tems"),
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 1
|
"hidden": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"credit_limit",
|
"fieldname": "credit_limit",
|
||||||
"label": __("Credit Limit"),
|
"label": __("Credit Limit"),
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"hidden": 1
|
"hidden": 1
|
||||||
|
@ -106,6 +106,7 @@ class ReceivablePayableReport(object):
|
|||||||
party = gle.party,
|
party = gle.party,
|
||||||
posting_date = gle.posting_date,
|
posting_date = gle.posting_date,
|
||||||
account_currency = gle.account_currency,
|
account_currency = gle.account_currency,
|
||||||
|
remarks = gle.remarks if self.filters.get("show_remarks") else None,
|
||||||
invoiced = 0.0,
|
invoiced = 0.0,
|
||||||
paid = 0.0,
|
paid = 0.0,
|
||||||
credit_note = 0.0,
|
credit_note = 0.0,
|
||||||
@ -583,10 +584,12 @@ class ReceivablePayableReport(object):
|
|||||||
else:
|
else:
|
||||||
select_fields = "debit, credit"
|
select_fields = "debit, credit"
|
||||||
|
|
||||||
|
remarks = ", remarks" if self.filters.get("show_remarks") else ""
|
||||||
|
|
||||||
self.gl_entries = frappe.db.sql("""
|
self.gl_entries = frappe.db.sql("""
|
||||||
select
|
select
|
||||||
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
|
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
|
||||||
against_voucher_type, against_voucher, account_currency, {0}
|
against_voucher_type, against_voucher, account_currency, {0} {remarks}
|
||||||
from
|
from
|
||||||
`tabGL Entry`
|
`tabGL Entry`
|
||||||
where
|
where
|
||||||
@ -595,7 +598,7 @@ class ReceivablePayableReport(object):
|
|||||||
and party_type=%s
|
and party_type=%s
|
||||||
and (party is not null and party != '')
|
and (party is not null and party != '')
|
||||||
{1} {2} {3}"""
|
{1} {2} {3}"""
|
||||||
.format(select_fields, date_condition, conditions, order_by), values, as_dict=True)
|
.format(select_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True)
|
||||||
|
|
||||||
def get_sales_invoices_or_customers_based_on_sales_person(self):
|
def get_sales_invoices_or_customers_based_on_sales_person(self):
|
||||||
if self.filters.get("sales_person"):
|
if self.filters.get("sales_person"):
|
||||||
@ -754,6 +757,10 @@ class ReceivablePayableReport(object):
|
|||||||
self.add_column(label=_('Voucher Type'), fieldname='voucher_type', fieldtype='Data')
|
self.add_column(label=_('Voucher Type'), fieldname='voucher_type', fieldtype='Data')
|
||||||
self.add_column(label=_('Voucher No'), fieldname='voucher_no', fieldtype='Dynamic Link',
|
self.add_column(label=_('Voucher No'), fieldname='voucher_no', fieldtype='Dynamic Link',
|
||||||
options='voucher_type', width=180)
|
options='voucher_type', width=180)
|
||||||
|
|
||||||
|
if self.filters.show_remarks:
|
||||||
|
self.add_column(label=_('Remarks'), fieldname='remarks', fieldtype='Text', width=200),
|
||||||
|
|
||||||
self.add_column(label='Due Date', fieldtype='Date')
|
self.add_column(label='Due Date', fieldtype='Date')
|
||||||
|
|
||||||
if self.party_type == "Supplier":
|
if self.party_type == "Supplier":
|
||||||
|
@ -260,7 +260,12 @@ def get_company_currency(filters=None):
|
|||||||
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, start_date, filters):
|
||||||
for entries in gl_entries_by_account.values():
|
for entries in gl_entries_by_account.values():
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
d = accounts_by_name.get(entry.account_name)
|
if entry.account_number:
|
||||||
|
account_name = entry.account_number + ' - ' + entry.account_name
|
||||||
|
else:
|
||||||
|
account_name = entry.account_name
|
||||||
|
|
||||||
|
d = accounts_by_name.get(account_name)
|
||||||
if d:
|
if d:
|
||||||
for company in companies:
|
for company in companies:
|
||||||
# check if posting date is within the period
|
# check if posting date is within the period
|
||||||
@ -307,7 +312,14 @@ def update_parent_account_names(accounts):
|
|||||||
of account_number and suffix of company abbr. This function adds key called
|
of account_number and suffix of company abbr. This function adds key called
|
||||||
`parent_account_name` which does not have such prefix/suffix.
|
`parent_account_name` which does not have such prefix/suffix.
|
||||||
"""
|
"""
|
||||||
name_to_account_map = { d.name : d.account_name for d in accounts }
|
name_to_account_map = {}
|
||||||
|
|
||||||
|
for d in accounts:
|
||||||
|
if d.account_number:
|
||||||
|
account_name = d.account_number + ' - ' + d.account_name
|
||||||
|
else:
|
||||||
|
account_name = d.account_name
|
||||||
|
name_to_account_map[d.name] = account_name
|
||||||
|
|
||||||
for account in accounts:
|
for account in accounts:
|
||||||
if account.parent_account:
|
if account.parent_account:
|
||||||
@ -420,7 +432,11 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g
|
|||||||
convert_to_presentation_currency(gl_entries, currency_info, filters.get('company'))
|
convert_to_presentation_currency(gl_entries, currency_info, filters.get('company'))
|
||||||
|
|
||||||
for entry in gl_entries:
|
for entry in gl_entries:
|
||||||
account_name = entry.account_name
|
if entry.account_number:
|
||||||
|
account_name = entry.account_number + ' - ' + entry.account_name
|
||||||
|
else:
|
||||||
|
account_name = entry.account_name
|
||||||
|
|
||||||
validate_entries(account_name, entry, accounts_by_name, accounts)
|
validate_entries(account_name, entry, accounts_by_name, accounts)
|
||||||
gl_entries_by_account.setdefault(account_name, []).append(entry)
|
gl_entries_by_account.setdefault(account_name, []).append(entry)
|
||||||
|
|
||||||
@ -491,7 +507,12 @@ def filter_accounts(accounts, depth=10):
|
|||||||
parent_children_map = {}
|
parent_children_map = {}
|
||||||
accounts_by_name = {}
|
accounts_by_name = {}
|
||||||
for d in accounts:
|
for d in accounts:
|
||||||
accounts_by_name[d.account_name] = d
|
if d.account_number:
|
||||||
|
account_name = d.account_number + ' - ' + d.account_name
|
||||||
|
else:
|
||||||
|
account_name = d.account_name
|
||||||
|
accounts_by_name[account_name] = d
|
||||||
|
|
||||||
parent_children_map.setdefault(d.parent_account or None, []).append(d)
|
parent_children_map.setdefault(d.parent_account or None, []).append(d)
|
||||||
|
|
||||||
filtered_accounts = []
|
filtered_accounts = []
|
||||||
|
@ -110,9 +110,26 @@ frappe.query_reports["General Ledger"] = {
|
|||||||
"fieldname":"group_by",
|
"fieldname":"group_by",
|
||||||
"label": __("Group by"),
|
"label": __("Group by"),
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"options": ["", __("Group by Voucher"), __("Group by Voucher (Consolidated)"),
|
"options": [
|
||||||
__("Group by Account"), __("Group by Party")],
|
"",
|
||||||
"default": __("Group by Voucher (Consolidated)")
|
{
|
||||||
|
label: __("Group by Voucher"),
|
||||||
|
value: "Group by Voucher",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Group by Voucher (Consolidated)"),
|
||||||
|
value: "Group by Voucher (Consolidated)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Group by Account"),
|
||||||
|
value: "Group by Account",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Group by Party"),
|
||||||
|
value: "Group by Party",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"default": "Group by Voucher (Consolidated)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"tax_id",
|
"fieldname":"tax_id",
|
||||||
|
@ -62,14 +62,14 @@ def validate_filters(filters, account_details):
|
|||||||
if not account_details.get(account):
|
if not account_details.get(account):
|
||||||
frappe.throw(_("Account {0} does not exists").format(account))
|
frappe.throw(_("Account {0} does not exists").format(account))
|
||||||
|
|
||||||
if (filters.get("account") and filters.get("group_by") == _('Group by Account')):
|
if (filters.get("account") and filters.get("group_by") == 'Group by Account'):
|
||||||
filters.account = frappe.parse_json(filters.get('account'))
|
filters.account = frappe.parse_json(filters.get('account'))
|
||||||
for account in filters.account:
|
for account in filters.account:
|
||||||
if account_details[account].is_group == 0:
|
if account_details[account].is_group == 0:
|
||||||
frappe.throw(_("Can not filter based on Child Account, if grouped by Account"))
|
frappe.throw(_("Can not filter based on Child Account, if grouped by Account"))
|
||||||
|
|
||||||
if (filters.get("voucher_no")
|
if (filters.get("voucher_no")
|
||||||
and filters.get("group_by") in [_('Group by Voucher')]):
|
and filters.get("group_by") in ['Group by Voucher']):
|
||||||
frappe.throw(_("Can not filter based on Voucher No, if grouped by Voucher"))
|
frappe.throw(_("Can not filter based on Voucher No, if grouped by Voucher"))
|
||||||
|
|
||||||
if filters.from_date > filters.to_date:
|
if filters.from_date > filters.to_date:
|
||||||
@ -153,7 +153,7 @@ def get_gl_entries(filters, accounting_dimensions):
|
|||||||
if filters.get("include_dimensions"):
|
if filters.get("include_dimensions"):
|
||||||
order_by_statement = "order by posting_date, creation"
|
order_by_statement = "order by posting_date, creation"
|
||||||
|
|
||||||
if filters.get("group_by") == _("Group by Voucher"):
|
if filters.get("group_by") == "Group by Voucher":
|
||||||
order_by_statement = "order by posting_date, voucher_type, voucher_no"
|
order_by_statement = "order by posting_date, voucher_type, voucher_no"
|
||||||
|
|
||||||
if filters.get("include_default_book_entries"):
|
if filters.get("include_default_book_entries"):
|
||||||
@ -312,13 +312,13 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
|
|||||||
# Opening for filtered account
|
# Opening for filtered account
|
||||||
data.append(totals.opening)
|
data.append(totals.opening)
|
||||||
|
|
||||||
if filters.get("group_by") != _('Group by Voucher (Consolidated)'):
|
if filters.get("group_by") != 'Group by Voucher (Consolidated)':
|
||||||
for acc, acc_dict in iteritems(gle_map):
|
for acc, acc_dict in iteritems(gle_map):
|
||||||
# acc
|
# acc
|
||||||
if acc_dict.entries:
|
if acc_dict.entries:
|
||||||
# opening
|
# opening
|
||||||
data.append({})
|
data.append({})
|
||||||
if filters.get("group_by") != _("Group by Voucher"):
|
if filters.get("group_by") != "Group by Voucher":
|
||||||
data.append(acc_dict.totals.opening)
|
data.append(acc_dict.totals.opening)
|
||||||
|
|
||||||
data += acc_dict.entries
|
data += acc_dict.entries
|
||||||
@ -327,7 +327,7 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
|
|||||||
data.append(acc_dict.totals.total)
|
data.append(acc_dict.totals.total)
|
||||||
|
|
||||||
# closing
|
# closing
|
||||||
if filters.get("group_by") != _("Group by Voucher"):
|
if filters.get("group_by") != "Group by Voucher":
|
||||||
data.append(acc_dict.totals.closing)
|
data.append(acc_dict.totals.closing)
|
||||||
data.append({})
|
data.append({})
|
||||||
else:
|
else:
|
||||||
@ -357,9 +357,9 @@ def get_totals_dict():
|
|||||||
)
|
)
|
||||||
|
|
||||||
def group_by_field(group_by):
|
def group_by_field(group_by):
|
||||||
if group_by == _('Group by Party'):
|
if group_by == 'Group by Party':
|
||||||
return 'party'
|
return 'party'
|
||||||
elif group_by in [_('Group by Voucher (Consolidated)'), _('Group by Account')]:
|
elif group_by in ['Group by Voucher (Consolidated)', 'Group by Account']:
|
||||||
return 'account'
|
return 'account'
|
||||||
else:
|
else:
|
||||||
return 'voucher_no'
|
return 'voucher_no'
|
||||||
@ -423,9 +423,9 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
|||||||
elif gle.posting_date <= to_date:
|
elif gle.posting_date <= to_date:
|
||||||
update_value_in_dict(gle_map[gle.get(group_by)].totals, 'total', gle)
|
update_value_in_dict(gle_map[gle.get(group_by)].totals, 'total', gle)
|
||||||
update_value_in_dict(totals, 'total', gle)
|
update_value_in_dict(totals, 'total', gle)
|
||||||
if filters.get("group_by") != _('Group by Voucher (Consolidated)'):
|
if filters.get("group_by") != 'Group by Voucher (Consolidated)':
|
||||||
gle_map[gle.get(group_by)].entries.append(gle)
|
gle_map[gle.get(group_by)].entries.append(gle)
|
||||||
elif filters.get("group_by") == _('Group by Voucher (Consolidated)'):
|
elif filters.get("group_by") == 'Group by Voucher (Consolidated)':
|
||||||
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
|
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
|
||||||
for dim in accounting_dimensions:
|
for dim in accounting_dimensions:
|
||||||
keylist.append(gle.get(dim))
|
keylist.append(gle.get(dim))
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
{
|
{
|
||||||
"add_total_row": 0,
|
"add_total_row": 1,
|
||||||
|
"columns": [],
|
||||||
"creation": "2018-08-21 11:25:00.551823",
|
"creation": "2018-08-21 11:25:00.551823",
|
||||||
|
"disable_prepared_report": 0,
|
||||||
"disabled": 0,
|
"disabled": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Report",
|
"doctype": "Report",
|
||||||
|
"filters": [],
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_standard": "Yes",
|
"is_standard": "Yes",
|
||||||
"modified": "2018-09-21 11:25:00.551823",
|
"modified": "2021-09-20 17:43:39.518851",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "TDS Computation Summary",
|
"name": "TDS Computation Summary",
|
||||||
|
@ -2,11 +2,10 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import flt
|
|
||||||
|
|
||||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
from erpnext.accounts.report.tds_payable_monthly.tds_payable_monthly import (
|
||||||
get_advance_vouchers,
|
get_result,
|
||||||
get_debit_note_amount,
|
get_tds_docs,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
|
|
||||||
@ -17,9 +16,12 @@ def execute(filters=None):
|
|||||||
filters.naming_series = frappe.db.get_single_value('Buying Settings', 'supp_master_name')
|
filters.naming_series = frappe.db.get_single_value('Buying Settings', 'supp_master_name')
|
||||||
|
|
||||||
columns = get_columns(filters)
|
columns = get_columns(filters)
|
||||||
res = get_result(filters)
|
tds_docs, tds_accounts, tax_category_map = get_tds_docs(filters)
|
||||||
|
|
||||||
return columns, res
|
res = get_result(filters, tds_docs, tds_accounts, tax_category_map)
|
||||||
|
final_result = group_by_supplier_and_category(res)
|
||||||
|
|
||||||
|
return columns, final_result
|
||||||
|
|
||||||
def validate_filters(filters):
|
def validate_filters(filters):
|
||||||
''' Validate if dates are properly set and lie in the same fiscal year'''
|
''' Validate if dates are properly set and lie in the same fiscal year'''
|
||||||
@ -33,81 +35,39 @@ def validate_filters(filters):
|
|||||||
|
|
||||||
filters["fiscal_year"] = from_year
|
filters["fiscal_year"] = from_year
|
||||||
|
|
||||||
def get_result(filters):
|
def group_by_supplier_and_category(data):
|
||||||
# if no supplier selected, fetch data for all tds applicable supplier
|
supplier_category_wise_map = {}
|
||||||
# else fetch relevant data for selected supplier
|
|
||||||
pan = "pan" if frappe.db.has_column("Supplier", "pan") else "tax_id"
|
|
||||||
fields = ["name", pan+" as pan", "tax_withholding_category", "supplier_type", "supplier_name"]
|
|
||||||
|
|
||||||
if filters.supplier:
|
for row in data:
|
||||||
filters.supplier = frappe.db.get_list('Supplier',
|
supplier_category_wise_map.setdefault((row.get('supplier'), row.get('section_code')), {
|
||||||
{"name": filters.supplier}, fields)
|
'pan': row.get('pan'),
|
||||||
else:
|
'supplier': row.get('supplier'),
|
||||||
filters.supplier = frappe.db.get_list('Supplier',
|
'supplier_name': row.get('supplier_name'),
|
||||||
{"tax_withholding_category": ["!=", ""]}, fields)
|
'section_code': row.get('section_code'),
|
||||||
|
'entity_type': row.get('entity_type'),
|
||||||
|
'tds_rate': row.get('tds_rate'),
|
||||||
|
'total_amount_credited': 0.0,
|
||||||
|
'tds_deducted': 0.0
|
||||||
|
})
|
||||||
|
|
||||||
|
supplier_category_wise_map.get((row.get('supplier'), row.get('section_code')))['total_amount_credited'] += \
|
||||||
|
row.get('total_amount_credited', 0.0)
|
||||||
|
|
||||||
|
supplier_category_wise_map.get((row.get('supplier'), row.get('section_code')))['tds_deducted'] += \
|
||||||
|
row.get('tds_deducted', 0.0)
|
||||||
|
|
||||||
|
final_result = get_final_result(supplier_category_wise_map)
|
||||||
|
|
||||||
|
return final_result
|
||||||
|
|
||||||
|
|
||||||
|
def get_final_result(supplier_category_wise_map):
|
||||||
out = []
|
out = []
|
||||||
for supplier in filters.supplier:
|
for key, value in supplier_category_wise_map.items():
|
||||||
tds = frappe.get_doc("Tax Withholding Category", supplier.tax_withholding_category)
|
out.append(value)
|
||||||
rate = [d.tax_withholding_rate for d in tds.rates if d.fiscal_year == filters.fiscal_year]
|
|
||||||
|
|
||||||
if rate:
|
|
||||||
rate = rate[0]
|
|
||||||
|
|
||||||
try:
|
|
||||||
account = [d.account for d in tds.accounts if d.company == filters.company][0]
|
|
||||||
|
|
||||||
except IndexError:
|
|
||||||
account = []
|
|
||||||
total_invoiced_amount, tds_deducted = get_invoice_and_tds_amount(supplier.name, account,
|
|
||||||
filters.company, filters.from_date, filters.to_date, filters.fiscal_year)
|
|
||||||
|
|
||||||
if total_invoiced_amount or tds_deducted:
|
|
||||||
row = [supplier.pan, supplier.name]
|
|
||||||
|
|
||||||
if filters.naming_series == 'Naming Series':
|
|
||||||
row.append(supplier.supplier_name)
|
|
||||||
|
|
||||||
row.extend([tds.name, supplier.supplier_type, rate, total_invoiced_amount, tds_deducted])
|
|
||||||
out.append(row)
|
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, fiscal_year):
|
|
||||||
''' calculate total invoice amount and total tds deducted for given supplier '''
|
|
||||||
|
|
||||||
entries = frappe.db.sql("""
|
|
||||||
select voucher_no, credit
|
|
||||||
from `tabGL Entry`
|
|
||||||
where party in (%s) and credit > 0
|
|
||||||
and company=%s and is_cancelled = 0
|
|
||||||
and posting_date between %s and %s
|
|
||||||
""", (supplier, company, from_date, to_date), as_dict=1)
|
|
||||||
|
|
||||||
supplier_credit_amount = flt(sum(d.credit for d in entries))
|
|
||||||
|
|
||||||
vouchers = [d.voucher_no for d in entries]
|
|
||||||
vouchers += get_advance_vouchers([supplier], company=company,
|
|
||||||
from_date=from_date, to_date=to_date)
|
|
||||||
|
|
||||||
tds_deducted = 0
|
|
||||||
if vouchers:
|
|
||||||
tds_deducted = flt(frappe.db.sql("""
|
|
||||||
select sum(credit)
|
|
||||||
from `tabGL Entry`
|
|
||||||
where account=%s and posting_date between %s and %s
|
|
||||||
and company=%s and credit > 0 and voucher_no in ({0})
|
|
||||||
""".format(', '.join("'%s'" % d for d in vouchers)),
|
|
||||||
(account, from_date, to_date, company))[0][0])
|
|
||||||
|
|
||||||
date_range_filter = [fiscal_year, from_date, to_date]
|
|
||||||
|
|
||||||
debit_note_amount = get_debit_note_amount([supplier], date_range_filter, company=company)
|
|
||||||
|
|
||||||
total_invoiced_amount = supplier_credit_amount + tds_deducted - debit_note_amount
|
|
||||||
|
|
||||||
return total_invoiced_amount, tds_deducted
|
|
||||||
|
|
||||||
def get_columns(filters):
|
def get_columns(filters):
|
||||||
columns = [
|
columns = [
|
||||||
{
|
{
|
||||||
@ -149,7 +109,7 @@ def get_columns(filters):
|
|||||||
{
|
{
|
||||||
"label": _("TDS Rate %"),
|
"label": _("TDS Rate %"),
|
||||||
"fieldname": "tds_rate",
|
"fieldname": "tds_rate",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Percent",
|
||||||
"width": 90
|
"width": 90
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -16,69 +16,6 @@ frappe.query_reports["TDS Payable Monthly"] = {
|
|||||||
"label": __("Supplier"),
|
"label": __("Supplier"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Supplier",
|
"options": "Supplier",
|
||||||
"get_query": function() {
|
|
||||||
return {
|
|
||||||
"filters": {
|
|
||||||
"tax_withholding_category": ["!=", ""],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
on_change: function() {
|
|
||||||
frappe.query_report.set_filter_value("purchase_invoice", "");
|
|
||||||
frappe.query_report.refresh();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname":"purchase_invoice",
|
|
||||||
"label": __("Purchase Invoice"),
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"options": "Purchase Invoice",
|
|
||||||
"get_query": function() {
|
|
||||||
return {
|
|
||||||
"filters": {
|
|
||||||
"name": ["in", frappe.query_report.invoices]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
on_change: function() {
|
|
||||||
let supplier = frappe.query_report.get_filter_value('supplier');
|
|
||||||
if(!supplier) return; // return if no supplier selected
|
|
||||||
|
|
||||||
// filter invoices based on selected supplier
|
|
||||||
let invoices = [];
|
|
||||||
frappe.query_report.invoice_data.map(d => {
|
|
||||||
if(d.supplier==supplier)
|
|
||||||
invoices.push(d.name)
|
|
||||||
});
|
|
||||||
frappe.query_report.invoices = invoices;
|
|
||||||
frappe.query_report.refresh();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname":"purchase_order",
|
|
||||||
"label": __("Purchase Order"),
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"options": "Purchase Order",
|
|
||||||
"get_query": function() {
|
|
||||||
return {
|
|
||||||
"filters": {
|
|
||||||
"name": ["in", frappe.query_report.invoices]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
on_change: function() {
|
|
||||||
let supplier = frappe.query_report.get_filter_value('supplier');
|
|
||||||
if(!supplier) return; // return if no supplier selected
|
|
||||||
|
|
||||||
// filter invoices based on selected supplier
|
|
||||||
let invoices = [];
|
|
||||||
frappe.query_report.invoice_data.map(d => {
|
|
||||||
if(d.supplier==supplier)
|
|
||||||
invoices.push(d.name)
|
|
||||||
});
|
|
||||||
frappe.query_report.invoices = invoices;
|
|
||||||
frappe.query_report.refresh();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"from_date",
|
"fieldname":"from_date",
|
||||||
@ -96,23 +33,5 @@ frappe.query_reports["TDS Payable Monthly"] = {
|
|||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
"width": "60px"
|
"width": "60px"
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
|
|
||||||
onload: function(report) {
|
|
||||||
// fetch all tds applied invoices
|
|
||||||
frappe.call({
|
|
||||||
"method": "erpnext.accounts.report.tds_payable_monthly.tds_payable_monthly.get_tds_invoices_and_orders",
|
|
||||||
callback: function(r) {
|
|
||||||
let invoices = [];
|
|
||||||
|
|
||||||
r.message.map(d => {
|
|
||||||
invoices.push(d.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
report["invoice_data"] = r.message.invoices;
|
|
||||||
report["invoices"] = invoices;
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
{
|
{
|
||||||
"add_total_row": 1,
|
"add_total_row": 1,
|
||||||
|
"columns": [],
|
||||||
"creation": "2018-08-21 11:32:30.874923",
|
"creation": "2018-08-21 11:32:30.874923",
|
||||||
"disable_prepared_report": 0,
|
"disable_prepared_report": 0,
|
||||||
"disabled": 0,
|
"disabled": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Report",
|
"doctype": "Report",
|
||||||
|
"filters": [],
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_standard": "Yes",
|
"is_standard": "Yes",
|
||||||
"modified": "2019-09-24 13:46:16.473711",
|
"modified": "2021-09-20 12:05:50.387572",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "TDS Payable Monthly",
|
"name": "TDS Payable Monthly",
|
||||||
|
@ -8,19 +8,12 @@ from frappe import _
|
|||||||
|
|
||||||
|
|
||||||
def execute(filters=None):
|
def execute(filters=None):
|
||||||
filters["invoices"] = frappe.cache().hget("invoices", frappe.session.user)
|
|
||||||
validate_filters(filters)
|
validate_filters(filters)
|
||||||
set_filters(filters)
|
tds_docs, tds_accounts, tax_category_map = get_tds_docs(filters)
|
||||||
|
|
||||||
# TDS payment entries
|
|
||||||
payment_entries = get_payment_entires(filters)
|
|
||||||
|
|
||||||
columns = get_columns(filters)
|
columns = get_columns(filters)
|
||||||
if not filters.get("invoices"):
|
|
||||||
return columns, []
|
|
||||||
|
|
||||||
res = get_result(filters, payment_entries)
|
|
||||||
|
|
||||||
|
res = get_result(filters, tds_docs, tds_accounts, tax_category_map)
|
||||||
return columns, res
|
return columns, res
|
||||||
|
|
||||||
def validate_filters(filters):
|
def validate_filters(filters):
|
||||||
@ -28,109 +21,59 @@ def validate_filters(filters):
|
|||||||
if filters.from_date > filters.to_date:
|
if filters.from_date > filters.to_date:
|
||||||
frappe.throw(_("From Date must be before To Date"))
|
frappe.throw(_("From Date must be before To Date"))
|
||||||
|
|
||||||
def set_filters(filters):
|
def get_result(filters, tds_docs, tds_accounts, tax_category_map):
|
||||||
invoices = []
|
supplier_map = get_supplier_pan_map()
|
||||||
|
tax_rate_map = get_tax_rate_map(filters)
|
||||||
if not filters.get("invoices"):
|
gle_map = get_gle_map(filters, tds_docs)
|
||||||
filters["invoices"] = get_tds_invoices_and_orders()
|
|
||||||
|
|
||||||
if filters.supplier and filters.purchase_invoice:
|
|
||||||
for d in filters["invoices"]:
|
|
||||||
if d.name == filters.purchase_invoice and d.supplier == filters.supplier:
|
|
||||||
invoices.append(d)
|
|
||||||
elif filters.supplier and not filters.purchase_invoice:
|
|
||||||
for d in filters["invoices"]:
|
|
||||||
if d.supplier == filters.supplier:
|
|
||||||
invoices.append(d)
|
|
||||||
elif filters.purchase_invoice and not filters.supplier:
|
|
||||||
for d in filters["invoices"]:
|
|
||||||
if d.name == filters.purchase_invoice:
|
|
||||||
invoices.append(d)
|
|
||||||
elif filters.supplier and filters.purchase_order:
|
|
||||||
for d in filters.get("invoices"):
|
|
||||||
if d.name == filters.purchase_order and d.supplier == filters.supplier:
|
|
||||||
invoices.append(d)
|
|
||||||
elif filters.supplier and not filters.purchase_order:
|
|
||||||
for d in filters.get("invoices"):
|
|
||||||
if d.supplier == filters.supplier:
|
|
||||||
invoices.append(d)
|
|
||||||
elif filters.purchase_order and not filters.supplier:
|
|
||||||
for d in filters.get("invoices"):
|
|
||||||
if d.name == filters.purchase_order:
|
|
||||||
invoices.append(d)
|
|
||||||
|
|
||||||
filters["invoices"] = invoices if invoices else filters["invoices"]
|
|
||||||
filters.naming_series = frappe.db.get_single_value('Buying Settings', 'supp_master_name')
|
|
||||||
|
|
||||||
#print(filters.get('invoices'))
|
|
||||||
|
|
||||||
def get_result(filters, payment_entries):
|
|
||||||
supplier_map, tds_docs = get_supplier_map(filters, payment_entries)
|
|
||||||
documents = [d.get('name') for d in filters.get('invoices')] + [d.get('name') for d in payment_entries]
|
|
||||||
|
|
||||||
gle_map = get_gle_map(filters, documents)
|
|
||||||
|
|
||||||
out = []
|
out = []
|
||||||
for d in gle_map:
|
for name, details in gle_map.items():
|
||||||
tds_deducted, total_amount_credited = 0, 0
|
tds_deducted, total_amount_credited = 0, 0
|
||||||
supplier = supplier_map[d]
|
tax_withholding_category = tax_category_map.get(name)
|
||||||
|
rate = tax_rate_map.get(tax_withholding_category)
|
||||||
|
|
||||||
tds_doc = tds_docs[supplier.tax_withholding_category]
|
for entry in details:
|
||||||
account_list = [i.account for i in tds_doc.accounts if i.company == filters.company]
|
supplier = entry.party or entry.against
|
||||||
|
posting_date = entry.posting_date
|
||||||
|
voucher_type = entry.voucher_type
|
||||||
|
|
||||||
if account_list:
|
if entry.account in tds_accounts:
|
||||||
account = account_list[0]
|
tds_deducted += (entry.credit - entry.debit)
|
||||||
|
|
||||||
for k in gle_map[d]:
|
total_amount_credited += (entry.credit - entry.debit)
|
||||||
if k.party == supplier_map[d] and k.credit > 0:
|
|
||||||
total_amount_credited += (k.credit - k.debit)
|
|
||||||
elif account_list and k.account == account and (k.credit - k.debit) > 0:
|
|
||||||
tds_deducted = (k.credit - k.debit)
|
|
||||||
total_amount_credited += (k.credit - k.debit)
|
|
||||||
voucher_type = k.voucher_type
|
|
||||||
|
|
||||||
rate = [i.tax_withholding_rate for i in tds_doc.rates
|
if rate and tds_deducted:
|
||||||
if i.fiscal_year == gle_map[d][0].fiscal_year]
|
row = {
|
||||||
|
'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier).pan,
|
||||||
if rate and len(rate) > 0 and tds_deducted:
|
'supplier': supplier_map.get(supplier).name
|
||||||
rate = rate[0]
|
}
|
||||||
|
|
||||||
row = [supplier.pan, supplier.name]
|
|
||||||
|
|
||||||
if filters.naming_series == 'Naming Series':
|
if filters.naming_series == 'Naming Series':
|
||||||
row.append(supplier.supplier_name)
|
row.update({'supplier_name': supplier_map.get(supplier).supplier_name})
|
||||||
|
|
||||||
|
row.update({
|
||||||
|
'section_code': tax_withholding_category,
|
||||||
|
'entity_type': supplier_map.get(supplier).supplier_type,
|
||||||
|
'tds_rate': rate,
|
||||||
|
'total_amount_credited': total_amount_credited,
|
||||||
|
'tds_deducted': tds_deducted,
|
||||||
|
'transaction_date': posting_date,
|
||||||
|
'transaction_type': voucher_type,
|
||||||
|
'ref_no': name
|
||||||
|
})
|
||||||
|
|
||||||
row.extend([tds_doc.name, supplier.supplier_type, rate, total_amount_credited,
|
|
||||||
tds_deducted, gle_map[d][0].posting_date, voucher_type, d])
|
|
||||||
out.append(row)
|
out.append(row)
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def get_supplier_map(filters, payment_entries):
|
def get_supplier_pan_map():
|
||||||
# create a supplier_map of the form {"purchase_invoice": {supplier_name, pan, tds_name}}
|
supplier_map = frappe._dict()
|
||||||
# pre-fetch all distinct applicable tds docs
|
suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name'])
|
||||||
supplier_map, tds_docs = {}, {}
|
|
||||||
pan = "pan" if frappe.db.has_column("Supplier", "pan") else "tax_id"
|
|
||||||
supplier_list = [d.supplier for d in filters["invoices"]]
|
|
||||||
|
|
||||||
supplier_detail = frappe.db.get_all('Supplier',
|
for d in suppliers:
|
||||||
{"name": ["in", supplier_list]},
|
supplier_map[d.name] = d
|
||||||
["tax_withholding_category", "name", pan+" as pan", "supplier_type", "supplier_name"])
|
|
||||||
|
|
||||||
for d in filters["invoices"]:
|
return supplier_map
|
||||||
supplier_map[d.get("name")] = [k for k in supplier_detail
|
|
||||||
if k.name == d.get("supplier")][0]
|
|
||||||
|
|
||||||
for d in payment_entries:
|
|
||||||
supplier_map[d.get("name")] = [k for k in supplier_detail
|
|
||||||
if k.name == d.get("supplier")][0]
|
|
||||||
|
|
||||||
for d in supplier_detail:
|
|
||||||
if d.get("tax_withholding_category") not in tds_docs:
|
|
||||||
tds_docs[d.get("tax_withholding_category")] = \
|
|
||||||
frappe.get_doc("Tax Withholding Category", d.get("tax_withholding_category"))
|
|
||||||
|
|
||||||
return supplier_map, tds_docs
|
|
||||||
|
|
||||||
def get_gle_map(filters, documents):
|
def get_gle_map(filters, documents):
|
||||||
# create gle_map of the form
|
# create gle_map of the form
|
||||||
@ -140,10 +83,9 @@ def get_gle_map(filters, documents):
|
|||||||
gle = frappe.db.get_all('GL Entry',
|
gle = frappe.db.get_all('GL Entry',
|
||||||
{
|
{
|
||||||
"voucher_no": ["in", documents],
|
"voucher_no": ["in", documents],
|
||||||
'is_cancelled': 0,
|
"credit": (">", 0)
|
||||||
'posting_date': ("between", [filters.get('from_date'), filters.get('to_date')]),
|
|
||||||
},
|
},
|
||||||
["fiscal_year", "credit", "debit", "account", "voucher_no", "posting_date", "voucher_type"],
|
["credit", "debit", "account", "voucher_no", "posting_date", "voucher_type", "against", "party"],
|
||||||
)
|
)
|
||||||
|
|
||||||
for d in gle:
|
for d in gle:
|
||||||
@ -233,39 +175,57 @@ def get_columns(filters):
|
|||||||
|
|
||||||
return columns
|
return columns
|
||||||
|
|
||||||
def get_payment_entires(filters):
|
def get_tds_docs(filters):
|
||||||
filter_dict = {
|
tds_documents = []
|
||||||
'posting_date': ("between", [filters.get('from_date'), filters.get('to_date')]),
|
purchase_invoices = []
|
||||||
'party_type': 'Supplier',
|
payment_entries = []
|
||||||
'apply_tax_withholding_amount': 1
|
journal_entries = []
|
||||||
|
tax_category_map = {}
|
||||||
|
|
||||||
|
tds_accounts = frappe.get_all("Tax Withholding Account", {'company': filters.get('company')},
|
||||||
|
pluck="account")
|
||||||
|
|
||||||
|
query_filters = {
|
||||||
|
"credit": ('>', 0),
|
||||||
|
"account": ("in", tds_accounts),
|
||||||
|
"posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]),
|
||||||
|
"is_cancelled": 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if filters.get('purchase_invoice') or filters.get('purchase_order'):
|
if filters.get('supplier'):
|
||||||
parent = frappe.db.get_all('Payment Entry Reference',
|
query_filters.update({'against': filters.get('supplier')})
|
||||||
{'reference_name': ('in', [d.get('name') for d in filters.get('invoices')])}, ['parent'])
|
|
||||||
|
|
||||||
filter_dict.update({'name': ('in', [d.get('parent') for d in parent])})
|
tds_docs = frappe.get_all("GL Entry", query_filters, ["voucher_no", "voucher_type", "against", "party"])
|
||||||
|
|
||||||
payment_entries = frappe.get_all('Payment Entry', fields=['name', 'party_name as supplier'],
|
for d in tds_docs:
|
||||||
filters=filter_dict)
|
if d.voucher_type == "Purchase Invoice":
|
||||||
|
purchase_invoices.append(d.voucher_no)
|
||||||
|
elif d.voucher_type == "Payment Entry":
|
||||||
|
payment_entries.append(d.voucher_no)
|
||||||
|
elif d.voucher_type == "Journal Entry":
|
||||||
|
journal_entries.append(d.voucher_no)
|
||||||
|
|
||||||
return payment_entries
|
tds_documents.append(d.voucher_no)
|
||||||
|
|
||||||
@frappe.whitelist()
|
if purchase_invoices:
|
||||||
def get_tds_invoices_and_orders():
|
get_tax_category_map(purchase_invoices, 'Purchase Invoice', tax_category_map)
|
||||||
# fetch tds applicable supplier and fetch invoices for these suppliers
|
|
||||||
suppliers = [d.name for d in frappe.db.get_list("Supplier",
|
|
||||||
{"tax_withholding_category": ["!=", ""]}, ["name"])]
|
|
||||||
|
|
||||||
invoices = frappe.db.get_list("Purchase Invoice",
|
if payment_entries:
|
||||||
{"supplier": ["in", suppliers]}, ["name", "supplier"])
|
get_tax_category_map(payment_entries, 'Payment Entry', tax_category_map)
|
||||||
|
|
||||||
orders = frappe.db.get_list("Purchase Order",
|
if journal_entries:
|
||||||
{"supplier": ["in", suppliers]}, ["name", "supplier"])
|
get_tax_category_map(journal_entries, 'Journal Entry', tax_category_map)
|
||||||
|
|
||||||
invoices = invoices + orders
|
return tds_documents, tds_accounts, tax_category_map
|
||||||
invoices = [d for d in invoices if d.supplier]
|
|
||||||
|
|
||||||
frappe.cache().hset("invoices", frappe.session.user, invoices)
|
def get_tax_category_map(vouchers, doctype, tax_category_map):
|
||||||
|
tax_category_map.update(frappe._dict(frappe.get_all(doctype,
|
||||||
|
filters = {'name': ('in', vouchers)}, fields=['name', 'tax_withholding_category'], as_list=1)))
|
||||||
|
|
||||||
return invoices
|
def get_tax_rate_map(filters):
|
||||||
|
rate_map = frappe.get_all('Tax Withholding Rate', filters={
|
||||||
|
'from_date': ('<=', filters.get('from_date')),
|
||||||
|
'to_date': ('>=', filters.get('to_date'))
|
||||||
|
}, fields=['parent', 'tax_withholding_rate'], as_list=1)
|
||||||
|
|
||||||
|
return frappe._dict(rate_map)
|
@ -4,9 +4,10 @@
|
|||||||
frappe.query_reports["Unpaid Expense Claim"] = {
|
frappe.query_reports["Unpaid Expense Claim"] = {
|
||||||
"filters": [
|
"filters": [
|
||||||
{
|
{
|
||||||
"fieldname":"employee",
|
"fieldname": "employee",
|
||||||
"label": __("Employee"),
|
"label": __("Employee"),
|
||||||
"fieldtype": "Link"
|
"fieldtype": "Link",
|
||||||
|
"options": "Employee"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -100,15 +100,15 @@ def convert_to_presentation_currency(gl_entries, currency_info, company):
|
|||||||
if entry.get('credit'):
|
if entry.get('credit'):
|
||||||
entry['credit'] = credit_in_account_currency
|
entry['credit'] = credit_in_account_currency
|
||||||
else:
|
else:
|
||||||
value = debit or credit
|
|
||||||
date = currency_info['report_date']
|
date = currency_info['report_date']
|
||||||
converted_value = convert(value, presentation_currency, company_currency, date)
|
converted_debit_value = convert(debit, presentation_currency, company_currency, date)
|
||||||
|
converted_credit_value = convert(credit, presentation_currency, company_currency, date)
|
||||||
|
|
||||||
if entry.get('debit'):
|
if entry.get('debit'):
|
||||||
entry['debit'] = converted_value
|
entry['debit'] = converted_debit_value
|
||||||
|
|
||||||
if entry.get('credit'):
|
if entry.get('credit'):
|
||||||
entry['credit'] = converted_value
|
entry['credit'] = converted_credit_value
|
||||||
|
|
||||||
converted_gl_list.append(entry)
|
converted_gl_list.append(entry)
|
||||||
|
|
||||||
|
@ -5,21 +5,48 @@ import unittest
|
|||||||
from frappe.test_runner import make_test_objects
|
from frappe.test_runner import make_test_objects
|
||||||
|
|
||||||
from erpnext.accounts.party import get_party_shipping_address
|
from erpnext.accounts.party import get_party_shipping_address
|
||||||
|
from erpnext.accounts.utils import get_future_stock_vouchers, get_voucherwise_gl_entries
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
|
|
||||||
|
|
||||||
class TestUtils(unittest.TestCase):
|
class TestUtils(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super(TestUtils, cls).setUpClass()
|
super(TestUtils, cls).setUpClass()
|
||||||
make_test_objects('Address', ADDRESS_RECORDS)
|
make_test_objects("Address", ADDRESS_RECORDS)
|
||||||
|
|
||||||
def test_get_party_shipping_address(self):
|
def test_get_party_shipping_address(self):
|
||||||
address = get_party_shipping_address('Customer', '_Test Customer 1')
|
address = get_party_shipping_address("Customer", "_Test Customer 1")
|
||||||
self.assertEqual(address, '_Test Billing Address 2 Title-Billing')
|
self.assertEqual(address, "_Test Billing Address 2 Title-Billing")
|
||||||
|
|
||||||
def test_get_party_shipping_address2(self):
|
def test_get_party_shipping_address2(self):
|
||||||
address = get_party_shipping_address('Customer', '_Test Customer 2')
|
address = get_party_shipping_address("Customer", "_Test Customer 2")
|
||||||
self.assertEqual(address, '_Test Shipping Address 2 Title-Shipping')
|
self.assertEqual(address, "_Test Shipping Address 2 Title-Shipping")
|
||||||
|
|
||||||
|
def test_get_voucher_wise_gl_entry(self):
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
item_code="_Test Item",
|
||||||
|
posting_date="2021-02-01",
|
||||||
|
rate=100,
|
||||||
|
qty=1,
|
||||||
|
warehouse="Stores - TCP1",
|
||||||
|
company="_Test Company with perpetual inventory",
|
||||||
|
)
|
||||||
|
|
||||||
|
future_vouchers = get_future_stock_vouchers("2021-01-01", "00:00:00", for_items=["_Test Item"])
|
||||||
|
|
||||||
|
voucher_type_and_no = ("Purchase Receipt", pr.name)
|
||||||
|
self.assertTrue(
|
||||||
|
voucher_type_and_no in future_vouchers,
|
||||||
|
msg="get_future_stock_vouchers not returning correct value",
|
||||||
|
)
|
||||||
|
|
||||||
|
posting_date = "2021-01-01"
|
||||||
|
gl_entries = get_voucherwise_gl_entries(future_vouchers, posting_date)
|
||||||
|
self.assertTrue(
|
||||||
|
voucher_type_and_no in gl_entries, msg="get_voucherwise_gl_entries not returning expected GLes",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
ADDRESS_RECORDS = [
|
ADDRESS_RECORDS = [
|
||||||
@ -31,12 +58,8 @@ ADDRESS_RECORDS = [
|
|||||||
"city": "Lagos",
|
"city": "Lagos",
|
||||||
"country": "Nigeria",
|
"country": "Nigeria",
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{"link_doctype": "Customer", "link_name": "_Test Customer 2", "doctype": "Dynamic Link"}
|
||||||
"link_doctype": "Customer",
|
],
|
||||||
"link_name": "_Test Customer 2",
|
|
||||||
"doctype": "Dynamic Link"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"doctype": "Address",
|
"doctype": "Address",
|
||||||
@ -46,12 +69,8 @@ ADDRESS_RECORDS = [
|
|||||||
"city": "Lagos",
|
"city": "Lagos",
|
||||||
"country": "Nigeria",
|
"country": "Nigeria",
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{"link_doctype": "Customer", "link_name": "_Test Customer 2", "doctype": "Dynamic Link"}
|
||||||
"link_doctype": "Customer",
|
],
|
||||||
"link_name": "_Test Customer 2",
|
|
||||||
"doctype": "Dynamic Link"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"doctype": "Address",
|
"doctype": "Address",
|
||||||
@ -62,12 +81,8 @@ ADDRESS_RECORDS = [
|
|||||||
"country": "Nigeria",
|
"country": "Nigeria",
|
||||||
"is_shipping_address": "1",
|
"is_shipping_address": "1",
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{"link_doctype": "Customer", "link_name": "_Test Customer 2", "doctype": "Dynamic Link"}
|
||||||
"link_doctype": "Customer",
|
],
|
||||||
"link_name": "_Test Customer 2",
|
|
||||||
"doctype": "Dynamic Link"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"doctype": "Address",
|
"doctype": "Address",
|
||||||
@ -78,11 +93,7 @@ ADDRESS_RECORDS = [
|
|||||||
"country": "Nigeria",
|
"country": "Nigeria",
|
||||||
"is_shipping_address": "1",
|
"is_shipping_address": "1",
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{"link_doctype": "Customer", "link_name": "_Test Customer 1", "doctype": "Dynamic Link"}
|
||||||
"link_doctype": "Customer",
|
],
|
||||||
"link_name": "_Test Customer 1",
|
},
|
||||||
"doctype": "Dynamic Link"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
|
@ -963,6 +963,9 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
|
|||||||
|
|
||||||
Only fetches GLE fields required for comparing with new GLE.
|
Only fetches GLE fields required for comparing with new GLE.
|
||||||
Check compare_existing_and_expected_gle function below.
|
Check compare_existing_and_expected_gle function below.
|
||||||
|
|
||||||
|
returns:
|
||||||
|
Dict[Tuple[voucher_type, voucher_no], List[GL Entries]]
|
||||||
"""
|
"""
|
||||||
gl_entries = {}
|
gl_entries = {}
|
||||||
if not future_stock_vouchers:
|
if not future_stock_vouchers:
|
||||||
@ -971,7 +974,7 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
|
|||||||
voucher_nos = [d[1] for d in future_stock_vouchers]
|
voucher_nos = [d[1] for d in future_stock_vouchers]
|
||||||
|
|
||||||
gles = frappe.db.sql("""
|
gles = frappe.db.sql("""
|
||||||
select name, account, credit, debit, cost_center, project
|
select name, account, credit, debit, cost_center, project, voucher_type, voucher_no
|
||||||
from `tabGL Entry`
|
from `tabGL Entry`
|
||||||
where
|
where
|
||||||
posting_date >= %s and voucher_no in (%s)""" %
|
posting_date >= %s and voucher_no in (%s)""" %
|
||||||
|
@ -394,10 +394,6 @@ class Asset(AccountsController):
|
|||||||
if cint(self.number_of_depreciations_booked) > cint(row.total_number_of_depreciations):
|
if cint(self.number_of_depreciations_booked) > cint(row.total_number_of_depreciations):
|
||||||
frappe.throw(_("Number of Depreciations Booked cannot be greater than Total Number of Depreciations"))
|
frappe.throw(_("Number of Depreciations Booked cannot be greater than Total Number of Depreciations"))
|
||||||
|
|
||||||
if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(nowdate()):
|
|
||||||
frappe.msgprint(_("Depreciation Row {0}: Depreciation Start Date is entered as past date")
|
|
||||||
.format(row.idx), title=_('Warning'), indicator='red')
|
|
||||||
|
|
||||||
if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date):
|
if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date):
|
||||||
frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date")
|
frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date")
|
||||||
.format(row.idx))
|
.format(row.idx))
|
||||||
|
@ -645,12 +645,18 @@ class TestAsset(unittest.TestCase):
|
|||||||
pr = make_purchase_receipt(item_code="Macbook Pro",
|
pr = make_purchase_receipt(item_code="Macbook Pro",
|
||||||
qty=1, rate=8000.0, location="Test Location")
|
qty=1, rate=8000.0, location="Test Location")
|
||||||
|
|
||||||
|
finance_book = frappe.new_doc('Finance Book')
|
||||||
|
finance_book.finance_book_name = 'Income Tax'
|
||||||
|
finance_book.for_income_tax = 1
|
||||||
|
finance_book.insert(ignore_if_duplicate=1)
|
||||||
|
|
||||||
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
|
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
|
||||||
asset = frappe.get_doc('Asset', asset_name)
|
asset = frappe.get_doc('Asset', asset_name)
|
||||||
asset.calculate_depreciation = 1
|
asset.calculate_depreciation = 1
|
||||||
asset.available_for_use_date = '2030-07-12'
|
asset.available_for_use_date = '2030-07-12'
|
||||||
asset.purchase_date = '2030-01-01'
|
asset.purchase_date = '2030-01-01'
|
||||||
asset.append("finance_books", {
|
asset.append("finance_books", {
|
||||||
|
"finance_book": finance_book.name,
|
||||||
"expected_value_after_useful_life": 1000,
|
"expected_value_after_useful_life": 1000,
|
||||||
"depreciation_method": "Written Down Value",
|
"depreciation_method": "Written Down Value",
|
||||||
"total_number_of_depreciations": 3,
|
"total_number_of_depreciations": 3,
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
"fieldname": "supp_master_name",
|
"fieldname": "supp_master_name",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Supplier Naming By",
|
"label": "Supplier Naming By",
|
||||||
"options": "Supplier Name\nNaming Series"
|
"options": "Supplier Name\nNaming Series\nAuto Name"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "supplier_group",
|
"fieldname": "supplier_group",
|
||||||
@ -123,7 +123,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-06-24 10:38:28.934525",
|
"modified": "2021-09-08 19:26:23.548837",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Buying Settings",
|
"name": "Buying Settings",
|
||||||
|
@ -425,7 +425,10 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
|||||||
status: ["!=", "Stopped"],
|
status: ["!=", "Stopped"],
|
||||||
per_ordered: ["<", 100],
|
per_ordered: ["<", 100],
|
||||||
company: me.frm.doc.company
|
company: me.frm.doc.company
|
||||||
}
|
},
|
||||||
|
allow_child_item_selection: true,
|
||||||
|
child_fielname: "items",
|
||||||
|
child_columns: ["item_code", "qty"]
|
||||||
})
|
})
|
||||||
}, __("Get Items From"));
|
}, __("Get Items From"));
|
||||||
|
|
||||||
|
@ -1121,6 +1121,7 @@
|
|||||||
"fetch_from": "supplier.represents_company",
|
"fetch_from": "supplier.represents_company",
|
||||||
"fieldname": "represents_company",
|
"fieldname": "represents_company",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
|
"ignore_user_permissions": 1,
|
||||||
"label": "Represents Company",
|
"label": "Represents Company",
|
||||||
"options": "Company",
|
"options": "Company",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
@ -1143,7 +1144,7 @@
|
|||||||
"idx": 105,
|
"idx": 105,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-08-30 20:03:14.008804",
|
"modified": "2021-09-28 13:10:47.955401",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Purchase Order",
|
"name": "Purchase Order",
|
||||||
|
@ -394,12 +394,10 @@ def get_item_from_material_requests_based_on_supplier(source_name, target_doc =
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_supplier_tag():
|
def get_supplier_tag():
|
||||||
if not frappe.cache().hget("Supplier", "Tags"):
|
filters = {"document_type": "Supplier"}
|
||||||
filters = {"document_type": "Supplier"}
|
tags = list(set(tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag))
|
||||||
tags = list(set(tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag))
|
|
||||||
frappe.cache().hset("Supplier", "Tags", tags)
|
|
||||||
|
|
||||||
return frappe.cache().hget("Supplier", "Tags")
|
return tags
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@frappe.validate_and_sanitize_search_inputs
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
|
@ -433,12 +433,12 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{
|
||||||
"group": "Item Group",
|
"group": "Allowed Items",
|
||||||
"link_doctype": "Supplier Item Group",
|
"link_doctype": "Party Specific Item",
|
||||||
"link_fieldname": "supplier"
|
"link_fieldname": "party"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2021-08-27 18:02:44.314077",
|
"modified": "2021-09-06 17:37:56.522233",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Supplier",
|
"name": "Supplier",
|
||||||
|
@ -10,7 +10,7 @@ from frappe.contacts.address_and_contact import (
|
|||||||
delete_contact_and_address,
|
delete_contact_and_address,
|
||||||
load_address_and_contact,
|
load_address_and_contact,
|
||||||
)
|
)
|
||||||
from frappe.model.naming import set_name_by_naming_series
|
from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options
|
||||||
|
|
||||||
from erpnext.accounts.party import get_dashboard_info, validate_party_accounts
|
from erpnext.accounts.party import get_dashboard_info, validate_party_accounts
|
||||||
from erpnext.utilities.transaction_base import TransactionBase
|
from erpnext.utilities.transaction_base import TransactionBase
|
||||||
@ -40,8 +40,10 @@ class Supplier(TransactionBase):
|
|||||||
supp_master_name = frappe.defaults.get_global_default('supp_master_name')
|
supp_master_name = frappe.defaults.get_global_default('supp_master_name')
|
||||||
if supp_master_name == 'Supplier Name':
|
if supp_master_name == 'Supplier Name':
|
||||||
self.name = self.supplier_name
|
self.name = self.supplier_name
|
||||||
else:
|
elif supp_master_name == 'Naming Series':
|
||||||
set_name_by_naming_series(self)
|
set_name_by_naming_series(self)
|
||||||
|
else:
|
||||||
|
self.name = set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self)
|
||||||
|
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
if not self.naming_series:
|
if not self.naming_series:
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
# -*- 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
|
|
||||||
|
|
||||||
|
|
||||||
class SupplierItemGroup(Document):
|
|
||||||
def validate(self):
|
|
||||||
exists = frappe.db.exists({
|
|
||||||
'doctype': 'Supplier Item Group',
|
|
||||||
'supplier': self.supplier,
|
|
||||||
'item_group': self.item_group
|
|
||||||
})
|
|
||||||
if exists:
|
|
||||||
frappe.throw(_("Item Group has already been linked to this supplier."))
|
|
@ -30,7 +30,14 @@ frappe.query_reports["Purchase Order Analysis"] = {
|
|||||||
"default": frappe.datetime.get_today()
|
"default": frappe.datetime.get_today()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "purchase_order",
|
"fieldname":"project",
|
||||||
|
"label": __("Project"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"width": "80",
|
||||||
|
"options": "Project"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "name",
|
||||||
"label": __("Purchase Order"),
|
"label": __("Purchase Order"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"width": "80",
|
"width": "80",
|
||||||
|
@ -41,14 +41,12 @@ def get_conditions(filters):
|
|||||||
if filters.get("from_date") and filters.get("to_date"):
|
if filters.get("from_date") and filters.get("to_date"):
|
||||||
conditions += " and po.transaction_date between %(from_date)s and %(to_date)s"
|
conditions += " and po.transaction_date between %(from_date)s and %(to_date)s"
|
||||||
|
|
||||||
if filters.get("company"):
|
for field in ['company', 'name', 'status']:
|
||||||
conditions += " and po.company = %(company)s"
|
if filters.get(field):
|
||||||
|
conditions += f" and po.{field} = %({field})s"
|
||||||
|
|
||||||
if filters.get("purchase_order"):
|
if filters.get('project'):
|
||||||
conditions += " and po.name = %(purchase_order)s"
|
conditions += " and poi.project = %(project)s"
|
||||||
|
|
||||||
if filters.get("status"):
|
|
||||||
conditions += " and po.status in %(status)s"
|
|
||||||
|
|
||||||
return conditions
|
return conditions
|
||||||
|
|
||||||
@ -57,6 +55,7 @@ def get_data(conditions, filters):
|
|||||||
SELECT
|
SELECT
|
||||||
po.transaction_date as date,
|
po.transaction_date as date,
|
||||||
poi.schedule_date as required_date,
|
poi.schedule_date as required_date,
|
||||||
|
poi.project,
|
||||||
po.name as purchase_order,
|
po.name as purchase_order,
|
||||||
po.status, po.supplier, poi.item_code,
|
po.status, po.supplier, poi.item_code,
|
||||||
poi.qty, poi.received_qty,
|
poi.qty, poi.received_qty,
|
||||||
@ -175,6 +174,12 @@ def get_columns(filters):
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Supplier",
|
"options": "Supplier",
|
||||||
"width": 130
|
"width": 130
|
||||||
|
},{
|
||||||
|
"label": _("Project"),
|
||||||
|
"fieldname": "project",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Project",
|
||||||
|
"width": 130
|
||||||
}]
|
}]
|
||||||
|
|
||||||
if not filters.get("group_by_po"):
|
if not filters.get("group_by_po"):
|
||||||
|
@ -685,13 +685,17 @@ class AccountsController(TransactionBase):
|
|||||||
.format(d.reference_name, d.against_order))
|
.format(d.reference_name, d.against_order))
|
||||||
|
|
||||||
def set_advance_gain_or_loss(self):
|
def set_advance_gain_or_loss(self):
|
||||||
if not self.get("advances"):
|
if self.get('conversion_rate') == 1 or not self.get("advances"):
|
||||||
|
return
|
||||||
|
|
||||||
|
is_purchase_invoice = self.doctype == 'Purchase Invoice'
|
||||||
|
party_account = self.credit_to if is_purchase_invoice else self.debit_to
|
||||||
|
if get_account_currency(party_account) != self.currency:
|
||||||
return
|
return
|
||||||
|
|
||||||
for d in self.get("advances"):
|
for d in self.get("advances"):
|
||||||
advance_exchange_rate = d.ref_exchange_rate
|
advance_exchange_rate = d.ref_exchange_rate
|
||||||
if (d.allocated_amount and self.conversion_rate != 1
|
if (d.allocated_amount and self.conversion_rate != advance_exchange_rate):
|
||||||
and self.conversion_rate != advance_exchange_rate):
|
|
||||||
|
|
||||||
base_allocated_amount_in_ref_rate = advance_exchange_rate * d.allocated_amount
|
base_allocated_amount_in_ref_rate = advance_exchange_rate * d.allocated_amount
|
||||||
base_allocated_amount_in_inv_rate = self.conversion_rate * d.allocated_amount
|
base_allocated_amount_in_inv_rate = self.conversion_rate * d.allocated_amount
|
||||||
@ -710,7 +714,7 @@ class AccountsController(TransactionBase):
|
|||||||
|
|
||||||
gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account')
|
gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account')
|
||||||
if not gain_loss_account:
|
if not gain_loss_account:
|
||||||
frappe.throw(_("Please set Default Exchange Gain/Loss Account in Company {}")
|
frappe.throw(_("Please set default Exchange Gain/Loss Account in Company {}")
|
||||||
.format(self.get('company')))
|
.format(self.get('company')))
|
||||||
account_currency = get_account_currency(gain_loss_account)
|
account_currency = get_account_currency(gain_loss_account)
|
||||||
if account_currency != self.company_currency:
|
if account_currency != self.company_currency:
|
||||||
@ -729,7 +733,7 @@ class AccountsController(TransactionBase):
|
|||||||
"against": party,
|
"against": party,
|
||||||
dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss),
|
dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss),
|
||||||
dr_or_cr: abs(d.exchange_gain_loss),
|
dr_or_cr: abs(d.exchange_gain_loss),
|
||||||
"cost_center": self.cost_center,
|
"cost_center": self.cost_center or erpnext.get_default_cost_center(self.company),
|
||||||
"project": self.project
|
"project": self.project
|
||||||
}, item=d)
|
}, item=d)
|
||||||
)
|
)
|
||||||
@ -980,42 +984,55 @@ class AccountsController(TransactionBase):
|
|||||||
item_allowance = {}
|
item_allowance = {}
|
||||||
global_qty_allowance, global_amount_allowance = None, None
|
global_qty_allowance, global_amount_allowance = None, None
|
||||||
|
|
||||||
|
role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill')
|
||||||
|
user_roles = frappe.get_roles()
|
||||||
|
|
||||||
|
total_overbilled_amt = 0.0
|
||||||
|
|
||||||
for item in self.get("items"):
|
for item in self.get("items"):
|
||||||
if item.get(item_ref_dn):
|
if not item.get(item_ref_dn):
|
||||||
ref_amt = flt(frappe.db.get_value(ref_dt + " Item",
|
continue
|
||||||
item.get(item_ref_dn), based_on), self.precision(based_on, item))
|
|
||||||
if not ref_amt:
|
|
||||||
frappe.msgprint(
|
|
||||||
_("Warning: System will not check overbilling since amount for Item {0} in {1} is zero")
|
|
||||||
.format(item.item_code, ref_dt))
|
|
||||||
else:
|
|
||||||
already_billed = frappe.db.sql("""
|
|
||||||
select sum(%s)
|
|
||||||
from `tab%s`
|
|
||||||
where %s=%s and docstatus=1 and parent != %s
|
|
||||||
""" % (based_on, self.doctype + " Item", item_ref_dn, '%s', '%s'),
|
|
||||||
(item.get(item_ref_dn), self.name))[0][0]
|
|
||||||
|
|
||||||
total_billed_amt = flt(flt(already_billed) + flt(item.get(based_on)),
|
ref_amt = flt(frappe.db.get_value(ref_dt + " Item",
|
||||||
self.precision(based_on, item))
|
item.get(item_ref_dn), based_on), self.precision(based_on, item))
|
||||||
|
if not ref_amt:
|
||||||
|
frappe.msgprint(
|
||||||
|
_("System will not check overbilling since amount for Item {0} in {1} is zero")
|
||||||
|
.format(item.item_code, ref_dt), title=_("Warning"), indicator="orange")
|
||||||
|
continue
|
||||||
|
|
||||||
allowance, item_allowance, global_qty_allowance, global_amount_allowance = \
|
already_billed = frappe.db.sql("""
|
||||||
get_allowance_for(item.item_code, item_allowance, global_qty_allowance, global_amount_allowance, "amount")
|
select sum(%s)
|
||||||
|
from `tab%s`
|
||||||
|
where %s=%s and docstatus=1 and parent != %s
|
||||||
|
""" % (based_on, self.doctype + " Item", item_ref_dn, '%s', '%s'),
|
||||||
|
(item.get(item_ref_dn), self.name))[0][0]
|
||||||
|
|
||||||
max_allowed_amt = flt(ref_amt * (100 + allowance) / 100)
|
total_billed_amt = flt(flt(already_billed) + flt(item.get(based_on)),
|
||||||
|
self.precision(based_on, item))
|
||||||
|
|
||||||
if total_billed_amt < 0 and max_allowed_amt < 0:
|
allowance, item_allowance, global_qty_allowance, global_amount_allowance = \
|
||||||
# while making debit note against purchase return entry(purchase receipt) getting overbill error
|
get_allowance_for(item.item_code, item_allowance, global_qty_allowance, global_amount_allowance, "amount")
|
||||||
total_billed_amt = abs(total_billed_amt)
|
|
||||||
max_allowed_amt = abs(max_allowed_amt)
|
|
||||||
|
|
||||||
role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill')
|
max_allowed_amt = flt(ref_amt * (100 + allowance) / 100)
|
||||||
|
|
||||||
if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles():
|
if total_billed_amt < 0 and max_allowed_amt < 0:
|
||||||
if self.doctype != "Purchase Invoice":
|
# while making debit note against purchase return entry(purchase receipt) getting overbill error
|
||||||
self.throw_overbill_exception(item, max_allowed_amt)
|
total_billed_amt = abs(total_billed_amt)
|
||||||
elif not cint(frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")):
|
max_allowed_amt = abs(max_allowed_amt)
|
||||||
self.throw_overbill_exception(item, max_allowed_amt)
|
|
||||||
|
overbill_amt = total_billed_amt - max_allowed_amt
|
||||||
|
total_overbilled_amt += overbill_amt
|
||||||
|
|
||||||
|
if overbill_amt > 0.01 and role_allowed_to_over_bill not in user_roles:
|
||||||
|
if self.doctype != "Purchase Invoice":
|
||||||
|
self.throw_overbill_exception(item, max_allowed_amt)
|
||||||
|
elif not cint(frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")):
|
||||||
|
self.throw_overbill_exception(item, max_allowed_amt)
|
||||||
|
|
||||||
|
if role_allowed_to_over_bill in user_roles and total_overbilled_amt > 0.1:
|
||||||
|
frappe.msgprint(_("Overbilling of {} ignored because you have {} role.")
|
||||||
|
.format(total_overbilled_amt, role_allowed_to_over_bill), title=_("Warning"), indicator="orange")
|
||||||
|
|
||||||
def throw_overbill_exception(self, item, max_allowed_amt):
|
def throw_overbill_exception(self, item, max_allowed_amt):
|
||||||
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
|
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
|
||||||
@ -1668,14 +1685,18 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
|
|||||||
return list(payment_entries_against_order) + list(unallocated_payment_entries)
|
return list(payment_entries_against_order) + list(unallocated_payment_entries)
|
||||||
|
|
||||||
def update_invoice_status():
|
def update_invoice_status():
|
||||||
# Daily update the status of the invoices
|
"""Updates status as Overdue for applicable invoices. Runs daily."""
|
||||||
|
|
||||||
frappe.db.sql(""" update `tabSales Invoice` set status = 'Overdue'
|
|
||||||
where due_date < CURDATE() and docstatus = 1 and outstanding_amount > 0""")
|
|
||||||
|
|
||||||
frappe.db.sql(""" update `tabPurchase Invoice` set status = 'Overdue'
|
|
||||||
where due_date < CURDATE() and docstatus = 1 and outstanding_amount > 0""")
|
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):
|
def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):
|
||||||
|
@ -7,6 +7,7 @@ import json
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe import scrub
|
||||||
from frappe.desk.reportview import get_filters_cond, get_match_cond
|
from frappe.desk.reportview import get_filters_cond, get_match_cond
|
||||||
from frappe.utils import nowdate, unique
|
from frappe.utils import nowdate, unique
|
||||||
|
|
||||||
@ -223,18 +224,29 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
|
|||||||
if not field in searchfields]
|
if not field in searchfields]
|
||||||
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
|
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
|
||||||
|
|
||||||
if filters and isinstance(filters, dict) and filters.get('supplier'):
|
if filters and isinstance(filters, dict):
|
||||||
item_group_list = frappe.get_all('Supplier Item Group',
|
if filters.get('customer') or filters.get('supplier'):
|
||||||
filters = {'supplier': filters.get('supplier')}, fields = ['item_group'])
|
party = filters.get('customer') or filters.get('supplier')
|
||||||
|
item_rules_list = frappe.get_all('Party Specific Item',
|
||||||
|
filters = {'party': party}, fields = ['restrict_based_on', 'based_on_value'])
|
||||||
|
|
||||||
item_groups = []
|
filters_dict = {}
|
||||||
for i in item_group_list:
|
for rule in item_rules_list:
|
||||||
item_groups.append(i.item_group)
|
if rule['restrict_based_on'] == 'Item':
|
||||||
|
rule['restrict_based_on'] = 'name'
|
||||||
|
filters_dict[rule.restrict_based_on] = []
|
||||||
|
|
||||||
del filters['supplier']
|
for rule in item_rules_list:
|
||||||
|
filters_dict[rule.restrict_based_on].append(rule.based_on_value)
|
||||||
|
|
||||||
|
for filter in filters_dict:
|
||||||
|
filters[scrub(filter)] = ['in', filters_dict[filter]]
|
||||||
|
|
||||||
|
if filters.get('customer'):
|
||||||
|
del filters['customer']
|
||||||
|
else:
|
||||||
|
del filters['supplier']
|
||||||
|
|
||||||
if item_groups:
|
|
||||||
filters['item_group'] = ['in', item_groups]
|
|
||||||
|
|
||||||
description_cond = ''
|
description_cond = ''
|
||||||
if frappe.db.count('Item', cache=True) < 50000:
|
if frappe.db.count('Item', cache=True) < 50000:
|
||||||
@ -307,7 +319,7 @@ def bom(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
@frappe.validate_and_sanitize_search_inputs
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||||
cond = ''
|
cond = ''
|
||||||
if filters.get('customer'):
|
if filters and filters.get('customer'):
|
||||||
cond = """(`tabProject`.customer = %s or
|
cond = """(`tabProject`.customer = %s or
|
||||||
ifnull(`tabProject`.customer,"")="") and""" %(frappe.db.escape(filters.get("customer")))
|
ifnull(`tabProject`.customer,"")="") and""" %(frappe.db.escape(filters.get("customer")))
|
||||||
|
|
||||||
|
87
erpnext/controllers/tests/test_queries.py
Normal file
87
erpnext/controllers/tests/test_queries.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import unittest
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from erpnext.controllers import queries
|
||||||
|
|
||||||
|
|
||||||
|
def add_default_params(func, doctype):
|
||||||
|
return partial(
|
||||||
|
func, doctype=doctype, txt="", searchfield="name", start=0, page_len=20, filters=None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestQueries(unittest.TestCase):
|
||||||
|
|
||||||
|
# All tests are based on doctype/test_records.json
|
||||||
|
|
||||||
|
def assert_nested_in(self, item, container):
|
||||||
|
self.assertIn(item, [vals for tuples in container for vals in tuples])
|
||||||
|
|
||||||
|
def test_employee_query(self):
|
||||||
|
query = add_default_params(queries.employee_query, "Employee")
|
||||||
|
|
||||||
|
self.assertGreaterEqual(len(query(txt="_Test Employee")), 3)
|
||||||
|
self.assertGreaterEqual(len(query(txt="_Test Employee 1")), 1)
|
||||||
|
|
||||||
|
def test_lead_query(self):
|
||||||
|
query = add_default_params(queries.lead_query, "Lead")
|
||||||
|
|
||||||
|
self.assertGreaterEqual(len(query(txt="_Test Lead")), 4)
|
||||||
|
self.assertEqual(len(query(txt="_Test Lead 4")), 1)
|
||||||
|
|
||||||
|
def test_customer_query(self):
|
||||||
|
query = add_default_params(queries.customer_query, "Customer")
|
||||||
|
|
||||||
|
self.assertGreaterEqual(len(query(txt="_Test Customer")), 7)
|
||||||
|
self.assertGreaterEqual(len(query(txt="_Test Customer USD")), 1)
|
||||||
|
|
||||||
|
def test_supplier_query(self):
|
||||||
|
query = add_default_params(queries.supplier_query, "Supplier")
|
||||||
|
|
||||||
|
self.assertGreaterEqual(len(query(txt="_Test Supplier")), 7)
|
||||||
|
self.assertGreaterEqual(len(query(txt="_Test Supplier USD")), 1)
|
||||||
|
|
||||||
|
def test_item_query(self):
|
||||||
|
query = add_default_params(queries.item_query, "Item")
|
||||||
|
|
||||||
|
self.assertGreaterEqual(len(query(txt="_Test Item")), 7)
|
||||||
|
self.assertEqual(len(query(txt="_Test Item Home Desktop 100 3")), 1)
|
||||||
|
|
||||||
|
fg_item = "_Test FG Item"
|
||||||
|
stock_items = query(txt=fg_item, filters={"is_stock_item": 1})
|
||||||
|
self.assert_nested_in("_Test FG Item", stock_items)
|
||||||
|
|
||||||
|
bundled_stock_items = query(txt="_test product bundle item 5", filters={"is_stock_item": 1})
|
||||||
|
self.assertEqual(len(bundled_stock_items), 0)
|
||||||
|
|
||||||
|
def test_bom_qury(self):
|
||||||
|
query = add_default_params(queries.bom, "BOM")
|
||||||
|
|
||||||
|
self.assertGreaterEqual(len(query(txt="_Test Item Home Desktop Manufactured")), 1)
|
||||||
|
|
||||||
|
def test_project_query(self):
|
||||||
|
query = add_default_params(queries.get_project_name, "BOM")
|
||||||
|
|
||||||
|
self.assertGreaterEqual(len(query(txt="_Test Project")), 1)
|
||||||
|
|
||||||
|
def test_account_query(self):
|
||||||
|
query = add_default_params(queries.get_account_list, "Account")
|
||||||
|
|
||||||
|
debtor_accounts = query(txt="Debtors", filters={"company": "_Test Company"})
|
||||||
|
self.assert_nested_in("Debtors - _TC", debtor_accounts)
|
||||||
|
|
||||||
|
def test_income_account_query(self):
|
||||||
|
query = add_default_params(queries.get_income_account, "Account")
|
||||||
|
|
||||||
|
self.assertGreaterEqual(len(query(filters={"company": "_Test Company"})), 1)
|
||||||
|
|
||||||
|
def test_expense_account_query(self):
|
||||||
|
query = add_default_params(queries.get_expense_account, "Account")
|
||||||
|
|
||||||
|
self.assertGreaterEqual(len(query(filters={"company": "_Test Company"})), 1)
|
||||||
|
|
||||||
|
def test_warehouse_query(self):
|
||||||
|
query = add_default_params(queries.warehouse_query, "Account")
|
||||||
|
|
||||||
|
wh = query(filters=[["Bin", "item_code", "=", "_Test Item"]])
|
||||||
|
self.assertGreaterEqual(len(wh), 1)
|
@ -7,6 +7,7 @@ import json
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.modules.utils import get_module_app
|
||||||
from frappe.utils import flt, has_common
|
from frappe.utils import flt, has_common
|
||||||
from frappe.utils.user import is_website_user
|
from frappe.utils.user import is_website_user
|
||||||
|
|
||||||
@ -21,8 +22,32 @@ def get_list_context(context=None):
|
|||||||
"get_list": get_transaction_list
|
"get_list": get_transaction_list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_webform_list_context(module):
|
||||||
|
if get_module_app(module) != 'erpnext':
|
||||||
|
return
|
||||||
|
return {
|
||||||
|
"get_list": get_webform_transaction_list
|
||||||
|
}
|
||||||
|
|
||||||
def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified"):
|
def get_webform_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified"):
|
||||||
|
""" Get List of transactions for custom doctypes """
|
||||||
|
from frappe.www.list import get_list
|
||||||
|
|
||||||
|
if not filters:
|
||||||
|
filters = []
|
||||||
|
|
||||||
|
meta = frappe.get_meta(doctype)
|
||||||
|
|
||||||
|
for d in meta.fields:
|
||||||
|
if d.fieldtype == 'Link' and d.fieldname != 'amended_from':
|
||||||
|
allowed_docs = [d.name for d in get_transaction_list(doctype=d.options, custom=True)]
|
||||||
|
allowed_docs.append('')
|
||||||
|
filters.append((d.fieldname, 'in', allowed_docs))
|
||||||
|
|
||||||
|
return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=False,
|
||||||
|
fields=None, order_by="modified")
|
||||||
|
|
||||||
|
def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified", custom=False):
|
||||||
user = frappe.session.user
|
user = frappe.session.user
|
||||||
ignore_permissions = False
|
ignore_permissions = False
|
||||||
|
|
||||||
@ -46,7 +71,7 @@ def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_p
|
|||||||
filters.append(('customer', 'in', customers))
|
filters.append(('customer', 'in', customers))
|
||||||
elif suppliers:
|
elif suppliers:
|
||||||
filters.append(('supplier', 'in', suppliers))
|
filters.append(('supplier', 'in', suppliers))
|
||||||
else:
|
elif not custom:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
if doctype == 'Request for Quotation':
|
if doctype == 'Request for Quotation':
|
||||||
@ -56,9 +81,16 @@ def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_p
|
|||||||
# Since customers and supplier do not have direct access to internal doctypes
|
# Since customers and supplier do not have direct access to internal doctypes
|
||||||
ignore_permissions = True
|
ignore_permissions = True
|
||||||
|
|
||||||
|
if not customers and not suppliers and custom:
|
||||||
|
ignore_permissions = False
|
||||||
|
filters = []
|
||||||
|
|
||||||
transactions = get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length,
|
transactions = get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length,
|
||||||
fields='name', ignore_permissions=ignore_permissions, order_by='modified desc')
|
fields='name', ignore_permissions=ignore_permissions, order_by='modified desc')
|
||||||
|
|
||||||
|
if custom:
|
||||||
|
return transactions
|
||||||
|
|
||||||
return post_process(doctype, transactions)
|
return post_process(doctype, transactions)
|
||||||
|
|
||||||
def get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length=20,
|
def get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length=20,
|
||||||
|
@ -132,10 +132,43 @@ frappe.ui.form.on("Opportunity", {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
currency: function(frm) {
|
||||||
|
let company_currency = erpnext.get_currency(frm.doc.company);
|
||||||
|
if (company_currency != frm.doc.company) {
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.setup.utils.get_exchange_rate",
|
||||||
|
args: {
|
||||||
|
from_currency: frm.doc.currency,
|
||||||
|
to_currency: company_currency
|
||||||
|
},
|
||||||
|
callback: function(r) {
|
||||||
|
if (r.message) {
|
||||||
|
frm.set_value('conversion_rate', flt(r.message));
|
||||||
|
frm.set_df_property('conversion_rate', 'description', '1 ' + frm.doc.currency
|
||||||
|
+ ' = [?] ' + company_currency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
frm.set_value('conversion_rate', 1.0);
|
||||||
|
frm.set_df_property('conversion_rate', 'hidden', 1);
|
||||||
|
frm.set_df_property('conversion_rate', 'description', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
frm.trigger('opportunity_amount');
|
||||||
|
frm.trigger('set_dynamic_field_label');
|
||||||
|
},
|
||||||
|
|
||||||
|
opportunity_amount: function(frm) {
|
||||||
|
frm.set_value('base_opportunity_amount', flt(frm.doc.opportunity_amount) * flt(frm.doc.conversion_rate));
|
||||||
|
},
|
||||||
|
|
||||||
set_dynamic_field_label: function(frm){
|
set_dynamic_field_label: function(frm){
|
||||||
if (frm.doc.opportunity_from) {
|
if (frm.doc.opportunity_from) {
|
||||||
frm.set_df_property("party_name", "label", frm.doc.opportunity_from);
|
frm.set_df_property("party_name", "label", frm.doc.opportunity_from);
|
||||||
}
|
}
|
||||||
|
frm.trigger('change_grid_labels');
|
||||||
|
frm.trigger('change_form_labels');
|
||||||
},
|
},
|
||||||
|
|
||||||
make_supplier_quotation: function(frm) {
|
make_supplier_quotation: function(frm) {
|
||||||
@ -152,6 +185,62 @@ frappe.ui.form.on("Opportunity", {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
change_form_labels: function(frm) {
|
||||||
|
let company_currency = erpnext.get_currency(frm.doc.company);
|
||||||
|
frm.set_currency_labels(["base_opportunity_amount", "base_total", "base_grand_total"], company_currency);
|
||||||
|
frm.set_currency_labels(["opportunity_amount", "total", "grand_total"], frm.doc.currency);
|
||||||
|
|
||||||
|
// toggle fields
|
||||||
|
frm.toggle_display(["conversion_rate", "base_opportunity_amount", "base_total", "base_grand_total"],
|
||||||
|
frm.doc.currency != company_currency);
|
||||||
|
},
|
||||||
|
|
||||||
|
change_grid_labels: function(frm) {
|
||||||
|
let company_currency = erpnext.get_currency(frm.doc.company);
|
||||||
|
frm.set_currency_labels(["base_rate", "base_amount"], company_currency, "items");
|
||||||
|
frm.set_currency_labels(["rate", "amount"], frm.doc.currency, "items");
|
||||||
|
|
||||||
|
let item_grid = frm.fields_dict.items.grid;
|
||||||
|
$.each(["base_rate", "base_amount"], function(i, fname) {
|
||||||
|
if(frappe.meta.get_docfield(item_grid.doctype, fname))
|
||||||
|
item_grid.set_column_disp(fname, frm.doc.currency != company_currency);
|
||||||
|
});
|
||||||
|
frm.refresh_fields();
|
||||||
|
},
|
||||||
|
|
||||||
|
calculate_total: function(frm) {
|
||||||
|
let total = 0, base_total = 0, grand_total = 0, base_grand_total = 0;
|
||||||
|
frm.doc.items.forEach(item => {
|
||||||
|
total += item.amount;
|
||||||
|
base_total += item.base_amount;
|
||||||
|
})
|
||||||
|
|
||||||
|
base_grand_total = base_total + frm.doc.base_opportunity_amount;
|
||||||
|
grand_total = total + frm.doc.opportunity_amount;
|
||||||
|
|
||||||
|
frm.set_value({
|
||||||
|
'total': flt(total),
|
||||||
|
'base_total': flt(base_total),
|
||||||
|
'grand_total': flt(grand_total),
|
||||||
|
'base_grand_total': flt(base_grand_total)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
frappe.ui.form.on("Opportunity Item", {
|
||||||
|
calculate: function(frm, cdt, cdn) {
|
||||||
|
let row = frappe.get_doc(cdt, cdn);
|
||||||
|
frappe.model.set_value(cdt, cdn, "amount", flt(row.qty) * flt(row.rate));
|
||||||
|
frappe.model.set_value(cdt, cdn, "base_rate", flt(frm.doc.conversion_rate) * flt(row.rate));
|
||||||
|
frappe.model.set_value(cdt, cdn, "base_amount", flt(frm.doc.conversion_rate) * flt(row.amount));
|
||||||
|
frm.trigger("calculate_total");
|
||||||
|
},
|
||||||
|
qty: function(frm, cdt, cdn) {
|
||||||
|
frm.trigger("calculate", cdt, cdn);
|
||||||
|
},
|
||||||
|
rate: function(frm, cdt, cdn) {
|
||||||
|
frm.trigger("calculate", cdt, cdn);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO commonify this code
|
// TODO commonify this code
|
||||||
@ -169,6 +258,7 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.setup_queries();
|
this.setup_queries();
|
||||||
|
this.frm.trigger('currency');
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_queries() {
|
setup_queries() {
|
||||||
|
@ -33,12 +33,20 @@
|
|||||||
"to_discuss",
|
"to_discuss",
|
||||||
"section_break_14",
|
"section_break_14",
|
||||||
"currency",
|
"currency",
|
||||||
"opportunity_amount",
|
"conversion_rate",
|
||||||
|
"base_opportunity_amount",
|
||||||
"with_items",
|
"with_items",
|
||||||
"column_break_17",
|
"column_break_17",
|
||||||
"probability",
|
"probability",
|
||||||
|
"opportunity_amount",
|
||||||
"items_section",
|
"items_section",
|
||||||
"items",
|
"items",
|
||||||
|
"section_break_32",
|
||||||
|
"base_total",
|
||||||
|
"base_grand_total",
|
||||||
|
"column_break_33",
|
||||||
|
"total",
|
||||||
|
"grand_total",
|
||||||
"contact_info",
|
"contact_info",
|
||||||
"customer_address",
|
"customer_address",
|
||||||
"address_display",
|
"address_display",
|
||||||
@ -425,12 +433,65 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Print Language",
|
"label": "Print Language",
|
||||||
"options": "Language"
|
"options": "Language"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "base_opportunity_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Opportunity Amount (Company Currency)",
|
||||||
|
"options": "Company:company:default_currency",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "with_items",
|
||||||
|
"fieldname": "section_break_32",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"hide_border": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "base_total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Total (Company Currency)",
|
||||||
|
"options": "Company:company:default_currency",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Total",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "conversion_rate",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Exchange Rate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_33",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "base_grand_total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Grand Total (Company Currency)",
|
||||||
|
"options": "Company:company:default_currency",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "grand_total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Grand Total",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-info-sign",
|
"icon": "fa fa-info-sign",
|
||||||
"idx": 195,
|
"idx": 195,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-08-25 10:28:24.923543",
|
"modified": "2021-09-06 10:02:18.609136",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "Opportunity",
|
"name": "Opportunity",
|
||||||
|
@ -9,9 +9,8 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.email.inbox import link_communication_to_document
|
from frappe.email.inbox import link_communication_to_document
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.utils import cint, cstr, get_fullname
|
from frappe.utils import cint, cstr, flt, get_fullname
|
||||||
|
|
||||||
from erpnext.accounts.party import get_party_account_currency
|
|
||||||
from erpnext.setup.utils import get_exchange_rate
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
from erpnext.utilities.transaction_base import TransactionBase
|
from erpnext.utilities.transaction_base import TransactionBase
|
||||||
|
|
||||||
@ -41,6 +40,23 @@ class Opportunity(TransactionBase):
|
|||||||
if not self.with_items:
|
if not self.with_items:
|
||||||
self.items = []
|
self.items = []
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.calculate_totals()
|
||||||
|
|
||||||
|
def calculate_totals(self):
|
||||||
|
total = base_total = 0
|
||||||
|
for item in self.get('items'):
|
||||||
|
item.amount = flt(item.rate) * flt(item.qty)
|
||||||
|
item.base_rate = flt(self.conversion_rate * item.rate)
|
||||||
|
item.base_amount = flt(self.conversion_rate * item.amount)
|
||||||
|
total += item.amount
|
||||||
|
base_total += item.base_amount
|
||||||
|
|
||||||
|
self.total = flt(total)
|
||||||
|
self.base_total = flt(base_total)
|
||||||
|
self.grand_total = flt(self.total) + flt(self.opportunity_amount)
|
||||||
|
self.base_grand_total = flt(self.base_total) + flt(self.base_opportunity_amount)
|
||||||
|
|
||||||
def make_new_lead_if_required(self):
|
def make_new_lead_if_required(self):
|
||||||
"""Set lead against new opportunity"""
|
"""Set lead against new opportunity"""
|
||||||
if (not self.get("party_name")) and self.contact_email:
|
if (not self.get("party_name")) and self.contact_email:
|
||||||
@ -224,13 +240,6 @@ def make_quotation(source_name, target_doc=None):
|
|||||||
|
|
||||||
company_currency = frappe.get_cached_value('Company', quotation.company, "default_currency")
|
company_currency = frappe.get_cached_value('Company', quotation.company, "default_currency")
|
||||||
|
|
||||||
if quotation.quotation_to == 'Customer' and quotation.party_name:
|
|
||||||
party_account_currency = get_party_account_currency("Customer", quotation.party_name, quotation.company)
|
|
||||||
else:
|
|
||||||
party_account_currency = company_currency
|
|
||||||
|
|
||||||
quotation.currency = party_account_currency or company_currency
|
|
||||||
|
|
||||||
if company_currency == quotation.currency:
|
if company_currency == quotation.currency:
|
||||||
exchange_rate = 1
|
exchange_rate = 1
|
||||||
else:
|
else:
|
||||||
@ -254,7 +263,7 @@ def make_quotation(source_name, target_doc=None):
|
|||||||
"doctype": "Quotation",
|
"doctype": "Quotation",
|
||||||
"field_map": {
|
"field_map": {
|
||||||
"opportunity_from": "quotation_to",
|
"opportunity_from": "quotation_to",
|
||||||
"name": "enq_no",
|
"name": "enq_no"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Opportunity Item": {
|
"Opportunity Item": {
|
||||||
|
@ -63,6 +63,10 @@ class TestOpportunity(unittest.TestCase):
|
|||||||
self.assertEqual(opp_doc.opportunity_from, "Customer")
|
self.assertEqual(opp_doc.opportunity_from, "Customer")
|
||||||
self.assertEqual(opp_doc.party_name, customer.name)
|
self.assertEqual(opp_doc.party_name, customer.name)
|
||||||
|
|
||||||
|
def test_opportunity_item(self):
|
||||||
|
opportunity_doc = make_opportunity(with_items=1, rate=1100, qty=2)
|
||||||
|
self.assertEqual(opportunity_doc.total, 2200)
|
||||||
|
|
||||||
def make_opportunity(**args):
|
def make_opportunity(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|
||||||
@ -71,6 +75,7 @@ def make_opportunity(**args):
|
|||||||
"company": args.company or "_Test Company",
|
"company": args.company or "_Test Company",
|
||||||
"opportunity_from": args.opportunity_from or "Customer",
|
"opportunity_from": args.opportunity_from or "Customer",
|
||||||
"opportunity_type": "Sales",
|
"opportunity_type": "Sales",
|
||||||
|
"conversion_rate": 1.0,
|
||||||
"with_items": args.with_items or 0,
|
"with_items": args.with_items or 0,
|
||||||
"transaction_date": today()
|
"transaction_date": today()
|
||||||
})
|
})
|
||||||
@ -85,6 +90,7 @@ def make_opportunity(**args):
|
|||||||
opp_doc.append('items', {
|
opp_doc.append('items', {
|
||||||
"item_code": args.item_code or "_Test Item",
|
"item_code": args.item_code or "_Test Item",
|
||||||
"qty": args.qty or 1,
|
"qty": args.qty or 1,
|
||||||
|
"rate": args.rate or 1000,
|
||||||
"uom": "_Test UOM"
|
"uom": "_Test UOM"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,469 +1,177 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_events_in_timeline": 0,
|
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2013-02-22 01:27:51",
|
"creation": "2013-02-22 01:27:51",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"item_code",
|
||||||
|
"item_name",
|
||||||
|
"col_break1",
|
||||||
|
"uom",
|
||||||
|
"qty",
|
||||||
|
"section_break_6",
|
||||||
|
"brand",
|
||||||
|
"item_group",
|
||||||
|
"description",
|
||||||
|
"column_break_8",
|
||||||
|
"image",
|
||||||
|
"image_view",
|
||||||
|
"quantity_and_rate_section",
|
||||||
|
"base_rate",
|
||||||
|
"base_amount",
|
||||||
|
"column_break_16",
|
||||||
|
"rate",
|
||||||
|
"amount"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "item_code",
|
"fieldname": "item_code",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Item Code",
|
"label": "Item Code",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"oldfieldname": "item_code",
|
"oldfieldname": "item_code",
|
||||||
"oldfieldtype": "Link",
|
"oldfieldtype": "Link",
|
||||||
"options": "Item",
|
"options": "Item"
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "col_break1",
|
"fieldname": "col_break1",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"default": "1",
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "qty",
|
"fieldname": "qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Qty",
|
"label": "Qty",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"oldfieldname": "qty",
|
"oldfieldname": "qty",
|
||||||
"oldfieldtype": "Currency",
|
"oldfieldtype": "Currency"
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"description": "",
|
|
||||||
"fieldname": "item_group",
|
"fieldname": "item_group",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Item Group",
|
"label": "Item Group",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"oldfieldname": "item_group",
|
"oldfieldname": "item_group",
|
||||||
"oldfieldtype": "Link",
|
"oldfieldtype": "Link",
|
||||||
"options": "Item Group",
|
"options": "Item Group",
|
||||||
"permlevel": 0,
|
"print_hide": 1
|
||||||
"print_hide": 1,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "brand",
|
"fieldname": "brand",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Brand",
|
"label": "Brand",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"oldfieldname": "brand",
|
"oldfieldname": "brand",
|
||||||
"oldfieldtype": "Link",
|
"oldfieldtype": "Link",
|
||||||
"options": "Brand",
|
"options": "Brand",
|
||||||
"permlevel": 0,
|
"print_hide": 1
|
||||||
"print_hide": 1,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"collapsible": 1,
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "section_break_6",
|
"fieldname": "section_break_6",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"hidden": 0,
|
"label": "Description"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "uom",
|
"fieldname": "uom",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "UOM",
|
"label": "UOM",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"oldfieldname": "uom",
|
"oldfieldname": "uom",
|
||||||
"oldfieldtype": "Link",
|
"oldfieldtype": "Link",
|
||||||
"options": "UOM",
|
"options": "UOM"
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "item_name",
|
"fieldname": "item_name",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 1,
|
"in_global_search": 1,
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Item Name",
|
"label": "Item Name",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"oldfieldname": "item_name",
|
"oldfieldname": "item_name",
|
||||||
"oldfieldtype": "Data",
|
"oldfieldtype": "Data"
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "description",
|
"fieldname": "description",
|
||||||
"fieldtype": "Text Editor",
|
"fieldtype": "Text Editor",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Description",
|
"label": "Description",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"oldfieldname": "description",
|
"oldfieldname": "description",
|
||||||
"oldfieldtype": "Text",
|
"oldfieldtype": "Text",
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": "300px",
|
"print_width": "300px",
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": "300px"
|
"width": "300px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "column_break_8",
|
"fieldname": "column_break_8",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "image",
|
"fieldname": "image",
|
||||||
"fieldtype": "Attach",
|
"fieldtype": "Attach",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"ignore_user_permissions": 0,
|
"label": "Image"
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Image",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "image_view",
|
"fieldname": "image_view",
|
||||||
"fieldtype": "Image",
|
"fieldtype": "Image",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Image View",
|
"label": "Image View",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "image",
|
"options": "image",
|
||||||
"permlevel": 0,
|
"print_hide": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 1,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"fieldname": "rate",
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "basic_rate",
|
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"hidden": 1,
|
"in_list_view": 1,
|
||||||
"ignore_user_permissions": 0,
|
"label": "Rate",
|
||||||
"ignore_xss_filter": 0,
|
"options": "currency",
|
||||||
"in_filter": 0,
|
"reqd": 1
|
||||||
"in_global_search": 0,
|
},
|
||||||
"in_list_view": 0,
|
{
|
||||||
"in_standard_filter": 0,
|
"fieldname": "quantity_and_rate_section",
|
||||||
"label": "Basic Rate",
|
"fieldtype": "Section Break",
|
||||||
"length": 0,
|
"label": "Quantity and Rate"
|
||||||
"no_copy": 0,
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "base_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Amount (Company Currency)",
|
||||||
|
"options": "Company:company:default_currency",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_16",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Amount",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "base_rate",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Rate (Company Currency)",
|
||||||
"oldfieldname": "basic_rate",
|
"oldfieldname": "basic_rate",
|
||||||
"oldfieldtype": "Currency",
|
"oldfieldtype": "Currency",
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"print_hide_if_no_value": 0,
|
"read_only": 1,
|
||||||
"read_only": 0,
|
"reqd": 1
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
|
||||||
"hide_heading": 0,
|
|
||||||
"hide_toolbar": 0,
|
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"max_attachments": 0,
|
"links": [],
|
||||||
"modified": "2018-12-28 15:43:09.382012",
|
"modified": "2021-07-30 16:39:09.775720",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "Opportunity Item",
|
"name": "Opportunity Item",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 0,
|
"sort_field": "modified",
|
||||||
"read_only": 0,
|
"sort_order": "DESC",
|
||||||
"read_only_onload": 0,
|
"track_changes": 1
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"track_changes": 1,
|
|
||||||
"track_seen": 0,
|
|
||||||
"track_views": 0
|
|
||||||
}
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
frappe.query_reports["Opportunity Summary by Sales Stage"] = {
|
||||||
|
"filters": [
|
||||||
|
{
|
||||||
|
fieldname: "based_on",
|
||||||
|
label: __("Based On"),
|
||||||
|
fieldtype: "Select",
|
||||||
|
options: "Opportunity Owner\nSource\nOpportunity Type",
|
||||||
|
default: "Opportunity Owner"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "data_based_on",
|
||||||
|
label: __("Data Based On"),
|
||||||
|
fieldtype: "Select",
|
||||||
|
options: "Number\nAmount",
|
||||||
|
default: "Number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "from_date",
|
||||||
|
label: __("From Date"),
|
||||||
|
fieldtype: "Date",
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "to_date",
|
||||||
|
label: __("To Date"),
|
||||||
|
fieldtype: "Date",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "status",
|
||||||
|
label: __("Status"),
|
||||||
|
fieldtype: "MultiSelectList",
|
||||||
|
get_data: function() {
|
||||||
|
return [
|
||||||
|
{value: "Open", description: "Status"},
|
||||||
|
{value: "Converted", description: "Status"},
|
||||||
|
{value: "Quotation", description: "Status"},
|
||||||
|
{value: "Replied", description: "Status"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "opportunity_source",
|
||||||
|
label: __("Oppoturnity Source"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Lead Source",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "opportunity_type",
|
||||||
|
label: __("Opportunity Type"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Opportunity Type",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "company",
|
||||||
|
label: __("Company"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Company",
|
||||||
|
default: frappe.defaults.get_user_default("Company")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"add_total_row": 0,
|
||||||
|
"columns": [],
|
||||||
|
"creation": "2021-07-28 12:18:24.028737",
|
||||||
|
"disable_prepared_report": 0,
|
||||||
|
"disabled": 0,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Report",
|
||||||
|
"filters": [],
|
||||||
|
"idx": 0,
|
||||||
|
"is_standard": "Yes",
|
||||||
|
"modified": "2021-07-28 12:18:24.028737",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "CRM",
|
||||||
|
"name": "Opportunity Summary by Sales Stage",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"prepared_report": 0,
|
||||||
|
"ref_doctype": "Opportunity",
|
||||||
|
"report_name": "Opportunity Summary by Sales Stage ",
|
||||||
|
"report_type": "Script Report",
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "Sales User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Sales Manager"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,254 @@
|
|||||||
|
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
import json
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
import pandas
|
||||||
|
from frappe import _
|
||||||
|
from frappe.utils import flt
|
||||||
|
from six import iteritems
|
||||||
|
|
||||||
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
|
|
||||||
|
|
||||||
|
def execute(filters=None):
|
||||||
|
return OpportunitySummaryBySalesStage(filters).run()
|
||||||
|
|
||||||
|
class OpportunitySummaryBySalesStage(object):
|
||||||
|
def __init__(self,filters=None):
|
||||||
|
self.filters = frappe._dict(filters or {})
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.get_columns()
|
||||||
|
self.get_data()
|
||||||
|
self.get_chart_data()
|
||||||
|
return self.columns, self.data, None, self.chart
|
||||||
|
|
||||||
|
def get_columns(self):
|
||||||
|
self.columns = []
|
||||||
|
|
||||||
|
if self.filters.get('based_on') == 'Opportunity Owner':
|
||||||
|
self.columns.append({
|
||||||
|
'label': _('Opportunity Owner'),
|
||||||
|
'fieldname': 'opportunity_owner',
|
||||||
|
'width': 200
|
||||||
|
})
|
||||||
|
|
||||||
|
if self.filters.get('based_on') == 'Source':
|
||||||
|
self.columns.append({
|
||||||
|
'label': _('Source'),
|
||||||
|
'fieldname': 'source',
|
||||||
|
'fieldtype': 'Link',
|
||||||
|
'options': 'Lead Source',
|
||||||
|
'width': 200
|
||||||
|
})
|
||||||
|
|
||||||
|
if self.filters.get('based_on') == 'Opportunity Type':
|
||||||
|
self.columns.append({
|
||||||
|
'label': _('Opportunity Type'),
|
||||||
|
'fieldname': 'opportunity_type',
|
||||||
|
'width': 200
|
||||||
|
})
|
||||||
|
|
||||||
|
self.set_sales_stage_columns()
|
||||||
|
|
||||||
|
def set_sales_stage_columns(self):
|
||||||
|
self.sales_stage_list = frappe.db.get_list('Sales Stage', pluck='name')
|
||||||
|
|
||||||
|
for sales_stage in self.sales_stage_list:
|
||||||
|
if self.filters.get('data_based_on') == 'Number':
|
||||||
|
self.columns.append({
|
||||||
|
'label': _(sales_stage),
|
||||||
|
'fieldname': sales_stage,
|
||||||
|
'fieldtype': 'Int',
|
||||||
|
'width': 150
|
||||||
|
})
|
||||||
|
|
||||||
|
elif self.filters.get('data_based_on') == 'Amount':
|
||||||
|
self.columns.append({
|
||||||
|
'label': _(sales_stage),
|
||||||
|
'fieldname': sales_stage,
|
||||||
|
'fieldtype': 'Currency',
|
||||||
|
'width': 150
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
self.data = []
|
||||||
|
|
||||||
|
based_on = {
|
||||||
|
'Opportunity Owner': '_assign',
|
||||||
|
'Source': 'source',
|
||||||
|
'Opportunity Type': 'opportunity_type'
|
||||||
|
}[self.filters.get('based_on')]
|
||||||
|
|
||||||
|
data_based_on = {
|
||||||
|
'Number': 'count(name) as count',
|
||||||
|
'Amount': 'opportunity_amount as amount',
|
||||||
|
}[self.filters.get('data_based_on')]
|
||||||
|
|
||||||
|
self.get_data_query(based_on, data_based_on)
|
||||||
|
|
||||||
|
self.get_rows()
|
||||||
|
|
||||||
|
def get_data_query(self, based_on, data_based_on):
|
||||||
|
if self.filters.get('data_based_on') == 'Number':
|
||||||
|
group_by = '{},{}'.format('sales_stage', based_on)
|
||||||
|
self.query_result = frappe.db.get_list('Opportunity',
|
||||||
|
filters=self.get_conditions(),
|
||||||
|
fields=['sales_stage', data_based_on, based_on],
|
||||||
|
group_by=group_by
|
||||||
|
)
|
||||||
|
|
||||||
|
elif self.filters.get('data_based_on') == 'Amount':
|
||||||
|
self.query_result = frappe.db.get_list('Opportunity',
|
||||||
|
filters=self.get_conditions(),
|
||||||
|
fields=['sales_stage', based_on, data_based_on, 'currency']
|
||||||
|
)
|
||||||
|
|
||||||
|
self.convert_to_base_currency()
|
||||||
|
|
||||||
|
dataframe = pandas.DataFrame.from_records(self.query_result)
|
||||||
|
dataframe.replace(to_replace=[None], value='Not Assigned', inplace=True)
|
||||||
|
result = dataframe.groupby(['sales_stage', based_on], as_index=False)['amount'].sum()
|
||||||
|
|
||||||
|
self.grouped_data = []
|
||||||
|
|
||||||
|
for i in range(len(result['amount'])):
|
||||||
|
self.grouped_data.append({
|
||||||
|
'sales_stage': result['sales_stage'][i],
|
||||||
|
based_on : result[based_on][i],
|
||||||
|
'amount': result['amount'][i]
|
||||||
|
})
|
||||||
|
|
||||||
|
self.query_result = self.grouped_data
|
||||||
|
|
||||||
|
def get_rows(self):
|
||||||
|
self.data = []
|
||||||
|
self.get_formatted_data()
|
||||||
|
|
||||||
|
for based_on,data in iteritems(self.formatted_data):
|
||||||
|
row_based_on={
|
||||||
|
'Opportunity Owner': 'opportunity_owner',
|
||||||
|
'Source': 'source',
|
||||||
|
'Opportunity Type': 'opportunity_type'
|
||||||
|
}[self.filters.get('based_on')]
|
||||||
|
|
||||||
|
row = {row_based_on: based_on}
|
||||||
|
|
||||||
|
for d in self.query_result:
|
||||||
|
sales_stage = d.get('sales_stage')
|
||||||
|
row[sales_stage] = data.get(sales_stage)
|
||||||
|
|
||||||
|
self.data.append(row)
|
||||||
|
|
||||||
|
def get_formatted_data(self):
|
||||||
|
self.formatted_data = frappe._dict()
|
||||||
|
|
||||||
|
for d in self.query_result:
|
||||||
|
data_based_on ={
|
||||||
|
'Number': 'count',
|
||||||
|
'Amount': 'amount'
|
||||||
|
}[self.filters.get('data_based_on')]
|
||||||
|
|
||||||
|
based_on ={
|
||||||
|
'Opportunity Owner': '_assign',
|
||||||
|
'Source': 'source',
|
||||||
|
'Opportunity Type': 'opportunity_type'
|
||||||
|
}[self.filters.get('based_on')]
|
||||||
|
|
||||||
|
if self.filters.get('based_on') == 'Opportunity Owner':
|
||||||
|
if d.get(based_on) == '[]' or d.get(based_on) is None or d.get(based_on) == 'Not Assigned':
|
||||||
|
assignments = ['Not Assigned']
|
||||||
|
else:
|
||||||
|
assignments = json.loads(d.get(based_on))
|
||||||
|
|
||||||
|
sales_stage = d.get('sales_stage')
|
||||||
|
count = d.get(data_based_on)
|
||||||
|
|
||||||
|
if assignments:
|
||||||
|
if len(assignments) > 1:
|
||||||
|
for assigned_to in assignments:
|
||||||
|
self.set_formatted_data_based_on_sales_stage(assigned_to, sales_stage, count)
|
||||||
|
else:
|
||||||
|
assigned_to = assignments[0]
|
||||||
|
self.set_formatted_data_based_on_sales_stage(assigned_to, sales_stage, count)
|
||||||
|
else:
|
||||||
|
value = d.get(based_on)
|
||||||
|
sales_stage = d.get('sales_stage')
|
||||||
|
count = d.get(data_based_on)
|
||||||
|
self.set_formatted_data_based_on_sales_stage(value, sales_stage, count)
|
||||||
|
|
||||||
|
def set_formatted_data_based_on_sales_stage(self, based_on, sales_stage, count):
|
||||||
|
self.formatted_data.setdefault(based_on, frappe._dict()).setdefault(sales_stage, 0)
|
||||||
|
self.formatted_data[based_on][sales_stage] += count
|
||||||
|
|
||||||
|
def get_conditions(self):
|
||||||
|
filters = []
|
||||||
|
|
||||||
|
if self.filters.get('company'):
|
||||||
|
filters.append({'company': self.filters.get('company')})
|
||||||
|
|
||||||
|
if self.filters.get('opportunity_type'):
|
||||||
|
filters.append({'opportunity_type': self.filters.get('opportunity_type')})
|
||||||
|
|
||||||
|
if self.filters.get('opportunity_source'):
|
||||||
|
filters.append({'source': self.filters.get('opportunity_source')})
|
||||||
|
|
||||||
|
if self.filters.get('status'):
|
||||||
|
filters.append({'status': ('in',self.filters.get('status'))})
|
||||||
|
|
||||||
|
if self.filters.get('from_date') and self.filters.get('to_date'):
|
||||||
|
filters.append(['transaction_date', 'between', [self.filters.get('from_date'), self.filters.get('to_date')]])
|
||||||
|
|
||||||
|
return filters
|
||||||
|
|
||||||
|
def get_chart_data(self):
|
||||||
|
labels = []
|
||||||
|
datasets = []
|
||||||
|
values = [0] * 8
|
||||||
|
|
||||||
|
for sales_stage in self.sales_stage_list:
|
||||||
|
labels.append(sales_stage)
|
||||||
|
|
||||||
|
options = {
|
||||||
|
'Number': 'count',
|
||||||
|
'Amount': 'amount'
|
||||||
|
}[self.filters.get('data_based_on')]
|
||||||
|
|
||||||
|
for data in self.query_result:
|
||||||
|
for count in range(len(values)):
|
||||||
|
if data['sales_stage'] == labels[count]:
|
||||||
|
values[count] = values[count] + data[options]
|
||||||
|
|
||||||
|
datasets.append({'name':options, 'values':values})
|
||||||
|
|
||||||
|
self.chart = {
|
||||||
|
'data':{
|
||||||
|
'labels': labels,
|
||||||
|
'datasets': datasets
|
||||||
|
},
|
||||||
|
'type':'line'
|
||||||
|
}
|
||||||
|
|
||||||
|
def currency_conversion(self,from_currency,to_currency):
|
||||||
|
cacheobj = frappe.cache()
|
||||||
|
|
||||||
|
if cacheobj.get(from_currency):
|
||||||
|
return flt(str(cacheobj.get(from_currency),'UTF-8'))
|
||||||
|
|
||||||
|
else:
|
||||||
|
value = get_exchange_rate(from_currency,to_currency)
|
||||||
|
cacheobj.set(from_currency,value)
|
||||||
|
return flt(str(cacheobj.get(from_currency),'UTF-8'))
|
||||||
|
|
||||||
|
def get_default_currency(self):
|
||||||
|
company = self.filters.get('company')
|
||||||
|
return frappe.db.get_value('Company', company, 'default_currency')
|
||||||
|
|
||||||
|
def convert_to_base_currency(self):
|
||||||
|
default_currency = self.get_default_currency()
|
||||||
|
for data in self.query_result:
|
||||||
|
if data.get('currency') != default_currency:
|
||||||
|
opportunity_currency = data.get('currency')
|
||||||
|
value = self.currency_conversion(opportunity_currency,default_currency)
|
||||||
|
data['amount'] = data['amount'] * value
|
@ -0,0 +1,94 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
from erpnext.crm.report.opportunity_summary_by_sales_stage.opportunity_summary_by_sales_stage import (
|
||||||
|
execute,
|
||||||
|
)
|
||||||
|
from erpnext.crm.report.sales_pipeline_analytics.test_sales_pipeline_analytics import (
|
||||||
|
create_company,
|
||||||
|
create_customer,
|
||||||
|
create_opportunity,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestOpportunitySummaryBySalesStage(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(self):
|
||||||
|
frappe.db.delete("Opportunity")
|
||||||
|
create_company()
|
||||||
|
create_customer()
|
||||||
|
create_opportunity()
|
||||||
|
|
||||||
|
def test_opportunity_summary_by_sales_stage(self):
|
||||||
|
self.check_for_opportunity_owner()
|
||||||
|
self.check_for_source()
|
||||||
|
self.check_for_opportunity_type()
|
||||||
|
self.check_all_filters()
|
||||||
|
|
||||||
|
def check_for_opportunity_owner(self):
|
||||||
|
filters = {
|
||||||
|
'based_on': "Opportunity Owner",
|
||||||
|
'data_based_on': "Number",
|
||||||
|
'company': "Best Test"
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [{
|
||||||
|
'opportunity_owner': "Not Assigned",
|
||||||
|
'Prospecting': 1
|
||||||
|
}]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data, report[1])
|
||||||
|
|
||||||
|
def check_for_source(self):
|
||||||
|
filters = {
|
||||||
|
'based_on': "Source",
|
||||||
|
'data_based_on': "Number",
|
||||||
|
'company': "Best Test"
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [{
|
||||||
|
'source': 'Cold Calling',
|
||||||
|
'Prospecting': 1
|
||||||
|
}]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data, report[1])
|
||||||
|
|
||||||
|
def check_for_opportunity_type(self):
|
||||||
|
filters = {
|
||||||
|
'based_on': "Opportunity Type",
|
||||||
|
'data_based_on': "Number",
|
||||||
|
'company': "Best Test"
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [{
|
||||||
|
'opportunity_type': 'Sales',
|
||||||
|
'Prospecting': 1
|
||||||
|
}]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data, report[1])
|
||||||
|
|
||||||
|
def check_all_filters(self):
|
||||||
|
filters = {
|
||||||
|
'based_on': "Opportunity Type",
|
||||||
|
'data_based_on': "Number",
|
||||||
|
'company': "Best Test",
|
||||||
|
'opportunity_source': "Cold Calling",
|
||||||
|
'opportunity_type': "Sales",
|
||||||
|
'status': ["Open"]
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [{
|
||||||
|
'opportunity_type': 'Sales',
|
||||||
|
'Prospecting': 1
|
||||||
|
}]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data, report[1])
|
@ -0,0 +1,70 @@
|
|||||||
|
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
frappe.query_reports["Sales Pipeline Analytics"] = {
|
||||||
|
"filters": [
|
||||||
|
{
|
||||||
|
fieldname: "pipeline_by",
|
||||||
|
label: __("Pipeline By"),
|
||||||
|
fieldtype: "Select",
|
||||||
|
options: "Owner\nSales Stage",
|
||||||
|
default: "Owner"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "from_date",
|
||||||
|
label: __("From Date"),
|
||||||
|
fieldtype: "Date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "to_date",
|
||||||
|
label: __("To Date"),
|
||||||
|
fieldtype: "Date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "range",
|
||||||
|
label: __("Range"),
|
||||||
|
fieldtype: "Select",
|
||||||
|
options: "Monthly\nQuarterly",
|
||||||
|
default: "Monthly"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "assigned_to",
|
||||||
|
label: __("Assigned To"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "status",
|
||||||
|
label: __("Status"),
|
||||||
|
fieldtype: "Select",
|
||||||
|
options: "Open\nQuotation\nConverted\nReplied"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "based_on",
|
||||||
|
label: __("Based On"),
|
||||||
|
fieldtype: "Select",
|
||||||
|
options: "Number\nAmount",
|
||||||
|
default: "Number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "company",
|
||||||
|
label: __("Company"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Company",
|
||||||
|
default: frappe.defaults.get_user_default("Company")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "opportunity_source",
|
||||||
|
label: __("Opportunity Source"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Lead Source"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "opportunity_type",
|
||||||
|
label: __("Opportunity Type"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Opportunity Type"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
};
|
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"add_total_row": 0,
|
||||||
|
"columns": [],
|
||||||
|
"creation": "2021-07-01 17:29:09.530787",
|
||||||
|
"disable_prepared_report": 0,
|
||||||
|
"disabled": 0,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Report",
|
||||||
|
"filters": [],
|
||||||
|
"idx": 0,
|
||||||
|
"is_standard": "Yes",
|
||||||
|
"modified": "2021-07-01 17:45:17.612861",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "CRM",
|
||||||
|
"name": "Sales Pipeline Analytics",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"prepared_report": 0,
|
||||||
|
"ref_doctype": "Opportunity",
|
||||||
|
"report_name": "Sales Pipeline Analytics",
|
||||||
|
"report_type": "Script Report",
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "Sales User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Sales Manager"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,333 @@
|
|||||||
|
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import json
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
import pandas
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from frappe import _
|
||||||
|
from frappe.utils import cint, flt
|
||||||
|
from six import iteritems
|
||||||
|
|
||||||
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
|
|
||||||
|
|
||||||
|
def execute(filters=None):
|
||||||
|
return SalesPipelineAnalytics(filters).run()
|
||||||
|
|
||||||
|
class SalesPipelineAnalytics(object):
|
||||||
|
def __init__(self, filters=None):
|
||||||
|
self.filters = frappe._dict(filters or {})
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.get_columns()
|
||||||
|
self.get_data()
|
||||||
|
self.get_chart_data()
|
||||||
|
|
||||||
|
return self.columns, self.data, None, self.chart
|
||||||
|
|
||||||
|
def get_columns(self):
|
||||||
|
self.columns = []
|
||||||
|
|
||||||
|
self.set_range_columns()
|
||||||
|
self.set_pipeline_based_on_column()
|
||||||
|
|
||||||
|
def set_range_columns(self):
|
||||||
|
based_on = {
|
||||||
|
'Number': 'Int',
|
||||||
|
'Amount': 'Currency'
|
||||||
|
}[self.filters.get('based_on')]
|
||||||
|
|
||||||
|
if self.filters.get('range') == 'Monthly':
|
||||||
|
month_list = self.get_month_list()
|
||||||
|
|
||||||
|
for month in month_list:
|
||||||
|
self.columns.append({
|
||||||
|
'fieldname': month,
|
||||||
|
'fieldtype': based_on,
|
||||||
|
'label': month,
|
||||||
|
'width': 200
|
||||||
|
})
|
||||||
|
|
||||||
|
elif self.filters.get('range') == 'Quarterly':
|
||||||
|
for quarter in range(1, 5):
|
||||||
|
self.columns.append({
|
||||||
|
'fieldname': f'Q{quarter}',
|
||||||
|
'fieldtype': based_on,
|
||||||
|
'label': f'Q{quarter}',
|
||||||
|
'width': 200
|
||||||
|
})
|
||||||
|
|
||||||
|
def set_pipeline_based_on_column(self):
|
||||||
|
if self.filters.get('pipeline_by') == 'Owner':
|
||||||
|
self.columns.insert(0, {
|
||||||
|
'fieldname': 'opportunity_owner',
|
||||||
|
'label': _('Opportunity Owner'),
|
||||||
|
'width': 200
|
||||||
|
})
|
||||||
|
|
||||||
|
elif self.filters.get('pipeline_by') == 'Sales Stage':
|
||||||
|
self.columns.insert(0, {
|
||||||
|
'fieldname': 'sales_stage',
|
||||||
|
'label': _('Sales Stage'),
|
||||||
|
'width': 200
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_fields(self):
|
||||||
|
self.based_on ={
|
||||||
|
'Owner': '_assign as opportunity_owner',
|
||||||
|
'Sales Stage': 'sales_stage'
|
||||||
|
}[self.filters.get('pipeline_by')]
|
||||||
|
|
||||||
|
self.data_based_on ={
|
||||||
|
'Number': 'count(name) as count',
|
||||||
|
'Amount': 'opportunity_amount as amount'
|
||||||
|
}[self.filters.get('based_on')]
|
||||||
|
|
||||||
|
self.group_by_based_on = {
|
||||||
|
'Owner': '_assign',
|
||||||
|
'Sales Stage': 'sales_stage'
|
||||||
|
}[self.filters.get('pipeline_by')]
|
||||||
|
|
||||||
|
self.group_by_period = {
|
||||||
|
'Monthly': 'month(expected_closing)',
|
||||||
|
'Quarterly': 'QUARTER(expected_closing)'
|
||||||
|
}[self.filters.get('range')]
|
||||||
|
|
||||||
|
self.pipeline_by = {
|
||||||
|
'Owner': 'opportunity_owner',
|
||||||
|
'Sales Stage': 'sales_stage'
|
||||||
|
}[self.filters.get('pipeline_by')]
|
||||||
|
|
||||||
|
self.duration = {
|
||||||
|
'Monthly': 'monthname(expected_closing) as month',
|
||||||
|
'Quarterly': 'QUARTER(expected_closing) as quarter'
|
||||||
|
}[self.filters.get('range')]
|
||||||
|
|
||||||
|
self.period_by = {
|
||||||
|
'Monthly': 'month',
|
||||||
|
'Quarterly': 'quarter'
|
||||||
|
}[self.filters.get('range')]
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
self.get_fields()
|
||||||
|
|
||||||
|
if self.filters.get('based_on') == 'Number':
|
||||||
|
self.query_result = frappe.db.get_list('Opportunity',
|
||||||
|
filters=self.get_conditions(),
|
||||||
|
fields=[self.based_on, self.data_based_on, self.duration],
|
||||||
|
group_by='{},{}'.format(self.group_by_based_on, self.group_by_period),
|
||||||
|
order_by=self.group_by_period
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.filters.get('based_on') == 'Amount':
|
||||||
|
self.query_result = frappe.db.get_list('Opportunity',
|
||||||
|
filters=self.get_conditions(),
|
||||||
|
fields=[self.based_on, self.data_based_on, self.duration, 'currency']
|
||||||
|
)
|
||||||
|
|
||||||
|
self.convert_to_base_currency()
|
||||||
|
|
||||||
|
dataframe = pandas.DataFrame.from_records(self.query_result)
|
||||||
|
dataframe.replace(to_replace=[None], value='Not Assigned', inplace=True)
|
||||||
|
result = dataframe.groupby([self.pipeline_by, self.period_by], as_index=False)['amount'].sum()
|
||||||
|
|
||||||
|
self.grouped_data = []
|
||||||
|
|
||||||
|
for i in range(len(result['amount'])):
|
||||||
|
self.grouped_data.append({
|
||||||
|
self.pipeline_by : result[self.pipeline_by][i],
|
||||||
|
self.period_by : result[self.period_by][i],
|
||||||
|
'amount': result['amount'][i]
|
||||||
|
})
|
||||||
|
|
||||||
|
self.query_result = self.grouped_data
|
||||||
|
|
||||||
|
self.get_periodic_data()
|
||||||
|
self.append_data(self.pipeline_by, self.period_by)
|
||||||
|
|
||||||
|
def get_conditions(self):
|
||||||
|
conditions = []
|
||||||
|
|
||||||
|
if self.filters.get('opportunity_source'):
|
||||||
|
conditions.append({'source': self.filters.get('opportunity_source')})
|
||||||
|
|
||||||
|
if self.filters.get('opportunity_type'):
|
||||||
|
conditions.append({'opportunity_type': self.filters.get('opportunity_type')})
|
||||||
|
|
||||||
|
if self.filters.get('status'):
|
||||||
|
conditions.append({'status': self.filters.get('status')})
|
||||||
|
|
||||||
|
if self.filters.get('company'):
|
||||||
|
conditions.append({'company': self.filters.get('company')})
|
||||||
|
|
||||||
|
if self.filters.get('from_date') and self.filters.get('to_date'):
|
||||||
|
conditions.append(['expected_closing', 'between',
|
||||||
|
[self.filters.get('from_date'), self.filters.get('to_date')]])
|
||||||
|
|
||||||
|
return conditions
|
||||||
|
|
||||||
|
def get_chart_data(self):
|
||||||
|
labels = []
|
||||||
|
datasets = []
|
||||||
|
|
||||||
|
self.append_to_dataset(datasets)
|
||||||
|
|
||||||
|
for column in self.columns:
|
||||||
|
if column['fieldname'] != 'opportunity_owner' and column['fieldname'] != 'sales_stage':
|
||||||
|
labels.append(column['fieldname'])
|
||||||
|
|
||||||
|
self.chart = {
|
||||||
|
'data':{
|
||||||
|
'labels': labels,
|
||||||
|
'datasets': datasets
|
||||||
|
},
|
||||||
|
'type':'line'
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.chart
|
||||||
|
|
||||||
|
def get_periodic_data(self):
|
||||||
|
self.periodic_data = frappe._dict()
|
||||||
|
|
||||||
|
based_on = {
|
||||||
|
'Number': 'count',
|
||||||
|
'Amount': 'amount'
|
||||||
|
}[self.filters.get('based_on')]
|
||||||
|
|
||||||
|
pipeline_by = {
|
||||||
|
'Owner': 'opportunity_owner',
|
||||||
|
'Sales Stage': 'sales_stage'
|
||||||
|
}[self.filters.get('pipeline_by')]
|
||||||
|
|
||||||
|
frequency = {
|
||||||
|
'Monthly': 'month',
|
||||||
|
'Quarterly': 'quarter'
|
||||||
|
}[self.filters.get('range')]
|
||||||
|
|
||||||
|
for info in self.query_result:
|
||||||
|
if self.filters.get('range') == 'Monthly':
|
||||||
|
period = info.get(frequency)
|
||||||
|
if self.filters.get('range') == 'Quarterly':
|
||||||
|
period = f'Q{cint(info.get("quarter"))}'
|
||||||
|
|
||||||
|
value = info.get(pipeline_by)
|
||||||
|
count_or_amount = info.get(based_on)
|
||||||
|
|
||||||
|
if self.filters.get('pipeline_by') == 'Owner':
|
||||||
|
if value == 'Not Assigned' or value == '[]' or value is None:
|
||||||
|
assigned_to = ['Not Assigned']
|
||||||
|
else:
|
||||||
|
assigned_to = json.loads(value)
|
||||||
|
self.check_for_assigned_to(period, value, count_or_amount, assigned_to, info)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.set_formatted_data(period, value, count_or_amount, None)
|
||||||
|
|
||||||
|
def set_formatted_data(self, period, value, count_or_amount, assigned_to):
|
||||||
|
if assigned_to:
|
||||||
|
if len(assigned_to) > 1:
|
||||||
|
if self.filters.get('assigned_to'):
|
||||||
|
for user in assigned_to:
|
||||||
|
if self.filters.get('assigned_to') == user:
|
||||||
|
value = user
|
||||||
|
self.periodic_data.setdefault(value, frappe._dict()).setdefault(period, 0)
|
||||||
|
self.periodic_data[value][period] += count_or_amount
|
||||||
|
else:
|
||||||
|
for user in assigned_to:
|
||||||
|
value = user
|
||||||
|
self.periodic_data.setdefault(value, frappe._dict()).setdefault(period, 0)
|
||||||
|
self.periodic_data[value][period] += count_or_amount
|
||||||
|
else:
|
||||||
|
value = assigned_to[0]
|
||||||
|
self.periodic_data.setdefault(value, frappe._dict()).setdefault(period, 0)
|
||||||
|
self.periodic_data[value][period] += count_or_amount
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.periodic_data.setdefault(value, frappe._dict()).setdefault(period, 0)
|
||||||
|
self.periodic_data[value][period] += count_or_amount
|
||||||
|
|
||||||
|
def check_for_assigned_to(self, period, value, count_or_amount, assigned_to, info):
|
||||||
|
if self.filters.get('assigned_to'):
|
||||||
|
for data in json.loads(info.get('opportunity_owner')):
|
||||||
|
if data == self.filters.get('assigned_to'):
|
||||||
|
self.set_formatted_data(period, data, count_or_amount, assigned_to)
|
||||||
|
else:
|
||||||
|
self.set_formatted_data(period, value, count_or_amount, assigned_to)
|
||||||
|
|
||||||
|
def get_month_list(self):
|
||||||
|
month_list= []
|
||||||
|
current_date = date.today()
|
||||||
|
month_number = date.today().month
|
||||||
|
|
||||||
|
for month in range(month_number,13):
|
||||||
|
month_list.append(current_date.strftime('%B'))
|
||||||
|
current_date = current_date + relativedelta(months=1)
|
||||||
|
|
||||||
|
return month_list
|
||||||
|
|
||||||
|
def append_to_dataset(self, datasets):
|
||||||
|
range_by = {
|
||||||
|
'Monthly': 'month',
|
||||||
|
'Quarterly': 'quarter'
|
||||||
|
}[self.filters.get('range')]
|
||||||
|
|
||||||
|
based_on = {
|
||||||
|
'Amount': 'amount',
|
||||||
|
'Number': 'count'
|
||||||
|
}[self.filters.get('based_on')]
|
||||||
|
|
||||||
|
if self.filters.get('range') == 'Quarterly':
|
||||||
|
frequency_list = [1,2,3,4]
|
||||||
|
count = [0] * 4
|
||||||
|
|
||||||
|
if self.filters.get('range') == 'Monthly':
|
||||||
|
frequency_list = self.get_month_list()
|
||||||
|
count = [0] * 12
|
||||||
|
|
||||||
|
for info in self.query_result:
|
||||||
|
for i in range(len(frequency_list)):
|
||||||
|
if info[range_by] == frequency_list[i]:
|
||||||
|
count[i] = count[i] + info[based_on]
|
||||||
|
datasets.append({'name': based_on, 'values': count})
|
||||||
|
|
||||||
|
def append_data(self, pipeline_by, period_by):
|
||||||
|
self.data = []
|
||||||
|
for pipeline,period_data in iteritems(self.periodic_data):
|
||||||
|
row = {pipeline_by : pipeline}
|
||||||
|
for info in self.query_result:
|
||||||
|
if self.filters.get('range') == 'Monthly':
|
||||||
|
period = info.get(period_by)
|
||||||
|
|
||||||
|
if self.filters.get('range') == 'Quarterly':
|
||||||
|
period = f'Q{cint(info.get(period_by))}'
|
||||||
|
|
||||||
|
count = period_data.get(period,0.0)
|
||||||
|
row[period] = count
|
||||||
|
|
||||||
|
self.data.append(row)
|
||||||
|
|
||||||
|
def get_default_currency(self):
|
||||||
|
company = self.filters.get('company')
|
||||||
|
return frappe.db.get_value('Company',company,['default_currency'])
|
||||||
|
|
||||||
|
def get_currency_rate(self, from_currency, to_currency):
|
||||||
|
cacheobj = frappe.cache()
|
||||||
|
|
||||||
|
if cacheobj.get(from_currency):
|
||||||
|
return flt(str(cacheobj.get(from_currency),'UTF-8'))
|
||||||
|
|
||||||
|
else:
|
||||||
|
value = get_exchange_rate(from_currency, to_currency)
|
||||||
|
cacheobj.set(from_currency, value)
|
||||||
|
return flt(str(cacheobj.get(from_currency),'UTF-8'))
|
||||||
|
|
||||||
|
def convert_to_base_currency(self):
|
||||||
|
default_currency = self.get_default_currency()
|
||||||
|
for data in self.query_result:
|
||||||
|
if data.get('currency') != default_currency:
|
||||||
|
opportunity_currency = data.get('currency')
|
||||||
|
value = self.get_currency_rate(opportunity_currency,default_currency)
|
||||||
|
data['amount'] = data['amount'] * value
|
@ -0,0 +1,238 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
from erpnext.crm.report.sales_pipeline_analytics.sales_pipeline_analytics import execute
|
||||||
|
|
||||||
|
|
||||||
|
class TestSalesPipelineAnalytics(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(self):
|
||||||
|
frappe.db.delete("Opportunity")
|
||||||
|
create_company()
|
||||||
|
create_customer()
|
||||||
|
create_opportunity()
|
||||||
|
|
||||||
|
def test_sales_pipeline_analytics(self):
|
||||||
|
self.check_for_monthly_and_number()
|
||||||
|
self.check_for_monthly_and_amount()
|
||||||
|
self.check_for_quarterly_and_number()
|
||||||
|
self.check_for_quarterly_and_amount()
|
||||||
|
self.check_for_all_filters()
|
||||||
|
|
||||||
|
def check_for_monthly_and_number(self):
|
||||||
|
filters = {
|
||||||
|
'pipeline_by':"Owner",
|
||||||
|
'range':"Monthly",
|
||||||
|
'based_on':"Number",
|
||||||
|
'status':"Open",
|
||||||
|
'opportunity_type':"Sales",
|
||||||
|
'company':"Best Test"
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [
|
||||||
|
{
|
||||||
|
'opportunity_owner':'Not Assigned',
|
||||||
|
'August':1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data,report[1])
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
'pipeline_by':"Sales Stage",
|
||||||
|
'range':"Monthly",
|
||||||
|
'based_on':"Number",
|
||||||
|
'status':"Open",
|
||||||
|
'opportunity_type':"Sales",
|
||||||
|
'company':"Best Test"
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [
|
||||||
|
{
|
||||||
|
'sales_stage':'Prospecting',
|
||||||
|
'August':1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data,report[1])
|
||||||
|
|
||||||
|
def check_for_monthly_and_amount(self):
|
||||||
|
filters = {
|
||||||
|
'pipeline_by':"Owner",
|
||||||
|
'range':"Monthly",
|
||||||
|
'based_on':"Amount",
|
||||||
|
'status':"Open",
|
||||||
|
'opportunity_type':"Sales",
|
||||||
|
'company':"Best Test"
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [
|
||||||
|
{
|
||||||
|
'opportunity_owner':'Not Assigned',
|
||||||
|
'August':150000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data,report[1])
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
'pipeline_by':"Sales Stage",
|
||||||
|
'range':"Monthly",
|
||||||
|
'based_on':"Amount",
|
||||||
|
'status':"Open",
|
||||||
|
'opportunity_type':"Sales",
|
||||||
|
'company':"Best Test"
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [
|
||||||
|
{
|
||||||
|
'sales_stage':'Prospecting',
|
||||||
|
'August':150000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data,report[1])
|
||||||
|
|
||||||
|
def check_for_quarterly_and_number(self):
|
||||||
|
filters = {
|
||||||
|
'pipeline_by':"Owner",
|
||||||
|
'range':"Quarterly",
|
||||||
|
'based_on':"Number",
|
||||||
|
'status':"Open",
|
||||||
|
'opportunity_type':"Sales",
|
||||||
|
'company':"Best Test"
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [
|
||||||
|
{
|
||||||
|
'opportunity_owner':'Not Assigned',
|
||||||
|
'Q3':1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data,report[1])
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
'pipeline_by':"Sales Stage",
|
||||||
|
'range':"Quarterly",
|
||||||
|
'based_on':"Number",
|
||||||
|
'status':"Open",
|
||||||
|
'opportunity_type':"Sales",
|
||||||
|
'company':"Best Test"
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [
|
||||||
|
{
|
||||||
|
'sales_stage':'Prospecting',
|
||||||
|
'Q3':1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data,report[1])
|
||||||
|
|
||||||
|
def check_for_quarterly_and_amount(self):
|
||||||
|
filters = {
|
||||||
|
'pipeline_by':"Owner",
|
||||||
|
'range':"Quarterly",
|
||||||
|
'based_on':"Amount",
|
||||||
|
'status':"Open",
|
||||||
|
'opportunity_type':"Sales",
|
||||||
|
'company':"Best Test"
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [
|
||||||
|
{
|
||||||
|
'opportunity_owner':'Not Assigned',
|
||||||
|
'Q3':150000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data,report[1])
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
'pipeline_by':"Sales Stage",
|
||||||
|
'range':"Quarterly",
|
||||||
|
'based_on':"Amount",
|
||||||
|
'status':"Open",
|
||||||
|
'opportunity_type':"Sales",
|
||||||
|
'company':"Best Test"
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [
|
||||||
|
{
|
||||||
|
'sales_stage':'Prospecting',
|
||||||
|
'Q3':150000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data,report[1])
|
||||||
|
|
||||||
|
def check_for_all_filters(self):
|
||||||
|
filters = {
|
||||||
|
'pipeline_by':"Owner",
|
||||||
|
'range':"Monthly",
|
||||||
|
'based_on':"Number",
|
||||||
|
'status':"Open",
|
||||||
|
'opportunity_type':"Sales",
|
||||||
|
'company':"Best Test",
|
||||||
|
'opportunity_source':'Cold Calling',
|
||||||
|
'from_date': '2021-08-01',
|
||||||
|
'to_date':'2021-08-31'
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [
|
||||||
|
{
|
||||||
|
'opportunity_owner':'Not Assigned',
|
||||||
|
'August': 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data,report[1])
|
||||||
|
|
||||||
|
def create_company():
|
||||||
|
doc = frappe.db.exists('Company','Best Test')
|
||||||
|
if not doc:
|
||||||
|
doc = frappe.new_doc('Company')
|
||||||
|
doc.company_name = 'Best Test'
|
||||||
|
doc.default_currency = "INR"
|
||||||
|
doc.insert()
|
||||||
|
|
||||||
|
def create_customer():
|
||||||
|
doc = frappe.db.exists("Customer","_Test NC")
|
||||||
|
if not doc:
|
||||||
|
doc = frappe.new_doc("Customer")
|
||||||
|
doc.customer_name = '_Test NC'
|
||||||
|
doc.insert()
|
||||||
|
|
||||||
|
def create_opportunity():
|
||||||
|
doc = frappe.db.exists({"doctype":"Opportunity","party_name":"_Test NC"})
|
||||||
|
if not doc:
|
||||||
|
doc = frappe.new_doc("Opportunity")
|
||||||
|
doc.opportunity_from = "Customer"
|
||||||
|
customer_name = frappe.db.get_value("Customer",{"customer_name":'_Test NC'},['customer_name'])
|
||||||
|
doc.party_name = customer_name
|
||||||
|
doc.opportunity_amount = 150000
|
||||||
|
doc.source = "Cold Calling"
|
||||||
|
doc.currency = "INR"
|
||||||
|
doc.expected_closing = "2021-08-31"
|
||||||
|
doc.company = 'Best Test'
|
||||||
|
doc.insert()
|
@ -147,6 +147,24 @@
|
|||||||
"onboard": 1,
|
"onboard": 1,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 1,
|
||||||
|
"label": "Sales Pipeline Analytics",
|
||||||
|
"link_to": "Sales Pipeline Analytics",
|
||||||
|
"link_type": "Report",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 1,
|
||||||
|
"label": "Opportunity Summary by Sales Stage",
|
||||||
|
"link_to": "Opportunity Summary by Sales Stage",
|
||||||
|
"link_type": "Report",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"dependencies": "",
|
"dependencies": "",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@ -403,7 +421,7 @@
|
|||||||
"type": "Link"
|
"type": "Link"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2021-08-05 12:15:56.913091",
|
"modified": "2021-08-19 19:08:08.728876",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "CRM",
|
"name": "CRM",
|
||||||
|
@ -85,10 +85,8 @@ def add_bank_accounts(response, bank, company):
|
|||||||
if not acc_subtype:
|
if not acc_subtype:
|
||||||
add_account_subtype(account["subtype"])
|
add_account_subtype(account["subtype"])
|
||||||
|
|
||||||
existing_bank_account = frappe.db.exists("Bank Account", {
|
bank_account_name = "{} - {}".format(account["name"], bank["bank_name"])
|
||||||
'account_name': account["name"],
|
existing_bank_account = frappe.db.exists("Bank Account", bank_account_name)
|
||||||
'bank': bank["bank_name"]
|
|
||||||
})
|
|
||||||
|
|
||||||
if not existing_bank_account:
|
if not existing_bank_account:
|
||||||
try:
|
try:
|
||||||
@ -197,6 +195,7 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
|
|||||||
|
|
||||||
plaid = PlaidConnector(access_token)
|
plaid = PlaidConnector(access_token)
|
||||||
|
|
||||||
|
transactions = []
|
||||||
try:
|
try:
|
||||||
transactions = plaid.get_transactions(start_date=start_date, end_date=end_date, account_id=account_id)
|
transactions = plaid.get_transactions(start_date=start_date, end_date=end_date, account_id=account_id)
|
||||||
except ItemError as e:
|
except ItemError as e:
|
||||||
@ -205,7 +204,7 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
|
|||||||
msg += _("Please refresh or reset the Plaid linking of the Bank {}.").format(bank) + " "
|
msg += _("Please refresh or reset the Plaid linking of the Bank {}.").format(bank) + " "
|
||||||
frappe.log_error(msg, title=_("Plaid Link Refresh Required"))
|
frappe.log_error(msg, title=_("Plaid Link Refresh Required"))
|
||||||
|
|
||||||
return transactions or []
|
return transactions
|
||||||
|
|
||||||
|
|
||||||
def new_bank_transaction(transaction):
|
def new_bank_transaction(transaction):
|
||||||
|
@ -62,6 +62,7 @@ treeviews = ['Account', 'Cost Center', 'Warehouse', 'Item Group', 'Customer Grou
|
|||||||
# website
|
# website
|
||||||
update_website_context = ["erpnext.shopping_cart.utils.update_website_context", "erpnext.education.doctype.education_settings.education_settings.update_website_context"]
|
update_website_context = ["erpnext.shopping_cart.utils.update_website_context", "erpnext.education.doctype.education_settings.education_settings.update_website_context"]
|
||||||
my_account_context = "erpnext.shopping_cart.utils.update_my_account_context"
|
my_account_context = "erpnext.shopping_cart.utils.update_my_account_context"
|
||||||
|
webform_list_context = "erpnext.controllers.website_list_for_contact.get_webform_list_context"
|
||||||
|
|
||||||
calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday List", "Course Schedule"]
|
calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday List", "Course Schedule"]
|
||||||
|
|
||||||
@ -80,7 +81,7 @@ website_generators = ["Item Group", "Item", "BOM", "Sales Partner",
|
|||||||
"Job Opening", "Student Admission"]
|
"Job Opening", "Student Admission"]
|
||||||
|
|
||||||
website_context = {
|
website_context = {
|
||||||
"favicon": "/assets/erpnext/images/erpnext-favicon.svg",
|
"favicon": "/assets/erpnext/images/erpnext-favicon.svg",
|
||||||
"splash_image": "/assets/erpnext/images/erpnext-logo.svg"
|
"splash_image": "/assets/erpnext/images/erpnext-logo.svg"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,6 +275,7 @@ doc_events = {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Payment Entry": {
|
"Payment Entry": {
|
||||||
|
"validate": "erpnext.regional.india.utils.update_place_of_supply",
|
||||||
"on_submit": ["erpnext.regional.create_transaction_log", "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status", "erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
|
"on_submit": ["erpnext.regional.create_transaction_log", "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status", "erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
|
||||||
"on_trash": "erpnext.regional.check_deletion_permission"
|
"on_trash": "erpnext.regional.check_deletion_permission"
|
||||||
},
|
},
|
||||||
@ -303,6 +305,9 @@ doc_events = {
|
|||||||
},
|
},
|
||||||
"Company": {
|
"Company": {
|
||||||
"on_trash": "erpnext.regional.india.utils.delete_gst_settings_for_company"
|
"on_trash": "erpnext.regional.india.utils.delete_gst_settings_for_company"
|
||||||
|
},
|
||||||
|
"Integration Request": {
|
||||||
|
"validate": "erpnext.accounts.doctype.payment_request.payment_request.validate_payment"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -323,6 +328,7 @@ scheduler_events = {
|
|||||||
},
|
},
|
||||||
"all": [
|
"all": [
|
||||||
"erpnext.projects.doctype.project.project.project_status_update_reminder",
|
"erpnext.projects.doctype.project.project.project_status_update_reminder",
|
||||||
|
"erpnext.hr.doctype.interview.interview.send_interview_reminder",
|
||||||
"erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts"
|
"erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts"
|
||||||
],
|
],
|
||||||
"hourly": [
|
"hourly": [
|
||||||
@ -366,6 +372,7 @@ scheduler_events = {
|
|||||||
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
|
"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.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
|
||||||
"erpnext.non_profit.doctype.membership.membership.set_expired_status"
|
"erpnext.non_profit.doctype.membership.membership.set_expired_status"
|
||||||
|
"erpnext.hr.doctype.interview.interview.send_daily_feedback_reminder"
|
||||||
],
|
],
|
||||||
"daily_long": [
|
"daily_long": [
|
||||||
"erpnext.setup.doctype.email_digest.email_digest.send",
|
"erpnext.setup.doctype.email_digest.email_digest.send",
|
||||||
@ -420,7 +427,7 @@ accounting_dimension_doctypes = ["GL Entry", "Sales Invoice", "Purchase Invoice"
|
|||||||
"Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule",
|
"Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule",
|
||||||
"Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation",
|
"Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation",
|
||||||
"Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription",
|
"Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription",
|
||||||
"Subscription Plan"
|
"Subscription Plan", "POS Invoice", "POS Invoice Item"
|
||||||
]
|
]
|
||||||
|
|
||||||
regional_overrides = {
|
regional_overrides = {
|
||||||
|
@ -9,83 +9,86 @@ frappe.listview_settings['Attendance'] = {
|
|||||||
return [__(doc.status), "orange", "status,=," + doc.status];
|
return [__(doc.status), "orange", "status,=," + doc.status];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onload: function(list_view) {
|
onload: function(list_view) {
|
||||||
let me = this;
|
let me = this;
|
||||||
const months = moment.months()
|
const months = moment.months();
|
||||||
list_view.page.add_inner_button( __("Mark Attendance"), function() {
|
list_view.page.add_inner_button(__("Mark Attendance"), function() {
|
||||||
let dialog = new frappe.ui.Dialog({
|
let dialog = new frappe.ui.Dialog({
|
||||||
title: __("Mark Attendance"),
|
title: __("Mark Attendance"),
|
||||||
fields: [
|
fields: [{
|
||||||
{
|
fieldname: 'employee',
|
||||||
fieldname: 'employee',
|
label: __('For Employee'),
|
||||||
label: __('For Employee'),
|
fieldtype: 'Link',
|
||||||
fieldtype: 'Link',
|
options: 'Employee',
|
||||||
options: 'Employee',
|
get_query: () => {
|
||||||
get_query: () => {
|
return {query: "erpnext.controllers.queries.employee_query"};
|
||||||
return {query: "erpnext.controllers.queries.employee_query"}
|
},
|
||||||
},
|
reqd: 1,
|
||||||
reqd: 1,
|
onchange: function() {
|
||||||
onchange: function() {
|
dialog.set_df_property("unmarked_days", "hidden", 1);
|
||||||
dialog.set_df_property("unmarked_days", "hidden", 1);
|
dialog.set_df_property("status", "hidden", 1);
|
||||||
dialog.set_df_property("status", "hidden", 1);
|
dialog.set_df_property("month", "value", '');
|
||||||
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.set_df_property("unmarked_days", "options", []);
|
||||||
dialog.no_unmarked_days_left = false;
|
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",
|
label: __("Status"),
|
||||||
fieldname: "month",
|
fieldtype: "Select",
|
||||||
options: months,
|
fieldname: "status",
|
||||||
reqd: 1,
|
options: ["Present", "Absent", "Half Day", "Work From Home"],
|
||||||
onchange: function() {
|
hidden: 1,
|
||||||
if(dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
|
reqd: 1,
|
||||||
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: __("Unmarked Attendance for days"),
|
label: __("Unmarked Attendance for days"),
|
||||||
fieldname: "unmarked_days",
|
fieldname: "unmarked_days",
|
||||||
fieldtype: "MultiCheck",
|
fieldtype: "MultiCheck",
|
||||||
options: [],
|
options: [],
|
||||||
columns: 2,
|
columns: 2,
|
||||||
hidden: 1
|
hidden: 1
|
||||||
},
|
}],
|
||||||
],
|
primary_action(data) {
|
||||||
primary_action(data) {
|
|
||||||
if (cur_dialog.no_unmarked_days_left) {
|
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 {
|
} 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({
|
frappe.call({
|
||||||
method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance",
|
method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance",
|
||||||
args: {
|
args: {
|
||||||
data: data
|
data: data
|
||||||
},
|
},
|
||||||
callback: function(r) {
|
callback: function (r) {
|
||||||
if (r.message === 1) {
|
if (r.message === 1) {
|
||||||
frappe.show_alert({message: __("Attendance Marked"), indicator: 'blue'});
|
frappe.show_alert({
|
||||||
|
message: __("Attendance Marked"),
|
||||||
|
indicator: 'blue'
|
||||||
|
});
|
||||||
cur_dialog.hide();
|
cur_dialog.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -101,21 +104,26 @@ frappe.listview_settings['Attendance'] = {
|
|||||||
dialog.show();
|
dialog.show();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
get_multi_select_options: function(employee, month){
|
|
||||||
|
get_multi_select_options: function(employee, month) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days',
|
method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days',
|
||||||
async: false,
|
async: false,
|
||||||
args:{
|
args: {
|
||||||
employee: employee,
|
employee: employee,
|
||||||
month: month,
|
month: month,
|
||||||
}
|
}
|
||||||
}).then(r => {
|
}).then(r => {
|
||||||
var options = [];
|
var options = [];
|
||||||
for(var d in r.message){
|
for (var d in r.message) {
|
||||||
var momentObj = moment(r.message[d], 'YYYY-MM-DD');
|
var momentObj = moment(r.message[d], 'YYYY-MM-DD');
|
||||||
var date = momentObj.format('DD-MM-YYYY');
|
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);
|
resolve(options);
|
||||||
});
|
});
|
||||||
|
@ -15,19 +15,20 @@ erpnext.hr.EmployeeController = class EmployeeController extends frappe.ui.form.
|
|||||||
}
|
}
|
||||||
|
|
||||||
refresh() {
|
refresh() {
|
||||||
var me = this;
|
|
||||||
erpnext.toggle_naming_series();
|
erpnext.toggle_naming_series();
|
||||||
}
|
}
|
||||||
|
|
||||||
date_of_birth() {
|
date_of_birth() {
|
||||||
return cur_frm.call({
|
return cur_frm.call({
|
||||||
method: "get_retirement_date",
|
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() {
|
salutation() {
|
||||||
if(this.frm.doc.salutation) {
|
if (this.frm.doc.salutation) {
|
||||||
this.frm.set_value("gender", {
|
this.frm.set_value("gender", {
|
||||||
"Mr": "Male",
|
"Mr": "Male",
|
||||||
"Ms": "Female"
|
"Ms": "Female"
|
||||||
@ -36,8 +37,9 @@ erpnext.hr.EmployeeController = class EmployeeController extends frappe.ui.form.
|
|||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
frappe.ui.form.on('Employee',{
|
|
||||||
setup: function(frm) {
|
frappe.ui.form.on('Employee', {
|
||||||
|
setup: function (frm) {
|
||||||
frm.set_query("leave_policy", function() {
|
frm.set_query("leave_policy", function() {
|
||||||
return {
|
return {
|
||||||
"filters": {
|
"filters": {
|
||||||
@ -46,7 +48,7 @@ frappe.ui.form.on('Employee',{
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onload:function(frm) {
|
onload: function (frm) {
|
||||||
frm.set_query("department", function() {
|
frm.set_query("department", function() {
|
||||||
return {
|
return {
|
||||||
"filters": {
|
"filters": {
|
||||||
@ -55,23 +57,28 @@ frappe.ui.form.on('Employee',{
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
prefered_contact_email:function(frm){
|
prefered_contact_email: function(frm) {
|
||||||
frm.events.update_contact(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';
|
var prefered_email_fieldname = frappe.model.scrub(frm.doc.prefered_contact_email) || 'user_id';
|
||||||
frm.set_value("prefered_email",
|
frm.set_value("prefered_email",
|
||||||
frm.fields_dict[prefered_email_fieldname].value)
|
frm.fields_dict[prefered_email_fieldname].value);
|
||||||
},
|
},
|
||||||
|
|
||||||
status: function(frm) {
|
status: function(frm) {
|
||||||
return frm.call({
|
return frm.call({
|
||||||
method: "deactivate_sales_person",
|
method: "deactivate_sales_person",
|
||||||
@ -81,19 +88,63 @@ frappe.ui.form.on('Employee',{
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
create_user: function(frm) {
|
create_user: function(frm) {
|
||||||
if (!frm.doc.prefered_email)
|
if (!frm.doc.prefered_email) {
|
||||||
{
|
frappe.throw(__("Please enter Preferred Contact Email"));
|
||||||
frappe.throw(__("Please enter Preferred Contact Email"))
|
|
||||||
}
|
}
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.hr.doctype.employee.employee.create_user",
|
method: "erpnext.hr.doctype.employee.employee.create_user",
|
||||||
args: { employee: frm.doc.name, email: frm.doc.prefered_email },
|
args: {
|
||||||
callback: function(r)
|
employee: frm.doc.name,
|
||||||
{
|
email: frm.doc.prefered_email
|
||||||
frm.set_value("user_id", r.message)
|
},
|
||||||
|
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")
|
||||||
|
},
|
||||||
|
];
|
||||||
|
@ -13,7 +13,7 @@ from erpnext.hr.utils import get_holidays_for_employee
|
|||||||
# HOLIDAY REMINDERS
|
# HOLIDAY REMINDERS
|
||||||
# -----------------
|
# -----------------
|
||||||
def send_reminders_in_advance_weekly():
|
def send_reminders_in_advance_weekly():
|
||||||
to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders") or 1)
|
to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders"))
|
||||||
frequency = frappe.db.get_single_value("HR Settings", "frequency")
|
frequency = frappe.db.get_single_value("HR Settings", "frequency")
|
||||||
if not (to_send_in_advance and frequency == "Weekly"):
|
if not (to_send_in_advance and frequency == "Weekly"):
|
||||||
return
|
return
|
||||||
@ -21,7 +21,7 @@ def send_reminders_in_advance_weekly():
|
|||||||
send_advance_holiday_reminders("Weekly")
|
send_advance_holiday_reminders("Weekly")
|
||||||
|
|
||||||
def send_reminders_in_advance_monthly():
|
def send_reminders_in_advance_monthly():
|
||||||
to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders") or 1)
|
to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders"))
|
||||||
frequency = frappe.db.get_single_value("HR Settings", "frequency")
|
frequency = frappe.db.get_single_value("HR Settings", "frequency")
|
||||||
if not (to_send_in_advance and frequency == "Monthly"):
|
if not (to_send_in_advance and frequency == "Monthly"):
|
||||||
return
|
return
|
||||||
@ -79,7 +79,7 @@ def send_holidays_reminder_in_advance(employee, holidays):
|
|||||||
# ------------------
|
# ------------------
|
||||||
def send_birthday_reminders():
|
def send_birthday_reminders():
|
||||||
"""Send Employee birthday reminders if no 'Stop Birthday Reminders' is not set."""
|
"""Send Employee birthday reminders if no 'Stop Birthday Reminders' is not set."""
|
||||||
to_send = int(frappe.db.get_single_value("HR Settings", "send_birthday_reminders") or 1)
|
to_send = int(frappe.db.get_single_value("HR Settings", "send_birthday_reminders"))
|
||||||
if not to_send:
|
if not to_send:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -184,7 +184,7 @@ def get_employees_having_an_event_today(event_type):
|
|||||||
# --------------------------
|
# --------------------------
|
||||||
def send_work_anniversary_reminders():
|
def send_work_anniversary_reminders():
|
||||||
"""Send Employee Work Anniversary Reminders if 'Send Work Anniversary Reminders' is checked"""
|
"""Send Employee Work Anniversary Reminders if 'Send Work Anniversary Reminders' is checked"""
|
||||||
to_send = int(frappe.db.get_single_value("HR Settings", "send_work_anniversary_reminders") or 1)
|
to_send = int(frappe.db.get_single_value("HR Settings", "send_work_anniversary_reminders"))
|
||||||
if not to_send:
|
if not to_send:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ frappe.ui.form.on('Employee Advance', {
|
|||||||
frm.trigger('make_return_entry');
|
frm.trigger('make_return_entry');
|
||||||
}, __('Create'));
|
}, __('Create'));
|
||||||
} else if (frm.doc.repay_unclaimed_amount_from_salary == 1 && frappe.model.can_create("Additional Salary")) {
|
} else if (frm.doc.repay_unclaimed_amount_from_salary == 1 && frappe.model.can_create("Additional Salary")) {
|
||||||
frm.add_custom_button(__("Deduction from salary"), function() {
|
frm.add_custom_button(__("Deduction from Salary"), function() {
|
||||||
frm.events.make_deduction_via_additional_salary(frm);
|
frm.events.make_deduction_via_additional_salary(frm);
|
||||||
}, __('Create'));
|
}, __('Create'));
|
||||||
}
|
}
|
||||||
|
@ -170,7 +170,7 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "repay_unclaimed_amount_from_salary",
|
"fieldname": "repay_unclaimed_amount_from_salary",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Repay unclaimed amount from salary"
|
"label": "Repay Unclaimed Amount from Salary"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:cur_frm.doc.employee",
|
"depends_on": "eval:cur_frm.doc.employee",
|
||||||
@ -200,10 +200,11 @@
|
|||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-03-31 22:31:53.746659",
|
"modified": "2021-09-11 18:38:38.617478",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Employee Advance",
|
"name": "Employee Advance",
|
||||||
|
"naming_rule": "By \"Naming Series\" field",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
@ -172,7 +172,10 @@ def get_paying_amount_paying_exchange_rate(payment_account, doc):
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_return_through_additional_salary(doc):
|
def create_return_through_additional_salary(doc):
|
||||||
import json
|
import json
|
||||||
doc = frappe._dict(json.loads(doc))
|
|
||||||
|
if isinstance(doc, str):
|
||||||
|
doc = frappe._dict(json.loads(doc))
|
||||||
|
|
||||||
additional_salary = frappe.new_doc('Additional Salary')
|
additional_salary = frappe.new_doc('Additional Salary')
|
||||||
additional_salary.employee = doc.employee
|
additional_salary.employee = doc.employee
|
||||||
additional_salary.currency = doc.currency
|
additional_salary.currency = doc.currency
|
||||||
|
@ -12,8 +12,11 @@ import erpnext
|
|||||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||||
from erpnext.hr.doctype.employee_advance.employee_advance import (
|
from erpnext.hr.doctype.employee_advance.employee_advance import (
|
||||||
EmployeeAdvanceOverPayment,
|
EmployeeAdvanceOverPayment,
|
||||||
|
create_return_through_additional_salary,
|
||||||
make_bank_entry,
|
make_bank_entry,
|
||||||
)
|
)
|
||||||
|
from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component
|
||||||
|
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||||
|
|
||||||
|
|
||||||
class TestEmployeeAdvance(unittest.TestCase):
|
class TestEmployeeAdvance(unittest.TestCase):
|
||||||
@ -33,6 +36,46 @@ class TestEmployeeAdvance(unittest.TestCase):
|
|||||||
journal_entry1 = make_payment_entry(advance)
|
journal_entry1 = make_payment_entry(advance)
|
||||||
self.assertRaises(EmployeeAdvanceOverPayment, journal_entry1.submit)
|
self.assertRaises(EmployeeAdvanceOverPayment, journal_entry1.submit)
|
||||||
|
|
||||||
|
def test_repay_unclaimed_amount_from_salary(self):
|
||||||
|
employee_name = make_employee("_T@employe.advance")
|
||||||
|
advance = make_employee_advance(employee_name, {"repay_unclaimed_amount_from_salary": 1})
|
||||||
|
|
||||||
|
args = {"type": "Deduction"}
|
||||||
|
create_salary_component("Advance Salary - Deduction", **args)
|
||||||
|
make_salary_structure("Test Additional Salary for Advance Return", "Monthly", employee=employee_name)
|
||||||
|
|
||||||
|
# additional salary for 700 first
|
||||||
|
advance.reload()
|
||||||
|
additional_salary = create_return_through_additional_salary(advance)
|
||||||
|
additional_salary.salary_component = "Advance Salary - Deduction"
|
||||||
|
additional_salary.payroll_date = nowdate()
|
||||||
|
additional_salary.amount = 700
|
||||||
|
additional_salary.insert()
|
||||||
|
additional_salary.submit()
|
||||||
|
|
||||||
|
advance.reload()
|
||||||
|
self.assertEqual(advance.return_amount, 700)
|
||||||
|
|
||||||
|
# additional salary for remaining 300
|
||||||
|
additional_salary = create_return_through_additional_salary(advance)
|
||||||
|
additional_salary.salary_component = "Advance Salary - Deduction"
|
||||||
|
additional_salary.payroll_date = nowdate()
|
||||||
|
additional_salary.amount = 300
|
||||||
|
additional_salary.insert()
|
||||||
|
additional_salary.submit()
|
||||||
|
|
||||||
|
advance.reload()
|
||||||
|
self.assertEqual(advance.return_amount, 1000)
|
||||||
|
|
||||||
|
# update advance return amount on additional salary cancellation
|
||||||
|
additional_salary.cancel()
|
||||||
|
advance.reload()
|
||||||
|
self.assertEqual(advance.return_amount, 700)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
|
||||||
def make_payment_entry(advance):
|
def make_payment_entry(advance):
|
||||||
journal_entry = frappe.get_doc(make_bank_entry("Employee Advance", advance.name))
|
journal_entry = frappe.get_doc(make_bank_entry("Employee Advance", advance.name))
|
||||||
journal_entry.cheque_no = "123123"
|
journal_entry.cheque_no = "123123"
|
||||||
@ -41,7 +84,7 @@ def make_payment_entry(advance):
|
|||||||
|
|
||||||
return journal_entry
|
return journal_entry
|
||||||
|
|
||||||
def make_employee_advance(employee_name):
|
def make_employee_advance(employee_name, args=None):
|
||||||
doc = frappe.new_doc("Employee Advance")
|
doc = frappe.new_doc("Employee Advance")
|
||||||
doc.employee = employee_name
|
doc.employee = employee_name
|
||||||
doc.company = "_Test company"
|
doc.company = "_Test company"
|
||||||
@ -51,6 +94,10 @@ def make_employee_advance(employee_name):
|
|||||||
doc.advance_amount = 1000
|
doc.advance_amount = 1000
|
||||||
doc.posting_date = nowdate()
|
doc.posting_date = nowdate()
|
||||||
doc.advance_account = "_Test Employee Advance - _TC"
|
doc.advance_account = "_Test Employee Advance - _TC"
|
||||||
|
|
||||||
|
if args:
|
||||||
|
doc.update(args)
|
||||||
|
|
||||||
doc.insert()
|
doc.insert()
|
||||||
doc.submit()
|
doc.submit()
|
||||||
|
|
||||||
|
@ -55,8 +55,7 @@ def mark_employee_attendance(employee_list, status, date, leave_type=None, compa
|
|||||||
else:
|
else:
|
||||||
leave_type = None
|
leave_type = None
|
||||||
|
|
||||||
if not company:
|
company = frappe.db.get_value("Employee", employee['employee'], "Company", cache=True)
|
||||||
company = frappe.db.get_value("Employee", employee['employee'], "Company")
|
|
||||||
|
|
||||||
attendance=frappe.get_doc(dict(
|
attendance=frappe.get_doc(dict(
|
||||||
doctype='Attendance',
|
doctype='Attendance',
|
||||||
|
@ -71,6 +71,7 @@ def get_job_applicant():
|
|||||||
applicant = frappe.new_doc('Job Applicant')
|
applicant = frappe.new_doc('Job Applicant')
|
||||||
applicant.applicant_name = 'Test Researcher'
|
applicant.applicant_name = 'Test Researcher'
|
||||||
applicant.email_id = 'test@researcher.com'
|
applicant.email_id = 'test@researcher.com'
|
||||||
|
applicant.designation = 'Researcher'
|
||||||
applicant.status = 'Open'
|
applicant.status = 'Open'
|
||||||
applicant.cover_letter = 'I am a great Researcher.'
|
applicant.cover_letter = 'I am a great Researcher.'
|
||||||
applicant.insert()
|
applicant.insert()
|
||||||
|
@ -38,8 +38,10 @@ def create_job_applicant(source_name, target_doc=None):
|
|||||||
status = "Open"
|
status = "Open"
|
||||||
|
|
||||||
job_applicant = frappe.new_doc("Job Applicant")
|
job_applicant = frappe.new_doc("Job Applicant")
|
||||||
|
job_applicant.source = "Employee Referral"
|
||||||
job_applicant.employee_referral = emp_ref.name
|
job_applicant.employee_referral = emp_ref.name
|
||||||
job_applicant.status = status
|
job_applicant.status = status
|
||||||
|
job_applicant.designation = emp_ref.for_designation
|
||||||
job_applicant.applicant_name = emp_ref.full_name
|
job_applicant.applicant_name = emp_ref.full_name
|
||||||
job_applicant.email_id = emp_ref.email
|
job_applicant.email_id = emp_ref.email
|
||||||
job_applicant.phone_number = emp_ref.contact_no
|
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):
|
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):
|
def test_workflow_and_status_sync(self):
|
||||||
emp_ref = create_employee_referral()
|
emp_ref = create_employee_referral()
|
||||||
|
|
||||||
@ -50,6 +55,10 @@ class TestEmployeeReferral(unittest.TestCase):
|
|||||||
add_sal = create_additional_salary(emp_ref)
|
add_sal = create_additional_salary(emp_ref)
|
||||||
self.assertTrue(add_sal.ref_docname, emp_ref.name)
|
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():
|
def create_employee_referral():
|
||||||
emp_ref = frappe.new_doc("Employee Referral")
|
emp_ref = frappe.new_doc("Employee Referral")
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user