mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-19 04:59:18 +00:00
Merge branch 'version-13' of https://github.com/frappe/erpnext into enterprise-hotfix
This commit is contained in:
6
.github/helper/documentation.py
vendored
6
.github/helper/documentation.py
vendored
@@ -34,9 +34,9 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
if response.ok:
|
if response.ok:
|
||||||
payload = response.json()
|
payload = response.json()
|
||||||
title = payload.get("title", "").lower().strip()
|
title = (payload.get("title") or "").lower().strip()
|
||||||
head_sha = payload.get("head", {}).get("sha")
|
head_sha = (payload.get("head") or {}).get("sha")
|
||||||
body = payload.get("body", "").lower()
|
body = (payload.get("body") or "").lower()
|
||||||
|
|
||||||
if (title.startswith("feat")
|
if (title.startswith("feat")
|
||||||
and head_sha
|
and head_sha
|
||||||
|
|||||||
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'
|
|
||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import frappe
|
|||||||
|
|
||||||
from erpnext.hooks import regional_overrides
|
from erpnext.hooks import regional_overrides
|
||||||
|
|
||||||
__version__ = '13.11.1'
|
__version__ = '13.12.1'
|
||||||
|
|
||||||
def get_default_company(user=None):
|
def get_default_company(user=None):
|
||||||
'''Get default company for user'''
|
'''Get default company for user'''
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -10,13 +10,17 @@ frappe.ui.form.on('Chart of Accounts Importer', {
|
|||||||
// make company mandatory
|
// make company mandatory
|
||||||
frm.set_df_property('company', 'reqd', frm.doc.company ? 0 : 1);
|
frm.set_df_property('company', 'reqd', frm.doc.company ? 0 : 1);
|
||||||
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);
|
||||||
|
|
||||||
|
if (frm.doc.import_file) {
|
||||||
|
frappe.run_serially([
|
||||||
|
() => generate_tree_preview(frm),
|
||||||
|
() => create_import_button(frm),
|
||||||
|
() => frm.set_df_property('chart_preview', 'hidden', 0)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -77,9 +81,6 @@ frappe.ui.form.on('Chart of Accounts Importer', {
|
|||||||
if (!frm.doc.import_file) {
|
if (!frm.doc.import_file) {
|
||||||
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 {
|
|
||||||
generate_tree_preview(frm);
|
|
||||||
validate_csv_data(frm);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -104,26 +105,9 @@ 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 () {
|
frm.page.set_primary_action(__("Import"), function () {
|
||||||
frappe.call({
|
return frappe.call({
|
||||||
method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.import_coa",
|
method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.import_coa",
|
||||||
args: {
|
args: {
|
||||||
file_name: frm.doc.import_file,
|
file_name: frm.doc.import_file,
|
||||||
@@ -132,7 +116,7 @@ var create_import_button = function(frm) {
|
|||||||
freeze: true,
|
freeze: true,
|
||||||
freeze_message: __("Creating Accounts..."),
|
freeze_message: __("Creating Accounts..."),
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
if(!r.exc) {
|
if (!r.exc) {
|
||||||
clearInterval(frm.page["interval"]);
|
clearInterval(frm.page["interval"]);
|
||||||
frm.page.set_indicator(__('Import Successful'), 'blue');
|
frm.page.set_indicator(__('Import Successful'), 'blue');
|
||||||
create_reset_button(frm);
|
create_reset_button(frm);
|
||||||
@@ -150,12 +134,33 @@ var create_reset_button = function(frm) {
|
|||||||
}).addClass('btn btn-primary');
|
}).addClass('btn btn-primary');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var validate_coa = function(frm) {
|
||||||
|
if (frm.doc.import_file) {
|
||||||
|
let parent = __('All Accounts');
|
||||||
|
return frappe.call({
|
||||||
|
'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,
|
||||||
|
for_validate: 1
|
||||||
|
},
|
||||||
|
callback: function(r) {
|
||||||
|
if (r.message['show_import_button']) {
|
||||||
|
frm.page['show_import_button'] = Boolean(r.message['show_import_button']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var generate_tree_preview = function(frm) {
|
var generate_tree_preview = function(frm) {
|
||||||
let parent = __('All Accounts');
|
let parent = __('All Accounts');
|
||||||
$(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper to load new data
|
$(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper to load new data
|
||||||
|
|
||||||
// generate tree structure based on the csv data
|
// generate tree structure based on the csv data
|
||||||
new frappe.ui.Tree({
|
return new frappe.ui.Tree({
|
||||||
parent: $(frm.fields_dict['chart_tree'].wrapper),
|
parent: $(frm.fields_dict['chart_tree'].wrapper),
|
||||||
label: parent,
|
label: parent,
|
||||||
expandable: true,
|
expandable: true,
|
||||||
|
|||||||
@@ -26,7 +26,18 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import
|
|||||||
|
|
||||||
class ChartofAccountsImporter(Document):
|
class ChartofAccountsImporter(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
validate_accounts(self.import_file)
|
if self.import_file:
|
||||||
|
get_coa('Chart of Accounts Importer', 'All Accounts', file_name=self.import_file, for_validate=1)
|
||||||
|
|
||||||
|
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'),
|
||||||
|
title=(_("Wrong Template")))
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def validate_company(company):
|
def validate_company(company):
|
||||||
@@ -56,6 +67,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 +132,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 +143,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 +314,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 +333,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 +345,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 +383,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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
@@ -128,6 +136,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,\
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
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,
|
||||||
@@ -1109,6 +1110,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:
|
||||||
@@ -1139,10 +1146,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:
|
||||||
@@ -1150,9 +1154,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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -1652,7 +1652,7 @@
|
|||||||
"label": "Status",
|
"label": "Status",
|
||||||
"length": 30,
|
"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
|
||||||
},
|
},
|
||||||
@@ -1954,6 +1954,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
|
||||||
@@ -2032,11 +2033,12 @@
|
|||||||
"link_fieldname": "consolidated_invoice"
|
"link_fieldname": "consolidated_invoice"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2021-09-08 15:24:25.486499",
|
"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": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -230,9 +230,6 @@ class SalesInvoice(SellingController):
|
|||||||
# this sequence because outstanding may get -ve
|
# this sequence because outstanding may get -ve
|
||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
|
|
||||||
if self.update_stock == 1:
|
|
||||||
self.repost_future_sle_and_gle()
|
|
||||||
|
|
||||||
if self.update_stock == 1:
|
if self.update_stock == 1:
|
||||||
self.repost_future_sle_and_gle()
|
self.repost_future_sle_and_gle()
|
||||||
|
|
||||||
@@ -1472,14 +1469,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:
|
||||||
@@ -1487,15 +1477,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:
|
||||||
@@ -1504,12 +1492,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"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -131,6 +131,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()
|
||||||
@@ -154,6 +155,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)
|
||||||
@@ -1665,6 +1667,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,
|
||||||
@@ -1816,6 +1819,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")
|
||||||
@@ -1915,11 +1959,7 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
self.assertEqual(target_doc.company, "_Test Company 1")
|
self.assertEqual(target_doc.company, "_Test Company 1")
|
||||||
self.assertEqual(target_doc.supplier, "_Test Internal Supplier")
|
self.assertEqual(target_doc.supplier, "_Test Internal Supplier")
|
||||||
|
|
||||||
def test_sle_if_target_warehouse_exists_accidentally(self):
|
def test_sle_for_target_warehouse(self):
|
||||||
"""
|
|
||||||
Check if inward entry exists if Target Warehouse accidentally exists
|
|
||||||
but Customer is not an internal customer.
|
|
||||||
"""
|
|
||||||
se = make_stock_entry(
|
se = make_stock_entry(
|
||||||
item_code="138-CMS Shoe",
|
item_code="138-CMS Shoe",
|
||||||
target="Finished Goods - _TC",
|
target="Finished Goods - _TC",
|
||||||
@@ -1940,9 +1980,9 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
sles = frappe.get_all("Stock Ledger Entry", filters={"voucher_no": si.name},
|
sles = frappe.get_all("Stock Ledger Entry", filters={"voucher_no": si.name},
|
||||||
fields=["name", "actual_qty"])
|
fields=["name", "actual_qty"])
|
||||||
|
|
||||||
# check if only one SLE for outward entry is created
|
# check if both SLEs are created
|
||||||
self.assertEqual(len(sles), 1)
|
self.assertEqual(len(sles), 2)
|
||||||
self.assertEqual(sles[0].actual_qty, -1)
|
self.assertEqual(sum(d.actual_qty for d in sles), 0.0)
|
||||||
|
|
||||||
# tear down
|
# tear down
|
||||||
si.cancel()
|
si.cancel()
|
||||||
@@ -2253,6 +2293,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-.#####'
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ def get_tax_withholding_details(tax_withholding_category, posting_date, company)
|
|||||||
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,
|
"from_date": tax_rate_detail.from_date,
|
||||||
@@ -206,18 +207,39 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
|||||||
|
|
||||||
def get_invoice_vouchers(parties, tax_details, 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)],
|
'posting_date': ['between', (tax_details.from_date, tax_details.to_date)],
|
||||||
'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')
|
||||||
|
})
|
||||||
|
|
||||||
|
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'):
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -390,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()
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,11 +140,6 @@ class Asset(AccountsController):
|
|||||||
if self.is_existing_asset:
|
if self.is_existing_asset:
|
||||||
return
|
return
|
||||||
|
|
||||||
docname = self.purchase_receipt or self.purchase_invoice
|
|
||||||
if docname:
|
|
||||||
doctype = 'Purchase Receipt' if self.purchase_receipt else 'Purchase Invoice'
|
|
||||||
date = frappe.db.get_value(doctype, docname, 'posting_date')
|
|
||||||
|
|
||||||
if self.available_for_use_date and getdate(self.available_for_use_date) < getdate(self.purchase_date):
|
if self.available_for_use_date and getdate(self.available_for_use_date) < getdate(self.purchase_date):
|
||||||
frappe.throw(_("Available-for-use Date should be after purchase date"))
|
frappe.throw(_("Available-for-use Date should be after purchase date"))
|
||||||
|
|
||||||
@@ -394,10 +389,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))
|
||||||
@@ -444,9 +435,10 @@ class Asset(AccountsController):
|
|||||||
if accumulated_depreciation_after_full_schedule:
|
if accumulated_depreciation_after_full_schedule:
|
||||||
accumulated_depreciation_after_full_schedule = max(accumulated_depreciation_after_full_schedule)
|
accumulated_depreciation_after_full_schedule = max(accumulated_depreciation_after_full_schedule)
|
||||||
|
|
||||||
asset_value_after_full_schedule = flt(flt(self.gross_purchase_amount) -
|
asset_value_after_full_schedule = flt(
|
||||||
flt(accumulated_depreciation_after_full_schedule),
|
flt(self.gross_purchase_amount) -
|
||||||
self.precision('gross_purchase_amount'))
|
flt(self.opening_accumulated_depreciation) -
|
||||||
|
flt(accumulated_depreciation_after_full_schedule), self.precision('gross_purchase_amount'))
|
||||||
|
|
||||||
if (row.expected_value_after_useful_life and
|
if (row.expected_value_after_useful_life and
|
||||||
row.expected_value_after_useful_life < asset_value_after_full_schedule):
|
row.expected_value_after_useful_life < asset_value_after_full_schedule):
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
{
|
|
||||||
"actions": [],
|
|
||||||
"creation": "2021-05-07 18:16:40.621421",
|
|
||||||
"doctype": "DocType",
|
|
||||||
"editable_grid": 1,
|
|
||||||
"engine": "InnoDB",
|
|
||||||
"field_order": [
|
|
||||||
"supplier",
|
|
||||||
"item_group"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldname": "supplier",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Supplier",
|
|
||||||
"options": "Supplier",
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "item_group",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Item Group",
|
|
||||||
"options": "Item Group",
|
|
||||||
"reqd": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"index_web_pages_for_search": 1,
|
|
||||||
"links": [],
|
|
||||||
"modified": "2021-05-19 13:48:16.742303",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "Buying",
|
|
||||||
"name": "Supplier Item Group",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [
|
|
||||||
{
|
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "System Manager",
|
|
||||||
"share": 1,
|
|
||||||
"write": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "Purchase User",
|
|
||||||
"share": 1,
|
|
||||||
"write": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "Purchase Manager",
|
|
||||||
"share": 1,
|
|
||||||
"write": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sort_field": "modified",
|
|
||||||
"sort_order": "DESC",
|
|
||||||
"track_changes": 1
|
|
||||||
}
|
|
||||||
@@ -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"):
|
||||||
|
|||||||
43
erpnext/change_log/v13/v13_12_0.md
Normal file
43
erpnext/change_log/v13/v13_12_0.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Version 13.12.0 Release Notes
|
||||||
|
|
||||||
|
### Features & Enhancements
|
||||||
|
- Merge POS invoices based on customer group ([#27471](https://github.com/frappe/erpnext/pull/27471))
|
||||||
|
|
||||||
|
- Get items from material request in purchase order ([#24725](https://github.com/frappe/erpnext/pull/24725))
|
||||||
|
- Earlier system was fetching all the items from material request to purchase order
|
||||||
|
- Now user can fetch the specific items from material request to purchase order
|
||||||
|
|
||||||
|
- Validity dates in Tax Withholding Rates ([#27258](https://github.com/frappe/erpnext/pull/27258))
|
||||||
|
- Replaced fiscal year with From Date and To Date, to start the TDS effect from the mid of the fiscal year.
|
||||||
|
|
||||||
|
- Toggle for reduced depreciation rate as per IT Act ([#27600](https://github.com/frappe/erpnext/pull/27600))
|
||||||
|
- Added a toggle in the Finance Book to enable/disable the automatic reduction in the depreciation rate.
|
||||||
|
|
||||||
|
- Deduct the TDS using Journal Entry ([#27451](https://github.com/frappe/erpnext/pull/27451))
|
||||||
|
- Refactored TDS payable monthly report to show the TDS data which was created using Journal Entry.
|
||||||
|
|
||||||
|
- Party specific item ([#27281](https://github.com/frappe/erpnext/pull/27281))
|
||||||
|
- User can set the specific items to the supplier
|
||||||
|
- While making purchase transactions, user can see the items which are linked to the respective supplier.
|
||||||
|
|
||||||
|
- Provision to add scrap items in job card ([#27483](https://github.com/frappe/erpnext/pull/27483))
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Maintain same rate in Stock Ledger until stock become positive ([#27227](https://github.com/frappe/erpnext/pull/27227))
|
||||||
|
- Duplicate Contact error on add Patient ([#27427](https://github.com/frappe/erpnext/pull/27427))
|
||||||
|
- Distribution of additional costs in Manufacture Stock Entry ([#27629](https://github.com/frappe/erpnext/pull/27629))
|
||||||
|
- Website Items with same Item name unhandled, thumbnails missing ([#27720](https://github.com/frappe/erpnext/pull/27720))
|
||||||
|
- Delivery Note for transfer w/o internal customer ([#27798](https://github.com/frappe/erpnext/pull/27798))
|
||||||
|
- Setting of gain/loss if party account is in company currency ([#27659](https://github.com/frappe/erpnext/pull/27659))
|
||||||
|
- Check if doctype has company_address field before setting the value ([#27441](https://github.com/frappe/erpnext/pull/27441))
|
||||||
|
- Removed b2c limit check from CDNR Invoices ([#27516](https://github.com/frappe/erpnext/pull/27516))
|
||||||
|
- Cannot delete a project if linked with sales order ([#27536](https://github.com/frappe/erpnext/pull/27536))
|
||||||
|
- Employee advance return through multiple additional salaries ([#27438](https://github.com/frappe/erpnext/pull/27438))
|
||||||
|
- Improvements in COA Importer ([#27584](https://github.com/frappe/erpnext/pull/27584))
|
||||||
|
- Validate if item exists on uploading items in stock reco ([#27543](https://github.com/frappe/erpnext/pull/27543))
|
||||||
|
- Handle Excess/Multiple Item Transfer against Job Card ([#27486](https://github.com/frappe/erpnext/pull/27486))
|
||||||
|
- Values with same account name and different account number in consolidated balance sheet report ([#27493](https://github.com/frappe/erpnext/pull/27493))
|
||||||
|
- Added project name in the purchase order analysis ([#27701](https://github.com/frappe/erpnext/pull/27701))
|
||||||
|
- Shopping Cart and Variant Selection ([#27508](https://github.com/frappe/erpnext/pull/27508))
|
||||||
|
|
||||||
@@ -690,13 +690,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
|
||||||
@@ -715,7 +719,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:
|
||||||
@@ -734,7 +738,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)
|
||||||
)
|
)
|
||||||
@@ -985,42 +989,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")
|
||||||
@@ -1673,14 +1690,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")))
|
||||||
|
|
||||||
|
|||||||
@@ -424,7 +424,7 @@ class SellingController(StockController):
|
|||||||
or (cint(self.is_return) and self.docstatus==2)):
|
or (cint(self.is_return) and self.docstatus==2)):
|
||||||
sl_entries.append(self.get_sle_for_source_warehouse(d))
|
sl_entries.append(self.get_sle_for_source_warehouse(d))
|
||||||
|
|
||||||
if d.target_warehouse and self.get("is_internal_customer"):
|
if d.target_warehouse:
|
||||||
sl_entries.append(self.get_sle_for_target_warehouse(d))
|
sl_entries.append(self.get_sle_for_target_warehouse(d))
|
||||||
|
|
||||||
if d.warehouse and ((not cint(self.is_return) and self.docstatus==2)
|
if d.warehouse and ((not cint(self.is_return) and self.docstatus==2)
|
||||||
@@ -559,6 +559,12 @@ class SellingController(StockController):
|
|||||||
frappe.throw(_("Row {0}: Delivery Warehouse ({1}) and Customer Warehouse ({2}) can not be same")
|
frappe.throw(_("Row {0}: Delivery Warehouse ({1}) and Customer Warehouse ({2}) can not be same")
|
||||||
.format(d.idx, warehouse, warehouse))
|
.format(d.idx, warehouse, warehouse))
|
||||||
|
|
||||||
|
if not self.get("is_internal_customer") and any(d.get("target_warehouse") for d in items):
|
||||||
|
msg = _("Target Warehouse is set for some items but the customer is not an internal customer.")
|
||||||
|
msg += " " + _("This {} will be treated as material transfer.").format(_(self.doctype))
|
||||||
|
frappe.msgprint(msg, title="Internal Transfer", alert=True)
|
||||||
|
|
||||||
|
|
||||||
def validate_items(self):
|
def validate_items(self):
|
||||||
# validate items to see if they have is_sales_item enabled
|
# validate items to see if they have is_sales_item enabled
|
||||||
from erpnext.controllers.buying_controller import validate_item_type
|
from erpnext.controllers.buying_controller import validate_item_type
|
||||||
|
|||||||
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,
|
||||||
|
|||||||
@@ -33,6 +33,16 @@ class WebsiteItem(WebsiteGenerator):
|
|||||||
no_cache=1
|
no_cache=1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def autoname(self):
|
||||||
|
# use naming series to accomodate items with same name (different item code)
|
||||||
|
from frappe.model.naming import make_autoname
|
||||||
|
|
||||||
|
from erpnext.setup.doctype.naming_series.naming_series import get_default_naming_series
|
||||||
|
|
||||||
|
naming_series = get_default_naming_series("Website Item")
|
||||||
|
if not self.name and naming_series:
|
||||||
|
self.name = make_autoname(naming_series, doc=self)
|
||||||
|
|
||||||
def onload(self):
|
def onload(self):
|
||||||
super(WebsiteItem, self).onload()
|
super(WebsiteItem, self).onload()
|
||||||
|
|
||||||
@@ -137,7 +147,7 @@ class WebsiteItem(WebsiteGenerator):
|
|||||||
|
|
||||||
def make_thumbnail(self):
|
def make_thumbnail(self):
|
||||||
"""Make a thumbnail of `website_image`"""
|
"""Make a thumbnail of `website_image`"""
|
||||||
if frappe.flags.in_import or frappe.flags.in_migrate:
|
if frappe.flags.in_import:
|
||||||
return
|
return
|
||||||
|
|
||||||
import requests.exceptions
|
import requests.exceptions
|
||||||
@@ -210,7 +220,7 @@ class WebsiteItem(WebsiteGenerator):
|
|||||||
|
|
||||||
self.get_product_details_section(context)
|
self.get_product_details_section(context)
|
||||||
|
|
||||||
if settings.enable_reviews:
|
if settings.get("enable_reviews"):
|
||||||
reviews_data = get_item_reviews(self.name)
|
reviews_data = get_item_reviews(self.name)
|
||||||
context.update(reviews_data)
|
context.update(reviews_data)
|
||||||
context.reviews = context.reviews[:4]
|
context.reviews = context.reviews[:4]
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ def get_product_info_for_website(item_code, skip_quotation_creation=False):
|
|||||||
|
|
||||||
cart_settings = get_shopping_cart_settings()
|
cart_settings = get_shopping_cart_settings()
|
||||||
if not cart_settings.enabled:
|
if not cart_settings.enabled:
|
||||||
return frappe._dict()
|
# return settings even if cart is disabled
|
||||||
|
return frappe._dict({
|
||||||
|
"product_info": {},
|
||||||
|
"cart_settings": cart_settings
|
||||||
|
})
|
||||||
|
|
||||||
cart_quotation = frappe._dict()
|
cart_quotation = frappe._dict()
|
||||||
if not skip_quotation_creation:
|
if not skip_quotation_creation:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{%- macro slide(image, title, subtitle, action, label, index, align="Left", theme="Dark") -%}
|
{%- macro slide(image, title, subtitle, action, label, index, align="Left", theme="Dark") -%}
|
||||||
{%- set align_class = resolve_class({
|
{%- set align_class = resolve_class({
|
||||||
'text-right': align == 'Right',
|
'text-right': align == 'Right',
|
||||||
'text-centre': align == 'Center',
|
'text-centre': align == 'Centre',
|
||||||
'text-left': align == 'Left',
|
'text-left': align == 'Left',
|
||||||
}) -%}
|
}) -%}
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
<div class="carousel-body container d-flex {{ align_class }}">
|
<div class="carousel-body container d-flex {{ align_class }}">
|
||||||
<div class="carousel-content align-self-center">
|
<div class="carousel-content align-self-center">
|
||||||
{%- if title -%}<h1 class="{{ heading_class }}">{{ title }}</h1>{%- endif -%}
|
{%- if title -%}<h1 class="{{ heading_class }}">{{ title }}</h1>{%- endif -%}
|
||||||
{%- if subtitle -%}<p class="text-muted mt-2">{{ subtitle }}</p>{%- endif -%}
|
{%- if subtitle -%}<p class="{{ heading_class }} mt-2">{{ subtitle }}</p>{%- endif -%}
|
||||||
{%- if action -%}
|
{%- if action -%}
|
||||||
<a href="{{ action }}" class="btn btn-primary mt-3">
|
<a href="{{ action }}" class="btn btn-primary mt-3">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
@@ -27,12 +27,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{%- endmacro -%}
|
{%- endmacro -%}
|
||||||
|
|
||||||
<div id="{{ slider_name }}" class="section-carousel carousel slide" data-ride="carousel">
|
{%- set hero_slider_id = 'id-' + frappe.utils.generate_hash('HeroSlider', 12) -%}
|
||||||
|
|
||||||
|
<div id="{{ hero_slider_id }}" class="section-carousel carousel slide" data-ride="carousel">
|
||||||
{%- if show_indicators -%}
|
{%- if show_indicators -%}
|
||||||
<ol class="carousel-indicators">
|
<ol class="carousel-indicators">
|
||||||
{%- for index in ['1', '2', '3', '4', '5'] -%}
|
{%- for index in ['1', '2', '3', '4', '5'] -%}
|
||||||
{%- if values['slide_' + index + '_image'] -%}
|
{%- if values['slide_' + index + '_image'] -%}
|
||||||
<li data-target="#{{ slider_name }}" data-slide-to="{{ frappe.utils.cint(index) - 1 }}" class="{{ 'active' if index=='1' else ''}}"></li>
|
<li data-target="#{{ hero_slider_id }}" data-slide-to="{{ frappe.utils.cint(index) - 1 }}" class="{{ 'active' if index=='1' else ''}}"></li>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
</ol>
|
</ol>
|
||||||
@@ -54,7 +56,7 @@
|
|||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
</div>
|
</div>
|
||||||
{%- if show_controls -%}
|
{%- if show_controls -%}
|
||||||
<a class="carousel-control-prev" href="#{{ slider_name }}" role="button" data-slide="prev">
|
<a class="carousel-control-prev" href="#{{ hero_slider_id }}" role="button" data-slide="prev">
|
||||||
<div class="carousel-control">
|
<div class="carousel-control">
|
||||||
<svg class="mr-1" width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg class="mr-1" width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M11.625 3.75L6.375 9L11.625 14.25" stroke="#4C5A67" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M11.625 3.75L6.375 9L11.625 14.25" stroke="#4C5A67" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
@@ -62,7 +64,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<span class="sr-only">Previous</span>
|
<span class="sr-only">Previous</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="carousel-control-next" href="#{{ slider_name }}" role="button" data-slide="next">
|
<a class="carousel-control-next" href="#{{ hero_slider_id }}" role="button" data-slide="next">
|
||||||
<div class="carousel-control">
|
<div class="carousel-control">
|
||||||
<svg class="ml-1" width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg class="ml-1" width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M6.375 14.25L11.625 9L6.375 3.75" stroke="#4C5A67" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M6.375 14.25L11.625 9L6.375 3.75" stroke="#4C5A67" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
@@ -73,13 +75,12 @@
|
|||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
$('.carousel').carousel({
|
frappe.ready(function () {
|
||||||
interval: false,
|
$('.carousel').carousel({
|
||||||
pause: "hover",
|
interval: false,
|
||||||
wrap: true
|
pause: "hover",
|
||||||
})
|
wrap: true
|
||||||
|
})
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class Patient(Document):
|
|||||||
frappe.db.set_value('Patient', self.name, 'status', 'Disabled')
|
frappe.db.set_value('Patient', self.name, 'status', 'Disabled')
|
||||||
else:
|
else:
|
||||||
send_registration_sms(self)
|
send_registration_sms(self)
|
||||||
self.reload() # self.notify_update()
|
self.reload()
|
||||||
|
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient'):
|
if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient'):
|
||||||
@@ -93,23 +93,27 @@ class Patient(Document):
|
|||||||
self.language = frappe.db.get_single_value('System Settings', 'language')
|
self.language = frappe.db.get_single_value('System Settings', 'language')
|
||||||
|
|
||||||
def create_website_user(self):
|
def create_website_user(self):
|
||||||
if self.email and not frappe.db.exists('User', self.email):
|
users = frappe.db.get_all('User', fields=['email', 'mobile_no'], or_filters={'email': self.email, 'mobile_no': self.mobile})
|
||||||
user = frappe.get_doc({
|
if users and users[0]:
|
||||||
'doctype': 'User',
|
frappe.throw(_("User exists with Email {}, Mobile {}<br>Please check email / mobile or disable 'Invite as User' to skip creating User")
|
||||||
'first_name': self.first_name,
|
.format(frappe.bold(users[0].email), frappe.bold(users[0].mobile_no)), frappe.DuplicateEntryError)
|
||||||
'last_name': self.last_name,
|
|
||||||
'email': self.email,
|
user = frappe.get_doc({
|
||||||
'user_type': 'Website User',
|
'doctype': 'User',
|
||||||
'gender': self.sex,
|
'first_name': self.first_name,
|
||||||
'phone': self.phone,
|
'last_name': self.last_name,
|
||||||
'mobile_no': self.mobile,
|
'email': self.email,
|
||||||
'birth_date': self.dob
|
'user_type': 'Website User',
|
||||||
})
|
'gender': self.sex,
|
||||||
user.flags.ignore_permissions = True
|
'phone': self.phone,
|
||||||
user.enabled = True
|
'mobile_no': self.mobile,
|
||||||
user.send_welcome_email = True
|
'birth_date': self.dob
|
||||||
user.add_roles('Patient')
|
})
|
||||||
frappe.db.set_value(self.doctype, self.name, 'user_id', user.name)
|
user.flags.ignore_permissions = True
|
||||||
|
user.enabled = True
|
||||||
|
user.send_welcome_email = True
|
||||||
|
user.add_roles('Patient')
|
||||||
|
self.db_set('user_id', user.name)
|
||||||
|
|
||||||
def autoname(self):
|
def autoname(self):
|
||||||
patient_name_by = frappe.db.get_single_value('Healthcare Settings', 'patient_name_by')
|
patient_name_by = frappe.db.get_single_value('Healthcare Settings', 'patient_name_by')
|
||||||
@@ -159,54 +163,65 @@ class Patient(Document):
|
|||||||
return {'invoice': sales_invoice.name}
|
return {'invoice': sales_invoice.name}
|
||||||
|
|
||||||
def set_contact(self):
|
def set_contact(self):
|
||||||
if frappe.db.exists('Dynamic Link', {'parenttype':'Contact', 'link_doctype':'Patient', 'link_name':self.name}):
|
contact = get_default_contact(self.doctype, self.name)
|
||||||
old_doc = self.get_doc_before_save()
|
|
||||||
if old_doc.email != self.email or old_doc.mobile != self.mobile or old_doc.phone != self.phone:
|
|
||||||
self.update_contact()
|
|
||||||
else:
|
|
||||||
self.reload()
|
|
||||||
if self.email or self.mobile or self.phone:
|
|
||||||
contact = frappe.get_doc({
|
|
||||||
'doctype': 'Contact',
|
|
||||||
'first_name': self.first_name,
|
|
||||||
'middle_name': self.middle_name,
|
|
||||||
'last_name': self.last_name,
|
|
||||||
'gender': self.sex,
|
|
||||||
'is_primary_contact': 1
|
|
||||||
})
|
|
||||||
contact.append('links', dict(link_doctype='Patient', link_name=self.name))
|
|
||||||
if self.customer:
|
|
||||||
contact.append('links', dict(link_doctype='Customer', link_name=self.customer))
|
|
||||||
|
|
||||||
contact.insert(ignore_permissions=True)
|
|
||||||
self.update_contact(contact) # update email, mobile and phone
|
|
||||||
|
|
||||||
def update_contact(self, contact=None):
|
|
||||||
if not contact:
|
|
||||||
contact_name = get_default_contact(self.doctype, self.name)
|
|
||||||
if contact_name:
|
|
||||||
contact = frappe.get_doc('Contact', contact_name)
|
|
||||||
|
|
||||||
if contact:
|
if contact:
|
||||||
if self.email and self.email != contact.email_id:
|
old_doc = self.get_doc_before_save()
|
||||||
for email in contact.email_ids:
|
if not old_doc:
|
||||||
email.is_primary = True if email.email_id == self.email else False
|
return
|
||||||
contact.add_email(self.email, is_primary=True)
|
|
||||||
contact.set_primary_email()
|
|
||||||
|
|
||||||
if self.mobile and self.mobile != contact.mobile_no:
|
if old_doc.email != self.email or old_doc.mobile != self.mobile or old_doc.phone != self.phone:
|
||||||
for mobile in contact.phone_nos:
|
self.update_contact(contact)
|
||||||
mobile.is_primary_mobile_no = True if mobile.phone == self.mobile else False
|
else:
|
||||||
contact.add_phone(self.mobile, is_primary_mobile_no=True)
|
if self.customer:
|
||||||
contact.set_primary('mobile_no')
|
# customer contact exists, link patient
|
||||||
|
contact = get_default_contact('Customer', self.customer)
|
||||||
|
|
||||||
if self.phone and self.phone != contact.phone:
|
if contact:
|
||||||
for phone in contact.phone_nos:
|
self.update_contact(contact)
|
||||||
phone.is_primary_phone = True if phone.phone == self.phone else False
|
else:
|
||||||
contact.add_phone(self.phone, is_primary_phone=True)
|
self.reload()
|
||||||
contact.set_primary('phone')
|
if self.email or self.mobile or self.phone:
|
||||||
|
contact = frappe.get_doc({
|
||||||
|
'doctype': 'Contact',
|
||||||
|
'first_name': self.first_name,
|
||||||
|
'middle_name': self.middle_name,
|
||||||
|
'last_name': self.last_name,
|
||||||
|
'gender': self.sex,
|
||||||
|
'is_primary_contact': 1
|
||||||
|
})
|
||||||
|
contact.append('links', dict(link_doctype='Patient', link_name=self.name))
|
||||||
|
if self.customer:
|
||||||
|
contact.append('links', dict(link_doctype='Customer', link_name=self.customer))
|
||||||
|
|
||||||
contact.flags.ignore_validate = True # disable hook TODO: safe?
|
contact.insert(ignore_permissions=True)
|
||||||
|
self.update_contact(contact.name)
|
||||||
|
|
||||||
|
def update_contact(self, contact):
|
||||||
|
contact = frappe.get_doc('Contact', contact)
|
||||||
|
|
||||||
|
if not contact.has_link(self.doctype, self.name):
|
||||||
|
contact.append('links', dict(link_doctype=self.doctype, link_name=self.name))
|
||||||
|
|
||||||
|
if self.email and self.email != contact.email_id:
|
||||||
|
for email in contact.email_ids:
|
||||||
|
email.is_primary = True if email.email_id == self.email else False
|
||||||
|
contact.add_email(self.email, is_primary=True)
|
||||||
|
contact.set_primary_email()
|
||||||
|
|
||||||
|
if self.mobile and self.mobile != contact.mobile_no:
|
||||||
|
for mobile in contact.phone_nos:
|
||||||
|
mobile.is_primary_mobile_no = True if mobile.phone == self.mobile else False
|
||||||
|
contact.add_phone(self.mobile, is_primary_mobile_no=True)
|
||||||
|
contact.set_primary('mobile_no')
|
||||||
|
|
||||||
|
if self.phone and self.phone != contact.phone:
|
||||||
|
for phone in contact.phone_nos:
|
||||||
|
phone.is_primary_phone = True if phone.phone == self.phone else False
|
||||||
|
contact.add_phone(self.phone, is_primary_phone=True)
|
||||||
|
contact.set_primary('phone')
|
||||||
|
|
||||||
|
contact.flags.skip_patient_update = True
|
||||||
contact.save(ignore_permissions=True)
|
contact.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -35,3 +35,40 @@ class TestPatient(unittest.TestCase):
|
|||||||
|
|
||||||
settings.collect_registration_fee = 0
|
settings.collect_registration_fee = 0
|
||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
|
def test_patient_contact(self):
|
||||||
|
frappe.db.sql("""delete from `tabPatient` where name like '_Test Patient%'""")
|
||||||
|
frappe.db.sql("""delete from `tabCustomer` where name like '_Test Patient%'""")
|
||||||
|
frappe.db.sql("""delete from `tabContact` where name like'_Test Patient%'""")
|
||||||
|
frappe.db.sql("""delete from `tabDynamic Link` where parent like '_Test Patient%'""")
|
||||||
|
|
||||||
|
patient = create_patient(patient_name='_Test Patient Contact', email='test-patient@example.com', mobile='+91 0000000001')
|
||||||
|
customer = frappe.db.get_value('Patient', patient, 'customer')
|
||||||
|
self.assertTrue(customer)
|
||||||
|
self.assertTrue(frappe.db.exists('Dynamic Link', {'parenttype': 'Contact', 'link_doctype': 'Patient', 'link_name': patient}))
|
||||||
|
self.assertTrue(frappe.db.exists('Dynamic Link', {'parenttype': 'Contact', 'link_doctype': 'Customer', 'link_name': customer}))
|
||||||
|
|
||||||
|
# a second patient linking with same customer
|
||||||
|
new_patient = create_patient(email='test-patient@example.com', mobile='+91 0000000009', customer=customer)
|
||||||
|
self.assertTrue(frappe.db.exists('Dynamic Link', {'parenttype': 'Contact', 'link_doctype': 'Patient', 'link_name': new_patient}))
|
||||||
|
self.assertTrue(frappe.db.exists('Dynamic Link', {'parenttype': 'Contact', 'link_doctype': 'Customer', 'link_name': customer}))
|
||||||
|
|
||||||
|
def test_patient_user(self):
|
||||||
|
frappe.db.sql("""delete from `tabUser` where email='test-patient-user@example.com'""")
|
||||||
|
frappe.db.sql("""delete from `tabDynamic Link` where parent like '_Test Patient%'""")
|
||||||
|
frappe.db.sql("""delete from `tabPatient` where name like '_Test Patient%'""")
|
||||||
|
|
||||||
|
patient = create_patient(patient_name='_Test Patient User', email='test-patient-user@example.com', mobile='+91 0000000009', create_user=True)
|
||||||
|
user = frappe.db.get_value('Patient', patient, 'user_id')
|
||||||
|
self.assertTrue(frappe.db.exists('User', user))
|
||||||
|
|
||||||
|
new_patient = frappe.get_doc({
|
||||||
|
'doctype': 'Patient',
|
||||||
|
'first_name': '_Test Patient Duplicate User',
|
||||||
|
'sex': 'Male',
|
||||||
|
'email': 'test-patient-user@example.com',
|
||||||
|
'mobile': '+91 0000000009',
|
||||||
|
'invite_user': 1
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertRaises(frappe.exceptions.DuplicateEntryError, new_patient.insert)
|
||||||
|
|||||||
@@ -307,14 +307,18 @@ def create_healthcare_docs(id=0):
|
|||||||
return patient, practitioner
|
return patient, practitioner
|
||||||
|
|
||||||
|
|
||||||
def create_patient(id=0):
|
def create_patient(id=0, patient_name=None, email=None, mobile=None, customer=None, create_user=False):
|
||||||
if frappe.db.exists('Patient', {'firstname':f'_Test Patient {str(id)}'}):
|
if frappe.db.exists('Patient', {'firstname':f'_Test Patient {str(id)}'}):
|
||||||
patient = frappe.db.get_value('Patient', {'first_name': f'_Test Patient {str(id)}'}, ['name'])
|
patient = frappe.db.get_value('Patient', {'first_name': f'_Test Patient {str(id)}'}, ['name'])
|
||||||
return patient
|
return patient
|
||||||
|
|
||||||
patient = frappe.new_doc('Patient')
|
patient = frappe.new_doc('Patient')
|
||||||
patient.first_name = f'_Test Patient {str(id)}'
|
patient.first_name = patient_name if patient_name else f'_Test Patient {str(id)}'
|
||||||
patient.sex = 'Female'
|
patient.sex = 'Female'
|
||||||
|
patient.mobile = mobile
|
||||||
|
patient.email = email
|
||||||
|
patient.customer = customer
|
||||||
|
patient.invite_user = create_user
|
||||||
patient.save(ignore_permissions=True)
|
patient.save(ignore_permissions=True)
|
||||||
|
|
||||||
return patient.name
|
return patient.name
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from __future__ import unicode_literals
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import nowdate
|
from frappe.utils import add_days, nowdate
|
||||||
|
|
||||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||||
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import (
|
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import (
|
||||||
@@ -38,7 +38,7 @@ class TestPatientMedicalRecord(unittest.TestCase):
|
|||||||
medical_rec = frappe.db.exists('Patient Medical Record', {'status': 'Open', 'reference_name': vital_signs.name})
|
medical_rec = frappe.db.exists('Patient Medical Record', {'status': 'Open', 'reference_name': vital_signs.name})
|
||||||
self.assertTrue(medical_rec)
|
self.assertTrue(medical_rec)
|
||||||
|
|
||||||
appointment = create_appointment(patient, practitioner, nowdate(), invoice=1, procedure_template=1)
|
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 1), invoice=1, procedure_template=1)
|
||||||
procedure = create_procedure(appointment)
|
procedure = create_procedure(appointment)
|
||||||
procedure.start_procedure()
|
procedure.start_procedure()
|
||||||
procedure.complete_procedure()
|
procedure.complete_procedure()
|
||||||
|
|||||||
@@ -776,7 +776,7 @@ def update_patient_email_and_phone_numbers(contact, method):
|
|||||||
Hook validate Contact
|
Hook validate Contact
|
||||||
Update linked Patients' primary mobile and phone numbers
|
Update linked Patients' primary mobile and phone numbers
|
||||||
'''
|
'''
|
||||||
if 'Healthcare' not in frappe.get_active_domains():
|
if 'Healthcare' not in frappe.get_active_domains() or contact.flags.skip_patient_update:
|
||||||
return
|
return
|
||||||
|
|
||||||
if contact.is_primary_contact and (contact.email_id or contact.mobile_no or contact.phone):
|
if contact.is_primary_contact and (contact.email_id or contact.mobile_no or contact.phone):
|
||||||
@@ -784,9 +784,15 @@ def update_patient_email_and_phone_numbers(contact, method):
|
|||||||
|
|
||||||
for link in patient_links:
|
for link in patient_links:
|
||||||
contact_details = frappe.db.get_value('Patient', link.get('link_name'), ['email', 'mobile', 'phone'], as_dict=1)
|
contact_details = frappe.db.get_value('Patient', link.get('link_name'), ['email', 'mobile', 'phone'], as_dict=1)
|
||||||
|
|
||||||
|
new_contact_details = {}
|
||||||
|
|
||||||
if contact.email_id and contact.email_id != contact_details.get('email'):
|
if contact.email_id and contact.email_id != contact_details.get('email'):
|
||||||
frappe.db.set_value('Patient', link.get('link_name'), 'email', contact.email_id)
|
new_contact_details.update({'email': contact.email_id})
|
||||||
if contact.mobile_no and contact.mobile_no != contact_details.get('mobile'):
|
if contact.mobile_no and contact.mobile_no != contact_details.get('mobile'):
|
||||||
frappe.db.set_value('Patient', link.get('link_name'), 'mobile', contact.mobile_no)
|
new_contact_details.update({'mobile': contact.mobile_no})
|
||||||
if contact.phone and contact.phone != contact_details.get('phone'):
|
if contact.phone and contact.phone != contact_details.get('phone'):
|
||||||
frappe.db.set_value('Patient', link.get('link_name'), 'phone', contact.phone)
|
new_contact_details.update({'phone': contact.phone})
|
||||||
|
|
||||||
|
if new_contact_details:
|
||||||
|
frappe.db.set_value('Patient', link.get('link_name'), new_contact_details)
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ treeviews = ['Account', 'Cost Center', 'Warehouse', 'Item Group', 'Customer Grou
|
|||||||
# website
|
# website
|
||||||
update_website_context = ["erpnext.e_commerce.shopping_cart.utils.update_website_context", "erpnext.education.doctype.education_settings.education_settings.update_website_context"]
|
update_website_context = ["erpnext.e_commerce.shopping_cart.utils.update_website_context", "erpnext.education.doctype.education_settings.education_settings.update_website_context"]
|
||||||
my_account_context = "erpnext.e_commerce.shopping_cart.utils.update_my_account_context"
|
my_account_context = "erpnext.e_commerce.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"]
|
||||||
|
|
||||||
|
|||||||
@@ -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,7 +200,7 @@
|
|||||||
],
|
],
|
||||||
"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",
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ frappe.ui.form.on('Training Result', {
|
|||||||
frm.set_value("employees" ,"");
|
frm.set_value("employees" ,"");
|
||||||
if (r.message) {
|
if (r.message) {
|
||||||
$.each(r.message, function(i, d) {
|
$.each(r.message, function(i, d) {
|
||||||
var row = frappe.model.add_child(cur_frm.doc, "Training Result Employee", "employees");
|
var row = frappe.model.add_child(frm.doc, "Training Result Employee", "employees");
|
||||||
row.employee = d.employee;
|
row.employee = d.employee;
|
||||||
row.employee_name = d.employee_name;
|
row.employee_name = d.employee_name;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -148,7 +148,10 @@ def set_employee_name(doc):
|
|||||||
def update_employee(employee, details, date=None, cancel=False):
|
def update_employee(employee, details, date=None, cancel=False):
|
||||||
internal_work_history = {}
|
internal_work_history = {}
|
||||||
for item in details:
|
for item in details:
|
||||||
fieldtype = frappe.get_meta("Employee").get_field(item.fieldname).fieldtype
|
field = frappe.get_meta("Employee").get_field(item.fieldname)
|
||||||
|
if not field:
|
||||||
|
continue
|
||||||
|
fieldtype = field.fieldtype
|
||||||
new_data = item.new if not cancel else item.current
|
new_data = item.new if not cancel else item.current
|
||||||
if fieldtype == "Date" and new_data:
|
if fieldtype == "Date" and new_data:
|
||||||
new_data = getdate(new_data)
|
new_data = getdate(new_data)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ frappe.ui.form.on('Maintenance Schedule', {
|
|||||||
},
|
},
|
||||||
refresh: function (frm) {
|
refresh: function (frm) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
frm.toggle_display('generate_schedule', !(frm.is_new()));
|
frm.toggle_display('generate_schedule', !(frm.is_new() || frm.doc.docstatus));
|
||||||
frm.toggle_display('schedule', !(frm.is_new()));
|
frm.toggle_display('schedule', !(frm.is_new()));
|
||||||
}, 10);
|
}, 10);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ from erpnext.utilities.transaction_base import TransactionBase, delete_events
|
|||||||
class MaintenanceSchedule(TransactionBase):
|
class MaintenanceSchedule(TransactionBase):
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def generate_schedule(self):
|
def generate_schedule(self):
|
||||||
|
if self.docstatus != 0:
|
||||||
|
return
|
||||||
self.set('schedules', [])
|
self.set('schedules', [])
|
||||||
frappe.db.sql("""delete from `tabMaintenance Schedule Detail`
|
|
||||||
where parent=%s""", (self.name))
|
|
||||||
count = 1
|
count = 1
|
||||||
for d in self.get('items'):
|
for d in self.get('items'):
|
||||||
self.validate_maintenance_detail()
|
self.validate_maintenance_detail()
|
||||||
@@ -47,7 +47,7 @@ class MaintenanceSchedule(TransactionBase):
|
|||||||
"Yearly": 365
|
"Yearly": 365
|
||||||
}
|
}
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
if item.periodicity and item.start_date:
|
if item.periodicity and item.periodicity != "Random" and item.start_date:
|
||||||
if not item.end_date:
|
if not item.end_date:
|
||||||
if item.no_of_visits:
|
if item.no_of_visits:
|
||||||
item.end_date = add_days(item.start_date, item.no_of_visits * days_in_period[item.periodicity])
|
item.end_date = add_days(item.start_date, item.no_of_visits * days_in_period[item.periodicity])
|
||||||
|
|||||||
@@ -89,13 +89,14 @@
|
|||||||
"width": "160px"
|
"width": "160px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"columns": 2,
|
"columns": 2,
|
||||||
|
"default": "Pending",
|
||||||
"fieldname": "completion_status",
|
"fieldname": "completion_status",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Completion Status",
|
"label": "Completion Status",
|
||||||
"options": "Pending\nPartially Completed\nFully Completed",
|
"options": "Pending\nPartially Completed\nFully Completed"
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_3",
|
"fieldname": "column_break_3",
|
||||||
@@ -125,10 +126,11 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-05-27 16:07:25.905015",
|
"modified": "2021-09-16 21:25:22.506485",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Maintenance",
|
"module": "Maintenance",
|
||||||
"name": "Maintenance Schedule Detail",
|
"name": "Maintenance Schedule Detail",
|
||||||
|
"naming_rule": "Random",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
|
|||||||
@@ -1135,7 +1135,8 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
query_filters["has_variants"] = 0
|
query_filters["has_variants"] = 0
|
||||||
|
|
||||||
if filters and filters.get("is_stock_item"):
|
if filters and filters.get("is_stock_item"):
|
||||||
query_filters["is_stock_item"] = 1
|
or_cond_filters["is_stock_item"] = 1
|
||||||
|
or_cond_filters["has_variants"] = 1
|
||||||
|
|
||||||
return frappe.get_list("Item",
|
return frappe.get_list("Item",
|
||||||
fields = fields, filters=query_filters,
|
fields = fields, filters=query_filters,
|
||||||
|
|||||||
@@ -677,7 +677,7 @@ def get_job_details(start, end, filters=None):
|
|||||||
conditions = get_filters_cond("Job Card", filters, [])
|
conditions = get_filters_cond("Job Card", filters, [])
|
||||||
|
|
||||||
job_cards = frappe.db.sql(""" SELECT `tabJob Card`.name, `tabJob Card`.work_order,
|
job_cards = frappe.db.sql(""" SELECT `tabJob Card`.name, `tabJob Card`.work_order,
|
||||||
`tabJob Card`.employee_name, `tabJob Card`.status, ifnull(`tabJob Card`.remarks, ''),
|
`tabJob Card`.status, ifnull(`tabJob Card`.remarks, ''),
|
||||||
min(`tabJob Card Time Log`.from_time) as from_time,
|
min(`tabJob Card Time Log`.from_time) as from_time,
|
||||||
max(`tabJob Card Time Log`.to_time) as to_time
|
max(`tabJob Card Time Log`.to_time) as to_time
|
||||||
FROM `tabJob Card` , `tabJob Card Time Log`
|
FROM `tabJob Card` , `tabJob Card Time Log`
|
||||||
@@ -687,7 +687,7 @@ def get_job_details(start, end, filters=None):
|
|||||||
|
|
||||||
for d in job_cards:
|
for d in job_cards:
|
||||||
subject_data = []
|
subject_data = []
|
||||||
for field in ["name", "work_order", "remarks", "employee_name"]:
|
for field in ["name", "work_order", "remarks"]:
|
||||||
if not d.get(field): continue
|
if not d.get(field): continue
|
||||||
|
|
||||||
subject_data.append(d.get(field))
|
subject_data.append(d.get(field))
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ import unittest
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import random_string
|
from frappe.utils import random_string
|
||||||
|
|
||||||
|
from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError, OverlapError
|
||||||
from erpnext.manufacturing.doctype.job_card.job_card import (
|
from erpnext.manufacturing.doctype.job_card.job_card import (
|
||||||
make_stock_entry as make_stock_entry_from_jc,
|
make_stock_entry as make_stock_entry_from_jc,
|
||||||
OperationMismatchError,
|
|
||||||
OverlapError
|
|
||||||
)
|
)
|
||||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||||
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
|
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
|
||||||
|
|||||||
@@ -242,6 +242,8 @@ frappe.ui.form.on('Production Plan', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
get_sub_assembly_items: function(frm) {
|
get_sub_assembly_items: function(frm) {
|
||||||
|
frm.dirty();
|
||||||
|
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "get_sub_assembly_items",
|
method: "get_sub_assembly_items",
|
||||||
freeze: true,
|
freeze: true,
|
||||||
|
|||||||
@@ -457,7 +457,8 @@ class ProductionPlan(Document):
|
|||||||
|
|
||||||
def prepare_args_for_sub_assembly_items(self, row, args):
|
def prepare_args_for_sub_assembly_items(self, row, args):
|
||||||
for field in ["production_item", "item_name", "qty", "fg_warehouse",
|
for field in ["production_item", "item_name", "qty", "fg_warehouse",
|
||||||
"description", "bom_no", "stock_uom", "bom_level", "production_plan_item"]:
|
"description", "bom_no", "stock_uom", "bom_level",
|
||||||
|
"production_plan_item", "schedule_date"]:
|
||||||
args[field] = row.get(field)
|
args[field] = row.get(field)
|
||||||
|
|
||||||
args.update({
|
args.update({
|
||||||
@@ -561,8 +562,6 @@ class ProductionPlan(Document):
|
|||||||
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
|
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
|
||||||
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
|
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
|
||||||
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
|
def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
|
||||||
bom_data = sorted(bom_data, key = lambda i: i.bom_level)
|
bom_data = sorted(bom_data, key = lambda i: i.bom_level)
|
||||||
|
|
||||||
|
|||||||
@@ -310,8 +310,15 @@ erpnext.patches.v13_0.custom_fields_for_taxjar_integration
|
|||||||
erpnext.patches.v13_0.set_operation_time_based_on_operating_cost
|
erpnext.patches.v13_0.set_operation_time_based_on_operating_cost
|
||||||
erpnext.patches.v13_0.validate_options_for_data_field
|
erpnext.patches.v13_0.validate_options_for_data_field
|
||||||
erpnext.patches.v13_0.create_accounting_dimensions_in_pos_doctypes
|
erpnext.patches.v13_0.create_accounting_dimensions_in_pos_doctypes
|
||||||
erpnext.patches.v13_0.create_website_items
|
erpnext.patches.v13_0.create_website_items #30-09-2021
|
||||||
erpnext.patches.v13_0.populate_e_commerce_settings
|
erpnext.patches.v13_0.populate_e_commerce_settings
|
||||||
erpnext.patches.v13_0.make_homepage_products_website_items
|
erpnext.patches.v13_0.make_homepage_products_website_items
|
||||||
erpnext.patches.v13_0.update_dates_in_tax_withholding_category
|
erpnext.patches.v13_0.update_dates_in_tax_withholding_category
|
||||||
|
erpnext.patches.v13_0.replace_supplier_item_group_with_party_specific_item
|
||||||
erpnext.patches.v13_0.gst_fields_for_pos_invoice
|
erpnext.patches.v13_0.gst_fields_for_pos_invoice
|
||||||
|
erpnext.patches.v13_0.create_accounting_dimensions_in_pos_doctypes
|
||||||
|
erpnext.patches.v13_0.create_custom_field_for_finance_book
|
||||||
|
erpnext.patches.v13_0.modify_invalid_gain_loss_gl_entries
|
||||||
|
erpnext.patches.v13_0.fix_additional_cost_in_mfg_stock_entry
|
||||||
|
erpnext.patches.v13_0.shopping_cart_to_ecommerce
|
||||||
|
erpnext.patches.v13_0.set_status_in_maintenance_schedule_table
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
company = frappe.get_all('Company', filters = {'country': 'India'})
|
||||||
|
if not company:
|
||||||
|
return
|
||||||
|
|
||||||
|
custom_field = {
|
||||||
|
'Finance Book': [
|
||||||
|
{
|
||||||
|
'fieldname': 'for_income_tax',
|
||||||
|
'label': 'For Income Tax',
|
||||||
|
'fieldtype': 'Check',
|
||||||
|
'insert_after': 'finance_book_name',
|
||||||
|
'description': 'If the asset is put to use for less than 180 days, the first Depreciation Rate will be reduced by 50%.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
create_custom_fields(custom_field, update=1)
|
||||||
@@ -46,28 +46,32 @@ def execute():
|
|||||||
count = 0
|
count = 0
|
||||||
for item in items:
|
for item in items:
|
||||||
if frappe.db.exists("Website Item", {"item_code": item.item_code}):
|
if frappe.db.exists("Website Item", {"item_code": item.item_code}):
|
||||||
continue
|
# if website item already exists check for empty thumbnail
|
||||||
|
web_item_doc = frappe.get_doc("Website Item", {"item_code": item.item_code})
|
||||||
|
if web_item_doc.website_image and not web_item_doc.thumbnail:
|
||||||
|
web_item_doc.make_thumbnail()
|
||||||
|
web_item_doc.save()
|
||||||
|
else:
|
||||||
|
# else make new website item from item (publish item)
|
||||||
|
website_item = make_website_item(item, save=False)
|
||||||
|
website_item.ranking = item.get("weightage")
|
||||||
|
for field in web_fields_to_map:
|
||||||
|
website_item.update({field: item.get(field)})
|
||||||
|
website_item.save()
|
||||||
|
|
||||||
# make website item from item (publish item)
|
# move Website Item Group & Website Specification table to Website Item
|
||||||
website_item = make_website_item(item, save=False)
|
for doctype in ("Website Item Group", "Item Website Specification"):
|
||||||
website_item.ranking = item.get("weightage")
|
web_item, item_code = website_item.name, item.item_code
|
||||||
for field in web_fields_to_map:
|
frappe.db.sql(f"""
|
||||||
website_item.update({field: item.get(field)})
|
Update
|
||||||
website_item.save()
|
`tab{doctype}`
|
||||||
|
set
|
||||||
# move Website Item Group & Website Specification table to Website Item
|
parenttype = 'Website Item',
|
||||||
for doctype in ("Website Item Group", "Item Website Specification"):
|
parent = '{web_item}'
|
||||||
web_item, item_code = website_item.name, item.item_code
|
where
|
||||||
frappe.db.sql(f"""
|
parenttype = 'Item'
|
||||||
Update
|
and parent = '{item_code}'
|
||||||
`tab{doctype}`
|
""")
|
||||||
set
|
|
||||||
parenttype = 'Website Item',
|
|
||||||
parent = '{web_item}'
|
|
||||||
where
|
|
||||||
parenttype = 'Item'
|
|
||||||
and parent = '{item_code}'
|
|
||||||
""")
|
|
||||||
|
|
||||||
count += 1
|
count += 1
|
||||||
if count % 20 == 0: # commit after every 20 items
|
if count % 20 == 0: # commit after every 20 items
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
from typing import List, NewType
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
StockEntryCode = NewType("StockEntryCode", str)
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
stock_entry_codes = find_broken_stock_entries()
|
||||||
|
|
||||||
|
for stock_entry_code in stock_entry_codes:
|
||||||
|
patched_stock_entry = patch_additional_cost(stock_entry_code)
|
||||||
|
create_repost_item_valuation(patched_stock_entry)
|
||||||
|
|
||||||
|
|
||||||
|
def find_broken_stock_entries() -> List[StockEntryCode]:
|
||||||
|
period_closing_date = frappe.db.get_value(
|
||||||
|
"Period Closing Voucher", {"docstatus": 1}, "posting_date", order_by="posting_date desc"
|
||||||
|
)
|
||||||
|
|
||||||
|
stock_entries_to_patch = frappe.db.sql(
|
||||||
|
"""
|
||||||
|
select se.name, sum(sed.additional_cost) as item_additional_cost, se.total_additional_costs
|
||||||
|
from `tabStock Entry` se
|
||||||
|
join `tabStock Entry Detail` sed
|
||||||
|
on sed.parent = se.name
|
||||||
|
where
|
||||||
|
se.docstatus = 1 and
|
||||||
|
se.posting_date > %s
|
||||||
|
group by
|
||||||
|
sed.parent
|
||||||
|
having
|
||||||
|
item_additional_cost != se.total_additional_costs
|
||||||
|
""",
|
||||||
|
period_closing_date,
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return [d.name for d in stock_entries_to_patch]
|
||||||
|
|
||||||
|
|
||||||
|
def patch_additional_cost(code: StockEntryCode):
|
||||||
|
stock_entry = frappe.get_doc("Stock Entry", code)
|
||||||
|
stock_entry.distribute_additional_costs()
|
||||||
|
stock_entry.update_valuation_rate()
|
||||||
|
stock_entry.set_total_incoming_outgoing_value()
|
||||||
|
stock_entry.set_total_amount()
|
||||||
|
stock_entry.db_update()
|
||||||
|
for item in stock_entry.items:
|
||||||
|
item.db_update()
|
||||||
|
return stock_entry
|
||||||
|
|
||||||
|
|
||||||
|
def create_repost_item_valuation(stock_entry):
|
||||||
|
from erpnext.controllers.stock_controller import create_repost_item_valuation_entry
|
||||||
|
|
||||||
|
# turn on recalculate flag so reposting corrects the incoming/outgoing rates.
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
{"voucher_no": stock_entry.name, "actual_qty": (">", 0)},
|
||||||
|
"recalculate_rate",
|
||||||
|
1,
|
||||||
|
update_modified=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
create_repost_item_valuation_entry(
|
||||||
|
args=frappe._dict(
|
||||||
|
{
|
||||||
|
"posting_date": stock_entry.posting_date,
|
||||||
|
"posting_time": stock_entry.posting_time,
|
||||||
|
"voucher_type": stock_entry.doctype,
|
||||||
|
"voucher_no": stock_entry.name,
|
||||||
|
"company": stock_entry.company,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
49
erpnext/patches/v13_0/modify_invalid_gain_loss_gl_entries.py
Normal file
49
erpnext/patches/v13_0/modify_invalid_gain_loss_gl_entries.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc('accounts', 'doctype', 'purchase_invoice_advance')
|
||||||
|
frappe.reload_doc('accounts', 'doctype', 'sales_invoice_advance')
|
||||||
|
|
||||||
|
purchase_invoices = frappe.db.sql("""
|
||||||
|
select
|
||||||
|
parenttype as type, parent as name
|
||||||
|
from
|
||||||
|
`tabPurchase Invoice Advance`
|
||||||
|
where
|
||||||
|
ref_exchange_rate = 1
|
||||||
|
and docstatus = 1
|
||||||
|
and ifnull(exchange_gain_loss, '') != ''
|
||||||
|
group by
|
||||||
|
parent
|
||||||
|
""", as_dict=1)
|
||||||
|
|
||||||
|
sales_invoices = frappe.db.sql("""
|
||||||
|
select
|
||||||
|
parenttype as type, parent as name
|
||||||
|
from
|
||||||
|
`tabSales Invoice Advance`
|
||||||
|
where
|
||||||
|
ref_exchange_rate = 1
|
||||||
|
and docstatus = 1
|
||||||
|
and ifnull(exchange_gain_loss, '') != ''
|
||||||
|
group by
|
||||||
|
parent
|
||||||
|
""", as_dict=1)
|
||||||
|
|
||||||
|
if purchase_invoices + sales_invoices:
|
||||||
|
frappe.log_error(json.dumps(purchase_invoices + sales_invoices, indent=2), title="Patch Log")
|
||||||
|
|
||||||
|
for invoice in purchase_invoices + sales_invoices:
|
||||||
|
doc = frappe.get_doc(invoice.type, invoice.name)
|
||||||
|
doc.docstatus = 2
|
||||||
|
doc.make_gl_entries()
|
||||||
|
for advance in doc.advances:
|
||||||
|
if advance.ref_exchange_rate == 1:
|
||||||
|
advance.db_set('exchange_gain_loss', 0, False)
|
||||||
|
doc.docstatus = 1
|
||||||
|
doc.make_gl_entries()
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Copyright (c) 2019, Frappe and Contributors
|
||||||
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
if frappe.db.table_exists('Supplier Item Group'):
|
||||||
|
frappe.reload_doc("selling", "doctype", "party_specific_item")
|
||||||
|
sig = frappe.db.get_all("Supplier Item Group", fields=["name", "supplier", "item_group"])
|
||||||
|
for item in sig:
|
||||||
|
psi = frappe.new_doc("Party Specific Item")
|
||||||
|
psi.party_type = "Supplier"
|
||||||
|
psi.party = item.supplier
|
||||||
|
psi.restrict_based_on = "Item Group"
|
||||||
|
psi.based_on_value = item.item_group
|
||||||
|
psi.insert()
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc("maintenance", "doctype", "Maintenance Schedule Detail")
|
||||||
|
frappe.db.sql("""
|
||||||
|
UPDATE `tabMaintenance Schedule Detail`
|
||||||
|
SET completion_status = 'Pending'
|
||||||
|
WHERE docstatus < 2
|
||||||
|
""")
|
||||||
29
erpnext/patches/v13_0/shopping_cart_to_ecommerce.py
Normal file
29
erpnext/patches/v13_0/shopping_cart_to_ecommerce.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import click
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
|
||||||
|
frappe.delete_doc("DocType", "Shopping Cart Settings", ignore_missing=True)
|
||||||
|
frappe.delete_doc("DocType", "Products Settings", ignore_missing=True)
|
||||||
|
frappe.delete_doc("DocType", "Supplier Item Group", ignore_missing=True)
|
||||||
|
|
||||||
|
if frappe.db.get_single_value("E Commerce Settings", "enabled"):
|
||||||
|
notify_users()
|
||||||
|
|
||||||
|
|
||||||
|
def notify_users():
|
||||||
|
|
||||||
|
click.secho(
|
||||||
|
"Shopping cart and Product settings are merged into E-commerce settings.\n"
|
||||||
|
"Checkout the documentation to learn more:"
|
||||||
|
"https://docs.erpnext.com/docs/v13/user/manual/en/e_commerce/set_up_e_commerce",
|
||||||
|
fg="yellow",
|
||||||
|
)
|
||||||
|
|
||||||
|
note = frappe.new_doc("Note")
|
||||||
|
note.title = "New E-Commerce Module"
|
||||||
|
note.public = 1
|
||||||
|
note.notify_on_login = 1
|
||||||
|
note.content = """<div class="ql-editor read-mode"><p>You are seeing this message because Shopping Cart is enabled on your site. </p><p><br></p><p>Shopping Cart Settings and Products settings are now merged into "E Commerce Settings". </p><p><br></p><p>You can learn about new and improved E-Commerce features in the official documentation.</p><ol><li data-list="bullet"><span class="ql-ui" contenteditable="false"></span><a href="https://docs.erpnext.com/docs/v13/user/manual/en/e_commerce/set_up_e_commerce" rel="noopener noreferrer">https://docs.erpnext.com/docs/v13/user/manual/en/e_commerce/set_up_e_commerce</a></li></ol><p><br></p></div>"""
|
||||||
|
note.save()
|
||||||
@@ -13,7 +13,7 @@ def execute():
|
|||||||
frappe.reload_doc('stock', 'doctype', 'stock_settings')
|
frappe.reload_doc('stock', 'doctype', 'stock_settings')
|
||||||
|
|
||||||
def update_from_return_docs(doctype):
|
def update_from_return_docs(doctype):
|
||||||
for return_doc in frappe.get_all(doctype, filters={'is_return' : 1, 'docstatus' : 1}):
|
for return_doc in frappe.get_all(doctype, filters={'is_return' : 1, 'docstatus' : 1, 'return_against': ('!=', '')}):
|
||||||
# Update original receipt/delivery document from return
|
# Update original receipt/delivery document from return
|
||||||
return_doc = frappe.get_cached_doc(doctype, return_doc.name)
|
return_doc = frappe.get_cached_doc(doctype, return_doc.name)
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -14,12 +14,11 @@ from erpnext.hr.utils import validate_active_employee
|
|||||||
|
|
||||||
class AdditionalSalary(Document):
|
class AdditionalSalary(Document):
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
if self.ref_doctype == "Employee Advance" and self.ref_docname:
|
self.update_return_amount_in_employee_advance()
|
||||||
frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", self.amount)
|
|
||||||
|
|
||||||
self.update_employee_referral()
|
self.update_employee_referral()
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
|
self.update_return_amount_in_employee_advance()
|
||||||
self.update_employee_referral(cancel=True)
|
self.update_employee_referral(cancel=True)
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
@@ -98,6 +97,17 @@ class AdditionalSalary(Document):
|
|||||||
frappe.throw(_("Additional Salary for referral bonus can only be created against Employee Referral with status {0}").format(
|
frappe.throw(_("Additional Salary for referral bonus can only be created against Employee Referral with status {0}").format(
|
||||||
frappe.bold("Accepted")))
|
frappe.bold("Accepted")))
|
||||||
|
|
||||||
|
def update_return_amount_in_employee_advance(self):
|
||||||
|
if self.ref_doctype == "Employee Advance" and self.ref_docname:
|
||||||
|
return_amount = frappe.db.get_value("Employee Advance", self.ref_docname, "return_amount")
|
||||||
|
|
||||||
|
if self.docstatus == 2:
|
||||||
|
return_amount -= self.amount
|
||||||
|
else:
|
||||||
|
return_amount += self.amount
|
||||||
|
|
||||||
|
frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", return_amount)
|
||||||
|
|
||||||
def update_employee_referral(self, cancel=False):
|
def update_employee_referral(self, cancel=False):
|
||||||
if self.ref_doctype == "Employee Referral":
|
if self.ref_doctype == "Employee Referral":
|
||||||
status = "Unpaid" if cancel else "Paid"
|
status = "Unpaid" if cancel else "Paid"
|
||||||
|
|||||||
@@ -141,6 +141,9 @@ class Project(Document):
|
|||||||
if self.sales_order:
|
if self.sales_order:
|
||||||
frappe.db.set_value("Sales Order", self.sales_order, "project", self.name)
|
frappe.db.set_value("Sales Order", self.sales_order, "project", self.name)
|
||||||
|
|
||||||
|
def on_trash(self):
|
||||||
|
frappe.db.set_value("Sales Order", {"project": self.name}, "project", "")
|
||||||
|
|
||||||
def update_percent_complete(self):
|
def update_percent_complete(self):
|
||||||
if self.percent_complete_method == "Manual":
|
if self.percent_complete_method == "Manual":
|
||||||
if self.status == "Completed":
|
if self.status == "Completed":
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from frappe.utils import add_days, getdate, nowdate
|
|||||||
|
|
||||||
from erpnext.projects.doctype.project_template.test_project_template import make_project_template
|
from erpnext.projects.doctype.project_template.test_project_template import make_project_template
|
||||||
from erpnext.projects.doctype.task.test_task import create_task
|
from erpnext.projects.doctype.task.test_task import create_task
|
||||||
|
from erpnext.selling.doctype.sales_order.sales_order import make_project as make_project_from_so
|
||||||
|
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||||
|
|
||||||
test_records = frappe.get_test_records('Project')
|
test_records = frappe.get_test_records('Project')
|
||||||
test_ignore = ["Sales Order"]
|
test_ignore = ["Sales Order"]
|
||||||
@@ -96,6 +98,21 @@ class TestProject(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(len(tasks), 2)
|
self.assertEqual(len(tasks), 2)
|
||||||
|
|
||||||
|
def test_project_linking_with_sales_order(self):
|
||||||
|
so = make_sales_order()
|
||||||
|
project = make_project_from_so(so.name)
|
||||||
|
|
||||||
|
project.save()
|
||||||
|
self.assertEqual(project.sales_order, so.name)
|
||||||
|
|
||||||
|
so.reload()
|
||||||
|
self.assertEqual(so.project, project.name)
|
||||||
|
|
||||||
|
project.delete()
|
||||||
|
|
||||||
|
so.reload()
|
||||||
|
self.assertFalse(so.project)
|
||||||
|
|
||||||
def get_project(name, template):
|
def get_project(name, template):
|
||||||
|
|
||||||
project = frappe.get_doc(dict(
|
project = frappe.get_doc(dict(
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
{
|
{
|
||||||
fieldtype: "HTML",
|
fieldtype: "HTML",
|
||||||
fieldname: "no_matching_vouchers",
|
fieldname: "no_matching_vouchers",
|
||||||
options: "<div class='text-muted text-center'>No Matching Vouchers Found</div>"
|
options: "<div class=\"text-muted text-center\">No Matching Vouchers Found</div>"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldtype: "Section Break",
|
fieldtype: "Section Break",
|
||||||
|
|||||||
@@ -345,26 +345,14 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
scan_barcode: function() {
|
scan_barcode: function() {
|
||||||
let scan_barcode_field = this.frm.fields_dict["scan_barcode"];
|
let me = this;
|
||||||
|
|
||||||
let show_description = function(idx, exist = null) {
|
|
||||||
if (exist) {
|
|
||||||
frappe.show_alert({
|
|
||||||
message: __('Row #{0}: Qty increased by 1', [idx]),
|
|
||||||
indicator: 'green'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
frappe.show_alert({
|
|
||||||
message: __('Row #{0}: Item added', [idx]),
|
|
||||||
indicator: 'green'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.frm.doc.scan_barcode) {
|
if(this.frm.doc.scan_barcode) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.selling.page.point_of_sale.point_of_sale.search_for_serial_or_batch_or_barcode_number",
|
method: "erpnext.selling.page.point_of_sale.point_of_sale.search_for_serial_or_batch_or_barcode_number",
|
||||||
args: { search_value: this.frm.doc.scan_barcode }
|
args: {
|
||||||
|
search_value: this.frm.doc.scan_barcode
|
||||||
|
}
|
||||||
}).then(r => {
|
}).then(r => {
|
||||||
const data = r && r.message;
|
const data = r && r.message;
|
||||||
if (!data || Object.keys(data).length === 0) {
|
if (!data || Object.keys(data).length === 0) {
|
||||||
@@ -375,50 +363,97 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cur_grid = this.frm.fields_dict.items.grid;
|
me.modify_table_after_scan(data);
|
||||||
|
|
||||||
let row_to_modify = null;
|
|
||||||
const existing_item_row = this.frm.doc.items.find(d => d.item_code === data.item_code);
|
|
||||||
const blank_item_row = this.frm.doc.items.find(d => !d.item_code);
|
|
||||||
|
|
||||||
if (existing_item_row) {
|
|
||||||
row_to_modify = existing_item_row;
|
|
||||||
} else if (blank_item_row) {
|
|
||||||
row_to_modify = blank_item_row;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!row_to_modify) {
|
|
||||||
// add new row
|
|
||||||
row_to_modify = frappe.model.add_child(this.frm.doc, cur_grid.doctype, 'items');
|
|
||||||
}
|
|
||||||
|
|
||||||
show_description(row_to_modify.idx, row_to_modify.item_code);
|
|
||||||
|
|
||||||
this.frm.from_barcode = this.frm.from_barcode ? this.frm.from_barcode + 1 : 1;
|
|
||||||
frappe.model.set_value(row_to_modify.doctype, row_to_modify.name, {
|
|
||||||
item_code: data.item_code,
|
|
||||||
qty: (row_to_modify.qty || 0) + 1
|
|
||||||
});
|
|
||||||
|
|
||||||
['serial_no', 'batch_no', 'barcode'].forEach(field => {
|
|
||||||
if (data[field] && frappe.meta.has_field(row_to_modify.doctype, field)) {
|
|
||||||
|
|
||||||
let value = (row_to_modify[field] && field === "serial_no")
|
|
||||||
? row_to_modify[field] + '\n' + data[field] : data[field];
|
|
||||||
|
|
||||||
frappe.model.set_value(row_to_modify.doctype,
|
|
||||||
row_to_modify.name, field, value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
scan_barcode_field.set_value('');
|
|
||||||
refresh_field("items");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
apply_default_taxes: function() {
|
modify_table_after_scan(data) {
|
||||||
|
let scan_barcode_field = this.frm.fields_dict["scan_barcode"];
|
||||||
|
let cur_grid = this.frm.fields_dict.items.grid;
|
||||||
|
let row_to_modify = null;
|
||||||
|
|
||||||
|
// Check if batch is scanned and table has batch no field
|
||||||
|
let batch_no_scan = Boolean(data.batch_no) && frappe.meta.has_field(cur_grid.doctype, "batch_no");
|
||||||
|
|
||||||
|
if (batch_no_scan) {
|
||||||
|
row_to_modify = this.get_batch_row_to_modify(data.batch_no);
|
||||||
|
} else {
|
||||||
|
// serial or barcode scan
|
||||||
|
row_to_modify = this.get_row_to_modify_on_scan(row_to_modify, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!row_to_modify) {
|
||||||
|
// add new row if new item/batch is scanned
|
||||||
|
row_to_modify = frappe.model.add_child(this.frm.doc, cur_grid.doctype, 'items');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.show_scan_message(row_to_modify.idx, row_to_modify.item_code);
|
||||||
|
this.set_scanned_values(row_to_modify, data, scan_barcode_field);
|
||||||
|
},
|
||||||
|
|
||||||
|
set_scanned_values(row_to_modify, data, scan_barcode_field) {
|
||||||
|
// increase qty and set scanned value and item in row
|
||||||
|
this.frm.from_barcode = this.frm.from_barcode ? this.frm.from_barcode + 1 : 1;
|
||||||
|
frappe.model.set_value(row_to_modify.doctype, row_to_modify.name, {
|
||||||
|
item_code: data.item_code,
|
||||||
|
qty: (row_to_modify.qty || 0) + 1
|
||||||
|
});
|
||||||
|
|
||||||
|
['serial_no', 'batch_no', 'barcode'].forEach(field => {
|
||||||
|
if (data[field] && frappe.meta.has_field(row_to_modify.doctype, field)) {
|
||||||
|
let is_serial_no = row_to_modify[field] && field === "serial_no";
|
||||||
|
let value = data[field];
|
||||||
|
|
||||||
|
if (is_serial_no) {
|
||||||
|
value = row_to_modify[field] + '\n' + data[field];
|
||||||
|
}
|
||||||
|
|
||||||
|
frappe.model.set_value(row_to_modify.doctype, row_to_modify.name, field, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
scan_barcode_field.set_value('');
|
||||||
|
refresh_field("items");
|
||||||
|
},
|
||||||
|
|
||||||
|
get_row_to_modify_on_scan(row_to_modify, data) {
|
||||||
|
// get an existing item row to increment or blank row to modify
|
||||||
|
const existing_item_row = this.frm.doc.items.find(d => d.item_code === data.item_code);
|
||||||
|
const blank_item_row = this.frm.doc.items.find(d => !d.item_code);
|
||||||
|
|
||||||
|
if (existing_item_row) {
|
||||||
|
row_to_modify = existing_item_row;
|
||||||
|
} else if (blank_item_row) {
|
||||||
|
row_to_modify = blank_item_row;
|
||||||
|
}
|
||||||
|
|
||||||
|
return row_to_modify;
|
||||||
|
},
|
||||||
|
|
||||||
|
get_batch_row_to_modify(batch_no) {
|
||||||
|
// get row if batch already exists in table
|
||||||
|
const existing_batch_row = this.frm.doc.items.find(d => d.batch_no === batch_no);
|
||||||
|
return existing_batch_row || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
show_scan_message (idx, exist = null) {
|
||||||
|
// show new row or qty increase toast
|
||||||
|
if (exist) {
|
||||||
|
frappe.show_alert({
|
||||||
|
message: __('Row #{0}: Qty increased by 1', [idx]),
|
||||||
|
indicator: 'green'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
frappe.show_alert({
|
||||||
|
message: __('Row #{0}: Item added', [idx]),
|
||||||
|
indicator: 'green'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
apply_default_taxes() {
|
||||||
var me = this;
|
var me = this;
|
||||||
var taxes_and_charges_field = frappe.meta.get_docfield(me.frm.doc.doctype, "taxes_and_charges",
|
var taxes_and_charges_field = frappe.meta.get_docfield(me.frm.doc.doctype, "taxes_and_charges",
|
||||||
me.frm.doc.name);
|
me.frm.doc.name);
|
||||||
@@ -617,6 +652,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
|||||||
me.frm.script_manager.trigger('qty', item.doctype, item.name);
|
me.frm.script_manager.trigger('qty', item.doctype, item.name);
|
||||||
if (!me.frm.doc.set_warehouse)
|
if (!me.frm.doc.set_warehouse)
|
||||||
me.frm.script_manager.trigger('warehouse', item.doctype, item.name);
|
me.frm.script_manager.trigger('warehouse', item.doctype, item.name);
|
||||||
|
me.apply_price_list(item, true);
|
||||||
}, undefined, !frappe.flags.hide_serial_batch_dialog);
|
}, undefined, !frappe.flags.hide_serial_batch_dialog);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ erpnext.HierarchyChart = class {
|
|||||||
});
|
});
|
||||||
|
|
||||||
node.parent.append(node_card);
|
node.parent.append(node_card);
|
||||||
node.$link = $(`#${node.id}`);
|
node.$link = $(`[id="${node.id}"]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
show() {
|
show() {
|
||||||
@@ -223,7 +223,7 @@ erpnext.HierarchyChart = class {
|
|||||||
let node = undefined;
|
let node = undefined;
|
||||||
|
|
||||||
$.each(r.message, (_i, data) => {
|
$.each(r.message, (_i, data) => {
|
||||||
if ($(`#${data.id}`).length)
|
if ($(`[id="${data.id}"]`).length)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
node = new me.Node({
|
node = new me.Node({
|
||||||
@@ -263,7 +263,7 @@ erpnext.HierarchyChart = class {
|
|||||||
this.refresh_connectors(node.parent_id);
|
this.refresh_connectors(node.parent_id);
|
||||||
|
|
||||||
// rebuild incoming connections
|
// rebuild incoming connections
|
||||||
let grandparent = $(`#${node.parent_id}`).attr('data-parent');
|
let grandparent = $(`[id="${node.parent_id}"]`).attr('data-parent');
|
||||||
this.refresh_connectors(grandparent);
|
this.refresh_connectors(grandparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +282,7 @@ erpnext.HierarchyChart = class {
|
|||||||
|
|
||||||
show_active_path(node) {
|
show_active_path(node) {
|
||||||
// mark node parent on active path
|
// mark node parent on active path
|
||||||
$(`#${node.parent_id}`).addClass('active-path');
|
$(`[id="${node.parent_id}"]`).addClass('active-path');
|
||||||
}
|
}
|
||||||
|
|
||||||
load_children(node, deep=false) {
|
load_children(node, deep=false) {
|
||||||
@@ -317,7 +317,7 @@ erpnext.HierarchyChart = class {
|
|||||||
|
|
||||||
render_child_nodes(node, child_nodes) {
|
render_child_nodes(node, child_nodes) {
|
||||||
const last_level = this.$hierarchy.find('.level:last').index();
|
const last_level = this.$hierarchy.find('.level:last').index();
|
||||||
const current_level = $(`#${node.id}`).parent().parent().parent().index();
|
const current_level = $(`[id="${node.id}"]`).parent().parent().parent().index();
|
||||||
|
|
||||||
if (last_level === current_level) {
|
if (last_level === current_level) {
|
||||||
this.$hierarchy.append(`
|
this.$hierarchy.append(`
|
||||||
@@ -382,7 +382,7 @@ erpnext.HierarchyChart = class {
|
|||||||
node.$children = $('<ul class="node-children"></ul>');
|
node.$children = $('<ul class="node-children"></ul>');
|
||||||
|
|
||||||
const last_level = this.$hierarchy.find('.level:last').index();
|
const last_level = this.$hierarchy.find('.level:last').index();
|
||||||
const node_level = $(`#${node.id}`).parent().parent().parent().index();
|
const node_level = $(`[id="${node.id}"]`).parent().parent().parent().index();
|
||||||
|
|
||||||
if (last_level === node_level) {
|
if (last_level === node_level) {
|
||||||
this.$hierarchy.append(`
|
this.$hierarchy.append(`
|
||||||
@@ -489,7 +489,7 @@ erpnext.HierarchyChart = class {
|
|||||||
set_path_attributes(path, parent_id, child_id) {
|
set_path_attributes(path, parent_id, child_id) {
|
||||||
path.setAttribute("data-parent", parent_id);
|
path.setAttribute("data-parent", parent_id);
|
||||||
path.setAttribute("data-child", child_id);
|
path.setAttribute("data-child", child_id);
|
||||||
const parent = $(`#${parent_id}`);
|
const parent = $(`[id="${parent_id}"]`);
|
||||||
|
|
||||||
if (parent.hasClass('active')) {
|
if (parent.hasClass('active')) {
|
||||||
path.setAttribute("class", "active-connector");
|
path.setAttribute("class", "active-connector");
|
||||||
@@ -513,7 +513,7 @@ erpnext.HierarchyChart = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
collapse_previous_level_nodes(node) {
|
collapse_previous_level_nodes(node) {
|
||||||
let node_parent = $(`#${node.parent_id}`);
|
let node_parent = $(`[id="${node.parent_id}"]`);
|
||||||
let previous_level_nodes = node_parent.parent().parent().children('li');
|
let previous_level_nodes = node_parent.parent().parent().children('li');
|
||||||
let node_card = undefined;
|
let node_card = undefined;
|
||||||
|
|
||||||
@@ -545,7 +545,7 @@ erpnext.HierarchyChart = class {
|
|||||||
|
|
||||||
setup_node_click_action(node) {
|
setup_node_click_action(node) {
|
||||||
let me = this;
|
let me = this;
|
||||||
let node_element = $(`#${node.id}`);
|
let node_element = $(`[id="${node.id}"]`);
|
||||||
|
|
||||||
node_element.click(function() {
|
node_element.click(function() {
|
||||||
const is_sibling = me.selected_node.parent_id === node.parent_id;
|
const is_sibling = me.selected_node.parent_id === node.parent_id;
|
||||||
@@ -563,7 +563,7 @@ erpnext.HierarchyChart = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setup_edit_node_action(node) {
|
setup_edit_node_action(node) {
|
||||||
let node_element = $(`#${node.id}`);
|
let node_element = $(`[id="${node.id}"]`);
|
||||||
let me = this;
|
let me = this;
|
||||||
|
|
||||||
node_element.find('.btn-edit-node').click(function() {
|
node_element.find('.btn-edit-node').click(function() {
|
||||||
@@ -572,7 +572,7 @@ erpnext.HierarchyChart = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
remove_levels_after_node(node) {
|
remove_levels_after_node(node) {
|
||||||
let level = $(`#${node.id}`).parent().parent().parent().index();
|
let level = $(`[id="${node.id}"]`).parent().parent().parent().index();
|
||||||
|
|
||||||
level = $('.hierarchy > li:eq('+ level + ')');
|
level = $('.hierarchy > li:eq('+ level + ')');
|
||||||
level.nextAll('li').remove();
|
level.nextAll('li').remove();
|
||||||
@@ -595,7 +595,7 @@ erpnext.HierarchyChart = class {
|
|||||||
const parent = $(path).data('parent');
|
const parent = $(path).data('parent');
|
||||||
const child = $(path).data('child');
|
const child = $(path).data('child');
|
||||||
|
|
||||||
if ($(`#${parent}`).length && $(`#${child}`).length)
|
if ($(`[id="${parent}"]`).length && $(`[id="${child}"]`).length)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
$(path).remove();
|
$(path).remove();
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ erpnext.HierarchyChartMobile = class {
|
|||||||
});
|
});
|
||||||
|
|
||||||
node.parent.append(node_card);
|
node.parent.append(node_card);
|
||||||
node.$link = $(`#${node.id}`);
|
node.$link = $(`[id="${node.id}"]`);
|
||||||
node.$link.addClass('mobile-node');
|
node.$link.addClass('mobile-node');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +184,7 @@ erpnext.HierarchyChartMobile = class {
|
|||||||
this.refresh_connectors(node.parent_id, node.id);
|
this.refresh_connectors(node.parent_id, node.id);
|
||||||
|
|
||||||
// rebuild incoming connections of parent
|
// rebuild incoming connections of parent
|
||||||
let grandparent = $(`#${node.parent_id}`).attr('data-parent');
|
let grandparent = $(`[id="${node.parent_id}"]`).attr('data-parent');
|
||||||
this.refresh_connectors(grandparent, node.parent_id);
|
this.refresh_connectors(grandparent, node.parent_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +221,7 @@ erpnext.HierarchyChartMobile = class {
|
|||||||
|
|
||||||
show_active_path(node) {
|
show_active_path(node) {
|
||||||
// mark node parent on active path
|
// mark node parent on active path
|
||||||
$(`#${node.parent_id}`).addClass('active-path');
|
$(`[id="${node.parent_id}"]`).addClass('active-path');
|
||||||
}
|
}
|
||||||
|
|
||||||
load_children(node) {
|
load_children(node) {
|
||||||
@@ -256,7 +256,7 @@ erpnext.HierarchyChartMobile = class {
|
|||||||
if (child_nodes) {
|
if (child_nodes) {
|
||||||
$.each(child_nodes, (_i, data) => {
|
$.each(child_nodes, (_i, data) => {
|
||||||
this.add_node(node, data);
|
this.add_node(node, data);
|
||||||
$(`#${data.id}`).addClass('active-child');
|
$(`[id="${data.id}"]`).addClass('active-child');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.add_connector(node.id, data.id);
|
this.add_connector(node.id, data.id);
|
||||||
@@ -293,9 +293,9 @@ erpnext.HierarchyChartMobile = class {
|
|||||||
|
|
||||||
let connector = undefined;
|
let connector = undefined;
|
||||||
|
|
||||||
if ($(`#${parent_id}`).hasClass('active')) {
|
if ($(`[id="${parent_id}"]`).hasClass('active')) {
|
||||||
connector = this.get_connector_for_active_node(parent_node, child_node);
|
connector = this.get_connector_for_active_node(parent_node, child_node);
|
||||||
} else if ($(`#${parent_id}`).hasClass('active-path')) {
|
} else if ($(`[id="${parent_id}"]`).hasClass('active-path')) {
|
||||||
connector = this.get_connector_for_collapsed_node(parent_node, child_node);
|
connector = this.get_connector_for_collapsed_node(parent_node, child_node);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,7 +351,7 @@ erpnext.HierarchyChartMobile = class {
|
|||||||
set_path_attributes(path, parent_id, child_id) {
|
set_path_attributes(path, parent_id, child_id) {
|
||||||
path.setAttribute("data-parent", parent_id);
|
path.setAttribute("data-parent", parent_id);
|
||||||
path.setAttribute("data-child", child_id);
|
path.setAttribute("data-child", child_id);
|
||||||
const parent = $(`#${parent_id}`);
|
const parent = $(`[id="${parent_id}"]`);
|
||||||
|
|
||||||
if (parent.hasClass('active')) {
|
if (parent.hasClass('active')) {
|
||||||
path.setAttribute("class", "active-connector");
|
path.setAttribute("class", "active-connector");
|
||||||
@@ -374,7 +374,7 @@ erpnext.HierarchyChartMobile = class {
|
|||||||
|
|
||||||
setup_node_click_action(node) {
|
setup_node_click_action(node) {
|
||||||
let me = this;
|
let me = this;
|
||||||
let node_element = $(`#${node.id}`);
|
let node_element = $(`[id="${node.id}"]`);
|
||||||
|
|
||||||
node_element.click(function() {
|
node_element.click(function() {
|
||||||
let el = undefined;
|
let el = undefined;
|
||||||
@@ -398,7 +398,7 @@ erpnext.HierarchyChartMobile = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setup_edit_node_action(node) {
|
setup_edit_node_action(node) {
|
||||||
let node_element = $(`#${node.id}`);
|
let node_element = $(`[id="${node.id}"]`);
|
||||||
let me = this;
|
let me = this;
|
||||||
|
|
||||||
node_element.find('.btn-edit-node').click(function() {
|
node_element.find('.btn-edit-node').click(function() {
|
||||||
@@ -512,7 +512,7 @@ erpnext.HierarchyChartMobile = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
remove_levels_after_node(node) {
|
remove_levels_after_node(node) {
|
||||||
let level = $(`#${node.id}`).parent().parent().index();
|
let level = $(`[id="${node.id}"]`).parent().parent().index();
|
||||||
|
|
||||||
level = $('.hierarchy-mobile > li:eq('+ level + ')');
|
level = $('.hierarchy-mobile > li:eq('+ level + ')');
|
||||||
level.nextAll('li').remove();
|
level.nextAll('li').remove();
|
||||||
@@ -533,7 +533,7 @@ erpnext.HierarchyChartMobile = class {
|
|||||||
const parent = $(path).data('parent');
|
const parent = $(path).data('parent');
|
||||||
const child = $(path).data('child');
|
const child = $(path).data('child');
|
||||||
|
|
||||||
if ($(`#${parent}`).length && $(`#${child}`).length)
|
if ($(`[id="${parent}"]`).length && $(`[id="${child}"]`).length)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
$(path).remove();
|
$(path).remove();
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ body.product-page {
|
|||||||
.carousel-control-prev,
|
.carousel-control-prev,
|
||||||
.carousel-control-next {
|
.carousel-control-next {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
width: 8%;
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.carousel-body {
|
.carousel-body {
|
||||||
@@ -44,6 +52,8 @@ body.product-page {
|
|||||||
|
|
||||||
.carousel-content {
|
.carousel-content {
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
|
margin-left: 5rem;
|
||||||
|
margin-right: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
|
|||||||
@@ -708,6 +708,15 @@ def make_custom_fields(update=True):
|
|||||||
'fieldtype': 'Data',
|
'fieldtype': 'Data',
|
||||||
'insert_after': 'email'
|
'insert_after': 'email'
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
'Finance Book': [
|
||||||
|
{
|
||||||
|
'fieldname': 'for_income_tax',
|
||||||
|
'label': 'For Income Tax',
|
||||||
|
'fieldtype': 'Check',
|
||||||
|
'insert_after': 'finance_book_name',
|
||||||
|
'description': 'If the asset is put to use for less than 180 days, the first Depreciation Rate will be reduced by 50%.'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
create_custom_fields(custom_fields, update=update)
|
create_custom_fields(custom_fields, update=update)
|
||||||
@@ -797,7 +806,7 @@ def set_salary_components(docs):
|
|||||||
|
|
||||||
def set_tax_withholding_category(company):
|
def set_tax_withholding_category(company):
|
||||||
accounts = []
|
accounts = []
|
||||||
fiscal_year = None
|
fiscal_year_details = None
|
||||||
abbr = frappe.get_value("Company", company, "abbr")
|
abbr = frappe.get_value("Company", company, "abbr")
|
||||||
tds_account = frappe.get_value("Account", 'TDS Payable - {0}'.format(abbr), 'name')
|
tds_account = frappe.get_value("Account", 'TDS Payable - {0}'.format(abbr), 'name')
|
||||||
|
|
||||||
|
|||||||
@@ -249,6 +249,9 @@ def is_internal_transfer(party_details, doctype):
|
|||||||
elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
|
elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
|
||||||
destination_gstin = party_details.supplier_gstin
|
destination_gstin = party_details.supplier_gstin
|
||||||
|
|
||||||
|
if not destination_gstin or party_details.gstin:
|
||||||
|
return False
|
||||||
|
|
||||||
if party_details.gstin == destination_gstin:
|
if party_details.gstin == destination_gstin:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
@@ -857,12 +860,13 @@ def get_depreciation_amount(asset, depreciable_value, row):
|
|||||||
rate_of_depreciation = row.rate_of_depreciation
|
rate_of_depreciation = row.rate_of_depreciation
|
||||||
# if its the first depreciation
|
# if its the first depreciation
|
||||||
if depreciable_value == asset.gross_purchase_amount:
|
if depreciable_value == asset.gross_purchase_amount:
|
||||||
# as per IT act, if the asset is purchased in the 2nd half of fiscal year, then rate is divided by 2
|
if row.finance_book and frappe.db.get_value('Finance Book', row.finance_book, 'for_income_tax'):
|
||||||
diff = date_diff(row.depreciation_start_date, asset.available_for_use_date)
|
# as per IT act, if the asset is purchased in the 2nd half of fiscal year, then rate is divided by 2
|
||||||
if diff <= 180:
|
diff = date_diff(row.depreciation_start_date, asset.available_for_use_date)
|
||||||
rate_of_depreciation = rate_of_depreciation / 2
|
if diff <= 180:
|
||||||
frappe.msgprint(
|
rate_of_depreciation = rate_of_depreciation / 2
|
||||||
_('As per IT Act, the rate of depreciation for the first depreciation entry is reduced by 50%.'))
|
frappe.msgprint(
|
||||||
|
_('As per IT Act, the rate of depreciation for the first depreciation entry is reduced by 50%.'))
|
||||||
|
|
||||||
depreciation_amount = flt(depreciable_value * (flt(rate_of_depreciation) / 100))
|
depreciation_amount = flt(depreciable_value * (flt(rate_of_depreciation) / 100))
|
||||||
|
|
||||||
|
|||||||
@@ -510,8 +510,14 @@
|
|||||||
"idx": 363,
|
"idx": 363,
|
||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [
|
||||||
"modified": "2021-08-25 18:56:09.929905",
|
{
|
||||||
|
"group": "Allowed Items",
|
||||||
|
"link_doctype": "Party Specific Item",
|
||||||
|
"link_fieldname": "party"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"modified": "2021-09-06 17:38:54.196663",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Customer",
|
"name": "Customer",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('Supplier Item Group', {
|
frappe.ui.form.on('Party Specific Item', {
|
||||||
// refresh: function(frm) {
|
// refresh: function(frm) {
|
||||||
|
|
||||||
// }
|
// }
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-08-27 19:28:07.559978",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"party_type",
|
||||||
|
"party",
|
||||||
|
"column_break_3",
|
||||||
|
"restrict_based_on",
|
||||||
|
"based_on_value"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "party_type",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Party Type",
|
||||||
|
"options": "Customer\nSupplier",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "party",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Party Name",
|
||||||
|
"options": "party_type",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "restrict_based_on",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Restrict Items Based On",
|
||||||
|
"options": "Item\nItem Group\nBrand",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_3",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "based_on_value",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Based On Value",
|
||||||
|
"options": "restrict_based_on",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-09-14 13:27:58.612334",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Selling",
|
||||||
|
"name": "Party Specific Item",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"title_field": "party",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class PartySpecificItem(Document):
|
||||||
|
def validate(self):
|
||||||
|
exists = frappe.db.exists({
|
||||||
|
'doctype': 'Party Specific Item',
|
||||||
|
'party_type': self.party_type,
|
||||||
|
'party': self.party,
|
||||||
|
'restrict_based_on': self.restrict_based_on,
|
||||||
|
'based_on': self.based_on_value,
|
||||||
|
})
|
||||||
|
if exists:
|
||||||
|
frappe.throw(_("This item filter has already been applied for the {0}").format(self.party_type))
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
from erpnext.controllers.queries import item_query
|
||||||
|
|
||||||
|
test_dependencies = ['Item', 'Customer', 'Supplier']
|
||||||
|
|
||||||
|
def create_party_specific_item(**args):
|
||||||
|
psi = frappe.new_doc("Party Specific Item")
|
||||||
|
psi.party_type = args.get('party_type')
|
||||||
|
psi.party = args.get('party')
|
||||||
|
psi.restrict_based_on = args.get('restrict_based_on')
|
||||||
|
psi.based_on_value = args.get('based_on_value')
|
||||||
|
psi.insert()
|
||||||
|
|
||||||
|
class TestPartySpecificItem(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.customer = frappe.get_last_doc("Customer")
|
||||||
|
self.supplier = frappe.get_last_doc("Supplier")
|
||||||
|
self.item = frappe.get_last_doc("Item")
|
||||||
|
|
||||||
|
def test_item_query_for_customer(self):
|
||||||
|
create_party_specific_item(party_type='Customer', party=self.customer.name, restrict_based_on='Item', based_on_value=self.item.name)
|
||||||
|
filters = {'is_sales_item': 1, 'customer': self.customer.name}
|
||||||
|
items = item_query(doctype= 'Item', txt= '', searchfield= 'name', start= 0, page_len= 20,filters=filters, as_dict= False)
|
||||||
|
for item in items:
|
||||||
|
self.assertEqual(item[0], self.item.name)
|
||||||
|
|
||||||
|
def test_item_query_for_supplier(self):
|
||||||
|
create_party_specific_item(party_type='Supplier', party=self.supplier.name, restrict_based_on='Item Group', based_on_value=self.item.item_group)
|
||||||
|
filters = {'supplier': self.supplier.name, 'is_purchase_item': 1}
|
||||||
|
items = item_query(doctype= 'Item', txt= '', searchfield= 'name', start= 0, page_len= 20,filters=filters, as_dict= False)
|
||||||
|
for item in items:
|
||||||
|
self.assertEqual(item[2], self.item.item_group)
|
||||||
@@ -1480,6 +1480,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
|
||||||
@@ -1512,7 +1513,7 @@
|
|||||||
"idx": 105,
|
"idx": 105,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-09-01 15:12:24.115483",
|
"modified": "2021-09-28 13:09:51.515542",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Sales Order",
|
"name": "Sales Order",
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"editable_price_list_rate",
|
"editable_price_list_rate",
|
||||||
"validate_selling_price",
|
"validate_selling_price",
|
||||||
"editable_bundle_item_rates",
|
"editable_bundle_item_rates",
|
||||||
"transaction_settings_section",
|
"sales_transactions_settings_section",
|
||||||
"so_required",
|
"so_required",
|
||||||
"dn_required",
|
"dn_required",
|
||||||
"sales_update_frequency",
|
"sales_update_frequency",
|
||||||
@@ -143,15 +143,14 @@
|
|||||||
{
|
{
|
||||||
"default": "Stop",
|
"default": "Stop",
|
||||||
"depends_on": "maintain_same_sales_rate",
|
"depends_on": "maintain_same_sales_rate",
|
||||||
"description": "Configure the action to stop the transaction or just warn if the same rate is not maintained.",
|
|
||||||
"fieldname": "maintain_same_rate_action",
|
"fieldname": "maintain_same_rate_action",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Action If Same Rate is Not Maintained",
|
"label": "Action if Same Rate is Not Maintained Throughout Sales Cycle",
|
||||||
"mandatory_depends_on": "maintain_same_sales_rate",
|
"mandatory_depends_on": "maintain_same_sales_rate",
|
||||||
"options": "Stop\nWarn"
|
"options": "Stop\nWarn"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval: doc.maintain_same_rate_action == 'Stop'",
|
"depends_on": "eval: doc.maintain_same_sales_rate && doc.maintain_same_rate_action == 'Stop'",
|
||||||
"fieldname": "role_to_override_stop_action",
|
"fieldname": "role_to_override_stop_action",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Role Allowed to Override Stop Action",
|
"label": "Role Allowed to Override Stop Action",
|
||||||
@@ -191,13 +190,15 @@
|
|||||||
"label": "Item Price Settings"
|
"label": "Item Price Settings"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "transaction_settings_section",
|
"fieldname": "sales_transactions_settings_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Transaction Settings"
|
"label": "Transaction Settings"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_5",
|
"default": "0",
|
||||||
"fieldtype": "Column Break"
|
"fieldname": "editable_bundle_item_rates",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Calculate Product Bundle Price based on Child Items' Rates"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-cog",
|
"icon": "fa fa-cog",
|
||||||
@@ -205,7 +206,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-09-20 19:38:10.175989",
|
"modified": "2021-09-21 19:38:10.175989",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Selling Settings",
|
"name": "Selling Settings",
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
|
|||||||
this.frm.set_query("item_code", "items", function() {
|
this.frm.set_query("item_code", "items", function() {
|
||||||
return {
|
return {
|
||||||
query: "erpnext.controllers.queries.item_query",
|
query: "erpnext.controllers.queries.item_query",
|
||||||
filters: {'is_sales_item': 1}
|
filters: {'is_sales_item': 1, 'customer': cur_frm.doc.customer}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user