From c1ba35b25b056053c886b732cd59ab6c08ba1484 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 20 Jun 2021 12:42:06 +0530 Subject: [PATCH 001/147] feat: Organizational Chart --- erpnext/hr/doctype/employee/employee.py | 5 +- .../hr/page/organizational_chart/__init__.py | 0 .../page/organizational_chart/node_card.html | 27 ++ .../organizational_chart.js | 409 ++++++++++++++++++ .../organizational_chart.json | 26 ++ .../organizational_chart.py | 49 +++ erpnext/public/build.json | 3 +- erpnext/public/scss/organizational_chart.scss | 209 +++++++++ 8 files changed, 725 insertions(+), 3 deletions(-) create mode 100644 erpnext/hr/page/organizational_chart/__init__.py create mode 100644 erpnext/hr/page/organizational_chart/node_card.html create mode 100644 erpnext/hr/page/organizational_chart/organizational_chart.js create mode 100644 erpnext/hr/page/organizational_chart/organizational_chart.json create mode 100644 erpnext/hr/page/organizational_chart/organizational_chart.py create mode 100644 erpnext/public/scss/organizational_chart.scss diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index ed7d588434..7917e3abf5 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -476,13 +476,14 @@ def get_employee_emails(employee_list): return employee_emails @frappe.whitelist() -def get_children(doctype, parent=None, company=None, is_root=False, is_tree=False): +def get_children(doctype, parent=None, company=None, is_root=False, is_tree=False, fields=None): filters = [['status', '!=', 'Left']] if company and company != 'All Companies': filters.append(['company', '=', company]) - fields = ['name as value', 'employee_name as title'] + if not fields: + fields = ['name as value', 'employee_name as title'] if is_root: parent = '' diff --git a/erpnext/hr/page/organizational_chart/__init__.py b/erpnext/hr/page/organizational_chart/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/page/organizational_chart/node_card.html b/erpnext/hr/page/organizational_chart/node_card.html new file mode 100644 index 0000000000..057c45ee86 --- /dev/null +++ b/erpnext/hr/page/organizational_chart/node_card.html @@ -0,0 +1,27 @@ +
+
+
+ + + +
+
+
+ {{ name }} +
+ {{ frappe.utils.icon("edit", "xs") }} + {{ __("Edit") }} +
+
+
+
{{ title }}
+ + {% if connections == 1 %} +
· {{ connections }} {{ __("Connection") }}
+ {% else %} +
· {{ connections }} {{ __("Connections") }}
+ {% endif %} +
+
+
+
\ No newline at end of file diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.js b/erpnext/hr/page/organizational_chart/organizational_chart.js new file mode 100644 index 0000000000..04bd9422bd --- /dev/null +++ b/erpnext/hr/page/organizational_chart/organizational_chart.js @@ -0,0 +1,409 @@ +frappe.pages['organizational-chart'].on_page_load = function(wrapper) { + frappe.ui.make_app_page({ + parent: wrapper, + title: __('Organizational Chart'), + single_column: true + }); + + let organizational_chart = new OrganizationalChart(wrapper); + $(wrapper).bind('show', ()=> { + organizational_chart.show(); + }); +}; + +class OrganizationalChart { + + constructor(wrapper) { + this.wrapper = $(wrapper); + this.page = wrapper.page; + + this.page.main.css({ + 'min-height': '300px', + 'max-height': '600px', + 'overflow': 'auto', + 'position': 'relative' + }); + this.page.main.addClass('frappe-card'); + + this.nodes = {}; + this.setup_node_class(); + } + + setup_node_class() { + let me = this; + this.Node = class { + constructor({ + id, parent, parent_id, image, name, title, expandable, connections, is_root // eslint-disable-line + }) { + // to setup values passed via constructor + $.extend(this, arguments[0]); + + this.expanded = 0; + + me.nodes[this.id] = this; + me.make_node_element(this); + me.setup_node_click_action(this); + } + } + } + + make_node_element(node) { + let node_card = frappe.render_template('node_card', { + id: node.id, + name: node.name, + title: node.title, + image: node.image, + parent: node.parent_id, + connections: node.connections + }); + + node.parent.append(node_card); + node.$link = $(`#${node.id}`); + } + + show() { + frappe.breadcrumbs.add('HR'); + + let me = this; + let company = this.page.add_field({ + fieldtype: 'Link', + options: 'Company', + fieldname: 'company', + placeholder: __('Select Company'), + default: frappe.defaults.get_default('company'), + only_select: true, + reqd: 1, + change: () => { + me.company = undefined; + + if (company.get_value() && me.company != company.get_value()) { + me.company = company.get_value(); + + // svg for connectors + me.make_svg_markers() + + if (me.$hierarchy) + me.$hierarchy.remove(); + + // setup hierarchy + me.$hierarchy = $( + ``); + + me.page.main.append(me.$hierarchy); + me.render_root_node(); + } + } + }); + + company.refresh(); + $(`[data-fieldname="company"]`).trigger('change'); + } + + make_svg_markers() { + $('#arrows').remove(); + + this.page.main.prepend(` + + + + + + + + + + + + + + + + + + + `); + } + + render_root_node() { + this.method = 'erpnext.hr.page.organizational_chart.organizational_chart.get_children'; + + let me = this; + + frappe.call({ + method: me.method, + args: { + company: me.company + }, + callback: function(r) { + if (r.message.length) { + let data = r.message[0]; + + let root_node = new me.Node({ + id: data.name, + parent: me.$hierarchy.find('.root-level'), + parent_id: undefined, + image: data.image, + name: data.employee_name, + title: data.designation, + expandable: true, + connections: data.connections, + is_root: true, + }); + + me.expand_node(root_node); + } + } + }) + } + + expand_node(node) { + let is_sibling = this.selected_node && this.selected_node.parent_id === node.parent_id; + this.set_selected_node(node); + this.show_active_path(node); + this.collapse_previous_level_nodes(node); + + // since the previous node collapses, all connections to that node need to be rebuilt + // if a sibling node is clicked, connections don't need to be rebuilt + if (!is_sibling) { + // rebuild outgoing connections + this.refresh_connectors(node.parent_id); + + // rebuild incoming connections + let grandparent = $(`#${node.parent_id}`).attr('data-parent'); + this.refresh_connectors(grandparent) + } + + if (node.expandable && !node.expanded) { + return this.load_children(node); + } + } + + collapse_node() { + if (this.selected_node.expandable) { + this.selected_node.$children.hide(); + $(`path[data-parent="${this.selected_node.id}"]`).hide(); + this.selected_node.expanded = false; + } + } + + show_active_path(node) { + // mark node parent on active path + $(`#${node.parent_id}`).addClass('active-path'); + } + + load_children(node) { + frappe.run_serially([ + () => this.get_child_nodes(node.id), + (child_nodes) => this.render_child_nodes(node, child_nodes) + ]); + } + + get_child_nodes(node_id) { + let me = this; + return new Promise(resolve => { + frappe.call({ + method: this.method, + args: { + parent: node_id, + company: me.company + }, + callback: (r) => { + resolve(r.message); + } + }); + }); + } + + render_child_nodes(node, child_nodes) { + const last_level = this.$hierarchy.find('.level:last').index(); + const current_level = $(`#${node.id}`).parent().parent().parent().index(); + + if (last_level === current_level) { + this.$hierarchy.append(` +
  • + `); + } + + if (!node.$children) { + node.$children = $('') + .hide() + .appendTo(this.$hierarchy.find('.level:last')); + + node.$children.empty(); + + if (child_nodes) { + $.each(child_nodes, (_i, data) => { + this.add_node(node, data); + + setTimeout(() => { + this.add_connector(node.id, data.name); + }, 250); + }); + } + } + + node.$children.show(); + $(`path[data-parent="${node.id}"]`).show(); + node.expanded = true; + } + + add_node(node, data) { + var $li = $('
  • '); + + return new this.Node({ + id: data.name, + parent: $li.appendTo(node.$children), + parent_id: node.id, + image: data.image, + name: data.employee_name, + title: data.designation, + expandable: data.expandable, + connections: data.connections, + children: undefined + }); + } + + add_connector(parent_id, child_id) { + let parent_node = document.querySelector(`#${parent_id}`); + let child_node = document.querySelector(`#${child_id}`); + + // variable for the namespace + const svgns = 'http://www.w3.org/2000/svg'; + let path = document.createElementNS(svgns, 'path'); + + // we need to connect right side of the parent to the left side of the child node + let pos_parent_right = { + x: parent_node.offsetLeft + parent_node.offsetWidth, + y: parent_node.offsetTop + parent_node.offsetHeight / 2 + }; + let pos_child_left = { + x: child_node.offsetLeft - 5, + y: child_node.offsetTop + child_node.offsetHeight / 2 + }; + + let connector = + "M" + + (pos_parent_right.x) + "," + (pos_parent_right.y) + " " + + "C" + + (pos_parent_right.x + 100) + "," + (pos_parent_right.y) + " " + + (pos_child_left.x - 100) + "," + (pos_child_left.y) + " " + + (pos_child_left.x) + "," + (pos_child_left.y); + + path.setAttribute("d", connector); + path.setAttribute("data-parent", parent_id); + path.setAttribute("data-child", child_id); + + if ($(`#${parent_id}`).hasClass('active')) { + path.setAttribute("class", "active-connector"); + path.setAttribute("marker-start", "url(#arrowstart-active)"); + path.setAttribute("marker-end", "url(#arrowhead-active)"); + } else if ($(`#${parent_id}`).hasClass('active-path')) { + path.setAttribute("class", "collapsed-connector"); + path.setAttribute("marker-start", "url(#arrowstart-collapsed)"); + path.setAttribute("marker-end", "url(#arrowhead-collapsed)"); + } + + $('#connectors').append(path); + } + + set_selected_node(node) { + // remove .active class from the current node + $('.active').removeClass('active'); + + // add active class to the newly selected node + this.selected_node = node; + node.$link.addClass('active'); + } + + collapse_previous_level_nodes(node) { + let node_parent = $(`#${node.parent_id}`); + + let previous_level_nodes = node_parent.parent().parent().children('li'); + if (node_parent.parent().hasClass('root-level')) { + previous_level_nodes = node_parent.parent().children('li'); + } + + let node_card = undefined; + + previous_level_nodes.each(function() { + node_card = $(this).find('.node-card'); + + if (!node_card.hasClass('active-path')) { + node_card.addClass('collapsed'); + } + }); + } + + refresh_connectors(node_parent) { + if (!node_parent) return; + + $(`path[data-parent="${node_parent}"]`).remove(); + + frappe.run_serially([ + () => this.get_child_nodes(node_parent), + (child_nodes) => { + if (child_nodes) { + $.each(child_nodes, (_i, data) => { + this.add_connector(node_parent, data.name); + }); + } + } + ]); + } + + setup_node_click_action(node) { + let me = this; + let node_element = $(`#${node.id}`); + + node_element.click(function() { + let is_sibling = me.selected_node.parent_id === node.parent_id; + + if (is_sibling) { + me.collapse_node(); + } else if (node_element.is(':visible') + && (node_element.hasClass('collapsed') || node_element.hasClass('active-path'))) { + me.remove_levels_after_node(node); + me.remove_orphaned_connectors(); + } + + me.expand_node(node); + }); + } + + remove_levels_after_node(node) { + let level = $(`#${node.id}`).parent().parent().parent(); + + if ($(`#${node.id}`).parent().hasClass('root-level')) { + level = $(`#${node.id}`).parent(); + } + + level = $('.hierarchy > li:eq('+ level.index() + ')'); + level.nextAll('li').remove(); + + let nodes = level.find('.node-card'); + let node_object = undefined; + + $.each(nodes, (_i, element) => { + node_object = this.nodes[element.id]; + node_object.expanded = 0; + node_object.$children = undefined; + }); + + nodes.removeClass('collapsed active-path'); + } + + remove_orphaned_connectors() { + let paths = $('#connectors > path'); + $.each(paths, (_i, path) => { + let parent = $(path).data('parent'); + let child = $(path).data('child'); + + if ($(parent).length || $(child).length) + return; + + $(path).remove(); + }) + } +} diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.json b/erpnext/hr/page/organizational_chart/organizational_chart.json new file mode 100644 index 0000000000..d802781320 --- /dev/null +++ b/erpnext/hr/page/organizational_chart/organizational_chart.json @@ -0,0 +1,26 @@ +{ + "content": null, + "creation": "2021-05-25 10:53:10.107241", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2021-05-25 10:53:18.201931", + "modified_by": "Administrator", + "module": "HR", + "name": "organizational-chart", + "owner": "Administrator", + "page_name": "Organizational Chart", + "roles": [ + { + "role": "HR User" + }, + { + "role": "HR Manager" + } + ], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 0, + "title": "Organizational Chart" +} \ No newline at end of file diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.py b/erpnext/hr/page/organizational_chart/organizational_chart.py new file mode 100644 index 0000000000..be2964530b --- /dev/null +++ b/erpnext/hr/page/organizational_chart/organizational_chart.py @@ -0,0 +1,49 @@ +from __future__ import unicode_literals +import frappe + +@frappe.whitelist() +def get_children(parent=None, company=None, is_root=False, is_tree=False, fields=None): + + filters = [['status', '!=', 'Left']] + if company and company != 'All Companies': + filters.append(['company', '=', company]) + + if not fields: + fields = ['employee_name', 'name', 'reports_to', 'image', 'designation'] + + if is_root: + parent = '' + if parent and company and parent!=company: + filters.append(['reports_to', '=', parent]) + else: + filters.append(['reports_to', '=', '']) + + employees = frappe.get_list('Employee', fields=fields, + filters=filters, order_by='name') + + for employee in employees: + is_expandable = frappe.get_all('Employee', filters=[ + ['reports_to', '=', employee.get('name')] + ]) + employee.connections = get_connections(employee.name) + employee.expandable = 1 if is_expandable else 0 + + return employees + + +def get_connections(employee): + num_connections = 0 + + connections = frappe.get_list('Employee', filters=[ + ['reports_to', '=', employee] + ]) + num_connections += len(connections) + + while connections: + for entry in connections: + connections = frappe.get_list('Employee', filters=[ + ['reports_to', '=', entry.name] + ]) + num_connections += len(connections) + + return num_connections \ No newline at end of file diff --git a/erpnext/public/build.json b/erpnext/public/build.json index 7a3cb838a9..d3ebcdf7e7 100644 --- a/erpnext/public/build.json +++ b/erpnext/public/build.json @@ -3,7 +3,8 @@ "public/less/erpnext.less", "public/less/hub.less", "public/scss/call_popup.scss", - "public/scss/point-of-sale.scss" + "public/scss/point-of-sale.scss", + "public/scss/organizational_chart.scss" ], "css/marketplace.css": [ "public/less/hub.less" diff --git a/erpnext/public/scss/organizational_chart.scss b/erpnext/public/scss/organizational_chart.scss new file mode 100644 index 0000000000..62f6ddcb6e --- /dev/null +++ b/erpnext/public/scss/organizational_chart.scss @@ -0,0 +1,209 @@ +.node-card { + background: white; + stroke: 1px solid var(--gray-200); + box-shadow: var(--shadow-base); + border-radius: 0.5rem; + padding: 0.75rem; + margin-left: 3rem; + width: 18rem; + + .btn-edit-node { + display: none; + } + + .edit-chart-node { + display: none; + } + + .node-edit-icon { + display: none; + } +} + +.node-image { + width: 3.0rem; + height: 3.0rem; +} + +.node-name { + font-size: 1rem; + line-height: 1.72; +} + +.node-title { + font-size: 0.75rem; + line-height: 1.35; +} + +.node-connections { + font-size: 0.75rem; + line-height: 1.35; +} + +.node-card.active { + background: var(--blue-50); + border: 1px solid var(--blue-500); + box-shadow: var(--shadow-md); + border-radius: 0.5rem; + padding: 0.75rem; + width: 18rem; + height: 5rem; + + .btn-edit-node { + display: flex; + background: var(--blue-100); + color: var(--blue-500); + padding: .25rem .5rem; + font-size: .75rem; + justify-content: center; + box-shadow: var(--shadow-sm); + } + + .edit-chart-node { + display: block; + } + + .node-edit-icon { + display: block; + } + + .edit-chart-node { + margin-right: 0.25rem; + } + + .node-edit-icon > .icon{ + stroke: var(--blue-500); + } + + .node-name { + align-items: center; + justify-content: space-between; + margin-bottom: 2px; + } +} + +.node-card.active-path { + background: var(--blue-100); + border: 1px solid var(--blue-300); + box-shadow: var(--shadow-sm); + border-radius: 0.5rem; + padding: 0.75rem; + width: 15rem; + height: 3.0rem; + + .btn-edit-node { + display: none !important; + } + + .edit-chart-node { + display: none; + } + + .node-edit-icon { + display: none; + } + + .node-info { + display: none; + } + + .node-title { + display: none; + } + + .node-connections { + display: none; + } + + .node-name { + font-size: 0.85rem; + line-height: 1.35; + } + + .node-image { + width: 1.5rem; + height: 1.5rem; + } + + .node-meta { + align-items: baseline; + } +} + +.node-card.collapsed { + background: white; + stroke: 1px solid var(--gray-200); + box-shadow: var(--shadow-sm); + border-radius: 0.5rem; + padding: 0.75rem; + width: 15rem; + height: 3.0rem; + + .btn-edit-node { + display: none !important; + } + + .edit-chart-node { + display: none; + } + + .node-edit-icon { + display: none; + } + + .node-info { + display: none; + } + + .node-title { + display: none; + } + + .node-connections { + display: none; + } + + .node-name { + font-size: 0.85rem; + line-height: 1.35; + } + + .node-image { + width: 1.5rem; + height: 1.5rem; + } + + .node-meta { + align-items: baseline; + } +} + +// horizontal hierarchy tree view +.hierarchy { + display: flex; + padding-top: 30px; +} + +.hierarchy li { + list-style-type: none; +} + +.child-node { + margin: 0px 0px 16px 0px; +} + +.level { + margin-right: 8px; +} + +#arrows { + position: absolute; +} + +.active-connector { + stroke: var(--blue-500); +} + +.collapsed-connector { + stroke: var(--blue-300); +} From cce19db826be54b401a9514cb6517810c98e6f7c Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 21 Jun 2021 21:55:50 +0530 Subject: [PATCH 002/147] feat: org chart mobile interactions --- .../page/organizational_chart/node_card.html | 4 +- .../organizational_chart.js | 329 +++++++++++++++++- .../organizational_chart.py | 6 +- erpnext/public/scss/organizational_chart.scss | 70 ++++ 4 files changed, 404 insertions(+), 5 deletions(-) diff --git a/erpnext/hr/page/organizational_chart/node_card.html b/erpnext/hr/page/organizational_chart/node_card.html index 057c45ee86..e42e54f690 100644 --- a/erpnext/hr/page/organizational_chart/node_card.html +++ b/erpnext/hr/page/organizational_chart/node_card.html @@ -17,9 +17,9 @@
    {{ title }}
    {% if connections == 1 %} -
    · {{ connections }} {{ __("Connection") }}
    +
    · {{ connections }}
    {% else %} -
    · {{ connections }} {{ __("Connections") }}
    +
    · {{ connections }}
    {% endif %} diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.js b/erpnext/hr/page/organizational_chart/organizational_chart.js index 04bd9422bd..5739a112de 100644 --- a/erpnext/hr/page/organizational_chart/organizational_chart.js +++ b/erpnext/hr/page/organizational_chart/organizational_chart.js @@ -5,13 +5,20 @@ frappe.pages['organizational-chart'].on_page_load = function(wrapper) { single_column: true }); - let organizational_chart = new OrganizationalChart(wrapper); + // let organizational_chart = undefined; + // if (frappe.is_mobile()) { + // organizational_chart = new OrgChartMobile(wrapper); + // } else { + // organizational_chart = new OrgChart(wrapper); + // } + + let organizational_chart = new OrgChartMobile(wrapper); $(wrapper).bind('show', ()=> { organizational_chart.show(); }); }; -class OrganizationalChart { +class OrgChart { constructor(wrapper) { this.wrapper = $(wrapper); @@ -407,3 +414,321 @@ class OrganizationalChart { }) } } + + +class OrgChartMobile { + + constructor(wrapper) { + this.wrapper = $(wrapper); + this.page = wrapper.page; + + this.page.main.css({ + 'min-height': '300px', + 'max-height': '600px', + 'overflow': 'auto', + 'position': 'relative' + }); + this.page.main.addClass('frappe-card'); + + this.nodes = {}; + this.setup_node_class(); + } + + setup_node_class() { + let me = this; + this.Node = class { + constructor({ + id, parent, parent_id, image, name, title, expandable, connections, is_root // eslint-disable-line + }) { + // to setup values passed via constructor + $.extend(this, arguments[0]); + + this.expanded = 0; + + me.nodes[this.id] = this; + me.make_node_element(this); + me.setup_node_click_action(this); + } + } + } + + make_node_element(node) { + let node_card = frappe.render_template('node_card', { + id: node.id, + name: node.name, + title: node.title, + image: node.image, + parent: node.parent_id, + connections: node.connections, + is_mobile: 1 + }); + + node.parent.append(node_card); + node.$link = $(`#${node.id}`); + node.$link.addClass('mobile-node'); + } + + show() { + frappe.breadcrumbs.add('HR'); + + let me = this; + let company = this.page.add_field({ + fieldtype: 'Link', + options: 'Company', + fieldname: 'company', + placeholder: __('Select Company'), + default: frappe.defaults.get_default('company'), + only_select: true, + reqd: 1, + change: () => { + me.company = undefined; + + if (company.get_value() && me.company != company.get_value()) { + me.company = company.get_value(); + + if (me.$hierarchy) + me.$hierarchy.remove(); + + // setup hierarchy + me.$hierarchy = $( + ``); + + me.page.main.append(me.$hierarchy); + me.render_root_node(); + } + } + }); + + company.refresh(); + $(`[data-fieldname="company"]`).trigger('change'); + } + + render_root_node() { + this.method = 'erpnext.hr.page.organizational_chart.organizational_chart.get_children'; + + let me = this; + + frappe.call({ + method: me.method, + args: { + company: me.company + }, + callback: function(r) { + if (r.message.length) { + let data = r.message[0]; + + let root_node = new me.Node({ + id: data.name, + parent: me.$hierarchy.find('.root-level'), + parent_id: undefined, + image: data.image, + name: data.employee_name, + title: data.designation, + expandable: true, + connections: data.connections, + is_root: true, + }); + + me.expand_node(root_node); + } + } + }) + } + + expand_node(node) { + this.set_selected_node(node); + this.show_active_path(node); + + if (node.expandable && !node.expanded) { + return this.load_children(node); + } + } + + collapse_node() { + let node = this.selected_node; + if (node.expandable) { + node.$children.hide(); + node.expanded = false; + + // add a collapsed level to show the collapsed parent + // and a button beside it to move to that level + let node_parent = node.$link.parent(); + node_parent.prepend( + `
    ` + ); + + node_parent + .find('.collapsed-level') + .append(node.$link); + + frappe.run_serially([ + () => this.get_child_nodes(node.parent_id, node.id), + (child_nodes) => this.get_node_group(child_nodes, node.id), + (node_group) => { + node_parent.find('.collapsed-level') + .append(node_group); + } + ]); + } + } + + show_active_path(node) { + // mark node parent on active path + $(`#${node.parent_id}`).addClass('active-path'); + } + + load_children(node) { + frappe.run_serially([ + () => this.get_child_nodes(node.id), + (child_nodes) => this.render_child_nodes(node, child_nodes) + ]); + } + + get_child_nodes(node_id, exclude_node=null) { + let me = this; + return new Promise(resolve => { + frappe.call({ + method: this.method, + args: { + parent: node_id, + company: me.company, + exclude_node: exclude_node + }, + callback: (r) => { + resolve(r.message); + } + }); + }); + } + + render_child_nodes(node, child_nodes) { + if (!node.$children) { + node.$children = $('') + .hide() + .appendTo(node.$link.parent()); + + node.$children.empty(); + + if (child_nodes) { + $.each(child_nodes, (_i, data) => { + this.add_node(node, data); + $(`#${data.name}`).addClass('active-child'); + }); + } + } + + node.$children.show(); + node.expanded = true; + } + + add_node(node, data) { + var $li = $('
  • '); + + return new this.Node({ + id: data.name, + parent: $li.appendTo(node.$children), + parent_id: node.id, + image: data.image, + name: data.employee_name, + title: data.designation, + expandable: data.expandable, + connections: data.connections, + children: undefined + }); + } + + set_selected_node(node) { + // remove .active class from the current node + $('.active').removeClass('active'); + + // add active class to the newly selected node + this.selected_node = node; + node.$link.addClass('active'); + } + + setup_node_click_action(node) { + let me = this; + let node_element = $(`#${node.id}`); + let node_object = null; + + node_element.click(function() { + if (node_element.is(':visible') && node_element.hasClass('active-path')) { + me.remove_levels_after_node(node); + } else { + me.add_node_to_hierarchy(node, true); + me.collapse_node(); + } + + me.expand_node(node); + }); + } + + add_node_to_hierarchy(node) { + this.$hierarchy.append(` +
  • +
    +
    +
  • + `); + + node.$link.appendTo(this.$hierarchy.find('.level:last')); + } + + get_node_group(nodes, sibling) { + let limit = 2; + const display_nodes = nodes.slice(0, limit); + const extra_nodes = nodes.slice(limit); + + let html = display_nodes.map(node => + this.get_avatar(node) + ).join(''); + + if (extra_nodes.length === 1) { + let node = extra_nodes[0]; + html += this.get_avatar(node); + } else if (extra_nodes.length > 1) { + html = ` + ${html} + +
    + +${extra_nodes.length} +
    +
    + `; + } + + const $node_group = + $(`
    +
    + ${html} +
    +
    `); + + return $node_group; + } + + get_avatar(node) { + return ` + + ` + } + + remove_levels_after_node(node) { + let level = $(`#${node.id}`).parent().parent(); + + level = $('.hierarchy-mobile > li:eq('+ (level.index()) + ')'); + level.nextAll('li').remove(); + + let current_node = level.find(`#${node.id}`); + let node_object = this.nodes[node.id]; + + current_node.removeClass('active-child active-path'); + node_object.expanded = 0; + node_object.$children = undefined; + + level.empty().append(current_node); + } +} \ No newline at end of file diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.py b/erpnext/hr/page/organizational_chart/organizational_chart.py index be2964530b..ae91a919b2 100644 --- a/erpnext/hr/page/organizational_chart/organizational_chart.py +++ b/erpnext/hr/page/organizational_chart/organizational_chart.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import frappe @frappe.whitelist() -def get_children(parent=None, company=None, is_root=False, is_tree=False, fields=None): +def get_children(parent=None, company=None, exclude_node=None, is_root=False, is_tree=False, fields=None): filters = [['status', '!=', 'Left']] if company and company != 'All Companies': @@ -13,6 +13,10 @@ def get_children(parent=None, company=None, is_root=False, is_tree=False, fields if is_root: parent = '' + + if exclude_node: + filters.append(['name', '!=', exclude_node]) + if parent and company and parent!=company: filters.append(['reports_to', '=', parent]) else: diff --git a/erpnext/public/scss/organizational_chart.scss b/erpnext/public/scss/organizational_chart.scss index 62f6ddcb6e..02446be11a 100644 --- a/erpnext/public/scss/organizational_chart.scss +++ b/erpnext/public/scss/organizational_chart.scss @@ -6,6 +6,7 @@ padding: 0.75rem; margin-left: 3rem; width: 18rem; + overflow: hidden; .btn-edit-node { display: none; @@ -207,3 +208,72 @@ .collapsed-connector { stroke: var(--blue-300); } + +// mobile + +.hierarchy-mobile { + display: flex; + flex-direction: column; + align-items: center; + padding-top: 30px; + padding-left: 0px; +} + +.hierarchy-mobile li { + list-style-type: none; + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.mobile-node { + margin-left: 0; +} + +.mobile-node.active-path { + width: 12.25rem; +} + +.active-child { + width: 15.5rem; +} + +.mobile-node .node-connections { + max-width: 80px; +} + +.hierarchy-mobile .node-children { + margin-top: 16px; +} + +// node group + +.collapsed-level { + margin-bottom: 16px; +} + +.node-group { + background: white; + border: 1px solid var(--gray-300); + box-shadow: var(--shadow-sm); + border-radius: 0.5rem; + padding: 0.75rem; + margin-left: 12px; + width: 5rem; + height: 3rem; + overflow: hidden; +} + +.node-group .avatar-group { + margin-left: 0px; +} + +.node-group .avatar-extra-count { + background-color: var(--blue-100); + color: var(--blue-500); +} + +.node-group .avatar-frame { + width: 1.5rem; + height: 1.5rem; +} \ No newline at end of file From 6e3a7b4a751d6fa53b8659695753cf17518f1579 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 29 Jun 2021 11:12:47 +0530 Subject: [PATCH 003/147] feat(mobile): sibling node group expansion and rendering --- .../organizational_chart.js | 64 +++++++++++++++---- erpnext/public/scss/organizational_chart.scss | 9 ++- 2 files changed, 60 insertions(+), 13 deletions(-) diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.js b/erpnext/hr/page/organizational_chart/organizational_chart.js index 5739a112de..edaf46162e 100644 --- a/erpnext/hr/page/organizational_chart/organizational_chart.js +++ b/erpnext/hr/page/organizational_chart/organizational_chart.js @@ -565,11 +565,12 @@ class OrgChartMobile { frappe.run_serially([ () => this.get_child_nodes(node.parent_id, node.id), - (child_nodes) => this.get_node_group(child_nodes, node.id), + (child_nodes) => this.get_node_group(child_nodes, node.parent_id), (node_group) => { node_parent.find('.collapsed-level') .append(node_group); - } + }, + () => this.setup_node_group_action() ]); } } @@ -651,7 +652,6 @@ class OrgChartMobile { setup_node_click_action(node) { let me = this; let node_element = $(`#${node.id}`); - let node_object = null; node_element.click(function() { if (node_element.is(':visible') && node_element.hasClass('active-path')) { @@ -665,6 +665,15 @@ class OrgChartMobile { }); } + setup_node_group_action() { + let me = this; + + $('.node-group').on('click', function() { + let parent = $(this).attr('data-parent'); + me.expand_sibling_group_node(parent); + }); + } + add_node_to_hierarchy(node) { this.$hierarchy.append(`
  • @@ -676,7 +685,7 @@ class OrgChartMobile { node.$link.appendTo(this.$hierarchy.find('.level:last')); } - get_node_group(nodes, sibling) { + get_node_group(nodes, parent, collapsed=true) { let limit = 2; const display_nodes = nodes.slice(0, limit); const extra_nodes = nodes.slice(limit); @@ -700,14 +709,23 @@ class OrgChartMobile { `; } - const $node_group = - $(`
    -
    - ${html} -
    -
    `); + if (html) { + const $node_group = + $(`
    +
    + ${html} +
    +
    `); - return $node_group; + if (collapsed) + $node_group.addClass('collapsed'); + else + $node_group.addClass('mb-4'); + + return $node_group; + } + + return null; } get_avatar(node) { @@ -716,6 +734,30 @@ class OrgChartMobile { ` } + expand_sibling_group_node(parent) { + let node_object = this.nodes[parent]; + let node = node_object.$link; + node.removeClass('active-child active-path'); + node_object.expanded = 0; + node_object.$children = undefined; + + // show parent's siblings and expand parent node + frappe.run_serially([ + () => this.get_child_nodes(node_object.parent_id, node_object.id), + (child_nodes) => this.get_node_group(child_nodes, node_object.parent_id, false), + (node_group) => { + this.$hierarchy.empty().append(node_group) }, + () => this.setup_node_group_action(), + () => { + this.$hierarchy.append(` +
  • + `); + this.$hierarchy.append(node); + this.expand_node(node_object); + } + ]); + } + remove_levels_after_node(node) { let level = $(`#${node.id}`).parent().parent(); diff --git a/erpnext/public/scss/organizational_chart.scss b/erpnext/public/scss/organizational_chart.scss index 02446be11a..b6d50a0470 100644 --- a/erpnext/public/scss/organizational_chart.scss +++ b/erpnext/public/scss/organizational_chart.scss @@ -258,10 +258,10 @@ box-shadow: var(--shadow-sm); border-radius: 0.5rem; padding: 0.75rem; - margin-left: 12px; - width: 5rem; + width: 18rem; height: 3rem; overflow: hidden; + align-items: center; } .node-group .avatar-group { @@ -276,4 +276,9 @@ .node-group .avatar-frame { width: 1.5rem; height: 1.5rem; +} + +.node-group.collapsed { + width: 5rem; + margin-left: 12px; } \ No newline at end of file From 25e8723032aec69e7347ece7e714331cc2a35815 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 29 Jun 2021 15:06:09 +0530 Subject: [PATCH 004/147] fix: expanded node group interactions and visibility --- .../organizational_chart.js | 23 +++++++++++++++---- erpnext/public/scss/organizational_chart.scss | 9 +++++++- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.js b/erpnext/hr/page/organizational_chart/organizational_chart.js index edaf46162e..f693cf6ba6 100644 --- a/erpnext/hr/page/organizational_chart/organizational_chart.js +++ b/erpnext/hr/page/organizational_chart/organizational_chart.js @@ -486,6 +486,13 @@ class OrgChartMobile { if (company.get_value() && me.company != company.get_value()) { me.company = company.get_value(); + if (me.$sibling_group) + me.$sibling_group.remove(); + + // setup sibling group wrapper + me.$sibling_group = $(`
    `); + me.page.main.append(me.$sibling_group); + if (me.$hierarchy) me.$hierarchy.remove(); @@ -541,6 +548,12 @@ class OrgChartMobile { this.set_selected_node(node); this.show_active_path(node); + if (this.$sibling_group) { + const sibling_parent = this.$sibling_group.find('.node-group').attr('data-parent'); + if (node.parent_id !== sibling_parent) + this.$sibling_group.empty(); + } + if (node.expandable && !node.expanded) { return this.load_children(node); } @@ -719,8 +732,6 @@ class OrgChartMobile { if (collapsed) $node_group.addClass('collapsed'); - else - $node_group.addClass('mb-4'); return $node_group; } @@ -746,13 +757,15 @@ class OrgChartMobile { () => this.get_child_nodes(node_object.parent_id, node_object.id), (child_nodes) => this.get_node_group(child_nodes, node_object.parent_id, false), (node_group) => { - this.$hierarchy.empty().append(node_group) }, + if (node_group) + this.$sibling_group.empty().append(node_group); + }, () => this.setup_node_group_action(), () => { - this.$hierarchy.append(` + this.$hierarchy.empty().append(`
  • `); - this.$hierarchy.append(node); + this.$hierarchy.find('.level').append(node); this.expand_node(node_object); } ]); diff --git a/erpnext/public/scss/organizational_chart.scss b/erpnext/public/scss/organizational_chart.scss index b6d50a0470..6012c01573 100644 --- a/erpnext/public/scss/organizational_chart.scss +++ b/erpnext/public/scss/organizational_chart.scss @@ -215,7 +215,7 @@ display: flex; flex-direction: column; align-items: center; - padding-top: 30px; + padding-top: 10px; padding-left: 0px; } @@ -250,6 +250,7 @@ .collapsed-level { margin-bottom: 16px; + width: 18rem; } .node-group { @@ -281,4 +282,10 @@ .node-group.collapsed { width: 5rem; margin-left: 12px; +} + +.sibling-group { + display: flex; + flex-direction: column; + align-items: center; } \ No newline at end of file From f5314293c6215841e87dab7462fc44d3c777804d Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 29 Jun 2021 17:48:44 +0530 Subject: [PATCH 005/147] feat: connectors for mobile node cards --- .../organizational_chart.js | 138 ++++++++++++++++++ erpnext/public/scss/organizational_chart.scss | 1 + 2 files changed, 139 insertions(+) diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.js b/erpnext/hr/page/organizational_chart/organizational_chart.js index f693cf6ba6..15334bd4ca 100644 --- a/erpnext/hr/page/organizational_chart/organizational_chart.js +++ b/erpnext/hr/page/organizational_chart/organizational_chart.js @@ -486,6 +486,9 @@ class OrgChartMobile { if (company.get_value() && me.company != company.get_value()) { me.company = company.get_value(); + // svg for connectors + me.make_svg_markers() + if (me.$sibling_group) me.$sibling_group.remove(); @@ -512,6 +515,31 @@ class OrgChartMobile { $(`[data-fieldname="company"]`).trigger('change'); } + make_svg_markers() { + $('#arrows').remove(); + + this.page.main.prepend(` + + + + + + + + + + + + + + + + + + + `); + } + render_root_node() { this.method = 'erpnext.hr.page.organizational_chart.organizational_chart.get_children'; @@ -554,6 +582,14 @@ class OrgChartMobile { this.$sibling_group.empty(); } + // since the previous/parent node collapses, all connections to that node need to be rebuilt + // rebuild outgoing connections of parent + this.refresh_connectors(node.parent_id, node.id); + + // rebuild incoming connections of parent + let grandparent = $(`#${node.parent_id}`).attr('data-parent'); + this.refresh_connectors(grandparent, node.parent_id); + if (node.expandable && !node.expanded) { return this.load_children(node); } @@ -629,6 +665,10 @@ class OrgChartMobile { $.each(child_nodes, (_i, data) => { this.add_node(node, data); $(`#${data.name}`).addClass('active-child'); + + setTimeout(() => { + this.add_connector(node.id, data.name); + }, 250); }); } } @@ -653,6 +693,83 @@ class OrgChartMobile { }); } + add_connector(parent_id, child_id) { + let parent_node = document.querySelector(`#${parent_id}`); + let child_node = document.querySelector(`#${child_id}`); + + // variable for the namespace + const svgns = 'http://www.w3.org/2000/svg'; + let path = document.createElementNS(svgns, 'path'); + + let connector = undefined; + + if ($(`#${parent_id}`).hasClass('active')) { + connector = this.get_connector_for_active_node(parent_node, child_node); + } else if ($(`#${parent_id}`).hasClass('active-path')) { + connector = this.get_connector_for_collapsed_node(parent_node, child_node); + } + + path.setAttribute("d", connector); + this.set_path_attributes(path, parent_id, child_id); + + $('#connectors').append(path); + } + + get_connector_for_active_node(parent_node, child_node) { + // we need to connect the bottom left of the parent to the left side of the child node + let pos_parent_bottom = { + x: parent_node.offsetLeft + 20, + y: parent_node.offsetTop + parent_node.offsetHeight + }; + let pos_child_left = { + x: child_node.offsetLeft - 5, + y: child_node.offsetTop + child_node.offsetHeight / 2 + }; + + let connector = + "M" + + (pos_parent_bottom.x) + "," + (pos_parent_bottom.y) + " " + + "L" + + (pos_parent_bottom.x) + "," + (pos_child_left.y) + " " + + "L" + + (pos_child_left.x) + "," + (pos_child_left.y); + + return connector; + } + + get_connector_for_collapsed_node(parent_node, child_node) { + // we need to connect the bottom left of the parent to the top left of the child node + let pos_parent_bottom = { + x: parent_node.offsetLeft + 20, + y: parent_node.offsetTop + parent_node.offsetHeight + }; + let pos_child_top = { + x: child_node.offsetLeft + 20, + y: child_node.offsetTop + }; + + let connector = + "M" + + (pos_parent_bottom.x) + "," + (pos_parent_bottom.y) + " " + + "L" + + (pos_child_top.x) + "," + (pos_child_top.y); + + return connector; + } + + set_path_attributes(path, parent_id, child_id) { + path.setAttribute("data-parent", parent_id); + path.setAttribute("data-child", child_id); + + if ($(`#${parent_id}`).hasClass('active')) { + path.setAttribute("class", "active-connector"); + path.setAttribute("marker-start", "url(#arrowstart-active)"); + path.setAttribute("marker-end", "url(#arrowhead-active)"); + } else if ($(`#${parent_id}`).hasClass('active-path')) { + path.setAttribute("class", "collapsed-connector"); + } + } + set_selected_node(node) { // remove .active class from the current node $('.active').removeClass('active'); @@ -669,6 +786,7 @@ class OrgChartMobile { node_element.click(function() { if (node_element.is(':visible') && node_element.hasClass('active-path')) { me.remove_levels_after_node(node); + me.remove_orphaned_connectors(); } else { me.add_node_to_hierarchy(node, true); me.collapse_node(); @@ -786,4 +904,24 @@ class OrgChartMobile { level.empty().append(current_node); } + + remove_orphaned_connectors() { + let paths = $('#connectors > path'); + $.each(paths, (_i, path) => { + let parent = $(path).data('parent'); + let child = $(path).data('child'); + + if ($(parent).length || $(child).length) + return; + + $(path).remove(); + }) + } + + refresh_connectors(node_parent, node_id) { + if (!node_parent) return; + + $(`path[data-parent="${node_parent}"]`).remove(); + this.add_connector(node_parent, node_id); + } } \ No newline at end of file diff --git a/erpnext/public/scss/organizational_chart.scss b/erpnext/public/scss/organizational_chart.scss index 6012c01573..16b8792432 100644 --- a/erpnext/public/scss/organizational_chart.scss +++ b/erpnext/public/scss/organizational_chart.scss @@ -199,6 +199,7 @@ #arrows { position: absolute; + overflow: visible; } .active-connector { From b7c61ff6510b4c6afcda7f315388c61068c13765 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 29 Jun 2021 18:21:42 +0530 Subject: [PATCH 006/147] fix: don't refresh connections for same node - remove all connectors while expanding a group node --- .../organizational_chart/organizational_chart.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.js b/erpnext/hr/page/organizational_chart/organizational_chart.js index 15334bd4ca..efb367ad44 100644 --- a/erpnext/hr/page/organizational_chart/organizational_chart.js +++ b/erpnext/hr/page/organizational_chart/organizational_chart.js @@ -573,6 +573,7 @@ class OrgChartMobile { } expand_node(node) { + const is_same_node = (this.selected_node && this.selected_node.id === node.id); this.set_selected_node(node); this.show_active_path(node); @@ -582,13 +583,15 @@ class OrgChartMobile { this.$sibling_group.empty(); } - // since the previous/parent node collapses, all connections to that node need to be rebuilt - // rebuild outgoing connections of parent - this.refresh_connectors(node.parent_id, node.id); + if (!is_same_node) { + // since the previous/parent node collapses, all connections to that node need to be rebuilt + // rebuild outgoing connections of parent + this.refresh_connectors(node.parent_id, node.id); - // rebuild incoming connections of parent - let grandparent = $(`#${node.parent_id}`).attr('data-parent'); - this.refresh_connectors(grandparent, node.parent_id); + // rebuild incoming connections of parent + let grandparent = $(`#${node.parent_id}`).attr('data-parent'); + this.refresh_connectors(grandparent, node.parent_id); + } if (node.expandable && !node.expanded) { return this.load_children(node); @@ -884,6 +887,7 @@ class OrgChartMobile {
  • `); this.$hierarchy.find('.level').append(node); + $(`#connectors`).empty(); this.expand_node(node_object); } ]); From bcc998e8c236766f4a8eadd5f0080050dfc7f160 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 29 Jun 2021 19:15:08 +0530 Subject: [PATCH 007/147] chore: create separate files for Desktop and Mobile view and bundle assets --- .../organizational_chart.js | 930 +----------------- erpnext/public/build.json | 9 +- .../hierarchy_chart_desktop.js | 396 ++++++++ .../hierarchy_chart/hierarchy_chart_mobile.js | 513 ++++++++++ .../js/templates}/node_card.html | 0 ...tional_chart.scss => hierarchy_chart.scss} | 0 6 files changed, 925 insertions(+), 923 deletions(-) create mode 100644 erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js create mode 100644 erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js rename erpnext/{hr/page/organizational_chart => public/js/templates}/node_card.html (100%) rename erpnext/public/scss/{organizational_chart.scss => hierarchy_chart.scss} (100%) diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.js b/erpnext/hr/page/organizational_chart/organizational_chart.js index efb367ad44..0fe724c78e 100644 --- a/erpnext/hr/page/organizational_chart/organizational_chart.js +++ b/erpnext/hr/page/organizational_chart/organizational_chart.js @@ -5,927 +5,15 @@ frappe.pages['organizational-chart'].on_page_load = function(wrapper) { single_column: true }); - // let organizational_chart = undefined; - // if (frappe.is_mobile()) { - // organizational_chart = new OrgChartMobile(wrapper); - // } else { - // organizational_chart = new OrgChart(wrapper); - // } - - let organizational_chart = new OrgChartMobile(wrapper); - $(wrapper).bind('show', ()=> { - organizational_chart.show(); - }); -}; - -class OrgChart { - - constructor(wrapper) { - this.wrapper = $(wrapper); - this.page = wrapper.page; - - this.page.main.css({ - 'min-height': '300px', - 'max-height': '600px', - 'overflow': 'auto', - 'position': 'relative' - }); - this.page.main.addClass('frappe-card'); - - this.nodes = {}; - this.setup_node_class(); - } - - setup_node_class() { - let me = this; - this.Node = class { - constructor({ - id, parent, parent_id, image, name, title, expandable, connections, is_root // eslint-disable-line - }) { - // to setup values passed via constructor - $.extend(this, arguments[0]); - - this.expanded = 0; - - me.nodes[this.id] = this; - me.make_node_element(this); - me.setup_node_click_action(this); - } - } - } - - make_node_element(node) { - let node_card = frappe.render_template('node_card', { - id: node.id, - name: node.name, - title: node.title, - image: node.image, - parent: node.parent_id, - connections: node.connections - }); - - node.parent.append(node_card); - node.$link = $(`#${node.id}`); - } - - show() { - frappe.breadcrumbs.add('HR'); - - let me = this; - let company = this.page.add_field({ - fieldtype: 'Link', - options: 'Company', - fieldname: 'company', - placeholder: __('Select Company'), - default: frappe.defaults.get_default('company'), - only_select: true, - reqd: 1, - change: () => { - me.company = undefined; - - if (company.get_value() && me.company != company.get_value()) { - me.company = company.get_value(); - - // svg for connectors - me.make_svg_markers() - - if (me.$hierarchy) - me.$hierarchy.remove(); - - // setup hierarchy - me.$hierarchy = $( - `
      -
    • -
    `); - - me.page.main.append(me.$hierarchy); - me.render_root_node(); - } - } - }); - - company.refresh(); - $(`[data-fieldname="company"]`).trigger('change'); - } - - make_svg_markers() { - $('#arrows').remove(); - - this.page.main.prepend(` - - - - - - - - - - - - - - - - - - - `); - } - - render_root_node() { - this.method = 'erpnext.hr.page.organizational_chart.organizational_chart.get_children'; - - let me = this; - - frappe.call({ - method: me.method, - args: { - company: me.company - }, - callback: function(r) { - if (r.message.length) { - let data = r.message[0]; - - let root_node = new me.Node({ - id: data.name, - parent: me.$hierarchy.find('.root-level'), - parent_id: undefined, - image: data.image, - name: data.employee_name, - title: data.designation, - expandable: true, - connections: data.connections, - is_root: true, - }); - - me.expand_node(root_node); - } - } - }) - } - - expand_node(node) { - let is_sibling = this.selected_node && this.selected_node.parent_id === node.parent_id; - this.set_selected_node(node); - this.show_active_path(node); - this.collapse_previous_level_nodes(node); - - // since the previous node collapses, all connections to that node need to be rebuilt - // if a sibling node is clicked, connections don't need to be rebuilt - if (!is_sibling) { - // rebuild outgoing connections - this.refresh_connectors(node.parent_id); - - // rebuild incoming connections - let grandparent = $(`#${node.parent_id}`).attr('data-parent'); - this.refresh_connectors(grandparent) - } - - if (node.expandable && !node.expanded) { - return this.load_children(node); - } - } - - collapse_node() { - if (this.selected_node.expandable) { - this.selected_node.$children.hide(); - $(`path[data-parent="${this.selected_node.id}"]`).hide(); - this.selected_node.expanded = false; - } - } - - show_active_path(node) { - // mark node parent on active path - $(`#${node.parent_id}`).addClass('active-path'); - } - - load_children(node) { - frappe.run_serially([ - () => this.get_child_nodes(node.id), - (child_nodes) => this.render_child_nodes(node, child_nodes) - ]); - } - - get_child_nodes(node_id) { - let me = this; - return new Promise(resolve => { - frappe.call({ - method: this.method, - args: { - parent: node_id, - company: me.company - }, - callback: (r) => { - resolve(r.message); - } - }); - }); - } - - render_child_nodes(node, child_nodes) { - const last_level = this.$hierarchy.find('.level:last').index(); - const current_level = $(`#${node.id}`).parent().parent().parent().index(); - - if (last_level === current_level) { - this.$hierarchy.append(` -
  • - `); - } - - if (!node.$children) { - node.$children = $('
      ') - .hide() - .appendTo(this.$hierarchy.find('.level:last')); - - node.$children.empty(); - - if (child_nodes) { - $.each(child_nodes, (_i, data) => { - this.add_node(node, data); - - setTimeout(() => { - this.add_connector(node.id, data.name); - }, 250); - }); - } - } - - node.$children.show(); - $(`path[data-parent="${node.id}"]`).show(); - node.expanded = true; - } - - add_node(node, data) { - var $li = $('
    • '); - - return new this.Node({ - id: data.name, - parent: $li.appendTo(node.$children), - parent_id: node.id, - image: data.image, - name: data.employee_name, - title: data.designation, - expandable: data.expandable, - connections: data.connections, - children: undefined - }); - } - - add_connector(parent_id, child_id) { - let parent_node = document.querySelector(`#${parent_id}`); - let child_node = document.querySelector(`#${child_id}`); - - // variable for the namespace - const svgns = 'http://www.w3.org/2000/svg'; - let path = document.createElementNS(svgns, 'path'); - - // we need to connect right side of the parent to the left side of the child node - let pos_parent_right = { - x: parent_node.offsetLeft + parent_node.offsetWidth, - y: parent_node.offsetTop + parent_node.offsetHeight / 2 - }; - let pos_child_left = { - x: child_node.offsetLeft - 5, - y: child_node.offsetTop + child_node.offsetHeight / 2 - }; - - let connector = - "M" + - (pos_parent_right.x) + "," + (pos_parent_right.y) + " " + - "C" + - (pos_parent_right.x + 100) + "," + (pos_parent_right.y) + " " + - (pos_child_left.x - 100) + "," + (pos_child_left.y) + " " + - (pos_child_left.x) + "," + (pos_child_left.y); - - path.setAttribute("d", connector); - path.setAttribute("data-parent", parent_id); - path.setAttribute("data-child", child_id); - - if ($(`#${parent_id}`).hasClass('active')) { - path.setAttribute("class", "active-connector"); - path.setAttribute("marker-start", "url(#arrowstart-active)"); - path.setAttribute("marker-end", "url(#arrowhead-active)"); - } else if ($(`#${parent_id}`).hasClass('active-path')) { - path.setAttribute("class", "collapsed-connector"); - path.setAttribute("marker-start", "url(#arrowstart-collapsed)"); - path.setAttribute("marker-end", "url(#arrowhead-collapsed)"); - } - - $('#connectors').append(path); - } - - set_selected_node(node) { - // remove .active class from the current node - $('.active').removeClass('active'); - - // add active class to the newly selected node - this.selected_node = node; - node.$link.addClass('active'); - } - - collapse_previous_level_nodes(node) { - let node_parent = $(`#${node.parent_id}`); - - let previous_level_nodes = node_parent.parent().parent().children('li'); - if (node_parent.parent().hasClass('root-level')) { - previous_level_nodes = node_parent.parent().children('li'); - } - - let node_card = undefined; - - previous_level_nodes.each(function() { - node_card = $(this).find('.node-card'); - - if (!node_card.hasClass('active-path')) { - node_card.addClass('collapsed'); - } - }); - } - - refresh_connectors(node_parent) { - if (!node_parent) return; - - $(`path[data-parent="${node_parent}"]`).remove(); - - frappe.run_serially([ - () => this.get_child_nodes(node_parent), - (child_nodes) => { - if (child_nodes) { - $.each(child_nodes, (_i, data) => { - this.add_connector(node_parent, data.name); - }); - } - } - ]); - } - - setup_node_click_action(node) { - let me = this; - let node_element = $(`#${node.id}`); - - node_element.click(function() { - let is_sibling = me.selected_node.parent_id === node.parent_id; - - if (is_sibling) { - me.collapse_node(); - } else if (node_element.is(':visible') - && (node_element.hasClass('collapsed') || node_element.hasClass('active-path'))) { - me.remove_levels_after_node(node); - me.remove_orphaned_connectors(); - } - - me.expand_node(node); - }); - } - - remove_levels_after_node(node) { - let level = $(`#${node.id}`).parent().parent().parent(); - - if ($(`#${node.id}`).parent().hasClass('root-level')) { - level = $(`#${node.id}`).parent(); - } - - level = $('.hierarchy > li:eq('+ level.index() + ')'); - level.nextAll('li').remove(); - - let nodes = level.find('.node-card'); - let node_object = undefined; - - $.each(nodes, (_i, element) => { - node_object = this.nodes[element.id]; - node_object.expanded = 0; - node_object.$children = undefined; - }); - - nodes.removeClass('collapsed active-path'); - } - - remove_orphaned_connectors() { - let paths = $('#connectors > path'); - $.each(paths, (_i, path) => { - let parent = $(path).data('parent'); - let child = $(path).data('child'); - - if ($(parent).length || $(child).length) - return; - - $(path).remove(); - }) - } -} - - -class OrgChartMobile { - - constructor(wrapper) { - this.wrapper = $(wrapper); - this.page = wrapper.page; - - this.page.main.css({ - 'min-height': '300px', - 'max-height': '600px', - 'overflow': 'auto', - 'position': 'relative' - }); - this.page.main.addClass('frappe-card'); - - this.nodes = {}; - this.setup_node_class(); - } - - setup_node_class() { - let me = this; - this.Node = class { - constructor({ - id, parent, parent_id, image, name, title, expandable, connections, is_root // eslint-disable-line - }) { - // to setup values passed via constructor - $.extend(this, arguments[0]); - - this.expanded = 0; - - me.nodes[this.id] = this; - me.make_node_element(this); - me.setup_node_click_action(this); - } - } - } - - make_node_element(node) { - let node_card = frappe.render_template('node_card', { - id: node.id, - name: node.name, - title: node.title, - image: node.image, - parent: node.parent_id, - connections: node.connections, - is_mobile: 1 - }); - - node.parent.append(node_card); - node.$link = $(`#${node.id}`); - node.$link.addClass('mobile-node'); - } - - show() { - frappe.breadcrumbs.add('HR'); - - let me = this; - let company = this.page.add_field({ - fieldtype: 'Link', - options: 'Company', - fieldname: 'company', - placeholder: __('Select Company'), - default: frappe.defaults.get_default('company'), - only_select: true, - reqd: 1, - change: () => { - me.company = undefined; - - if (company.get_value() && me.company != company.get_value()) { - me.company = company.get_value(); - - // svg for connectors - me.make_svg_markers() - - if (me.$sibling_group) - me.$sibling_group.remove(); - - // setup sibling group wrapper - me.$sibling_group = $(`
      `); - me.page.main.append(me.$sibling_group); - - if (me.$hierarchy) - me.$hierarchy.remove(); - - // setup hierarchy - me.$hierarchy = $( - `
        -
      • -
      `); - - me.page.main.append(me.$hierarchy); - me.render_root_node(); - } - } - }); - - company.refresh(); - $(`[data-fieldname="company"]`).trigger('change'); - } - - make_svg_markers() { - $('#arrows').remove(); - - this.page.main.prepend(` - - - - - - - - - - - - - - - - - - - `); - } - - render_root_node() { - this.method = 'erpnext.hr.page.organizational_chart.organizational_chart.get_children'; - - let me = this; - - frappe.call({ - method: me.method, - args: { - company: me.company - }, - callback: function(r) { - if (r.message.length) { - let data = r.message[0]; - - let root_node = new me.Node({ - id: data.name, - parent: me.$hierarchy.find('.root-level'), - parent_id: undefined, - image: data.image, - name: data.employee_name, - title: data.designation, - expandable: true, - connections: data.connections, - is_root: true, - }); - - me.expand_node(root_node); - } - } - }) - } - - expand_node(node) { - const is_same_node = (this.selected_node && this.selected_node.id === node.id); - this.set_selected_node(node); - this.show_active_path(node); - - if (this.$sibling_group) { - const sibling_parent = this.$sibling_group.find('.node-group').attr('data-parent'); - if (node.parent_id !== sibling_parent) - this.$sibling_group.empty(); - } - - if (!is_same_node) { - // since the previous/parent node collapses, all connections to that node need to be rebuilt - // rebuild outgoing connections of parent - this.refresh_connectors(node.parent_id, node.id); - - // rebuild incoming connections of parent - let grandparent = $(`#${node.parent_id}`).attr('data-parent'); - this.refresh_connectors(grandparent, node.parent_id); - } - - if (node.expandable && !node.expanded) { - return this.load_children(node); - } - } - - collapse_node() { - let node = this.selected_node; - if (node.expandable) { - node.$children.hide(); - node.expanded = false; - - // add a collapsed level to show the collapsed parent - // and a button beside it to move to that level - let node_parent = node.$link.parent(); - node_parent.prepend( - `
      ` - ); - - node_parent - .find('.collapsed-level') - .append(node.$link); - - frappe.run_serially([ - () => this.get_child_nodes(node.parent_id, node.id), - (child_nodes) => this.get_node_group(child_nodes, node.parent_id), - (node_group) => { - node_parent.find('.collapsed-level') - .append(node_group); - }, - () => this.setup_node_group_action() - ]); - } - } - - show_active_path(node) { - // mark node parent on active path - $(`#${node.parent_id}`).addClass('active-path'); - } - - load_children(node) { - frappe.run_serially([ - () => this.get_child_nodes(node.id), - (child_nodes) => this.render_child_nodes(node, child_nodes) - ]); - } - - get_child_nodes(node_id, exclude_node=null) { - let me = this; - return new Promise(resolve => { - frappe.call({ - method: this.method, - args: { - parent: node_id, - company: me.company, - exclude_node: exclude_node - }, - callback: (r) => { - resolve(r.message); - } - }); - }); - } - - render_child_nodes(node, child_nodes) { - if (!node.$children) { - node.$children = $('
        ') - .hide() - .appendTo(node.$link.parent()); - - node.$children.empty(); - - if (child_nodes) { - $.each(child_nodes, (_i, data) => { - this.add_node(node, data); - $(`#${data.name}`).addClass('active-child'); - - setTimeout(() => { - this.add_connector(node.id, data.name); - }, 250); - }); - } - } - - node.$children.show(); - node.expanded = true; - } - - add_node(node, data) { - var $li = $('
      • '); - - return new this.Node({ - id: data.name, - parent: $li.appendTo(node.$children), - parent_id: node.id, - image: data.image, - name: data.employee_name, - title: data.designation, - expandable: data.expandable, - connections: data.connections, - children: undefined - }); - } - - add_connector(parent_id, child_id) { - let parent_node = document.querySelector(`#${parent_id}`); - let child_node = document.querySelector(`#${child_id}`); - - // variable for the namespace - const svgns = 'http://www.w3.org/2000/svg'; - let path = document.createElementNS(svgns, 'path'); - - let connector = undefined; - - if ($(`#${parent_id}`).hasClass('active')) { - connector = this.get_connector_for_active_node(parent_node, child_node); - } else if ($(`#${parent_id}`).hasClass('active-path')) { - connector = this.get_connector_for_collapsed_node(parent_node, child_node); - } - - path.setAttribute("d", connector); - this.set_path_attributes(path, parent_id, child_id); - - $('#connectors').append(path); - } - - get_connector_for_active_node(parent_node, child_node) { - // we need to connect the bottom left of the parent to the left side of the child node - let pos_parent_bottom = { - x: parent_node.offsetLeft + 20, - y: parent_node.offsetTop + parent_node.offsetHeight - }; - let pos_child_left = { - x: child_node.offsetLeft - 5, - y: child_node.offsetTop + child_node.offsetHeight / 2 - }; - - let connector = - "M" + - (pos_parent_bottom.x) + "," + (pos_parent_bottom.y) + " " + - "L" + - (pos_parent_bottom.x) + "," + (pos_child_left.y) + " " + - "L" + - (pos_child_left.x) + "," + (pos_child_left.y); - - return connector; - } - - get_connector_for_collapsed_node(parent_node, child_node) { - // we need to connect the bottom left of the parent to the top left of the child node - let pos_parent_bottom = { - x: parent_node.offsetLeft + 20, - y: parent_node.offsetTop + parent_node.offsetHeight - }; - let pos_child_top = { - x: child_node.offsetLeft + 20, - y: child_node.offsetTop - }; - - let connector = - "M" + - (pos_parent_bottom.x) + "," + (pos_parent_bottom.y) + " " + - "L" + - (pos_child_top.x) + "," + (pos_child_top.y); - - return connector; - } - - set_path_attributes(path, parent_id, child_id) { - path.setAttribute("data-parent", parent_id); - path.setAttribute("data-child", child_id); - - if ($(`#${parent_id}`).hasClass('active')) { - path.setAttribute("class", "active-connector"); - path.setAttribute("marker-start", "url(#arrowstart-active)"); - path.setAttribute("marker-end", "url(#arrowhead-active)"); - } else if ($(`#${parent_id}`).hasClass('active-path')) { - path.setAttribute("class", "collapsed-connector"); - } - } - - set_selected_node(node) { - // remove .active class from the current node - $('.active').removeClass('active'); - - // add active class to the newly selected node - this.selected_node = node; - node.$link.addClass('active'); - } - - setup_node_click_action(node) { - let me = this; - let node_element = $(`#${node.id}`); - - node_element.click(function() { - if (node_element.is(':visible') && node_element.hasClass('active-path')) { - me.remove_levels_after_node(node); - me.remove_orphaned_connectors(); + $(wrapper).bind('show', () => { + frappe.require('/assets/js/hierarchy-chart.min.js', () => { + let organizational_chart = undefined; + if (frappe.is_mobile()) { + organizational_chart = new erpnext.HierarchyChartMobile(wrapper); } else { - me.add_node_to_hierarchy(node, true); - me.collapse_node(); + organizational_chart = new erpnext.HierarchyChart(wrapper); } - - me.expand_node(node); + organizational_chart.show(); }); - } - - setup_node_group_action() { - let me = this; - - $('.node-group').on('click', function() { - let parent = $(this).attr('data-parent'); - me.expand_sibling_group_node(parent); - }); - } - - add_node_to_hierarchy(node) { - this.$hierarchy.append(` -
      • -
        -
        -
      • - `); - - node.$link.appendTo(this.$hierarchy.find('.level:last')); - } - - get_node_group(nodes, parent, collapsed=true) { - let limit = 2; - const display_nodes = nodes.slice(0, limit); - const extra_nodes = nodes.slice(limit); - - let html = display_nodes.map(node => - this.get_avatar(node) - ).join(''); - - if (extra_nodes.length === 1) { - let node = extra_nodes[0]; - html += this.get_avatar(node); - } else if (extra_nodes.length > 1) { - html = ` - ${html} - -
        - +${extra_nodes.length} -
        -
        - `; - } - - if (html) { - const $node_group = - $(`
        -
        - ${html} -
        -
        `); - - if (collapsed) - $node_group.addClass('collapsed'); - - return $node_group; - } - - return null; - } - - get_avatar(node) { - return ` - - ` - } - - expand_sibling_group_node(parent) { - let node_object = this.nodes[parent]; - let node = node_object.$link; - node.removeClass('active-child active-path'); - node_object.expanded = 0; - node_object.$children = undefined; - - // show parent's siblings and expand parent node - frappe.run_serially([ - () => this.get_child_nodes(node_object.parent_id, node_object.id), - (child_nodes) => this.get_node_group(child_nodes, node_object.parent_id, false), - (node_group) => { - if (node_group) - this.$sibling_group.empty().append(node_group); - }, - () => this.setup_node_group_action(), - () => { - this.$hierarchy.empty().append(` -
      • - `); - this.$hierarchy.find('.level').append(node); - $(`#connectors`).empty(); - this.expand_node(node_object); - } - ]); - } - - remove_levels_after_node(node) { - let level = $(`#${node.id}`).parent().parent(); - - level = $('.hierarchy-mobile > li:eq('+ (level.index()) + ')'); - level.nextAll('li').remove(); - - let current_node = level.find(`#${node.id}`); - let node_object = this.nodes[node.id]; - - current_node.removeClass('active-child active-path'); - node_object.expanded = 0; - node_object.$children = undefined; - - level.empty().append(current_node); - } - - remove_orphaned_connectors() { - let paths = $('#connectors > path'); - $.each(paths, (_i, path) => { - let parent = $(path).data('parent'); - let child = $(path).data('child'); - - if ($(parent).length || $(child).length) - return; - - $(path).remove(); - }) - } - - refresh_connectors(node_parent, node_id) { - if (!node_parent) return; - - $(`path[data-parent="${node_parent}"]`).remove(); - this.add_connector(node_parent, node_id); - } -} \ No newline at end of file + }); +}; \ No newline at end of file diff --git a/erpnext/public/build.json b/erpnext/public/build.json index d3ebcdf7e7..3c60e3ee50 100644 --- a/erpnext/public/build.json +++ b/erpnext/public/build.json @@ -4,7 +4,7 @@ "public/less/hub.less", "public/scss/call_popup.scss", "public/scss/point-of-sale.scss", - "public/scss/organizational_chart.scss" + "public/scss/hierarchy_chart.scss" ], "css/marketplace.css": [ "public/less/hub.less" @@ -44,7 +44,8 @@ "public/js/call_popup/call_popup.js", "public/js/utils/dimension_tree_filter.js", "public/js/telephony.js", - "public/js/templates/call_link.html" + "public/js/templates/call_link.html", + "public/js/templates/node_card.html" ], "js/item-dashboard.min.js": [ "stock/dashboard/item_dashboard.html", @@ -67,5 +68,9 @@ "public/js/bank_reconciliation_tool/data_table_manager.js", "public/js/bank_reconciliation_tool/number_card.js", "public/js/bank_reconciliation_tool/dialog_manager.js" + ], + "js/hierarchy-chart.min.js": [ + "public/js/hierarchy_chart/hierarchy_chart_desktop.js", + "public/js/hierarchy_chart/hierarchy_chart_mobile.js" ] } diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js new file mode 100644 index 0000000000..fd84d4ea5c --- /dev/null +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js @@ -0,0 +1,396 @@ +erpnext.HierarchyChart = class { + + constructor(wrapper) { + this.wrapper = $(wrapper); + this.page = wrapper.page; + + this.page.main.css({ + 'min-height': '300px', + 'max-height': '600px', + 'overflow': 'auto', + 'position': 'relative' + }); + this.page.main.addClass('frappe-card'); + + this.nodes = {}; + this.setup_node_class(); + } + + setup_node_class() { + let me = this; + this.Node = class { + constructor({ + id, parent, parent_id, image, name, title, expandable, connections, is_root // eslint-disable-line + }) { + // to setup values passed via constructor + $.extend(this, arguments[0]); + + this.expanded = 0; + + me.nodes[this.id] = this; + me.make_node_element(this); + me.setup_node_click_action(this); + } + } + } + + make_node_element(node) { + let node_card = frappe.render_template('node_card', { + id: node.id, + name: node.name, + title: node.title, + image: node.image, + parent: node.parent_id, + connections: node.connections + }); + + node.parent.append(node_card); + node.$link = $(`#${node.id}`); + } + + show() { + frappe.breadcrumbs.add('HR'); + + let me = this; + let company = this.page.add_field({ + fieldtype: 'Link', + options: 'Company', + fieldname: 'company', + placeholder: __('Select Company'), + default: frappe.defaults.get_default('company'), + only_select: true, + reqd: 1, + change: () => { + me.company = undefined; + + if (company.get_value() && me.company != company.get_value()) { + me.company = company.get_value(); + + // svg for connectors + me.make_svg_markers() + + if (me.$hierarchy) + me.$hierarchy.remove(); + + // setup hierarchy + me.$hierarchy = $( + `
          +
        • +
        `); + + me.page.main.append(me.$hierarchy); + me.render_root_node(); + } + } + }); + + company.refresh(); + $(`[data-fieldname="company"]`).trigger('change'); + } + + make_svg_markers() { + $('#arrows').remove(); + + this.page.main.prepend(` + + + + + + + + + + + + + + + + + + + `); + } + + render_root_node() { + this.method = 'erpnext.hr.page.organizational_chart.organizational_chart.get_children'; + + let me = this; + + frappe.call({ + method: me.method, + args: { + company: me.company + }, + callback: function(r) { + if (r.message.length) { + let data = r.message[0]; + + let root_node = new me.Node({ + id: data.name, + parent: me.$hierarchy.find('.root-level'), + parent_id: undefined, + image: data.image, + name: data.employee_name, + title: data.designation, + expandable: true, + connections: data.connections, + is_root: true, + }); + + me.expand_node(root_node); + } + } + }) + } + + expand_node(node) { + let is_sibling = this.selected_node && this.selected_node.parent_id === node.parent_id; + this.set_selected_node(node); + this.show_active_path(node); + this.collapse_previous_level_nodes(node); + + // since the previous node collapses, all connections to that node need to be rebuilt + // if a sibling node is clicked, connections don't need to be rebuilt + if (!is_sibling) { + // rebuild outgoing connections + this.refresh_connectors(node.parent_id); + + // rebuild incoming connections + let grandparent = $(`#${node.parent_id}`).attr('data-parent'); + this.refresh_connectors(grandparent) + } + + if (node.expandable && !node.expanded) { + return this.load_children(node); + } + } + + collapse_node() { + if (this.selected_node.expandable) { + this.selected_node.$children.hide(); + $(`path[data-parent="${this.selected_node.id}"]`).hide(); + this.selected_node.expanded = false; + } + } + + show_active_path(node) { + // mark node parent on active path + $(`#${node.parent_id}`).addClass('active-path'); + } + + load_children(node) { + frappe.run_serially([ + () => this.get_child_nodes(node.id), + (child_nodes) => this.render_child_nodes(node, child_nodes) + ]); + } + + get_child_nodes(node_id) { + let me = this; + return new Promise(resolve => { + frappe.call({ + method: this.method, + args: { + parent: node_id, + company: me.company + }, + callback: (r) => { + resolve(r.message); + } + }); + }); + } + + render_child_nodes(node, child_nodes) { + const last_level = this.$hierarchy.find('.level:last').index(); + const current_level = $(`#${node.id}`).parent().parent().parent().index(); + + if (last_level === current_level) { + this.$hierarchy.append(` +
      • + `); + } + + if (!node.$children) { + node.$children = $('
          ') + .hide() + .appendTo(this.$hierarchy.find('.level:last')); + + node.$children.empty(); + + if (child_nodes) { + $.each(child_nodes, (_i, data) => { + this.add_node(node, data); + + setTimeout(() => { + this.add_connector(node.id, data.name); + }, 250); + }); + } + } + + node.$children.show(); + $(`path[data-parent="${node.id}"]`).show(); + node.expanded = true; + } + + add_node(node, data) { + var $li = $('
        • '); + + return new this.Node({ + id: data.name, + parent: $li.appendTo(node.$children), + parent_id: node.id, + image: data.image, + name: data.employee_name, + title: data.designation, + expandable: data.expandable, + connections: data.connections, + children: undefined + }); + } + + add_connector(parent_id, child_id) { + let parent_node = document.querySelector(`#${parent_id}`); + let child_node = document.querySelector(`#${child_id}`); + + // variable for the namespace + const svgns = 'http://www.w3.org/2000/svg'; + let path = document.createElementNS(svgns, 'path'); + + // we need to connect right side of the parent to the left side of the child node + let pos_parent_right = { + x: parent_node.offsetLeft + parent_node.offsetWidth, + y: parent_node.offsetTop + parent_node.offsetHeight / 2 + }; + let pos_child_left = { + x: child_node.offsetLeft - 5, + y: child_node.offsetTop + child_node.offsetHeight / 2 + }; + + let connector = + "M" + + (pos_parent_right.x) + "," + (pos_parent_right.y) + " " + + "C" + + (pos_parent_right.x + 100) + "," + (pos_parent_right.y) + " " + + (pos_child_left.x - 100) + "," + (pos_child_left.y) + " " + + (pos_child_left.x) + "," + (pos_child_left.y); + + path.setAttribute("d", connector); + path.setAttribute("data-parent", parent_id); + path.setAttribute("data-child", child_id); + + if ($(`#${parent_id}`).hasClass('active')) { + path.setAttribute("class", "active-connector"); + path.setAttribute("marker-start", "url(#arrowstart-active)"); + path.setAttribute("marker-end", "url(#arrowhead-active)"); + } else if ($(`#${parent_id}`).hasClass('active-path')) { + path.setAttribute("class", "collapsed-connector"); + path.setAttribute("marker-start", "url(#arrowstart-collapsed)"); + path.setAttribute("marker-end", "url(#arrowhead-collapsed)"); + } + + $('#connectors').append(path); + } + + set_selected_node(node) { + // remove .active class from the current node + $('.active').removeClass('active'); + + // add active class to the newly selected node + this.selected_node = node; + node.$link.addClass('active'); + } + + collapse_previous_level_nodes(node) { + let node_parent = $(`#${node.parent_id}`); + + let previous_level_nodes = node_parent.parent().parent().children('li'); + if (node_parent.parent().hasClass('root-level')) { + previous_level_nodes = node_parent.parent().children('li'); + } + + let node_card = undefined; + + previous_level_nodes.each(function() { + node_card = $(this).find('.node-card'); + + if (!node_card.hasClass('active-path')) { + node_card.addClass('collapsed'); + } + }); + } + + refresh_connectors(node_parent) { + if (!node_parent) return; + + $(`path[data-parent="${node_parent}"]`).remove(); + + frappe.run_serially([ + () => this.get_child_nodes(node_parent), + (child_nodes) => { + if (child_nodes) { + $.each(child_nodes, (_i, data) => { + this.add_connector(node_parent, data.name); + }); + } + } + ]); + } + + setup_node_click_action(node) { + let me = this; + let node_element = $(`#${node.id}`); + + node_element.click(function() { + let is_sibling = me.selected_node.parent_id === node.parent_id; + + if (is_sibling) { + me.collapse_node(); + } else if (node_element.is(':visible') + && (node_element.hasClass('collapsed') || node_element.hasClass('active-path'))) { + me.remove_levels_after_node(node); + me.remove_orphaned_connectors(); + } + + me.expand_node(node); + }); + } + + remove_levels_after_node(node) { + let level = $(`#${node.id}`).parent().parent().parent(); + + if ($(`#${node.id}`).parent().hasClass('root-level')) { + level = $(`#${node.id}`).parent(); + } + + level = $('.hierarchy > li:eq('+ level.index() + ')'); + level.nextAll('li').remove(); + + let nodes = level.find('.node-card'); + let node_object = undefined; + + $.each(nodes, (_i, element) => { + node_object = this.nodes[element.id]; + node_object.expanded = 0; + node_object.$children = undefined; + }); + + nodes.removeClass('collapsed active-path'); + } + + remove_orphaned_connectors() { + let paths = $('#connectors > path'); + $.each(paths, (_i, path) => { + let parent = $(path).data('parent'); + let child = $(path).data('child'); + + if ($(parent).length || $(child).length) + return; + + $(path).remove(); + }) + } +} \ No newline at end of file diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js new file mode 100644 index 0000000000..c705681438 --- /dev/null +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js @@ -0,0 +1,513 @@ +erpnext.HierarchyChartMobile = class { + + constructor(wrapper) { + this.wrapper = $(wrapper); + this.page = wrapper.page; + + this.page.main.css({ + 'min-height': '300px', + 'max-height': '600px', + 'overflow': 'auto', + 'position': 'relative' + }); + this.page.main.addClass('frappe-card'); + + this.nodes = {}; + this.setup_node_class(); + } + + setup_node_class() { + let me = this; + this.Node = class { + constructor({ + id, parent, parent_id, image, name, title, expandable, connections, is_root // eslint-disable-line + }) { + // to setup values passed via constructor + $.extend(this, arguments[0]); + + this.expanded = 0; + + me.nodes[this.id] = this; + me.make_node_element(this); + me.setup_node_click_action(this); + } + } + } + + make_node_element(node) { + let node_card = frappe.render_template('node_card', { + id: node.id, + name: node.name, + title: node.title, + image: node.image, + parent: node.parent_id, + connections: node.connections, + is_mobile: 1 + }); + + node.parent.append(node_card); + node.$link = $(`#${node.id}`); + node.$link.addClass('mobile-node'); + } + + show() { + frappe.breadcrumbs.add('HR'); + + let me = this; + let company = this.page.add_field({ + fieldtype: 'Link', + options: 'Company', + fieldname: 'company', + placeholder: __('Select Company'), + default: frappe.defaults.get_default('company'), + only_select: true, + reqd: 1, + change: () => { + me.company = undefined; + + if (company.get_value() && me.company != company.get_value()) { + me.company = company.get_value(); + + // svg for connectors + me.make_svg_markers() + + if (me.$sibling_group) + me.$sibling_group.remove(); + + // setup sibling group wrapper + me.$sibling_group = $(`
          `); + me.page.main.append(me.$sibling_group); + + if (me.$hierarchy) + me.$hierarchy.remove(); + + // setup hierarchy + me.$hierarchy = $( + `
            +
          • +
          `); + + me.page.main.append(me.$hierarchy); + me.render_root_node(); + } + } + }); + + company.refresh(); + $(`[data-fieldname="company"]`).trigger('change'); + } + + make_svg_markers() { + $('#arrows').remove(); + + this.page.main.prepend(` + + + + + + + + + + + + + + + + + + + `); + } + + render_root_node() { + this.method = 'erpnext.hr.page.organizational_chart.organizational_chart.get_children'; + + let me = this; + + frappe.call({ + method: me.method, + args: { + company: me.company + }, + callback: function(r) { + if (r.message.length) { + let data = r.message[0]; + + let root_node = new me.Node({ + id: data.name, + parent: me.$hierarchy.find('.root-level'), + parent_id: undefined, + image: data.image, + name: data.employee_name, + title: data.designation, + expandable: true, + connections: data.connections, + is_root: true, + }); + + me.expand_node(root_node); + } + } + }) + } + + expand_node(node) { + const is_same_node = (this.selected_node && this.selected_node.id === node.id); + this.set_selected_node(node); + this.show_active_path(node); + + if (this.$sibling_group) { + const sibling_parent = this.$sibling_group.find('.node-group').attr('data-parent'); + if (node.parent_id !== sibling_parent) + this.$sibling_group.empty(); + } + + if (!is_same_node) { + // since the previous/parent node collapses, all connections to that node need to be rebuilt + // rebuild outgoing connections of parent + this.refresh_connectors(node.parent_id, node.id); + + // rebuild incoming connections of parent + let grandparent = $(`#${node.parent_id}`).attr('data-parent'); + this.refresh_connectors(grandparent, node.parent_id); + } + + if (node.expandable && !node.expanded) { + return this.load_children(node); + } + } + + collapse_node() { + let node = this.selected_node; + if (node.expandable) { + node.$children.hide(); + node.expanded = false; + + // add a collapsed level to show the collapsed parent + // and a button beside it to move to that level + let node_parent = node.$link.parent(); + node_parent.prepend( + `
          ` + ); + + node_parent + .find('.collapsed-level') + .append(node.$link); + + frappe.run_serially([ + () => this.get_child_nodes(node.parent_id, node.id), + (child_nodes) => this.get_node_group(child_nodes, node.parent_id), + (node_group) => { + node_parent.find('.collapsed-level') + .append(node_group); + }, + () => this.setup_node_group_action() + ]); + } + } + + show_active_path(node) { + // mark node parent on active path + $(`#${node.parent_id}`).addClass('active-path'); + } + + load_children(node) { + frappe.run_serially([ + () => this.get_child_nodes(node.id), + (child_nodes) => this.render_child_nodes(node, child_nodes) + ]); + } + + get_child_nodes(node_id, exclude_node=null) { + let me = this; + return new Promise(resolve => { + frappe.call({ + method: this.method, + args: { + parent: node_id, + company: me.company, + exclude_node: exclude_node + }, + callback: (r) => { + resolve(r.message); + } + }); + }); + } + + render_child_nodes(node, child_nodes) { + if (!node.$children) { + node.$children = $('
            ') + .hide() + .appendTo(node.$link.parent()); + + node.$children.empty(); + + if (child_nodes) { + $.each(child_nodes, (_i, data) => { + this.add_node(node, data); + $(`#${data.name}`).addClass('active-child'); + + setTimeout(() => { + this.add_connector(node.id, data.name); + }, 250); + }); + } + } + + node.$children.show(); + node.expanded = true; + } + + add_node(node, data) { + var $li = $('
          • '); + + return new this.Node({ + id: data.name, + parent: $li.appendTo(node.$children), + parent_id: node.id, + image: data.image, + name: data.employee_name, + title: data.designation, + expandable: data.expandable, + connections: data.connections, + children: undefined + }); + } + + add_connector(parent_id, child_id) { + let parent_node = document.querySelector(`#${parent_id}`); + let child_node = document.querySelector(`#${child_id}`); + + // variable for the namespace + const svgns = 'http://www.w3.org/2000/svg'; + let path = document.createElementNS(svgns, 'path'); + + let connector = undefined; + + if ($(`#${parent_id}`).hasClass('active')) { + connector = this.get_connector_for_active_node(parent_node, child_node); + } else if ($(`#${parent_id}`).hasClass('active-path')) { + connector = this.get_connector_for_collapsed_node(parent_node, child_node); + } + + path.setAttribute("d", connector); + this.set_path_attributes(path, parent_id, child_id); + + $('#connectors').append(path); + } + + get_connector_for_active_node(parent_node, child_node) { + // we need to connect the bottom left of the parent to the left side of the child node + let pos_parent_bottom = { + x: parent_node.offsetLeft + 20, + y: parent_node.offsetTop + parent_node.offsetHeight + }; + let pos_child_left = { + x: child_node.offsetLeft - 5, + y: child_node.offsetTop + child_node.offsetHeight / 2 + }; + + let connector = + "M" + + (pos_parent_bottom.x) + "," + (pos_parent_bottom.y) + " " + + "L" + + (pos_parent_bottom.x) + "," + (pos_child_left.y) + " " + + "L" + + (pos_child_left.x) + "," + (pos_child_left.y); + + return connector; + } + + get_connector_for_collapsed_node(parent_node, child_node) { + // we need to connect the bottom left of the parent to the top left of the child node + let pos_parent_bottom = { + x: parent_node.offsetLeft + 20, + y: parent_node.offsetTop + parent_node.offsetHeight + }; + let pos_child_top = { + x: child_node.offsetLeft + 20, + y: child_node.offsetTop + }; + + let connector = + "M" + + (pos_parent_bottom.x) + "," + (pos_parent_bottom.y) + " " + + "L" + + (pos_child_top.x) + "," + (pos_child_top.y); + + return connector; + } + + set_path_attributes(path, parent_id, child_id) { + path.setAttribute("data-parent", parent_id); + path.setAttribute("data-child", child_id); + + if ($(`#${parent_id}`).hasClass('active')) { + path.setAttribute("class", "active-connector"); + path.setAttribute("marker-start", "url(#arrowstart-active)"); + path.setAttribute("marker-end", "url(#arrowhead-active)"); + } else if ($(`#${parent_id}`).hasClass('active-path')) { + path.setAttribute("class", "collapsed-connector"); + } + } + + set_selected_node(node) { + // remove .active class from the current node + $('.active').removeClass('active'); + + // add active class to the newly selected node + this.selected_node = node; + node.$link.addClass('active'); + } + + setup_node_click_action(node) { + let me = this; + let node_element = $(`#${node.id}`); + + node_element.click(function() { + if (node_element.is(':visible') && node_element.hasClass('active-path')) { + me.remove_levels_after_node(node); + me.remove_orphaned_connectors(); + } else { + me.add_node_to_hierarchy(node, true); + me.collapse_node(); + } + + me.expand_node(node); + }); + } + + setup_node_group_action() { + let me = this; + + $('.node-group').on('click', function() { + let parent = $(this).attr('data-parent'); + me.expand_sibling_group_node(parent); + }); + } + + add_node_to_hierarchy(node) { + this.$hierarchy.append(` +
          • +
            +
            +
          • + `); + + node.$link.appendTo(this.$hierarchy.find('.level:last')); + } + + get_node_group(nodes, parent, collapsed=true) { + let limit = 2; + const display_nodes = nodes.slice(0, limit); + const extra_nodes = nodes.slice(limit); + + let html = display_nodes.map(node => + this.get_avatar(node) + ).join(''); + + if (extra_nodes.length === 1) { + let node = extra_nodes[0]; + html += this.get_avatar(node); + } else if (extra_nodes.length > 1) { + html = ` + ${html} + +
            + +${extra_nodes.length} +
            +
            + `; + } + + if (html) { + const $node_group = + $(`
            +
            + ${html} +
            +
            `); + + if (collapsed) + $node_group.addClass('collapsed'); + + return $node_group; + } + + return null; + } + + get_avatar(node) { + return ` + + ` + } + + expand_sibling_group_node(parent) { + let node_object = this.nodes[parent]; + let node = node_object.$link; + node.removeClass('active-child active-path'); + node_object.expanded = 0; + node_object.$children = undefined; + + // show parent's siblings and expand parent node + frappe.run_serially([ + () => this.get_child_nodes(node_object.parent_id, node_object.id), + (child_nodes) => this.get_node_group(child_nodes, node_object.parent_id, false), + (node_group) => { + if (node_group) + this.$sibling_group.empty().append(node_group); + }, + () => this.setup_node_group_action(), + () => { + this.$hierarchy.empty().append(` +
          • + `); + this.$hierarchy.find('.level').append(node); + $(`#connectors`).empty(); + this.expand_node(node_object); + } + ]); + } + + remove_levels_after_node(node) { + let level = $(`#${node.id}`).parent().parent(); + + level = $('.hierarchy-mobile > li:eq('+ (level.index()) + ')'); + level.nextAll('li').remove(); + + let current_node = level.find(`#${node.id}`); + let node_object = this.nodes[node.id]; + + current_node.removeClass('active-child active-path'); + node_object.expanded = 0; + node_object.$children = undefined; + + level.empty().append(current_node); + } + + remove_orphaned_connectors() { + let paths = $('#connectors > path'); + $.each(paths, (_i, path) => { + let parent = $(path).data('parent'); + let child = $(path).data('child'); + + if ($(parent).length || $(child).length) + return; + + $(path).remove(); + }) + } + + refresh_connectors(node_parent, node_id) { + if (!node_parent) return; + + $(`path[data-parent="${node_parent}"]`).remove(); + this.add_connector(node_parent, node_id); + } +} \ No newline at end of file diff --git a/erpnext/hr/page/organizational_chart/node_card.html b/erpnext/public/js/templates/node_card.html similarity index 100% rename from erpnext/hr/page/organizational_chart/node_card.html rename to erpnext/public/js/templates/node_card.html diff --git a/erpnext/public/scss/organizational_chart.scss b/erpnext/public/scss/hierarchy_chart.scss similarity index 100% rename from erpnext/public/scss/organizational_chart.scss rename to erpnext/public/scss/hierarchy_chart.scss From f15e8b7f5a50cc686e3139b4aee63726a01f6413 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 29 Jun 2021 21:26:47 +0530 Subject: [PATCH 008/147] refactor: add options to chart - method to return the node data - wrapper for showing the hierarchy --- .../organizational_chart.js | 6 ++-- .../organizational_chart.py | 6 ++-- .../hierarchy_chart_desktop.js | 28 +++++++++------- .../hierarchy_chart/hierarchy_chart_mobile.js | 32 +++++++++++-------- 4 files changed, 41 insertions(+), 31 deletions(-) diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.js b/erpnext/hr/page/organizational_chart/organizational_chart.js index 0fe724c78e..ca9855286c 100644 --- a/erpnext/hr/page/organizational_chart/organizational_chart.js +++ b/erpnext/hr/page/organizational_chart/organizational_chart.js @@ -8,10 +8,12 @@ frappe.pages['organizational-chart'].on_page_load = function(wrapper) { $(wrapper).bind('show', () => { frappe.require('/assets/js/hierarchy-chart.min.js', () => { let organizational_chart = undefined; + let method = 'erpnext.hr.page.organizational_chart.organizational_chart.get_children'; + if (frappe.is_mobile()) { - organizational_chart = new erpnext.HierarchyChartMobile(wrapper); + organizational_chart = new erpnext.HierarchyChartMobile(wrapper, method); } else { - organizational_chart = new erpnext.HierarchyChart(wrapper); + organizational_chart = new erpnext.HierarchyChart(wrapper, method); } organizational_chart.show(); }); diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.py b/erpnext/hr/page/organizational_chart/organizational_chart.py index ae91a919b2..f3aa13897d 100644 --- a/erpnext/hr/page/organizational_chart/organizational_chart.py +++ b/erpnext/hr/page/organizational_chart/organizational_chart.py @@ -9,7 +9,7 @@ def get_children(parent=None, company=None, exclude_node=None, is_root=False, is filters.append(['company', '=', company]) if not fields: - fields = ['employee_name', 'name', 'reports_to', 'image', 'designation'] + fields = ['employee_name as name', 'name as id', 'reports_to', 'image', 'designation as title'] if is_root: parent = '' @@ -27,9 +27,9 @@ def get_children(parent=None, company=None, exclude_node=None, is_root=False, is for employee in employees: is_expandable = frappe.get_all('Employee', filters=[ - ['reports_to', '=', employee.get('name')] + ['reports_to', '=', employee.get('id')] ]) - employee.connections = get_connections(employee.name) + employee.connections = get_connections(employee.id) employee.expandable = 1 if is_expandable else 0 return employees diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js index fd84d4ea5c..052f140c13 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js @@ -1,8 +1,14 @@ erpnext.HierarchyChart = class { - - constructor(wrapper) { + /* Options: + - wrapper: wrapper for the hierarchy view + - method: + - to get the data for each node + - this method should return id, name, title, image, and connections for each node + */ + constructor(wrapper, method) { this.wrapper = $(wrapper); this.page = wrapper.page; + this.method = method; this.page.main.css({ 'min-height': '300px', @@ -114,8 +120,6 @@ erpnext.HierarchyChart = class { } render_root_node() { - this.method = 'erpnext.hr.page.organizational_chart.organizational_chart.get_children'; - let me = this; frappe.call({ @@ -128,12 +132,12 @@ erpnext.HierarchyChart = class { let data = r.message[0]; let root_node = new me.Node({ - id: data.name, + id: data.id, parent: me.$hierarchy.find('.root-level'), parent_id: undefined, image: data.image, - name: data.employee_name, - title: data.designation, + name: data.name, + title: data.title, expandable: true, connections: data.connections, is_root: true, @@ -225,7 +229,7 @@ erpnext.HierarchyChart = class { this.add_node(node, data); setTimeout(() => { - this.add_connector(node.id, data.name); + this.add_connector(node.id, data.id); }, 250); }); } @@ -240,12 +244,12 @@ erpnext.HierarchyChart = class { var $li = $('
          • '); return new this.Node({ - id: data.name, + id: data.id, parent: $li.appendTo(node.$children), parent_id: node.id, image: data.image, - name: data.employee_name, - title: data.designation, + name: data.name, + title: data.title, expandable: data.expandable, connections: data.connections, children: undefined @@ -333,7 +337,7 @@ erpnext.HierarchyChart = class { (child_nodes) => { if (child_nodes) { $.each(child_nodes, (_i, data) => { - this.add_connector(node_parent, data.name); + this.add_connector(node_parent, data.id); }); } } diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js index c705681438..1b8bc2e8e0 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js @@ -1,8 +1,14 @@ erpnext.HierarchyChartMobile = class { - - constructor(wrapper) { + /* Options: + - wrapper: wrapper for the hierarchy view + - method: + - to get the data for each node + - this method should return id, name, title, image, and connections for each node + */ + constructor(wrapper, method) { this.wrapper = $(wrapper); this.page = wrapper.page; + this.method = method; this.page.main.css({ 'min-height': '300px', @@ -123,8 +129,6 @@ erpnext.HierarchyChartMobile = class { } render_root_node() { - this.method = 'erpnext.hr.page.organizational_chart.organizational_chart.get_children'; - let me = this; frappe.call({ @@ -137,12 +141,12 @@ erpnext.HierarchyChartMobile = class { let data = r.message[0]; let root_node = new me.Node({ - id: data.name, + id: data.id, parent: me.$hierarchy.find('.root-level'), parent_id: undefined, image: data.image, - name: data.employee_name, - title: data.designation, + name: data.name, + title: data.title, expandable: true, connections: data.connections, is_root: true, @@ -249,10 +253,10 @@ erpnext.HierarchyChartMobile = class { if (child_nodes) { $.each(child_nodes, (_i, data) => { this.add_node(node, data); - $(`#${data.name}`).addClass('active-child'); + $(`#${data.id}`).addClass('active-child'); setTimeout(() => { - this.add_connector(node.id, data.name); + this.add_connector(node.id, data.id); }, 250); }); } @@ -266,12 +270,12 @@ erpnext.HierarchyChartMobile = class { var $li = $('
          • '); return new this.Node({ - id: data.name, + id: data.id, parent: $li.appendTo(node.$children), parent_id: node.id, image: data.image, - name: data.employee_name, - title: data.designation, + name: data.name, + title: data.title, expandable: data.expandable, connections: data.connections, children: undefined @@ -418,7 +422,7 @@ erpnext.HierarchyChartMobile = class { ${html}
            + title="${extra_nodes.map(node => node.name).join(', ')}"> +${extra_nodes.length}
            @@ -443,7 +447,7 @@ erpnext.HierarchyChartMobile = class { } get_avatar(node) { - return ` + return ` ` } From c40b9d276e04cb521dd33601a84acb53100f40c0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 29 Jun 2021 21:51:21 +0530 Subject: [PATCH 009/147] feat: setup node edit action --- .../organizational_chart/organizational_chart.js | 4 ++-- .../js/hierarchy_chart/hierarchy_chart_desktop.js | 14 +++++++++++++- .../js/hierarchy_chart/hierarchy_chart_mobile.js | 14 +++++++++++++- erpnext/public/js/templates/node_card.html | 4 ++-- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.js b/erpnext/hr/page/organizational_chart/organizational_chart.js index ca9855286c..a138886768 100644 --- a/erpnext/hr/page/organizational_chart/organizational_chart.js +++ b/erpnext/hr/page/organizational_chart/organizational_chart.js @@ -11,9 +11,9 @@ frappe.pages['organizational-chart'].on_page_load = function(wrapper) { let method = 'erpnext.hr.page.organizational_chart.organizational_chart.get_children'; if (frappe.is_mobile()) { - organizational_chart = new erpnext.HierarchyChartMobile(wrapper, method); + organizational_chart = new erpnext.HierarchyChartMobile('Employee', wrapper, method); } else { - organizational_chart = new erpnext.HierarchyChart(wrapper, method); + organizational_chart = new erpnext.HierarchyChart('Employee', wrapper, method); } organizational_chart.show(); }); diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js index 052f140c13..0823ec77a8 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js @@ -1,14 +1,16 @@ erpnext.HierarchyChart = class { /* Options: + - doctype - wrapper: wrapper for the hierarchy view - method: - to get the data for each node - this method should return id, name, title, image, and connections for each node */ - constructor(wrapper, method) { + constructor(doctype, wrapper, method) { this.wrapper = $(wrapper); this.page = wrapper.page; this.method = method; + this.doctype = doctype; this.page.main.css({ 'min-height': '300px', @@ -36,6 +38,7 @@ erpnext.HierarchyChart = class { me.nodes[this.id] = this; me.make_node_element(this); me.setup_node_click_action(this); + me.setup_edit_node_action(this); } } } @@ -363,6 +366,15 @@ erpnext.HierarchyChart = class { }); } + setup_edit_node_action(node) { + let node_element = $(`#${node.id}`); + let me = this; + + node_element.find('.btn-edit-node').click(function() { + frappe.set_route('Form', me.doctype, node.id); + }); + } + remove_levels_after_node(node) { let level = $(`#${node.id}`).parent().parent().parent(); diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js index 1b8bc2e8e0..4b09714d0a 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js @@ -1,14 +1,16 @@ erpnext.HierarchyChartMobile = class { /* Options: + - doctype - wrapper: wrapper for the hierarchy view - method: - to get the data for each node - this method should return id, name, title, image, and connections for each node */ - constructor(wrapper, method) { + constructor(doctype, wrapper, method) { this.wrapper = $(wrapper); this.page = wrapper.page; this.method = method; + this.doctype = doctype this.page.main.css({ 'min-height': '300px', @@ -36,6 +38,7 @@ erpnext.HierarchyChartMobile = class { me.nodes[this.id] = this; me.make_node_element(this); me.setup_node_click_action(this); + me.setup_edit_node_action(this); } } } @@ -385,6 +388,15 @@ erpnext.HierarchyChartMobile = class { }); } + setup_edit_node_action(node) { + let node_element = $(`#${node.id}`); + let me = this; + + node_element.find('.btn-edit-node').click(function() { + frappe.set_route('Form', me.doctype, node.id); + }); + } + setup_node_group_action() { let me = this; diff --git a/erpnext/public/js/templates/node_card.html b/erpnext/public/js/templates/node_card.html index e42e54f690..30aedab4bb 100644 --- a/erpnext/public/js/templates/node_card.html +++ b/erpnext/public/js/templates/node_card.html @@ -17,9 +17,9 @@
            {{ title }}
            {% if connections == 1 %} -
            · {{ connections }}
            +
            · {{ connections }} Connection
            {% else %} -
            · {{ connections }}
            +
            · {{ connections }} Connections
            {% endif %} From 7558b28b794f09826bcf8f42385605dcdc819086 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 29 Jun 2021 21:57:27 +0530 Subject: [PATCH 010/147] fix: revert changes in employee descendants query --- erpnext/hr/doctype/employee/employee.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index 7917e3abf5..ed7d588434 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -476,14 +476,13 @@ def get_employee_emails(employee_list): return employee_emails @frappe.whitelist() -def get_children(doctype, parent=None, company=None, is_root=False, is_tree=False, fields=None): +def get_children(doctype, parent=None, company=None, is_root=False, is_tree=False): filters = [['status', '!=', 'Left']] if company and company != 'All Companies': filters.append(['company', '=', company]) - if not fields: - fields = ['name as value', 'employee_name as title'] + fields = ['name as value', 'employee_name as title'] if is_root: parent = '' From a7507f7af63c259fd61164c39a5dbd5d6e3c2df1 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 30 Jun 2021 01:46:10 +0530 Subject: [PATCH 011/147] refactor: use arcs instead of bezier curves for cleaner connectors --- .../hierarchy_chart_desktop.js | 54 +++++++++++++++---- erpnext/public/scss/hierarchy_chart.scss | 2 +- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js index 0823ec77a8..ba811be586 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js @@ -277,15 +277,53 @@ erpnext.HierarchyChart = class { y: child_node.offsetTop + child_node.offsetHeight / 2 }; - let connector = - "M" + - (pos_parent_right.x) + "," + (pos_parent_right.y) + " " + - "C" + - (pos_parent_right.x + 100) + "," + (pos_parent_right.y) + " " + - (pos_child_left.x - 100) + "," + (pos_child_left.y) + " " + - (pos_child_left.x) + "," + (pos_child_left.y); + let connector = this.get_connector(pos_parent_right, pos_child_left); path.setAttribute("d", connector); + this.set_path_attributes(path, parent_id, child_id); + + $('#connectors').append(path); + } + + get_connector(pos_parent_right, pos_child_left) { + if (pos_parent_right.y === pos_child_left.y) { + // don't add arcs if it's a straight line + return "M" + + (pos_parent_right.x) + "," + (pos_parent_right.y) + " " + + "L"+ + (pos_child_left.x) + "," + (pos_child_left.y); + } else { + let arc_1 = ""; + let arc_2 = ""; + let offset = 0; + + if (pos_parent_right.y > pos_child_left.y) { + // if child is above parent on Y axis 1st arc is anticlocwise + // second arc is clockwise + arc_1 = "a10,10 1 0 0 10,-10 "; + arc_2 = "a10,10 0 0 1 10,-10 "; + offset = 10; + } else { + // if child is below parent on Y axis 1st arc is clockwise + // second arc is anticlockwise + arc_1 = "a10,10 0 0 1 10,10 "; + arc_2 = "a10,10 1 0 0 10,10 "; + offset = -10; + } + + return "M" + (pos_parent_right.x) + "," + (pos_parent_right.y) + " " + + "L" + + (pos_parent_right.x + 40) + "," + (pos_parent_right.y) + " " + + arc_1 + + "L" + + (pos_parent_right.x + 50) + "," + (pos_child_left.y + offset) + " " + + arc_2 + + "L"+ + (pos_child_left.x) + "," + (pos_child_left.y); + } + } + + set_path_attributes(path, parent_id, child_id) { path.setAttribute("data-parent", parent_id); path.setAttribute("data-child", child_id); @@ -298,8 +336,6 @@ erpnext.HierarchyChart = class { path.setAttribute("marker-start", "url(#arrowstart-collapsed)"); path.setAttribute("marker-end", "url(#arrowhead-collapsed)"); } - - $('#connectors').append(path); } set_selected_node(node) { diff --git a/erpnext/public/scss/hierarchy_chart.scss b/erpnext/public/scss/hierarchy_chart.scss index 16b8792432..16137fdb5f 100644 --- a/erpnext/public/scss/hierarchy_chart.scss +++ b/erpnext/public/scss/hierarchy_chart.scss @@ -48,7 +48,6 @@ border-radius: 0.5rem; padding: 0.75rem; width: 18rem; - height: 5rem; .btn-edit-node { display: flex; @@ -195,6 +194,7 @@ .level { margin-right: 8px; + align-items: flex-start; } #arrows { From b8a18bfef1f3c86758d63bc3cde172ecc1758f43 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 30 Jun 2021 01:57:43 +0530 Subject: [PATCH 012/147] feat: add arc to connectors in mobile view --- erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js index 4b09714d0a..b09b428b30 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js @@ -322,7 +322,8 @@ erpnext.HierarchyChartMobile = class { "M" + (pos_parent_bottom.x) + "," + (pos_parent_bottom.y) + " " + "L" + - (pos_parent_bottom.x) + "," + (pos_child_left.y) + " " + + (pos_parent_bottom.x) + "," + (pos_child_left.y - 10) + " " + + "a10,10 1 0 0 10,10 " + "L" + (pos_child_left.x) + "," + (pos_child_left.y); From 77b0b8a877108062818b5e0f72995966a85f9af2 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 30 Jun 2021 02:29:16 +0530 Subject: [PATCH 013/147] fix: edit node button overflowing --- erpnext/public/js/templates/node_card.html | 2 +- erpnext/public/scss/hierarchy_chart.scss | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/templates/node_card.html b/erpnext/public/js/templates/node_card.html index 30aedab4bb..c3d8e010b5 100644 --- a/erpnext/public/js/templates/node_card.html +++ b/erpnext/public/js/templates/node_card.html @@ -8,7 +8,7 @@
            {{ name }} -
            + diff --git a/erpnext/public/scss/hierarchy_chart.scss b/erpnext/public/scss/hierarchy_chart.scss index 16137fdb5f..eefc14d679 100644 --- a/erpnext/public/scss/hierarchy_chart.scss +++ b/erpnext/public/scss/hierarchy_chart.scss @@ -57,6 +57,7 @@ font-size: .75rem; justify-content: center; box-shadow: var(--shadow-sm); + margin-left: auto; } .edit-chart-node { @@ -79,6 +80,7 @@ align-items: center; justify-content: space-between; margin-bottom: 2px; + width: 12.2rem; } } From 2fcd05aa82042456b50c9dad42d1e97d57afb80f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 2 Jul 2021 18:15:18 +0530 Subject: [PATCH 014/147] fix: sider --- .../hierarchy_chart_desktop.js | 25 +++++++++---------- .../hierarchy_chart/hierarchy_chart_mobile.js | 18 ++++++------- erpnext/public/scss/hierarchy_chart.scss | 5 +--- 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js index ba811be586..9e82fb2002 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js @@ -40,7 +40,7 @@ erpnext.HierarchyChart = class { me.setup_node_click_action(this); me.setup_edit_node_action(this); } - } + }; } make_node_element(node) { @@ -76,7 +76,7 @@ erpnext.HierarchyChart = class { me.company = company.get_value(); // svg for connectors - me.make_svg_markers() + me.make_svg_markers(); if (me.$hierarchy) me.$hierarchy.remove(); @@ -149,7 +149,7 @@ erpnext.HierarchyChart = class { me.expand_node(root_node); } } - }) + }); } expand_node(node) { @@ -166,7 +166,7 @@ erpnext.HierarchyChart = class { // rebuild incoming connections let grandparent = $(`#${node.parent_id}`).attr('data-parent'); - this.refresh_connectors(grandparent) + this.refresh_connectors(grandparent); } if (node.expandable && !node.expanded) { @@ -176,8 +176,8 @@ erpnext.HierarchyChart = class { collapse_node() { if (this.selected_node.expandable) { - this.selected_node.$children.hide(); - $(`path[data-parent="${this.selected_node.id}"]`).hide(); + this.selected_node.$children.hide('fast'); + $(`path[data-parent="${this.selected_node.id}"]`).hide('fast'); this.selected_node.expanded = false; } } @@ -222,15 +222,14 @@ erpnext.HierarchyChart = class { if (!node.$children) { node.$children = $('
              ') - .hide() - .appendTo(this.$hierarchy.find('.level:last')); + .hide() + .appendTo(this.$hierarchy.find('.level:last')); node.$children.empty(); if (child_nodes) { $.each(child_nodes, (_i, data) => { this.add_node(node, data); - setTimeout(() => { this.add_connector(node.id, data.id); }, 250); @@ -238,8 +237,8 @@ erpnext.HierarchyChart = class { } } - node.$children.show(); - $(`path[data-parent="${node.id}"]`).show(); + node.$children.show('fast'); + $(`path[data-parent="${node.id}"]`).show('fast'); node.expanded = true; } @@ -443,6 +442,6 @@ erpnext.HierarchyChart = class { return; $(path).remove(); - }) + }); } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js index b09b428b30..2ff00baa7c 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js @@ -10,7 +10,7 @@ erpnext.HierarchyChartMobile = class { this.wrapper = $(wrapper); this.page = wrapper.page; this.method = method; - this.doctype = doctype + this.doctype = doctype; this.page.main.css({ 'min-height': '300px', @@ -40,7 +40,7 @@ erpnext.HierarchyChartMobile = class { me.setup_node_click_action(this); me.setup_edit_node_action(this); } - } + }; } make_node_element(node) { @@ -78,7 +78,7 @@ erpnext.HierarchyChartMobile = class { me.company = company.get_value(); // svg for connectors - me.make_svg_markers() + me.make_svg_markers(); if (me.$sibling_group) me.$sibling_group.remove(); @@ -158,7 +158,7 @@ erpnext.HierarchyChartMobile = class { me.expand_node(root_node); } } - }) + }); } expand_node(node) { @@ -248,8 +248,8 @@ erpnext.HierarchyChartMobile = class { render_child_nodes(node, child_nodes) { if (!node.$children) { node.$children = $('
                ') - .hide() - .appendTo(node.$link.parent()); + .hide() + .appendTo(node.$link.parent()); node.$children.empty(); @@ -462,7 +462,7 @@ erpnext.HierarchyChartMobile = class { get_avatar(node) { return ` - ` + `; } expand_sibling_group_node(parent) { @@ -518,7 +518,7 @@ erpnext.HierarchyChartMobile = class { return; $(path).remove(); - }) + }); } refresh_connectors(node_parent, node_id) { @@ -527,4 +527,4 @@ erpnext.HierarchyChartMobile = class { $(`path[data-parent="${node_parent}"]`).remove(); this.add_connector(node_parent, node_id); } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/erpnext/public/scss/hierarchy_chart.scss b/erpnext/public/scss/hierarchy_chart.scss index eefc14d679..a54bf6f332 100644 --- a/erpnext/public/scss/hierarchy_chart.scss +++ b/erpnext/public/scss/hierarchy_chart.scss @@ -62,16 +62,13 @@ .edit-chart-node { display: block; + margin-right: 0.25rem; } .node-edit-icon { display: block; } - .edit-chart-node { - margin-right: 0.25rem; - } - .node-edit-icon > .icon{ stroke: var(--blue-500); } From ad066033925ed8f8e5bd6c2e8b7f9c3cc54e5e27 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 6 Jul 2021 18:16:49 +0530 Subject: [PATCH 015/147] fix: removing orphaned connectors --- .../js/hierarchy_chart/hierarchy_chart_desktop.js | 14 ++++++-------- .../js/hierarchy_chart/hierarchy_chart_mobile.js | 6 ++---- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js index 9e82fb2002..1896ac7c39 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js @@ -176,8 +176,8 @@ erpnext.HierarchyChart = class { collapse_node() { if (this.selected_node.expandable) { - this.selected_node.$children.hide('fast'); - $(`path[data-parent="${this.selected_node.id}"]`).hide('fast'); + this.selected_node.$children.hide(); + $(`path[data-parent="${this.selected_node.id}"]`).hide(); this.selected_node.expanded = false; } } @@ -237,8 +237,8 @@ erpnext.HierarchyChart = class { } } - node.$children.show('fast'); - $(`path[data-parent="${node.id}"]`).show('fast'); + node.$children.show(); + $(`path[data-parent="${node.id}"]`).show(); node.expanded = true; } @@ -262,9 +262,7 @@ erpnext.HierarchyChart = class { let parent_node = document.querySelector(`#${parent_id}`); let child_node = document.querySelector(`#${child_id}`); - // variable for the namespace - const svgns = 'http://www.w3.org/2000/svg'; - let path = document.createElementNS(svgns, 'path'); + let path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); // we need to connect right side of the parent to the left side of the child node let pos_parent_right = { @@ -438,7 +436,7 @@ erpnext.HierarchyChart = class { let parent = $(path).data('parent'); let child = $(path).data('child'); - if ($(parent).length || $(child).length) + if ($(`#${parent}`).length && $(`#${child}`).length) return; $(path).remove(); diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js index 2ff00baa7c..102cbb03b0 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js @@ -289,9 +289,7 @@ erpnext.HierarchyChartMobile = class { let parent_node = document.querySelector(`#${parent_id}`); let child_node = document.querySelector(`#${child_id}`); - // variable for the namespace - const svgns = 'http://www.w3.org/2000/svg'; - let path = document.createElementNS(svgns, 'path'); + let path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); let connector = undefined; @@ -514,7 +512,7 @@ erpnext.HierarchyChartMobile = class { let parent = $(path).data('parent'); let child = $(path).data('child'); - if ($(parent).length || $(child).length) + if ($(`#${parent}`).length && $(`#${child}`).length) return; $(path).remove(); From 6d5ee25bba0222b122d873340784cc82ee8309fc Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 7 Jul 2021 09:43:28 +0530 Subject: [PATCH 016/147] fix: unnecessary variables --- .../js/hierarchy_chart/hierarchy_chart_desktop.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js index 1896ac7c39..8d0685f80d 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js @@ -153,7 +153,7 @@ erpnext.HierarchyChart = class { } expand_node(node) { - let is_sibling = this.selected_node && this.selected_node.parent_id === node.parent_id; + const is_sibling = this.selected_node && this.selected_node.parent_id === node.parent_id; this.set_selected_node(node); this.show_active_path(node); this.collapse_previous_level_nodes(node); @@ -243,11 +243,9 @@ erpnext.HierarchyChart = class { } add_node(node, data) { - var $li = $('
              • '); - return new this.Node({ id: data.id, - parent: $li.appendTo(node.$children), + parent: $('
              • ').appendTo(node.$children), parent_id: node.id, image: data.image, name: data.name, @@ -259,8 +257,8 @@ erpnext.HierarchyChart = class { } add_connector(parent_id, child_id) { - let parent_node = document.querySelector(`#${parent_id}`); - let child_node = document.querySelector(`#${child_id}`); + const parent_node = document.querySelector(`#${parent_id}`); + const child_node = document.querySelector(`#${child_id}`); let path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); @@ -276,7 +274,7 @@ erpnext.HierarchyChart = class { let connector = this.get_connector(pos_parent_right, pos_child_left); - path.setAttribute("d", connector); + path.setAttribute('d', connector); this.set_path_attributes(path, parent_id, child_id); $('#connectors').append(path); @@ -385,7 +383,7 @@ erpnext.HierarchyChart = class { let node_element = $(`#${node.id}`); node_element.click(function() { - let is_sibling = me.selected_node.parent_id === node.parent_id; + const is_sibling = me.selected_node.parent_id === node.parent_id; if (is_sibling) { me.collapse_node(); From 6eec25127369e2b3af3658d11a80132facfee29e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 7 Jul 2021 12:05:50 +0530 Subject: [PATCH 017/147] feat: handle multiple root / orphan nodes --- .../organizational_chart.py | 3 +- .../hierarchy_chart_desktop.js | 48 +++++++++---------- .../hierarchy_chart/hierarchy_chart_mobile.js | 35 +++++++------- erpnext/public/scss/hierarchy_chart.scss | 4 ++ 4 files changed, 49 insertions(+), 41 deletions(-) diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.py b/erpnext/hr/page/organizational_chart/organizational_chart.py index f3aa13897d..77b8df7520 100644 --- a/erpnext/hr/page/organizational_chart/organizational_chart.py +++ b/erpnext/hr/page/organizational_chart/organizational_chart.py @@ -17,7 +17,7 @@ def get_children(parent=None, company=None, exclude_node=None, is_root=False, is if exclude_node: filters.append(['name', '!=', exclude_node]) - if parent and company and parent!=company: + if parent and company and parent != company: filters.append(['reports_to', '=', parent]) else: filters.append(['reports_to', '=', '']) @@ -32,6 +32,7 @@ def get_children(parent=None, company=None, exclude_node=None, is_root=False, is employee.connections = get_connections(employee.id) employee.expandable = 1 if is_expandable else 0 + employees.sort(key=lambda x: x['connections'], reverse=True) return employees diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js index 8d0685f80d..e89a98ac4f 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js @@ -84,11 +84,13 @@ erpnext.HierarchyChart = class { // setup hierarchy me.$hierarchy = $( `
                  -
                • +
                • +
                    +
                  `); me.page.main.append(me.$hierarchy); - me.render_root_node(); + me.render_root_nodes(); } } }); @@ -122,7 +124,7 @@ erpnext.HierarchyChart = class { `); } - render_root_node() { + render_root_nodes() { let me = this; frappe.call({ @@ -132,21 +134,28 @@ erpnext.HierarchyChart = class { }, callback: function(r) { if (r.message.length) { - let data = r.message[0]; + let nodes = r.message; + let node = undefined; + let first_root = undefined; - let root_node = new me.Node({ - id: data.id, - parent: me.$hierarchy.find('.root-level'), - parent_id: undefined, - image: data.image, - name: data.name, - title: data.title, - expandable: true, - connections: data.connections, - is_root: true, + $.each(nodes, (i, data) => { + node = new me.Node({ + id: data.id, + parent: $('
                • ').appendTo(me.$hierarchy.find('.node-children')), + parent_id: undefined, + image: data.image, + name: data.name, + title: data.title, + expandable: true, + connections: data.connections, + is_root: true + }); + + if (i == 0) + first_root = node; }); - me.expand_node(root_node); + me.expand_node(first_root); } } }); @@ -344,12 +353,7 @@ erpnext.HierarchyChart = class { collapse_previous_level_nodes(node) { let node_parent = $(`#${node.parent_id}`); - let previous_level_nodes = node_parent.parent().parent().children('li'); - if (node_parent.parent().hasClass('root-level')) { - previous_level_nodes = node_parent.parent().children('li'); - } - let node_card = undefined; previous_level_nodes.each(function() { @@ -409,10 +413,6 @@ erpnext.HierarchyChart = class { remove_levels_after_node(node) { let level = $(`#${node.id}`).parent().parent().parent(); - if ($(`#${node.id}`).parent().hasClass('root-level')) { - level = $(`#${node.id}`).parent(); - } - level = $('.hierarchy > li:eq('+ level.index() + ')'); level.nextAll('li').remove(); diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js index 102cbb03b0..5eee27b5fc 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js @@ -97,7 +97,7 @@ erpnext.HierarchyChartMobile = class { `); me.page.main.append(me.$hierarchy); - me.render_root_node(); + me.render_root_nodes(); } } }); @@ -131,7 +131,7 @@ erpnext.HierarchyChartMobile = class { `); } - render_root_node() { + render_root_nodes() { let me = this; frappe.call({ @@ -141,21 +141,21 @@ erpnext.HierarchyChartMobile = class { }, callback: function(r) { if (r.message.length) { - let data = r.message[0]; + let nodes = r.message; - let root_node = new me.Node({ - id: data.id, - parent: me.$hierarchy.find('.root-level'), - parent_id: undefined, - image: data.image, - name: data.name, - title: data.title, - expandable: true, - connections: data.connections, - is_root: true, + $.each(nodes, (_i, data) => { + return new me.Node({ + id: data.id, + parent: me.$hierarchy.find('.root-level'), + parent_id: undefined, + image: data.image, + name: data.name, + title: data.title, + expandable: true, + connections: data.connections, + is_root: true + }); }); - - me.expand_node(root_node); } } }); @@ -375,7 +375,10 @@ erpnext.HierarchyChartMobile = class { let node_element = $(`#${node.id}`); node_element.click(function() { - if (node_element.is(':visible') && node_element.hasClass('active-path')) { + if (node.is_root) { + me.$hierarchy.empty(); + me.add_node_to_hierarchy(node, true); + } else if (node_element.is(':visible') && node_element.hasClass('active-path')) { me.remove_levels_after_node(node); me.remove_orphaned_connectors(); } else { diff --git a/erpnext/public/scss/hierarchy_chart.scss b/erpnext/public/scss/hierarchy_chart.scss index a54bf6f332..dd523c3443 100644 --- a/erpnext/public/scss/hierarchy_chart.scss +++ b/erpnext/public/scss/hierarchy_chart.scss @@ -246,6 +246,10 @@ margin-top: 16px; } +.root-level .node-card { + margin: 0 0 16px; +} + // node group .collapsed-level { From df3bb9ea8caffe47e3696c10f711a393d78e6630 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 8 Jul 2021 09:53:31 +0530 Subject: [PATCH 018/147] perf: Optimise Rendering - optimise get_children function - use promises instead of callbacks - optimise selectors - use const wherever possible - use pure js instead of jquery for connectors for faster rendering --- .../organizational_chart.py | 22 ++--- .../hierarchy_chart_desktop.js | 82 +++++++++---------- .../hierarchy_chart/hierarchy_chart_mobile.js | 65 +++++++-------- 3 files changed, 78 insertions(+), 91 deletions(-) diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.py b/erpnext/hr/page/organizational_chart/organizational_chart.py index 77b8df7520..46578f3aaf 100644 --- a/erpnext/hr/page/organizational_chart/organizational_chart.py +++ b/erpnext/hr/page/organizational_chart/organizational_chart.py @@ -2,33 +2,25 @@ from __future__ import unicode_literals import frappe @frappe.whitelist() -def get_children(parent=None, company=None, exclude_node=None, is_root=False, is_tree=False, fields=None): +def get_children(parent=None, company=None): filters = [['status', '!=', 'Left']] if company and company != 'All Companies': filters.append(['company', '=', company]) - if not fields: - fields = ['employee_name as name', 'name as id', 'reports_to', 'image', 'designation as title'] - - if is_root: - parent = '' - - if exclude_node: - filters.append(['name', '!=', exclude_node]) - if parent and company and parent != company: filters.append(['reports_to', '=', parent]) else: filters.append(['reports_to', '=', '']) - employees = frappe.get_list('Employee', fields=fields, - filters=filters, order_by='name') + employees = frappe.get_list('Employee', + fields=['employee_name as name', 'name as id', 'reports_to', 'image', 'designation as title'], + filters=filters, + order_by='name' + ) for employee in employees: - is_expandable = frappe.get_all('Employee', filters=[ - ['reports_to', '=', employee.get('id')] - ]) + is_expandable = frappe.db.count('Employee', filters={'reports_to': employee.get('id')}) employee.connections = get_connections(employee.id) employee.expandable = 1 if is_expandable else 0 diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js index e89a98ac4f..bf366792a9 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js @@ -7,7 +7,6 @@ erpnext.HierarchyChart = class { - this method should return id, name, title, image, and connections for each node */ constructor(doctype, wrapper, method) { - this.wrapper = $(wrapper); this.page = wrapper.page; this.method = method; this.doctype = doctype; @@ -61,6 +60,8 @@ erpnext.HierarchyChart = class { frappe.breadcrumbs.add('HR'); let me = this; + if ($(`[data-fieldname="company"]`).length) return; + let company = this.page.add_field({ fieldtype: 'Link', options: 'Company', @@ -131,32 +132,30 @@ erpnext.HierarchyChart = class { method: me.method, args: { company: me.company - }, - callback: function(r) { - if (r.message.length) { - let nodes = r.message; - let node = undefined; - let first_root = undefined; + } + }).then(r => { + if (r.message.length) { + let node = undefined; + let first_root = undefined; - $.each(nodes, (i, data) => { - node = new me.Node({ - id: data.id, - parent: $('
                • ').appendTo(me.$hierarchy.find('.node-children')), - parent_id: undefined, - image: data.image, - name: data.name, - title: data.title, - expandable: true, - connections: data.connections, - is_root: true - }); - - if (i == 0) - first_root = node; + $.each(r.message, (i, data) => { + node = new me.Node({ + id: data.id, + parent: $('
                • ').appendTo(me.$hierarchy.find('.node-children')), + parent_id: undefined, + image: data.image, + name: data.name, + title: data.title, + expandable: true, + connections: data.connections, + is_root: true }); - me.expand_node(first_root); - } + if (i == 0) + first_root = node; + }); + + me.expand_node(first_root); } }); } @@ -204,18 +203,14 @@ erpnext.HierarchyChart = class { } get_child_nodes(node_id) { - let me = this; return new Promise(resolve => { frappe.call({ method: this.method, args: { parent: node_id, - company: me.company - }, - callback: (r) => { - resolve(r.message); + company: this.company } - }); + }).then(r => resolve(r.message)); }); } @@ -266,27 +261,28 @@ erpnext.HierarchyChart = class { } add_connector(parent_id, child_id) { + // using pure javascript for better performance const parent_node = document.querySelector(`#${parent_id}`); const child_node = document.querySelector(`#${child_id}`); let path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); // we need to connect right side of the parent to the left side of the child node - let pos_parent_right = { + const pos_parent_right = { x: parent_node.offsetLeft + parent_node.offsetWidth, y: parent_node.offsetTop + parent_node.offsetHeight / 2 }; - let pos_child_left = { + const pos_child_left = { x: child_node.offsetLeft - 5, y: child_node.offsetTop + child_node.offsetHeight / 2 }; - let connector = this.get_connector(pos_parent_right, pos_child_left); + const connector = this.get_connector(pos_parent_right, pos_child_left); path.setAttribute('d', connector); this.set_path_attributes(path, parent_id, child_id); - $('#connectors').append(path); + document.getElementById('connectors').appendChild(path); } get_connector(pos_parent_right, pos_child_left) { @@ -330,12 +326,13 @@ erpnext.HierarchyChart = class { set_path_attributes(path, parent_id, child_id) { path.setAttribute("data-parent", parent_id); path.setAttribute("data-child", child_id); + const parent = $(`#${parent_id}`); - if ($(`#${parent_id}`).hasClass('active')) { + if (parent.hasClass('active')) { path.setAttribute("class", "active-connector"); path.setAttribute("marker-start", "url(#arrowstart-active)"); path.setAttribute("marker-end", "url(#arrowhead-active)"); - } else if ($(`#${parent_id}`).hasClass('active-path')) { + } else if (parent.hasClass('active-path')) { path.setAttribute("class", "collapsed-connector"); path.setAttribute("marker-start", "url(#arrowstart-collapsed)"); path.setAttribute("marker-end", "url(#arrowhead-collapsed)"); @@ -343,8 +340,9 @@ erpnext.HierarchyChart = class { } set_selected_node(node) { - // remove .active class from the current node - $('.active').removeClass('active'); + // remove active class from the current node + if (this.selected_node) + this.selected_node.$link.removeClass('active'); // add active class to the newly selected node this.selected_node = node; @@ -411,9 +409,9 @@ erpnext.HierarchyChart = class { } remove_levels_after_node(node) { - let level = $(`#${node.id}`).parent().parent().parent(); + let level = $(`#${node.id}`).parent().parent().parent().index(); - level = $('.hierarchy > li:eq('+ level.index() + ')'); + level = $('.hierarchy > li:eq('+ level + ')'); level.nextAll('li').remove(); let nodes = level.find('.node-card'); @@ -431,8 +429,8 @@ erpnext.HierarchyChart = class { remove_orphaned_connectors() { let paths = $('#connectors > path'); $.each(paths, (_i, path) => { - let parent = $(path).data('parent'); - let child = $(path).data('child'); + const parent = $(path).data('parent'); + const child = $(path).data('child'); if ($(`#${parent}`).length && $(`#${child}`).length) return; diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js index 5eee27b5fc..17062e2585 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js @@ -7,7 +7,6 @@ erpnext.HierarchyChartMobile = class { - this method should return id, name, title, image, and connections for each node */ constructor(doctype, wrapper, method) { - this.wrapper = $(wrapper); this.page = wrapper.page; this.method = method; this.doctype = doctype; @@ -63,6 +62,8 @@ erpnext.HierarchyChartMobile = class { frappe.breadcrumbs.add('HR'); let me = this; + if ($(`[data-fieldname="company"]`).length) return; + let company = this.page.add_field({ fieldtype: 'Link', options: 'Company', @@ -139,24 +140,21 @@ erpnext.HierarchyChartMobile = class { args: { company: me.company }, - callback: function(r) { - if (r.message.length) { - let nodes = r.message; - - $.each(nodes, (_i, data) => { - return new me.Node({ - id: data.id, - parent: me.$hierarchy.find('.root-level'), - parent_id: undefined, - image: data.image, - name: data.name, - title: data.title, - expandable: true, - connections: data.connections, - is_root: true - }); + }).then(r => { + if (r.message.length) { + $.each(r.message, (_i, data) => { + return new me.Node({ + id: data.id, + parent: me.$hierarchy.find('.root-level'), + parent_id: undefined, + image: data.image, + name: data.name, + title: data.title, + expandable: true, + connections: data.connections, + is_root: true }); - } + }); } }); } @@ -237,11 +235,8 @@ erpnext.HierarchyChartMobile = class { parent: node_id, company: me.company, exclude_node: exclude_node - }, - callback: (r) => { - resolve(r.message); } - }); + }).then(r => resolve(r.message)); }); } @@ -286,10 +281,10 @@ erpnext.HierarchyChartMobile = class { } add_connector(parent_id, child_id) { - let parent_node = document.querySelector(`#${parent_id}`); - let child_node = document.querySelector(`#${child_id}`); + const parent_node = document.querySelector(`#${parent_id}`); + const child_node = document.querySelector(`#${child_id}`); - let path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); let connector = undefined; @@ -299,10 +294,10 @@ erpnext.HierarchyChartMobile = class { connector = this.get_connector_for_collapsed_node(parent_node, child_node); } - path.setAttribute("d", connector); + path.setAttribute('d', connector); this.set_path_attributes(path, parent_id, child_id); - $('#connectors').append(path); + document.getElementById('connectors').appendChild(path); } get_connector_for_active_node(parent_node, child_node) { @@ -351,19 +346,21 @@ erpnext.HierarchyChartMobile = class { set_path_attributes(path, parent_id, child_id) { path.setAttribute("data-parent", parent_id); path.setAttribute("data-child", child_id); + const parent = $(`#${parent_id}`); - if ($(`#${parent_id}`).hasClass('active')) { + if (parent.hasClass('active')) { path.setAttribute("class", "active-connector"); path.setAttribute("marker-start", "url(#arrowstart-active)"); path.setAttribute("marker-end", "url(#arrowhead-active)"); - } else if ($(`#${parent_id}`).hasClass('active-path')) { + } else if (parent.hasClass('active-path')) { path.setAttribute("class", "collapsed-connector"); } } set_selected_node(node) { // remove .active class from the current node - $('.active').removeClass('active'); + if (this.selected_node) + this.selected_node.$link.removeClass('active'); // add active class to the newly selected node this.selected_node = node; @@ -494,9 +491,9 @@ erpnext.HierarchyChartMobile = class { } remove_levels_after_node(node) { - let level = $(`#${node.id}`).parent().parent(); + let level = $(`#${node.id}`).parent().parent().index(); - level = $('.hierarchy-mobile > li:eq('+ (level.index()) + ')'); + level = $('.hierarchy-mobile > li:eq('+ level + ')'); level.nextAll('li').remove(); let current_node = level.find(`#${node.id}`); @@ -512,8 +509,8 @@ erpnext.HierarchyChartMobile = class { remove_orphaned_connectors() { let paths = $('#connectors > path'); $.each(paths, (_i, path) => { - let parent = $(path).data('parent'); - let child = $(path).data('child'); + const parent = $(path).data('parent'); + const child = $(path).data('child'); if ($(`#${parent}`).length && $(`#${child}`).length) return; From 48018b8d8c50caee0e8b6ff6bd5a81a35806dcdb Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 8 Jul 2021 11:23:50 +0530 Subject: [PATCH 019/147] fix: do not sort by number of connections --- .../hr/page/organizational_chart/organizational_chart.py | 1 - .../public/js/hierarchy_chart/hierarchy_chart_desktop.js | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.py b/erpnext/hr/page/organizational_chart/organizational_chart.py index 46578f3aaf..ce84b3c744 100644 --- a/erpnext/hr/page/organizational_chart/organizational_chart.py +++ b/erpnext/hr/page/organizational_chart/organizational_chart.py @@ -24,7 +24,6 @@ def get_children(parent=None, company=None): employee.connections = get_connections(employee.id) employee.expandable = 1 if is_expandable else 0 - employees.sort(key=lambda x: x['connections'], reverse=True) return employees diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js index bf366792a9..374787c6ef 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js @@ -135,8 +135,8 @@ erpnext.HierarchyChart = class { } }).then(r => { if (r.message.length) { + let expand_node = undefined; let node = undefined; - let first_root = undefined; $.each(r.message, (i, data) => { node = new me.Node({ @@ -151,11 +151,11 @@ erpnext.HierarchyChart = class { is_root: true }); - if (i == 0) - first_root = node; + if (!expand_node && data.connections) + expand_node = node; }); - me.expand_node(first_root); + me.expand_node(expand_node); } }); } From 05ffc0d3e0fc061639494df2978557c63f7d2d25 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 8 Jul 2021 16:55:42 +0530 Subject: [PATCH 020/147] feat: use icon for connections on mobile view --- .../js/hierarchy_chart/hierarchy_chart_mobile.js | 2 +- erpnext/public/js/templates/node_card.html | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js index 17062e2585..1985299378 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js @@ -50,7 +50,7 @@ erpnext.HierarchyChartMobile = class { image: node.image, parent: node.parent_id, connections: node.connections, - is_mobile: 1 + is_mobile: true }); node.parent.append(node_card); diff --git a/erpnext/public/js/templates/node_card.html b/erpnext/public/js/templates/node_card.html index c3d8e010b5..fb94df85ed 100644 --- a/erpnext/public/js/templates/node_card.html +++ b/erpnext/public/js/templates/node_card.html @@ -16,10 +16,16 @@
                  {{ title }}
                  - {% if connections == 1 %} -
                  · {{ connections }} Connection
                  + {% if is_mobile %} +
                  + · {{ connections }} +
                  {% else %} -
                  · {{ connections }} Connections
                  + {% if connections == 1 %} +
                  · {{ connections }} Connection
                  + {% else %} +
                  · {{ connections }} Connections
                  + {% endif %} {% endif %}
                  From 09c24c79496e942d749ff8e5a4ef502453064557 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 8 Jul 2021 17:05:40 +0530 Subject: [PATCH 021/147] fix: exclude active node while fetching sibling group --- .../hr/page/organizational_chart/organizational_chart.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.py b/erpnext/hr/page/organizational_chart/organizational_chart.py index ce84b3c744..1e03e3d06a 100644 --- a/erpnext/hr/page/organizational_chart/organizational_chart.py +++ b/erpnext/hr/page/organizational_chart/organizational_chart.py @@ -2,8 +2,7 @@ from __future__ import unicode_literals import frappe @frappe.whitelist() -def get_children(parent=None, company=None): - +def get_children(parent=None, company=None, exclude_node=None): filters = [['status', '!=', 'Left']] if company and company != 'All Companies': filters.append(['company', '=', company]) @@ -13,6 +12,9 @@ def get_children(parent=None, company=None): else: filters.append(['reports_to', '=', '']) + if exclude_node: + filters.append(['name', '!=', exclude_node]) + employees = frappe.get_list('Employee', fields=['employee_name as name', 'name as id', 'reports_to', 'image', 'designation as title'], filters=filters, From 06fc9e7847622cf0443c166d3514d6a2288e4902 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 8 Jul 2021 18:44:53 +0530 Subject: [PATCH 022/147] fix: sibling group expansion not working for root nodes --- .../hierarchy_chart/hierarchy_chart_mobile.js | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js index 1985299378..d48b4c8f36 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js @@ -88,16 +88,7 @@ erpnext.HierarchyChartMobile = class { me.$sibling_group = $(`
                  `); me.page.main.append(me.$sibling_group); - if (me.$hierarchy) - me.$hierarchy.remove(); - - // setup hierarchy - me.$hierarchy = $( - `
                    -
                  • -
                  `); - - me.page.main.append(me.$hierarchy); + me.setup_hierarchy() me.render_root_nodes(); } } @@ -132,6 +123,19 @@ erpnext.HierarchyChartMobile = class { `); } + setup_hierarchy() { + $(`#connectors`).empty(); + if (this.$hierarchy) + this.$hierarchy.remove(); + + this.$hierarchy = $( + `
                    +
                  • +
                  `); + + this.page.main.append(this.$hierarchy); + } + render_root_nodes() { let me = this; @@ -142,10 +146,13 @@ erpnext.HierarchyChartMobile = class { }, }).then(r => { if (r.message.length) { + let root_level = me.$hierarchy.find('.root-level'); + root_level.empty(); + $.each(r.message, (_i, data) => { return new me.Node({ id: data.id, - parent: me.$hierarchy.find('.root-level'), + parent: root_level, parent_id: undefined, image: data.image, name: data.name, @@ -401,7 +408,12 @@ erpnext.HierarchyChartMobile = class { $('.node-group').on('click', function() { let parent = $(this).attr('data-parent'); - me.expand_sibling_group_node(parent); + if (parent === 'undefined') { + me.setup_hierarchy(); + me.render_root_nodes(); + } else { + me.expand_sibling_group_node(parent); + } }); } From 24b31c0bf93a53c4f98fe5020b9575288b2a9aab Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 9 Jul 2021 01:03:02 +0530 Subject: [PATCH 023/147] fix(mobile): collapsed nodes not expanding --- .../hierarchy_chart/hierarchy_chart_mobile.js | 58 ++++++++++--------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js index d48b4c8f36..58530eaaf9 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js @@ -88,7 +88,7 @@ erpnext.HierarchyChartMobile = class { me.$sibling_group = $(`
                  `); me.page.main.append(me.$sibling_group); - me.setup_hierarchy() + me.setup_hierarchy(); me.render_root_nodes(); } } @@ -194,9 +194,9 @@ erpnext.HierarchyChartMobile = class { collapse_node() { let node = this.selected_node; - if (node.expandable) { + if (node.expandable && node.$children) { node.$children.hide(); - node.expanded = false; + node.expanded = 0; // add a collapsed level to show the collapsed parent // and a button beside it to move to that level @@ -212,10 +212,7 @@ erpnext.HierarchyChartMobile = class { frappe.run_serially([ () => this.get_child_nodes(node.parent_id, node.id), (child_nodes) => this.get_node_group(child_nodes, node.parent_id), - (node_group) => { - node_parent.find('.collapsed-level') - .append(node_group); - }, + (node_group) => node_parent.find('.collapsed-level').append(node_group), () => this.setup_node_group_action() ]); } @@ -268,7 +265,7 @@ erpnext.HierarchyChartMobile = class { } node.$children.show(); - node.expanded = true; + node.expanded = 1; } add_node(node, data) { @@ -380,13 +377,16 @@ erpnext.HierarchyChartMobile = class { node_element.click(function() { if (node.is_root) { + var el = $(this).detach(); me.$hierarchy.empty(); - me.add_node_to_hierarchy(node, true); + $(`#connectors`).empty(); + me.add_node_to_hierarchy(el, node); } else if (node_element.is(':visible') && node_element.hasClass('active-path')) { me.remove_levels_after_node(node); me.remove_orphaned_connectors(); } else { - me.add_node_to_hierarchy(node, true); + var el = $(this).detach(); + me.add_node_to_hierarchy(el, node); me.collapse_node(); } @@ -417,15 +417,15 @@ erpnext.HierarchyChartMobile = class { }); } - add_node_to_hierarchy(node) { - this.$hierarchy.append(` -
                • -
                  -
                  -
                • - `); + add_node_to_hierarchy(node_element, node) { + this.$hierarchy.append(`
                • `); + node_element.removeClass('active-child active-path'); + this.$hierarchy.find('.level:last').append(node_element); - node.$link.appendTo(this.$hierarchy.find('.level:last')); + let node_object = this.nodes[node.id]; + node_object.expanded = 0; + node_object.$children = undefined; + this.nodes[node.id] = node_object; } get_node_group(nodes, parent, collapsed=true) { @@ -478,9 +478,11 @@ erpnext.HierarchyChartMobile = class { expand_sibling_group_node(parent) { let node_object = this.nodes[parent]; let node = node_object.$link; + node.removeClass('active-child active-path'); node_object.expanded = 0; node_object.$children = undefined; + this.nodes[node.id] = node_object; // show parent's siblings and expand parent node frappe.run_serially([ @@ -491,17 +493,21 @@ erpnext.HierarchyChartMobile = class { this.$sibling_group.empty().append(node_group); }, () => this.setup_node_group_action(), - () => { - this.$hierarchy.empty().append(` -
                • - `); - this.$hierarchy.find('.level').append(node); - $(`#connectors`).empty(); - this.expand_node(node_object); - } + () => this.reattach_and_expand_node(node, node_object) ]); } + reattach_and_expand_node(node, node_object) { + var el = node.detach(); + + this.$hierarchy.empty().append(` +
                • + `); + this.$hierarchy.find('.level').append(el); + $(`#connectors`).empty(); + this.expand_node(node_object); + } + remove_levels_after_node(node) { let level = $(`#${node.id}`).parent().parent().index(); From 4582f28d0d072feddc7cbfcab4aac063fdfaad36 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 9 Jul 2021 01:25:26 +0530 Subject: [PATCH 024/147] fix: sider --- erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js index 58530eaaf9..5a6f168876 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js @@ -376,8 +376,9 @@ erpnext.HierarchyChartMobile = class { let node_element = $(`#${node.id}`); node_element.click(function() { + let el = $(this).detach(); + if (node.is_root) { - var el = $(this).detach(); me.$hierarchy.empty(); $(`#connectors`).empty(); me.add_node_to_hierarchy(el, node); @@ -385,7 +386,6 @@ erpnext.HierarchyChartMobile = class { me.remove_levels_after_node(node); me.remove_orphaned_connectors(); } else { - var el = $(this).detach(); me.add_node_to_hierarchy(el, node); me.collapse_node(); } From 40793f4a18c4a23d1414654116657323d7ea800c Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 13 Jul 2021 13:17:19 +0530 Subject: [PATCH 025/147] test: introduce cypress tests Co-authored-by: Ankush Co-authored-by: Nabin Hait --- .eslintrc | 6 +- .github/helper/install.sh | 2 +- .github/workflows/ui-tests.yml | 108 +++++++++++++++++++++++++++ cypress.json | 11 +++ cypress/fixtures/example.json | 5 ++ cypress/integration/test_customer.js | 13 ++++ cypress/plugins/index.js | 17 +++++ cypress/support/commands.js | 31 ++++++++ cypress/support/index.js | 26 +++++++ cypress/tsconfig.json | 12 +++ 10 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ui-tests.yml create mode 100644 cypress.json create mode 100644 cypress/fixtures/example.json create mode 100644 cypress/integration/test_customer.js create mode 100644 cypress/plugins/index.js create mode 100644 cypress/support/commands.js create mode 100644 cypress/support/index.js create mode 100644 cypress/tsconfig.json diff --git a/.eslintrc b/.eslintrc index 3b6ab7498d..ecfaab23ee 100644 --- a/.eslintrc +++ b/.eslintrc @@ -147,10 +147,14 @@ "Chart": true, "Cypress": true, "cy": true, + "describe": true, + "expect": true, "it": true, "context": true, "before": true, "beforeEach": true, - "onScan": true + "onScan": true, + "extend_cscript": true, + "localforage": true } } diff --git a/.github/helper/install.sh b/.github/helper/install.sh index 7b0f944c66..a6a6069d35 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -42,5 +42,5 @@ sed -i 's/socketio:/# socketio:/g' Procfile sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile bench get-app erpnext "${GITHUB_WORKSPACE}" -bench start & +bench start &> bench_run_logs.txt & bench --site test_site reinstall --yes diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml new file mode 100644 index 0000000000..412a05b0a1 --- /dev/null +++ b/.github/workflows/ui-tests.yml @@ -0,0 +1,108 @@ +name: UI + +on: + pull_request: + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-18.04 + + strategy: + fail-fast: false + + name: UI Tests (Cypress) + + services: + mysql: + image: mariadb:10.3 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: YES + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + + steps: + - name: Clone + uses: actions/checkout@v2 + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.7 + + - uses: actions/setup-node@v2 + with: + node-version: 14 + check-latest: true + + - name: Add to Hosts + run: | + echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts + + - name: Cache pip + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Cache node modules + uses: actions/cache@v2 + env: + cache-name: cache-node-modules + with: + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v2 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Cache cypress binary + uses: actions/cache@v2 + with: + path: ~/.cache + key: ${{ runner.os }}-cypress- + restore-keys: | + ${{ runner.os }}-cypress- + ${{ runner.os }}- + + - name: Install + run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh + env: + DB: mariadb + TYPE: ui + + - name: Site Setup + run: cd ~/frappe-bench/ && bench --site test_site execute erpnext.setup.utils.before_tests + + - name: cypress pre-requisites + run: cd ~/frappe-bench/apps/frappe && yarn add cypress-file-upload@^5 --no-lockfile + + + - name: Build Assets + run: cd ~/frappe-bench/ && bench build + + - name: UI Tests + run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless + env: + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + + - name: Show bench console if tests failed + if: ${{ failure() }} + run: cat ~/frappe-bench/bench_run_logs.txt diff --git a/cypress.json b/cypress.json new file mode 100644 index 0000000000..2f5026f62c --- /dev/null +++ b/cypress.json @@ -0,0 +1,11 @@ +{ + "baseUrl": "http://test_site:8000/", + "projectId": "da59y9", + "adminPassword": "admin", + "defaultCommandTimeout": 20000, + "pageLoadTimeout": 15000, + "retries": { + "runMode": 2, + "openMode": 2 + } +} \ No newline at end of file diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json new file mode 100644 index 0000000000..da18d9352a --- /dev/null +++ b/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} \ No newline at end of file diff --git a/cypress/integration/test_customer.js b/cypress/integration/test_customer.js new file mode 100644 index 0000000000..3d6ed5d0d8 --- /dev/null +++ b/cypress/integration/test_customer.js @@ -0,0 +1,13 @@ + +context('Customer', () => { + before(() => { + cy.login(); + }); + it('Check Customer Group', () => { + cy.visit(`app/customer/`); + cy.get('.primary-action').click(); + cy.wait(500); + cy.get('.custom-actions > .btn').click(); + cy.get_field('customer_group', 'Link').should('have.value', 'All Customer Groups'); + }); +}); diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js new file mode 100644 index 0000000000..07d9804a73 --- /dev/null +++ b/cypress/plugins/index.js @@ -0,0 +1,17 @@ +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +module.exports = () => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +}; diff --git a/cypress/support/commands.js b/cypress/support/commands.js new file mode 100644 index 0000000000..7ddc80ab8d --- /dev/null +++ b/cypress/support/commands.js @@ -0,0 +1,31 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }); +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }); +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }); +// +// +// -- This is will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }); + +const slug = (name) => name.toLowerCase().replace(" ", "-"); + +Cypress.Commands.add("go_to_doc", (doctype, name) => { + cy.visit(`/app/${slug(doctype)}/${encodeURIComponent(name)}`); +}); diff --git a/cypress/support/index.js b/cypress/support/index.js new file mode 100644 index 0000000000..72070cc81c --- /dev/null +++ b/cypress/support/index.js @@ -0,0 +1,26 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; +import '../../../frappe/cypress/support/commands' // eslint-disable-line + + +// Alternatively you can use CommonJS syntax: +// require('./commands') + +Cypress.Cookies.defaults({ + preserve: 'sid' +}); diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json new file mode 100644 index 0000000000..d90ebf6856 --- /dev/null +++ b/cypress/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "allowJs": true, + "baseUrl": "../node_modules", + "types": [ + "cypress" + ] + }, + "include": [ + "**/*.*" + ] +} \ No newline at end of file From f004b404d1ed344790600d3a1d2e038a53370031 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 14 Jul 2021 23:50:54 +0530 Subject: [PATCH 026/147] test: UI tests for org chart desktop --- .../test_organizational_chart_desktop.js | 102 ++++++++++++++++++ .../hierarchy_chart_desktop.js | 3 +- erpnext/tests/ui_test_helpers.py | 53 +++++++++ 3 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 cypress/integration/test_organizational_chart_desktop.js create mode 100644 erpnext/tests/ui_test_helpers.py diff --git a/cypress/integration/test_organizational_chart_desktop.js b/cypress/integration/test_organizational_chart_desktop.js new file mode 100644 index 0000000000..d50d551330 --- /dev/null +++ b/cypress/integration/test_organizational_chart_desktop.js @@ -0,0 +1,102 @@ +context('Organizational Chart', () => { + before(() => { + cy.login(); + cy.visit('/app/website'); + + cy.visit(`app/organizational-chart`); + cy.fill_field('company', 'Test Org Chart'); + cy.get('body').click(); + cy.wait(500); + }); + + beforeEach(() => { + cy.window().its('frappe').then(frappe => { + return frappe.call('erpnext.tests.ui_test_helpers.create_employee_records'); + }).as('employee_records'); + }); + + it('renders root nodes and loads children for the first expandable node', () => { + // check rendered root nodes and the node name, title, connections + cy.get('.hierarchy').find('.root-level ul.node-children').children() + .should('have.length', 2) + .first() + .as('first-child'); + + cy.get('@first-child').get('.node-name').contains('Test Employee 1'); + cy.get('@first-child').get('.node-info').find('.node-title').contains('CEO'); + cy.get('@first-child').get('.node-info').find('.node-connections').contains('· 2 Connections'); + + // check children of first node + cy.get('@employee_records').then(employee_records => { + // children of 1st root visible + cy.get(`[data-parent="${employee_records.message[0]}"]`).as('child-node') + cy.get('@child-node') + .should('have.length', 1) + .should('be.visible'); + cy.get('@child-node').get('.node-name').contains('Test Employee 3'); + + // connectors between first root node and immediate child + cy.get(`path[data-parent="${employee_records.message[0]}"]`) + .should('be.visible') + .invoke('attr', 'data-child') + .should('equal', employee_records.message[2]); + }); + }); + + it('hides active nodes children and connectors on expanding sibling node', () => { + cy.get('@employee_records').then(employee_records => { + // click sibling + cy.get(`#${employee_records.message[1]}`) + .click() + .should('have.class', 'active'); + + // child nodes and connectors hidden + cy.get(`[data-parent="${employee_records.message[0]}"]`).should('not.be.visible'); + cy.get(`path[data-parent="${employee_records.message[0]}"]`).should('not.be.visible'); + }); + }); + + it('collapses previous level nodes and refreshes connectors on expanding child node', () => { + cy.get('@employee_records').then(employee_records => { + // click child node + cy.get(`#${employee_records.message[3]}`) + .click() + .should('have.class', 'active'); + + // previous level nodes: parent should be on active-path; other nodes should be collapsed + cy.get(`#${employee_records.message[0]}`).should('have.class', 'collapsed'); + cy.get(`#${employee_records.message[1]}`).should('have.class', 'active-path'); + + // previous level connectors refreshed + cy.get(`path[data-parent="${employee_records.message[1]}"]`) + .should('have.class', 'collapsed-connector'); + + // child node's children and connectors rendered + cy.get(`[data-parent="${employee_records.message[3]}"]`).should('be.visible'); + cy.get(`path[data-parent="${employee_records.message[3]}"]`).should('be.visible'); + }); + }); + + it('expands previous level nodes', () => { + cy.get('@employee_records').then(employee_records => { + cy.get(`#${employee_records.message[0]}`) + .click() + .should('have.class', 'active'); + + cy.get(`[data-parent="${employee_records.message[0]}"]`) + .should('be.visible'); + + cy.get('ul.hierarchy').children().should('have.length', 2); + cy.get(`#connectors`).children().should('have.length', 1); + }); + }); + + it('edit node navigates to employee master', () => { + cy.get('@employee_records').then(employee_records => { + cy.get(`#${employee_records.message[0]}`).find('.btn-edit-node') + .click(); + + cy.url().should('include', `/employee/${employee_records.message[0]}`); + }); + }); +}); diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js index 374787c6ef..fe4d17c210 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js @@ -49,7 +49,8 @@ erpnext.HierarchyChart = class { title: node.title, image: node.image, parent: node.parent_id, - connections: node.connections + connections: node.connections, + is_mobile: false }); node.parent.append(node_card); diff --git a/erpnext/tests/ui_test_helpers.py b/erpnext/tests/ui_test_helpers.py new file mode 100644 index 0000000000..8e67b1cd34 --- /dev/null +++ b/erpnext/tests/ui_test_helpers.py @@ -0,0 +1,53 @@ +import frappe +from frappe import _ +from frappe.utils import getdate + +@frappe.whitelist() +def create_employee_records(): + company = create_company() + create_missing_designation() + + emp1 = create_employee('Test Employee 1', 'CEO') + emp2 = create_employee('Test Employee 2', 'CTO') + emp3 = create_employee('Test Employee 3', 'Head of Marketing and Sales', emp1) + emp4 = create_employee('Test Employee 4', 'Project Manager', emp2) + emp5 = create_employee('Test Employee 5', 'Analyst', emp3) + emp6 = create_employee('Test Employee 6', 'Software Developer', emp4) + + employees = [emp1, emp2, emp3, emp4, emp5, emp6] + return employees + +def create_company(): + company = frappe.db.exists('Company', 'Test Org Chart') + if not company: + company = frappe.get_doc({ + 'doctype': 'Company', + 'company_name': 'Test Org Chart', + 'country': 'India', + 'default_currency': 'INR' + }).insert().name + + return company + +def create_employee(first_name, designation, reports_to=None): + employee = frappe.db.exists('Employee', {'first_name': first_name, 'designation': designation}) + if not employee: + employee = frappe.get_doc({ + 'doctype': 'Employee', + 'first_name': first_name, + 'company': 'Test Org Chart', + 'gender': 'Female', + 'date_of_birth': getdate('08-12-1998'), + 'date_of_joining': getdate('01-01-2021'), + 'designation': designation, + 'reports_to': reports_to + }).insert().name + + return employee + +def create_missing_designation(): + if not frappe.db.exists('Designation', 'CTO'): + frappe.get_doc({ + 'doctype': 'Designation', + 'designation_name': 'CTO' + }).insert() \ No newline at end of file From ee7eaf9c70484dec3db6f282ca2f74a4a4ac2a2d Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 15 Jul 2021 19:19:09 +0530 Subject: [PATCH 027/147] test: UI tests for org chart mobile fix(mobile): detach node before emptying hierarchy fix(mobile): sibling group not rendering for first level --- .../test_organizational_chart_mobile.js | 182 ++++++++++++++++++ .../hierarchy_chart/hierarchy_chart_mobile.js | 13 +- erpnext/tests/ui_test_helpers.py | 7 +- 3 files changed, 195 insertions(+), 7 deletions(-) create mode 100644 cypress/integration/test_organizational_chart_mobile.js diff --git a/cypress/integration/test_organizational_chart_mobile.js b/cypress/integration/test_organizational_chart_mobile.js new file mode 100644 index 0000000000..656051289f --- /dev/null +++ b/cypress/integration/test_organizational_chart_mobile.js @@ -0,0 +1,182 @@ +context('Organizational Chart Mobile', () => { + before(() => { + cy.login(); + cy.viewport(375, 667); + cy.visit('/app/website'); + + cy.visit(`app/organizational-chart`); + cy.wait(500); + cy.fill_field('company', 'Test Org Chart'); + cy.get('body').click(); + cy.wait(500); + }); + + beforeEach(() => { + cy.viewport(375, 667); + cy.wait(500); + + cy.window().its('frappe').then(frappe => { + return frappe.call('erpnext.tests.ui_test_helpers.create_employee_records'); + }).as('employee_records'); + }); + + it('renders root nodes', () => { + // check rendered root nodes and the node name, title, connections + cy.get('.hierarchy-mobile').find('.root-level').children() + .should('have.length', 2) + .first() + .as('first-child'); + + cy.get('@first-child').get('.node-name').contains('Test Employee 1'); + cy.get('@first-child').get('.node-info').find('.node-title').contains('CEO'); + cy.get('@first-child').get('.node-info').find('.node-connections').contains('· 2'); + }); + + it('expands root node', () => { + cy.get('@employee_records').then(employee_records => { + cy.get(`#${employee_records.message[1]}`) + .click() + .should('have.class', 'active'); + + // other root node removed + cy.get(`#${employee_records.message[0]}`).should('not.exist'); + + // children of active root node + cy.get('.hierarchy-mobile').find('.level').first().find('ul.node-children').children() + .should('have.length', 2) + + cy.get(`[data-parent="${employee_records.message[1]}"]`).first().as('child-node'); + cy.get('@child-node').should('be.visible'); + + cy.get('@child-node') + .get('.node-name') + .contains('Test Employee 4'); + + // connectors between root node and immediate children + cy.get(`path[data-parent="${employee_records.message[1]}"]`).as('connectors'); + cy.get('@connectors') + .should('have.length', 2) + .should('be.visible') + + cy.get('@connectors') + .first() + .invoke('attr', 'data-child') + .should('eq', employee_records.message[3]); + }); + }); + + it('expands child node', () => { + cy.get('@employee_records').then(employee_records => { + cy.get(`#${employee_records.message[3]}`) + .click() + .should('have.class', 'active') + .as('expanded_node'); + + // 2 levels on screen; 1 on active path; 1 collapsed + cy.get('.hierarchy-mobile').children().should('have.length', 2); + cy.get(`#${employee_records.message[1]}`).should('have.class', 'active-path'); + + // children of expanded node visible + cy.get('@expanded_node') + .next() + .should('have.class', 'node-children') + .as('node-children'); + + cy.get('@node-children').children().should('have.length', 1); + cy.get('@node-children') + .first() + .get('.node-card') + .should('have.class', 'active-child') + .contains('Test Employee 7'); + + // orphan connectors removed + cy.get(`#connectors`).children().should('have.length', 2); + }); + }); + + it('renders sibling group', () => { + cy.get('@employee_records').then(employee_records => { + // sibling group visible for parent + cy.get(`#${employee_records.message[1]}`) + .next() + .as('sibling_group'); + + cy.get('@sibling_group') + .should('have.attr', 'data-parent', 'undefined') + .should('have.class', 'node-group') + .and('have.class', 'collapsed') + + cy.get('@sibling_group').get('.avatar-group').children().as('siblings'); + cy.get('@siblings').should('have.length', 1); + cy.get('@siblings') + .first() + .should('have.attr', 'title', 'Test Employee 1'); + + }); + }); + + it('expands previous level nodes', () => { + cy.get('@employee_records').then(employee_records => { + cy.get(`#${employee_records.message[6]}`) + .click() + .should('have.class', 'active'); + + // clicking on previous level node should remove all the nodes ahead + // and expand that node + cy.get(`#${employee_records.message[3]}`).click(); + cy.get(`#${employee_records.message[3]}`) + .should('have.class', 'active') + .should('not.have.class', 'active-path'); + + cy.get(`#${employee_records.message[6]}`).should('have.class', 'active-child'); + cy.get('.hierarchy-mobile').children().should('have.length', 2); + cy.get(`#connectors`).children().should('have.length', 2); + }); + }); + + it('expands sibling group', () => { + cy.get('@employee_records').then(employee_records => { + // sibling group visible for parent + cy.get(`#${employee_records.message[6]}`).click() + + cy.get(`#${employee_records.message[3]}`) + .next() + .click(); + + // siblings of parent should be visible + cy.get('.hierarchy-mobile').prev().as('sibling_group'); + cy.get('@sibling_group') + .should('exist') + .should('have.class', 'sibling-group') + .should('not.have.class', 'collapsed'); + + cy.get(`#${employee_records.message[1]}`) + .should('be.visible') + .should('have.class', 'active'); + + cy.get(`[data-parent="${employee_records.message[1]}"]`) + .should('be.visible') + .should('have.length', 2) + .should('have.class', 'active-child'); + }); + }); + + it('goes to the respective level after clicking on non-collapsed sibling group', () => { + // click on non-collapsed sibling group + cy.get('.hierarchy-mobile') + .prev() + .click(); + + // should take you to that level + cy.get('.hierarchy-mobile').find('li.level .node-card').should('have.length', 2); + }); + + it('edit node navigates to employee master', () => { + cy.get('@employee_records').then(employee_records => { + cy.get(`#${employee_records.message[0]}`).find('.btn-edit-node') + .click(); + + cy.url().should('include', `/employee/${employee_records.message[0]}`); + }); + }); +}); diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js index 5a6f168876..bd7946a1e1 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js @@ -128,6 +128,9 @@ erpnext.HierarchyChartMobile = class { if (this.$hierarchy) this.$hierarchy.remove(); + if (this.$sibling_group) + this.$sibling_group.empty(); + this.$hierarchy = $( `
                  • @@ -173,7 +176,7 @@ erpnext.HierarchyChartMobile = class { if (this.$sibling_group) { const sibling_parent = this.$sibling_group.find('.node-group').attr('data-parent'); - if (node.parent_id !== sibling_parent) + if (node.parent_id !== undefined && node.parent_id != sibling_parent) this.$sibling_group.empty(); } @@ -376,9 +379,10 @@ erpnext.HierarchyChartMobile = class { let node_element = $(`#${node.id}`); node_element.click(function() { - let el = $(this).detach(); + let el = undefined; if (node.is_root) { + el = $(this).detach(); me.$hierarchy.empty(); $(`#connectors`).empty(); me.add_node_to_hierarchy(el, node); @@ -386,6 +390,7 @@ erpnext.HierarchyChartMobile = class { me.remove_levels_after_node(node); me.remove_orphaned_connectors(); } else { + el = $(this).detach(); me.add_node_to_hierarchy(el, node); me.collapse_node(); } @@ -514,10 +519,10 @@ erpnext.HierarchyChartMobile = class { level = $('.hierarchy-mobile > li:eq('+ level + ')'); level.nextAll('li').remove(); - let current_node = level.find(`#${node.id}`); let node_object = this.nodes[node.id]; - + let current_node = level.find(`#${node.id}`).detach(); current_node.removeClass('active-child active-path'); + node_object.expanded = 0; node_object.$children = undefined; diff --git a/erpnext/tests/ui_test_helpers.py b/erpnext/tests/ui_test_helpers.py index 8e67b1cd34..99748dca02 100644 --- a/erpnext/tests/ui_test_helpers.py +++ b/erpnext/tests/ui_test_helpers.py @@ -11,10 +11,11 @@ def create_employee_records(): emp2 = create_employee('Test Employee 2', 'CTO') emp3 = create_employee('Test Employee 3', 'Head of Marketing and Sales', emp1) emp4 = create_employee('Test Employee 4', 'Project Manager', emp2) - emp5 = create_employee('Test Employee 5', 'Analyst', emp3) - emp6 = create_employee('Test Employee 6', 'Software Developer', emp4) + emp5 = create_employee('Test Employee 5', 'Engineer', emp2) + emp6 = create_employee('Test Employee 6', 'Analyst', emp3) + emp7 = create_employee('Test Employee 7', 'Software Developer', emp4) - employees = [emp1, emp2, emp3, emp4, emp5, emp6] + employees = [emp1, emp2, emp3, emp4, emp5, emp6, emp7] return employees def create_company(): From 8961a267f649f487eb9f9e3fc63aa6d40765e193 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 15 Jul 2021 19:32:15 +0530 Subject: [PATCH 028/147] fix: sider --- cypress/integration/test_organizational_chart_desktop.js | 4 ++-- cypress/integration/test_organizational_chart_mobile.js | 8 ++++---- erpnext/tests/ui_test_helpers.py | 3 +-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/cypress/integration/test_organizational_chart_desktop.js b/cypress/integration/test_organizational_chart_desktop.js index d50d551330..807ef5731a 100644 --- a/cypress/integration/test_organizational_chart_desktop.js +++ b/cypress/integration/test_organizational_chart_desktop.js @@ -29,7 +29,7 @@ context('Organizational Chart', () => { // check children of first node cy.get('@employee_records').then(employee_records => { // children of 1st root visible - cy.get(`[data-parent="${employee_records.message[0]}"]`).as('child-node') + cy.get(`[data-parent="${employee_records.message[0]}"]`).as('child-node'); cy.get('@child-node') .should('have.length', 1) .should('be.visible'); @@ -39,7 +39,7 @@ context('Organizational Chart', () => { cy.get(`path[data-parent="${employee_records.message[0]}"]`) .should('be.visible') .invoke('attr', 'data-child') - .should('equal', employee_records.message[2]); + .should('equal', employee_records.message[2]); }); }); diff --git a/cypress/integration/test_organizational_chart_mobile.js b/cypress/integration/test_organizational_chart_mobile.js index 656051289f..f48972bdc6 100644 --- a/cypress/integration/test_organizational_chart_mobile.js +++ b/cypress/integration/test_organizational_chart_mobile.js @@ -43,7 +43,7 @@ context('Organizational Chart Mobile', () => { // children of active root node cy.get('.hierarchy-mobile').find('.level').first().find('ul.node-children').children() - .should('have.length', 2) + .should('have.length', 2); cy.get(`[data-parent="${employee_records.message[1]}"]`).first().as('child-node'); cy.get('@child-node').should('be.visible'); @@ -56,7 +56,7 @@ context('Organizational Chart Mobile', () => { cy.get(`path[data-parent="${employee_records.message[1]}"]`).as('connectors'); cy.get('@connectors') .should('have.length', 2) - .should('be.visible') + .should('be.visible'); cy.get('@connectors') .first() @@ -104,7 +104,7 @@ context('Organizational Chart Mobile', () => { cy.get('@sibling_group') .should('have.attr', 'data-parent', 'undefined') .should('have.class', 'node-group') - .and('have.class', 'collapsed') + .and('have.class', 'collapsed'); cy.get('@sibling_group').get('.avatar-group').children().as('siblings'); cy.get('@siblings').should('have.length', 1); @@ -137,7 +137,7 @@ context('Organizational Chart Mobile', () => { it('expands sibling group', () => { cy.get('@employee_records').then(employee_records => { // sibling group visible for parent - cy.get(`#${employee_records.message[6]}`).click() + cy.get(`#${employee_records.message[6]}`).click(); cy.get(`#${employee_records.message[3]}`) .next() diff --git a/erpnext/tests/ui_test_helpers.py b/erpnext/tests/ui_test_helpers.py index 99748dca02..f66d69ba23 100644 --- a/erpnext/tests/ui_test_helpers.py +++ b/erpnext/tests/ui_test_helpers.py @@ -1,10 +1,9 @@ import frappe -from frappe import _ from frappe.utils import getdate @frappe.whitelist() def create_employee_records(): - company = create_company() + create_company() create_missing_designation() emp1 = create_employee('Test Employee 1', 'CEO') From 2c3866a53ea29c20029b5dba75f5184a490b36ff Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 16 Jul 2021 10:32:38 +0530 Subject: [PATCH 029/147] ci(cypress): use env variable for key documentation ref: https://docs.cypress.io/guides/guides/command-line\#cypress-run --- .github/workflows/ui-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 412a05b0a1..0f13e653ec 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -101,7 +101,7 @@ jobs: - name: UI Tests run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless env: - CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + CYPRESS_RECORD_KEY: 60a8e3bf-08f5-45b1-9269-2b207d7d30cd - name: Show bench console if tests failed if: ${{ failure() }} From 9d89b2afcf2cb2cffa8857dc66b0289a165b711b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 19 Jul 2021 15:47:31 +0530 Subject: [PATCH 030/147] fix: UI tests --- cypress.json | 2 +- cypress/integration/test_organizational_chart_desktop.js | 6 ++++-- cypress/integration/test_organizational_chart_mobile.js | 7 ++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/cypress.json b/cypress.json index 2f5026f62c..afcd657c53 100644 --- a/cypress.json +++ b/cypress.json @@ -1,5 +1,5 @@ { - "baseUrl": "http://test_site:8000/", + "baseUrl": "http://test_site:8000", "projectId": "da59y9", "adminPassword": "admin", "defaultCommandTimeout": 20000, diff --git a/cypress/integration/test_organizational_chart_desktop.js b/cypress/integration/test_organizational_chart_desktop.js index 807ef5731a..b11b9ea6ab 100644 --- a/cypress/integration/test_organizational_chart_desktop.js +++ b/cypress/integration/test_organizational_chart_desktop.js @@ -2,9 +2,11 @@ context('Organizational Chart', () => { before(() => { cy.login(); cy.visit('/app/website'); + cy.awesomebar('Organizational Chart'); + + cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); + cy.get('@input').type('Test Org Chart'); - cy.visit(`app/organizational-chart`); - cy.fill_field('company', 'Test Org Chart'); cy.get('body').click(); cy.wait(500); }); diff --git a/cypress/integration/test_organizational_chart_mobile.js b/cypress/integration/test_organizational_chart_mobile.js index f48972bdc6..a42562ff2e 100644 --- a/cypress/integration/test_organizational_chart_mobile.js +++ b/cypress/integration/test_organizational_chart_mobile.js @@ -3,10 +3,11 @@ context('Organizational Chart Mobile', () => { cy.login(); cy.viewport(375, 667); cy.visit('/app/website'); + cy.awesomebar('Organizational Chart'); + + cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); + cy.get('@input').type('Test Org Chart'); - cy.visit(`app/organizational-chart`); - cy.wait(500); - cy.fill_field('company', 'Test Org Chart'); cy.get('body').click(); cy.wait(500); }); From 7270ab5c2057ec21b2baf37daf937905f1886dc1 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 19 Jul 2021 16:26:17 +0530 Subject: [PATCH 031/147] fix(tests): clear filter before typing --- cypress/integration/test_organizational_chart_desktop.js | 2 +- cypress/integration/test_organizational_chart_mobile.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cypress/integration/test_organizational_chart_desktop.js b/cypress/integration/test_organizational_chart_desktop.js index b11b9ea6ab..516d254b81 100644 --- a/cypress/integration/test_organizational_chart_desktop.js +++ b/cypress/integration/test_organizational_chart_desktop.js @@ -5,7 +5,7 @@ context('Organizational Chart', () => { cy.awesomebar('Organizational Chart'); cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); - cy.get('@input').type('Test Org Chart'); + cy.get('@input').clear().type('Test Org Chart'); cy.get('body').click(); cy.wait(500); diff --git a/cypress/integration/test_organizational_chart_mobile.js b/cypress/integration/test_organizational_chart_mobile.js index a42562ff2e..503db68c18 100644 --- a/cypress/integration/test_organizational_chart_mobile.js +++ b/cypress/integration/test_organizational_chart_mobile.js @@ -6,7 +6,7 @@ context('Organizational Chart Mobile', () => { cy.awesomebar('Organizational Chart'); cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); - cy.get('@input').type('Test Org Chart'); + cy.get('@input').clear().type('Test Org Chart'); cy.get('body').click(); cy.wait(500); From 6e46be5058be06ecbd180f507f8267c1bc44b07c Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 19 Jul 2021 17:03:17 +0530 Subject: [PATCH 032/147] fix(tests): apply filters correctly --- .../test_organizational_chart_desktop.js | 7 ++++--- .../test_organizational_chart_mobile.js | 21 +++++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/cypress/integration/test_organizational_chart_desktop.js b/cypress/integration/test_organizational_chart_desktop.js index 516d254b81..cb12eb5c0c 100644 --- a/cypress/integration/test_organizational_chart_desktop.js +++ b/cypress/integration/test_organizational_chart_desktop.js @@ -5,14 +5,15 @@ context('Organizational Chart', () => { cy.awesomebar('Organizational Chart'); cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); - cy.get('@input').clear().type('Test Org Chart'); + cy.get('@input').clear().wait(200).type('Test Org Chart'); + cy.get('@input').type('{enter}', { delay: 100 }); + cy.get('@input').blur(); - cy.get('body').click(); cy.wait(500); }); beforeEach(() => { - cy.window().its('frappe').then(frappe => { + return cy.window().its('frappe').then(frappe => { return frappe.call('erpnext.tests.ui_test_helpers.create_employee_records'); }).as('employee_records'); }); diff --git a/cypress/integration/test_organizational_chart_mobile.js b/cypress/integration/test_organizational_chart_mobile.js index 503db68c18..a1d3c0083c 100644 --- a/cypress/integration/test_organizational_chart_mobile.js +++ b/cypress/integration/test_organizational_chart_mobile.js @@ -6,9 +6,10 @@ context('Organizational Chart Mobile', () => { cy.awesomebar('Organizational Chart'); cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); - cy.get('@input').clear().type('Test Org Chart'); + cy.get('@input').clear().wait(200).type('Test Org Chart'); + cy.get('@input').type('{enter}', { delay: 100 }); + cy.get('@input').blur(); - cy.get('body').click(); cy.wait(500); }); @@ -16,7 +17,7 @@ context('Organizational Chart Mobile', () => { cy.viewport(375, 667); cy.wait(500); - cy.window().its('frappe').then(frappe => { + return cy.window().its('frappe').then(frappe => { return frappe.call('erpnext.tests.ui_test_helpers.create_employee_records'); }).as('employee_records'); }); @@ -163,13 +164,15 @@ context('Organizational Chart Mobile', () => { }); it('goes to the respective level after clicking on non-collapsed sibling group', () => { - // click on non-collapsed sibling group - cy.get('.hierarchy-mobile') - .prev() - .click(); + cy.get('@employee_records').then(() => { + // click on non-collapsed sibling group + cy.get('.hierarchy-mobile') + .prev() + .click(); - // should take you to that level - cy.get('.hierarchy-mobile').find('li.level .node-card').should('have.length', 2); + // should take you to that level + cy.get('.hierarchy-mobile').find('li.level .node-card').should('have.length', 2); + }); }); it('edit node navigates to employee master', () => { From e327148edf315dec5a284868a9013e2e8a1a04eb Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 19 Jul 2021 17:34:15 +0530 Subject: [PATCH 033/147] fix: tests --- cypress/integration/test_organizational_chart_desktop.js | 6 +++--- cypress/integration/test_organizational_chart_mobile.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cypress/integration/test_organizational_chart_desktop.js b/cypress/integration/test_organizational_chart_desktop.js index cb12eb5c0c..95592e2f6a 100644 --- a/cypress/integration/test_organizational_chart_desktop.js +++ b/cypress/integration/test_organizational_chart_desktop.js @@ -4,10 +4,10 @@ context('Organizational Chart', () => { cy.visit('/app/website'); cy.awesomebar('Organizational Chart'); - cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); - cy.get('@input').clear().wait(200).type('Test Org Chart'); + cy.get('.frappe-control[data-fieldname=company] input').first().focus().as('input'); + cy.get('@input').clear().wait(200).type('Test Org Chart', { force: true }); cy.get('@input').type('{enter}', { delay: 100 }); - cy.get('@input').blur(); + cy.get('@input').blur({ force: true }); cy.wait(500); }); diff --git a/cypress/integration/test_organizational_chart_mobile.js b/cypress/integration/test_organizational_chart_mobile.js index a1d3c0083c..632d15ba6c 100644 --- a/cypress/integration/test_organizational_chart_mobile.js +++ b/cypress/integration/test_organizational_chart_mobile.js @@ -5,10 +5,10 @@ context('Organizational Chart Mobile', () => { cy.visit('/app/website'); cy.awesomebar('Organizational Chart'); - cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); - cy.get('@input').clear().wait(200).type('Test Org Chart'); + cy.get('.frappe-control[data-fieldname=company] input').first().focus().as('input'); + cy.get('@input').clear().wait(200).type('Test Org Chart', { force: true }); cy.get('@input').type('{enter}', { delay: 100 }); - cy.get('@input').blur(); + cy.get('@input').blur({ force: true }); cy.wait(500); }); From 89c5bb6066737418a01c18ec202c8fa78424a9b2 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 19 Jul 2021 22:19:28 +0530 Subject: [PATCH 034/147] fix: tests --- .../test_organizational_chart_desktop.js | 31 +++++++--------- .../test_organizational_chart_mobile.js | 37 ++++++++----------- erpnext/tests/ui_test_helpers.py | 6 +++ 3 files changed, 34 insertions(+), 40 deletions(-) diff --git a/cypress/integration/test_organizational_chart_desktop.js b/cypress/integration/test_organizational_chart_desktop.js index 95592e2f6a..0493732812 100644 --- a/cypress/integration/test_organizational_chart_desktop.js +++ b/cypress/integration/test_organizational_chart_desktop.js @@ -1,21 +1,17 @@ context('Organizational Chart', () => { before(() => { cy.login(); - cy.visit('/app/website'); + cy.visit('/app'); + + cy.call('erpnext.tests.ui_test_helpers.create_employee_records'); cy.awesomebar('Organizational Chart'); - cy.get('.frappe-control[data-fieldname=company] input').first().focus().as('input'); - cy.get('@input').clear().wait(200).type('Test Org Chart', { force: true }); - cy.get('@input').type('{enter}', { delay: 100 }); + cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); + cy.get('@input') + .clear({ force: true }) + .type('Test Org Chart', { force: true }); + cy.wait(200); cy.get('@input').blur({ force: true }); - - cy.wait(500); - }); - - beforeEach(() => { - return cy.window().its('frappe').then(frappe => { - return frappe.call('erpnext.tests.ui_test_helpers.create_employee_records'); - }).as('employee_records'); }); it('renders root nodes and loads children for the first expandable node', () => { @@ -29,8 +25,7 @@ context('Organizational Chart', () => { cy.get('@first-child').get('.node-info').find('.node-title').contains('CEO'); cy.get('@first-child').get('.node-info').find('.node-connections').contains('· 2 Connections'); - // check children of first node - cy.get('@employee_records').then(employee_records => { + cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { // children of 1st root visible cy.get(`[data-parent="${employee_records.message[0]}"]`).as('child-node'); cy.get('@child-node') @@ -47,7 +42,7 @@ context('Organizational Chart', () => { }); it('hides active nodes children and connectors on expanding sibling node', () => { - cy.get('@employee_records').then(employee_records => { + cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { // click sibling cy.get(`#${employee_records.message[1]}`) .click() @@ -60,7 +55,7 @@ context('Organizational Chart', () => { }); it('collapses previous level nodes and refreshes connectors on expanding child node', () => { - cy.get('@employee_records').then(employee_records => { + cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { // click child node cy.get(`#${employee_records.message[3]}`) .click() @@ -81,7 +76,7 @@ context('Organizational Chart', () => { }); it('expands previous level nodes', () => { - cy.get('@employee_records').then(employee_records => { + cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { cy.get(`#${employee_records.message[0]}`) .click() .should('have.class', 'active'); @@ -95,7 +90,7 @@ context('Organizational Chart', () => { }); it('edit node navigates to employee master', () => { - cy.get('@employee_records').then(employee_records => { + cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { cy.get(`#${employee_records.message[0]}`).find('.btn-edit-node') .click(); diff --git a/cypress/integration/test_organizational_chart_mobile.js b/cypress/integration/test_organizational_chart_mobile.js index 632d15ba6c..1dcfbcfeb1 100644 --- a/cypress/integration/test_organizational_chart_mobile.js +++ b/cypress/integration/test_organizational_chart_mobile.js @@ -2,24 +2,17 @@ context('Organizational Chart Mobile', () => { before(() => { cy.login(); cy.viewport(375, 667); - cy.visit('/app/website'); + cy.visit('/app'); + + cy.call('erpnext.tests.ui_test_helpers.create_employee_records'); cy.awesomebar('Organizational Chart'); - cy.get('.frappe-control[data-fieldname=company] input').first().focus().as('input'); - cy.get('@input').clear().wait(200).type('Test Org Chart', { force: true }); - cy.get('@input').type('{enter}', { delay: 100 }); + cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); + cy.get('@input') + .clear({ force: true }) + .type('Test Org Chart', { force: true }); + cy.wait(200); cy.get('@input').blur({ force: true }); - - cy.wait(500); - }); - - beforeEach(() => { - cy.viewport(375, 667); - cy.wait(500); - - return cy.window().its('frappe').then(frappe => { - return frappe.call('erpnext.tests.ui_test_helpers.create_employee_records'); - }).as('employee_records'); }); it('renders root nodes', () => { @@ -35,7 +28,7 @@ context('Organizational Chart Mobile', () => { }); it('expands root node', () => { - cy.get('@employee_records').then(employee_records => { + cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { cy.get(`#${employee_records.message[1]}`) .click() .should('have.class', 'active'); @@ -68,7 +61,7 @@ context('Organizational Chart Mobile', () => { }); it('expands child node', () => { - cy.get('@employee_records').then(employee_records => { + cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { cy.get(`#${employee_records.message[3]}`) .click() .should('have.class', 'active') @@ -97,7 +90,7 @@ context('Organizational Chart Mobile', () => { }); it('renders sibling group', () => { - cy.get('@employee_records').then(employee_records => { + cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { // sibling group visible for parent cy.get(`#${employee_records.message[1]}`) .next() @@ -118,7 +111,7 @@ context('Organizational Chart Mobile', () => { }); it('expands previous level nodes', () => { - cy.get('@employee_records').then(employee_records => { + cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { cy.get(`#${employee_records.message[6]}`) .click() .should('have.class', 'active'); @@ -137,7 +130,7 @@ context('Organizational Chart Mobile', () => { }); it('expands sibling group', () => { - cy.get('@employee_records').then(employee_records => { + cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { // sibling group visible for parent cy.get(`#${employee_records.message[6]}`).click(); @@ -164,7 +157,7 @@ context('Organizational Chart Mobile', () => { }); it('goes to the respective level after clicking on non-collapsed sibling group', () => { - cy.get('@employee_records').then(() => { + cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(() => { // click on non-collapsed sibling group cy.get('.hierarchy-mobile') .prev() @@ -176,7 +169,7 @@ context('Organizational Chart Mobile', () => { }); it('edit node navigates to employee master', () => { - cy.get('@employee_records').then(employee_records => { + cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { cy.get(`#${employee_records.message[0]}`).find('.btn-edit-node') .click(); diff --git a/erpnext/tests/ui_test_helpers.py b/erpnext/tests/ui_test_helpers.py index f66d69ba23..fc3aa29824 100644 --- a/erpnext/tests/ui_test_helpers.py +++ b/erpnext/tests/ui_test_helpers.py @@ -17,6 +17,12 @@ def create_employee_records(): employees = [emp1, emp2, emp3, emp4, emp5, emp6, emp7] return employees +@frappe.whitelist() +def get_employee_records(): + return frappe.db.get_list('Employee', filters={ + 'company': 'Test Org Chart' + }, pluck='name', order_by='name') + def create_company(): company = frappe.db.exists('Company', 'Test Org Chart') if not company: From 7176c0847e6aeb10dd0f68e501707074cf3345e1 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 19 Jul 2021 23:34:02 +0530 Subject: [PATCH 035/147] fix: tests --- .../test_organizational_chart_desktop.js | 19 +++++++++---------- .../test_organizational_chart_mobile.js | 19 +++++++++---------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/cypress/integration/test_organizational_chart_desktop.js b/cypress/integration/test_organizational_chart_desktop.js index 0493732812..52863f18e0 100644 --- a/cypress/integration/test_organizational_chart_desktop.js +++ b/cypress/integration/test_organizational_chart_desktop.js @@ -1,17 +1,16 @@ context('Organizational Chart', () => { before(() => { cy.login(); - cy.visit('/app'); - - cy.call('erpnext.tests.ui_test_helpers.create_employee_records'); + cy.visit('/app/website'); cy.awesomebar('Organizational Chart'); - cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); - cy.get('@input') - .clear({ force: true }) - .type('Test Org Chart', { force: true }); - cy.wait(200); - cy.get('@input').blur({ force: true }); + cy.call('erpnext.tests.ui_test_helpers.create_employee_records').then(() => { + cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); + cy.get('@input') + .clear({ force: true }) + .type('Test Org Chart{enter}', { force: true }) + .blur({ force: true }); + }); }); it('renders root nodes and loads children for the first expandable node', () => { @@ -27,7 +26,7 @@ context('Organizational Chart', () => { cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { // children of 1st root visible - cy.get(`[data-parent="${employee_records.message[0]}"]`).as('child-node'); + cy.get(`div[data-parent="${employee_records.message[0]}"]`).as('child-node'); cy.get('@child-node') .should('have.length', 1) .should('be.visible'); diff --git a/cypress/integration/test_organizational_chart_mobile.js b/cypress/integration/test_organizational_chart_mobile.js index 1dcfbcfeb1..2272a31046 100644 --- a/cypress/integration/test_organizational_chart_mobile.js +++ b/cypress/integration/test_organizational_chart_mobile.js @@ -2,17 +2,16 @@ context('Organizational Chart Mobile', () => { before(() => { cy.login(); cy.viewport(375, 667); - cy.visit('/app'); - - cy.call('erpnext.tests.ui_test_helpers.create_employee_records'); + cy.visit('/app/website'); cy.awesomebar('Organizational Chart'); - cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); - cy.get('@input') - .clear({ force: true }) - .type('Test Org Chart', { force: true }); - cy.wait(200); - cy.get('@input').blur({ force: true }); + cy.call('erpnext.tests.ui_test_helpers.create_employee_records').then(() => { + cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); + cy.get('@input') + .clear({ force: true }) + .type('Test Org Chart{enter}', { force: true }) + .blur({ force: true }); + }); }); it('renders root nodes', () => { @@ -40,7 +39,7 @@ context('Organizational Chart Mobile', () => { cy.get('.hierarchy-mobile').find('.level').first().find('ul.node-children').children() .should('have.length', 2); - cy.get(`[data-parent="${employee_records.message[1]}"]`).first().as('child-node'); + cy.get(`div[data-parent="${employee_records.message[1]}"]`).first().as('child-node'); cy.get('@child-node').should('be.visible'); cy.get('@child-node') From eb65ce662a5bdde4bd8a54a8268363d2651a3c09 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 20 Jul 2021 10:23:52 +0530 Subject: [PATCH 036/147] fix(test): increase timeout for record creation --- .../test_organizational_chart_desktop.js | 27 ++++++++++++++----- .../test_organizational_chart_mobile.js | 27 ++++++++++++++----- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/cypress/integration/test_organizational_chart_desktop.js b/cypress/integration/test_organizational_chart_desktop.js index 52863f18e0..0da4e560a7 100644 --- a/cypress/integration/test_organizational_chart_desktop.js +++ b/cypress/integration/test_organizational_chart_desktop.js @@ -4,12 +4,27 @@ context('Organizational Chart', () => { cy.visit('/app/website'); cy.awesomebar('Organizational Chart'); - cy.call('erpnext.tests.ui_test_helpers.create_employee_records').then(() => { - cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); - cy.get('@input') - .clear({ force: true }) - .type('Test Org Chart{enter}', { force: true }) - .blur({ force: true }); + cy.window().its('frappe.csrf_token').then(csrf_token => { + return cy.request({ + url: `/api/method/erpnext.tests.ui_test_helpers.create_employee_records`, + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + }, + timeout: 60000 + }) + .then(res => { + expect(res.status).eq(200); + cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); + cy.get('@input') + .clear({ force: true }) + .type('Test Org Chart{enter}', { force: true }) + .blur({ force: true }); + + cy.get('body').click(); + }); }); }); diff --git a/cypress/integration/test_organizational_chart_mobile.js b/cypress/integration/test_organizational_chart_mobile.js index 2272a31046..0374678a1a 100644 --- a/cypress/integration/test_organizational_chart_mobile.js +++ b/cypress/integration/test_organizational_chart_mobile.js @@ -5,12 +5,27 @@ context('Organizational Chart Mobile', () => { cy.visit('/app/website'); cy.awesomebar('Organizational Chart'); - cy.call('erpnext.tests.ui_test_helpers.create_employee_records').then(() => { - cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); - cy.get('@input') - .clear({ force: true }) - .type('Test Org Chart{enter}', { force: true }) - .blur({ force: true }); + cy.window().its('frappe.csrf_token').then(csrf_token => { + return cy.request({ + url: `/api/method/erpnext.tests.ui_test_helpers.create_employee_records`, + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + }, + timeout: 60000 + }) + .then(res => { + expect(res.status).eq(200); + cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); + cy.get('@input') + .clear({ force: true }) + .type('Test Org Chart{enter}', { force: true }) + .blur({ force: true }); + + cy.get('body').click(); + }); }); }); From 41dd0c5a8a75d674f151f09fe9412fdd42347d82 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 20 Jul 2021 10:55:05 +0530 Subject: [PATCH 037/147] fix: sider --- .../test_organizational_chart_desktop.js | 3 +- .../test_organizational_chart_mobile.js | 36 +++++++++---------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/cypress/integration/test_organizational_chart_desktop.js b/cypress/integration/test_organizational_chart_desktop.js index 0da4e560a7..57b7f7dced 100644 --- a/cypress/integration/test_organizational_chart_desktop.js +++ b/cypress/integration/test_organizational_chart_desktop.js @@ -14,8 +14,7 @@ context('Organizational Chart', () => { 'X-Frappe-CSRF-Token': csrf_token }, timeout: 60000 - }) - .then(res => { + }).then(res => { expect(res.status).eq(200); cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); cy.get('@input') diff --git a/cypress/integration/test_organizational_chart_mobile.js b/cypress/integration/test_organizational_chart_mobile.js index 0374678a1a..214229f6f6 100644 --- a/cypress/integration/test_organizational_chart_mobile.js +++ b/cypress/integration/test_organizational_chart_mobile.js @@ -7,25 +7,25 @@ context('Organizational Chart Mobile', () => { cy.window().its('frappe.csrf_token').then(csrf_token => { return cy.request({ - url: `/api/method/erpnext.tests.ui_test_helpers.create_employee_records`, - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'X-Frappe-CSRF-Token': csrf_token - }, - timeout: 60000 - }) - .then(res => { - expect(res.status).eq(200); - cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); - cy.get('@input') - .clear({ force: true }) - .type('Test Org Chart{enter}', { force: true }) - .blur({ force: true }); + url: `/api/method/erpnext.tests.ui_test_helpers.create_employee_records`, + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + }, + timeout: 60000 + }) + .then(res => { + expect(res.status).eq(200); + cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); + cy.get('@input') + .clear({ force: true }) + .type('Test Org Chart{enter}', { force: true }) + .blur({ force: true }); - cy.get('body').click(); - }); + cy.get('body').click(); + }); }); }); From 0222ee03580a37cc304b70a7e698d0fcc416c686 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 20 Jul 2021 12:19:44 +0530 Subject: [PATCH 038/147] fix: sider --- .../test_organizational_chart_desktop.js | 2 -- .../test_organizational_chart_mobile.js | 35 +++++++++---------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/cypress/integration/test_organizational_chart_desktop.js b/cypress/integration/test_organizational_chart_desktop.js index 57b7f7dced..fb46bbb433 100644 --- a/cypress/integration/test_organizational_chart_desktop.js +++ b/cypress/integration/test_organizational_chart_desktop.js @@ -21,8 +21,6 @@ context('Organizational Chart', () => { .clear({ force: true }) .type('Test Org Chart{enter}', { force: true }) .blur({ force: true }); - - cy.get('body').click(); }); }); }); diff --git a/cypress/integration/test_organizational_chart_mobile.js b/cypress/integration/test_organizational_chart_mobile.js index 214229f6f6..df90dbfa22 100644 --- a/cypress/integration/test_organizational_chart_mobile.js +++ b/cypress/integration/test_organizational_chart_mobile.js @@ -7,25 +7,22 @@ context('Organizational Chart Mobile', () => { cy.window().its('frappe.csrf_token').then(csrf_token => { return cy.request({ - url: `/api/method/erpnext.tests.ui_test_helpers.create_employee_records`, - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'X-Frappe-CSRF-Token': csrf_token - }, - timeout: 60000 - }) - .then(res => { - expect(res.status).eq(200); - cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); - cy.get('@input') - .clear({ force: true }) - .type('Test Org Chart{enter}', { force: true }) - .blur({ force: true }); - - cy.get('body').click(); - }); + url: `/api/method/erpnext.tests.ui_test_helpers.create_employee_records`, + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + }, + timeout: 60000 + }).then(res => { + expect(res.status).eq(200); + cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); + cy.get('@input') + .clear({ force: true }) + .type('Test Org Chart{enter}', { force: true }) + .blur({ force: true }); + }); }); }); From e1dc6980d09266b3db115546403d8bbdc0946b1e Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 5 Jul 2021 14:58:32 +0530 Subject: [PATCH 039/147] feat(Accounts Settings): Add 'Enable Discount Accounting' checkbox --- .../doctype/accounts_settings/accounts_settings.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 703e93c075..676c6a8b47 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -12,6 +12,7 @@ "role_allowed_to_over_bill", "make_payment_via_journal_entry", "column_break_11", + "enable_discount_accounting", "check_supplier_invoice_uniqueness", "unlink_payment_on_cancellation_of_invoice", "automatically_fetch_payment_terms", @@ -261,6 +262,12 @@ "fieldname": "post_change_gl_entries", "fieldtype": "Check", "label": "Create Ledger Entries for Change Amount" + }, + { + "default": "0", + "fieldname": "enable_discount_accounting", + "fieldtype": "Check", + "label": "Enable Discount Accounting" } ], "icon": "icon-cog", @@ -268,7 +275,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-06-17 20:26:03.721202", + "modified": "2021-07-05 14:56:19.820731", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", From a6690a8a3df1e467850f98c7a561d2b5b0b295eb Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 5 Jul 2021 15:09:05 +0530 Subject: [PATCH 040/147] feat(Sales Invoice): Add 'Discount Account' field in Items table --- .../doctype/sales_invoice_item/sales_invoice_item.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index 8e6952a93c..b65903bf5e 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -63,6 +63,7 @@ "finance_book", "col_break4", "expense_account", + "discount_account", "deferred_revenue", "deferred_revenue_account", "service_stop_date", @@ -821,12 +822,18 @@ "no_copy": 1, "options": "currency", "read_only": 1 + }, + { + "fieldname": "discount_account", + "fieldtype": "Link", + "label": "Discount Account", + "options": "Account" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-02-23 01:05:22.123527", + "modified": "2021-07-05 15:07:22.857128", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", From f6a9374356658f6375b50312838620ae6e14de5a Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 6 Jul 2021 00:36:06 +0530 Subject: [PATCH 041/147] feat: Create GL Entries for discount accounting --- .../doctype/sales_invoice/sales_invoice.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 6d1f6249c1..04c789d30a 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -846,6 +846,7 @@ class SalesInvoice(SellingController): self.allocate_advance_taxes(gl_entries) self.make_item_gl_entries(gl_entries) + self.make_discount_gl_entries(gl_entries) # merge gl entries before adding pos entries gl_entries = merge_similar_entries(gl_entries) @@ -959,6 +960,40 @@ class SalesInvoice(SellingController): erpnext.is_perpetual_inventory_enabled(self.company): gl_entries += super(SalesInvoice, self).get_gl_entries() + def make_discount_gl_entries(self, gl_entries): + enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) + + if enable_discount_accounting: + for item in self.get("items"): + if item.get('discount_amount') and item.get('discount_account'): + account_currency = get_account_currency(item.discount_account) + gl_entries.append( + self.get_gl_dict({ + "account": item.discount_account, + "against": self.customer, + "debit": flt(item.discount_amount), + "debit_in_account_currency": flt(item.discount_amount), + "cost_center": self.cost_center, + "project": self.project + }, account_currency, item=self) + ) + + income_account = (item.income_account + if (not item.enable_deferred_revenue or self.is_return) + else item.deferred_revenue_account) + + account_currency = get_account_currency(income_account) + gl_entries.append( + self.get_gl_dict({ + "account": income_account, + "against": self.customer, + "credit": flt(item.discount_amount), + "credit_in_account_currency": flt(item.discount_amount), + "cost_center": item.cost_center, + "project": item.project or self.project + }, account_currency, item=item) + ) + def make_loyalty_point_redemption_gle(self, gl_entries): if cint(self.redeem_loyalty_points): gl_entries.append( From c4e54295601b4dd6da940212ed561cb5f3abb20c Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 6 Jul 2021 16:28:19 +0530 Subject: [PATCH 042/147] feat: Filter list for Discount Account field in Items table --- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index f813425e6b..dc0d9da97b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -510,6 +510,14 @@ cur_frm.set_query("income_account", "items", function(doc) { } }); +// Discount Account in Details Table +// -------------------------------- +cur_frm.set_query("discount_account", "items", function(doc) { + return{ + query: "erpnext.controllers.queries.get_income_account", + filters: {'company': doc.company} + } +}); // Cost Center in Details Table // ----------------------------- From acb9e207ec96f642d5ce935157af4315717c0f57 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 6 Jul 2021 16:29:19 +0530 Subject: [PATCH 043/147] feat: Add Default Discount Account field --- erpnext/stock/doctype/item/item.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 6fed9efa63..f1c413e00a 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -91,6 +91,7 @@ "is_sales_item", "column_break3", "max_discount", + "default_discount_account", "deferred_revenue", "deferred_revenue_account", "enable_deferred_revenue", @@ -1058,6 +1059,12 @@ "fieldname": "website_image_alt", "fieldtype": "Data", "label": "Image Description" + }, + { + "fieldname": "default_discount_account", + "fieldtype": "Link", + "label": "Default Discount Account", + "options": "Account" } ], "has_web_view": 1, @@ -1067,7 +1074,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 1, - "modified": "2021-03-18 14:04:38.575519", + "modified": "2021-07-06 00:46:15.878648", "modified_by": "Administrator", "module": "Stock", "name": "Item", @@ -1138,4 +1145,4 @@ "sort_order": "DESC", "title_field": "item_name", "track_changes": 1 -} +} \ No newline at end of file From cdfefa261e021dcba4d1f09b064f2b7baf6d72f1 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 6 Jul 2021 16:51:12 +0530 Subject: [PATCH 044/147] feat: Assign Item's Default Discount Account if present --- erpnext/stock/get_item_details.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index cf52803fca..bf082f89be 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -287,6 +287,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): "warehouse": warehouse, "income_account": get_default_income_account(args, item_defaults, item_group_defaults, brand_defaults), "expense_account": expense_account or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults) , + "discount_account": None or get_default_discount_account(args, item_defaults), "cost_center": get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults), 'has_serial_no': item.has_serial_no, 'has_batch_no': item.has_batch_no, @@ -589,6 +590,10 @@ def get_default_expense_account(args, item, item_group, brand): or brand.get("expense_account") or args.expense_account) +def get_default_discount_account(args, item_defaults): + return (item_defaults.default_discount_account + or args.discount_account) + def get_default_deferred_account(args, item, fieldname=None): if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): return (item.get(fieldname) From f48fa2e7f3893020b9ccbd546ff83e53a2c1c581 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 6 Jul 2021 20:22:13 +0530 Subject: [PATCH 045/147] feat: Toggle display for discount accounting fields according to enable_discount_accounting --- .../doctype/accounts_settings/accounts_settings.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index ac4a2d6f16..9e33eb395b 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -21,6 +21,7 @@ class AccountsSettings(Document): self.validate_stale_days() self.enable_payment_schedule_in_print() + self.toggle_discount_accounting_fields() def validate_stale_days(self): if not self.allow_stale and cint(self.stale_days) <= 0: @@ -33,3 +34,9 @@ class AccountsSettings(Document): for doctype in ("Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"): make_property_setter(doctype, "due_date", "print_hide", show_in_print, "Check", validate_fields_for_doctype=False) make_property_setter(doctype, "payment_schedule", "print_hide", 0 if show_in_print else 1, "Check", validate_fields_for_doctype=False) + + def toggle_discount_accounting_fields(self): + enable_discount_accounting = cint(self.enable_discount_accounting) + + make_property_setter("Sales Invoice Item", "discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) + make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) \ No newline at end of file From d24b5f707a870e1e1c44ce0caadff9005c79f633 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 12 Jul 2021 18:56:06 +0530 Subject: [PATCH 046/147] fix: Add description for Enable Discount Accounting checkbox --- .../doctype/accounts_settings/accounts_settings.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 676c6a8b47..49a2afee85 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -12,7 +12,6 @@ "role_allowed_to_over_bill", "make_payment_via_journal_entry", "column_break_11", - "enable_discount_accounting", "check_supplier_invoice_uniqueness", "unlink_payment_on_cancellation_of_invoice", "automatically_fetch_payment_terms", @@ -20,6 +19,7 @@ "book_asset_depreciation_entry_automatically", "unlink_advance_payment_on_cancelation_of_order", "post_change_gl_entries", + "enable_discount_accounting", "tax_settings_section", "determine_address_tax_category_from", "column_break_19", @@ -265,6 +265,7 @@ }, { "default": "0", + "description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account", "fieldname": "enable_discount_accounting", "fieldtype": "Check", "label": "Enable Discount Accounting" @@ -275,7 +276,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-07-05 14:56:19.820731", + "modified": "2021-07-12 18:54:29.084958", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", From cfb94175a2458b47ec54fb0ccabcb0a682004504 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 12 Jul 2021 19:07:54 +0530 Subject: [PATCH 047/147] fix: Filter Discount Account list --- .../doctype/sales_invoice/sales_invoice.js | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index dc0d9da97b..17228a3c28 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -510,15 +510,6 @@ cur_frm.set_query("income_account", "items", function(doc) { } }); -// Discount Account in Details Table -// -------------------------------- -cur_frm.set_query("discount_account", "items", function(doc) { - return{ - query: "erpnext.controllers.queries.get_income_account", - filters: {'company': doc.company} - } -}); - // Cost Center in Details Table // ----------------------------- cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function(doc) { @@ -626,6 +617,19 @@ frappe.ui.form.on('Sales Invoice', { } } + // discount account + frm.fields_dict['items'].grid.get_field('discount_account').get_query = function(doc) { + if (erpnext.is_perpetual_inventory_enabled(doc.company)) { + return { + filters: { + 'report_type': 'Profit and Loss', + 'company': doc.company, + "is_group": 0 + } + } + } + } + frm.fields_dict['items'].grid.get_field('deferred_revenue_account').get_query = function(doc) { return { filters: { From 613d08faad544cd0c6855e30bf082538c2c992cf Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 12 Jul 2021 22:32:38 +0530 Subject: [PATCH 048/147] fix: Copy discount account from first row to all Items --- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 17228a3c28..2b48d66dca 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -347,8 +347,8 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte items_add: function(doc, cdt, cdn) { var row = frappe.get_doc(cdt, cdn); - this.frm.script_manager.copy_from_first_row("items", row, ["income_account", "cost_center"]); - }, + this.frm.script_manager.copy_from_first_row("items", row, ["income_account", "discount_account", "cost_center"]); + } set_dynamic_labels: function() { this._super(); From 546c8d125c6563ffa57f31b08a79948c7ce0d8ba Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 13 Jul 2021 01:37:30 +0530 Subject: [PATCH 049/147] fix: Move Default Discount Account field to Item Defaults --- erpnext/stock/doctype/item/item.json | 9 +- .../doctype/item_default/item_default.json | 548 ++++-------------- erpnext/stock/get_item_details.py | 4 +- 3 files changed, 104 insertions(+), 457 deletions(-) diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index f1c413e00a..f662bbd1c7 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -91,7 +91,6 @@ "is_sales_item", "column_break3", "max_discount", - "default_discount_account", "deferred_revenue", "deferred_revenue_account", "enable_deferred_revenue", @@ -1059,12 +1058,6 @@ "fieldname": "website_image_alt", "fieldtype": "Data", "label": "Image Description" - }, - { - "fieldname": "default_discount_account", - "fieldtype": "Link", - "label": "Default Discount Account", - "options": "Account" } ], "has_web_view": 1, @@ -1074,7 +1067,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 1, - "modified": "2021-07-06 00:46:15.878648", + "modified": "2021-07-13 01:29:06.071827", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item_default/item_default.json b/erpnext/stock/doctype/item_default/item_default.json index 96b5dfdc8f..bc171604f4 100644 --- a/erpnext/stock/doctype/item_default/item_default.json +++ b/erpnext/stock/doctype/item_default/item_default.json @@ -1,464 +1,118 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2018-05-03 02:29:24.444341", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2018-05-03 02:29:24.444341", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "default_warehouse", + "column_break_3", + "default_price_list", + "default_discount_account", + "purchase_defaults", + "buying_cost_center", + "default_supplier", + "column_break_8", + "expense_account", + "selling_defaults", + "selling_cost_center", + "column_break_12", + "income_account" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_warehouse", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Default Warehouse", - "length": 0, - "no_copy": 0, - "options": "Warehouse", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "default_warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Default Warehouse", + "options": "Warehouse" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_price_list", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Default Price List", - "length": 0, - "no_copy": 0, - "options": "Price List", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "default_price_list", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Default Price List", + "options": "Price List" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "purchase_defaults", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Purchase Defaults", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "purchase_defaults", + "fieldtype": "Section Break", + "label": "Purchase Defaults" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "buying_cost_center", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Default Buying Cost Center", - "length": 0, - "no_copy": 0, - "options": "Cost Center", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "buying_cost_center", + "fieldtype": "Link", + "label": "Default Buying Cost Center", + "options": "Cost Center" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_supplier", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Default Supplier", - "length": 0, - "no_copy": 0, - "options": "Supplier", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "default_supplier", + "fieldtype": "Link", + "label": "Default Supplier", + "options": "Supplier" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_8", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "expense_account", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Default Expense Account", - "length": 0, - "no_copy": 0, - "options": "Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "expense_account", + "fieldtype": "Link", + "label": "Default Expense Account", + "options": "Account" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "selling_defaults", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Sales Defaults", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "selling_defaults", + "fieldtype": "Section Break", + "label": "Sales Defaults" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "selling_cost_center", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Default Selling Cost Center", - "length": 0, - "no_copy": 0, - "options": "Cost Center", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "selling_cost_center", + "fieldtype": "Link", + "label": "Default Selling Cost Center", + "options": "Cost Center" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_12", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "income_account", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Default Income Account", - "length": 0, - "no_copy": 0, - "options": "Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "income_account", + "fieldtype": "Link", + "label": "Default Income Account", + "options": "Account" + }, + { + "fieldname": "default_discount_account", + "fieldtype": "Link", + "label": "Default Discount Account", + "options": "Account" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-12-07 11:48:07.638935", - "modified_by": "Administrator", - "module": "Stock", - "name": "Item Default", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "istable": 1, + "links": [], + "modified": "2021-07-13 01:26:03.860065", + "modified_by": "Administrator", + "module": "Stock", + "name": "Item Default", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index bf082f89be..cadb4aa97b 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -590,8 +590,8 @@ def get_default_expense_account(args, item, item_group, brand): or brand.get("expense_account") or args.expense_account) -def get_default_discount_account(args, item_defaults): - return (item_defaults.default_discount_account +def get_default_discount_account(args, item): + return (item.get("default_discount_account") or args.discount_account) def get_default_deferred_account(args, item, fieldname=None): From ee025b501fbb15ab21ceaf8f3bf36f892279c4ed Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 13 Jul 2021 01:43:41 +0530 Subject: [PATCH 050/147] fix: Filter options for Default Discount Account --- erpnext/stock/doctype/item/item.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 264baeaa47..2fee57f2d1 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -274,6 +274,17 @@ $.extend(erpnext.item, { } } + frm.fields_dict["item_defaults"].grid.get_field("default_discount_account").get_query = function(doc, cdt, cdn) { + const row = locals[cdt][cdn]; + return { + filters: { + 'report_type': 'Profit and Loss', + 'company': row.company, + "is_group": 0 + } + } + } + frm.fields_dict["item_defaults"].grid.get_field("buying_cost_center").get_query = function(doc, cdt, cdn) { const row = locals[cdt][cdn]; return { From bde5c619db8e80796a923cc57af1d97cc235f125 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 13 Jul 2021 02:06:03 +0530 Subject: [PATCH 051/147] fix: Add Discount Account field --- .../purchase_invoice_item/purchase_invoice_item.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 8a55ff87e3..922b567d15 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -73,6 +73,7 @@ "manufacturer_part_no", "accounting", "expense_account", + "discount_account", "col_break5", "is_fixed_asset", "asset_location", @@ -849,12 +850,18 @@ "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "discount_account", + "fieldtype": "Link", + "label": "Discount Account", + "options": "Account" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-06-16 19:57:03.101571", + "modified": "2021-07-13 02:04:37.787882", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", From e2c83c3bafe00951a66bb9405bd6da911539a875 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 13 Jul 2021 02:07:46 +0530 Subject: [PATCH 052/147] fix: Display Discount Account only if Enable Discount Accounting is checked --- .../accounts/doctype/accounts_settings/accounts_settings.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 9e33eb395b..d1abdba379 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -38,5 +38,7 @@ class AccountsSettings(Document): def toggle_discount_accounting_fields(self): enable_discount_accounting = cint(self.enable_discount_accounting) - make_property_setter("Sales Invoice Item", "discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) + for doctype in ["Sales Invoice Item", "Purchase Invoice Item"]: + make_property_setter(doctype, "discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) + make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) \ No newline at end of file From 377775ad8ead56e000717ce769e4da06fb5e7855 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 13 Jul 2021 02:09:40 +0530 Subject: [PATCH 053/147] fix: Copy Discount Account from first row --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index dc9094c3e9..81e3aaa8a5 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -365,7 +365,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ items_add: function(doc, cdt, cdn) { var row = frappe.get_doc(cdt, cdn); this.frm.script_manager.copy_from_first_row("items", row, - ["expense_account", "cost_center", "project"]); + ["expense_account", "discount_account", "cost_center", "project"]); }, on_submit: function() { From e06eb8e6a9fe6f6c36bf83f49bb5f72f1203b794 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 13 Jul 2021 02:14:18 +0530 Subject: [PATCH 054/147] fix: Filter options for Discount Account --- .../doctype/purchase_invoice/purchase_invoice.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 81e3aaa8a5..a1ee5d0076 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -508,6 +508,16 @@ frappe.ui.form.on("Purchase Invoice", { } } } + + frm.fields_dict['items'].grid.get_field('discount_account').get_query = function(doc) { + return { + filters: { + 'report_type': 'Profit and Loss', + 'company': doc.company, + "is_group": 0 + } + } + } }, refresh: function(frm) { From fd2852e87e3b03c019cff84669f908fdc70f3704 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 13 Jul 2021 03:01:02 +0530 Subject: [PATCH 055/147] fix: Create common function for discount accounting --- .../purchase_invoice/purchase_invoice.py | 1 + .../doctype/sales_invoice/sales_invoice.py | 34 -------------- erpnext/controllers/accounts_controller.py | 45 +++++++++++++++++++ 3 files changed, 46 insertions(+), 34 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index f7992797ed..78d1ee972f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -446,6 +446,7 @@ class PurchaseInvoice(BuyingController): self.make_supplier_gl_entry(gl_entries) self.make_item_gl_entries(gl_entries) + self.make_discount_gl_entries(gl_entries) if self.check_asset_cwip_enabled(): self.get_asset_gl_entry(gl_entries) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 04c789d30a..15fae84b79 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -960,40 +960,6 @@ class SalesInvoice(SellingController): erpnext.is_perpetual_inventory_enabled(self.company): gl_entries += super(SalesInvoice, self).get_gl_entries() - def make_discount_gl_entries(self, gl_entries): - enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) - - if enable_discount_accounting: - for item in self.get("items"): - if item.get('discount_amount') and item.get('discount_account'): - account_currency = get_account_currency(item.discount_account) - gl_entries.append( - self.get_gl_dict({ - "account": item.discount_account, - "against": self.customer, - "debit": flt(item.discount_amount), - "debit_in_account_currency": flt(item.discount_amount), - "cost_center": self.cost_center, - "project": self.project - }, account_currency, item=self) - ) - - income_account = (item.income_account - if (not item.enable_deferred_revenue or self.is_return) - else item.deferred_revenue_account) - - account_currency = get_account_currency(income_account) - gl_entries.append( - self.get_gl_dict({ - "account": income_account, - "against": self.customer, - "credit": flt(item.discount_amount), - "credit_in_account_currency": flt(item.discount_amount), - "cost_center": item.cost_center, - "project": item.project or self.project - }, account_currency, item=item) - ) - def make_loyalty_point_redemption_gle(self, gl_entries): if cint(self.redeem_loyalty_points): gl_entries.append( diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 4c313c43a7..0a11582508 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -808,6 +808,51 @@ class AccountsController(TransactionBase): tax_map[tax.account_head] -= allocated_amount allocated_tax_map[tax.account_head] -= allocated_amount + def make_discount_gl_entries(self, gl_entries): + enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) + + if enable_discount_accounting: + for item in self.get("items"): + if item.get('discount_amount') and item.get('discount_account'): + if self.doctype == "Purchase Invoice": + dr_or_cr = "credit" + rev_dr_cr = "debit" + supplier_or_customer = self.supplier + income_or_expense_account = (item.expense_account + if (not item.enable_deferred_expense or self.is_return) + else item.deferred_expense_account) + else: + dr_or_cr = "debit" + rev_dr_cr = "credit" + supplier_or_customer = self.customer + income_or_expense_account = (item.income_account + if (not item.enable_deferred_revenue or self.is_return) + else item.deferred_revenue_account) + + account_currency = get_account_currency(item.discount_account) + gl_entries.append( + self.get_gl_dict({ + "account": item.discount_account, + "against": supplier_or_customer, + dr_or_cr: flt(item.discount_amount), + dr_or_cr + "_in_account_currency": flt(item.discount_amount), + "cost_center": self.cost_center, + "project": self.project + }, account_currency, item=self) + ) + + account_currency = get_account_currency(income_or_expense_account) + gl_entries.append( + self.get_gl_dict({ + "account": income_or_expense_account, + "against": supplier_or_customer, + rev_dr_cr: flt(item.discount_amount), + rev_dr_cr + "_in_account_currency": flt(item.discount_amount), + "cost_center": item.cost_center, + "project": item.project or self.project + }, account_currency, item=item) + ) + def allocate_advance_taxes(self, gl_entries): tax_map = self.get_tax_map() for pe in self.get("advances"): From 38e87fce099100d1db3386730140212bc8a42d51 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 13 Jul 2021 17:41:29 +0530 Subject: [PATCH 056/147] fix: Add tests for discount accounting --- .../purchase_invoice/test_purchase_invoice.py | 20 ++++++++++++++++++- .../sales_invoice/test_sales_invoice.py | 15 ++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index ca4d009956..bd7ded269c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -230,6 +230,16 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account][1], gle.debit) self.assertEqual(expected_values[gle.account][2], gle.credit) + def test_purchase_invoice_with_discount_accounting_enabled(self): + enable_discount_accounting() + + discount_account = create_account(account_name="Discount Account", + parent_account="Indirect Expenses - _TC", company="_Test Company") + pi = make_purchase_invoice(discount_account=discount_account, discount_amount=100) + + discount_amount = frappe.db.get_value('GL Entry', {'account': discount_account, 'voucher_no': pi.name}, 'credit') + self.assertEqual(discount_amount, 100) + def test_purchase_invoice_change_naming_series(self): pi = frappe.copy_doc(test_records[1]) pi.insert() @@ -1170,6 +1180,11 @@ def unlink_payment_on_cancel_of_invoice(enable=1): accounts_settings.unlink_payment_on_cancellation_of_invoice = enable accounts_settings.save() +def enable_discount_accounting(enable=1): + accounts_settings = frappe.get_doc("Accounts Settings") + accounts_settings.enable_discount_accounting = enable + accounts_settings.save() + def make_purchase_invoice(**args): pi = frappe.new_doc("Purchase Invoice") args = frappe._dict(args) @@ -1192,6 +1207,7 @@ def make_purchase_invoice(**args): pi.return_against = args.return_against pi.is_subcontracted = args.is_subcontracted or "No" pi.supplier_warehouse = args.supplier_warehouse or "_Test Warehouse 1 - _TC" + pi.cost_center = args.cost_center or "_Test Cost Center - _TC" pi.append("items", { "item_code": args.item or args.item_code or "_Test Item", @@ -1200,7 +1216,9 @@ def make_purchase_invoice(**args): "received_qty": args.received_qty or 0, "rejected_qty": args.rejected_qty or 0, "rate": args.rate or 50, - 'expense_account': args.expense_account or '_Test Account Cost for Goods Sold - _TC', + "expense_account": args.expense_account or '_Test Account Cost for Goods Sold - _TC', + "discount_account": args.discount_account or None, + "discount_amount": args.discount_amount or 0, "conversion_factor": 1.0, "serial_no": args.serial_no, "stock_uom": args.uom or "_Test UOM", diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index dbc7f8632f..d03a874c28 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1984,6 +1984,18 @@ class TestSalesInvoice(unittest.TestCase): sales_invoice.save() self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC") + def test_sales_invoice_with_discount_accounting_enabled(self): + from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import enable_discount_accounting + + enable_discount_accounting() + + discount_account = create_account(account_name="Discount Account", + parent_account="Indirect Expenses - _TC", company="_Test Company") + si = create_sales_invoice(discount_account=discount_account, discount_amount=100) + + discount_amount = frappe.db.get_value('GL Entry', {'account': discount_account, 'voucher_no': si.name}, 'debit') + self.assertEqual(discount_amount, 100) + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() si.naming_series = 'INV-2020-.#####' @@ -2152,6 +2164,7 @@ def create_sales_invoice(**args): si.currency=args.currency or "INR" si.conversion_rate = args.conversion_rate or 1 si.naming_series = args.naming_series or "T-SINV-" + si.cost_center = args.cost_center or "_Test Cost Center - _TC" si.append("items", { "item_code": args.item or args.item_code or "_Test Item", @@ -2165,6 +2178,8 @@ def create_sales_invoice(**args): "rate": args.rate if args.get("rate") is not None else 100, "income_account": args.income_account or "Sales - _TC", "expense_account": args.expense_account or "Cost of Goods Sold - _TC", + "discount_account": args.discount_account or None, + "discount_amount": args.discount_amount or 0, "cost_center": args.cost_center or "_Test Cost Center - _TC", "serial_no": args.serial_no, "conversion_factor": 1 From 9e788cfdcd866780b16195fff411a4dc4bfd822d Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 15 Jul 2021 22:01:02 +0530 Subject: [PATCH 057/147] fix: Add Additional Discount Account field --- .../accounts/doctype/sales_invoice/sales_invoice.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index e7dd6b8a60..5c09b71cf3 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -104,6 +104,7 @@ "section_break_49", "apply_discount_on", "base_discount_amount", + "additional_discount_account", "column_break_51", "additional_discount_percentage", "discount_amount", @@ -1966,6 +1967,12 @@ "fieldname": "disable_rounded_total", "fieldtype": "Check", "label": "Disable Rounded Total" + }, + { + "fieldname": "additional_discount_account", + "fieldtype": "Link", + "label": "Additional Discount Account", + "options": "Account" } ], "icon": "fa fa-file-text", @@ -1978,7 +1985,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2021-05-20 22:48:33.988881", + "modified": "2021-07-15 21:57:17.544279", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", From c1d65cfa8d987c734b412026a7fbc5aec427359a Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 15 Jul 2021 22:01:38 +0530 Subject: [PATCH 058/147] fix: Filter options for Additional Discount Account --- .../accounts/doctype/sales_invoice/sales_invoice.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 2b48d66dca..fab0e11324 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -591,6 +591,16 @@ frappe.ui.form.on('Sales Invoice', { }; }); + frm.set_query("additional_discount_account", function() { + return { + filters: { + company: frm.doc.company, + is_group: 0, + root_type: "Profit and Loss", + } + }; + }); + frm.custom_make_buttons = { 'Delivery Note': 'Delivery', 'Sales Invoice': 'Return / Credit Note', From b64787ee9ca81aa18904419e072296c1932ed15a Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 15 Jul 2021 22:02:43 +0530 Subject: [PATCH 059/147] fix: Only display Additional Discount Account if Enable Discount Accounting is checked --- .../accounts/doctype/accounts_settings/accounts_settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index d1abdba379..053f061acc 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -41,4 +41,5 @@ class AccountsSettings(Document): for doctype in ["Sales Invoice Item", "Purchase Invoice Item"]: make_property_setter(doctype, "discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) - make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) \ No newline at end of file + make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) + make_property_setter("Sales Invoice", "additional_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) \ No newline at end of file From 38327fc17764516a5844af6ee4fcf36986e715b6 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 15 Jul 2021 22:03:46 +0530 Subject: [PATCH 060/147] fix: Make additional GL Entries for discount applied on taxes --- erpnext/controllers/accounts_controller.py | 36 ++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 0a11582508..8fc4023f2e 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -852,6 +852,42 @@ class AccountsController(TransactionBase): "project": item.project or self.project }, account_currency, item=item) ) + + if self.get('discount_amount') and self.get('additional_discount_account'): + self.make_gle_for_additional_discount_applied_on_taxes(gl_entries) + + def make_gle_for_additional_discount_applied_on_taxes(self, gl_entries): + for tax in self.get("taxes"): + if flt(tax.base_tax_amount_after_discount_amount) and flt(tax.base_tax_amount): + account_currency = get_account_currency(tax.account_head) + additional_discount_applied_on_taxes = flt(tax.base_tax_amount) - flt(tax.base_tax_amount_after_discount_amount) + + gl_entries.append( + self.get_gl_dict({ + "account": tax.account_head, + "against": self.customer, + "credit": flt(additional_discount_applied_on_taxes, + tax.precision("tax_amount_after_discount_amount")), + "credit_in_account_currency": (flt(additional_discount_applied_on_taxes, + tax.precision("base_tax_amount_after_discount_amount")) if account_currency==self.company_currency else + flt(additional_discount_applied_on_taxes, tax.precision("tax_amount_after_discount_amount"))), + "cost_center": tax.cost_center + }, account_currency, item=tax) + ) + + gl_entries.append( + self.get_gl_dict({ + "account": self.additional_discount_account, + "against": self.customer, + "debit": flt(additional_discount_applied_on_taxes, + tax.precision("tax_amount_after_discount_amount")), + "debit_in_account_currency": (flt(additional_discount_applied_on_taxes, + tax.precision("base_tax_amount_after_discount_amount")) if account_currency==self.company_currency else + flt(additional_discount_applied_on_taxes, tax.precision("tax_amount_after_discount_amount"))), + "cost_center": tax.cost_center + }, account_currency, item=tax) + ) + def allocate_advance_taxes(self, gl_entries): tax_map = self.get_tax_map() From d6f409addcedaff16339000374b74a5d93be31db Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 16 Jul 2021 02:18:45 +0530 Subject: [PATCH 061/147] fix: Sider issues --- erpnext/stock/doctype/item/item.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 2fee57f2d1..4566618385 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -282,8 +282,8 @@ $.extend(erpnext.item, { 'company': row.company, "is_group": 0 } - } - } + }; + }; frm.fields_dict["item_defaults"].grid.get_field("buying_cost_center").get_query = function(doc, cdt, cdn) { const row = locals[cdt][cdn]; From d0d9e83ad202f22d1cc3c6c21f20f9b934ad3700 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 17 Jul 2021 17:34:50 +0530 Subject: [PATCH 062/147] fix: Check all expected GL Entries --- .../doctype/purchase_invoice/test_purchase_invoice.py | 11 +++++++++-- .../doctype/sales_invoice/test_sales_invoice.py | 9 +++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index bd7ded269c..36fbc7e351 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -231,14 +231,21 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account][2], gle.credit) def test_purchase_invoice_with_discount_accounting_enabled(self): + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import check_gl_entries + enable_discount_accounting() discount_account = create_account(account_name="Discount Account", parent_account="Indirect Expenses - _TC", company="_Test Company") pi = make_purchase_invoice(discount_account=discount_account, discount_amount=100) - discount_amount = frappe.db.get_value('GL Entry', {'account': discount_account, 'voucher_no': pi.name}, 'credit') - self.assertEqual(discount_amount, 100) + expected_gle = [ + ["Discount Account - _TC", 0.0, 100.0, nowdate()], + ["_Test Account Cost for Goods Sold - _TC", 350.0, 0.0, nowdate()], + ["Creditors - _TC", 0.0, 250.0, nowdate()] + ] + + check_gl_entries(self, pi.name, expected_gle, nowdate()) def test_purchase_invoice_change_naming_series(self): pi = frappe.copy_doc(test_records[1]) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index d03a874c28..2ddfad9c31 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1992,9 +1992,14 @@ class TestSalesInvoice(unittest.TestCase): discount_account = create_account(account_name="Discount Account", parent_account="Indirect Expenses - _TC", company="_Test Company") si = create_sales_invoice(discount_account=discount_account, discount_amount=100) + + expected_gle = [ + ["Discount Account - _TC", 100.0, 0.0, nowdate()], + ["Sales - _TC", 0.0, 200.0, nowdate()], + ["Debtors - _TC", 100.0, 0.0, nowdate()] + ] - discount_amount = frappe.db.get_value('GL Entry', {'account': discount_account, 'voucher_no': si.name}, 'debit') - self.assertEqual(discount_amount, 100) + check_gl_entries(self, si.name, expected_gle, nowdate()) def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() From 03f270697194c81f50a66d3318cbf2928ddfb9e8 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 17 Jul 2021 17:40:43 +0530 Subject: [PATCH 063/147] fix: Add Additional Discount Account field --- .../doctype/purchase_invoice/purchase_invoice.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 00ef7d5c18..96ae828f46 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -96,6 +96,7 @@ "section_break_44", "apply_discount_on", "base_discount_amount", + "additional_discount_account", "column_break_46", "additional_discount_percentage", "discount_amount", @@ -1377,13 +1378,19 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "additional_discount_account", + "fieldtype": "Link", + "label": "Additional Discount Account", + "options": "Account" } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2021-06-15 18:20:56.806195", + "modified": "2021-07-17 17:37:50.570595", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", From ff25683378769b5869f19dc49f8165d18938a8fd Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 17 Jul 2021 17:41:06 +0530 Subject: [PATCH 064/147] fix: Filter options for Additional Discount Account --- .../doctype/purchase_invoice/purchase_invoice.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index a1ee5d0076..4ed31cc227 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -499,6 +499,16 @@ frappe.ui.form.on("Purchase Invoice", { 'Payment Entry': 'Payment' } + frm.set_query("additional_discount_account", function() { + return { + filters: { + company: frm.doc.company, + is_group: 0, + root_type: "Profit and Loss", + } + }; + }); + frm.fields_dict['items'].grid.get_field('deferred_expense_account').get_query = function(doc) { return { filters: { From 9dd2a9e8973fe4e5c70dc4aa65683870281e4091 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 17 Jul 2021 17:42:36 +0530 Subject: [PATCH 065/147] fix: Create ledger entries for discount applied on taxes in make_tax_gl_entries --- .../accounts/doctype/purchase_invoice/purchase_invoice.py | 5 +++++ erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 5 +++++ erpnext/controllers/accounts_controller.py | 3 --- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 78d1ee972f..006f5bb0e8 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -888,6 +888,11 @@ class PurchaseInvoice(BuyingController): "remarks": self.remarks or "Accounting Entry for Stock" }, item=tax)) + enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) + + if enable_discount_accounting and self.get('discount_amount') and self.get('additional_discount_account'): + self.make_gle_for_additional_discount_applied_on_taxes(gl_entries) + def make_internal_transfer_gl_entries(self, gl_entries): if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges): account_currency = get_account_currency(self.unrealized_profit_loss_account) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 15fae84b79..ee9b59e769 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -902,6 +902,11 @@ class SalesInvoice(SellingController): }, account_currency, item=tax) ) + enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) + + if enable_discount_accounting and self.get('discount_amount') and self.get('additional_discount_account'): + self.make_gle_for_additional_discount_applied_on_taxes(gl_entries) + def make_internal_transfer_gl_entries(self, gl_entries): if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges): account_currency = get_account_currency(self.unrealized_profit_loss_account) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 8fc4023f2e..f4593c2a76 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -852,9 +852,6 @@ class AccountsController(TransactionBase): "project": item.project or self.project }, account_currency, item=item) ) - - if self.get('discount_amount') and self.get('additional_discount_account'): - self.make_gle_for_additional_discount_applied_on_taxes(gl_entries) def make_gle_for_additional_discount_applied_on_taxes(self, gl_entries): for tax in self.get("taxes"): From 4da7c5882ba8a05c42a013c3b0690b1701da14f9 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 17 Jul 2021 17:45:35 +0530 Subject: [PATCH 066/147] fix: Only display Additional Discount Account if Enable Discount Accounting is checked --- .../accounts/doctype/accounts_settings/accounts_settings.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 053f061acc..24b0ec4d4a 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -41,5 +41,7 @@ class AccountsSettings(Document): for doctype in ["Sales Invoice Item", "Purchase Invoice Item"]: make_property_setter(doctype, "discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) - make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) - make_property_setter("Sales Invoice", "additional_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) \ No newline at end of file + for doctype in ["Sales Invoice", "Purchase Invoice"]: + make_property_setter(doctype, "additional_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) + + make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) \ No newline at end of file From d62af77ca8e88b83a921e892fffd583ffd21f6c6 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 17 Jul 2021 17:47:20 +0530 Subject: [PATCH 067/147] fix: Remove unnecessary condition --- .../accounts/doctype/sales_invoice/sales_invoice.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index fab0e11324..a5341df2e3 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -629,13 +629,11 @@ frappe.ui.form.on('Sales Invoice', { // discount account frm.fields_dict['items'].grid.get_field('discount_account').get_query = function(doc) { - if (erpnext.is_perpetual_inventory_enabled(doc.company)) { - return { - filters: { - 'report_type': 'Profit and Loss', - 'company': doc.company, - "is_group": 0 - } + return { + filters: { + 'report_type': 'Profit and Loss', + 'company': doc.company, + "is_group": 0 } } } From 99652876d090781916ce8af600e619ea63be0d86 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 17 Jul 2021 18:45:21 +0530 Subject: [PATCH 068/147] fix: Add test for additional discount applied on taxes --- .../sales_invoice/test_sales_invoice.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 2ddfad9c31..1e2e92f609 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2001,6 +2001,35 @@ class TestSalesInvoice(unittest.TestCase): check_gl_entries(self, si.name, expected_gle, nowdate()) + def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self): + from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import enable_discount_accounting + + enable_discount_accounting() + additional_discount_account = create_account(account_name="Discount Account", + parent_account="Indirect Expenses - _TC", company="_Test Company") + + si = create_sales_invoice(rate=75000, do_not_save=1) + si.apply_discount_on = "Grand Total" + si.additional_discount_account = additional_discount_account + si.additional_discount_percentage = 10 + si.append("taxes", { + "charge_type": "On Net Total", + "account_head": "CGST - _TC", + "cost_center": "Main - _TC", + "description": "CGST @ 9.0", + "rate": 9 + }) + si.submit() + + expected_gle = [ + ["Sales - _TC", 0.0, 67500.0, nowdate()], + ["Discount Account - _TC", 675.0, 0.0, nowdate()], + ["CGST - _TC", 0.0, 6750.0, nowdate()], + ["Debtors - _TC", 73575.0, 0.0, nowdate()] + ] + + check_gl_entries(self, si.name, expected_gle, nowdate()) + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() si.naming_series = 'INV-2020-.#####' From 99cb89f449b6013457f538cd027b72556e18ebf8 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 17 Jul 2021 19:49:16 +0530 Subject: [PATCH 069/147] fix: Switch debit and credit for ledger entries for discount applied on taxes for Purchase Invoice --- erpnext/controllers/accounts_controller.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f4593c2a76..aa2fe29bc6 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -858,14 +858,22 @@ class AccountsController(TransactionBase): if flt(tax.base_tax_amount_after_discount_amount) and flt(tax.base_tax_amount): account_currency = get_account_currency(tax.account_head) additional_discount_applied_on_taxes = flt(tax.base_tax_amount) - flt(tax.base_tax_amount_after_discount_amount) + if self.doctype == 'Purchase Invoice': + against = self.supplier + dr_or_cr = "debit" + rev_dr_cr = "credit" + else: + against = self.customer + dr_or_cr = "credit" + rev_dr_cr = "debit" gl_entries.append( self.get_gl_dict({ "account": tax.account_head, - "against": self.customer, - "credit": flt(additional_discount_applied_on_taxes, + "against": against, + dr_or_cr: flt(additional_discount_applied_on_taxes, tax.precision("tax_amount_after_discount_amount")), - "credit_in_account_currency": (flt(additional_discount_applied_on_taxes, + dr_or_cr + "_in_account_currency": (flt(additional_discount_applied_on_taxes, tax.precision("base_tax_amount_after_discount_amount")) if account_currency==self.company_currency else flt(additional_discount_applied_on_taxes, tax.precision("tax_amount_after_discount_amount"))), "cost_center": tax.cost_center @@ -875,17 +883,16 @@ class AccountsController(TransactionBase): gl_entries.append( self.get_gl_dict({ "account": self.additional_discount_account, - "against": self.customer, - "debit": flt(additional_discount_applied_on_taxes, + "against": against, + rev_dr_cr: flt(additional_discount_applied_on_taxes, tax.precision("tax_amount_after_discount_amount")), - "debit_in_account_currency": (flt(additional_discount_applied_on_taxes, + rev_dr_cr + "_in_account_currency": (flt(additional_discount_applied_on_taxes, tax.precision("base_tax_amount_after_discount_amount")) if account_currency==self.company_currency else flt(additional_discount_applied_on_taxes, tax.precision("tax_amount_after_discount_amount"))), "cost_center": tax.cost_center }, account_currency, item=tax) ) - def allocate_advance_taxes(self, gl_entries): tax_map = self.get_tax_map() for pe in self.get("advances"): From 251f2296018a996737da6eadddcb650994b3f728 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 17 Jul 2021 19:49:42 +0530 Subject: [PATCH 070/147] fix: Add test for additional discount applied on taxes --- .../purchase_invoice/test_purchase_invoice.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 36fbc7e351..4eb71f8e70 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -247,6 +247,35 @@ class TestPurchaseInvoice(unittest.TestCase): check_gl_entries(self, pi.name, expected_gle, nowdate()) + def test_additional_discount_for_purchase_invoice_with_discount_accounting_enabled(self): + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import check_gl_entries + + enable_discount_accounting() + additional_discount_account = create_account(account_name="Discount Account", + parent_account="Indirect Expenses - _TC", company="_Test Company") + + pi = make_purchase_invoice(qty=1, rate=75000, do_not_save=1) + pi.apply_discount_on = "Grand Total" + pi.additional_discount_account = additional_discount_account + pi.additional_discount_percentage = 10 + pi.append("taxes", { + "charge_type": "On Net Total", + "account_head": "CGST - _TC", + "cost_center": "Main - _TC", + "description": "CGST @ 9.0", + "rate": 9 + }) + pi.submit() + + expected_gle = [ + ["Discount Account - _TC", 0.0, 675.0, nowdate()], + ["CGST - _TC", 6750.0, 0.0, nowdate()], + ["_Test Account Cost for Goods Sold - _TC", 67500.0, 0.0, nowdate()], + ["Creditors - _TC", 0.0, 73575.0, nowdate()] + ] + + check_gl_entries(self, pi.name, expected_gle, nowdate()) + def test_purchase_invoice_change_naming_series(self): pi = frappe.copy_doc(test_records[1]) pi.insert() From b4a8bc8e4cbacb80e077b450195c1be346524387 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Mon, 19 Jul 2021 23:43:36 +0530 Subject: [PATCH 071/147] fix: Use the item's cost centre instead of the Invoice's Co-authored-by: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> --- erpnext/controllers/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index aa2fe29bc6..65dbe17ec1 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -836,7 +836,7 @@ class AccountsController(TransactionBase): "against": supplier_or_customer, dr_or_cr: flt(item.discount_amount), dr_or_cr + "_in_account_currency": flt(item.discount_amount), - "cost_center": self.cost_center, + "cost_center": item.cost_center, "project": self.project }, account_currency, item=self) ) From e7e9bda1235f2b9d35b1f8a9d3b525a026bf4dd5 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Mon, 19 Jul 2021 23:44:21 +0530 Subject: [PATCH 072/147] fix: Use the item's project instead of the invoice's Co-authored-by: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> --- erpnext/controllers/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 65dbe17ec1..2aac4968a2 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -837,7 +837,7 @@ class AccountsController(TransactionBase): dr_or_cr: flt(item.discount_amount), dr_or_cr + "_in_account_currency": flt(item.discount_amount), "cost_center": item.cost_center, - "project": self.project + "project": item.project }, account_currency, item=self) ) From f421dc7ca74f97794c8eca5bf52fcfabef178cfe Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Mon, 19 Jul 2021 23:44:55 +0530 Subject: [PATCH 073/147] fix: GL Entry creation Co-authored-by: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> --- erpnext/controllers/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 2aac4968a2..9b3336cde5 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -838,7 +838,7 @@ class AccountsController(TransactionBase): dr_or_cr + "_in_account_currency": flt(item.discount_amount), "cost_center": item.cost_center, "project": item.project - }, account_currency, item=self) + }, account_currency, item=item) ) account_currency = get_account_currency(income_or_expense_account) From 821b75f1b119b5ef9be2278889882db3b73af33c Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Mon, 19 Jul 2021 23:46:38 +0530 Subject: [PATCH 074/147] fix: Filter for additional_discount_account Co-authored-by: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> --- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index a5341df2e3..0256227886 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -596,7 +596,7 @@ frappe.ui.form.on('Sales Invoice', { filters: { company: frm.doc.company, is_group: 0, - root_type: "Profit and Loss", + report_type: "Profit and Loss", } }; }); From ed6ebdf5ed297352ac7120bb910662d8c4a41183 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Mon, 19 Jul 2021 23:47:58 +0530 Subject: [PATCH 075/147] fix: Filter for additional_discount_account Co-authored-by: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 4ed31cc227..bf78c028c8 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -504,7 +504,7 @@ frappe.ui.form.on("Purchase Invoice", { filters: { company: frm.doc.company, is_group: 0, - root_type: "Profit and Loss", + report_type: "Profit and Loss", } }; }); From 566e8f849903644cad157fb8ae9a790d304e84e4 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 20 Jul 2021 03:46:02 +0530 Subject: [PATCH 076/147] fix: Create GL Entries for Additional Discount Account --- .../purchase_invoice/purchase_invoice.py | 11 ++-- .../doctype/sales_invoice/sales_invoice.py | 31 +++++++--- erpnext/controllers/accounts_controller.py | 60 ++++++------------- 3 files changed, 45 insertions(+), 57 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 006f5bb0e8..feae213d92 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -609,7 +609,11 @@ class PurchaseInvoice(BuyingController): if (not item.enable_deferred_expense or self.is_return) else item.deferred_expense_account) if not item.is_fixed_asset: - amount = flt(item.base_net_amount, item.precision("base_net_amount")) + if frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting'): + amount = flt(item.base_amount, item.precision("base_amount")) + else: + amount = flt(item.base_net_amount, item.precision("base_net_amount")) + else: amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount")) @@ -888,11 +892,6 @@ class PurchaseInvoice(BuyingController): "remarks": self.remarks or "Accounting Entry for Stock" }, item=tax)) - enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) - - if enable_discount_accounting and self.get('discount_amount') and self.get('additional_discount_account'): - self.make_gle_for_additional_discount_applied_on_taxes(gl_entries) - def make_internal_transfer_gl_entries(self, gl_entries): if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges): account_currency = get_account_currency(self.unrealized_profit_loss_account) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index ee9b59e769..52c2a9c42c 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -902,11 +902,6 @@ class SalesInvoice(SellingController): }, account_currency, item=tax) ) - enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) - - if enable_discount_accounting and self.get('discount_amount') and self.get('additional_discount_account'): - self.make_gle_for_additional_discount_applied_on_taxes(gl_entries) - def make_internal_transfer_gl_entries(self, gl_entries): if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges): account_currency = get_account_currency(self.unrealized_profit_loss_account) @@ -946,15 +941,17 @@ class SalesInvoice(SellingController): income_account = (item.income_account if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account) + amount, base_amount = self.get_amount_and_base_amount(item) + account_currency = get_account_currency(income_account) gl_entries.append( self.get_gl_dict({ "account": income_account, "against": self.customer, - "credit": flt(item.base_net_amount, item.precision("base_net_amount")), - "credit_in_account_currency": (flt(item.base_net_amount, item.precision("base_net_amount")) + "credit": flt(base_amount, item.precision("base_net_amount")), + "credit_in_account_currency": (flt(base_amount, item.precision("base_net_amount")) if account_currency==self.company_currency - else flt(item.net_amount, item.precision("net_amount"))), + else flt(amount, item.precision("net_amount"))), "cost_center": item.cost_center, "project": item.project or self.project }, account_currency, item=item) @@ -965,6 +962,24 @@ class SalesInvoice(SellingController): erpnext.is_perpetual_inventory_enabled(self.company): gl_entries += super(SalesInvoice, self).get_gl_entries() + def get_amount_and_base_amount(self, item): + amount = item.net_amount + base_amount = item.base_net_amount + + enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) + + if enable_discount_accounting and self.get('discount_amount') and self.get('additional_discount_account'): + amount = item.amount + base_amount = item.base_amount + + return amount, base_amount + + def set_asset_status(self, asset): + if self.is_return: + asset.set_status() + else: + asset.set_status("Sold" if self.docstatus==1 else None) + def make_loyalty_point_redemption_gle(self, gl_entries): if cint(self.redeem_loyalty_points): gl_entries.append( diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 9b3336cde5..59879e0df5 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -812,19 +812,23 @@ class AccountsController(TransactionBase): enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) if enable_discount_accounting: + if self.doctype == "Purchase Invoice": + dr_or_cr = "credit" + rev_dr_cr = "debit" + supplier_or_customer = self.supplier + + else: + dr_or_cr = "debit" + rev_dr_cr = "credit" + supplier_or_customer = self.customer + for item in self.get("items"): if item.get('discount_amount') and item.get('discount_account'): if self.doctype == "Purchase Invoice": - dr_or_cr = "credit" - rev_dr_cr = "debit" - supplier_or_customer = self.supplier income_or_expense_account = (item.expense_account if (not item.enable_deferred_expense or self.is_return) else item.deferred_expense_account) else: - dr_or_cr = "debit" - rev_dr_cr = "credit" - supplier_or_customer = self.customer income_or_expense_account = (item.income_account if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account) @@ -853,46 +857,16 @@ class AccountsController(TransactionBase): }, account_currency, item=item) ) - def make_gle_for_additional_discount_applied_on_taxes(self, gl_entries): - for tax in self.get("taxes"): - if flt(tax.base_tax_amount_after_discount_amount) and flt(tax.base_tax_amount): - account_currency = get_account_currency(tax.account_head) - additional_discount_applied_on_taxes = flt(tax.base_tax_amount) - flt(tax.base_tax_amount_after_discount_amount) - if self.doctype == 'Purchase Invoice': - against = self.supplier - dr_or_cr = "debit" - rev_dr_cr = "credit" - else: - against = self.customer - dr_or_cr = "credit" - rev_dr_cr = "debit" - - gl_entries.append( - self.get_gl_dict({ - "account": tax.account_head, - "against": against, - dr_or_cr: flt(additional_discount_applied_on_taxes, - tax.precision("tax_amount_after_discount_amount")), - dr_or_cr + "_in_account_currency": (flt(additional_discount_applied_on_taxes, - tax.precision("base_tax_amount_after_discount_amount")) if account_currency==self.company_currency else - flt(additional_discount_applied_on_taxes, tax.precision("tax_amount_after_discount_amount"))), - "cost_center": tax.cost_center - }, account_currency, item=tax) - ) - + if self.get('discount_amount') and self.get('additional_discount_account'): gl_entries.append( self.get_gl_dict({ "account": self.additional_discount_account, - "against": against, - rev_dr_cr: flt(additional_discount_applied_on_taxes, - tax.precision("tax_amount_after_discount_amount")), - rev_dr_cr + "_in_account_currency": (flt(additional_discount_applied_on_taxes, - tax.precision("base_tax_amount_after_discount_amount")) if account_currency==self.company_currency else - flt(additional_discount_applied_on_taxes, tax.precision("tax_amount_after_discount_amount"))), - "cost_center": tax.cost_center - }, account_currency, item=tax) - ) - + "against": supplier_or_customer, + dr_or_cr: self.discount_amount, + "cost_center": self.cost_center + }, item=self) + ) + def allocate_advance_taxes(self, gl_entries): tax_map = self.get_tax_map() for pe in self.get("advances"): From 1f6c05f0139a8ad9f1d22c4c35552e51e767180f Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 20 Jul 2021 03:52:39 +0530 Subject: [PATCH 077/147] fix: Make discount_account mandatory if discount accounting is enabled --- erpnext/accounts/doctype/accounts_settings/accounts_settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 24b0ec4d4a..a3a32d5e97 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -40,6 +40,7 @@ class AccountsSettings(Document): for doctype in ["Sales Invoice Item", "Purchase Invoice Item"]: make_property_setter(doctype, "discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) + make_property_setter(doctype, "discount_account", "mandatory", enable_discount_accounting, "Check", validate_fields_for_doctype=False) for doctype in ["Sales Invoice", "Purchase Invoice"]: make_property_setter(doctype, "additional_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) From 45327e04db1a35f439e874e2e5175c87683af959 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 20 Jul 2021 05:16:33 +0530 Subject: [PATCH 078/147] fix: Tests --- .../purchase_invoice/test_purchase_invoice.py | 43 +++++++++++++------ .../sales_invoice/test_sales_invoice.py | 25 +++++------ 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 4eb71f8e70..0487ef6444 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -231,8 +231,6 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account][2], gle.credit) def test_purchase_invoice_with_discount_accounting_enabled(self): - from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import check_gl_entries - enable_discount_accounting() discount_account = create_account(account_name="Discount Account", @@ -240,38 +238,45 @@ class TestPurchaseInvoice(unittest.TestCase): pi = make_purchase_invoice(discount_account=discount_account, discount_amount=100) expected_gle = [ - ["Discount Account - _TC", 0.0, 100.0, nowdate()], ["_Test Account Cost for Goods Sold - _TC", 350.0, 0.0, nowdate()], - ["Creditors - _TC", 0.0, 250.0, nowdate()] + ["Creditors - _TC", 0.0, 250.0, nowdate()], + ["Discount Account - _TC", 0.0, 100.0, nowdate()] ] check_gl_entries(self, pi.name, expected_gle, nowdate()) def test_additional_discount_for_purchase_invoice_with_discount_accounting_enabled(self): - from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import check_gl_entries - enable_discount_accounting() additional_discount_account = create_account(account_name="Discount Account", parent_account="Indirect Expenses - _TC", company="_Test Company") - pi = make_purchase_invoice(qty=1, rate=75000, do_not_save=1) + pi = make_purchase_invoice(qty=1, rate=100, do_not_save=1) pi.apply_discount_on = "Grand Total" pi.additional_discount_account = additional_discount_account - pi.additional_discount_percentage = 10 + pi.additional_discount_percentage = 20 pi.append("taxes", { "charge_type": "On Net Total", "account_head": "CGST - _TC", "cost_center": "Main - _TC", "description": "CGST @ 9.0", - "rate": 9 + "base_tax_amount": 20, + "base_tax_amount_after_discount_amount": 20 }) pi.submit() + # gle = frappe.get_all( + # "GL Entry", + # fields = ['account', 'debit', 'credit', 'posting_date'], + # filters = {'voucher_no': pi.name} + # ) + # for gl in gle: + # print(gl, "\n") + expected_gle = [ - ["Discount Account - _TC", 0.0, 675.0, nowdate()], - ["CGST - _TC", 6750.0, 0.0, nowdate()], - ["_Test Account Cost for Goods Sold - _TC", 67500.0, 0.0, nowdate()], - ["Creditors - _TC", 0.0, 73575.0, nowdate()] + ["CGST - _TC", 20.0, 0.0, nowdate()], + ["Creditors - _TC", 0.0, 96.0, nowdate()], + ["Discount Account - _TC", 0.0, 24.0, nowdate()], + ["_Test Account Cost for Goods Sold - _TC", 100.0, 0.0, nowdate()] ] check_gl_entries(self, pi.name, expected_gle, nowdate()) @@ -1186,6 +1191,18 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_gle[i][0], gle.account) self.assertEqual(expected_gle[i][1], gle.amount) +def check_gl_entries(doc, voucher_no, expected_gle, posting_date): + gl_entries = frappe.db.sql("""select account, debit, credit, posting_date + from `tabGL Entry` + where voucher_type='Purchase Invoice' and voucher_no=%s and posting_date >= %s + order by posting_date asc, account asc""", (voucher_no, posting_date), as_dict=1) + + for i, gle in enumerate(gl_entries): + doc.assertEqual(expected_gle[i][0], gle.account) + doc.assertEqual(expected_gle[i][1], gle.debit) + doc.assertEqual(expected_gle[i][2], gle.credit) + doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) + def update_tax_witholding_category(company, account, date): from erpnext.accounts.utils import get_fiscal_year diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 1e2e92f609..9f7d18bdb4 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1994,12 +1994,12 @@ class TestSalesInvoice(unittest.TestCase): si = create_sales_invoice(discount_account=discount_account, discount_amount=100) expected_gle = [ + ["Debtors - _TC", 100.0, 0.0, nowdate()], ["Discount Account - _TC", 100.0, 0.0, nowdate()], - ["Sales - _TC", 0.0, 200.0, nowdate()], - ["Debtors - _TC", 100.0, 0.0, nowdate()] + ["Sales - _TC", 0.0, 200.0, nowdate()] ] - check_gl_entries(self, si.name, expected_gle, nowdate()) + check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self): from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import enable_discount_accounting @@ -2008,27 +2008,28 @@ class TestSalesInvoice(unittest.TestCase): additional_discount_account = create_account(account_name="Discount Account", parent_account="Indirect Expenses - _TC", company="_Test Company") - si = create_sales_invoice(rate=75000, do_not_save=1) + si = create_sales_invoice(rate=100, do_not_save=1) si.apply_discount_on = "Grand Total" si.additional_discount_account = additional_discount_account - si.additional_discount_percentage = 10 + si.additional_discount_percentage = 20 si.append("taxes", { - "charge_type": "On Net Total", + "charge_type": "Actual", "account_head": "CGST - _TC", "cost_center": "Main - _TC", "description": "CGST @ 9.0", - "rate": 9 + "rate": 0, + "tax_amount": 20 }) si.submit() expected_gle = [ - ["Sales - _TC", 0.0, 67500.0, nowdate()], - ["Discount Account - _TC", 675.0, 0.0, nowdate()], - ["CGST - _TC", 0.0, 6750.0, nowdate()], - ["Debtors - _TC", 73575.0, 0.0, nowdate()] + ["CGST - _TC", 0.0, 20.0, nowdate()], + ["Debtors - _TC", 96.0, 0.0, nowdate()], + ["Discount Account - _TC", 24.0, 0.0, nowdate()], + ["Sales - _TC", 0.0, 100.0, nowdate()] ] - check_gl_entries(self, si.name, expected_gle, nowdate()) + check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() From 4fa409e0194fde280647b31c96a404099cf99eb1 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 20 Jul 2021 22:03:44 +0530 Subject: [PATCH 079/147] fix: Add mandatory_depends_on property for Discount Account --- .../doctype/accounts_settings/accounts_settings.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index a3a32d5e97..5544913292 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -40,9 +40,16 @@ class AccountsSettings(Document): for doctype in ["Sales Invoice Item", "Purchase Invoice Item"]: make_property_setter(doctype, "discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, "discount_account", "mandatory", enable_discount_accounting, "Check", validate_fields_for_doctype=False) + if enable_discount_accounting: + make_property_setter(doctype, "discount_account", "mandatory_depends_on", "eval: doc.discount_amount", "Code", validate_fields_for_doctype=False) + else: + make_property_setter(doctype, "discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False) for doctype in ["Sales Invoice", "Purchase Invoice"]: make_property_setter(doctype, "additional_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) + if enable_discount_accounting: + make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "eval: doc.discount_amount", "Code", validate_fields_for_doctype=False) + else: + make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False) make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) \ No newline at end of file From fc09d845c5b38dd60783dfd287a71f7ab0dcbad9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 21 Jul 2021 15:25:09 +0530 Subject: [PATCH 080/147] fix: Syntax Error --- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 0256227886..56f11650ff 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -348,7 +348,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte items_add: function(doc, cdt, cdn) { var row = frappe.get_doc(cdt, cdn); this.frm.script_manager.copy_from_first_row("items", row, ["income_account", "discount_account", "cost_center"]); - } + }, set_dynamic_labels: function() { this._super(); From 5a06019440f80b2c3719f195cdf5619d3715b759 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 21 Jul 2021 15:25:40 +0530 Subject: [PATCH 081/147] fix: GL For taxes if discount applied on Grand Total --- .../doctype/sales_invoice/sales_invoice.py | 26 +++++++------------ erpnext/controllers/accounts_controller.py | 21 +++++++++++++++ 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 52c2a9c42c..2539de7b12 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -886,18 +886,22 @@ class SalesInvoice(SellingController): ) def make_tax_gl_entries(self, gl_entries): + enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) + for tax in self.get("taxes"): + amount, base_amount = self.get_tax_amounts(tax, enable_discount_accounting) + if flt(tax.base_tax_amount_after_discount_amount): account_currency = get_account_currency(tax.account_head) gl_entries.append( self.get_gl_dict({ "account": tax.account_head, "against": self.customer, - "credit": flt(tax.base_tax_amount_after_discount_amount, + "credit": flt(base_amount, tax.precision("tax_amount_after_discount_amount")), - "credit_in_account_currency": (flt(tax.base_tax_amount_after_discount_amount, + "credit_in_account_currency": (flt(base_amount, tax.precision("base_tax_amount_after_discount_amount")) if account_currency==self.company_currency else - flt(tax.tax_amount_after_discount_amount, tax.precision("tax_amount_after_discount_amount"))), + flt(amount, tax.precision("tax_amount_after_discount_amount"))), "cost_center": tax.cost_center }, account_currency, item=tax) ) @@ -916,6 +920,8 @@ class SalesInvoice(SellingController): def make_item_gl_entries(self, gl_entries): # income account gl entries + enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) + for item in self.get("items"): if flt(item.base_net_amount, item.precision("base_net_amount")): if item.is_fixed_asset: @@ -941,7 +947,7 @@ class SalesInvoice(SellingController): income_account = (item.income_account if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account) - amount, base_amount = self.get_amount_and_base_amount(item) + amount, base_amount = self.get_amount_and_base_amount(item, enable_discount_accounting) account_currency = get_account_currency(income_account) gl_entries.append( @@ -962,18 +968,6 @@ class SalesInvoice(SellingController): erpnext.is_perpetual_inventory_enabled(self.company): gl_entries += super(SalesInvoice, self).get_gl_entries() - def get_amount_and_base_amount(self, item): - amount = item.net_amount - base_amount = item.base_net_amount - - enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) - - if enable_discount_accounting and self.get('discount_amount') and self.get('additional_discount_account'): - amount = item.amount - base_amount = item.base_amount - - return amount, base_amount - def set_asset_status(self, asset): if self.is_return: asset.set_status() diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 59879e0df5..3d048c3686 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -808,6 +808,27 @@ class AccountsController(TransactionBase): tax_map[tax.account_head] -= allocated_amount allocated_tax_map[tax.account_head] -= allocated_amount + def get_amount_and_base_amount(self, item, enable_discount_accounting): + amount = item.net_amount + base_amount = item.base_net_amount + + if enable_discount_accounting and self.get('discount_amount') and self.get('additional_discount_account'): + amount = item.amount + base_amount = item.base_amount + + return amount, base_amount + + def get_tax_amounts(self, tax, enable_discount_accounting): + amount = tax.tax_amount_after_discount_amount + base_amount = tax.base_tax_amount_after_discount_amount + + if enable_discount_accounting and self.get('discount_amount') and self.get('additional_discount_account') \ + and self.get('apply_discount_on') == 'Grand Total': + amount = tax.tax_amount + base_amount = tax.base_tax_amount + + return amount, base_amount + def make_discount_gl_entries(self, gl_entries): enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) From c765073c2a56688835b04d72fc335f654e2ef86a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 21 Jul 2021 22:28:32 +0530 Subject: [PATCH 082/147] fix: Tests --- .../purchase_invoice/test_purchase_invoice.py | 20 +++++++++---------- .../sales_invoice/test_sales_invoice.py | 9 +++++---- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 0487ef6444..e20e385f63 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -250,17 +250,16 @@ class TestPurchaseInvoice(unittest.TestCase): additional_discount_account = create_account(account_name="Discount Account", parent_account="Indirect Expenses - _TC", company="_Test Company") - pi = make_purchase_invoice(qty=1, rate=100, do_not_save=1) + pi = make_purchase_invoice(qty=1, rate=100, do_not_save=1, parent_cost_center="Main - _TC") pi.apply_discount_on = "Grand Total" pi.additional_discount_account = additional_discount_account pi.additional_discount_percentage = 20 pi.append("taxes", { - "charge_type": "On Net Total", - "account_head": "CGST - _TC", + "charge_type": "Actual", + "account_head": "_Test Account VAT - _TC", "cost_center": "Main - _TC", - "description": "CGST @ 9.0", - "base_tax_amount": 20, - "base_tax_amount_after_discount_amount": 20 + "description": "Test", + "tax_amount": 20 }) pi.submit() @@ -273,10 +272,10 @@ class TestPurchaseInvoice(unittest.TestCase): # print(gl, "\n") expected_gle = [ - ["CGST - _TC", 20.0, 0.0, nowdate()], + ["_Test Account Cost for Goods Sold - _TC", 100.0, 0.0, nowdate()], + ["_Test Account VAT - _TC", 20.0, 0.0, nowdate()], ["Creditors - _TC", 0.0, 96.0, nowdate()], - ["Discount Account - _TC", 0.0, 24.0, nowdate()], - ["_Test Account Cost for Goods Sold - _TC", 100.0, 0.0, nowdate()] + ["Discount Account - _TC", 0.0, 24.0, nowdate()] ] check_gl_entries(self, pi.name, expected_gle, nowdate()) @@ -1197,6 +1196,7 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): where voucher_type='Purchase Invoice' and voucher_no=%s and posting_date >= %s order by posting_date asc, account asc""", (voucher_no, posting_date), as_dict=1) + print(gl_entries) for i, gle in enumerate(gl_entries): doc.assertEqual(expected_gle[i][0], gle.account) doc.assertEqual(expected_gle[i][1], gle.debit) @@ -1260,7 +1260,7 @@ def make_purchase_invoice(**args): pi.return_against = args.return_against pi.is_subcontracted = args.is_subcontracted or "No" pi.supplier_warehouse = args.supplier_warehouse or "_Test Warehouse 1 - _TC" - pi.cost_center = args.cost_center or "_Test Cost Center - _TC" + pi.cost_center = args.parent_cost_center pi.append("items", { "item_code": args.item or args.item_code or "_Test Item", diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 9f7d18bdb4..1c0177d3d0 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2014,16 +2014,17 @@ class TestSalesInvoice(unittest.TestCase): si.additional_discount_percentage = 20 si.append("taxes", { "charge_type": "Actual", - "account_head": "CGST - _TC", + "account_head": "_Test Account VAT - _TC", "cost_center": "Main - _TC", - "description": "CGST @ 9.0", + "parent_cost_center": "Main - _TC", + "description": "Test", "rate": 0, "tax_amount": 20 }) si.submit() expected_gle = [ - ["CGST - _TC", 0.0, 20.0, nowdate()], + ["_Test Account VAT - _TC", 0.0, 20.0, nowdate()], ["Debtors - _TC", 96.0, 0.0, nowdate()], ["Discount Account - _TC", 24.0, 0.0, nowdate()], ["Sales - _TC", 0.0, 100.0, nowdate()] @@ -2199,7 +2200,7 @@ def create_sales_invoice(**args): si.currency=args.currency or "INR" si.conversion_rate = args.conversion_rate or 1 si.naming_series = args.naming_series or "T-SINV-" - si.cost_center = args.cost_center or "_Test Cost Center - _TC" + si.cost_center = args.parent_cost_center si.append("items", { "item_code": args.item or args.item_code or "_Test Item", From 117676175728e3f34edbfea9b989da26ba9a6e84 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 21 Jul 2021 23:19:47 +0530 Subject: [PATCH 083/147] feat: Expand All nodes option in Desktop view --- .../hierarchy_chart_desktop.js | 152 +++++++++++++++--- erpnext/public/scss/hierarchy_chart.scss | 1 + erpnext/utilities/hierarchy_chart.py | 29 ++++ 3 files changed, 159 insertions(+), 23 deletions(-) create mode 100644 erpnext/utilities/hierarchy_chart.py diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js index fe4d17c210..694c26567a 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js @@ -36,7 +36,11 @@ erpnext.HierarchyChart = class { me.nodes[this.id] = this; me.make_node_element(this); - me.setup_node_click_action(this); + + if (!me.all_nodes_expanded) { + me.setup_node_click_action(this); + } + me.setup_edit_node_action(this); } }; @@ -60,8 +64,9 @@ erpnext.HierarchyChart = class { show() { frappe.breadcrumbs.add('HR'); - let me = this; + this.setup_actions(); if ($(`[data-fieldname="company"]`).length) return; + let me = this; let company = this.page.add_field({ fieldtype: 'Link', @@ -79,20 +84,9 @@ erpnext.HierarchyChart = class { // svg for connectors me.make_svg_markers(); - - if (me.$hierarchy) - me.$hierarchy.remove(); - - // setup hierarchy - me.$hierarchy = $( - `
                      -
                    • -
                        -
                      • -
                      `); - - me.page.main.append(me.$hierarchy); + me.setup_hierarchy() me.render_root_nodes(); + me.all_nodes_expanded = false; } } }); @@ -101,6 +95,42 @@ erpnext.HierarchyChart = class { $(`[data-fieldname="company"]`).trigger('change'); } + setup_actions() { + let me = this; + this.page.add_inner_button(__('Expand All'), function() { + me.load_children(me.root_node, true); + me.all_nodes_expanded = true; + + me.page.remove_inner_button(__('Expand All')); + me.page.add_inner_button(__('Collapse All'), function() { + me.setup_hierarchy(); + me.render_root_nodes(); + me.all_nodes_expanded = false; + + me.page.remove_inner_button(__('Collapse All')); + me.setup_actions(); + }); + }); + } + + setup_hierarchy() { + if (this.$hierarchy) + this.$hierarchy.remove(); + + $(`#connectors`).empty(); + + // setup hierarchy + this.$hierarchy = $( + `
                        +
                      • +
                          +
                        • +
                        `); + + this.page.main.append(this.$hierarchy); + this.nodes = {}; + } + make_svg_markers() { $('#arrows').remove(); @@ -126,7 +156,7 @@ erpnext.HierarchyChart = class { `); } - render_root_nodes() { + render_root_nodes(expanded_view=false) { let me = this; frappe.call({ @@ -156,7 +186,10 @@ erpnext.HierarchyChart = class { expand_node = node; }); - me.expand_node(expand_node); + if (!expanded_view) { + me.root_node = expand_node; + me.expand_node(expand_node); + } } }); } @@ -196,11 +229,20 @@ erpnext.HierarchyChart = class { $(`#${node.parent_id}`).addClass('active-path'); } - load_children(node) { - frappe.run_serially([ - () => this.get_child_nodes(node.id), - (child_nodes) => this.render_child_nodes(node, child_nodes) - ]); + load_children(node, deep=false) { + if (!deep) { + frappe.run_serially([ + () => this.get_child_nodes(node.id), + (child_nodes) => this.render_child_nodes(node, child_nodes) + ]); + } else { + frappe.run_serially([ + () => this.setup_hierarchy(), + () => this.render_root_nodes(true), + () => this.get_all_nodes(node.id, node.name), + (data_list) => this.render_children_of_all_nodes(data_list) + ]); + } } get_child_nodes(node_id) { @@ -247,6 +289,70 @@ erpnext.HierarchyChart = class { node.expanded = true; } + get_all_nodes(node_id, node_name) { + return new Promise(resolve => { + frappe.call({ + method: 'erpnext.utilities.hierarchy_chart.get_all_nodes', + args: { + method: this.method, + company: this.company, + parent: node_id, + parent_name: node_name + }, + callback: (r) => { + resolve(r.message); + } + }); + }); + } + + render_children_of_all_nodes(data_list) { + let entry = undefined; + let node = undefined; + + while(data_list.length) { + // to avoid overlapping connectors + entry = data_list.shift(); + node = this.nodes[entry.parent]; + if (node) { + this.render_child_nodes_for_expanded_view(node, entry.data); + } else { + data_list.push(entry); + } + } + } + + render_child_nodes_for_expanded_view(node, child_nodes) { + node.$children = $('
                          ') + + const last_level = this.$hierarchy.find('.level:last').index(); + const node_level = $(`#${node.id}`).parent().parent().parent().index(); + + if (last_level === node_level) { + this.$hierarchy.append(` +
                        • + `); + node.$children.appendTo(this.$hierarchy.find('.level:last')); + } else { + node.$children.appendTo(this.$hierarchy.find('.level:eq(' + (node_level + 1) + ')')); + } + + node.$children.hide().empty(); + + if (child_nodes) { + $.each(child_nodes, (_i, data) => { + this.add_node(node, data); + setTimeout(() => { + this.add_connector(node.id, data.id); + }, 250); + }); + } + + node.$children.show(); + $(`path[data-parent="${node.id}"]`).show(); + node.expanded = true; + } + add_node(node, data) { return new this.Node({ id: data.id, @@ -333,7 +439,7 @@ erpnext.HierarchyChart = class { path.setAttribute("class", "active-connector"); path.setAttribute("marker-start", "url(#arrowstart-active)"); path.setAttribute("marker-end", "url(#arrowhead-active)"); - } else if (parent.hasClass('active-path')) { + } else { path.setAttribute("class", "collapsed-connector"); path.setAttribute("marker-start", "url(#arrowstart-collapsed)"); path.setAttribute("marker-end", "url(#arrowhead-collapsed)"); diff --git a/erpnext/public/scss/hierarchy_chart.scss b/erpnext/public/scss/hierarchy_chart.scss index dd523c3443..1c2f9421fa 100644 --- a/erpnext/public/scss/hierarchy_chart.scss +++ b/erpnext/public/scss/hierarchy_chart.scss @@ -194,6 +194,7 @@ .level { margin-right: 8px; align-items: flex-start; + flex-direction: column; } #arrows { diff --git a/erpnext/utilities/hierarchy_chart.py b/erpnext/utilities/hierarchy_chart.py new file mode 100644 index 0000000000..9b0279351f --- /dev/null +++ b/erpnext/utilities/hierarchy_chart.py @@ -0,0 +1,29 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ + +@frappe.whitelist() +def get_all_nodes(parent, parent_name, method, company): + '''Recursively gets all data from nodes''' + method = frappe.get_attr(method) + + if not method in frappe.whitelisted: + frappe.throw(_('Not Permitted'), frappe.PermissionError) + + data = method(parent, company) + result = [dict(parent=parent, parent_name=parent_name, data=data)] + + nodes_to_expand = [{'id': d.get('id'), 'name': d.get('name')} for d in data if d.get('expandable')] + + while nodes_to_expand: + parent = nodes_to_expand.pop(0) + data = method(parent.get('id'), company) + result.append(dict(parent=parent.get('id'), parent_name=parent.get('name'), data=data)) + for d in data: + if d.get('expandable'): + nodes_to_expand.append({'id': d.get('id'), 'name': d.get('name')}) + + return result \ No newline at end of file From 2ff0d3e0ebf5424ad8a50f65ad64b531a13f4b40 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 22 Jul 2021 10:43:16 +0530 Subject: [PATCH 084/147] fix: Test Cases --- .../doctype/purchase_invoice/test_purchase_invoice.py | 9 --------- .../accounts/doctype/sales_invoice/test_sales_invoice.py | 3 +-- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index e20e385f63..9d2acdc977 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -263,14 +263,6 @@ class TestPurchaseInvoice(unittest.TestCase): }) pi.submit() - # gle = frappe.get_all( - # "GL Entry", - # fields = ['account', 'debit', 'credit', 'posting_date'], - # filters = {'voucher_no': pi.name} - # ) - # for gl in gle: - # print(gl, "\n") - expected_gle = [ ["_Test Account Cost for Goods Sold - _TC", 100.0, 0.0, nowdate()], ["_Test Account VAT - _TC", 20.0, 0.0, nowdate()], @@ -1196,7 +1188,6 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): where voucher_type='Purchase Invoice' and voucher_no=%s and posting_date >= %s order by posting_date asc, account asc""", (voucher_no, posting_date), as_dict=1) - print(gl_entries) for i, gle in enumerate(gl_entries): doc.assertEqual(expected_gle[i][0], gle.account) doc.assertEqual(expected_gle[i][1], gle.debit) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 1c0177d3d0..a7fbbdd5ca 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2008,7 +2008,7 @@ class TestSalesInvoice(unittest.TestCase): additional_discount_account = create_account(account_name="Discount Account", parent_account="Indirect Expenses - _TC", company="_Test Company") - si = create_sales_invoice(rate=100, do_not_save=1) + si = create_sales_invoice(rate=100, parent_cost_center='Main - _TC', do_not_save=1) si.apply_discount_on = "Grand Total" si.additional_discount_account = additional_discount_account si.additional_discount_percentage = 20 @@ -2016,7 +2016,6 @@ class TestSalesInvoice(unittest.TestCase): "charge_type": "Actual", "account_head": "_Test Account VAT - _TC", "cost_center": "Main - _TC", - "parent_cost_center": "Main - _TC", "description": "Test", "rate": 0, "tax_amount": 20 From 57cb3ac023544c618b2c7c9fa5927a81cd31a848 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 25 Jul 2021 20:23:20 +0530 Subject: [PATCH 085/147] feat: add html2canvas for easily exporting html to images using canvas --- .eslintrc | 1 + package.json | 3 ++- yarn.lock | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index ecfaab23ee..46fb354c11 100644 --- a/.eslintrc +++ b/.eslintrc @@ -154,6 +154,7 @@ "before": true, "beforeEach": true, "onScan": true, + "html2canvas": true, "extend_cscript": true, "localforage": true } diff --git a/package.json b/package.json index c9ee7a622c..5bc1e56a21 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "snyk": "^1.518.0" }, "dependencies": { - "onscan.js": "^1.5.2" + "onscan.js": "^1.5.2", + "html2canvas": "^1.1.4" }, "scripts": { "snyk-protect": "snyk protect", diff --git a/yarn.lock b/yarn.lock index 0a2ac1affc..cc01d89344 100644 --- a/yarn.lock +++ b/yarn.lock @@ -688,6 +688,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base64-arraybuffer@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz#4b944fac0191aa5907afe2d8c999ccc57ce80f45" + integrity sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ== + base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -997,6 +1002,13 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== +css-line-break@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-1.1.1.tgz#d5e9bdd297840099eb0503c7310fd34927a026ef" + integrity sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA== + dependencies: + base64-arraybuffer "^0.2.0" + debug@^3.1.0, debug@^3.2.6: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" @@ -1472,6 +1484,13 @@ hosted-git-info@^3.0.4, hosted-git-info@^3.0.7: dependencies: lru-cache "^6.0.0" +html2canvas@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.1.4.tgz#53ae91cd26e9e9e623c56533cccb2e3f57c8124c" + integrity sha512-uHgQDwrXsRmFdnlOVFvHin9R7mdjjZvoBoXxicPR+NnucngkaLa5zIDW9fzMkiip0jSffyTyWedE8iVogYOeWg== + dependencies: + css-line-break "1.1.1" + http-cache-semantics@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" From 37198159aaaecb86b5bbcb4528b935922eb11f3c Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 25 Jul 2021 20:28:01 +0530 Subject: [PATCH 086/147] feat: Export chart option in desktop view --- .../hierarchy_chart_desktop.js | 97 +++++++++++++------ erpnext/public/scss/hierarchy_chart.scss | 10 +- erpnext/utilities/hierarchy_chart.py | 4 +- 3 files changed, 82 insertions(+), 29 deletions(-) diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js index 694c26567a..57d34d8225 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js @@ -1,3 +1,4 @@ +import html2canvas from 'html2canvas'; erpnext.HierarchyChart = class { /* Options: - doctype @@ -11,16 +12,20 @@ erpnext.HierarchyChart = class { this.method = method; this.doctype = doctype; + this.setup_page_style(); + this.page.main.addClass('frappe-card'); + + this.nodes = {}; + this.setup_node_class(); + } + + setup_page_style() { this.page.main.css({ 'min-height': '300px', 'max-height': '600px', 'overflow': 'auto', 'position': 'relative' }); - this.page.main.addClass('frappe-card'); - - this.nodes = {}; - this.setup_node_class(); } setup_node_class() { @@ -84,7 +89,7 @@ erpnext.HierarchyChart = class { // svg for connectors me.make_svg_markers(); - me.setup_hierarchy() + me.setup_hierarchy(); me.render_root_nodes(); me.all_nodes_expanded = false; } @@ -97,6 +102,10 @@ erpnext.HierarchyChart = class { setup_actions() { let me = this; + this.page.add_inner_button(__('Export'), function() { + me.export_chart(); + }); + this.page.add_inner_button(__('Expand All'), function() { me.load_children(me.root_node, true); me.all_nodes_expanded = true; @@ -113,6 +122,36 @@ erpnext.HierarchyChart = class { }); } + export_chart() { + this.page.main.css({ + 'min-height': '', + 'max-height': '', + 'overflow': 'visible', + 'position': 'fixed', + 'left': '0', + 'top': '0' + }); + + $('.node-card').addClass('exported'); + + html2canvas(document.querySelector('#hierarchy-chart-wrapper'), { + scrollY: -window.scrollY, + scrollX: 0 + }).then(function(canvas) { + // Export the canvas to its data URI representation + let dataURL = canvas.toDataURL('image/png'); + + // download the image + let a = document.createElement('a'); + a.href = dataURL; + a.download = 'hierarchy_chart'; + a.click(); + }); + + this.setup_page_style(); + $('.node-card').removeClass('exported'); + } + setup_hierarchy() { if (this.$hierarchy) this.$hierarchy.remove(); @@ -127,33 +166,37 @@ erpnext.HierarchyChart = class {
                        `); - this.page.main.append(this.$hierarchy); + this.page.main + .find('#hierarchy-chart-wrapper') + .append(this.$hierarchy); this.nodes = {}; } make_svg_markers() { $('#arrows').remove(); - this.page.main.prepend(` - - - - - - - - + this.page.main.append(` +
                        + + + + + + + + - - - - - - - - - - `); + + + + + + + + + + +
                        `); } render_root_nodes(expanded_view=false) { @@ -310,7 +353,7 @@ erpnext.HierarchyChart = class { let entry = undefined; let node = undefined; - while(data_list.length) { + while (data_list.length) { // to avoid overlapping connectors entry = data_list.shift(); node = this.nodes[entry.parent]; @@ -323,7 +366,7 @@ erpnext.HierarchyChart = class { } render_child_nodes_for_expanded_view(node, child_nodes) { - node.$children = $('
                          ') + node.$children = $('
                            '); const last_level = this.$hierarchy.find('.level:last').index(); const node_level = $(`#${node.id}`).parent().parent().parent().index(); diff --git a/erpnext/public/scss/hierarchy_chart.scss b/erpnext/public/scss/hierarchy_chart.scss index 1c2f9421fa..44288fe155 100644 --- a/erpnext/public/scss/hierarchy_chart.scss +++ b/erpnext/public/scss/hierarchy_chart.scss @@ -21,6 +21,10 @@ } } +.node-card.exported { + box-shadow: none +} + .node-image { width: 3.0rem; height: 3.0rem; @@ -178,9 +182,12 @@ } // horizontal hierarchy tree view +#hierarchy-chart-wrapper { + padding-top: 30px; +} + .hierarchy { display: flex; - padding-top: 30px; } .hierarchy li { @@ -200,6 +207,7 @@ #arrows { position: absolute; overflow: visible; + margin-top: -80px; } .active-connector { diff --git a/erpnext/utilities/hierarchy_chart.py b/erpnext/utilities/hierarchy_chart.py index 9b0279351f..22d3f28faa 100644 --- a/erpnext/utilities/hierarchy_chart.py +++ b/erpnext/utilities/hierarchy_chart.py @@ -3,14 +3,16 @@ from __future__ import unicode_literals import frappe +import os from frappe import _ +from frappe.utils.pdf import get_pdf @frappe.whitelist() def get_all_nodes(parent, parent_name, method, company): '''Recursively gets all data from nodes''' method = frappe.get_attr(method) - if not method in frappe.whitelisted: + if method not in frappe.whitelisted: frappe.throw(_('Not Permitted'), frappe.PermissionError) data = method(parent, company) From 475d856d6681bebd2586754b0c081735952e841e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 25 Jul 2021 20:39:51 +0530 Subject: [PATCH 087/147] fix(style): longer titles overflowing --- erpnext/public/scss/hierarchy_chart.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/public/scss/hierarchy_chart.scss b/erpnext/public/scss/hierarchy_chart.scss index 44288fe155..7f1077dbbd 100644 --- a/erpnext/public/scss/hierarchy_chart.scss +++ b/erpnext/public/scss/hierarchy_chart.scss @@ -40,6 +40,10 @@ line-height: 1.35; } +.node-info { + width: 12.7rem; +} + .node-connections { font-size: 0.75rem; line-height: 1.35; From 6bca87ddb9c39cd18abb30bc1b9f4cb5f07b451c Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 25 Jul 2021 21:34:51 +0530 Subject: [PATCH 088/147] fix: remove unnecessary imports --- erpnext/utilities/hierarchy_chart.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/utilities/hierarchy_chart.py b/erpnext/utilities/hierarchy_chart.py index 22d3f28faa..fb58a5d586 100644 --- a/erpnext/utilities/hierarchy_chart.py +++ b/erpnext/utilities/hierarchy_chart.py @@ -3,9 +3,7 @@ from __future__ import unicode_literals import frappe -import os from frappe import _ -from frappe.utils.pdf import get_pdf @frappe.whitelist() def get_all_nodes(parent, parent_name, method, company): From c676eaae57d353ff9ecd34fe28158e8216e94c9f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 25 Jul 2021 23:11:18 +0530 Subject: [PATCH 089/147] fix: test --- erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js index 57d34d8225..89fb8d5792 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js @@ -173,7 +173,7 @@ erpnext.HierarchyChart = class { } make_svg_markers() { - $('#arrows').remove(); + $('#hierarchy-chart-wrapper').remove(); this.page.main.append(`
                            From 1c9e516092e415ae27441b26e0a632427cd81f31 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 28 Jul 2021 11:38:44 +0530 Subject: [PATCH 090/147] fix: GL Entries for discount amount with item qty greater than 1 --- erpnext/controllers/accounts_controller.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 3d048c3686..8199b1040f 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -845,6 +845,7 @@ class AccountsController(TransactionBase): for item in self.get("items"): if item.get('discount_amount') and item.get('discount_account'): + discount_amount = item.discount_amount * item.qty if self.doctype == "Purchase Invoice": income_or_expense_account = (item.expense_account if (not item.enable_deferred_expense or self.is_return) @@ -859,8 +860,9 @@ class AccountsController(TransactionBase): self.get_gl_dict({ "account": item.discount_account, "against": supplier_or_customer, - dr_or_cr: flt(item.discount_amount), - dr_or_cr + "_in_account_currency": flt(item.discount_amount), + dr_or_cr: flt(discount_amount, item.precision('discount_amount')), + dr_or_cr + "_in_account_currency": flt(discount_amount * self.get('conversion_rate'), + item.precision('discount_amount')), "cost_center": item.cost_center, "project": item.project }, account_currency, item=item) @@ -871,8 +873,9 @@ class AccountsController(TransactionBase): self.get_gl_dict({ "account": income_or_expense_account, "against": supplier_or_customer, - rev_dr_cr: flt(item.discount_amount), - rev_dr_cr + "_in_account_currency": flt(item.discount_amount), + rev_dr_cr: flt(discount_amount, item.precision('discount_amount')), + rev_dr_cr + "_in_account_currency": flt(discount_amount * self.get('conversion_rate'), + item.precision('discount_amount')), "cost_center": item.cost_center, "project": item.project or self.project }, account_currency, item=item) From 1011c1b01acf5f606de2f9936bf9ea7411f1f53e Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 9 Jul 2021 01:44:34 +0530 Subject: [PATCH 091/147] fix: Clear Payment Schedule if PI has default Payment Schedule, but linked PO doensn't --- .../purchase_invoice/purchase_invoice.py | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index f7992797ed..147785a080 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1142,6 +1142,79 @@ class PurchaseInvoice(BuyingController): if update: self.db_set('status', self.status, update_modified = update_modified) +@frappe.whitelist() +def set_payment_terms_from_po(doc): + if isinstance(doc, six.string_types): + doc = json.loads(doc) + + purchase_order = doc.get('items')[0].get('purchase_order') + + if purchase_order and all_items_have_same_po(doc, purchase_order): + purchase_order = frappe.get_cached_doc('Purchase Order', purchase_order) + else: + return + + if has_default_payment_terms(doc) and not has_default_payment_terms(purchase_order): + doc['payment_schedule'] = [] + doc['payment_terms_template'] = purchase_order.payment_terms_template + + for schedule in purchase_order.payment_schedule: + payment_schedule = { + 'payment_term': schedule.payment_term, + 'due_date': schedule.due_date, + 'invoice_portion': schedule.invoice_portion, + 'discount_type': schedule.discount_type, + 'discount': schedule.discount, + 'base_payment_amount': schedule.base_payment_amount, + 'payment_amount': schedule.payment_amount, + 'outstanding': schedule.outstanding + } + doc['payment_schedule'].append(payment_schedule) + + return doc + +def all_items_have_same_po(doc, purchase_order): + for item in doc.get('items'): + if item.get('purchase_order') != purchase_order: + return False + + return True + +def has_default_payment_terms(doc): + if doc.get('payment_schedule')[0].get('invoice_portion') == 100: + return True + return False + +# to get details of purchase invoice/receipt from which this doc was created for exchange rate difference handling +def get_purchase_document_details(doc): + if doc.doctype == 'Purchase Invoice': + doc_reference = 'purchase_receipt' + items_reference = 'pr_detail' + parent_doctype = 'Purchase Receipt' + child_doctype = 'Purchase Receipt Item' + else: + doc_reference = 'purchase_invoice' + items_reference = 'purchase_invoice_item' + parent_doctype = 'Purchase Invoice' + child_doctype = 'Purchase Invoice Item' + + purchase_receipts_or_invoices = [] + items = [] + + for item in doc.get('items'): + if item.get(doc_reference): + purchase_receipts_or_invoices.append(item.get(doc_reference)) + if item.get(items_reference): + items.append(item.get(items_reference)) + + exchange_rate_map = frappe._dict(frappe.get_all(parent_doctype, filters={'name': ('in', + purchase_receipts_or_invoices)}, fields=['name', 'conversion_rate'], as_list=1)) + + net_rate_map = frappe._dict(frappe.get_all(child_doctype, filters={'name': ('in', + items)}, fields=['name', 'net_rate'], as_list=1)) + + return exchange_rate_map, net_rate_map + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context list_context = get_list_context(context) From b389c9e3759d8e62b2c7a7721c4d6d8a24c8fb77 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 17 Jul 2021 22:53:21 +0530 Subject: [PATCH 092/147] fix: Fetch Payment Terms from Sales/Purchase Orders --- .../purchase_invoice/purchase_invoice.py | 43 ------------------- .../doctype/purchase_order/purchase_order.py | 12 +++--- erpnext/controllers/accounts_controller.py | 43 +++++++++++++++++++ .../doctype/sales_order/sales_order.py | 6 +++ .../doctype/delivery_note/delivery_note.py | 6 +++ .../purchase_receipt/purchase_receipt.py | 5 +++ 6 files changed, 66 insertions(+), 49 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 147785a080..28a9bddc42 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1142,49 +1142,6 @@ class PurchaseInvoice(BuyingController): if update: self.db_set('status', self.status, update_modified = update_modified) -@frappe.whitelist() -def set_payment_terms_from_po(doc): - if isinstance(doc, six.string_types): - doc = json.loads(doc) - - purchase_order = doc.get('items')[0].get('purchase_order') - - if purchase_order and all_items_have_same_po(doc, purchase_order): - purchase_order = frappe.get_cached_doc('Purchase Order', purchase_order) - else: - return - - if has_default_payment_terms(doc) and not has_default_payment_terms(purchase_order): - doc['payment_schedule'] = [] - doc['payment_terms_template'] = purchase_order.payment_terms_template - - for schedule in purchase_order.payment_schedule: - payment_schedule = { - 'payment_term': schedule.payment_term, - 'due_date': schedule.due_date, - 'invoice_portion': schedule.invoice_portion, - 'discount_type': schedule.discount_type, - 'discount': schedule.discount, - 'base_payment_amount': schedule.base_payment_amount, - 'payment_amount': schedule.payment_amount, - 'outstanding': schedule.outstanding - } - doc['payment_schedule'].append(payment_schedule) - - return doc - -def all_items_have_same_po(doc, purchase_order): - for item in doc.get('items'): - if item.get('purchase_order') != purchase_order: - return False - - return True - -def has_default_payment_terms(doc): - if doc.get('payment_schedule')[0].get('invoice_portion') == 100: - return True - return False - # to get details of purchase invoice/receipt from which this doc was created for exchange rate difference handling def get_purchase_document_details(doc): if doc.doctype == 'Purchase Invoice': diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index eaa502ff7f..a0bac51046 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -443,6 +443,8 @@ def make_purchase_invoice_from_portal(purchase_order_name): frappe.response.location = '/purchase-invoices/' + doc.name def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions=False): + from erpnext.controllers.accounts_controller import fetch_payment_terms_from_order + def postprocess(source, target): target.flags.ignore_permissions = ignore_permissions set_missing_values(source, target) @@ -489,15 +491,13 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions }, } - if frappe.get_single("Accounts Settings").automatically_fetch_payment_terms == 1: - fields["Payment Schedule"] = { - "doctype": "Payment Schedule", - "add_if_empty": True - } - doc = get_mapped_doc("Purchase Order", source_name, fields, target_doc, postprocess, ignore_permissions=ignore_permissions) + automatically_fetch_payment_terms = cint(frappe.db.get_single_value('Accounts Settings', 'automatically_fetch_payment_terms')) + if automatically_fetch_payment_terms: + fetch_payment_terms_from_order(doc) + return doc @frappe.whitelist() diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index cdd865ac4a..f7ea77bae4 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1803,3 +1803,46 @@ def validate_regional(doc): @erpnext.allow_regional def validate_einvoice_fields(doc): pass + +def fetch_payment_terms_from_order(doc): + """ + Fetch Payment Terms from Purchase/Sales Order on creating a new Purchase/Sales Invoice. + """ + + if doc.doctype == "Sales Invoice": + po_or_so = doc.get('items')[0].get('sales_order') + po_or_so_doctype = "Sales Order" + po_or_so_doctype_name = "sales_order" + else: + po_or_so = doc.get('items')[0].get('purchase_order') + po_or_so_doctype = "Purchase Order" + po_or_so_doctype_name = "purchase_order" + + if po_or_so and all_items_have_same_po_or_so(doc, po_or_so, po_or_so_doctype_name): + po_or_so = frappe.get_cached_doc(po_or_so_doctype, po_or_so) + else: + doc.set_payment_schedule() + return + + doc.payment_schedule = [] + doc.payment_terms_template = po_or_so.payment_terms_template + + for schedule in po_or_so.payment_schedule: + payment_schedule = { + 'payment_term': schedule.payment_term, + 'due_date': schedule.due_date, + 'invoice_portion': schedule.invoice_portion, + 'discount_type': schedule.discount_type, + 'discount': schedule.discount, + 'base_payment_amount': schedule.base_payment_amount, + 'payment_amount': schedule.payment_amount, + 'outstanding': schedule.outstanding + } + doc.append("payment_schedule", payment_schedule) + +def all_items_have_same_po_or_so(doc, po_or_so, po_or_so_fieldname): + for item in doc.get('items'): + if item.get(po_or_so_fieldname) != po_or_so: + return False + + return True \ No newline at end of file diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 41f57a34d3..a58c381df3 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -621,6 +621,8 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): + from erpnext.controllers.accounts_controller import fetch_payment_terms_from_order + def postprocess(source, target): set_missing_values(source, target) #Get the advance paid Journal Entries in Sales Invoice Advance @@ -693,6 +695,10 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): } }, target_doc, postprocess, ignore_permissions=ignore_permissions) + automatically_fetch_payment_terms = cint(frappe.db.get_single_value('Accounts Settings', 'automatically_fetch_payment_terms')) + if automatically_fetch_payment_terms: + fetch_payment_terms_from_order(doclist) + return doclist @frappe.whitelist() diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 4808e948fc..1628f93019 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -414,6 +414,8 @@ def get_returned_qty_map(delivery_note): @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None): + from erpnext.controllers.accounts_controller import fetch_payment_terms_from_order + doc = frappe.get_doc('Delivery Note', source_name) to_make_invoice_qty_map = {} @@ -503,6 +505,10 @@ def make_sales_invoice(source_name, target_doc=None): } }, target_doc, set_missing_values) + automatically_fetch_payment_terms = cint(frappe.db.get_single_value('Accounts Settings', 'automatically_fetch_payment_terms')) + if automatically_fetch_payment_terms: + fetch_payment_terms_from_order(doc) + return doc @frappe.whitelist() diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 82c87a83a5..cf6cac273f 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -583,6 +583,7 @@ def update_billing_percentage(pr_doc, update_modified=True): @frappe.whitelist() def make_purchase_invoice(source_name, target_doc=None): from erpnext.accounts.party import get_payment_terms_template + from erpnext.controllers.accounts_controller import fetch_payment_terms_from_order doc = frappe.get_doc('Purchase Receipt', source_name) returned_qty_map = get_returned_qty_map(source_name) @@ -654,6 +655,10 @@ def make_purchase_invoice(source_name, target_doc=None): } }, target_doc, set_missing_values) + automatically_fetch_payment_terms = cint(frappe.db.get_single_value('Accounts Settings', 'automatically_fetch_payment_terms')) + if automatically_fetch_payment_terms: + fetch_payment_terms_from_order(doclist) + return doclist def get_invoiced_qty_map(purchase_receipt): From 6333c3bee5b4aaa95bc289519e8341361af19ff6 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 17 Jul 2021 22:55:24 +0530 Subject: [PATCH 093/147] fix: Remove unused imports From def7cc6cb3584ce32e8351e8b7faeb49a955a052 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 22 Jul 2021 05:57:42 +0530 Subject: [PATCH 094/147] fix: Modify set_payment_schedule() to include fetch_payment_terms_from_order() --- .../doctype/purchase_order/purchase_order.py | 4 +- erpnext/controllers/accounts_controller.py | 111 +++++++++++------- .../doctype/sales_order/sales_order.py | 4 +- .../doctype/delivery_note/delivery_note.py | 4 +- .../purchase_receipt/purchase_receipt.py | 3 +- 5 files changed, 70 insertions(+), 56 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index a0bac51046..f68d81909a 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -443,8 +443,6 @@ def make_purchase_invoice_from_portal(purchase_order_name): frappe.response.location = '/purchase-invoices/' + doc.name def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions=False): - from erpnext.controllers.accounts_controller import fetch_payment_terms_from_order - def postprocess(source, target): target.flags.ignore_permissions = ignore_permissions set_missing_values(source, target) @@ -496,7 +494,7 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions automatically_fetch_payment_terms = cint(frappe.db.get_single_value('Accounts Settings', 'automatically_fetch_payment_terms')) if automatically_fetch_payment_terms: - fetch_payment_terms_from_order(doc) + doc.set_payment_schedule() return doc diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f7ea77bae4..2bf5c77e61 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1105,7 +1105,14 @@ class AccountsController(TransactionBase): data = get_payment_terms(self.payment_terms_template, posting_date, grand_total, base_grand_total) for item in data: self.append("payment_schedule", item) - else: + + elif self.doctype in ["Sales Invoice", "Purchase Invoice"]: + po_or_so, doctype, fieldname = self.get_order_details() + + if self.linked_order_has_payment_terms(po_or_so, fieldname): + self.fetch_payment_terms_from_order(po_or_so, doctype) + + elif self.doctype not in ["Purchase Receipt"]: data = dict(due_date=due_date, invoice_portion=100, payment_amount=grand_total, base_payment_amount=base_grand_total) self.append("payment_schedule", data) else: @@ -1118,6 +1125,63 @@ class AccountsController(TransactionBase): d.base_payment_amount = flt(base_grand_total * self.get("conversion_rate"), d.precision('base_payment_amount')) + def get_order_details(self): + if self.doctype == "Sales Invoice": + po_or_so = self.get('items')[0].get('sales_order') + po_or_so_doctype = "Sales Order" + po_or_so_doctype_name = "sales_order" + + else: + po_or_so = self.get('items')[0].get('purchase_order') + po_or_so_doctype = "Purchase Order" + po_or_so_doctype_name = "purchase_order" + + return po_or_so, po_or_so_doctype, po_or_so_doctype_name + + def linked_order_has_payment_terms(self, po_or_so, fieldname): + if po_or_so and self.all_items_have_same_po_or_so(po_or_so, fieldname): + if self.linked_order_has_payment_terms_template(po_or_so): + return True + elif self.linked_order_has_payment_schedule(po_or_so): + return True + + return False + + def all_items_have_same_po_or_so(self, po_or_so, fieldname): + for item in self.get('items'): + if item.get(fieldname) != po_or_so: + return False + + return True + + def linked_order_has_payment_terms_template(self, po_or_so): + return frappe.get_value('Sales Order', po_or_so, 'payment_terms_template') + + def linked_order_has_payment_schedule(self, po_or_so): + return frappe.get_all('Payment Schedule', filters={'parent': po_or_so}) + + def fetch_payment_terms_from_order(self, po_or_so, po_or_so_doctype): + """ + Fetch Payment Terms from Purchase/Sales Order on creating a new Purchase/Sales Invoice. + """ + po_or_so = frappe.get_cached_doc(po_or_so_doctype, po_or_so) + + self.payment_schedule = [] + self.payment_terms_template = po_or_so.payment_terms_template + + for schedule in po_or_so.payment_schedule: + payment_schedule = { + 'payment_term': schedule.payment_term, + 'due_date': schedule.due_date, + 'invoice_portion': schedule.invoice_portion, + 'discount_type': schedule.discount_type, + 'discount': schedule.discount, + 'base_payment_amount': schedule.base_payment_amount, + 'payment_amount': schedule.payment_amount, + 'outstanding': schedule.outstanding + } + self.append("payment_schedule", payment_schedule) + def set_due_date(self): due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date] if due_dates: @@ -1802,47 +1866,4 @@ def validate_regional(doc): @erpnext.allow_regional def validate_einvoice_fields(doc): - pass - -def fetch_payment_terms_from_order(doc): - """ - Fetch Payment Terms from Purchase/Sales Order on creating a new Purchase/Sales Invoice. - """ - - if doc.doctype == "Sales Invoice": - po_or_so = doc.get('items')[0].get('sales_order') - po_or_so_doctype = "Sales Order" - po_or_so_doctype_name = "sales_order" - else: - po_or_so = doc.get('items')[0].get('purchase_order') - po_or_so_doctype = "Purchase Order" - po_or_so_doctype_name = "purchase_order" - - if po_or_so and all_items_have_same_po_or_so(doc, po_or_so, po_or_so_doctype_name): - po_or_so = frappe.get_cached_doc(po_or_so_doctype, po_or_so) - else: - doc.set_payment_schedule() - return - - doc.payment_schedule = [] - doc.payment_terms_template = po_or_so.payment_terms_template - - for schedule in po_or_so.payment_schedule: - payment_schedule = { - 'payment_term': schedule.payment_term, - 'due_date': schedule.due_date, - 'invoice_portion': schedule.invoice_portion, - 'discount_type': schedule.discount_type, - 'discount': schedule.discount, - 'base_payment_amount': schedule.base_payment_amount, - 'payment_amount': schedule.payment_amount, - 'outstanding': schedule.outstanding - } - doc.append("payment_schedule", payment_schedule) - -def all_items_have_same_po_or_so(doc, po_or_so, po_or_so_fieldname): - for item in doc.get('items'): - if item.get(po_or_so_fieldname) != po_or_so: - return False - - return True \ No newline at end of file + pass \ No newline at end of file diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index a58c381df3..2b9d516e21 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -621,8 +621,6 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): - from erpnext.controllers.accounts_controller import fetch_payment_terms_from_order - def postprocess(source, target): set_missing_values(source, target) #Get the advance paid Journal Entries in Sales Invoice Advance @@ -697,7 +695,7 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): automatically_fetch_payment_terms = cint(frappe.db.get_single_value('Accounts Settings', 'automatically_fetch_payment_terms')) if automatically_fetch_payment_terms: - fetch_payment_terms_from_order(doclist) + doclist.set_payment_schedule() return doclist diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 1628f93019..f99a01b820 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -414,8 +414,6 @@ def get_returned_qty_map(delivery_note): @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None): - from erpnext.controllers.accounts_controller import fetch_payment_terms_from_order - doc = frappe.get_doc('Delivery Note', source_name) to_make_invoice_qty_map = {} @@ -507,7 +505,7 @@ def make_sales_invoice(source_name, target_doc=None): automatically_fetch_payment_terms = cint(frappe.db.get_single_value('Accounts Settings', 'automatically_fetch_payment_terms')) if automatically_fetch_payment_terms: - fetch_payment_terms_from_order(doc) + doc.set_payment_schedule() return doc diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index cf6cac273f..b05cc7875c 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -583,7 +583,6 @@ def update_billing_percentage(pr_doc, update_modified=True): @frappe.whitelist() def make_purchase_invoice(source_name, target_doc=None): from erpnext.accounts.party import get_payment_terms_template - from erpnext.controllers.accounts_controller import fetch_payment_terms_from_order doc = frappe.get_doc('Purchase Receipt', source_name) returned_qty_map = get_returned_qty_map(source_name) @@ -657,7 +656,7 @@ def make_purchase_invoice(source_name, target_doc=None): automatically_fetch_payment_terms = cint(frappe.db.get_single_value('Accounts Settings', 'automatically_fetch_payment_terms')) if automatically_fetch_payment_terms: - fetch_payment_terms_from_order(doclist) + doc.set_payment_schedule() return doclist From 59d1cc02c525cb6710d2a38a5b9c40031f8bf9a8 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 22 Jul 2021 22:58:45 +0530 Subject: [PATCH 095/147] fix: Add test to check if payment terms are fetched when creating a Sales Invoice --- .../doctype/sales_order/test_sales_order.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 974648d6d4..bf6925473a 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1229,7 +1229,42 @@ class TestSalesOrder(unittest.TestCase): self.assertRaises(frappe.ValidationError, so.cancel) + def test_payment_terms_are_fetched_when_creating_invoice(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice + automatically_fetch_payment_terms() + + so = make_sales_order(uom="Nos", do_not_save=1) + create_payment_terms_template() + so.payment_terms_template = 'Test Receivable Template' + so.submit() + + si = create_sales_invoice(qty=10, do_not_save=1) + si.items[0].sales_order = so.name + si.items[0].so_detail = so.items[0].name + si.insert() + + self.assertEqual(so.payment_terms_template, si.payment_terms_template) + compare_payment_schedules(self, so, si) + +def automatically_fetch_payment_terms(enable=1): + accounts_settings = frappe.get_doc("Accounts Settings") + accounts_settings.automatically_fetch_payment_terms = enable + accounts_settings.save() + +def compare_payment_schedules(doc, doc1, doc2): + payment_schedule1 = frappe.db.sql("""select payment_term, description, due_date, mode_of_payment, invoice_portion, payment_amount + from `tabPayment Schedule` + where parenttype=%s and parent=%s + order by payment_term asc""", (doc1.doctype, doc1.name), as_dict=1) + + payment_schedule2 = frappe.db.sql("""select payment_term, description, due_date, mode_of_payment, invoice_portion, payment_amount + from `tabPayment Schedule` + where parenttype=%s and parent=%s + order by payment_term asc""", (doc2.doctype, doc2.name), as_dict=1) + + doc.assertEqual(payment_schedule1, payment_schedule2) def make_sales_order(**args): so = frappe.new_doc("Sales Order") From 293c5e10c3fa122715ca5f3ca67f9051a87bf01f Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 23 Jul 2021 03:23:29 +0530 Subject: [PATCH 096/147] fix: Add test to check if payment terms are fetched when creating a Sales Invoice --- .../delivery_note/test_delivery_note.py | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index f981aeb13b..8b1245eb40 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -17,7 +17,8 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry \ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, SerialNoWarehouseError from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation \ import create_stock_reconciliation, set_valuation_method -from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order, create_dn_against_so +from erpnext.selling.doctype.sales_order.test_sales_order \ + import make_sales_order, create_dn_against_so, automatically_fetch_payment_terms, compare_payment_schedules from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse from erpnext.stock.doctype.item.test_item import make_item @@ -759,6 +760,30 @@ class TestDeliveryNote(unittest.TestCase): self.assertTrue("TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item") + def test_payment_terms_are_fetched_when_creating_invoice(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice + + automatically_fetch_payment_terms() + + so = make_sales_order(uom="Nos", do_not_save=1) + create_payment_terms_template() + so.payment_terms_template = 'Test Receivable Template' + so.submit() + + dn = create_dn_against_so(so.name, delivered_qty=10) + + si = create_sales_invoice(qty=10, do_not_save=1) + si.items[0].delivery_note= dn.name + si.items[0].dn_detail = dn.items[0].name + si.items[0].sales_order = so.name + si.items[0].so_detail = so.items[0].name + + si.insert() + si.submit() + + self.assertEqual(so.payment_terms_template, si.payment_terms_template) + compare_payment_schedules(self, so, si) def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") From e94604f517ea77f6e82fe8e8c14cdb569742eee2 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 23 Jul 2021 03:36:37 +0530 Subject: [PATCH 097/147] fix: Add test to check if payment terms are fetched when creating a Purchase Invoice --- .../purchase_order/test_purchase_order.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 8563b97ab7..11cf39e5e2 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -968,8 +968,25 @@ class TestPurchaseOrder(unittest.TestCase): # To test if the PO does NOT have a Blanket Order self.assertEqual(po_doc.items[0].blanket_order, None) + def test_payment_terms_are_fetched_when_creating_invoice(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template + from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice + from erpnext.selling.doctype.sales_order.test_sales_order import automatically_fetch_payment_terms, compare_payment_schedules + automatically_fetch_payment_terms() + po = create_purchase_order(qty=10, rate=100, do_not_save=1) + create_payment_terms_template() + po.payment_terms_template = 'Test Receivable Template' + po.submit() + + pi = make_purchase_invoice(qty=10, rate=100, do_not_save=1) + pi.items[0].purchase_order = po.name + pi.items[0].po_detail = po.items[0].name + pi.insert() + + # self.assertEqual(po.payment_terms_template, pi.payment_terms_template) + compare_payment_schedules(self, po, pi) def make_pr_against_po(po, received_qty=0): pr = make_purchase_receipt(po) From 0413a5aafdc2ef05ce6aabfc275081aac8a737af Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 23 Jul 2021 03:44:56 +0530 Subject: [PATCH 098/147] fix: Add test to check if payment terms are fetched when creating a Purchase Invoice --- .../purchase_receipt/test_purchase_receipt.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 2eb8bfd5d2..44e3e1e70e 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1052,6 +1052,58 @@ class TestPurchaseReceipt(unittest.TestCase): frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', before_test_value) + def test_purchase_receipt_with_exchange_rate_difference(self): + from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice as create_purchase_invoice + from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_purchase_receipt as create_purchase_receipt + + pi = create_purchase_invoice(company="_Test Company with perpetual inventory", + cost_center = "Main - TCP1", + warehouse = "Stores - TCP1", + expense_account ="_Test Account Cost for Goods Sold - TCP1", + currency = "USD", conversion_rate = 70) + + pr = create_purchase_receipt(pi.name) + pr.conversion_rate = 80 + pr.items[0].purchase_invoice = pi.name + pr.items[0].purchase_invoice_item = pi.items[0].name + + pr.save() + pr.submit() + + # Get exchnage gain and loss account + exchange_gain_loss_account = frappe.db.get_value('Company', pr.company, 'exchange_gain_loss_account') + + # fetching the latest GL Entry with exchange gain and loss account account + amount = frappe.db.get_value('GL Entry', {'account': exchange_gain_loss_account, 'voucher_no': pr.name}, 'credit') + discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount) + + self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount) + + def test_payment_terms_are_fetched_when_creating_invoice(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template + from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order, make_pr_against_po + from erpnext.selling.doctype.sales_order.test_sales_order import automatically_fetch_payment_terms, compare_payment_schedules + + automatically_fetch_payment_terms() + + po = create_purchase_order(qty=10, rate=100, do_not_save=1) + create_payment_terms_template() + po.payment_terms_template = 'Test Receivable Template' + po.submit() + + pr = make_pr_against_po(po.name, received_qty=10) + + pi = make_purchase_invoice(qty=10, rate=100, do_not_save=1) + pi.items[0].purchase_receipt = pr.name + pi.items[0].pr_detail = pr.items[0].name + pi.items[0].purchase_order = po.name + pi.items[0].po_detail = po.items[0].name + pi.insert() + + # self.assertEqual(po.payment_terms_template, pi.payment_terms_template) + compare_payment_schedules(self, po, pi) + def get_sl_entries(voucher_type, voucher_no): return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s From 57df4a3aa1c5432eb889c7045de3d570fe35e90c Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 23 Jul 2021 03:45:59 +0530 Subject: [PATCH 099/147] fix: Rename tests --- erpnext/buying/doctype/purchase_order/test_purchase_order.py | 2 +- erpnext/selling/doctype/sales_order/test_sales_order.py | 2 +- erpnext/stock/doctype/delivery_note/test_delivery_note.py | 2 +- erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 11cf39e5e2..474c9cf3df 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -968,7 +968,7 @@ class TestPurchaseOrder(unittest.TestCase): # To test if the PO does NOT have a Blanket Order self.assertEqual(po_doc.items[0].blanket_order, None) - def test_payment_terms_are_fetched_when_creating_invoice(self): + def test_payment_terms_are_fetched_when_creating_purchase_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.selling.doctype.sales_order.test_sales_order import automatically_fetch_payment_terms, compare_payment_schedules diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index bf6925473a..3fbe0afd10 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1229,7 +1229,7 @@ class TestSalesOrder(unittest.TestCase): self.assertRaises(frappe.ValidationError, so.cancel) - def test_payment_terms_are_fetched_when_creating_invoice(self): + def test_payment_terms_are_fetched_when_creating_sales_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 8b1245eb40..ca8d8b99d3 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -760,7 +760,7 @@ class TestDeliveryNote(unittest.TestCase): self.assertTrue("TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item") - def test_payment_terms_are_fetched_when_creating_invoice(self): + def test_payment_terms_are_fetched_when_creating_sales_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 44e3e1e70e..9a5064fcc7 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1079,7 +1079,7 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount) - def test_payment_terms_are_fetched_when_creating_invoice(self): + def test_payment_terms_are_fetched_when_creating_purchase_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order, make_pr_against_po From 6343950d82e77ccffa0d444829bd65a8b23fc441 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 23 Jul 2021 03:58:27 +0530 Subject: [PATCH 100/147] fix: Sider issues --- erpnext/selling/doctype/sales_order/test_sales_order.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 3fbe0afd10..f4a089bcef 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1249,9 +1249,9 @@ class TestSalesOrder(unittest.TestCase): compare_payment_schedules(self, so, si) def automatically_fetch_payment_terms(enable=1): - accounts_settings = frappe.get_doc("Accounts Settings") - accounts_settings.automatically_fetch_payment_terms = enable - accounts_settings.save() + accounts_settings = frappe.get_doc("Accounts Settings") + accounts_settings.automatically_fetch_payment_terms = enable + accounts_settings.save() def compare_payment_schedules(doc, doc1, doc2): payment_schedule1 = frappe.db.sql("""select payment_term, description, due_date, mode_of_payment, invoice_portion, payment_amount From 01ab63189a70788c7c542bcd6a69b56280a46761 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 29 Jul 2021 19:18:35 +0530 Subject: [PATCH 101/147] fix: Check if Purchase Order has Payment Terms Template --- erpnext/controllers/accounts_controller.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 2bf5c77e61..913e70b30c 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1109,7 +1109,7 @@ class AccountsController(TransactionBase): elif self.doctype in ["Sales Invoice", "Purchase Invoice"]: po_or_so, doctype, fieldname = self.get_order_details() - if self.linked_order_has_payment_terms(po_or_so, fieldname): + if self.linked_order_has_payment_terms(po_or_so, fieldname, doctype): self.fetch_payment_terms_from_order(po_or_so, doctype) elif self.doctype not in ["Purchase Receipt"]: @@ -1138,9 +1138,9 @@ class AccountsController(TransactionBase): return po_or_so, po_or_so_doctype, po_or_so_doctype_name - def linked_order_has_payment_terms(self, po_or_so, fieldname): + def linked_order_has_payment_terms(self, po_or_so, fieldname, doctype): if po_or_so and self.all_items_have_same_po_or_so(po_or_so, fieldname): - if self.linked_order_has_payment_terms_template(po_or_so): + if self.linked_order_has_payment_terms_template(po_or_so, doctype): return True elif self.linked_order_has_payment_schedule(po_or_so): return True @@ -1154,8 +1154,8 @@ class AccountsController(TransactionBase): return True - def linked_order_has_payment_terms_template(self, po_or_so): - return frappe.get_value('Sales Order', po_or_so, 'payment_terms_template') + def linked_order_has_payment_terms_template(self, po_or_so, doctype): + return frappe.get_value(doctype, po_or_so, 'payment_terms_template') def linked_order_has_payment_schedule(self, po_or_so): return frappe.get_all('Payment Schedule', filters={'parent': po_or_so}) From f99696b75d3819214b3849bdc29d43d16142e4ce Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 2 Aug 2021 23:15:44 +0530 Subject: [PATCH 102/147] fix: Condition for fetching Payment Terms from Sales/Purchase Orders --- erpnext/controllers/accounts_controller.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 913e70b30c..f475bc2fe2 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1101,16 +1101,16 @@ class AccountsController(TransactionBase): base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total")) if not self.get("payment_schedule"): + if self.doctype in ["Sales Invoice", "Purchase Invoice"] and not self.get("payment_terms_template"): + po_or_so, doctype, fieldname = self.get_order_details() + if self.get("payment_terms_template"): data = get_payment_terms(self.payment_terms_template, posting_date, grand_total, base_grand_total) for item in data: self.append("payment_schedule", item) - elif self.doctype in ["Sales Invoice", "Purchase Invoice"]: - po_or_so, doctype, fieldname = self.get_order_details() - - if self.linked_order_has_payment_terms(po_or_so, fieldname, doctype): - self.fetch_payment_terms_from_order(po_or_so, doctype) + elif self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype): + self.fetch_payment_terms_from_order(po_or_so, doctype) elif self.doctype not in ["Purchase Receipt"]: data = dict(due_date=due_date, invoice_portion=100, payment_amount=grand_total, base_payment_amount=base_grand_total) From 8328f4523021e2c187fa6ec17beae20ffe500f04 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 4 Aug 2021 03:15:13 +0530 Subject: [PATCH 103/147] fix: Rename test to reflect changes in code --- erpnext/buying/doctype/purchase_order/test_purchase_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 474c9cf3df..d7db27cb54 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -632,7 +632,7 @@ class TestPurchaseOrder(unittest.TestCase): else: raise Exception - def test_terms_does_not_copy(self): + def test_terms_are_not_copied_if_automatically_fetch_payment_terms_is_unchecked(self): po = create_purchase_order() self.assertTrue(po.get('payment_schedule')) From 8fced95f8cad1a3e1d2561f2beb800b416b535ba Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Mon, 28 Jun 2021 16:50:20 +0530 Subject: [PATCH 104/147] feat: over transfer allowance for material transfers --- .../doctype/material_request/material_request.py | 9 ++++++++- .../stock/doctype/stock_settings/stock_settings.json | 11 +++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 3ad9909ad0..026b85e26d 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -162,8 +162,15 @@ class MaterialRequest(BuyingController): from `tabStock Entry Detail` where material_request = %s and material_request_item = %s and docstatus = 1""", (self.name, d.name))[0][0]) + mr_qty_allowance = frappe.db.get_single_value('Stock Settings', 'mr_qty_allowance') - if d.ordered_qty and d.ordered_qty > d.stock_qty: + if mr_qty_allowance: + allowed_qty = d.qty + (d.qty * (mr_qty_allowance/100)) + if d.ordered_qty and d.ordered_qty > allowed_qty: + frappe.throw(_("The total Issue / Transfer quantity {0} in Material Request {1} \ + cannot be greater than allowed requested quantity {2} for Item {3}").format(d.ordered_qty, d.parent, allowed_qty, d.item_code)) + + elif d.ordered_qty and d.ordered_qty > d.stock_qty: frappe.throw(_("The total Issue / Transfer quantity {0} in Material Request {1} \ cannot be greater than requested quantity {2} for Item {3}").format(d.ordered_qty, d.parent, d.qty, d.item_code)) diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 2a9dcfb67e..f75cb56138 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -18,6 +18,7 @@ "section_break_9", "over_delivery_receipt_allowance", "role_allowed_to_over_deliver_receive", + "mr_qty_allowance", "column_break_12", "auto_insert_price_list_rate_if_missing", "allow_negative_stock", @@ -283,6 +284,12 @@ "fieldtype": "Select", "label": "Action If Quality Inspection Is Rejected", "options": "Stop\nWarn" + }, + { + "description": "The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units.", + "fieldname": "mr_qty_allowance", + "fieldtype": "Float", + "label": "Over Transfer Allowance" } ], "icon": "icon-cog", @@ -290,7 +297,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-07-10 16:17:42.159829", + "modified": "2021-06-28 17:02:26.683002", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", @@ -310,4 +317,4 @@ "sort_field": "modified", "sort_order": "ASC", "track_changes": 1 -} \ No newline at end of file +} From 673bc58193354adf0973b474ff3ff9072918d279 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Wed, 30 Jun 2021 20:16:50 +0530 Subject: [PATCH 105/147] test: test case for over transfer of materials --- .../material_request/test_material_request.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 72a3a5e67c..b4776ba249 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -329,6 +329,58 @@ class TestMaterialRequest(unittest.TestCase): self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0) + def test_over_transfer_qty_allowance(self): + mr = frappe.new_doc('Material Request') + mr.company = "_Test Company" + mr.scheduled_date = today() + mr.append('items',{ + "item_code": "_Test FG Item", + "item_name": "_Test FG Item", + "qty": 10, + "schedule_date": today(), + "uom": "_Test UOM 1", + "warehouse": "_Test Warehouse - _TC" + }) + + mr.material_request_type = "Material Transfer" + mr.insert() + mr.submit() + + frappe.db.set_value('Stock Settings', None, 'mr_qty_allowance', 20) + + # map a stock entry + + se_doc = make_stock_entry(mr.name) + se_doc.update({ + "posting_date": today(), + "posting_time": "00:00", + }) + se_doc.get("items")[0].update({ + "qty": 13, + "transfer_qty": 12.0, + "s_warehouse": "_Test Warehouse - _TC", + "t_warehouse": "_Test Warehouse 1 - _TC", + "basic_rate": 1.0 + }) + + # make available the qty in _Test Warehouse 1 before transfer + sr = frappe.new_doc("Stock Reconciliation") + sr.company = "_Test Company" + sr.purpose = "Opening Stock" + sr.append('items', { + "item_code": "_Test FG Item", + "warehouse": "_Test Warehouse - _TC", + "qty": 20, + "valuation_rate": 0.01 + }) + sr.insert() + sr.submit() + se = frappe.copy_doc(se_doc) + se.insert() + self.assertRaises(frappe.ValidationError) + se.items[0].qty = 12 + se.submit() + def test_completed_qty_for_over_transfer(self): existing_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") existing_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") From 23c104555b33d35d381543adce7d6bf15be91460 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 5 Aug 2021 00:28:42 +0530 Subject: [PATCH 106/147] fix: Remove irrelevant code --- .../purchase_invoice/purchase_invoice.py | 30 ------------------- .../purchase_receipt/test_purchase_receipt.py | 27 ----------------- 2 files changed, 57 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 28a9bddc42..f7992797ed 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1142,36 +1142,6 @@ class PurchaseInvoice(BuyingController): if update: self.db_set('status', self.status, update_modified = update_modified) -# to get details of purchase invoice/receipt from which this doc was created for exchange rate difference handling -def get_purchase_document_details(doc): - if doc.doctype == 'Purchase Invoice': - doc_reference = 'purchase_receipt' - items_reference = 'pr_detail' - parent_doctype = 'Purchase Receipt' - child_doctype = 'Purchase Receipt Item' - else: - doc_reference = 'purchase_invoice' - items_reference = 'purchase_invoice_item' - parent_doctype = 'Purchase Invoice' - child_doctype = 'Purchase Invoice Item' - - purchase_receipts_or_invoices = [] - items = [] - - for item in doc.get('items'): - if item.get(doc_reference): - purchase_receipts_or_invoices.append(item.get(doc_reference)) - if item.get(items_reference): - items.append(item.get(items_reference)) - - exchange_rate_map = frappe._dict(frappe.get_all(parent_doctype, filters={'name': ('in', - purchase_receipts_or_invoices)}, fields=['name', 'conversion_rate'], as_list=1)) - - net_rate_map = frappe._dict(frappe.get_all(child_doctype, filters={'name': ('in', - items)}, fields=['name', 'net_rate'], as_list=1)) - - return exchange_rate_map, net_rate_map - def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context list_context = get_list_context(context) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 9a5064fcc7..fd6ac16fa3 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1052,33 +1052,6 @@ class TestPurchaseReceipt(unittest.TestCase): frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', before_test_value) - def test_purchase_receipt_with_exchange_rate_difference(self): - from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice as create_purchase_invoice - from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_purchase_receipt as create_purchase_receipt - - pi = create_purchase_invoice(company="_Test Company with perpetual inventory", - cost_center = "Main - TCP1", - warehouse = "Stores - TCP1", - expense_account ="_Test Account Cost for Goods Sold - TCP1", - currency = "USD", conversion_rate = 70) - - pr = create_purchase_receipt(pi.name) - pr.conversion_rate = 80 - pr.items[0].purchase_invoice = pi.name - pr.items[0].purchase_invoice_item = pi.items[0].name - - pr.save() - pr.submit() - - # Get exchnage gain and loss account - exchange_gain_loss_account = frappe.db.get_value('Company', pr.company, 'exchange_gain_loss_account') - - # fetching the latest GL Entry with exchange gain and loss account account - amount = frappe.db.get_value('GL Entry', {'account': exchange_gain_loss_account, 'voucher_no': pr.name}, 'credit') - discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount) - - self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount) - def test_payment_terms_are_fetched_when_creating_purchase_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice From 0b11420147066cd11616e474554dd474171e9078 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 5 Aug 2021 00:35:45 +0530 Subject: [PATCH 107/147] fix: Disable automcatically_fetch_payment_terms after running its associated tests --- erpnext/buying/doctype/purchase_order/test_purchase_order.py | 2 ++ erpnext/selling/doctype/sales_order/test_sales_order.py | 2 ++ erpnext/stock/doctype/delivery_note/test_delivery_note.py | 2 ++ erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py | 2 ++ 4 files changed, 8 insertions(+) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index d7db27cb54..0db54e4206 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -988,6 +988,8 @@ class TestPurchaseOrder(unittest.TestCase): # self.assertEqual(po.payment_terms_template, pi.payment_terms_template) compare_payment_schedules(self, po, pi) + automatically_fetch_payment_terms(enable=0) + def make_pr_against_po(po, received_qty=0): pr = make_purchase_receipt(po) pr.get("items")[0].qty = received_qty or 5 diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index f4a089bcef..5639ee8069 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1248,6 +1248,8 @@ class TestSalesOrder(unittest.TestCase): self.assertEqual(so.payment_terms_template, si.payment_terms_template) compare_payment_schedules(self, so, si) + automatically_fetch_payment_terms(enable=0) + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") accounts_settings.automatically_fetch_payment_terms = enable diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index ca8d8b99d3..756825e826 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -785,6 +785,8 @@ class TestDeliveryNote(unittest.TestCase): self.assertEqual(so.payment_terms_template, si.payment_terms_template) compare_payment_schedules(self, so, si) + automatically_fetch_payment_terms(enable=0) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") args = frappe._dict(args) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index fd6ac16fa3..34ea3257d5 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1077,6 +1077,8 @@ class TestPurchaseReceipt(unittest.TestCase): # self.assertEqual(po.payment_terms_template, pi.payment_terms_template) compare_payment_schedules(self, po, pi) + automatically_fetch_payment_terms(enable=0) + def get_sl_entries(voucher_type, voucher_no): return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s From 4a6ef9ab0f5d02cf86286e905a93261530be5d86 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 5 Aug 2021 00:52:55 +0530 Subject: [PATCH 108/147] fix: Compare Payment Schedules --- .../doctype/sales_order/test_sales_order.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 5639ee8069..a226da75cd 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -5,7 +5,7 @@ import json import unittest import frappe import frappe.permissions -from frappe.utils import flt, add_days, nowdate +from frappe.utils import flt, add_days, nowdate, getdate from frappe.core.doctype.user_permission.test_user_permission import create_user from erpnext.selling.doctype.sales_order.sales_order \ import make_material_request, make_delivery_note, make_sales_invoice, WarehouseRequired @@ -1256,17 +1256,11 @@ def automatically_fetch_payment_terms(enable=1): accounts_settings.save() def compare_payment_schedules(doc, doc1, doc2): - payment_schedule1 = frappe.db.sql("""select payment_term, description, due_date, mode_of_payment, invoice_portion, payment_amount - from `tabPayment Schedule` - where parenttype=%s and parent=%s - order by payment_term asc""", (doc1.doctype, doc1.name), as_dict=1) - - payment_schedule2 = frappe.db.sql("""select payment_term, description, due_date, mode_of_payment, invoice_portion, payment_amount - from `tabPayment Schedule` - where parenttype=%s and parent=%s - order by payment_term asc""", (doc2.doctype, doc2.name), as_dict=1) - - doc.assertEqual(payment_schedule1, payment_schedule2) + for index, schedule in enumerate(doc1.get('payment_schedule')): + doc.assertEqual(schedule.payment_term, doc2.payment_schedule[index].payment_term) + doc.assertEqual(getdate(schedule.due_date), doc2.payment_schedule[index].due_date) + doc.assertEqual(schedule.invoice_portion, doc2.payment_schedule[index].invoice_portion) + doc.assertEqual(schedule.payment_amount, doc2.payment_schedule[index].payment_amount) def make_sales_order(**args): so = frappe.new_doc("Sales Order") From e4a07d8ff349907129eccd1fdad48efa4429900d Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 5 Aug 2021 21:42:09 +0530 Subject: [PATCH 109/147] fix: Stop fetching amount while fetching Payment Terms --- erpnext/controllers/accounts_controller.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f475bc2fe2..7f389cc32a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1175,10 +1175,7 @@ class AccountsController(TransactionBase): 'due_date': schedule.due_date, 'invoice_portion': schedule.invoice_portion, 'discount_type': schedule.discount_type, - 'discount': schedule.discount, - 'base_payment_amount': schedule.base_payment_amount, - 'payment_amount': schedule.payment_amount, - 'outstanding': schedule.outstanding + 'discount': schedule.discount } self.append("payment_schedule", payment_schedule) From 5c21eea13d865103ee4d62f4922ae49b76382c4f Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 5 Aug 2021 21:50:09 +0530 Subject: [PATCH 110/147] fix: Fetch discount details from Payment Terms only if Discount Type = Percentage --- erpnext/controllers/accounts_controller.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 7f389cc32a..78e4ad31ad 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1174,9 +1174,14 @@ class AccountsController(TransactionBase): 'payment_term': schedule.payment_term, 'due_date': schedule.due_date, 'invoice_portion': schedule.invoice_portion, - 'discount_type': schedule.discount_type, - 'discount': schedule.discount + 'mode_of_payment': schedule.mode_of_payment, + 'description': schedule.description } + + if schedule.discount_type == 'Percentage': + payment_schedule['discount_type'] = schedule.discount_type + payment_schedule['discount'] = schedule.discount + self.append("payment_schedule", payment_schedule) def set_due_date(self): From fb80ca9e06ae57dbb61e1a3907b2cfc19b1fd925 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 5 Aug 2021 22:04:11 +0530 Subject: [PATCH 111/147] fix: Only fetch default Payment Terms Template if present --- erpnext/accounts/party.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index b97dc401e6..19a394f743 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals +from re import template import frappe, erpnext from frappe import _, msgprint, scrub @@ -59,7 +60,10 @@ def _get_party_details(party=None, account=None, party_type="Customer", company= billing_address=party_address, shipping_address=shipping_address) if fetch_payment_terms_template: - party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company) + payment_terms_template = get_payment_terms_template(party.name, party_type, company) + + if payment_terms_template: + party_details["payment_terms_template"] = payment_terms_template if not party_details.get("currency"): party_details["currency"] = currency From 232c7286364220e1286d593605e9401b35573486 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 5 Aug 2021 23:04:58 +0530 Subject: [PATCH 112/147] Revert "fix: Only fetch default Payment Terms Template if present" This reverts commit fb80ca9e06ae57dbb61e1a3907b2cfc19b1fd925. --- erpnext/accounts/party.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 19a394f743..b97dc401e6 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals -from re import template import frappe, erpnext from frappe import _, msgprint, scrub @@ -60,10 +59,7 @@ def _get_party_details(party=None, account=None, party_type="Customer", company= billing_address=party_address, shipping_address=shipping_address) if fetch_payment_terms_template: - payment_terms_template = get_payment_terms_template(party.name, party_type, company) - - if payment_terms_template: - party_details["payment_terms_template"] = payment_terms_template + party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company) if not party_details.get("currency"): party_details["currency"] = currency From 0bba425fe300305c7d55a543dd42f98549d9b021 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 6 Aug 2021 23:53:16 +0530 Subject: [PATCH 113/147] fix: Ignore default payment term templates when coping payment terms from orders --- .../purchase_invoice/purchase_invoice.js | 5 +- .../purchase_invoice/purchase_invoice.json | 635 +++++++++++---- .../doctype/sales_invoice/sales_invoice.json | 731 +++++++++++++----- erpnext/accounts/party.py | 4 +- .../doctype/purchase_order/purchase_order.py | 7 +- erpnext/controllers/accounts_controller.py | 31 +- erpnext/controllers/buying_controller.py | 3 +- erpnext/public/js/utils/party.js | 1 + .../purchase_receipt/purchase_receipt.py | 5 +- 9 files changed, 1060 insertions(+), 362 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index dc9094c3e9..299531d9f3 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -275,7 +275,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ // Do not update if inter company reference is there as the details will already be updated if(this.frm.updating_party_details || this.frm.doc.inter_company_invoice_reference) return; - + erpnext.utils.get_party_details(this.frm, "erpnext.accounts.party.get_party_details", { posting_date: this.frm.doc.posting_date, @@ -283,7 +283,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ party: this.frm.doc.supplier, party_type: "Supplier", account: this.frm.doc.credit_to, - price_list: this.frm.doc.buying_price_list + price_list: this.frm.doc.buying_price_list, + fetch_payment_terms_template: cint(!this.frm.doc.ignore_default_payment_terms_template) }, function() { me.apply_pricing_rule(); me.frm.doc.apply_tds = me.frm.supplier_tds ? 1 : 0; diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 00ef7d5c18..f1bf595a39 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -131,6 +131,7 @@ "advances", "payment_schedule_section", "payment_terms_template", + "ignore_default_payment_terms_template", "payment_schedule", "terms_section_break", "tc_name", @@ -175,7 +176,9 @@ "hidden": 1, "label": "Title", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "naming_series", @@ -187,7 +190,9 @@ "options": "ACC-PINV-.YYYY.-\nACC-PINV-RET-.YYYY.-", "print_hide": 1, "reqd": 1, - "set_only_once": 1 + "set_only_once": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "supplier", @@ -199,7 +204,9 @@ "options": "Supplier", "print_hide": 1, "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -211,7 +218,9 @@ "label": "Supplier Name", "oldfieldname": "supplier_name", "oldfieldtype": "Data", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fetch_from": "supplier.tax_id", @@ -219,21 +228,27 @@ "fieldtype": "Read Only", "label": "Tax Id", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "due_date", "fieldtype": "Date", "label": "Due Date", "oldfieldname": "due_date", - "oldfieldtype": "Date" + "oldfieldtype": "Date", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "is_paid", "fieldtype": "Check", "label": "Is Paid", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -241,19 +256,25 @@ "fieldtype": "Check", "label": "Is Return (Debit Note)", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "apply_tds", "fieldtype": "Check", "label": "Apply Tax Withholding Amount", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break1", "fieldtype": "Column Break", "oldfieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -263,13 +284,17 @@ "label": "Company", "options": "Company", "print_hide": 1, - "remember_last_selected_value": 1 + "remember_last_selected_value": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "cost_center", "fieldtype": "Link", "label": "Cost Center", - "options": "Cost Center" + "options": "Cost Center", + "show_days": 1, + "show_seconds": 1 }, { "default": "Today", @@ -281,7 +306,9 @@ "oldfieldtype": "Date", "print_hide": 1, "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "posting_time", @@ -290,6 +317,8 @@ "no_copy": 1, "print_hide": 1, "print_width": "100px", + "show_days": 1, + "show_seconds": 1, "width": "100px" }, { @@ -298,7 +327,9 @@ "fieldname": "set_posting_time", "fieldtype": "Check", "label": "Edit Posting Date and Time", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "amended_from", @@ -310,44 +341,58 @@ "oldfieldtype": "Link", "options": "Purchase Invoice", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "collapsible_depends_on": "eval:doc.on_hold", "fieldname": "sb_14", "fieldtype": "Section Break", - "label": "Hold Invoice" + "label": "Hold Invoice", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "on_hold", "fieldtype": "Check", - "label": "Hold Invoice" + "label": "Hold Invoice", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.on_hold", "description": "Once set, this invoice will be on hold till the set date", "fieldname": "release_date", "fieldtype": "Date", - "label": "Release Date" + "label": "Release Date", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "cb_17", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.on_hold", "fieldname": "hold_comment", "fieldtype": "Small Text", - "label": "Reason For Putting On Hold" + "label": "Reason For Putting On Hold", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "collapsible_depends_on": "bill_no", "fieldname": "supplier_invoice_details", "fieldtype": "Section Break", - "label": "Supplier Invoice Details" + "label": "Supplier Invoice Details", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "bill_no", @@ -355,11 +400,15 @@ "label": "Supplier Invoice No", "oldfieldname": "bill_no", "oldfieldtype": "Data", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_15", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "bill_date", @@ -368,13 +417,17 @@ "no_copy": 1, "oldfieldname": "bill_date", "oldfieldtype": "Date", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "return_against", "fieldname": "returns", "fieldtype": "Section Break", - "label": "Returns" + "label": "Returns", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "return_against", @@ -384,26 +437,34 @@ "no_copy": 1, "options": "Purchase Invoice", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "section_addresses", "fieldtype": "Section Break", - "label": "Address and Contact" + "label": "Address and Contact", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "supplier_address", "fieldtype": "Link", "label": "Select Supplier Address", "options": "Address", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "address_display", "fieldtype": "Small Text", "label": "Address", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_person", @@ -411,51 +472,67 @@ "in_global_search": 1, "label": "Contact Person", "options": "Contact", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_display", "fieldtype": "Small Text", "label": "Contact", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_mobile", "fieldtype": "Small Text", "label": "Mobile No", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_email", "fieldtype": "Small Text", "label": "Contact Email", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_break_address", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_address", "fieldtype": "Link", "label": "Select Shipping Address", "options": "Address", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_address_display", "fieldtype": "Small Text", "label": "Shipping Address", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "currency_and_price_list", "fieldtype": "Section Break", "label": "Currency and Price List", - "options": "fa fa-tag" + "options": "fa fa-tag", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "currency", @@ -464,7 +541,9 @@ "oldfieldname": "currency", "oldfieldtype": "Select", "options": "Currency", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "conversion_rate", @@ -473,18 +552,24 @@ "oldfieldname": "conversion_rate", "oldfieldtype": "Currency", "precision": "9", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break2", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "buying_price_list", "fieldtype": "Link", "label": "Price List", "options": "Price List", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "price_list_currency", @@ -492,14 +577,18 @@ "label": "Price List Currency", "options": "Currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "plc_conversion_rate", "fieldtype": "Float", "label": "Price List Exchange Rate", "precision": "9", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -508,11 +597,15 @@ "label": "Ignore Pricing Rule", "no_copy": 1, "permlevel": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "sec_warehouse", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "update_stock", @@ -521,7 +614,9 @@ "fieldtype": "Link", "label": "Set Accepted Warehouse", "options": "Warehouse", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "update_stock", @@ -531,11 +626,15 @@ "label": "Rejected Warehouse", "no_copy": 1, "options": "Warehouse", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_break_warehouse", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "No", @@ -543,25 +642,33 @@ "fieldtype": "Select", "label": "Raw Materials Supplied", "options": "No\nYes", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "items_section", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-shopping-cart" + "options": "fa fa-shopping-cart", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "update_stock", "fieldtype": "Check", "label": "Update Stock", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "scan_barcode", "fieldtype": "Data", - "label": "Scan Barcode" + "label": "Scan Barcode", + "show_days": 1, + "show_seconds": 1 }, { "allow_bulk_edit": 1, @@ -571,25 +678,33 @@ "oldfieldname": "entries", "oldfieldtype": "Table", "options": "Purchase Invoice Item", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "pricing_rule_details", "fieldtype": "Section Break", - "label": "Pricing Rules" + "label": "Pricing Rules", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "pricing_rules", "fieldtype": "Table", "label": "Pricing Rule Detail", "options": "Pricing Rule Detail", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible_depends_on": "supplied_items", "fieldname": "raw_materials_supplied", "fieldtype": "Section Break", - "label": "Raw Materials Supplied" + "label": "Raw Materials Supplied", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "update_stock", @@ -597,17 +712,23 @@ "fieldtype": "Table", "label": "Supplied Items", "no_copy": 1, - "options": "Purchase Receipt Item Supplied" + "options": "Purchase Receipt Item Supplied", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_26", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_qty", "fieldtype": "Float", "label": "Total Quantity", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_total", @@ -615,7 +736,9 @@ "label": "Total (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_net_total", @@ -625,18 +748,24 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_28", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total", "fieldtype": "Currency", "label": "Total", "options": "currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "net_total", @@ -646,42 +775,56 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_net_weight", "fieldtype": "Float", "label": "Total Net Weight", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_section", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-money" + "options": "fa fa-money", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "tax_category", "fieldtype": "Link", "label": "Tax Category", "options": "Tax Category", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_49", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_rule", "fieldtype": "Link", "label": "Shipping Rule", "options": "Shipping Rule", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_51", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_and_charges", @@ -690,7 +833,9 @@ "oldfieldname": "purchase_other_charges", "oldfieldtype": "Link", "options": "Purchase Taxes and Charges Template", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes", @@ -698,13 +843,17 @@ "label": "Purchase Taxes and Charges", "oldfieldname": "purchase_tax_details", "oldfieldtype": "Table", - "options": "Purchase Taxes and Charges" + "options": "Purchase Taxes and Charges", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "sec_tax_breakup", "fieldtype": "Section Break", - "label": "Tax Breakup" + "label": "Tax Breakup", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "other_charges_calculation", @@ -713,13 +862,17 @@ "no_copy": 1, "oldfieldtype": "HTML", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "totals", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-money" + "options": "fa fa-money", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_taxes_and_charges_added", @@ -729,7 +882,9 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_taxes_and_charges_deducted", @@ -739,7 +894,9 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_total_taxes_and_charges", @@ -749,11 +906,15 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_40", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_and_charges_added", @@ -763,7 +924,9 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_and_charges_deducted", @@ -773,7 +936,9 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_taxes_and_charges", @@ -781,14 +946,18 @@ "label": "Total Taxes and Charges", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "collapsible_depends_on": "discount_amount", "fieldname": "section_break_44", "fieldtype": "Section Break", - "label": "Additional Discount" + "label": "Additional Discount", + "show_days": 1, + "show_seconds": 1 }, { "default": "Grand Total", @@ -796,7 +965,9 @@ "fieldtype": "Select", "label": "Apply Additional Discount On", "options": "\nGrand Total\nNet Total", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_discount_amount", @@ -804,28 +975,38 @@ "label": "Additional Discount Amount (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_46", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "additional_discount_percentage", "fieldtype": "Float", "label": "Additional Discount Percentage", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "discount_amount", "fieldtype": "Currency", "label": "Additional Discount Amount", "options": "currency", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_49", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_grand_total", @@ -835,7 +1016,9 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -845,7 +1028,9 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -855,7 +1040,9 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_in_words", @@ -865,13 +1052,17 @@ "oldfieldname": "in_words", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break8", "fieldtype": "Column Break", "oldfieldtype": "Column Break", "print_hide": 1, + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -882,7 +1073,9 @@ "oldfieldname": "grand_total_import", "oldfieldtype": "Currency", "options": "currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -892,7 +1085,9 @@ "no_copy": 1, "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -902,7 +1097,9 @@ "no_copy": 1, "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "in_words", @@ -912,7 +1109,9 @@ "oldfieldname": "in_words_import", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_advance", @@ -923,7 +1122,9 @@ "oldfieldtype": "Currency", "options": "party_account_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "outstanding_amount", @@ -934,14 +1135,18 @@ "oldfieldtype": "Currency", "options": "party_account_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "depends_on": "grand_total", "fieldname": "disable_rounded_total", "fieldtype": "Check", - "label": "Disable Rounded Total" + "label": "Disable Rounded Total", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -949,20 +1154,26 @@ "depends_on": "eval:doc.is_paid===1||(doc.advances && doc.advances.length>0)", "fieldname": "payments_section", "fieldtype": "Section Break", - "label": "Payments" + "label": "Payments", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "mode_of_payment", "fieldtype": "Link", "label": "Mode of Payment", "options": "Mode of Payment", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "cash_bank_account", "fieldtype": "Link", "label": "Cash/Bank Account", - "options": "Account" + "options": "Account", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "clearance_date", @@ -970,11 +1181,15 @@ "label": "Clearance Date", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_br_payments", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "is_paid", @@ -983,7 +1198,9 @@ "label": "Paid Amount", "no_copy": 1, "options": "currency", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_paid_amount", @@ -992,7 +1209,9 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1000,7 +1219,9 @@ "depends_on": "grand_total", "fieldname": "write_off", "fieldtype": "Section Break", - "label": "Write Off" + "label": "Write Off", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "write_off_amount", @@ -1008,7 +1229,9 @@ "label": "Write Off Amount", "no_copy": 1, "options": "currency", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_write_off_amount", @@ -1017,11 +1240,15 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_61", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:flt(doc.write_off_amount)!=0", @@ -1029,7 +1256,9 @@ "fieldtype": "Link", "label": "Write Off Account", "options": "Account", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:flt(doc.write_off_amount)!=0", @@ -1037,7 +1266,9 @@ "fieldtype": "Link", "label": "Write Off Cost Center", "options": "Cost Center", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1047,13 +1278,17 @@ "label": "Advance Payments", "oldfieldtype": "Section Break", "options": "fa fa-money", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "allocate_advances_automatically", "fieldtype": "Check", - "label": "Set Advances and Allocate (FIFO)" + "label": "Set Advances and Allocate (FIFO)", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.allocate_advances_automatically", @@ -1061,7 +1296,9 @@ "fieldtype": "Button", "label": "Get Advances Paid", "oldfieldtype": "Button", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "advances", @@ -1071,20 +1308,26 @@ "oldfieldname": "advance_allocation_details", "oldfieldtype": "Table", "options": "Purchase Invoice Advance", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "collapsible_depends_on": "eval:(!doc.is_return)", "fieldname": "payment_schedule_section", "fieldtype": "Section Break", - "label": "Payment Terms" + "label": "Payment Terms", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "payment_terms_template", "fieldtype": "Link", "label": "Payment Terms Template", - "options": "Payment Terms Template" + "options": "Payment Terms Template", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "payment_schedule", @@ -1092,7 +1335,9 @@ "label": "Payment Schedule", "no_copy": 1, "options": "Payment Schedule", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1100,25 +1345,33 @@ "fieldname": "terms_section_break", "fieldtype": "Section Break", "label": "Terms and Conditions", - "options": "fa fa-legal" + "options": "fa fa-legal", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "tc_name", "fieldtype": "Link", "label": "Terms", "options": "Terms and Conditions", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "terms", "fieldtype": "Text Editor", - "label": "Terms and Conditions1" + "label": "Terms and Conditions1", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "printing_settings", "fieldtype": "Section Break", - "label": "Printing Settings" + "label": "Printing Settings", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1126,7 +1379,9 @@ "fieldtype": "Link", "label": "Letter Head", "options": "Letter Head", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1134,11 +1389,15 @@ "fieldname": "group_same_items", "fieldtype": "Check", "label": "Group same items", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_112", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1150,14 +1409,18 @@ "oldfieldtype": "Link", "options": "Print Heading", "print_hide": 1, - "report_hide": 1 + "report_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "language", "fieldtype": "Data", "label": "Print Language", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1166,7 +1429,9 @@ "label": "More Information", "oldfieldtype": "Section Break", "options": "fa fa-file-text", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "credit_to", @@ -1177,7 +1442,9 @@ "options": "Account", "print_hide": 1, "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "party_account_currency", @@ -1187,7 +1454,9 @@ "no_copy": 1, "options": "Currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "No", @@ -1197,7 +1466,9 @@ "oldfieldname": "is_opening", "oldfieldtype": "Select", "options": "No\nYes", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "against_expense_account", @@ -1207,11 +1478,15 @@ "no_copy": 1, "oldfieldname": "against_expense_account", "oldfieldtype": "Small Text", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_63", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "Draft", @@ -1220,7 +1495,9 @@ "in_standard_filter": 1, "label": "Status", "options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nUnpaid\nOverdue\nCancelled\nInternal Transfer", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "inter_company_invoice_reference", @@ -1229,7 +1506,9 @@ "no_copy": 1, "options": "Sales Invoice", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "remarks", @@ -1238,14 +1517,18 @@ "no_copy": 1, "oldfieldname": "remarks", "oldfieldtype": "Text", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "subscription_section", "fieldtype": "Section Break", "label": "Subscription Section", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1254,7 +1537,9 @@ "fieldtype": "Date", "label": "From Date", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1263,11 +1548,15 @@ "fieldtype": "Date", "label": "To Date", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_114", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "auto_repeat", @@ -1276,24 +1565,32 @@ "no_copy": 1, "options": "Auto Repeat", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, "depends_on": "eval: doc.auto_repeat", "fieldname": "update_auto_repeat_reference", "fieldtype": "Button", - "label": "Update Auto Repeat Reference" + "label": "Update Auto Repeat Reference", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", - "label": "Accounting Dimensions " + "label": "Accounting Dimensions ", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "dimension_col_break", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -1301,7 +1598,9 @@ "fieldname": "is_internal_supplier", "fieldtype": "Check", "label": "Is Internal Supplier", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "tax_withholding_category", @@ -1309,25 +1608,33 @@ "hidden": 1, "label": "Tax Withholding Category", "options": "Tax Withholding Category", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "billing_address", "fieldtype": "Link", "label": "Select Billing Address", - "options": "Address" + "options": "Address", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "billing_address_display", "fieldtype": "Small Text", "label": "Billing Address", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "project", "fieldtype": "Link", "label": "Project", - "options": "Project" + "options": "Project", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.is_internal_supplier", @@ -1335,7 +1642,9 @@ "fieldname": "unrealized_profit_loss_account", "fieldtype": "Link", "label": "Unrealized Profit / Loss Account", - "options": "Account" + "options": "Account", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.is_internal_supplier", @@ -1344,7 +1653,9 @@ "fieldname": "represents_company", "fieldtype": "Link", "label": "Represents Company", - "options": "Company" + "options": "Company", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.update_stock && doc.is_internal_supplier", @@ -1356,6 +1667,8 @@ "options": "Warehouse", "print_hide": 1, "print_width": "50px", + "show_days": 1, + "show_seconds": 1, "width": "50px" }, { @@ -1367,6 +1680,8 @@ "options": "Warehouse", "print_hide": 1, "print_width": "50px", + "show_days": 1, + "show_seconds": 1, "width": "50px" }, { @@ -1376,14 +1691,26 @@ "label": "Per Received", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "0", + "fieldname": "ignore_default_payment_terms_template", + "fieldtype": "Check", + "hidden": 1, + "label": "Ignore Default Payment Terms Template", + "read_only": 1, + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2021-06-15 18:20:56.806195", + "modified": "2021-08-07 17:53:14.351439", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index e7dd6b8a60..23e8091761 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -125,6 +125,7 @@ "get_advances", "advances", "payment_schedule_section", + "ignore_default_payment_terms_template", "payment_terms_template", "payment_schedule", "payments_section", @@ -195,7 +196,9 @@ "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, - "options": "fa fa-user" + "options": "fa fa-user", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -207,7 +210,9 @@ "hide_seconds": 1, "label": "Title", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -222,7 +227,9 @@ "options": "ACC-SINV-.YYYY.-\nACC-SINV-RET-.YYYY.-", "print_hide": 1, "reqd": 1, - "set_only_once": 1 + "set_only_once": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -236,7 +243,9 @@ "oldfieldtype": "Link", "options": "Customer", "print_hide": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -250,7 +259,9 @@ "label": "Customer Name", "oldfieldname": "customer_name", "oldfieldtype": "Data", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "tax_id", @@ -259,7 +270,9 @@ "hide_seconds": 1, "label": "Tax Id", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "project", @@ -271,7 +284,9 @@ "oldfieldname": "project_name", "oldfieldtype": "Link", "options": "Project", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -282,7 +297,9 @@ "label": "Include Payment (POS)", "oldfieldname": "is_pos", "oldfieldtype": "Check", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "is_pos", @@ -292,7 +309,9 @@ "hide_seconds": 1, "label": "POS Profile", "options": "POS Profile", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -302,14 +321,18 @@ "hide_seconds": 1, "label": "Is Return (Credit Note)", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break1", "fieldtype": "Column Break", "hide_days": 1, "hide_seconds": 1, - "oldfieldtype": "Column Break" + "oldfieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "company", @@ -323,7 +346,9 @@ "options": "Company", "print_hide": 1, "remember_last_selected_value": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "cost_center", @@ -331,7 +356,9 @@ "hide_days": 1, "hide_seconds": 1, "label": "Cost Center", - "options": "Cost Center" + "options": "Cost Center", + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -345,7 +372,9 @@ "oldfieldname": "posting_date", "oldfieldtype": "Date", "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "posting_time", @@ -356,7 +385,9 @@ "no_copy": 1, "oldfieldname": "posting_time", "oldfieldtype": "Time", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -366,7 +397,9 @@ "hide_days": 1, "hide_seconds": 1, "label": "Edit Posting Date and Time", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "due_date", @@ -376,7 +409,9 @@ "label": "Payment Due Date", "no_copy": 1, "oldfieldname": "due_date", - "oldfieldtype": "Date" + "oldfieldtype": "Date", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "amended_from", @@ -390,7 +425,9 @@ "oldfieldtype": "Link", "options": "Sales Invoice", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.return_against || doc.is_debit_note", @@ -403,7 +440,9 @@ "options": "Sales Invoice", "print_hide": 1, "read_only_depends_on": "eval:doc.is_return", - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -412,7 +451,9 @@ "fieldtype": "Check", "hide_days": 1, "hide_seconds": 1, - "label": "Update Billed Amount in Sales Order" + "label": "Update Billed Amount in Sales Order", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -421,7 +462,9 @@ "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, - "label": "Customer PO Details" + "label": "Customer PO Details", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -431,13 +474,17 @@ "hide_seconds": 1, "label": "Customer's Purchase Order", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_23", "fieldtype": "Column Break", "hide_days": 1, - "hide_seconds": 1 + "hide_seconds": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -445,7 +492,9 @@ "fieldtype": "Date", "hide_days": 1, "hide_seconds": 1, - "label": "Customer's Purchase Order Date" + "label": "Customer's Purchase Order Date", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -453,7 +502,9 @@ "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, - "label": "Address and Contact" + "label": "Address and Contact", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "customer_address", @@ -462,7 +513,9 @@ "hide_seconds": 1, "label": "Customer Address", "options": "Address", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "address_display", @@ -470,7 +523,9 @@ "hide_days": 1, "hide_seconds": 1, "label": "Address", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_person", @@ -480,7 +535,9 @@ "in_global_search": 1, "label": "Contact Person", "options": "Contact", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_display", @@ -488,7 +545,9 @@ "hide_days": 1, "hide_seconds": 1, "label": "Contact", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_mobile", @@ -497,7 +556,9 @@ "hide_days": 1, "hide_seconds": 1, "label": "Mobile No", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_email", @@ -508,7 +569,9 @@ "label": "Contact Email", "options": "Email", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "territory", @@ -517,13 +580,17 @@ "hide_seconds": 1, "label": "Territory", "options": "Territory", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_break4", "fieldtype": "Column Break", "hide_days": 1, - "hide_seconds": 1 + "hide_seconds": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_address_name", @@ -532,7 +599,9 @@ "hide_seconds": 1, "label": "Shipping Address Name", "options": "Address", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_address", @@ -541,7 +610,9 @@ "hide_seconds": 1, "label": "Shipping Address", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "company_address", @@ -550,7 +621,9 @@ "hide_seconds": 1, "label": "Company Address Name", "options": "Address", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "company_address_display", @@ -560,7 +633,9 @@ "hide_seconds": 1, "label": "Company Address", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -569,7 +644,9 @@ "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, - "label": "Currency and Price List" + "label": "Currency and Price List", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "currency", @@ -581,7 +658,9 @@ "oldfieldtype": "Select", "options": "Currency", "print_hide": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "description": "Rate at which Customer Currency is converted to customer's base currency", @@ -594,13 +673,17 @@ "oldfieldtype": "Currency", "precision": "9", "print_hide": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break2", "fieldtype": "Column Break", "hide_days": 1, "hide_seconds": 1, + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -613,7 +696,9 @@ "oldfieldtype": "Select", "options": "Price List", "print_hide": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "price_list_currency", @@ -624,7 +709,9 @@ "options": "Currency", "print_hide": 1, "read_only": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "description": "Rate at which Price list currency is converted to customer's base currency", @@ -635,7 +722,9 @@ "label": "Price List Exchange Rate", "precision": "9", "print_hide": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -646,14 +735,18 @@ "label": "Ignore Pricing Rule", "no_copy": 1, "permlevel": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "sec_warehouse", "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, - "label": "Warehouse" + "label": "Warehouse", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "update_stock", @@ -663,7 +756,9 @@ "hide_seconds": 1, "label": "Source Warehouse", "options": "Warehouse", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "items_section", @@ -672,7 +767,9 @@ "hide_seconds": 1, "label": "Items", "oldfieldtype": "Section Break", - "options": "fa fa-shopping-cart" + "options": "fa fa-shopping-cart", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -683,14 +780,18 @@ "label": "Update Stock", "oldfieldname": "update_stock", "oldfieldtype": "Check", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "scan_barcode", "fieldtype": "Data", "hide_days": 1, "hide_seconds": 1, - "label": "Scan Barcode" + "label": "Scan Barcode", + "show_days": 1, + "show_seconds": 1 }, { "allow_bulk_edit": 1, @@ -702,14 +803,18 @@ "oldfieldname": "entries", "oldfieldtype": "Table", "options": "Sales Invoice Item", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "pricing_rule_details", "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, - "label": "Pricing Rules" + "label": "Pricing Rules", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "pricing_rules", @@ -718,7 +823,9 @@ "hide_seconds": 1, "label": "Pricing Rule Detail", "options": "Pricing Rule Detail", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "packing_list", @@ -727,7 +834,9 @@ "hide_seconds": 1, "label": "Packing List", "options": "fa fa-suitcase", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "packed_items", @@ -736,7 +845,9 @@ "hide_seconds": 1, "label": "Packed Items", "options": "Packed Item", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "product_bundle_help", @@ -744,7 +855,9 @@ "hide_days": 1, "hide_seconds": 1, "label": "Product Bundle Help", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -754,7 +867,9 @@ "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, - "label": "Time Sheet List" + "label": "Time Sheet List", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "timesheets", @@ -763,7 +878,9 @@ "hide_seconds": 1, "label": "Time Sheets", "options": "Sales Invoice Timesheet", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -774,13 +891,17 @@ "label": "Total Billing Amount", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_30", "fieldtype": "Section Break", "hide_days": 1, - "hide_seconds": 1 + "hide_seconds": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_qty", @@ -788,7 +909,9 @@ "hide_days": 1, "hide_seconds": 1, "label": "Total Quantity", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_total", @@ -798,7 +921,9 @@ "label": "Total (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_net_total", @@ -811,13 +936,17 @@ "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_32", "fieldtype": "Column Break", "hide_days": 1, - "hide_seconds": 1 + "hide_seconds": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total", @@ -826,7 +955,9 @@ "hide_seconds": 1, "label": "Total", "options": "currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "net_total", @@ -836,7 +967,9 @@ "label": "Net Total", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_net_weight", @@ -845,7 +978,9 @@ "hide_seconds": 1, "label": "Total Net Weight", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_section", @@ -853,7 +988,9 @@ "hide_days": 1, "hide_seconds": 1, "oldfieldtype": "Section Break", - "options": "fa fa-money" + "options": "fa fa-money", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_and_charges", @@ -864,13 +1001,17 @@ "oldfieldname": "charge", "oldfieldtype": "Link", "options": "Sales Taxes and Charges Template", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_38", "fieldtype": "Column Break", "hide_days": 1, - "hide_seconds": 1 + "hide_seconds": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_rule", @@ -880,7 +1021,9 @@ "label": "Shipping Rule", "oldfieldtype": "Button", "options": "Shipping Rule", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "tax_category", @@ -889,13 +1032,17 @@ "hide_seconds": 1, "label": "Tax Category", "options": "Tax Category", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_40", "fieldtype": "Section Break", "hide_days": 1, - "hide_seconds": 1 + "hide_seconds": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes", @@ -905,7 +1052,9 @@ "label": "Sales Taxes and Charges", "oldfieldname": "other_charges", "oldfieldtype": "Table", - "options": "Sales Taxes and Charges" + "options": "Sales Taxes and Charges", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -913,7 +1062,9 @@ "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, - "label": "Tax Breakup" + "label": "Tax Breakup", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "other_charges_calculation", @@ -924,13 +1075,17 @@ "no_copy": 1, "oldfieldtype": "HTML", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_43", "fieldtype": "Section Break", "hide_days": 1, - "hide_seconds": 1 + "hide_seconds": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_total_taxes_and_charges", @@ -942,13 +1097,17 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_47", "fieldtype": "Column Break", "hide_days": 1, - "hide_seconds": 1 + "hide_seconds": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_taxes_and_charges", @@ -958,7 +1117,9 @@ "label": "Total Taxes and Charges", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -966,7 +1127,9 @@ "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, - "label": "Loyalty Points Redemption" + "label": "Loyalty Points Redemption", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "redeem_loyalty_points", @@ -976,7 +1139,9 @@ "hide_seconds": 1, "label": "Loyalty Points", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "redeem_loyalty_points", @@ -988,7 +1153,9 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -998,13 +1165,17 @@ "hide_seconds": 1, "label": "Redeem Loyalty Points", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_77", "fieldtype": "Column Break", "hide_days": 1, - "hide_seconds": 1 + "hide_seconds": 1, + "show_days": 1, + "show_seconds": 1 }, { "fetch_from": "customer.loyalty_program", @@ -1016,7 +1187,9 @@ "no_copy": 1, "options": "Loyalty Program", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "redeem_loyalty_points", @@ -1026,7 +1199,9 @@ "hide_seconds": 1, "label": "Redemption Account", "no_copy": 1, - "options": "Account" + "options": "Account", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "redeem_loyalty_points", @@ -1036,7 +1211,9 @@ "hide_seconds": 1, "label": "Redemption Cost Center", "no_copy": 1, - "options": "Cost Center" + "options": "Cost Center", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1045,7 +1222,9 @@ "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, - "label": "Additional Discount" + "label": "Additional Discount", + "show_days": 1, + "show_seconds": 1 }, { "default": "Grand Total", @@ -1055,7 +1234,9 @@ "hide_seconds": 1, "label": "Apply Additional Discount On", "options": "\nGrand Total\nNet Total", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_discount_amount", @@ -1065,13 +1246,17 @@ "label": "Additional Discount Amount (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_51", "fieldtype": "Column Break", "hide_days": 1, - "hide_seconds": 1 + "hide_seconds": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "additional_discount_percentage", @@ -1079,7 +1264,9 @@ "hide_days": 1, "hide_seconds": 1, "label": "Additional Discount Percentage", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "discount_amount", @@ -1088,7 +1275,9 @@ "hide_seconds": 1, "label": "Additional Discount Amount", "options": "currency", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "totals", @@ -1097,7 +1286,9 @@ "hide_seconds": 1, "oldfieldtype": "Section Break", "options": "fa fa-money", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_grand_total", @@ -1110,7 +1301,9 @@ "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -1122,7 +1315,9 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -1135,7 +1330,9 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "description": "In Words will be visible once you save the Sales Invoice.", @@ -1148,7 +1345,9 @@ "oldfieldname": "in_words", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break5", @@ -1157,6 +1356,8 @@ "hide_seconds": 1, "oldfieldtype": "Column Break", "print_hide": 1, + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -1171,7 +1372,9 @@ "oldfieldtype": "Currency", "options": "currency", "read_only": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -1183,7 +1386,9 @@ "no_copy": 1, "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -1196,7 +1401,9 @@ "oldfieldname": "rounded_total_export", "oldfieldtype": "Currency", "options": "currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "in_words", @@ -1208,7 +1415,9 @@ "oldfieldname": "in_words_export", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_advance", @@ -1220,7 +1429,9 @@ "oldfieldtype": "Currency", "options": "party_account_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "outstanding_amount", @@ -1233,7 +1444,9 @@ "oldfieldtype": "Currency", "options": "party_account_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1245,7 +1458,9 @@ "label": "Advance Payments", "oldfieldtype": "Section Break", "options": "fa fa-money", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -1253,7 +1468,9 @@ "fieldtype": "Check", "hide_days": 1, "hide_seconds": 1, - "label": "Allocate Advances Automatically (FIFO)" + "label": "Allocate Advances Automatically (FIFO)", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.allocate_advances_automatically", @@ -1262,7 +1479,9 @@ "hide_days": 1, "hide_seconds": 1, "label": "Get Advances Received", - "options": "set_advances" + "options": "set_advances", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "advances", @@ -1273,7 +1492,9 @@ "oldfieldname": "advance_adjustment_details", "oldfieldtype": "Table", "options": "Sales Invoice Advance", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1282,7 +1503,9 @@ "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, - "label": "Payment Terms" + "label": "Payment Terms", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:(!doc.is_pos && !doc.is_return)", @@ -1293,7 +1516,9 @@ "label": "Payment Terms Template", "no_copy": 1, "options": "Payment Terms Template", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:(!doc.is_pos && !doc.is_return)", @@ -1304,7 +1529,9 @@ "label": "Payment Schedule", "no_copy": 1, "options": "Payment Schedule", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.is_pos===1||(doc.advances && doc.advances.length>0)", @@ -1313,7 +1540,9 @@ "hide_days": 1, "hide_seconds": 1, "label": "Payments", - "options": "fa fa-money" + "options": "fa fa-money", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "is_pos", @@ -1326,7 +1555,9 @@ "oldfieldname": "cash_bank_account", "oldfieldtype": "Link", "options": "Account", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.is_pos===1", @@ -1336,13 +1567,17 @@ "hide_seconds": 1, "label": "Sales Invoice Payment", "options": "Sales Invoice Payment", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_84", "fieldtype": "Section Break", "hide_days": 1, - "hide_seconds": 1 + "hide_seconds": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_paid_amount", @@ -1353,13 +1588,17 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_86", "fieldtype": "Column Break", "hide_days": 1, - "hide_seconds": 1 + "hide_seconds": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval: doc.is_pos || doc.redeem_loyalty_points", @@ -1373,13 +1612,17 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_88", "fieldtype": "Section Break", "hide_days": 1, - "hide_seconds": 1 + "hide_seconds": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "is_pos", @@ -1391,13 +1634,17 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_90", "fieldtype": "Column Break", "hide_days": 1, - "hide_seconds": 1 + "hide_seconds": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "is_pos", @@ -1408,7 +1655,9 @@ "label": "Change Amount", "no_copy": 1, "options": "currency", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "is_pos", @@ -1418,7 +1667,9 @@ "hide_seconds": 1, "label": "Account for Change Amount", "options": "Account", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1429,6 +1680,8 @@ "hide_days": 1, "hide_seconds": 1, "label": "Write Off", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -1439,7 +1692,9 @@ "label": "Write Off Amount", "no_copy": 1, "options": "currency", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_write_off_amount", @@ -1450,7 +1705,9 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -1460,13 +1717,17 @@ "hide_days": 1, "hide_seconds": 1, "label": "Write Off Outstanding Amount", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_74", "fieldtype": "Column Break", "hide_days": 1, - "hide_seconds": 1 + "hide_seconds": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "write_off_account", @@ -1475,7 +1736,9 @@ "hide_seconds": 1, "label": "Write Off Account", "options": "Account", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "write_off_cost_center", @@ -1484,7 +1747,9 @@ "hide_seconds": 1, "label": "Write Off Cost Center", "options": "Cost Center", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1494,7 +1759,9 @@ "hide_days": 1, "hide_seconds": 1, "label": "Terms and Conditions", - "oldfieldtype": "Section Break" + "oldfieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "tc_name", @@ -1505,7 +1772,9 @@ "oldfieldname": "tc_name", "oldfieldtype": "Link", "options": "Terms and Conditions", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "terms", @@ -1514,7 +1783,9 @@ "hide_seconds": 1, "label": "Terms and Conditions Details", "oldfieldname": "terms", - "oldfieldtype": "Text Editor" + "oldfieldtype": "Text Editor", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1522,7 +1793,9 @@ "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, - "label": "Printing Settings" + "label": "Printing Settings", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1534,7 +1807,9 @@ "oldfieldname": "letter_head", "oldfieldtype": "Select", "options": "Letter Head", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1544,7 +1819,9 @@ "hide_days": 1, "hide_seconds": 1, "label": "Group same items", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "language", @@ -1553,13 +1830,17 @@ "hide_seconds": 1, "label": "Print Language", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_84", "fieldtype": "Column Break", "hide_days": 1, - "hide_seconds": 1 + "hide_seconds": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1573,7 +1854,9 @@ "oldfieldtype": "Link", "options": "Print Heading", "print_hide": 1, - "report_hide": 1 + "report_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1582,7 +1865,9 @@ "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, - "label": "More Information" + "label": "More Information", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "inter_company_invoice_reference", @@ -1591,7 +1876,9 @@ "hide_seconds": 1, "label": "Inter Company Invoice Reference", "options": "Purchase Invoice", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "customer_group", @@ -1601,7 +1888,9 @@ "hide_seconds": 1, "label": "Customer Group", "options": "Customer Group", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "campaign", @@ -1612,7 +1901,9 @@ "oldfieldname": "campaign", "oldfieldtype": "Link", "options": "Campaign", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -1622,13 +1913,17 @@ "hide_seconds": 1, "label": "Is Discounted", "no_copy": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_break23", "fieldtype": "Column Break", "hide_days": 1, "hide_seconds": 1, + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -1642,7 +1937,9 @@ "no_copy": 1, "options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "source", @@ -1653,7 +1950,9 @@ "oldfieldname": "source", "oldfieldtype": "Select", "options": "Lead Source", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1664,7 +1963,9 @@ "label": "Accounting Details", "oldfieldtype": "Section Break", "options": "fa fa-file-text", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "debit_to", @@ -1677,7 +1978,9 @@ "options": "Account", "print_hide": 1, "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "party_account_currency", @@ -1689,7 +1992,9 @@ "no_copy": 1, "options": "Currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "No", @@ -1701,7 +2006,9 @@ "oldfieldname": "is_opening", "oldfieldtype": "Select", "options": "No\nYes", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "c_form_applicable", @@ -1711,7 +2018,9 @@ "label": "C-Form Applicable", "no_copy": 1, "options": "No\nYes", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "c_form_no", @@ -1722,7 +2031,9 @@ "no_copy": 1, "options": "C-Form", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break8", @@ -1730,7 +2041,9 @@ "hide_days": 1, "hide_seconds": 1, "oldfieldtype": "Column Break", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "remarks", @@ -1741,7 +2054,9 @@ "no_copy": 1, "oldfieldname": "remarks", "oldfieldtype": "Text", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1753,7 +2068,9 @@ "label": "Commission", "oldfieldtype": "Section Break", "options": "fa fa-group", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "sales_partner", @@ -1764,7 +2081,9 @@ "oldfieldname": "sales_partner", "oldfieldtype": "Link", "options": "Sales Partner", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break10", @@ -1773,6 +2092,8 @@ "hide_seconds": 1, "oldfieldtype": "Column Break", "print_hide": 1, + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -1783,7 +2104,9 @@ "label": "Commission Rate (%)", "oldfieldname": "commission_rate", "oldfieldtype": "Currency", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_commission", @@ -1794,7 +2117,9 @@ "oldfieldname": "total_commission", "oldfieldtype": "Currency", "options": "Company:company:default_currency", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1804,7 +2129,9 @@ "hide_days": 1, "hide_seconds": 1, "label": "Sales Team", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1816,7 +2143,9 @@ "oldfieldname": "sales_team", "oldfieldtype": "Table", "options": "Sales Team", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1824,7 +2153,9 @@ "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, - "label": "Subscription Section" + "label": "Subscription Section", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1834,7 +2165,9 @@ "hide_seconds": 1, "label": "From Date", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1844,13 +2177,17 @@ "hide_seconds": 1, "label": "To Date", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_140", "fieldtype": "Column Break", "hide_days": 1, - "hide_seconds": 1 + "hide_seconds": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1862,7 +2199,9 @@ "no_copy": 1, "options": "Auto Repeat", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1871,7 +2210,9 @@ "fieldtype": "Button", "hide_days": 1, "hide_seconds": 1, - "label": "Update Auto Repeat Reference" + "label": "Update Auto Repeat Reference", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "against_income_account", @@ -1884,7 +2225,9 @@ "oldfieldname": "against_income_account", "oldfieldtype": "Small Text", "print_hide": 1, - "report_hide": 1 + "report_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1892,13 +2235,17 @@ "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, - "label": "Accounting Dimensions" + "label": "Accounting Dimensions", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "dimension_col_break", "fieldtype": "Column Break", "hide_days": 1, - "hide_seconds": 1 + "hide_seconds": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -1906,7 +2253,9 @@ "fieldname": "is_consolidated", "fieldtype": "Check", "label": "Is Consolidated", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -1916,14 +2265,18 @@ "hide_days": 1, "hide_seconds": 1, "label": "Is Internal Customer", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fetch_from": "company.tax_id", "fieldname": "company_tax_id", "fieldtype": "Data", "label": "Company Tax ID", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.is_internal_customer", @@ -1931,7 +2284,9 @@ "fieldname": "unrealized_profit_loss_account", "fieldtype": "Link", "label": "Unrealized Profit / Loss Account", - "options": "Account" + "options": "Account", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.is_internal_customer", @@ -1941,31 +2296,51 @@ "fieldtype": "Link", "label": "Represents Company", "options": "Company", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_55", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval: doc.is_internal_customer && doc.update_stock", "fieldname": "set_target_warehouse", "fieldtype": "Link", "label": "Set Target Warehouse", - "options": "Warehouse" + "options": "Warehouse", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "is_debit_note", "fieldtype": "Check", - "label": "Is Debit Note" + "label": "Is Debit Note", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "depends_on": "grand_total", "fieldname": "disable_rounded_total", "fieldtype": "Check", - "label": "Disable Rounded Total" + "label": "Disable Rounded Total", + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "0", + "fieldname": "ignore_default_payment_terms_template", + "fieldtype": "Check", + "hidden": 1, + "label": "Ignore Default Payment Terms Template", + "read_only": 1, + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-file-text", @@ -1978,7 +2353,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2021-05-20 22:48:33.988881", + "modified": "2021-08-06 23:02:20.445127", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index b97dc401e6..329f9a97b8 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -8,7 +8,7 @@ from frappe import _, msgprint, scrub from frappe.core.doctype.user_permission.user_permission import get_permitted_documents from frappe.model.utils import get_fetch_values from frappe.utils import (add_days, getdate, formatdate, date_diff, - add_years, get_timestamp, nowdate, flt, cstr, add_months, get_last_day) + add_years, get_timestamp, nowdate, flt, cstr, add_months, get_last_day, cint) from frappe.contacts.doctype.address.address import (get_address_display, get_default_address, get_company_address) from frappe.contacts.doctype.contact.contact import get_contact_details @@ -58,7 +58,7 @@ def _get_party_details(party=None, account=None, party_type="Customer", company= customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category, billing_address=party_address, shipping_address=shipping_address) - if fetch_payment_terms_template: + if cint(fetch_payment_terms_template): party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company) if not party_details.get("currency"): diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index f68d81909a..94684bff1c 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -447,10 +447,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions target.flags.ignore_permissions = ignore_permissions set_missing_values(source, target) #Get the advance paid Journal Entries in Purchase Invoice Advance - if target.get("allocate_advances_automatically"): target.set_advances() + target.set_payment_schedule() + def update_item(obj, target, source_parent): target.amount = flt(obj.amount) - flt(obj.billed_amt) target.base_amount = target.amount * flt(source_parent.conversion_rate) @@ -492,10 +493,6 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions doc = get_mapped_doc("Purchase Order", source_name, fields, target_doc, postprocess, ignore_permissions=ignore_permissions) - automatically_fetch_payment_terms = cint(frappe.db.get_single_value('Accounts Settings', 'automatically_fetch_payment_terms')) - if automatically_fetch_payment_terms: - doc.set_payment_schedule() - return doc @frappe.whitelist() diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 78e4ad31ad..00f40cda2c 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1091,6 +1091,8 @@ class AccountsController(TransactionBase): if self.doctype in ("Sales Invoice", "Purchase Invoice"): base_grand_total = base_grand_total - flt(self.base_write_off_amount) grand_total = grand_total - flt(self.write_off_amount) + po_or_so, doctype, fieldname = self.get_order_details() + automatically_fetch_payment_terms = cint(frappe.db.get_single_value('Accounts Settings', 'automatically_fetch_payment_terms')) if self.get("total_advance"): if party_account_currency == self.company_currency: @@ -1101,28 +1103,25 @@ class AccountsController(TransactionBase): base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total")) if not self.get("payment_schedule"): - if self.doctype in ["Sales Invoice", "Purchase Invoice"] and not self.get("payment_terms_template"): - po_or_so, doctype, fieldname = self.get_order_details() - - if self.get("payment_terms_template"): + if self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype): + self.fetch_payment_terms_from_order(po_or_so, doctype) + if self.get('payment_terms_template'): + self.ignore_default_payment_terms_template = 1 + elif self.get("payment_terms_template"): data = get_payment_terms(self.payment_terms_template, posting_date, grand_total, base_grand_total) for item in data: self.append("payment_schedule", item) - - elif self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype): - self.fetch_payment_terms_from_order(po_or_so, doctype) - elif self.doctype not in ["Purchase Receipt"]: data = dict(due_date=due_date, invoice_portion=100, payment_amount=grand_total, base_payment_amount=base_grand_total) self.append("payment_schedule", data) - else: - for d in self.get("payment_schedule"): - if d.invoice_portion: - d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount')) - d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('base_payment_amount')) - d.outstanding = d.payment_amount - elif not d.invoice_portion: - d.base_payment_amount = flt(base_grand_total * self.get("conversion_rate"), d.precision('base_payment_amount')) + + for d in self.get("payment_schedule"): + if d.invoice_portion: + d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount')) + d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('base_payment_amount')) + d.outstanding = d.payment_amount + elif not d.invoice_portion: + d.base_payment_amount = flt(base_grand_total * self.get("conversion_rate"), d.precision('base_payment_amount')) def get_order_details(self): diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 6a550e0e97..974ade3584 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -72,7 +72,8 @@ class BuyingController(StockController, Subcontracting): # set contact and address details for supplier, if they are not mentioned if getattr(self, "supplier", None): self.update_if_missing(get_party_details(self.supplier, party_type="Supplier", ignore_permissions=self.flags.ignore_permissions, - doctype=self.doctype, company=self.company, party_address=self.supplier_address, shipping_address=self.get('shipping_address'))) + doctype=self.doctype, company=self.company, party_address=self.supplier_address, shipping_address=self.get('shipping_address'), + fetch_payment_terms_template= not self.get('ignore_default_payment_terms_template'))) self.set_missing_item_details(for_validate) diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index a79eadc761..54df0d63cb 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -76,6 +76,7 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { if (args) { args.posting_date = frm.doc.posting_date || frm.doc.transaction_date; + args.fetch_payment_terms_template = cint(!frm.doc.ignore_default_payment_terms_template) } } if (!args || !args.party) return; diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index b05cc7875c..d071f01047 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -598,6 +598,7 @@ def make_purchase_invoice(source_name, target_doc=None): doc.run_method("onload") doc.run_method("set_missing_values") doc.run_method("calculate_taxes_and_totals") + doc.set_payment_schedule() def update_item(source_doc, target_doc, source_parent): target_doc.qty, returned_qty = get_pending_qty(source_doc) @@ -654,10 +655,6 @@ def make_purchase_invoice(source_name, target_doc=None): } }, target_doc, set_missing_values) - automatically_fetch_payment_terms = cint(frappe.db.get_single_value('Accounts Settings', 'automatically_fetch_payment_terms')) - if automatically_fetch_payment_terms: - doc.set_payment_schedule() - return doclist def get_invoiced_qty_map(purchase_receipt): From a27ef14db63b3492c48b0c7352e01df98fd18d5d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 7 Aug 2021 00:12:57 +0530 Subject: [PATCH 114/147] fix: Override template only if setting is enabled --- erpnext/controllers/accounts_controller.py | 3 ++- erpnext/public/js/utils/party.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 405216bf6f..d967100806 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1108,7 +1108,8 @@ class AccountsController(TransactionBase): base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total")) if not self.get("payment_schedule"): - if self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype): + if self.doctype in ["Sales Invoice", "Purchase Invoice"] and automatically_fetch_payment_terms \ + and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype): self.fetch_payment_terms_from_order(po_or_so, doctype) if self.get('payment_terms_template'): self.ignore_default_payment_terms_template = 1 diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index 54df0d63cb..4d432e3d5c 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -76,7 +76,7 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { if (args) { args.posting_date = frm.doc.posting_date || frm.doc.transaction_date; - args.fetch_payment_terms_template = cint(!frm.doc.ignore_default_payment_terms_template) + args.fetch_payment_terms_template = cint(!frm.doc.ignore_default_payment_terms_template); } } if (!args || !args.party) return; From 25d131a39ffa7624938ccb8bf4742913bf376c52 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 7 Aug 2021 17:39:40 +0530 Subject: [PATCH 115/147] test: Improve test case for not coping payment terms --- .../buying/doctype/purchase_order/purchase_order.py | 1 + .../doctype/purchase_order/test_purchase_order.py | 12 ++++++++---- erpnext/selling/doctype/sales_order/sales_order.py | 1 + 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 94684bff1c..a0b1e073cc 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -471,6 +471,7 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions "party_account_currency": "party_account_currency", "supplier_warehouse":"supplier_warehouse" }, + "field_no_map" : ["payment_terms_template"], "validation": { "docstatus": ["=", 1], } diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 0db54e4206..d06f9d2027 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -633,13 +633,17 @@ class TestPurchaseOrder(unittest.TestCase): raise Exception def test_terms_are_not_copied_if_automatically_fetch_payment_terms_is_unchecked(self): - po = create_purchase_order() - - self.assertTrue(po.get('payment_schedule')) + po = create_purchase_order(do_not_save=1) + po.payment_terms_template = '_Test Payment Term Template' + po.save() + po.submit() + company = frappe.get_doc('Company', '_Test Company', 'payment_terms', '_Test Payment Term Template 1') pi = make_pi_from_po(po.name) + pi.save() - self.assertFalse(pi.get('payment_schedule')) + self.assertEqual(pi.get('payment_terms_template'), '_Test Payment Term Template 1') + frappe.db.set_value('Company', '_Test Company', 'payment_terms', '') def test_terms_copied(self): po = create_purchase_order(do_not_save=1) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 2b9d516e21..bba54018ae 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -670,6 +670,7 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): "party_account_currency": "party_account_currency", "payment_terms_template": "payment_terms_template" }, + "field_no_map": ["payment_terms_template"], "validation": { "docstatus": ["=", 1] } From 5ace2767afac395e94991d1563d03eff1f53e9e9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 8 Aug 2021 19:17:38 +0530 Subject: [PATCH 116/147] test: Fix test cases for payment terms fetch --- .../buying/doctype/purchase_order/test_purchase_order.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index d06f9d2027..d668c76b6b 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -484,6 +484,9 @@ class TestPurchaseOrder(unittest.TestCase): def test_make_purchase_invoice_with_terms(self): + from erpnext.selling.doctype.sales_order.test_sales_order import automatically_fetch_payment_terms, compare_payment_schedules + + automatically_fetch_payment_terms() po = create_purchase_order(do_not_save=True) self.assertRaises(frappe.ValidationError, make_pi_from_po, po.name) @@ -509,6 +512,7 @@ class TestPurchaseOrder(unittest.TestCase): self.assertEqual(getdate(pi.payment_schedule[0].due_date), getdate(po.transaction_date)) self.assertEqual(pi.payment_schedule[1].payment_amount, 2500.0) self.assertEqual(getdate(pi.payment_schedule[1].due_date), add_days(getdate(po.transaction_date), 30)) + automatically_fetch_payment_terms(enable=0) def test_subcontracting(self): po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes") @@ -638,7 +642,7 @@ class TestPurchaseOrder(unittest.TestCase): po.save() po.submit() - company = frappe.get_doc('Company', '_Test Company', 'payment_terms', '_Test Payment Term Template 1') + frappe.db.set_value('Company', '_Test Company', 'payment_terms', '_Test Payment Term Template 1') pi = make_pi_from_po(po.name) pi.save() @@ -992,7 +996,7 @@ class TestPurchaseOrder(unittest.TestCase): # self.assertEqual(po.payment_terms_template, pi.payment_terms_template) compare_payment_schedules(self, po, pi) - automatically_fetch_payment_terms(enable=0) + automatically_fetch_payment_terms(enable=0) def make_pr_against_po(po, received_qty=0): pr = make_purchase_receipt(po) From a8166c06c7e471cfcf67d4ec6cacdc2d67e12485 Mon Sep 17 00:00:00 2001 From: Marica Date: Mon, 9 Aug 2021 12:24:04 +0530 Subject: [PATCH 117/147] fix: Faulty Gl Entry for Asset LCVs (#26803) * fix: Faulty Gl Entry for Asset LCVs - Both Gl entries were crediting in their respective accounts - Asset Account must be debited into * fix: Use keyword arguments instead of positional for better readability * chore: Test for LCV for draft asset created via Purchase Receipt --- .../test_landed_cost_voucher.py | 35 +++- .../purchase_receipt/purchase_receipt.py | 151 +++++++++++++++--- 2 files changed, 159 insertions(+), 27 deletions(-) diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index 32b08f60c4..128a2ab62f 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -11,6 +11,7 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \ from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.accounts.doctype.account.test_account import create_account +from erpnext.assets.doctype.asset.test_asset import create_asset_category, create_fixed_asset_item class TestLandedCostVoucher(unittest.TestCase): def test_landed_cost_voucher(self): @@ -250,6 +251,38 @@ class TestLandedCostVoucher(unittest.TestCase): self.assertEqual(entry.credit, amounts[0]) self.assertEqual(entry.credit_in_account_currency, amounts[1]) + def test_asset_lcv(self): + "Check if LCV for an Asset updates the Assets Gross Purchase Amount correctly." + if not frappe.db.exists("Asset Category", "Computers"): + create_asset_category() + + if not frappe.db.exists("Item", "Macbook Pro"): + create_fixed_asset_item() + + pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=50000) + + # check if draft asset was created + assets = frappe.db.get_all('Asset', filters={'purchase_receipt': pr.name}) + self.assertEqual(len(assets), 1) + + frappe.db.set_value("Company", pr.company, "capital_work_in_progress_account", "CWIP Account - _TC") + lcv = make_landed_cost_voucher( + company = pr.company, + receipt_document_type = "Purchase Receipt", + receipt_document=pr.name, + charges=80, + expense_account="Expenses Included In Valuation - _TC") + + lcv.save() + lcv.submit() + + # lcv updates amount in draft asset + self.assertEqual(frappe.db.get_value("Asset", assets[0].name, "gross_purchase_amount"), 50080) + + # tear down + lcv.cancel() + pr.cancel() + def make_landed_cost_voucher(** args): args = frappe._dict(args) ref_doc = frappe.get_doc(args.receipt_document_type, args.receipt_document) @@ -268,7 +301,7 @@ def make_landed_cost_voucher(** args): lcv.set("taxes", [{ "description": "Shipping Charges", - "expense_account": "Expenses Included In Valuation - TCP1", + "expense_account": args.expense_account or "Expenses Included In Valuation - TCP1", "amount": args.charges }]) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 26ea11e01d..96e14ef759 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -286,8 +286,16 @@ class PurchaseReceipt(BuyingController): and warehouse_account_name == supplier_warehouse_account: continue - self.add_gl_entry(gl_entries, warehouse_account_name, d.cost_center, stock_value_diff, 0.0, remarks, - stock_rbnb, account_currency=warehouse_account_currency, item=d) + self.add_gl_entry( + gl_entries=gl_entries, + account=warehouse_account_name, + cost_center=d.cost_center, + debit=stock_value_diff, + credit=0.0, + remarks=remarks, + against_account=stock_rbnb, + account_currency=warehouse_account_currency, + item=d) # GL Entry for from warehouse or Stock Received but not billed # Intentionally passed negative debit amount to avoid incorrect GL Entry validation @@ -300,9 +308,17 @@ class PurchaseReceipt(BuyingController): account = warehouse_account[d.from_warehouse]['account'] \ if d.from_warehouse else stock_rbnb - self.add_gl_entry(gl_entries, account, d.cost_center, - -1 * flt(d.base_net_amount, d.precision("base_net_amount")), 0.0, remarks, warehouse_account_name, - debit_in_account_currency=-1 * credit_amount, account_currency=credit_currency, item=d) + self.add_gl_entry( + gl_entries=gl_entries, + account=account, + cost_center=d.cost_center, + debit=-1 * flt(d.base_net_amount, d.precision("base_net_amount")), + credit=0.0, + remarks=remarks, + against_account=warehouse_account_name, + debit_in_account_currency=-1 * credit_amount, + account_currency=credit_currency, + item=d) # Amount added through landed-cos-voucher if d.landed_cost_voucher_amount and landed_cost_entries: @@ -311,14 +327,31 @@ class PurchaseReceipt(BuyingController): credit_amount = (flt(amount["base_amount"]) if (amount["base_amount"] or account_currency!=self.company_currency) else flt(amount["amount"])) - self.add_gl_entry(gl_entries, account, d.cost_center, 0.0, credit_amount, remarks, - warehouse_account_name, credit_in_account_currency=flt(amount["amount"]), - account_currency=account_currency, project=d.project, item=d) + self.add_gl_entry( + gl_entries=gl_entries, + account=account, + cost_center=d.cost_center, + debit=0.0, + credit=credit_amount, + remarks=remarks, + against_account=warehouse_account_name, + credit_in_account_currency=flt(amount["amount"]), + account_currency=account_currency, + project=d.project, + item=d) # sub-contracting warehouse if flt(d.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse): - self.add_gl_entry(gl_entries, supplier_warehouse_account, d.cost_center, 0.0, flt(d.rm_supp_cost), - remarks, warehouse_account_name, account_currency=supplier_warehouse_account_currency, item=d) + self.add_gl_entry( + gl_entries=gl_entries, + account=supplier_warehouse_account, + cost_center=d.cost_center, + debit=0.0, + credit=flt(d.rm_supp_cost), + remarks=remarks, + against_account=warehouse_account_name, + account_currency=supplier_warehouse_account_currency, + item=d) # divisional loss adjustment valuation_amount_as_per_doc = flt(d.base_net_amount, d.precision("base_net_amount")) + \ @@ -335,8 +368,17 @@ class PurchaseReceipt(BuyingController): cost_center = d.cost_center or frappe.get_cached_value("Company", self.company, "cost_center") - self.add_gl_entry(gl_entries, loss_account, cost_center, divisional_loss, 0.0, remarks, - warehouse_account_name, account_currency=credit_currency, project=d.project, item=d) + self.add_gl_entry( + gl_entries=gl_entries, + account=loss_account, + cost_center=cost_center, + debit=divisional_loss, + credit=0.0, + remarks=remarks, + against_account=warehouse_account_name, + account_currency=credit_currency, + project=d.project, + item=d) elif d.warehouse not in warehouse_with_no_account or \ d.rejected_warehouse not in warehouse_with_no_account: @@ -347,12 +389,30 @@ class PurchaseReceipt(BuyingController): debit_currency = get_account_currency(d.expense_account) remarks = self.get("remarks") or _("Accounting Entry for Service") - self.add_gl_entry(gl_entries, service_received_but_not_billed_account, d.cost_center, 0.0, d.amount, - remarks, d.expense_account, account_currency=credit_currency, project=d.project, + self.add_gl_entry( + gl_entries=gl_entries, + account=service_received_but_not_billed_account, + cost_center=d.cost_center, + debit=0.0, + credit=d.amount, + remarks=remarks, + against_account=d.expense_account, + account_currency=credit_currency, + project=d.project, voucher_detail_no=d.name, item=d) - self.add_gl_entry(gl_entries, d.expense_account, d.cost_center, d.amount, 0.0, remarks, service_received_but_not_billed_account, - account_currency = debit_currency, project=d.project, voucher_detail_no=d.name, item=d) + self.add_gl_entry( + gl_entries=gl_entries, + account=d.expense_account, + cost_center=d.cost_center, + debit=d.amount, + credit=0.0, + remarks=remarks, + against_account=service_received_but_not_billed_account, + account_currency = debit_currency, + project=d.project, + voucher_detail_no=d.name, + item=d) if warehouse_with_no_account: frappe.msgprint(_("No accounting entries for the following warehouses") + ": \n" + @@ -402,8 +462,15 @@ class PurchaseReceipt(BuyingController): applicable_amount = negative_expense_to_be_booked * (valuation_tax[tax.name] / total_valuation_amount) amount_including_divisional_loss -= applicable_amount - self.add_gl_entry(gl_entries, account, tax.cost_center, 0.0, applicable_amount, self.remarks or _("Accounting Entry for Stock"), - against_account, item=tax) + self.add_gl_entry( + gl_entries=gl_entries, + account=account, + cost_center=tax.cost_center, + debit=0.0, + credit=applicable_amount, + remarks=self.remarks or _("Accounting Entry for Stock"), + against_account=against_account, + item=tax) i += 1 @@ -456,15 +523,31 @@ class PurchaseReceipt(BuyingController): # debit cwip account debit_in_account_currency = (base_asset_amount if cwip_account_currency == self.company_currency else asset_amount) - self.add_gl_entry(gl_entries, cwip_account, item.cost_center, base_asset_amount, 0.0, remarks, - arbnb_account, debit_in_account_currency=debit_in_account_currency, item=item) + self.add_gl_entry( + gl_entries=gl_entries, + account=cwip_account, + cost_center=item.cost_center, + debit=base_asset_amount, + credit=0.0, + remarks=remarks, + against_account=arbnb_account, + debit_in_account_currency=debit_in_account_currency, + item=item) asset_rbnb_currency = get_account_currency(arbnb_account) # credit arbnb account credit_in_account_currency = (base_asset_amount if asset_rbnb_currency == self.company_currency else asset_amount) - self.add_gl_entry(gl_entries, arbnb_account, item.cost_center, 0.0, base_asset_amount, remarks, - cwip_account, credit_in_account_currency=credit_in_account_currency, item=item) + self.add_gl_entry( + gl_entries=gl_entries, + account=arbnb_account, + cost_center=item.cost_center, + debit=0.0, + credit=base_asset_amount, + remarks=remarks, + against_account=cwip_account, + credit_in_account_currency=credit_in_account_currency, + item=item) def add_lcv_gl_entries(self, item, gl_entries): expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation") @@ -477,11 +560,27 @@ class PurchaseReceipt(BuyingController): remarks = self.get("remarks") or _("Accounting Entry for Stock") - self.add_gl_entry(gl_entries, expenses_included_in_asset_valuation, item.cost_center, 0.0, flt(item.landed_cost_voucher_amount), - remarks, asset_account, project=item.project, item=item) + self.add_gl_entry( + gl_entries=gl_entries, + account=expenses_included_in_asset_valuation, + cost_center=item.cost_center, + debit=0.0, + credit=flt(item.landed_cost_voucher_amount), + remarks=remarks, + against_account=asset_account, + project=item.project, + item=item) - self.add_gl_entry(gl_entries, asset_account, item.cost_center, 0.0, flt(item.landed_cost_voucher_amount), - remarks, expenses_included_in_asset_valuation, project=item.project, item=item) + self.add_gl_entry( + gl_entries=gl_entries, + account=asset_account, + cost_center=item.cost_center, + debit=flt(item.landed_cost_voucher_amount), + credit=0.0, + remarks=remarks, + against_account=expenses_included_in_asset_valuation, + project=item.project, + item=item) def update_assets(self, item, valuation_rate): assets = frappe.db.get_all('Asset', From b73bb3e5013e08276f7af4f83321cc600b081ad0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 5 Aug 2021 22:38:00 +0530 Subject: [PATCH 118/147] fix: Add discount account handling in Purchase Invoice --- .../purchase_invoice/purchase_invoice.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index feae213d92..d0f4e96541 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -519,6 +519,10 @@ class PurchaseInvoice(BuyingController): if d.category in ('Valuation', 'Total and Valuation') and flt(d.base_tax_amount_after_discount_amount)] + exchange_rate_map, net_rate_map = get_purchase_document_details(self) + + enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) + for item in self.get("items"): if flt(item.base_net_amount): account_currency = get_account_currency(item.expense_account) @@ -609,11 +613,7 @@ class PurchaseInvoice(BuyingController): if (not item.enable_deferred_expense or self.is_return) else item.deferred_expense_account) if not item.is_fixed_asset: - if frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting'): - amount = flt(item.base_amount, item.precision("base_amount")) - else: - amount = flt(item.base_net_amount, item.precision("base_net_amount")) - + dummy, amount = self.get_amount_and_base_amount(item, enable_discount_accounting) else: amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount")) @@ -827,8 +827,11 @@ class PurchaseInvoice(BuyingController): def make_tax_gl_entries(self, gl_entries): # tax table gl entries valuation_tax = {} + enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) + for tax in self.get("taxes"): - if tax.category in ("Total", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount): + amount, base_amount = self.get_tax_amounts(tax, enable_discount_accounting) + if tax.category in ("Total", "Valuation and Total") and flt(base_amount): account_currency = get_account_currency(tax.account_head) dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" @@ -837,21 +840,21 @@ class PurchaseInvoice(BuyingController): self.get_gl_dict({ "account": tax.account_head, "against": self.supplier, - dr_or_cr: tax.base_tax_amount_after_discount_amount, - dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount \ + dr_or_cr: base_amount, + dr_or_cr + "_in_account_currency": base_amount \ if account_currency==self.company_currency \ - else tax.tax_amount_after_discount_amount, + else amount, "cost_center": tax.cost_center }, account_currency, item=tax) ) # accumulate valuation tax - if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount) \ + if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(base_amount) \ and not self.is_internal_transfer(): if self.auto_accounting_for_stock and not tax.cost_center: frappe.throw(_("Cost Center is required in row {0} in Taxes table for type {1}").format(tax.idx, _(tax.category))) valuation_tax.setdefault(tax.name, 0) valuation_tax[tax.name] += \ - (tax.add_deduct_tax == "Add" and 1 or -1) * flt(tax.base_tax_amount_after_discount_amount) + (tax.add_deduct_tax == "Add" and 1 or -1) * flt(base_amount) if self.is_opening == "No" and self.negative_expense_to_be_booked and valuation_tax: # credit valuation tax amount in "Expenses Included In Valuation" From 428ccebad95a82ca0a38c35924fe5756ac35311b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 5 Aug 2021 22:38:24 +0530 Subject: [PATCH 119/147] test: Update test cases for discount accounting --- .../purchase_invoice/test_purchase_invoice.py | 27 ++++++++++--------- .../sales_invoice/test_sales_invoice.py | 24 +++++++++-------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index d60f0a40b1..f0f5a58d15 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -235,39 +235,41 @@ class TestPurchaseInvoice(unittest.TestCase): discount_account = create_account(account_name="Discount Account", parent_account="Indirect Expenses - _TC", company="_Test Company") - pi = make_purchase_invoice(discount_account=discount_account, discount_amount=100) + pi = make_purchase_invoice(discount_account=discount_account, rate=45) expected_gle = [ - ["_Test Account Cost for Goods Sold - _TC", 350.0, 0.0, nowdate()], - ["Creditors - _TC", 0.0, 250.0, nowdate()], - ["Discount Account - _TC", 0.0, 100.0, nowdate()] + ["_Test Account Cost for Goods Sold - _TC", 250.0, 0.0, nowdate()], + ["Creditors - _TC", 0.0, 225.0, nowdate()], + ["Discount Account - _TC", 0.0, 25.0, nowdate()] ] check_gl_entries(self, pi.name, expected_gle, nowdate()) + enable_discount_accounting(enable=0) def test_additional_discount_for_purchase_invoice_with_discount_accounting_enabled(self): enable_discount_accounting() additional_discount_account = create_account(account_name="Discount Account", parent_account="Indirect Expenses - _TC", company="_Test Company") - pi = make_purchase_invoice(qty=1, rate=100, do_not_save=1, parent_cost_center="Main - _TC") + pi = make_purchase_invoice(do_not_save=1, parent_cost_center="Main - _TC") pi.apply_discount_on = "Grand Total" pi.additional_discount_account = additional_discount_account - pi.additional_discount_percentage = 20 + pi.additional_discount_percentage = 10 + pi.disable_rounded_total = 1 pi.append("taxes", { - "charge_type": "Actual", + "charge_type": "On Net Total", "account_head": "_Test Account VAT - _TC", "cost_center": "Main - _TC", "description": "Test", - "tax_amount": 20 + "rate": 10 }) pi.submit() expected_gle = [ - ["_Test Account Cost for Goods Sold - _TC", 100.0, 0.0, nowdate()], - ["_Test Account VAT - _TC", 20.0, 0.0, nowdate()], - ["Creditors - _TC", 0.0, 96.0, nowdate()], - ["Discount Account - _TC", 0.0, 24.0, nowdate()] + ["_Test Account Cost for Goods Sold - _TC", 250.0, 0.0, nowdate()], + ["_Test Account VAT - _TC", 25.0, 0.0, nowdate()], + ["Creditors - _TC", 0.0, 247.5, nowdate()], + ["Discount Account - _TC", 0.0, 27.5, nowdate()] ] check_gl_entries(self, pi.name, expected_gle, nowdate()) @@ -1260,6 +1262,7 @@ def make_purchase_invoice(**args): "received_qty": args.received_qty or 0, "rejected_qty": args.rejected_qty or 0, "rate": args.rate or 50, + "price_list_rate": args.price_list_rate or 50, "expense_account": args.expense_account or '_Test Account Cost for Goods Sold - _TC', "discount_account": args.discount_account or None, "discount_amount": args.discount_amount or 0, diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index af317db712..12f03be288 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1993,15 +1993,16 @@ class TestSalesInvoice(unittest.TestCase): discount_account = create_account(account_name="Discount Account", parent_account="Indirect Expenses - _TC", company="_Test Company") - si = create_sales_invoice(discount_account=discount_account, discount_amount=100) + si = create_sales_invoice(discount_account=discount_account, discount_percentage=10, rate=90) expected_gle = [ - ["Debtors - _TC", 100.0, 0.0, nowdate()], - ["Discount Account - _TC", 100.0, 0.0, nowdate()], - ["Sales - _TC", 0.0, 200.0, nowdate()] + ["Debtors - _TC", 90.0, 0.0, nowdate()], + ["Discount Account - _TC", 10.0, 0.0, nowdate()], + ["Sales - _TC", 0.0, 100.0, nowdate()] ] check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) + enable_discount_accounting(enable=0) def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self): from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import enable_discount_accounting @@ -2010,28 +2011,28 @@ class TestSalesInvoice(unittest.TestCase): additional_discount_account = create_account(account_name="Discount Account", parent_account="Indirect Expenses - _TC", company="_Test Company") - si = create_sales_invoice(rate=100, parent_cost_center='Main - _TC', do_not_save=1) + si = create_sales_invoice(parent_cost_center='Main - _TC', do_not_save=1) si.apply_discount_on = "Grand Total" si.additional_discount_account = additional_discount_account si.additional_discount_percentage = 20 si.append("taxes", { - "charge_type": "Actual", + "charge_type": "On Net Total", "account_head": "_Test Account VAT - _TC", "cost_center": "Main - _TC", "description": "Test", - "rate": 0, - "tax_amount": 20 + "rate": 10 }) si.submit() expected_gle = [ - ["_Test Account VAT - _TC", 0.0, 20.0, nowdate()], - ["Debtors - _TC", 96.0, 0.0, nowdate()], - ["Discount Account - _TC", 24.0, 0.0, nowdate()], + ["_Test Account VAT - _TC", 0.0, 10.0, nowdate()], + ["Debtors - _TC", 88, 0.0, nowdate()], + ["Discount Account - _TC", 22.0, 0.0, nowdate()], ["Sales - _TC", 0.0, 100.0, nowdate()] ] check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) + enable_discount_accounting(enable=0) def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() @@ -2238,6 +2239,7 @@ def create_sales_invoice(**args): "uom": args.uom or "Nos", "stock_uom": args.uom or "Nos", "rate": args.rate if args.get("rate") is not None else 100, + "price_list_rate": args.price_list_rate if args.get("price_list_rate") is not None else 100, "income_account": args.income_account or "Sales - _TC", "expense_account": args.expense_account or "Cost of Goods Sold - _TC", "discount_account": args.discount_account or None, From 797170e9138310da53ef33ecb2f8397f8b292882 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 10 Aug 2021 11:02:21 +0530 Subject: [PATCH 120/147] fix: Linting issues --- .../accounts/doctype/purchase_invoice/purchase_invoice.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index d0f4e96541..d7d9a3886a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -519,8 +519,6 @@ class PurchaseInvoice(BuyingController): if d.category in ('Valuation', 'Total and Valuation') and flt(d.base_tax_amount_after_discount_amount)] - exchange_rate_map, net_rate_map = get_purchase_document_details(self) - enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) for item in self.get("items"): @@ -841,8 +839,8 @@ class PurchaseInvoice(BuyingController): "account": tax.account_head, "against": self.supplier, dr_or_cr: base_amount, - dr_or_cr + "_in_account_currency": base_amount \ - if account_currency==self.company_currency \ + dr_or_cr + "_in_account_currency": base_amount + if account_currency==self.company_currency else amount, "cost_center": tax.cost_center }, account_currency, item=tax) From 415519af15244b985a79d91691f7f2ba9a9b691c Mon Sep 17 00:00:00 2001 From: noahjacob Date: Fri, 7 May 2021 13:00:46 +0530 Subject: [PATCH 121/147] feat: added multi-select fields to create multiple pricing rules. --- .../doctype/campaign_item/__init__.py | 0 .../doctype/campaign_item/campaign_item.json | 31 + .../doctype/campaign_item/campaign_item.py | 10 + .../doctype/customer_group_item/__init__.py | 0 .../customer_group_item.json | 31 + .../customer_group_item.py | 10 + .../doctype/customer_item/__init__.py | 0 .../doctype/customer_item/customer_item.json | 31 + .../doctype/customer_item/customer_item.py | 10 + .../promotional_scheme.json | 1226 ++--------------- .../promotional_scheme/promotional_scheme.py | 97 +- .../doctype/sales_partner_item/__init__.py | 0 .../sales_partner_item.json | 31 + .../sales_partner_item/sales_partner_item.py | 10 + .../doctype/supplier_group_item/__init__.py | 0 .../supplier_group_item.json | 31 + .../supplier_group_item.py | 10 + .../doctype/supplier_item/__init__.py | 0 .../doctype/supplier_item/supplier_item.json | 31 + .../doctype/supplier_item/supplier_item.py | 10 + .../doctype/territory_item/__init__.py | 0 .../territory_item/territory_item.json | 31 + .../doctype/territory_item/territory_item.py | 10 + 23 files changed, 443 insertions(+), 1167 deletions(-) create mode 100644 erpnext/accounts/doctype/campaign_item/__init__.py create mode 100644 erpnext/accounts/doctype/campaign_item/campaign_item.json create mode 100644 erpnext/accounts/doctype/campaign_item/campaign_item.py create mode 100644 erpnext/accounts/doctype/customer_group_item/__init__.py create mode 100644 erpnext/accounts/doctype/customer_group_item/customer_group_item.json create mode 100644 erpnext/accounts/doctype/customer_group_item/customer_group_item.py create mode 100644 erpnext/accounts/doctype/customer_item/__init__.py create mode 100644 erpnext/accounts/doctype/customer_item/customer_item.json create mode 100644 erpnext/accounts/doctype/customer_item/customer_item.py create mode 100644 erpnext/accounts/doctype/sales_partner_item/__init__.py create mode 100644 erpnext/accounts/doctype/sales_partner_item/sales_partner_item.json create mode 100644 erpnext/accounts/doctype/sales_partner_item/sales_partner_item.py create mode 100644 erpnext/accounts/doctype/supplier_group_item/__init__.py create mode 100644 erpnext/accounts/doctype/supplier_group_item/supplier_group_item.json create mode 100644 erpnext/accounts/doctype/supplier_group_item/supplier_group_item.py create mode 100644 erpnext/accounts/doctype/supplier_item/__init__.py create mode 100644 erpnext/accounts/doctype/supplier_item/supplier_item.json create mode 100644 erpnext/accounts/doctype/supplier_item/supplier_item.py create mode 100644 erpnext/accounts/doctype/territory_item/__init__.py create mode 100644 erpnext/accounts/doctype/territory_item/territory_item.json create mode 100644 erpnext/accounts/doctype/territory_item/territory_item.py diff --git a/erpnext/accounts/doctype/campaign_item/__init__.py b/erpnext/accounts/doctype/campaign_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/campaign_item/campaign_item.json b/erpnext/accounts/doctype/campaign_item/campaign_item.json new file mode 100644 index 0000000000..69383a482b --- /dev/null +++ b/erpnext/accounts/doctype/campaign_item/campaign_item.json @@ -0,0 +1,31 @@ +{ + "actions": [], + "creation": "2021-05-06 16:18:25.410476", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "campaign" + ], + "fields": [ + { + "fieldname": "campaign", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Campaign", + "options": "Campaign" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-05-07 10:43:49.717633", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Campaign Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/campaign_item/campaign_item.py b/erpnext/accounts/doctype/campaign_item/campaign_item.py new file mode 100644 index 0000000000..e5ca7e5368 --- /dev/null +++ b/erpnext/accounts/doctype/campaign_item/campaign_item.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class CampaignItem(Document): + pass diff --git a/erpnext/accounts/doctype/customer_group_item/__init__.py b/erpnext/accounts/doctype/customer_group_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/customer_group_item/customer_group_item.json b/erpnext/accounts/doctype/customer_group_item/customer_group_item.json new file mode 100644 index 0000000000..bd1229d4e0 --- /dev/null +++ b/erpnext/accounts/doctype/customer_group_item/customer_group_item.json @@ -0,0 +1,31 @@ +{ + "actions": [], + "creation": "2021-05-06 16:12:42.558878", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "customer_group" + ], + "fields": [ + { + "fieldname": "customer_group", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Customer Group", + "options": "Customer Group" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-05-07 10:39:21.563506", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Customer Group Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/customer_group_item/customer_group_item.py b/erpnext/accounts/doctype/customer_group_item/customer_group_item.py new file mode 100644 index 0000000000..ea24788ad5 --- /dev/null +++ b/erpnext/accounts/doctype/customer_group_item/customer_group_item.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class CustomerGroupItem(Document): + pass diff --git a/erpnext/accounts/doctype/customer_item/__init__.py b/erpnext/accounts/doctype/customer_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/customer_item/customer_item.json b/erpnext/accounts/doctype/customer_item/customer_item.json new file mode 100644 index 0000000000..f3dac02f94 --- /dev/null +++ b/erpnext/accounts/doctype/customer_item/customer_item.json @@ -0,0 +1,31 @@ +{ + "actions": [], + "creation": "2021-05-05 14:04:54.266353", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "customer" + ], + "fields": [ + { + "fieldname": "customer", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Customer ", + "options": "Customer" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-05-06 10:02:32.967841", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Customer Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/customer_item/customer_item.py b/erpnext/accounts/doctype/customer_item/customer_item.py new file mode 100644 index 0000000000..4aad84ec14 --- /dev/null +++ b/erpnext/accounts/doctype/customer_item/customer_item.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class CustomerItem(Document): + pass diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.json b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.json index cc71324dbc..1d68b23d6c 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.json +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.json @@ -1,1381 +1,339 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, + "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "Prompt", - "beta": 0, "creation": "2019-02-08 17:10:36.077402", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "section_break_1", + "apply_on", + "disable", + "column_break_3", + "items", + "item_groups", + "brands", + "mixed_conditions", + "is_cumulative", + "section_break_10", + "apply_rule_on_other", + "column_break_11", + "other_item_code", + "other_item_group", + "other_brand", + "section_break_8", + "selling", + "buying", + "column_break_12", + "applicable_for", + "customer", + "customer_group", + "territory", + "sales_partner", + "campaign", + "supplier", + "supplier_group", + "period_settings_section", + "valid_from", + "valid_upto", + "column_break_26", + "company", + "currency", + "section_break_14", + "price_discount_slabs", + "section_break_15", + "product_discount_slabs" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "section_break_1", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Item Code", - "fetch_if_empty": 0, "fieldname": "apply_on", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Apply On", - "length": 0, - "no_copy": 0, "options": "\nItem Code\nItem Group\nBrand\nTransaction", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "disable", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Disable", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Disable" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.apply_on == 'Item Code'", - "fetch_if_empty": 0, "fieldname": "items", "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Pricing Rule Item Code", - "length": 0, - "no_copy": 0, - "options": "Pricing Rule Item Code", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Pricing Rule Item Code" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.apply_on == 'Item Group'", - "fetch_if_empty": 0, "fieldname": "item_groups", "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Pricing Rule Item Group", - "length": 0, - "no_copy": 0, - "options": "Pricing Rule Item Group", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Pricing Rule Item Group" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.apply_on == 'Brand'", - "fetch_if_empty": 0, "fieldname": "brands", "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Pricing Rule Brand", - "length": 0, - "no_copy": 0, - "options": "Pricing Rule Brand", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Pricing Rule Brand" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "mixed_conditions", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Mixed Conditions", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Mixed Conditions" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "is_cumulative", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Is Cumulative", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Is Cumulative" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, "collapsible": 1, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "section_break_10", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Discount on Other Item", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Discount on Other Item" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, "fieldname": "apply_rule_on_other", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Apply Rule On Other", - "length": 0, - "no_copy": 0, - "options": "\nItem Code\nItem Group\nBrand", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "\nItem Code\nItem Group\nBrand" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, "fieldname": "column_break_11", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.apply_rule_on_other == 'Item Code'", - "fetch_if_empty": 0, "fieldname": "other_item_code", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Item Code", - "length": 0, - "no_copy": 0, - "options": "Item", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Item" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.apply_rule_on_other == 'Item Group'", - "fetch_if_empty": 0, "fieldname": "other_item_group", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Item Group", - "length": 0, - "no_copy": 0, - "options": "Item Group", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Item Group" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.apply_rule_on_other == 'Brand'", - "fetch_if_empty": 0, "fieldname": "other_brand", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Brand", - "length": 0, - "no_copy": 0, - "options": "Brand", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Brand" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, "collapsible": 1, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, "fieldname": "section_break_8", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Party Information", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Party Information" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "selling", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Selling", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Selling" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "buying", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Buying", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Buying" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, "fieldname": "column_break_12", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval: doc.buying || doc.selling", - "fetch_if_empty": 0, "fieldname": "applicable_for", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Applicable For", - "length": 0, - "no_copy": 0, - "options": "\nCustomer\nCustomer Group\nTerritory\nSales Partner\nCampaign\nSupplier\nSupplier Group", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "\nCustomer\nCustomer Group\nTerritory\nSales Partner\nCampaign\nSupplier\nSupplier Group" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.applicable_for=='Customer'", - "fetch_if_empty": 0, "fieldname": "customer", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, + "fieldtype": "Table MultiSelect", "label": "Customer", - "length": 0, - "no_copy": 0, - "options": "Customer", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Customer Item" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.applicable_for==\"Customer Group\"", - "fetch_if_empty": 0, "fieldname": "customer_group", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, + "fieldtype": "Table MultiSelect", "label": "Customer Group", - "length": 0, - "no_copy": 0, - "options": "Customer Group", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Customer Group Item" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.applicable_for==\"Territory\"", - "fetch_if_empty": 0, "fieldname": "territory", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, + "fieldtype": "Table MultiSelect", "label": "Territory", - "length": 0, - "no_copy": 0, - "options": "Territory", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Territory Item" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.applicable_for==\"Sales Partner\"", - "fetch_if_empty": 0, "fieldname": "sales_partner", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, + "fieldtype": "Table MultiSelect", "label": "Sales Partner", - "length": 0, - "no_copy": 0, - "options": "Sales Partner", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Sales Partner Item" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.applicable_for==\"Campaign\"", - "fetch_if_empty": 0, "fieldname": "campaign", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, + "fieldtype": "Table MultiSelect", "label": "Campaign", - "length": 0, - "no_copy": 0, - "options": "Campaign", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Campaign Item" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.applicable_for=='Supplier'", - "fetch_if_empty": 0, "fieldname": "supplier", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, + "fieldtype": "Table MultiSelect", "label": "Supplier", - "length": 0, - "no_copy": 0, - "options": "Supplier", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Supplier Item" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.applicable_for==\"Supplier Group\"", - "fetch_if_empty": 0, "fieldname": "supplier_group", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, + "fieldtype": "Table MultiSelect", "label": "Supplier Group", - "length": 0, - "no_copy": 0, - "options": "Supplier Group", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Supplier Group Item" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "period_settings_section", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Period Settings", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Period Settings" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Today", - "fetch_if_empty": 0, "fieldname": "valid_from", "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Valid From", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Valid From" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "valid_upto", "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Valid Upto", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Valid Upto" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "column_break_26", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "company", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Company", - "length": 0, - "no_copy": 0, "options": "Company", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "currency", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Currency", - "length": 0, - "no_copy": 0, - "options": "Currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Currency" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, "fieldname": "section_break_14", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Price Discount Slabs", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Price Discount Slabs" }, { "allow_bulk_edit": 1, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "price_discount_slabs", "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Promotional Scheme Price Discount", - "length": 0, - "no_copy": 0, - "options": "Promotional Scheme Price Discount", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Promotional Scheme Price Discount" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, "fieldname": "section_break_15", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Product Discount Slabs", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Product Discount Slabs" }, { "allow_bulk_edit": 1, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "product_discount_slabs", "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Promotional Scheme Product Discount", - "length": 0, - "no_copy": 0, - "options": "Promotional Scheme Product Discount", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Promotional Scheme Product Discount" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-03-25 12:14:27.486586", + "links": [], + "modified": "2021-05-06 16:20:22.039078", "modified_by": "Administrator", "module": "Accounts", "name": "Promotional Scheme", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Accounts Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Sales Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Accounts User", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py index 7d9302382f..ec42eda63a 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py @@ -30,17 +30,17 @@ class PromotionalScheme(Document): frappe.throw(_("Price or product discount slabs are required")) def on_update(self): - data = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name"], - filters = {'promotional_scheme': self.name}) or {} - + data = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name", "creation"], order_by = 'creation asc', + filters = {'promotional_scheme': self.name, 'applicable_for': self.applicable_for}) or {} self.update_pricing_rules(data) def update_pricing_rules(self, data): rules = {} count = 0 - + names = [] for d in data: - rules[d.get('promotional_scheme_id')] = d.get('name') + names.append(d.name) + rules[d.get('promotional_scheme_id')] = names docs = get_pricing_rules(self, rules) @@ -73,42 +73,73 @@ def get_pricing_rules(doc, rules = {}): def _get_pricing_rules(doc, child_doc, discount_fields, rules = {}): new_doc = [] args = get_args_for_pricing_rule(doc) + applicable_for = frappe.scrub(doc.get('applicable_for')) for d in doc.get(child_doc): if d.name in rules: - pr = frappe.get_doc('Pricing Rule', rules.get(d.name)) + for a in args.get(applicable_for): + + temp_args = args.copy() + docname = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name", applicable_for], + filters = {'promotional_scheme_id': d.name, applicable_for: a}) + + if docname: + pr = frappe.get_doc('Pricing Rule', docname[0].get('name')) + temp_args[applicable_for] = a + pr = set_args(temp_args,pr,doc,child_doc,discount_fields,d) + else: + pr = frappe.new_doc("Pricing Rule") + pr.title = make_autoname("{0}/.####".format(doc.name)) + temp_args[applicable_for] = a + pr = set_args(temp_args,pr,doc,child_doc,discount_fields,d) + + new_doc.append(pr) + else: - pr = frappe.new_doc("Pricing Rule") - pr.title = make_autoname("{0}/.####".format(doc.name)) + for i in range(len(args.get(applicable_for))) : - pr.update(args) - for field in (other_fields + discount_fields): - pr.set(field, d.get(field)) - - pr.promotional_scheme_id = d.name - pr.promotional_scheme = doc.name - pr.disable = d.disable if d.disable else doc.disable - pr.price_or_product_discount = ('Price' - if child_doc == 'price_discount_slabs' else 'Product') - - for field in ['items', 'item_groups', 'brands']: - if doc.get(field): - pr.set(field, []) - - apply_on = frappe.scrub(doc.get('apply_on')) - for d in doc.get(field): - pr.append(field, { - apply_on: d.get(apply_on), - 'uom': d.uom - }) - - new_doc.append(pr) + pr = frappe.new_doc("Pricing Rule") + pr.title = make_autoname("{0}/.####".format(doc.name)) + temp_args = args.copy() + temp_args[applicable_for] = args[applicable_for][i] + pr = set_args(temp_args,pr,doc,child_doc,discount_fields,d) + new_doc.append(pr) return new_doc +def set_args(args,pr,doc,child_doc,discount_fields,d): + pr.update(args) + for field in (other_fields + discount_fields): + pr.set(field, d.get(field)) + + pr.promotional_scheme_id = d.name + pr.promotional_scheme = doc.name + pr.disable = d.disable if d.disable else doc.disable + pr.price_or_product_discount = ('Price' + if child_doc == 'price_discount_slabs' else 'Product') + + for field in ['items', 'item_groups', 'brands']: + if doc.get(field): + pr.set(field, []) + + apply_on = frappe.scrub(doc.get('apply_on')) + for d in doc.get(field): + pr.append(field, { + apply_on: d.get(apply_on), + 'uom': d.uom + }) + return pr + def get_args_for_pricing_rule(doc): args = { 'promotional_scheme': doc.name } - + applicable_for = frappe.scrub(doc.get('applicable_for')) + for d in pricing_rule_fields: - args[d] = doc.get(d) - + if d == applicable_for: + items = [] + for i in doc.get(applicable_for): + items.append(i.get(applicable_for)) + args[d] = items + + else: + args[d] = doc.get(d) return args diff --git a/erpnext/accounts/doctype/sales_partner_item/__init__.py b/erpnext/accounts/doctype/sales_partner_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/sales_partner_item/sales_partner_item.json b/erpnext/accounts/doctype/sales_partner_item/sales_partner_item.json new file mode 100644 index 0000000000..c176e4d173 --- /dev/null +++ b/erpnext/accounts/doctype/sales_partner_item/sales_partner_item.json @@ -0,0 +1,31 @@ +{ + "actions": [], + "creation": "2021-05-06 16:17:44.329943", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "sales_partner" + ], + "fields": [ + { + "fieldname": "sales_partner", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Sales Partner ", + "options": "Sales Partner" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-05-07 10:43:37.532095", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Sales Partner Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_partner_item/sales_partner_item.py b/erpnext/accounts/doctype/sales_partner_item/sales_partner_item.py new file mode 100644 index 0000000000..9e6564845b --- /dev/null +++ b/erpnext/accounts/doctype/sales_partner_item/sales_partner_item.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class SalesPartnerItem(Document): + pass diff --git a/erpnext/accounts/doctype/supplier_group_item/__init__.py b/erpnext/accounts/doctype/supplier_group_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/supplier_group_item/supplier_group_item.json b/erpnext/accounts/doctype/supplier_group_item/supplier_group_item.json new file mode 100644 index 0000000000..67fac45845 --- /dev/null +++ b/erpnext/accounts/doctype/supplier_group_item/supplier_group_item.json @@ -0,0 +1,31 @@ +{ + "actions": [], + "creation": "2021-05-06 16:19:22.040795", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "supplier_group" + ], + "fields": [ + { + "fieldname": "supplier_group", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Supplier Group", + "options": "Supplier Group" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-05-07 10:43:59.877938", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Supplier Group Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/supplier_group_item/supplier_group_item.py b/erpnext/accounts/doctype/supplier_group_item/supplier_group_item.py new file mode 100644 index 0000000000..5ea2635482 --- /dev/null +++ b/erpnext/accounts/doctype/supplier_group_item/supplier_group_item.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class SupplierGroupItem(Document): + pass diff --git a/erpnext/accounts/doctype/supplier_item/__init__.py b/erpnext/accounts/doctype/supplier_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/supplier_item/supplier_item.json b/erpnext/accounts/doctype/supplier_item/supplier_item.json new file mode 100644 index 0000000000..95c4dc6db3 --- /dev/null +++ b/erpnext/accounts/doctype/supplier_item/supplier_item.json @@ -0,0 +1,31 @@ +{ + "actions": [], + "creation": "2021-05-06 16:18:54.758468", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "supplier" + ], + "fields": [ + { + "fieldname": "supplier", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Supplier", + "options": "Supplier" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-05-07 10:44:09.707778", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Supplier Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/supplier_item/supplier_item.py b/erpnext/accounts/doctype/supplier_item/supplier_item.py new file mode 100644 index 0000000000..b58b5dc057 --- /dev/null +++ b/erpnext/accounts/doctype/supplier_item/supplier_item.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class SupplierItem(Document): + pass diff --git a/erpnext/accounts/doctype/territory_item/__init__.py b/erpnext/accounts/doctype/territory_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/territory_item/territory_item.json b/erpnext/accounts/doctype/territory_item/territory_item.json new file mode 100644 index 0000000000..0f0fdea8c0 --- /dev/null +++ b/erpnext/accounts/doctype/territory_item/territory_item.json @@ -0,0 +1,31 @@ +{ + "actions": [], + "creation": "2021-05-06 16:16:51.885441", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "territory" + ], + "fields": [ + { + "fieldname": "territory", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Territory", + "options": "Territory" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-05-07 10:43:26.641030", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Territory Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/territory_item/territory_item.py b/erpnext/accounts/doctype/territory_item/territory_item.py new file mode 100644 index 0000000000..9adfc00572 --- /dev/null +++ b/erpnext/accounts/doctype/territory_item/territory_item.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class TerritoryItem(Document): + pass From a9ef56a1077fcefd224b8382f892085f99eb2b5e Mon Sep 17 00:00:00 2001 From: noahjacob Date: Fri, 7 May 2021 16:03:59 +0530 Subject: [PATCH 122/147] test: added test case for creating and updating --- .../test_promotional_scheme.py | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py index 8dc0499779..9c756258db 100644 --- a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py @@ -7,4 +7,59 @@ import frappe import unittest class TestPromotionalScheme(unittest.TestCase): - pass + def test_promotional_scheme(self): + ps = make_promotional_scheme() + price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name", "creation"], + filters = {'promotional_scheme': ps.name}) + self.assertTrue(len(price_rules),1) + price_doc_details = frappe.db.get_value('Pricing Rule',price_rules[0].name,['customer','min_qty','discount_percentage'],as_dict = 1) + self.assertTrue(price_doc_details.customer,'_Test Customer') + self.assertTrue(price_doc_details.min_qty,4) + self.assertTrue(price_doc_details.discount_percentage,20) + + ps.price_discount_slabs[0].min_qty = 6 + ps.append('customer',{ + 'customer': "_Test Customer 2" + }) + ps.save() + price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name"], + filters = {'promotional_scheme': ps.name}) + self.assertTrue(len(price_rules),2) + + price_doc_details = frappe.db.get_value('Pricing Rule',price_rules[1].name,['customer','min_qty','discount_percentage'],as_dict = 1) + self.assertTrue(price_doc_details.customer,'_Test Customer 2') + self.assertTrue(price_doc_details.min_qty,6) + self.assertTrue(price_doc_details.discount_percentage,20) + + price_doc_details = frappe.db.get_value('Pricing Rule',price_rules[0].name,['customer','min_qty','discount_percentage'],as_dict = 1) + self.assertTrue(price_doc_details.customer,'_Test Customer') + self.assertTrue(price_doc_details.min_qty,6) + + frappe.delete_doc('Promotional Scheme',ps.name) + price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name"], + filters = {'promotional_scheme': ps.name}) + self.assertEqual(price_rules,[]) + + + + + +def make_promotional_scheme(): + ps = frappe.new_doc('Promotional Scheme') + ps.name = '_Test Scheme' + ps.append('items',{ + 'item_code': 'Test Production Item 1' + }) + ps.selling = 1 + ps.append('price_discount_slabs',{ + 'min_qty': 4, + 'discount_percentage': 20, + 'rule_description': 'Test' + }) + ps.applicable_for = 'Customer' + ps.append('customer',{ + 'customer': "_Test Customer" + }) + ps.save() + + return ps \ No newline at end of file From c95d96e7ae7bd0a283e29d09346e590abb489640 Mon Sep 17 00:00:00 2001 From: noahjacob Date: Fri, 7 May 2021 16:17:00 +0530 Subject: [PATCH 123/147] refactor: changed variable names --- .../doctype/promotional_scheme/promotional_scheme.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py index ec42eda63a..ff3f9c5638 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py @@ -76,20 +76,20 @@ def _get_pricing_rules(doc, child_doc, discount_fields, rules = {}): applicable_for = frappe.scrub(doc.get('applicable_for')) for d in doc.get(child_doc): if d.name in rules: - for a in args.get(applicable_for): + for applicable_for_value in args.get(applicable_for): temp_args = args.copy() docname = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name", applicable_for], - filters = {'promotional_scheme_id': d.name, applicable_for: a}) + filters = {'promotional_scheme_id': d.name, applicable_for: applicable_for_value}) if docname: pr = frappe.get_doc('Pricing Rule', docname[0].get('name')) - temp_args[applicable_for] = a + temp_args[applicable_for] = applicable_for_value pr = set_args(temp_args,pr,doc,child_doc,discount_fields,d) else: pr = frappe.new_doc("Pricing Rule") pr.title = make_autoname("{0}/.####".format(doc.name)) - temp_args[applicable_for] = a + temp_args[applicable_for] = applicable_for_value pr = set_args(temp_args,pr,doc,child_doc,discount_fields,d) new_doc.append(pr) @@ -136,8 +136,8 @@ def get_args_for_pricing_rule(doc): for d in pricing_rule_fields: if d == applicable_for: items = [] - for i in doc.get(applicable_for): - items.append(i.get(applicable_for)) + for applicable_for_values in doc.get(applicable_for): + items.append(applicable_for_values.get(applicable_for)) args[d] = items else: From 3a663ac77fc89fd7e95f8fe7b1fee4ad044dac93 Mon Sep 17 00:00:00 2001 From: noahjacob Date: Fri, 7 May 2021 17:39:26 +0530 Subject: [PATCH 124/147] test: changed test item name --- .../doctype/promotional_scheme/test_promotional_scheme.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py index 9c756258db..940b76a9ad 100644 --- a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py @@ -48,7 +48,7 @@ def make_promotional_scheme(): ps = frappe.new_doc('Promotional Scheme') ps.name = '_Test Scheme' ps.append('items',{ - 'item_code': 'Test Production Item 1' + 'item_code': '_Test Item' }) ps.selling = 1 ps.append('price_discount_slabs',{ From a4cea1e56d68dbb4139324629bba1730508b33a3 Mon Sep 17 00:00:00 2001 From: noahjacob Date: Wed, 12 May 2021 16:50:41 +0530 Subject: [PATCH 125/147] refactor: variable names --- .../doctype/promotional_scheme/promotional_scheme.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py index ff3f9c5638..69ed94676d 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py @@ -106,14 +106,14 @@ def _get_pricing_rules(doc, child_doc, discount_fields, rules = {}): return new_doc -def set_args(args,pr,doc,child_doc,discount_fields,d): +def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields): pr.update(args) for field in (other_fields + discount_fields): - pr.set(field, d.get(field)) + pr.set(field, child_doc_fields.get(field)) - pr.promotional_scheme_id = d.name + pr.promotional_scheme_id = child_doc_fields.name pr.promotional_scheme = doc.name - pr.disable = d.disable if d.disable else doc.disable + pr.disable = child_doc_fields.disable if child_doc_fields.disable else doc.disable pr.price_or_product_discount = ('Price' if child_doc == 'price_discount_slabs' else 'Product') From 4fb1b6b80cb9ebeb249137eb7b24410547c745f9 Mon Sep 17 00:00:00 2001 From: noahjacob Date: Wed, 12 May 2021 16:51:02 +0530 Subject: [PATCH 126/147] fix: Sider --- .../promotional_scheme/test_promotional_scheme.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py index 940b76a9ad..99f9ed47e4 100644 --- a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py @@ -18,9 +18,8 @@ class TestPromotionalScheme(unittest.TestCase): self.assertTrue(price_doc_details.discount_percentage,20) ps.price_discount_slabs[0].min_qty = 6 - ps.append('customer',{ - 'customer': "_Test Customer 2" - }) + ps.append('customer', { + 'customer': "_Test Customer 2"}) ps.save() price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name"], filters = {'promotional_scheme': ps.name}) @@ -39,11 +38,7 @@ class TestPromotionalScheme(unittest.TestCase): price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name"], filters = {'promotional_scheme': ps.name}) self.assertEqual(price_rules,[]) - - - - def make_promotional_scheme(): ps = frappe.new_doc('Promotional Scheme') ps.name = '_Test Scheme' From 74bcb987f30dad26d66dd94caa6a3ca66eb00311 Mon Sep 17 00:00:00 2001 From: noahjacob Date: Tue, 18 May 2021 14:52:50 +0530 Subject: [PATCH 127/147] style: fixed formatting --- .../promotional_scheme/promotional_scheme.py | 6 ++-- .../test_promotional_scheme.py | 28 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py index 69ed94676d..2b7f156638 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py @@ -85,12 +85,12 @@ def _get_pricing_rules(doc, child_doc, discount_fields, rules = {}): if docname: pr = frappe.get_doc('Pricing Rule', docname[0].get('name')) temp_args[applicable_for] = applicable_for_value - pr = set_args(temp_args,pr,doc,child_doc,discount_fields,d) + pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d) else: pr = frappe.new_doc("Pricing Rule") pr.title = make_autoname("{0}/.####".format(doc.name)) temp_args[applicable_for] = applicable_for_value - pr = set_args(temp_args,pr,doc,child_doc,discount_fields,d) + pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d) new_doc.append(pr) @@ -101,7 +101,7 @@ def _get_pricing_rules(doc, child_doc, discount_fields, rules = {}): pr.title = make_autoname("{0}/.####".format(doc.name)) temp_args = args.copy() temp_args[applicable_for] = args[applicable_for][i] - pr = set_args(temp_args,pr,doc,child_doc,discount_fields,d) + pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d) new_doc.append(pr) return new_doc diff --git a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py index 99f9ed47e4..7354ef036c 100644 --- a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py @@ -12,10 +12,10 @@ class TestPromotionalScheme(unittest.TestCase): price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name", "creation"], filters = {'promotional_scheme': ps.name}) self.assertTrue(len(price_rules),1) - price_doc_details = frappe.db.get_value('Pricing Rule',price_rules[0].name,['customer','min_qty','discount_percentage'],as_dict = 1) - self.assertTrue(price_doc_details.customer,'_Test Customer') - self.assertTrue(price_doc_details.min_qty,4) - self.assertTrue(price_doc_details.discount_percentage,20) + price_doc_details = frappe.db.get_value('Pricing Rule', price_rules[0].name, ['customer', 'min_qty', 'discount_percentage'], as_dict = 1) + self.assertTrue(price_doc_details.customer, '_Test Customer') + self.assertTrue(price_doc_details.min_qty, 4) + self.assertTrue(price_doc_details.discount_percentage, 20) ps.price_discount_slabs[0].min_qty = 6 ps.append('customer', { @@ -23,21 +23,21 @@ class TestPromotionalScheme(unittest.TestCase): ps.save() price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name"], filters = {'promotional_scheme': ps.name}) - self.assertTrue(len(price_rules),2) + self.assertTrue(len(price_rules), 2) - price_doc_details = frappe.db.get_value('Pricing Rule',price_rules[1].name,['customer','min_qty','discount_percentage'],as_dict = 1) - self.assertTrue(price_doc_details.customer,'_Test Customer 2') - self.assertTrue(price_doc_details.min_qty,6) - self.assertTrue(price_doc_details.discount_percentage,20) + price_doc_details = frappe.db.get_value('Pricing Rule', price_rules[1].name, ['customer', 'min_qty', 'discount_percentage'], as_dict = 1) + self.assertTrue(price_doc_details.customer, '_Test Customer 2') + self.assertTrue(price_doc_details.min_qty, 6) + self.assertTrue(price_doc_details.discount_percentage, 20) - price_doc_details = frappe.db.get_value('Pricing Rule',price_rules[0].name,['customer','min_qty','discount_percentage'],as_dict = 1) - self.assertTrue(price_doc_details.customer,'_Test Customer') - self.assertTrue(price_doc_details.min_qty,6) + price_doc_details = frappe.db.get_value('Pricing Rule', price_rules[0].name, ['customer', 'min_qty', 'discount_percentage'], as_dict = 1) + self.assertTrue(price_doc_details.customer, '_Test Customer') + self.assertTrue(price_doc_details.min_qty, 6) - frappe.delete_doc('Promotional Scheme',ps.name) + frappe.delete_doc('Promotional Scheme', ps.name) price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name"], filters = {'promotional_scheme': ps.name}) - self.assertEqual(price_rules,[]) + self.assertEqual(price_rules, []) def make_promotional_scheme(): ps = frappe.new_doc('Promotional Scheme') From b214e624bb8dde5fb0713fa4920306d97ea2f178 Mon Sep 17 00:00:00 2001 From: noahjacob Date: Wed, 19 May 2021 16:18:37 +0530 Subject: [PATCH 128/147] style: fixed formatting --- .../promotional_scheme/promotional_scheme.py | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py index 2b7f156638..53239feb42 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py @@ -30,8 +30,15 @@ class PromotionalScheme(Document): frappe.throw(_("Price or product discount slabs are required")) def on_update(self): - data = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name", "creation"], order_by = 'creation asc', - filters = {'promotional_scheme': self.name, 'applicable_for': self.applicable_for}) or {} + data = frappe.get_all( + 'Pricing Rule', + fields = ["promotional_scheme_id", "name", "creation"], + filters = { + 'promotional_scheme': self.name, + 'applicable_for': self.applicable_for + }, + order_by = 'creation asc', + ) or {} self.update_pricing_rules(data) def update_pricing_rules(self, data): @@ -77,10 +84,15 @@ def _get_pricing_rules(doc, child_doc, discount_fields, rules = {}): for d in doc.get(child_doc): if d.name in rules: for applicable_for_value in args.get(applicable_for): - temp_args = args.copy() - docname = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name", applicable_for], - filters = {'promotional_scheme_id': d.name, applicable_for: applicable_for_value}) + docname = frappe.get_all( + 'Pricing Rule', + fields = ["promotional_scheme_id", "name", applicable_for], + filters = { + 'promotional_scheme_id': d.name, + applicable_for: applicable_for_value + } + ) if docname: pr = frappe.get_doc('Pricing Rule', docname[0].get('name')) @@ -139,7 +151,6 @@ def get_args_for_pricing_rule(doc): for applicable_for_values in doc.get(applicable_for): items.append(applicable_for_values.get(applicable_for)) args[d] = items - else: args[d] = doc.get(d) return args From dd86642037cfff7efb52aadab57a6454175e5b67 Mon Sep 17 00:00:00 2001 From: noahjacob Date: Mon, 24 May 2021 16:52:55 +0530 Subject: [PATCH 129/147] refactor: removed py2 code --- erpnext/accounts/doctype/campaign_item/campaign_item.py | 2 -- .../accounts/doctype/customer_group_item/customer_group_item.py | 2 -- erpnext/accounts/doctype/customer_item/customer_item.py | 2 -- .../accounts/doctype/sales_partner_item/sales_partner_item.py | 2 -- .../accounts/doctype/supplier_group_item/supplier_group_item.py | 2 -- erpnext/accounts/doctype/supplier_item/supplier_item.py | 2 -- erpnext/accounts/doctype/territory_item/territory_item.py | 2 -- 7 files changed, 14 deletions(-) diff --git a/erpnext/accounts/doctype/campaign_item/campaign_item.py b/erpnext/accounts/doctype/campaign_item/campaign_item.py index e5ca7e5368..4f5fd7f7d7 100644 --- a/erpnext/accounts/doctype/campaign_item/campaign_item.py +++ b/erpnext/accounts/doctype/campaign_item/campaign_item.py @@ -1,8 +1,6 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals # import frappe from frappe.model.document import Document diff --git a/erpnext/accounts/doctype/customer_group_item/customer_group_item.py b/erpnext/accounts/doctype/customer_group_item/customer_group_item.py index ea24788ad5..df782ac9e0 100644 --- a/erpnext/accounts/doctype/customer_group_item/customer_group_item.py +++ b/erpnext/accounts/doctype/customer_group_item/customer_group_item.py @@ -1,8 +1,6 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals # import frappe from frappe.model.document import Document diff --git a/erpnext/accounts/doctype/customer_item/customer_item.py b/erpnext/accounts/doctype/customer_item/customer_item.py index 4aad84ec14..a577145e4e 100644 --- a/erpnext/accounts/doctype/customer_item/customer_item.py +++ b/erpnext/accounts/doctype/customer_item/customer_item.py @@ -1,8 +1,6 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals # import frappe from frappe.model.document import Document diff --git a/erpnext/accounts/doctype/sales_partner_item/sales_partner_item.py b/erpnext/accounts/doctype/sales_partner_item/sales_partner_item.py index 9e6564845b..9796c7b0cc 100644 --- a/erpnext/accounts/doctype/sales_partner_item/sales_partner_item.py +++ b/erpnext/accounts/doctype/sales_partner_item/sales_partner_item.py @@ -1,8 +1,6 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals # import frappe from frappe.model.document import Document diff --git a/erpnext/accounts/doctype/supplier_group_item/supplier_group_item.py b/erpnext/accounts/doctype/supplier_group_item/supplier_group_item.py index 5ea2635482..de0444ee19 100644 --- a/erpnext/accounts/doctype/supplier_group_item/supplier_group_item.py +++ b/erpnext/accounts/doctype/supplier_group_item/supplier_group_item.py @@ -1,8 +1,6 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals # import frappe from frappe.model.document import Document diff --git a/erpnext/accounts/doctype/supplier_item/supplier_item.py b/erpnext/accounts/doctype/supplier_item/supplier_item.py index b58b5dc057..ad66e230c8 100644 --- a/erpnext/accounts/doctype/supplier_item/supplier_item.py +++ b/erpnext/accounts/doctype/supplier_item/supplier_item.py @@ -1,8 +1,6 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals # import frappe from frappe.model.document import Document diff --git a/erpnext/accounts/doctype/territory_item/territory_item.py b/erpnext/accounts/doctype/territory_item/territory_item.py index 9adfc00572..d46edc9dca 100644 --- a/erpnext/accounts/doctype/territory_item/territory_item.py +++ b/erpnext/accounts/doctype/territory_item/territory_item.py @@ -1,8 +1,6 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals # import frappe from frappe.model.document import Document From 10ce2f5d6ebcacf16604930e399659cb2eadb740 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Wed, 4 Aug 2021 21:10:25 +0530 Subject: [PATCH 130/147] refactor: added naming series for pricing rule --- .../doctype/pricing_rule/pricing_rule.json | 18 +++++++++++++----- .../promotional_scheme/promotional_scheme.py | 18 +++++++++--------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index 0be41b4063..99c5b34fa3 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -2,12 +2,13 @@ "actions": [], "allow_import": 1, "allow_rename": 1, - "autoname": "field:title", + "autoname": "naming_series:", "creation": "2014-02-21 15:02:51", "doctype": "DocType", "engine": "InnoDB", "field_order": [ "applicability_section", + "naming_series", "title", "disable", "apply_on", @@ -95,8 +96,7 @@ "fieldtype": "Data", "label": "Title", "no_copy": 1, - "reqd": 1, - "unique": 1 + "reqd": 1 }, { "default": "0", @@ -571,6 +571,13 @@ "fieldname": "is_recursive", "fieldtype": "Check", "label": "Is Recursive" + }, + { + "default": "PRLE-.####", + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "PRLE-.####" } ], "icon": "fa fa-gift", @@ -634,5 +641,6 @@ ], "show_name_in_global_search": 1, "sort_field": "modified", - "sort_order": "DESC" -} \ No newline at end of file + "sort_order": "DESC", + "title_field": "title" +} diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py index 53239feb42..f4ee1887c4 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py @@ -81,7 +81,7 @@ def _get_pricing_rules(doc, child_doc, discount_fields, rules = {}): new_doc = [] args = get_args_for_pricing_rule(doc) applicable_for = frappe.scrub(doc.get('applicable_for')) - for d in doc.get(child_doc): + for idx, d in enumerate(doc.get(child_doc)): if d.name in rules: for applicable_for_value in args.get(applicable_for): temp_args = args.copy() @@ -93,24 +93,24 @@ def _get_pricing_rules(doc, child_doc, discount_fields, rules = {}): applicable_for: applicable_for_value } ) - + if docname: pr = frappe.get_doc('Pricing Rule', docname[0].get('name')) temp_args[applicable_for] = applicable_for_value pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d) else: pr = frappe.new_doc("Pricing Rule") - pr.title = make_autoname("{0}/.####".format(doc.name)) + pr.title = doc.name temp_args[applicable_for] = applicable_for_value pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d) - + new_doc.append(pr) - + else: for i in range(len(args.get(applicable_for))) : pr = frappe.new_doc("Pricing Rule") - pr.title = make_autoname("{0}/.####".format(doc.name)) + pr.title = doc.name temp_args = args.copy() temp_args[applicable_for] = args[applicable_for][i] pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d) @@ -144,13 +144,13 @@ def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields): def get_args_for_pricing_rule(doc): args = { 'promotional_scheme': doc.name } applicable_for = frappe.scrub(doc.get('applicable_for')) - + for d in pricing_rule_fields: if d == applicable_for: items = [] for applicable_for_values in doc.get(applicable_for): - items.append(applicable_for_values.get(applicable_for)) - args[d] = items + items.append(applicable_for_values.get(applicable_for)) + args[d] = items else: args[d] = doc.get(d) return args From 2ab62a44845d6d9dcda249759b3ad3131cc50f3a Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Tue, 10 Aug 2021 11:43:59 +0530 Subject: [PATCH 131/147] fix: Missing method reset_issue_metrics added back to Issue doctype (#26574) --- erpnext/support/doctype/issue/issue.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 9c69deb6a4..e4c4af0365 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -222,6 +222,10 @@ class Issue(Document): }).insert(ignore_permissions=True) return replicated_issue.name + + def reset_issue_metrics(self): + self.db_set("resolution_time", None) + self.db_set("user_resolution_time", None) def before_insert(self): if frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): From 06b6b7e3cca968abc18e7136b8567ade9192003a Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 10 Aug 2021 13:10:46 +0530 Subject: [PATCH 132/147] fix: Set CWIP Account in company at the start to avoid flaky test --- .../doctype/landed_cost_voucher/test_landed_cost_voucher.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index 128a2ab62f..cb09d93380 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -253,6 +253,8 @@ class TestLandedCostVoucher(unittest.TestCase): def test_asset_lcv(self): "Check if LCV for an Asset updates the Assets Gross Purchase Amount correctly." + frappe.db.set_value("Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC") + if not frappe.db.exists("Asset Category", "Computers"): create_asset_category() @@ -265,7 +267,6 @@ class TestLandedCostVoucher(unittest.TestCase): assets = frappe.db.get_all('Asset', filters={'purchase_receipt': pr.name}) self.assertEqual(len(assets), 1) - frappe.db.set_value("Company", pr.company, "capital_work_in_progress_account", "CWIP Account - _TC") lcv = make_landed_cost_voucher( company = pr.company, receipt_document_type = "Purchase Receipt", From 24da00cada12c20973c9ea3c6914b8fb60c5754f Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Tue, 10 Aug 2021 13:17:41 +0530 Subject: [PATCH 133/147] fix: updating lead status while customer creation (#26607) * fix: updating lead status while customer creation * fix: changes requested --- erpnext/selling/doctype/customer/customer.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 818888c0c1..9785f6c7a9 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -134,9 +134,7 @@ class Customer(TransactionBase): '''If Customer created from Lead, update lead status to "Converted" update Customer link in Quotation, Opportunity''' if self.lead_name: - lead = frappe.get_doc('Lead', self.lead_name) - lead.status = 'Converted' - lead.save() + frappe.db.set_value("Lead", self.lead_name, "status", "Converted") def create_lead_address_contact(self): if self.lead_name: From 57191985761b6963cf2dd7c44c8300c0a86f1fe8 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 10 Aug 2021 13:56:48 +0530 Subject: [PATCH 134/147] feat: dynamic conditions for applying SLA (#26806) --- erpnext/support/doctype/issue/issue.py | 6 +-- .../service_level_agreement.json | 28 ++++++++++- .../service_level_agreement.py | 48 +++++++++++++++---- 3 files changed, 68 insertions(+), 14 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index e4c4af0365..b48925d33a 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -235,8 +235,7 @@ class Issue(Document): self.set_response_and_resolution_time() def set_response_and_resolution_time(self, priority=None, service_level_agreement=None): - service_level_agreement = get_active_service_level_agreement_for(priority=priority, - customer=self.customer, service_level_agreement=service_level_agreement) + service_level_agreement = get_active_service_level_agreement_for(self) if not service_level_agreement: if frappe.db.get_value("Issue", self.name, "service_level_agreement"): @@ -247,7 +246,8 @@ class Issue(Document): frappe.throw(_("This Service Level Agreement is specific to Customer {0}").format(service_level_agreement.customer)) self.service_level_agreement = service_level_agreement.name - self.priority = service_level_agreement.default_priority if not priority else priority + if not self.priority: + self.priority = service_level_agreement.default_priority priority = get_priority(self) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json index 939c199982..1678f04def 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -18,6 +18,10 @@ "entity_type", "column_break_10", "entity", + "filters_section", + "condition", + "column_break_15", + "condition_description", "agreement_details_section", "start_date", "active", @@ -171,10 +175,30 @@ "fieldtype": "Table", "label": "Pause SLA On", "options": "Pause SLA On Status" + }, + { + "fieldname": "filters_section", + "fieldtype": "Section Break", + "label": "Assignment Condition" + }, + { + "fieldname": "column_break_15", + "fieldtype": "Column Break" + }, + { + "fieldname": "condition", + "fieldtype": "Code", + "label": "Condition", + "options": "Python" + }, + { + "fieldname": "condition_description", + "fieldtype": "HTML", + "options": "

                            Condition Examples:

                            \n
                            doc.status==\"Open\"
                            doc.due_date==nowdate()
                            doc.total > 40000\n
                            " } ], "links": [], - "modified": "2020-06-10 12:30:15.050785", + "modified": "2021-07-27 11:16:45.596579", "modified_by": "Administrator", "module": "Support", "name": "Service Level Agreement", @@ -208,4 +232,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 70c469663b..ec0237e2ea 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -3,10 +3,12 @@ # For license information, please see license.txt from __future__ import unicode_literals + import frappe from frappe.model.document import Document from frappe import _ -from frappe.utils import getdate, get_weekdays, get_link_to_form +from frappe.utils import getdate, get_weekdays, get_link_to_form, nowdate +from frappe.utils.safe_exec import get_safe_globals class ServiceLevelAgreement(Document): @@ -14,6 +16,7 @@ class ServiceLevelAgreement(Document): self.validate_doc() self.check_priorities() self.check_support_and_resolution() + self.validate_condition() def check_priorities(self): default_priority = [] @@ -92,6 +95,14 @@ class ServiceLevelAgreement(Document): if frappe.db.exists("Service Level Agreement", {"entity_type": self.entity_type, "entity": self.entity, "name": ["!=", self.name]}): frappe.throw(_("Service Level Agreement with Entity Type {0} and Entity {1} already exists.").format(self.entity_type, self.entity)) + def validate_condition(self): + temp_doc = frappe.new_doc('Issue') + if self.condition: + try: + frappe.safe_eval(self.condition, None, get_context(temp_doc)) + except Exception: + frappe.throw(_("The Condition '{0}' is invalid").format(self.condition)) + def get_service_level_agreement_priority(self, priority): priority = frappe.get_doc("Service Level Priority", {"priority": priority, "parent": self.name}) @@ -112,7 +123,7 @@ def check_agreement_status(): if doc.end_date and getdate(doc.end_date) < getdate(frappe.utils.getdate()): frappe.db.set_value("Service Level Agreement", service_level_agreement.name, "active", 0) -def get_active_service_level_agreement_for(priority, customer=None, service_level_agreement=None): +def get_active_service_level_agreement_for(doc): if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): return @@ -121,23 +132,42 @@ def get_active_service_level_agreement_for(priority, customer=None, service_leve ["Service Level Agreement", "enable", "=", 1] ] - if priority: - filters.append(["Service Level Priority", "priority", "=", priority]) + if doc.get('priority'): + filters.append(["Service Level Priority", "priority", "=", doc.get('priority')]) + customer = doc.get('customer') or_filters = [ ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer)]] ] + + service_level_agreement = doc.get('service_level_agreement') if service_level_agreement: or_filters = [ - ["Service Level Agreement", "name", "=", service_level_agreement], + ["Service Level Agreement", "name", "=", doc.get('service_level_agreement')], ] - or_filters.append(["Service Level Agreement", "default_service_level_agreement", "=", 1]) + default_sla_filter = filters + [["Service Level Agreement", "default_service_level_agreement", "=", 1]] + default_sla = frappe.get_all("Service Level Agreement", filters=default_sla_filter, + fields=["name", "default_priority", "condition"]) - agreement = frappe.get_list("Service Level Agreement", filters=filters, or_filters=or_filters, - fields=["name", "default_priority"]) + filters += [["Service Level Agreement", "default_service_level_agreement", "=", 0]] + agreements = frappe.get_all("Service Level Agreement", filters=filters, or_filters=or_filters, + fields=["name", "default_priority", "condition"]) + + # check if the current document on which SLA is to be applied fulfills all the conditions + filtered_agreements = [] + for agreement in agreements: + condition = agreement.get('condition') + if not condition or (condition and frappe.safe_eval(condition, None, get_context(doc))): + filtered_agreements.append(agreement) - return agreement[0] if agreement else None + # if any default sla + filtered_agreements += default_sla + + return filtered_agreements[0] if filtered_agreements else None + +def get_context(doc): + return {"doc": doc.as_dict(), "nowdate": nowdate, "frappe": frappe._dict(utils=get_safe_globals().get("frappe").get("utils"))} def get_customer_group(customer): if customer: From f22b85825370fe75b8df694f3c8f86a572dc411b Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 22 Jul 2021 13:23:54 +0530 Subject: [PATCH 135/147] fix: Clean Serial No input on Server Side --- erpnext/controllers/stock_controller.py | 7 +++++++ erpnext/public/js/controllers/transaction.js | 2 +- erpnext/stock/doctype/serial_no/serial_no.py | 10 ++++++++-- erpnext/stock/doctype/stock_entry/stock_entry.py | 1 + .../stock_reconciliation/stock_reconciliation.py | 1 + 5 files changed, 18 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 17bd7354f9..17707ecae7 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -27,6 +27,7 @@ class StockController(AccountsController): if not self.get('is_return'): self.validate_inspection() self.validate_serialized_batch() + self.clean_serial_nos() self.validate_customer_provided_item() self.set_rate_of_stock_uom() self.validate_internal_transfer() @@ -72,6 +73,12 @@ class StockController(AccountsController): frappe.throw(_("Row #{0}: The batch {1} has already expired.") .format(d.idx, get_link_to_form("Batch", d.get("batch_no")))) + def clean_serial_nos(self): + for row in self.get("items"): + if hasattr(row, "serial_no") and row.serial_no: + # replace commas by linefeed and remove all spaces in string + row.serial_no = row.serial_no.replace(",", "\n").replace(" ", "") + def get_gl_entries(self, warehouse_account=None, default_expense_account=None, default_cost_center=None): diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 5475383759..b9fa9b7ebb 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -732,7 +732,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ this.frm.trigger("item_code", cdt, cdn); } else { - // Replacing all occurences of comma with carriage return + // Replace all occurences of comma with line feed item.serial_no = item.serial_no.replace(/,/g, '\n'); item.conversion_factor = item.conversion_factor || 1; refresh_field("serial_no", item.name, item.parentfield); diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index bad7b608ac..70312bc543 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -165,8 +165,14 @@ class SerialNo(StockController): ) ORDER BY posting_date desc, posting_time desc, creation desc""", - (self.item_code, self.company, - serial_no, serial_no+'\n%', '%\n'+serial_no, '%\n'+serial_no+'\n%'), as_dict=1): + ( + self.item_code, self.company, + serial_no, + serial_no+'\n%', + '%\n'+serial_no, + '%\n'+serial_no+'\n%' + ), + as_dict=1): if serial_no.upper() in get_serial_nos(sle.serial_no): if cint(sle.actual_qty) > 0: sle_dict.setdefault("incoming", []).append(sle) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 654755ec2f..3ff42bf974 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -76,6 +76,7 @@ class StockEntry(StockController): self.validate_difference_account() self.set_job_card_data() self.set_purpose_for_stock_entry() + self.clean_serial_nos() self.validate_duplicate_serial_no() if not self.from_bom: diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 2e09286507..324bb7a62d 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -31,6 +31,7 @@ class StockReconciliation(StockController): self.validate_expense_account() self.validate_customer_provided_item() self.set_zero_value_for_customer_provided_items() + self.clean_serial_nos() self.set_total_qty_and_amount() self.validate_putaway_capacity() From 510e31952d8f557530ba83d05dad11509447875d Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 10 Aug 2021 14:00:55 +0530 Subject: [PATCH 136/147] test: Serial no sanitation --- .../stock/doctype/serial_no/test_serial_no.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py index cde7fe07c6..b9a58cf43e 100644 --- a/erpnext/stock/doctype/serial_no/test_serial_no.py +++ b/erpnext/stock/doctype/serial_no/test_serial_no.py @@ -174,5 +174,23 @@ class TestSerialNo(unittest.TestCase): self.assertEqual(sn_doc.warehouse, "_Test Warehouse - _TC") self.assertEqual(sn_doc.purchase_document_no, se.name) + def test_serial_no_sanitation(self): + "Test if Serial No input is sanitised before entering the DB." + item_code = "_Test Serialized Item" + test_records = frappe.get_test_records('Stock Entry') + + se = frappe.copy_doc(test_records[0]) + se.get("items")[0].item_code = item_code + se.get("items")[0].qty = 3 + se.get("items")[0].serial_no = " _TS1, _TS2 , _TS3 " + se.get("items")[0].transfer_qty = 3 + se.set_stock_entry_type() + se.insert() + se.submit() + + self.assertEqual(se.get("items")[0].serial_no, "_TS1\n_TS2\n_TS3") + + frappe.db.rollback() + def tearDown(self): frappe.db.rollback() \ No newline at end of file From 1ba04fdfe542455bd703a56a48f4f65c1ac5f3e9 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 10 Aug 2021 15:47:36 +0530 Subject: [PATCH 137/147] fix: pos profile not mandatory for Sales Invoice (#26876) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 2539de7b12..eba8ba830f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -480,7 +480,7 @@ class SalesInvoice(SellingController): if not self.pos_profile: pos_profile = get_pos_profile(self.company) or {} if not pos_profile: - frappe.throw(_("No POS Profile found. Please create a New POS Profile first")) + return self.pos_profile = pos_profile.get('name') pos = {} From 793063bf4eb324d606be83e7424691148e4aa227 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Tue, 10 Aug 2021 15:57:30 +0530 Subject: [PATCH 138/147] fix: pos return payment mode issue (#26875) --- erpnext/public/js/controllers/taxes_and_totals.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 53d5278bbf..9d8fcb64f7 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -47,7 +47,10 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ if (in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype) && this.frm.doc.is_pos && this.frm.doc.is_return) { - this.update_paid_amount_for_return(); + if (this.frm.doc.doctype == "Sales Invoice") { + this.set_total_amount_to_default_mop(); + } + this.calculate_paid_amount(); } // Sales person's commission @@ -730,7 +733,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } }, - update_paid_amount_for_return: function() { + set_total_amount_to_default_mop: function() { var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total; if(this.frm.doc.party_account_currency == this.frm.doc.currency) { @@ -743,17 +746,14 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ precision("base_grand_total") ); } - this.frm.doc.payments.find(pay => { if (pay.default) { pay.amount = total_amount_to_pay; } else { - pay.amount = 0.0 + pay.amount = 0.0; } }); this.frm.refresh_fields(); - - this.calculate_paid_amount(); }, set_default_payment: function(total_amount_to_pay, update_paid_amount) { From 363225d2ba4d411beeae1d68950412e5c15238ea Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 10 Aug 2021 16:37:23 +0530 Subject: [PATCH 139/147] fix(asset): incorrect date difference calculation (#26805) --- erpnext/assets/doctype/asset/test_asset.py | 10 +++++----- erpnext/regional/india/utils.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 59fbe3b030..e23a715452 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -639,7 +639,7 @@ class TestAsset(unittest.TestCase): asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') asset = frappe.get_doc('Asset', asset_name) asset.calculate_depreciation = 1 - asset.available_for_use_date = '2030-06-12' + asset.available_for_use_date = '2030-07-12' asset.purchase_date = '2030-01-01' asset.append("finance_books", { "expected_value_after_useful_life": 1000, @@ -653,10 +653,10 @@ class TestAsset(unittest.TestCase): self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0) expected_schedules = [ - ["2030-12-31", 1106.85, 1106.85], - ["2031-12-31", 3446.58, 4553.43], - ["2032-12-31", 1723.29, 6276.72], - ["2033-06-12", 723.28, 7000.00] + ["2030-12-31", 942.47, 942.47], + ["2031-12-31", 3528.77, 4471.24], + ["2032-12-31", 1764.38, 6235.62], + ["2033-07-12", 764.38, 7000.00] ] schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 88c350ac89..a152797a5d 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -851,7 +851,7 @@ def get_depreciation_amount(asset, depreciable_value, row): # if its the first depreciation 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 - diff = date_diff(asset.available_for_use_date, row.depreciation_start_date) + diff = date_diff(row.depreciation_start_date, asset.available_for_use_date) if diff <= 180: rate_of_depreciation = rate_of_depreciation / 2 frappe.msgprint( From a7e0805039c997caf15f777d8a0a12ae83c91243 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 10 Aug 2021 17:16:15 +0530 Subject: [PATCH 140/147] fix(e-invoicing): cannot cancel invoice if IRN cancelled on portal (#26879) --- erpnext/patches.txt | 1 + .../v12_0/show_einvoice_irn_cancelled_field.py | 12 ++++++++++++ erpnext/regional/india/setup.py | 4 ++-- 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index ada3badd7c..a2b99dc934 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -294,6 +294,7 @@ erpnext.patches.v13_0.update_level_in_bom #1234sswef erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry erpnext.patches.v13_0.update_subscription_status_in_memberships erpnext.patches.v13_0.update_amt_in_work_order_required_items +erpnext.patches.v12_0.show_einvoice_irn_cancelled_field erpnext.patches.v13_0.delete_orphaned_tables erpnext.patches.v13_0.update_export_type_for_gst erpnext.patches.v13_0.update_tds_check_field #3 diff --git a/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py b/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py new file mode 100644 index 0000000000..2319c17b34 --- /dev/null +++ b/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + irn_cancelled_field = frappe.db.exists('Custom Field', {'dt': 'Sales Invoice', 'fieldname': 'irn_cancelled'}) + if irn_cancelled_field: + frappe.db.set_value('Custom Field', irn_cancelled_field, 'depends_on', 'eval: doc.irn') + frappe.db.set_value('Custom Field', irn_cancelled_field, 'read_only', 0) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index e9372f9b8f..b4f146ce57 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -457,7 +457,7 @@ def make_custom_fields(update=True): depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1, - depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), + depends_on='eval: doc.irn', allow_on_submit=1, insert_after='customer'), dict(fieldname='eway_bill_validity', label='E-Way Bill Validity', fieldtype='Data', no_copy=1, print_hide=1, depends_on='ewaybill', read_only=1, allow_on_submit=1, insert_after='ewaybill'), @@ -985,4 +985,4 @@ def create_gratuity_rule(): def update_accounts_settings_for_taxes(): if frappe.db.count('Company') == 1: - frappe.db.set_value('Accounts Settings', None, "add_taxes_from_item_tax_template", 0) \ No newline at end of file + frappe.db.set_value('Accounts Settings', None, "add_taxes_from_item_tax_template", 0) From 4ee6e32d74da3c7277a16a5dc48d3af2728e5ecf Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Tue, 10 Aug 2021 13:14:11 +0530 Subject: [PATCH 141/147] test(fix): fixed test case --- .../doctype/coupon_code/test_coupon_code.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py index 622bd33e20..5af12cde06 100644 --- a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py +++ b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py @@ -57,7 +57,7 @@ def test_create_test_data(): }) item_price.insert() # create test item pricing rule - if not frappe.db.exists("Pricing Rule","_Test Pricing Rule for _Test Item"): + if not frappe.db.exists("Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}): item_pricing_rule = frappe.get_doc({ "doctype": "Pricing Rule", "title": "_Test Pricing Rule for _Test Item", @@ -86,14 +86,15 @@ def test_create_test_data(): sales_partner.insert() # create test item coupon code if not frappe.db.exists("Coupon Code", "SAVE30"): + pricing_rule = frappe.db.get_value("Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}, ['name']) coupon_code = frappe.get_doc({ - "doctype": "Coupon Code", - "coupon_name":"SAVE30", - "coupon_code":"SAVE30", - "pricing_rule": "_Test Pricing Rule for _Test Item", - "valid_from": "2014-01-01", - "maximum_use":1, - "used":0 + "doctype": "Coupon Code", + "coupon_name":"SAVE30", + "coupon_code":"SAVE30", + "pricing_rule": pricing_rule, + "valid_from": "2014-01-01", + "maximum_use":1, + "used":0 }) coupon_code.insert() @@ -102,7 +103,7 @@ class TestCouponCode(unittest.TestCase): test_create_test_data() def tearDown(self): - frappe.set_user("Administrator") + frappe.set_user("Administrator") def test_sales_order_with_coupon_code(self): frappe.db.set_value("Coupon Code", "SAVE30", "used", 0) From 49b7c7575e9bc604fb9132929a63fb9b4bb1e484 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Tue, 10 Aug 2021 18:35:46 +0530 Subject: [PATCH 142/147] refactor: code cleanup --- .../promotional_scheme/promotional_scheme.py | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py index f4ee1887c4..0fade84cfd 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py @@ -25,12 +25,14 @@ product_discount_fields = ['free_item', 'free_qty', 'free_item_uom', class PromotionalScheme(Document): def validate(self): + if not self.selling and not self.buying: + frappe.throw(_("Atleast one of the Selling or Buying must be selected")) if not (self.price_discount_slabs or self.product_discount_slabs): frappe.throw(_("Price or product discount slabs are required")) def on_update(self): - data = frappe.get_all( + pricing_rules = frappe.get_all( 'Pricing Rule', fields = ["promotional_scheme_id", "name", "creation"], filters = { @@ -39,15 +41,15 @@ class PromotionalScheme(Document): }, order_by = 'creation asc', ) or {} - self.update_pricing_rules(data) + self.update_pricing_rules(pricing_rules) - def update_pricing_rules(self, data): + def update_pricing_rules(self, pricing_rules): rules = {} count = 0 names = [] - for d in data: - names.append(d.name) - rules[d.get('promotional_scheme_id')] = names + for rule in pricing_rules: + names.append(rule.name) + rules[rule.get('promotional_scheme_id')] = names docs = get_pricing_rules(self, rules) @@ -64,9 +66,9 @@ class PromotionalScheme(Document): frappe.msgprint(_("New {0} pricing rules are created").format(count)) def on_trash(self): - for d in frappe.get_all('Pricing Rule', + for rule in frappe.get_all('Pricing Rule', {'promotional_scheme': self.name}): - frappe.delete_doc('Pricing Rule', d.name) + frappe.delete_doc('Pricing Rule', rule.name) def get_pricing_rules(doc, rules = {}): new_doc = [] @@ -107,17 +109,20 @@ def _get_pricing_rules(doc, child_doc, discount_fields, rules = {}): new_doc.append(pr) else: - for i in range(len(args.get(applicable_for))) : - + applicable_for_values = args.get(applicable_for) or [] + for applicable_for_value in applicable_for_values: pr = frappe.new_doc("Pricing Rule") pr.title = doc.name temp_args = args.copy() - temp_args[applicable_for] = args[applicable_for][i] + temp_args[applicable_for] = applicable_for_value pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d) new_doc.append(pr) return new_doc + + + def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields): pr.update(args) for field in (other_fields + discount_fields): From af9863146dea5c41b4eee4a0024d6cac1cacc2b6 Mon Sep 17 00:00:00 2001 From: Marica Date: Tue, 10 Aug 2021 18:49:07 +0530 Subject: [PATCH 143/147] fix: Grammar in Error message --- .../accounts/doctype/promotional_scheme/promotional_scheme.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py index 0fade84cfd..3d7a891f33 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py @@ -26,7 +26,7 @@ product_discount_fields = ['free_item', 'free_qty', 'free_item_uom', class PromotionalScheme(Document): def validate(self): if not self.selling and not self.buying: - frappe.throw(_("Atleast one of the Selling or Buying must be selected")) + frappe.throw(_("Either 'Selling' or 'Buying' must be selected"), title=_("Mandatory")) if not (self.price_discount_slabs or self.product_discount_slabs): frappe.throw(_("Price or product discount slabs are required")) From 66784a16cb20088c870261cf2871d0c59f11f862 Mon Sep 17 00:00:00 2001 From: Anuja Pawar <60467153+Anuja-pawar@users.noreply.github.com> Date: Tue, 10 Aug 2021 19:38:16 +0530 Subject: [PATCH 144/147] fix: Sales Return cancellation if linked with Payment Entry (#26883) --- .../purchase_invoice/purchase_invoice.py | 4 +- .../doctype/sales_invoice/sales_invoice.py | 42 ++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index d7d9a3886a..85df225868 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -22,7 +22,7 @@ from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accoun from frappe.model.mapper import get_mapped_doc from six import iteritems from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_party, update_linked_doc,\ - unlink_inter_company_doc + unlink_inter_company_doc, check_if_return_invoice_linked_with_payment_entry from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details from erpnext.accounts.deferred_revenue import validate_service_stop_date from erpnext.stock.doctype.purchase_receipt.purchase_receipt import get_item_account_wise_additional_cost @@ -988,6 +988,8 @@ class PurchaseInvoice(BuyingController): }, item=self)) def on_cancel(self): + check_if_return_invoice_linked_with_payment_entry(self) + super(PurchaseInvoice, self).on_cancel() self.check_on_hold_or_closed_status() diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index eba8ba830f..ba1e729a46 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -290,6 +290,8 @@ class SalesInvoice(SellingController): self.update_time_sheet(None) def on_cancel(self): + check_if_return_invoice_linked_with_payment_entry(self) + super(SalesInvoice, self).on_cancel() self.check_sales_order_on_hold_or_close("sales_order") @@ -971,7 +973,7 @@ class SalesInvoice(SellingController): def set_asset_status(self, asset): if self.is_return: asset.set_status() - else: + else: asset.set_status("Sold" if self.docstatus==1 else None) def make_loyalty_point_redemption_gle(self, gl_entries): @@ -1939,3 +1941,41 @@ def create_dunning(source_name, target_doc=None): } }, target_doc, set_missing_values) return doclist + +def check_if_return_invoice_linked_with_payment_entry(self): + # If a Return invoice is linked with payment entry along with other invoices, + # the cancellation of the Return causes allocated amount to be greater than paid + + if not frappe.db.get_single_value('Accounts Settings', 'unlink_payment_on_cancellation_of_invoice'): + return + + payment_entries = [] + if self.is_return and self.return_against: + invoice = self.return_against + else: + invoice = self.name + + payment_entries = frappe.db.sql_list(""" + SELECT + t1.name + FROM + `tabPayment Entry` t1, `tabPayment Entry Reference` t2 + WHERE + t1.name = t2.parent + and t1.docstatus = 1 + and t2.reference_name = %s + and t2.allocated_amount < 0 + """, invoice) + + links_to_pe = [] + if payment_entries: + for payment in payment_entries: + payment_entry = frappe.get_doc("Payment Entry", payment) + if len(payment_entry.references) > 1: + links_to_pe.append(payment_entry.name) + if links_to_pe: + payment_entries_link = [get_link_to_form('Payment Entry', name, label=name) for name in links_to_pe] + message = _("Please cancel and amend the Payment Entry") + message += " " + ", ".join(payment_entries_link) + " " + message += _("to unallocate the amount of this Return Invoice before cancelling it.") + frappe.throw(message) From 1a39d1a3115605d7ccaf6285e7664197f1ff01a8 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 10 Aug 2021 19:38:39 +0530 Subject: [PATCH 145/147] fix: cost center & account validation in Sales/Purchase Taxes and Charges (#26881) --- .../sales_taxes_and_charges_template.py | 4 +++- .../test_records.json | 8 +++++++ erpnext/controllers/accounts_controller.py | 21 +++++++++++++++++++ erpnext/public/js/controllers/accounts.js | 8 +++++++ erpnext/setup/doctype/company/company.py | 6 +++--- .../setup_wizard/operations/taxes_setup.py | 3 ++- 6 files changed, 45 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py index 52d19d54a8..8f9eb6577b 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py @@ -6,7 +6,7 @@ import frappe from frappe import _ from frappe.utils import flt from frappe.model.document import Document -from erpnext.controllers.accounts_controller import validate_taxes_and_charges, validate_inclusive_tax +from erpnext.controllers.accounts_controller import validate_taxes_and_charges, validate_inclusive_tax, validate_cost_center, validate_account_head class SalesTaxesandChargesTemplate(Document): def validate(self): @@ -39,6 +39,8 @@ def valdiate_taxes_and_charges_template(doc): for tax in doc.get("taxes"): validate_taxes_and_charges(tax) + validate_account_head(tax, doc) + validate_cost_center(tax, doc) validate_inclusive_tax(tax, doc) def validate_disabled(doc): diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_records.json b/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_records.json index 2b737b9804..74db08d5b8 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_records.json +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_records.json @@ -8,6 +8,7 @@ "charge_type": "On Net Total", "description": "VAT", "doctype": "Sales Taxes and Charges", + "cost_center": "Main - _TC", "parentfield": "taxes", "rate": 6 }, @@ -16,6 +17,7 @@ "charge_type": "On Net Total", "description": "Service Tax", "doctype": "Sales Taxes and Charges", + "cost_center": "Main - _TC", "parentfield": "taxes", "rate": 6.36 } @@ -114,6 +116,7 @@ "charge_type": "On Net Total", "description": "VAT", "doctype": "Sales Taxes and Charges", + "cost_center": "Main - _TC", "parentfield": "taxes", "rate": 12 }, @@ -122,6 +125,7 @@ "charge_type": "On Net Total", "description": "Service Tax", "doctype": "Sales Taxes and Charges", + "cost_center": "Main - _TC", "parentfield": "taxes", "rate": 4 } @@ -137,6 +141,7 @@ "charge_type": "On Net Total", "description": "VAT", "doctype": "Sales Taxes and Charges", + "cost_center": "Main - _TC", "parentfield": "taxes", "rate": 12 }, @@ -145,6 +150,7 @@ "charge_type": "On Net Total", "description": "Service Tax", "doctype": "Sales Taxes and Charges", + "cost_center": "Main - _TC", "parentfield": "taxes", "rate": 4 } @@ -160,6 +166,7 @@ "charge_type": "On Net Total", "description": "VAT", "doctype": "Sales Taxes and Charges", + "cost_center": "Main - _TC", "parentfield": "taxes", "rate": 12 }, @@ -168,6 +175,7 @@ "charge_type": "On Net Total", "description": "Service Tax", "doctype": "Sales Taxes and Charges", + "cost_center": "Main - _TC", "parentfield": "taxes", "rate": 4 } diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index d65efa24b4..73d2411ede 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1437,6 +1437,27 @@ def validate_taxes_and_charges(tax): tax.rate = None +def validate_account_head(tax, doc): + company = frappe.get_cached_value('Account', + tax.account_head, 'company') + + if company != doc.company: + frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}') + .format(tax.idx, frappe.bold(tax.account_head), frappe.bold(doc.company)), title=_('Invalid Account')) + + +def validate_cost_center(tax, doc): + if not tax.cost_center: + return + + company = frappe.get_cached_value('Cost Center', + tax.cost_center, 'company') + + if company != doc.company: + frappe.throw(_('Row {0}: Cost Center {1} does not belong to Company {2}') + .format(tax.idx, frappe.bold(tax.cost_center), frappe.bold(doc.company)), title=_('Invalid Cost Center')) + + def validate_inclusive_tax(tax, doc): def _on_previous_row_error(row_range): throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, row_range)) diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index 7b997a1153..84c717676c 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -31,6 +31,14 @@ frappe.ui.form.on(cur_frm.doctype, { } } }); + frm.set_query("cost_center", "taxes", function(doc) { + return { + filters: { + "company": doc.company, + "is_group": 0 + } + }; + }); } }, validate: function(frm) { diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 8755125c81..95cbf5150c 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -108,6 +108,9 @@ class Company(NestedSet): frappe.flags.country_change = True self.create_default_accounts() self.create_default_warehouses() + + if not frappe.db.get_value("Cost Center", {"is_group": 0, "company": self.name}): + self.create_default_cost_center() if frappe.flags.country_change: install_country_fixtures(self.name, self.country) @@ -117,9 +120,6 @@ class Company(NestedSet): from erpnext.setup.setup_wizard.operations.install_fixtures import install_post_company_fixtures install_post_company_fixtures(frappe._dict({'company_name': self.name})) - if not frappe.db.get_value("Cost Center", {"is_group": 0, "company": self.name}): - self.create_default_cost_center() - if not frappe.local.flags.ignore_chart_of_accounts: self.set_default_accounts() if self.default_cash_account: diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index c7cc000518..c69f3d0b3f 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -124,7 +124,8 @@ def make_taxes_and_charges_template(company_name, doctype, template): account_data = tax_row.get('account_head') tax_row_defaults = { 'category': 'Total', - 'charge_type': 'On Net Total' + 'charge_type': 'On Net Total', + 'cost_center': frappe.db.get_value('Company', company_name, 'cost_center') } if doctype == 'Purchase Taxes and Charges Template': From a6aa6cd7d63eb426b986d995985985c2aae4f553 Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Tue, 10 Aug 2021 20:32:15 +0530 Subject: [PATCH 146/147] fix: timesheet amount issue (#25993) * fix: timesheet amount issue * fix: timesheet detail rate conversion * fix: condition to check timesheet currency * fix: removing console statement --- .../doctype/sales_invoice/sales_invoice.js | 55 ++++++++++--------- .../sales_invoice_timesheet.json | 14 ++++- .../projects/doctype/timesheet/timesheet.json | 3 +- .../projects/doctype/timesheet/timesheet.py | 16 +++++- 4 files changed, 58 insertions(+), 30 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 56f11650ff..568e7721a3 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -447,6 +447,15 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte this.frm.refresh_field("outstanding_amount"); this.frm.refresh_field("paid_amount"); this.frm.refresh_field("base_paid_amount"); + }, + + currency() { + this._super(); + $.each(cur_frm.doc.timesheets, function(i, d) { + let row = frappe.get_doc(d.doctype, d.name) + set_timesheet_detail_rate(row.doctype, row.name, cur_frm.doc.currency, row.timesheet_detail) + }); + calculate_total_billing_amount(cur_frm) } }); @@ -846,7 +855,8 @@ frappe.ui.form.on('Sales Invoice', { 'time_sheet': row.parent, 'billing_hours': row.billing_hours, 'billing_amount': flt(row.billing_amount) * flt(exchange_rate), - 'timesheet_detail': row.name + 'timesheet_detail': row.name, + 'project_name': row.project_name }); frm.refresh_field('timesheets'); calculate_total_billing_amount(frm); @@ -965,43 +975,34 @@ frappe.ui.form.on('Sales Invoice', { } }) -frappe.ui.form.on('Sales Invoice Timesheet', { - time_sheet: function(frm, cdt, cdn){ - var d = locals[cdt][cdn]; - if(d.time_sheet) { - frappe.call({ - method: "erpnext.projects.doctype.timesheet.timesheet.get_timesheet_data", - args: { - 'name': d.time_sheet, - 'project': frm.doc.project || null - }, - callback: function(r, rt) { - if(r.message){ - let data = r.message; - frappe.model.set_value(cdt, cdn, "billing_hours", data.billing_hours); - frappe.model.set_value(cdt, cdn, "billing_amount", data.billing_amount); - frappe.model.set_value(cdt, cdn, "timesheet_detail", data.timesheet_detail); - calculate_total_billing_amount(frm) - } - } - }) - } - } -}) - var calculate_total_billing_amount = function(frm) { var doc = frm.doc; doc.total_billing_amount = 0.0 - if(doc.timesheets) { + if (doc.timesheets) { $.each(doc.timesheets, function(index, data){ - doc.total_billing_amount += data.billing_amount + doc.total_billing_amount += flt(data.billing_amount) }) } refresh_field('total_billing_amount') } +var set_timesheet_detail_rate = function(cdt, cdn, currency, timelog) { + frappe.call({ + method: "erpnext.projects.doctype.timesheet.timesheet.get_timesheet_detail_rate", + args: { + timelog: timelog, + currency: currency + }, + callback: function(r) { + if (!r.exc && r.message) { + frappe.model.set_value(cdt, cdn, 'billing_amount', r.message); + } + } + }); +} + var select_loyalty_program = function(frm, loyalty_programs) { var dialog = new frappe.ui.Dialog({ title: __("Select Loyalty Program"), diff --git a/erpnext/accounts/doctype/sales_invoice_timesheet/sales_invoice_timesheet.json b/erpnext/accounts/doctype/sales_invoice_timesheet/sales_invoice_timesheet.json index f069e8dd0b..c90297328e 100644 --- a/erpnext/accounts/doctype/sales_invoice_timesheet/sales_invoice_timesheet.json +++ b/erpnext/accounts/doctype/sales_invoice_timesheet/sales_invoice_timesheet.json @@ -9,7 +9,9 @@ "description", "billing_hours", "billing_amount", + "column_break_5", "time_sheet", + "project_name", "timesheet_detail" ], "fields": [ @@ -61,11 +63,21 @@ "in_list_view": 1, "label": "Description", "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "project_name", + "fieldtype": "Data", + "label": "Project Name", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2021-05-20 22:33:57.234846", + "modified": "2021-06-08 14:43:02.748981", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Timesheet", diff --git a/erpnext/projects/doctype/timesheet/timesheet.json b/erpnext/projects/doctype/timesheet/timesheet.json index 75f7478ed1..be6771e56f 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.json +++ b/erpnext/projects/doctype/timesheet/timesheet.json @@ -310,6 +310,7 @@ "read_only": 1 }, { + "default": "1", "fieldname": "exchange_rate", "fieldtype": "Float", "label": "Exchange Rate" @@ -319,7 +320,7 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-18 16:10:08.249619", + "modified": "2021-06-09 12:08:53.930200", "modified_by": "Administrator", "module": "Projects", "name": "Timesheet", diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index ae38d4ca19..a0042eb7d1 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -227,7 +227,8 @@ def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to return frappe.db.sql("""SELECT tsd.name as name, tsd.parent as parent, tsd.billing_hours as billing_hours, tsd.billing_amount as billing_amount, tsd.activity_type as activity_type, - tsd.description as description, ts.currency as currency + tsd.description as description, ts.currency as currency, + tsd.project_name as project_name FROM `tabTimesheet Detail` tsd INNER JOIN `tabTimesheet` ts ON ts.name = tsd.parent WHERE tsd.parenttype = 'Timesheet' @@ -235,6 +236,19 @@ def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to and tsd.is_billable = 1 and tsd.sales_invoice is null""".format(condition), {'project': project, 'parent': parent, 'from_time': from_time, 'to_time': to_time}, as_dict=1) +@frappe.whitelist() +def get_timesheet_detail_rate(timelog, currency): + timelog_detail = frappe.db.sql("""SELECT tsd.billing_amount as billing_amount, + ts.currency as currency FROM `tabTimesheet Detail` tsd + INNER JOIN `tabTimesheet` ts ON ts.name=tsd.parent + WHERE tsd.name = '{0}'""".format(timelog), as_dict = 1)[0] + + if timelog_detail.currency: + exchange_rate = get_exchange_rate(timelog_detail.currency, currency) + + return timelog_detail.billing_amount * exchange_rate + return timelog_detail.billing_amount + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_timesheet(doctype, txt, searchfield, start, page_len, filters): From 1167a9bf9481ddc806343c7a7b7c9d4ef93b1a7d Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 10 Aug 2021 23:18:23 +0530 Subject: [PATCH 147/147] fix: unsetting of payment if no pos profile found (#26891) * fix: unseting of payment if no pos profile found (#26884) (cherry picked from commit b614834efedbef572e0567828f0d9d82e81331ee) # Conflicts: # erpnext/public/js/controllers/taxes_and_totals.js * fix: conflicts * fix: conflicts * fix: conflicts * fix: conflicts Co-authored-by: Afshan <33727827+AfshanKhan@users.noreply.github.com> Co-authored-by: Afshan --- erpnext/controllers/taxes_and_totals.py | 6 +----- erpnext/public/js/controllers/taxes_and_totals.js | 2 -- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 099c7d4346..05edb2530c 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -679,17 +679,13 @@ class calculate_taxes_and_totals(object): default_mode_of_payment = frappe.db.get_value('POS Payment Method', {'parent': self.doc.pos_profile, 'default': 1}, ['mode_of_payment'], as_dict=1) - self.doc.payments = [] - if default_mode_of_payment: + self.doc.payments = [] self.doc.append('payments', { 'mode_of_payment': default_mode_of_payment.mode_of_payment, 'amount': total_amount_to_pay, 'default': 1 }) - else: - self.doc.is_pos = 0 - self.doc.pos_profile = '' self.calculate_paid_amount() diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 9d8fcb64f7..90cb555939 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -749,8 +749,6 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ this.frm.doc.payments.find(pay => { if (pay.default) { pay.amount = total_amount_to_pay; - } else { - pay.amount = 0.0; } }); this.frm.refresh_fields();