diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 38d8a62f07..5a5c448026 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -132,16 +132,10 @@ def allow_regional(fn): return caller -def get_last_membership(): +def get_last_membership(member): '''Returns last membership if exists''' last_membership = frappe.get_all('Membership', 'name,to_date,membership_type', - dict(member=frappe.session.user, paid=1), order_by='to_date desc', limit=1) + dict(member=member, paid=1), order_by='to_date desc', limit=1) - return last_membership and last_membership[0] - -def is_member(): - '''Returns true if the user is still a member''' - last_membership = get_last_membership() - if last_membership and getdate(last_membership.to_date) > getdate(): - return True - return False + if last_membership: + return last_membership[0] diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py index 113bea0064..533eda31d5 100644 --- a/erpnext/accounts/doctype/account/test_account.py +++ b/erpnext/accounts/doctype/account/test_account.py @@ -254,7 +254,8 @@ def create_account(**kwargs): account_name = kwargs.get('account_name'), account_type = kwargs.get('account_type'), parent_account = kwargs.get('parent_account'), - company = kwargs.get('company') + company = kwargs.get('company'), + account_currency = kwargs.get('account_currency') )) account.save() diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js index 9a6c389339..65c5ff1cea 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js @@ -2,7 +2,6 @@ // For license information, please see license.txt frappe.ui.form.on('Accounting Dimension', { - refresh: function(frm) { frm.set_query('document_type', () => { let invalid_doctypes = frappe.model.core_doctypes_list; diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index f888d9e038..52e9ff8b76 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -203,7 +203,7 @@ def get_dimension_with_children(doctype, dimension): return all_dimensions @frappe.whitelist() -def get_dimension_filters(): +def get_dimensions(with_cost_center_and_project=False): dimension_filters = frappe.db.sql(""" SELECT label, fieldname, document_type FROM `tabAccounting Dimension` @@ -214,6 +214,18 @@ def get_dimension_filters(): FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p WHERE c.parent = p.name""", as_dict=1) + if with_cost_center_and_project: + dimension_filters.extend([ + { + 'fieldname': 'cost_center', + 'document_type': 'Cost Center' + }, + { + 'fieldname': 'project', + 'document_type': 'Project' + } + ]) + default_dimensions_map = {} for dimension in default_dimensions: default_dimensions_map.setdefault(dimension.company, {}) diff --git a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py index 104880f6f3..fc1d7e344a 100644 --- a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py @@ -11,37 +11,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import d class TestAccountingDimension(unittest.TestCase): def setUp(self): - frappe.set_user("Administrator") - - if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}): - dimension = frappe.get_doc({ - "doctype": "Accounting Dimension", - "document_type": "Department", - }).insert() - else: - dimension1 = frappe.get_doc("Accounting Dimension", "Department") - dimension1.disabled = 0 - dimension1.save() - - if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}): - dimension1 = frappe.get_doc({ - "doctype": "Accounting Dimension", - "document_type": "Location", - }) - - dimension1.append("dimension_defaults", { - "company": "_Test Company", - "reference_document": "Location", - "default_dimension": "Block 1", - "mandatory_for_bs": 1 - }) - - dimension1.insert() - dimension1.save() - else: - dimension1 = frappe.get_doc("Accounting Dimension", "Location") - dimension1.disabled = 0 - dimension1.save() + create_dimension() def test_dimension_against_sales_invoice(self): si = create_sales_invoice(do_not_save=1) @@ -101,6 +71,38 @@ class TestAccountingDimension(unittest.TestCase): def tearDown(self): disable_dimension() +def create_dimension(): + frappe.set_user("Administrator") + + if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}): + frappe.get_doc({ + "doctype": "Accounting Dimension", + "document_type": "Department", + }).insert() + else: + dimension = frappe.get_doc("Accounting Dimension", "Department") + dimension.disabled = 0 + dimension.save() + + if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}): + dimension1 = frappe.get_doc({ + "doctype": "Accounting Dimension", + "document_type": "Location", + }) + + dimension1.append("dimension_defaults", { + "company": "_Test Company", + "reference_document": "Location", + "default_dimension": "Block 1", + "mandatory_for_bs": 1 + }) + + dimension1.insert() + dimension1.save() + else: + dimension1 = frappe.get_doc("Accounting Dimension", "Location") + dimension1.disabled = 0 + dimension1.save() def disable_dimension(): dimension1 = frappe.get_doc("Accounting Dimension", "Department") diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/__init__.py b/erpnext/accounts/doctype/accounting_dimension_filter/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js new file mode 100644 index 0000000000..74b7b51676 --- /dev/null +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js @@ -0,0 +1,82 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Accounting Dimension Filter', { + refresh: function(frm, cdt, cdn) { + if (frm.doc.accounting_dimension) { + frm.set_df_property('dimensions', 'label', frm.doc.accounting_dimension, cdn, 'dimension_value'); + } + + let help_content = + ` + +
+

+ + {{__('Note: On checking Is Mandatory the accounting dimension will become mandatory against that specific account for all accounting transactions')}} +

+
`; + + frm.set_df_property('dimension_filter_help', 'options', help_content); + }, + onload: function(frm) { + frm.set_query('applicable_on_account', 'accounts', function() { + return { + filters: { + 'company': frm.doc.company + } + }; + }); + + frappe.db.get_list('Accounting Dimension', + {fields: ['document_type']}).then((res) => { + let options = ['Cost Center', 'Project']; + + res.forEach((dimension) => { + options.push(dimension.document_type); + }); + + frm.set_df_property('accounting_dimension', 'options', options); + }); + + frm.trigger('setup_filters'); + }, + + setup_filters: function(frm) { + let filters = {}; + + if (frm.doc.accounting_dimension) { + frappe.model.with_doctype(frm.doc.accounting_dimension, function() { + if (frappe.model.is_tree(frm.doc.accounting_dimension)) { + filters['is_group'] = 0; + } + + if (frappe.meta.has_field(frm.doc.accounting_dimension, 'company')) { + filters['company'] = frm.doc.company; + } + + frm.set_query('dimension_value', 'dimensions', function() { + return { + filters: filters + }; + }); + }); + } + }, + + accounting_dimension: function(frm) { + frm.clear_table("dimensions"); + let row = frm.add_child("dimensions"); + row.accounting_dimension = frm.doc.accounting_dimension; + frm.refresh_field("dimensions"); + frm.trigger('setup_filters'); + }, +}); + +frappe.ui.form.on('Allowed Dimension', { + dimensions_add: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + row.accounting_dimension = frm.doc.accounting_dimension; + frm.refresh_field("dimensions"); + } +}); \ No newline at end of file diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json new file mode 100644 index 0000000000..c0327ad0ad --- /dev/null +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json @@ -0,0 +1,134 @@ +{ + "actions": [], + "autoname": "format:{accounting_dimension}-{#####}", + "creation": "2020-11-08 18:28:11.906146", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "accounting_dimension", + "disabled", + "column_break_2", + "company", + "allow_or_restrict", + "section_break_4", + "accounts", + "column_break_6", + "dimensions", + "section_break_10", + "dimension_filter_help" + ], + "fields": [ + { + "fieldname": "accounting_dimension", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Accounting Dimension", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "hide_border": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "allow_or_restrict", + "fieldtype": "Select", + "label": "Allow Or Restrict Dimension", + "options": "Allow\nRestrict", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "accounts", + "fieldtype": "Table", + "label": "Applicable On Account", + "options": "Applicable On Account", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "depends_on": "eval:doc.accounting_dimension", + "fieldname": "dimensions", + "fieldtype": "Table", + "label": "Applicable Dimension", + "options": "Allowed Dimension", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "dimension_filter_help", + "fieldtype": "HTML", + "label": "Dimension Filter Help", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "section_break_10", + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-12-16 15:27:23.659285", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Accounting Dimension Filter", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py new file mode 100644 index 0000000000..6aef9caa74 --- /dev/null +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# Copyright, (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _, scrub +from frappe.model.document import Document + +class AccountingDimensionFilter(Document): + def validate(self): + self.validate_applicable_accounts() + + def validate_applicable_accounts(self): + accounts = frappe.db.sql( + """ + SELECT a.applicable_on_account as account + FROM `tabApplicable On Account` a, `tabAccounting Dimension Filter` d + WHERE d.name = a.parent + and d.name != %s + and d.accounting_dimension = %s + """, (self.name, self.accounting_dimension), as_dict=1) + + account_list = [d.account for d in accounts] + + for account in self.get('accounts'): + if account.applicable_on_account in account_list: + frappe.throw(_("Row {0}: {1} account already applied for Accounting Dimension {2}").format( + account.idx, frappe.bold(account.applicable_on_account), frappe.bold(self.accounting_dimension))) + +def get_dimension_filter_map(): + filters = frappe.db.sql(""" + SELECT + a.applicable_on_account, d.dimension_value, p.accounting_dimension, + p.allow_or_restrict, a.is_mandatory + FROM + `tabApplicable On Account` a, `tabAllowed Dimension` d, + `tabAccounting Dimension Filter` p + WHERE + p.name = a.parent + AND p.disabled = 0 + AND p.name = d.parent + """, as_dict=1) + + dimension_filter_map = {} + + for f in filters: + f.fieldname = scrub(f.accounting_dimension) + + build_map(dimension_filter_map, f.fieldname, f.applicable_on_account, f.dimension_value, + f.allow_or_restrict, f.is_mandatory) + + return dimension_filter_map + +def build_map(map_object, dimension, account, filter_value, allow_or_restrict, is_mandatory): + map_object.setdefault((dimension, account), { + 'allowed_dimensions': [], + 'is_mandatory': is_mandatory, + 'allow_or_restrict': allow_or_restrict + }) + map_object[(dimension, account)]['allowed_dimensions'].append(filter_value) diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py new file mode 100644 index 0000000000..7877abd026 --- /dev/null +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import create_dimension, disable_dimension +from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError + +class TestAccountingDimensionFilter(unittest.TestCase): + def setUp(self): + create_dimension() + create_accounting_dimension_filter() + self.invoice_list = [] + + def test_allowed_dimension_validation(self): + si = create_sales_invoice(do_not_save=1) + si.items[0].cost_center = 'Main - _TC' + si.department = 'Accounts - _TC' + si.location = 'Block 1' + si.save() + + self.assertRaises(InvalidAccountDimensionError, si.submit) + self.invoice_list.append(si) + + def test_mandatory_dimension_validation(self): + si = create_sales_invoice(do_not_save=1) + si.department = '' + si.location = 'Block 1' + + # Test with no department for Sales Account + si.items[0].department = '' + si.items[0].cost_center = '_Test Cost Center 2 - _TC' + si.save() + + self.assertRaises(MandatoryAccountDimensionError, si.submit) + self.invoice_list.append(si) + + def tearDown(self): + disable_dimension_filter() + disable_dimension() + + for si in self.invoice_list: + si.load_from_db() + if si.docstatus == 1: + si.cancel() + +def create_accounting_dimension_filter(): + if not frappe.db.get_value('Accounting Dimension Filter', + {'accounting_dimension': 'Cost Center'}): + frappe.get_doc({ + 'doctype': 'Accounting Dimension Filter', + 'accounting_dimension': 'Cost Center', + 'allow_or_restrict': 'Allow', + 'company': '_Test Company', + 'accounts': [{ + 'applicable_on_account': 'Sales - _TC', + }], + 'dimensions': [{ + 'accounting_dimension': 'Cost Center', + 'dimension_value': '_Test Cost Center 2 - _TC' + }] + }).insert() + else: + doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'}) + doc.disabled = 0 + doc.save() + + if not frappe.db.get_value('Accounting Dimension Filter', + {'accounting_dimension': 'Department'}): + frappe.get_doc({ + 'doctype': 'Accounting Dimension Filter', + 'accounting_dimension': 'Department', + 'allow_or_restrict': 'Allow', + 'company': '_Test Company', + 'accounts': [{ + 'applicable_on_account': 'Sales - _TC', + 'is_mandatory': 1 + }], + 'dimensions': [{ + 'accounting_dimension': 'Department', + 'dimension_value': 'Accounts - _TC' + }] + }).insert() + else: + doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Department'}) + doc.disabled = 0 + doc.save() + +def disable_dimension_filter(): + doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'}) + doc.disabled = 1 + doc.save() + + doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Department'}) + doc.disabled = 1 + doc.save() diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 41f9ce030a..a3c29b6d64 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -21,6 +21,7 @@ "book_asset_depreciation_entry_automatically", "add_taxes_from_item_tax_template", "automatically_fetch_payment_terms", + "delete_linked_ledger_entries", "deferred_accounting_settings_section", "automatically_process_deferred_accounting_entry", "book_deferred_entries_based_on", @@ -219,6 +220,12 @@ "fieldtype": "Select", "label": "Book Deferred Entries Based On", "options": "Days\nMonths" + }, + { + "default": "0", + "fieldname": "delete_linked_ledger_entries", + "fieldtype": "Check", + "label": "Delete Accounting and Stock Ledger Entries on deletion of Transaction" } ], "icon": "icon-cog", @@ -226,7 +233,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-10-13 11:32:52.268826", + "modified": "2021-01-05 13:04:00.118892", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", @@ -254,4 +261,4 @@ "sort_field": "modified", "sort_order": "ASC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/allowed_dimension/__init__.py b/erpnext/accounts/doctype/allowed_dimension/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json b/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json new file mode 100644 index 0000000000..7fe2a3c647 --- /dev/null +++ b/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json @@ -0,0 +1,43 @@ +{ + "actions": [], + "creation": "2020-11-08 18:22:36.001131", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "accounting_dimension", + "dimension_value" + ], + "fields": [ + { + "fieldname": "accounting_dimension", + "fieldtype": "Link", + "label": "Accounting Dimension", + "options": "DocType", + "read_only": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "dimension_value", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "options": "accounting_dimension", + "show_days": 1, + "show_seconds": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-11-23 09:56:19.744200", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Allowed Dimension", + "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/accounts/doctype/allowed_dimension/allowed_dimension.py b/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.py new file mode 100644 index 0000000000..c2afc1a262 --- /dev/null +++ b/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 AllowedDimension(Document): + pass diff --git a/erpnext/accounts/doctype/applicable_on_account/__init__.py b/erpnext/accounts/doctype/applicable_on_account/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json new file mode 100644 index 0000000000..95e98d0b67 --- /dev/null +++ b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json @@ -0,0 +1,46 @@ +{ + "actions": [], + "creation": "2020-11-08 18:20:00.944449", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "applicable_on_account", + "is_mandatory" + ], + "fields": [ + { + "fieldname": "applicable_on_account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Accounts", + "options": "Account", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "columns": 2, + "default": "0", + "fieldname": "is_mandatory", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Is Mandatory", + "show_days": 1, + "show_seconds": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-11-22 19:55:13.324136", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Applicable On Account", + "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/accounts/doctype/applicable_on_account/applicable_on_account.py b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.py new file mode 100644 index 0000000000..0fccaf302f --- /dev/null +++ b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 ApplicableOnAccount(Document): + pass diff --git a/erpnext/accounts/doctype/bank/bank.js b/erpnext/accounts/doctype/bank/bank.js index de9498e075..49b2b186c4 100644 --- a/erpnext/accounts/doctype/bank/bank.js +++ b/erpnext/accounts/doctype/bank/bank.js @@ -1,5 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt +frappe.provide('erpnext.integrations'); frappe.ui.form.on('Bank', { onload: function(frm) { @@ -20,7 +21,12 @@ frappe.ui.form.on('Bank', { frm.set_df_property('address_and_contact', 'hidden', 0); frappe.contacts.render_address_and_contact(frm); } - }, + if (frm.doc.plaid_access_token) { + frm.add_custom_button(__('Refresh Plaid Link'), () => { + new erpnext.integrations.refreshPlaidLink(frm.doc.plaid_access_token); + }); + } + } }); @@ -40,4 +46,79 @@ let add_fields_to_mapping_table = function (frm) { frm.doc.name).options = options; frm.fields_dict.bank_transaction_mapping.grid.refresh(); -}; \ No newline at end of file +}; + +erpnext.integrations.refreshPlaidLink = class refreshPlaidLink { + constructor(access_token) { + this.access_token = access_token; + this.plaidUrl = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js'; + this.init_config(); + } + + async init_config() { + this.plaid_env = await frappe.db.get_single_value('Plaid Settings', 'plaid_env'); + this.token = await this.get_link_token_for_update(); + this.init_plaid(); + } + + async get_link_token_for_update() { + const token = frappe.xcall( + 'erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_link_token_for_update', + { access_token: this.access_token } + ) + if (!token) { + frappe.throw(__('Cannot retrieve link token for update. Check Error Log for more information')); + } + return token; + } + + init_plaid() { + const me = this; + me.loadScript(me.plaidUrl) + .then(() => { + me.onScriptLoaded(me); + }) + .then(() => { + if (me.linkHandler) { + me.linkHandler.open(); + } + }) + .catch((error) => { + me.onScriptError(error); + }); + } + + loadScript(src) { + return new Promise(function (resolve, reject) { + if (document.querySelector("script[src='" + src + "']")) { + resolve(); + return; + } + const el = document.createElement('script'); + el.type = 'text/javascript'; + el.async = true; + el.src = src; + el.addEventListener('load', resolve); + el.addEventListener('error', reject); + el.addEventListener('abort', reject); + document.head.appendChild(el); + }); + } + + onScriptLoaded(me) { + me.linkHandler = Plaid.create({ + env: me.plaid_env, + token: me.token, + onSuccess: me.plaid_success + }); + } + + onScriptError(error) { + frappe.msgprint(__("There was an issue connecting to Plaid's authentication server. Check browser console for more information")); + console.log(error); + } + + plaid_success(token, response) { + frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' }); + } +}; diff --git a/erpnext/accounts/doctype/budget/budget.js b/erpnext/accounts/doctype/budget/budget.js index cadf1e7e0c..e162e3222d 100644 --- a/erpnext/accounts/doctype/budget/budget.js +++ b/erpnext/accounts/doctype/budget/budget.js @@ -1,24 +1,9 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt +frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on('Budget', { onload: function(frm) { - frm.set_query("cost_center", function() { - return { - filters: { - company: frm.doc.company - } - } - }) - - frm.set_query("project", function() { - return { - filters: { - company: frm.doc.company - } - } - }) - frm.set_query("account", "accounts", function() { return { filters: { @@ -26,16 +11,18 @@ frappe.ui.form.on('Budget', { report_type: "Profit and Loss", is_group: 0 } - } - }) - + }; + }); + frm.set_query("monthly_distribution", function() { return { filters: { fiscal_year: frm.doc.fiscal_year } - } - }) + }; + }); + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { diff --git a/erpnext/accounts/doctype/budget/test_budget.py b/erpnext/accounts/doctype/budget/test_budget.py index cd88b11761..c5ec23c829 100644 --- a/erpnext/accounts/doctype/budget/test_budget.py +++ b/erpnext/accounts/doctype/budget/test_budget.py @@ -122,8 +122,10 @@ class TestBudget(unittest.TestCase): frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") + project = frappe.get_value("Project", {"project_name": "_Test Project"}) + jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", project="_Test Project", posting_date=nowdate()) + "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", project=project, posting_date=nowdate()) self.assertRaises(BudgetError, jv.submit) @@ -147,8 +149,11 @@ class TestBudget(unittest.TestCase): budget = make_budget(budget_against="Project") + project = frappe.get_value("Project", {"project_name": "_Test Project"}) + jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 250000, "_Test Cost Center - _TC", project="_Test Project", posting_date=nowdate()) + "_Test Bank - _TC", 250000, "_Test Cost Center - _TC", + project=project, posting_date=nowdate()) self.assertRaises(BudgetError, jv.submit) @@ -184,9 +189,11 @@ class TestBudget(unittest.TestCase): if month > 9: month = 9 + project = frappe.get_value("Project", {"project_name": "_Test Project"}) for i in range(month + 1): jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True, project="_Test Project") + "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True, + project=project) self.assertTrue(frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name})) @@ -289,7 +296,7 @@ def make_budget(**args): budget = frappe.new_doc("Budget") if budget_against == "Project": - budget.project = "_Test Project" + budget.project = frappe.get_value("Project", {"project_name": "_Test Project"}) else: budget.cost_center =cost_center or "_Test Cost Center - _TC" diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index c441274908..b0a864f76c 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -11,8 +11,10 @@ from frappe.model.meta import get_field_precision from erpnext.accounts.party import validate_party_gle_currency, validate_party_frozen_disabled from erpnext.accounts.utils import get_account_currency from erpnext.accounts.utils import get_fiscal_year -from erpnext.exceptions import InvalidAccountCurrency +from erpnext.exceptions import InvalidAccountCurrency, InvalidAccountDimensionError, MandatoryAccountDimensionError from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_checks_for_pl_and_bs_accounts +from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import get_dimension_filter_map +from six import iteritems exclude_from_linked_with = True class GLEntry(Document): @@ -39,6 +41,7 @@ class GLEntry(Document): if not from_repost: self.validate_account_details(adv_adj) self.validate_dimensions_for_pl_and_bs() + self.validate_allowed_dimensions() validate_frozen_account(self.account, adv_adj) validate_balance_type(self.account, adv_adj) @@ -76,11 +79,9 @@ class GLEntry(Document): .format(self.voucher_type, self.voucher_no, self.account)) def validate_dimensions_for_pl_and_bs(self): - account_type = frappe.db.get_value("Account", self.account, "report_type") for dimension in get_checks_for_pl_and_bs_accounts(): - if account_type == "Profit and Loss" \ and self.company == dimension.company and dimension.mandatory_for_pl and not dimension.disabled: if not self.get(dimension.fieldname): @@ -93,6 +94,25 @@ class GLEntry(Document): frappe.throw(_("Accounting Dimension {0} is required for 'Balance Sheet' account {1}.") .format(dimension.label, self.account)) + def validate_allowed_dimensions(self): + dimension_filter_map = get_dimension_filter_map() + for key, value in iteritems(dimension_filter_map): + dimension = key[0] + account = key[1] + + if self.account == account: + if value['is_mandatory'] and not self.get(dimension): + frappe.throw(_("{0} is mandatory for account {1}").format( + frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), MandatoryAccountDimensionError) + + if value['allow_or_restrict'] == 'Allow': + if self.get(dimension) and self.get(dimension) not in value['allowed_dimensions']: + frappe.throw(_("Invalid value {0} for {1} against account {2}").format( + frappe.bold(self.get(dimension)), frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError) + else: + if self.get(dimension) and self.get(dimension) in value['allowed_dimensions']: + frappe.throw(_("Invalid value {0} for {1} against account {2}").format( + frappe.bold(self.get(dimension)), frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError) def check_pl_account(self): if self.is_opening=='Yes' and \ @@ -137,9 +157,10 @@ class GLEntry(Document): frappe.throw(_("{0} {1}: Cost Center {2} does not belong to Company {3}") .format(self.voucher_type, self.voucher_no, self.cost_center, self.company)) - if self.cost_center and _check_is_group(): - frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""") - .format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center))) + if not self.flags.from_repost and not self.voucher_type == 'Period Closing Voucher' \ + and self.cost_center and _check_is_group(): + frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""").format( + self.voucher_type, self.voucher_no, frappe.bold(self.cost_center))) def validate_party(self): validate_party_frozen_disabled(self.party_type, self.party) @@ -149,7 +170,7 @@ class GLEntry(Document): account_currency = get_account_currency(self.account) if not self.account_currency: - self.account_currency = company_currency + self.account_currency = account_currency or company_currency if account_currency != self.account_currency: frappe.throw(_("{0} {1}: Accounting Entry for {2} can only be made in currency: {3}") diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index ff12967155..37b03f3f0e 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -120,6 +120,8 @@ frappe.ui.form.on("Journal Entry", { } } }); + + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, voucher_type: function(frm){ @@ -197,6 +199,7 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({ this.load_defaults(); this.setup_queries(); this.setup_balance_formatter(); + erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype); }, onload_post_render: function() { @@ -222,15 +225,6 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({ return erpnext.journal_entry.account_query(me.frm); }); - me.frm.set_query("cost_center", "accounts", function(doc, cdt, cdn) { - return { - filters: { - company: me.frm.doc.company, - is_group: 0 - } - }; - }); - me.frm.set_query("party_type", "accounts", function(doc, cdt, cdn) { const row = locals[cdt][cdn]; @@ -406,6 +400,8 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({ } } cur_frm.cscript.update_totals(doc); + + erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'accounts'); }, }); diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py index b56f8e5fe2..5f003e022a 100644 --- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py @@ -160,7 +160,7 @@ class TestJournalEntry(unittest.TestCase): self.assertFalse(gle) def test_reverse_journal_entry(self): - from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry + from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry jv = make_journal_entry("_Test Bank USD - _TC", "Sales - _TC", 100, exchange_rate=50, save=False) @@ -299,15 +299,20 @@ class TestJournalEntry(unittest.TestCase): def test_jv_with_project(self): from erpnext.projects.doctype.project.test_project import make_project - project = make_project({ - 'project_name': 'Journal Entry Project', - 'project_template_name': 'Test Project Template', - 'start_date': '2020-01-01' - }) + + if not frappe.db.exists("Project", {"project_name": "Journal Entry Project"}): + project = make_project({ + 'project_name': 'Journal Entry Project', + 'project_template_name': 'Test Project Template', + 'start_date': '2020-01-01' + }) + project_name = project.name + else: + project_name = frappe.get_value("Project", {"project_name": "_Test Project"}) jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False) for d in jv.accounts: - d.project = project.project_name + d.project = project_name jv.voucher_type = "Bank Entry" jv.multi_currency = 0 jv.cheque_no = "112233" @@ -317,10 +322,10 @@ class TestJournalEntry(unittest.TestCase): expected_values = { "_Test Cash - _TC": { - "project": project.project_name + "project": project_name }, "_Test Bank - _TC": { - "project": project.project_name + "project": project_name } } diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.js b/erpnext/accounts/doctype/loyalty_program/loyalty_program.js index 524a671801..f90f86728d 100644 --- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.js +++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.js @@ -1,6 +1,8 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt +frappe.provide("erpnext.accounts.dimensions"); + frappe.ui.form.on('Loyalty Program', { setup: function(frm) { var help_content = @@ -46,20 +48,17 @@ frappe.ui.form.on('Loyalty Program', { }; }); - frm.set_query("cost_center", function() { - return { - filters: { - company: frm.doc.company - } - }; - }); - frm.set_value("company", frappe.defaults.get_user_default("Company")); + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { if (frm.doc.loyalty_program_type === "Single Tier Program" && frm.doc.collection_rules.length > 1) { frappe.throw(__("Please select the Multiple Tier Program type for more than one collection rules.")); } + }, + + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); } }); diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js index 3ce5701823..c087980798 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js @@ -36,6 +36,8 @@ frappe.ui.form.on('Opening Invoice Creation Tool', { frm.dashboard.show_progress(data.title, (data.count / data.total) * 100, data.message); frm.page.set_indicator(__('In Progress'), 'orange'); }); + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { @@ -100,6 +102,7 @@ frappe.ui.form.on('Opening Invoice Creation Tool', { } }) } + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, invoice_type: function(frm) { diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 9bdd26b805..f5c488d0f9 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -1,6 +1,7 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt {% include "erpnext/public/js/controllers/accounts.js" %} +frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on('Payment Entry', { onload: function(frm) { @@ -8,6 +9,8 @@ frappe.ui.form.on('Payment Entry', { if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); if (!frm.doc.paid_to) frm.set_value("paid_to_account_currency", null); } + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, setup: function(frm) { @@ -88,15 +91,6 @@ frappe.ui.form.on('Payment Entry', { } }); - frm.set_query("cost_center", "deductions", function() { - return { - filters: { - "is_group": 0, - "company": frm.doc.company - } - } - }); - frm.set_query("reference_doctype", "references", function() { if (frm.doc.party_type=="Customer") { var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"]; @@ -167,6 +161,7 @@ frappe.ui.form.on('Payment Entry', { company: function(frm) { frm.events.hide_unhide_fields(frm); frm.events.set_dynamic_labels(frm); + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, contact_person: function(frm) { diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index 7dd5b01770..a74fa062b6 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -8,7 +8,7 @@ from frappe import _ from erpnext.accounts.utils import get_account_currency from erpnext.controllers.accounts_controller import AccountsController from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (get_accounting_dimensions, - get_dimension_filters) + get_dimensions) class PeriodClosingVoucher(AccountsController): def validate(self): @@ -58,7 +58,7 @@ class PeriodClosingVoucher(AccountsController): for dimension in accounting_dimensions: dimension_fields.append('t1.{0}'.format(dimension)) - dimension_filters, default_dimensions = get_dimension_filters() + dimension_filters, default_dimensions = get_dimensions() pl_accounts = self.get_pl_balances(dimension_fields) diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js index 7f4f755480..efdeb1a5e8 100755 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.js +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js @@ -57,6 +57,8 @@ frappe.ui.form.on('POS Profile', { } }; }); + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { @@ -67,6 +69,7 @@ frappe.ui.form.on('POS Profile', { company: function(frm) { frm.trigger("toggle_display_account_head"); + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, toggle_display_account_head: function(frm) { diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 3863768a8b..06aa20bfc5 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -26,6 +26,11 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ }; }); }, + + company: function() { + erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype); + }, + onload: function() { this._super(); @@ -41,6 +46,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ if (this.frm.doc.supplier && this.frm.doc.__islocal) { this.frm.trigger('supplier'); } + + erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype); }, refresh: function(doc) { @@ -268,8 +275,11 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ supplier: function() { var me = this; - if(this.frm.updating_party_details) + + // 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, @@ -511,15 +521,6 @@ frappe.ui.form.on("Purchase Invoice", { } } } - - frm.set_query("cost_center", function() { - return { - filters: { - company: frm.doc.company, - is_group: 0 - } - }; - }); }, onload: function(frm) { diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index c64ffd878c..451c936881 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -57,8 +57,8 @@ "set_warehouse", "rejected_warehouse", "col_break_warehouse", + "set_from_warehouse", "is_subcontracted", - "supplier_warehouse", "items_section", "update_stock", "scan_barcode", @@ -515,6 +515,7 @@ }, { "depends_on": "update_stock", + "description": "Sets 'Accepted Warehouse' in each row of the items table.", "fieldname": "set_warehouse", "fieldtype": "Link", "label": "Set Accepted Warehouse", @@ -543,17 +544,6 @@ "options": "No\nYes", "print_hide": 1 }, - { - "depends_on": "eval:doc.is_subcontracted==\"Yes\"", - "fieldname": "supplier_warehouse", - "fieldtype": "Link", - "label": "Supplier Warehouse", - "no_copy": 1, - "options": "Warehouse", - "print_hide": 1, - "print_width": "50px", - "width": "50px" - }, { "fieldname": "items_section", "fieldtype": "Section Break", @@ -1232,7 +1222,9 @@ "fieldname": "inter_company_invoice_reference", "fieldtype": "Link", "label": "Inter Company Invoice Reference", + "no_copy": 1, "options": "Sales Invoice", + "print_hide": 1, "read_only": 1 }, { @@ -1356,13 +1348,25 @@ "fieldtype": "Link", "label": "Represents Company", "options": "Company" + }, + { + "depends_on": "eval:doc.update_stock && (doc.is_subcontracted==\"Yes\" || doc.is_internal_supplier)", + "description": "Sets 'From Warehouse' in each row of the items table.", + "fieldname": "set_from_warehouse", + "fieldtype": "Link", + "label": "Set From Warehouse", + "no_copy": 1, + "options": "Warehouse", + "print_hide": 1, + "print_width": "50px", + "width": "50px" } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2020-12-11 12:46:12.796378", + "modified": "2020-12-26 20:49:03.305063", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index b52678e8d3..dacd50a3e2 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -443,7 +443,7 @@ class PurchaseInvoice(BuyingController): else: self.stock_received_but_not_billed = None self.expenses_included_in_valuation = None - + self.negative_expense_to_be_booked = 0.0 gl_entries = [] @@ -457,7 +457,7 @@ class PurchaseInvoice(BuyingController): self.make_internal_transfer_gl_entries(gl_entries) gl_entries = make_regional_gl_entries(gl_entries, self) - + gl_entries = merge_similar_entries(gl_entries) self.make_payment_gl_entries(gl_entries) @@ -480,7 +480,7 @@ class PurchaseInvoice(BuyingController): grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total if grand_total and not self.is_internal_transfer(): - # Didnot use base_grand_total to book rounding loss gle + # Did not use base_grand_total to book rounding loss gle grand_total_in_company_currency = flt(grand_total * self.conversion_rate, self.precision("grand_total")) gl_entries.append( @@ -511,8 +511,8 @@ class PurchaseInvoice(BuyingController): voucher_wise_stock_value = {} if self.update_stock: for d in frappe.get_all('Stock Ledger Entry', - fields = ["voucher_detail_no", "stock_value_difference"], filters={'voucher_no': self.name}): - voucher_wise_stock_value.setdefault(d.voucher_detail_no, d.stock_value_difference) + fields = ["voucher_detail_no", "stock_value_difference", "warehouse"], filters={'voucher_no': self.name}): + voucher_wise_stock_value.setdefault((d.voucher_detail_no, d.warehouse), d.stock_value_difference) valuation_tax_accounts = [d.account_head for d in self.get("taxes") if d.category in ('Valuation', 'Total and Valuation') @@ -563,16 +563,17 @@ class PurchaseInvoice(BuyingController): ) else: - gl_entries.append( - self.get_gl_dict({ - "account": item.expense_account, - "against": self.supplier, - "debit": warehouse_debit_amount, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "cost_center": item.cost_center, - "project": item.project or self.project - }, account_currency, item=item) - ) + if not self.is_internal_transfer(): + gl_entries.append( + self.get_gl_dict({ + "account": item.expense_account, + "against": self.supplier, + "debit": warehouse_debit_amount, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "cost_center": item.cost_center, + "project": item.project or self.project + }, account_currency, item=item) + ) # Amount added through landed-cost-voucher if landed_cost_entries: @@ -582,7 +583,8 @@ class PurchaseInvoice(BuyingController): "against": item.expense_account, "cost_center": item.cost_center, "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(amount), + "credit": flt(amount["base_amount"]), + "credit_in_account_currency": flt(amount["amount"]), "project": item.project or self.project }, item=item)) @@ -624,13 +626,14 @@ class PurchaseInvoice(BuyingController): if expense_booked_in_pr: expense_account = service_received_but_not_billed_account - gl_entries.append(self.get_gl_dict({ - "account": expense_account, - "against": self.supplier, - "debit": amount, - "cost_center": item.cost_center, - "project": item.project or self.project - }, account_currency, item=item)) + if not self.is_internal_transfer(): + gl_entries.append(self.get_gl_dict({ + "account": expense_account, + "against": self.supplier, + "debit": amount, + "cost_center": item.cost_center, + "project": item.project or self.project + }, account_currency, item=item)) # If asset is bought through this document and not linked to PR if self.update_stock and item.landed_cost_voucher_amount: @@ -795,10 +798,10 @@ class PurchaseInvoice(BuyingController): # Stock ledger value is not matching with the warehouse amount if (self.update_stock and voucher_wise_stock_value.get(item.name) and - warehouse_debit_amount != flt(voucher_wise_stock_value.get(item.name), net_amt_precision)): + warehouse_debit_amount != flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)): cost_of_goods_sold_account = self.get_company_default("default_expense_account") - stock_amount = flt(voucher_wise_stock_value.get(item.name), net_amt_precision) + stock_amount = flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision) stock_adjustment_amt = warehouse_debit_amount - stock_amount gl_entries.append( @@ -999,10 +1002,10 @@ class PurchaseInvoice(BuyingController): self.delete_auto_created_batches() self.make_gl_entries_on_cancel() - + if self.update_stock == 1: self.repost_future_sle_and_gle() - + self.update_project() frappe.db.set(self, 'status', 'Cancelled') diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index c0506ba97f..2c088ce2b2 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -426,26 +426,31 @@ class TestPurchaseInvoice(unittest.TestCase): ) def test_total_purchase_cost_for_project(self): - make_project({'project_name':'_Test Project'}) + if not frappe.db.exists("Project", {"project_name": "_Test Project for Purchase"}): + project = make_project({'project_name':'_Test Project for Purchase'}) + else: + project = frappe.get_doc("Project", {"project_name": "_Test Project for Purchase"}) existing_purchase_cost = frappe.db.sql("""select sum(base_net_amount) - from `tabPurchase Invoice Item` where project = '_Test Project' and docstatus=1""") + from `tabPurchase Invoice Item` + where project = '{0}' + and docstatus=1""".format(project.name)) existing_purchase_cost = existing_purchase_cost and existing_purchase_cost[0][0] or 0 - pi = make_purchase_invoice(currency="USD", conversion_rate=60, project="_Test Project") - self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), + pi = make_purchase_invoice(currency="USD", conversion_rate=60, project=project.name) + self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"), existing_purchase_cost + 15000) - pi1 = make_purchase_invoice(qty=10, project="_Test Project") - self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), + pi1 = make_purchase_invoice(qty=10, project=project.name) + self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"), existing_purchase_cost + 15500) pi1.cancel() - self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), + self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"), existing_purchase_cost + 15000) pi.cancel() - self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), existing_purchase_cost) + self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"), existing_purchase_cost) def test_return_purchase_invoice_with_perpetual_inventory(self): pi = make_purchase_invoice(company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1", @@ -860,17 +865,17 @@ class TestPurchaseInvoice(unittest.TestCase): }) pi = make_purchase_invoice(credit_to="Creditors - _TC" ,do_not_save=1) - pi.items[0].project = item_project.project_name - pi.project = project.project_name + pi.items[0].project = item_project.name + pi.project = project.name pi.submit() expected_values = { "Creditors - _TC": { - "project": project.project_name + "project": project.name }, "_Test Account Cost for Goods Sold - _TC": { - "project": item_project.project_name + "project": item_project.name } } 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 f6d76e5050..1f7853dbf7 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-05-22 12:43:10", "doctype": "DocType", @@ -87,6 +88,7 @@ "po_detail", "purchase_receipt", "pr_detail", + "sales_invoice_item", "item_weight_details", "weight_per_unit", "total_weight", @@ -553,8 +555,8 @@ "fieldtype": "Link", "hidden": 1, "label": "Brand", - "print_hide": 1, - "options": "Brand" + "options": "Brand", + "print_hide": 1 }, { "fetch_from": "item_code.item_group", @@ -562,9 +564,9 @@ "fieldname": "item_group", "fieldtype": "Link", "label": "Item Group", + "options": "Item Group", "print_hide": 1, - "read_only": 1, - "options": "Item Group" + "read_only": 1 }, { "description": "Tax detail table fetched from item master as a string and stored in this field.\nUsed for Taxes and Charges", @@ -759,10 +761,11 @@ "read_only": 1 }, { + "depends_on": "eval:parent.is_internal_supplier && parent.update_stock", "fieldname": "from_warehouse", "fieldtype": "Link", "ignore_user_permissions": 1, - "label": "Supplier Warehouse", + "label": "From Warehouse", "options": "Warehouse" }, { @@ -779,11 +782,20 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "sales_invoice_item", + "fieldtype": "Data", + "label": "Sales Invoice Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, - "modified": "2020-08-20 11:48:01.398356", + "links": [], + "modified": "2020-12-26 17:20:36.415791", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", @@ -791,4 +803,4 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC" -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 89b716c180..f2a62cdacd 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -5,14 +5,17 @@ cur_frm.pformat.print_heading = 'Invoice'; {% include 'erpnext/selling/sales_common.js' %}; - - frappe.provide("erpnext.accounts"); + + erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.extend({ setup: function(doc) { this.setup_posting_date_time_check(); this._super(doc); }, + company: function() { + erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype); + }, onload: function() { var me = this; this._super(); @@ -33,6 +36,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte me.frm.refresh_fields(); } erpnext.queries.setup_warehouse_query(this.frm); + erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype); }, refresh: function(doc, dt, dn) { @@ -126,16 +130,15 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte this.set_default_print_format(); if (doc.docstatus == 1 && !doc.inter_company_invoice_reference) { - frappe.model.with_doc("Customer", me.frm.doc.customer, function() { - var customer = frappe.model.get_doc("Customer", me.frm.doc.customer); - var internal = customer.is_internal_customer; - var disabled = customer.disabled; - if (internal == 1 && disabled == 0) { - me.frm.add_custom_button("Inter Company Invoice", function() { - me.make_inter_company_invoice(); - }, __('Create')); - } - }); + let internal = me.frm.doc.is_internal_customer; + if (internal) { + let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Purchase Invoice" : + "Inter Company Purchase Invoice"; + + me.frm.add_custom_button(button_label, function() { + me.make_inter_company_invoice(); + }, __('Create')); + } } }, @@ -571,15 +574,6 @@ frappe.ui.form.on('Sales Invoice', { }; }); - frm.set_query("cost_center", function() { - return { - filters: { - company: frm.doc.company, - is_group: 0 - } - }; - }); - frm.set_query("unrealized_profit_loss_account", function() { return { filters: { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 6799fb986a..447cee42a7 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -60,6 +60,8 @@ "ignore_pricing_rule", "sec_warehouse", "set_warehouse", + "column_break_55", + "set_target_warehouse", "items_section", "update_stock", "scan_barcode", @@ -1969,13 +1971,24 @@ "label": "Represents Company", "options": "Company", "read_only": 1 + }, + { + "fieldname": "column_break_55", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: doc.is_internal_customer && doc.update_stock", + "fieldname": "set_target_warehouse", + "fieldtype": "Link", + "label": "Set Target Warehouse", + "options": "Warehouse" } ], "icon": "fa fa-file-text", "idx": 181, "is_submittable": 1, "links": [], - "modified": "2020-12-11 12:48:31.769958", + "modified": "2020-12-25 22:57:32.555067", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 566734e7d1..7116a6a62b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -6,7 +6,7 @@ import frappe, erpnext import frappe.defaults from frappe.utils import cint, flt, getdate, add_days, cstr, nowdate, get_link_to_form, formatdate from frappe import _, msgprint, throw -from erpnext.accounts.party import get_party_account, get_due_date +from erpnext.accounts.party import get_party_account, get_due_date, get_party_details from frappe.model.mapper import get_mapped_doc from erpnext.controllers.selling_controller import SellingController from erpnext.accounts.utils import get_account_currency @@ -21,6 +21,8 @@ from erpnext.accounts.general_ledger import get_round_off_account_and_cost_cente from erpnext.accounts.doctype.loyalty_program.loyalty_program import \ get_loyalty_program_details_with_points, get_loyalty_details, validate_loyalty_points from erpnext.accounts.deferred_revenue import validate_service_stop_date +from frappe.model.utils import get_fetch_values +from frappe.contacts.doctype.address.address import get_address_display from erpnext.healthcare.utils import manage_invoice_submit_cancel @@ -1534,7 +1536,7 @@ def validate_inter_company_transaction(doc, doctype): details = get_inter_company_details(doc, doctype) price_list = doc.selling_price_list if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"] else doc.buying_price_list valid_price_list = frappe.db.get_value("Price List", {"name": price_list, "buying": 1, "selling": 1}) - if not valid_price_list: + if not valid_price_list and not doc.is_internal_transfer(): frappe.throw(_("Selected Price List should have buying and selling fields checked.")) party = details.get("party") @@ -1557,6 +1559,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): if doctype in ["Sales Invoice", "Sales Order"]: source_doc = frappe.get_doc(doctype, source_name) target_doctype = "Purchase Invoice" if doctype == "Sales Invoice" else "Purchase Order" + target_detail_field = "sales_invoice_item" if doctype == "Sales Invoice" else "sales_order_item" source_document_warehouse_field = 'target_warehouse' target_document_warehouse_field = 'from_warehouse' else: @@ -1570,6 +1573,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): def set_missing_values(source, target): target.run_method("set_missing_values") + set_purchase_references(target) def update_details(source_doc, target_doc, source_parent): target_doc.inter_company_invoice_reference = source_doc.name @@ -1577,19 +1581,38 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): currency = frappe.db.get_value('Supplier', details.get('party'), 'default_currency') target_doc.company = details.get("company") target_doc.supplier = details.get("party") + target_doc.is_internal_supplier = 1 + target_doc.ignore_pricing_rule = 1 target_doc.buying_price_list = source_doc.selling_price_list + # Invert Addresses + update_address(target_doc, 'supplier_address', 'address_display', source_doc.company_address) + update_address(target_doc, 'shipping_address', 'shipping_address_display', source_doc.customer_address) + if currency: target_doc.currency = currency + + update_taxes(target_doc, party=target_doc.supplier, party_type='Supplier', company=target_doc.company, + doctype=target_doc.doctype, party_address=target_doc.supplier_address, + company_address=target_doc.shipping_address) + else: currency = frappe.db.get_value('Customer', details.get('party'), 'default_currency') target_doc.company = details.get("company") target_doc.customer = details.get("party") target_doc.selling_price_list = source_doc.buying_price_list + update_address(target_doc, 'company_address', 'company_address_display', source_doc.supplier_address) + update_address(target_doc, 'shipping_address_name', 'shipping_address', source_doc.shipping_address) + update_address(target_doc, 'customer_address', 'address_display', source_doc.shipping_address) + if currency: target_doc.currency = currency + update_taxes(target_doc, party=target_doc.customer, party_type='Customer', company=target_doc.company, + doctype=target_doc.doctype, party_address=target_doc.customer_address, + company_address=target_doc.company_address, shipping_address_name=target_doc.shipping_address_name) + item_field_map = { "doctype": target_doctype + " Item", "field_no_map": [ @@ -1597,25 +1620,33 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): "expense_account", "cost_center", "warehouse" - ] + ], + "field_map": { + 'rate': 'rate', + } } - if source_doc.get('update_stock'): - item_field_map.update({ - 'field_map': { - source_document_warehouse_field: target_document_warehouse_field, - 'batch_no': 'batch_no', - 'serial_no': 'serial_no' - } + if doctype in ["Sales Invoice", "Sales Order"]: + item_field_map["field_map"].update({ + "name": target_detail_field, }) + if source_doc.get('update_stock'): + item_field_map["field_map"].update({ + source_document_warehouse_field: target_document_warehouse_field, + 'batch_no': 'batch_no', + 'serial_no': 'serial_no' + }) doclist = get_mapped_doc(doctype, source_name, { doctype: { "doctype": target_doctype, "postprocess": update_details, + "set_target_warehouse": "set_from_warehouse", "field_no_map": [ - "taxes_and_charges" + "taxes_and_charges", + "set_warehouse", + "shipping_address" ] }, doctype +" Item": item_field_map @@ -1624,6 +1655,110 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): return doclist +def set_purchase_references(doc): + # add internal PO or PR links if any + if doc.is_internal_transfer(): + if doc.doctype == 'Purchase Receipt': + so_item_map = get_delivery_note_details(doc.inter_company_invoice_reference) + + if so_item_map: + pd_item_map, parent_child_map, warehouse_map = \ + get_pd_details('Purchase Order Item', so_item_map, 'sales_order_item') + + update_pr_items(doc, so_item_map, pd_item_map, parent_child_map, warehouse_map) + + elif doc.doctype == 'Purchase Invoice': + dn_item_map, so_item_map = get_sales_invoice_details(doc.inter_company_invoice_reference) + # First check for Purchase receipt + if list(dn_item_map.values()): + pd_item_map, parent_child_map, warehouse_map = \ + get_pd_details('Purchase Receipt Item', dn_item_map, 'delivery_note_item') + + update_pi_items(doc, 'pr_detail', 'purchase_receipt', + dn_item_map, pd_item_map, parent_child_map, warehouse_map) + + if list(so_item_map.values()): + pd_item_map, parent_child_map, warehouse_map = \ + get_pd_details('Purchase Order Item', so_item_map, 'sales_order_item') + + update_pi_items(doc, 'po_detail', 'purchase_order', + so_item_map, pd_item_map, parent_child_map, warehouse_map) + +def update_pi_items(doc, detail_field, parent_field, sales_item_map, + purchase_item_map, parent_child_map, warehouse_map): + for item in doc.get('items'): + item.set(detail_field, purchase_item_map.get(sales_item_map.get(item.sales_invoice_item))) + item.set(parent_field, parent_child_map.get(sales_item_map.get(item.sales_invoice_item))) + if doc.update_stock: + item.warehouse = warehouse_map.get(sales_item_map.get(item.sales_invoice_item)) + +def update_pr_items(doc, sales_item_map, purchase_item_map, parent_child_map, warehouse_map): + for item in doc.get('items'): + item.purchase_order_item = purchase_item_map.get(sales_item_map.get(item.delivery_note_item)) + item.warehouse = warehouse_map.get(sales_item_map.get(item.delivery_note_item)) + item.purchase_order = parent_child_map.get(sales_item_map.get(item.delivery_note_item)) + +def get_delivery_note_details(internal_reference): + so_item_map = {} + + si_item_details = frappe.get_all('Delivery Note Item', fields=['name', 'so_detail'], + filters={'parent': internal_reference}) + + for d in si_item_details: + so_item_map.setdefault(d.name, d.so_detail) + + return so_item_map + +def get_sales_invoice_details(internal_reference): + dn_item_map = {} + so_item_map = {} + + si_item_details = frappe.get_all('Sales Invoice Item', fields=['name', 'so_detail', + 'dn_detail'], filters={'parent': internal_reference}) + + for d in si_item_details: + if d.dn_detail: + dn_item_map.setdefault(d.name, d.dn_detail) + if d.so_detail: + so_item_map.setdefault(d.name, d.so_detail) + + return dn_item_map, so_item_map + +def get_pd_details(doctype, sd_detail_map, sd_detail_field): + pd_item_map = {} + accepted_warehouse_map = {} + parent_child_map = {} + + pd_item_details = frappe.get_all(doctype, + fields=[sd_detail_field, 'name', 'warehouse', 'parent'], filters={sd_detail_field: ('in', list(sd_detail_map.values()))}) + + for d in pd_item_details: + pd_item_map.setdefault(d.get(sd_detail_field), d.name) + parent_child_map.setdefault(d.get(sd_detail_field), d.parent) + accepted_warehouse_map.setdefault(d.get(sd_detail_field), d.warehouse) + + return pd_item_map, parent_child_map, accepted_warehouse_map + +def update_taxes(doc, party=None, party_type=None, company=None, doctype=None, party_address=None, + company_address=None, shipping_address_name=None, master_doctype=None): + # Update Party Details + party_details = get_party_details(party=party, party_type=party_type, company=company, + doctype=doctype, party_address=party_address, company_address=company_address, + shipping_address=shipping_address_name) + + # Update taxes and charges if any + doc.taxes_and_charges = party_details.get('taxes_and_charges') + doc.set('taxes', party_details.get('taxes')) + +def update_address(doc, address_field, address_display_field, address_name): + doc.set(address_field, address_name) + fetch_values = get_fetch_values(doc.doctype, address_field, address_name) + + for key, value in fetch_values.items(): + doc.set(key, value) + + doc.set(address_display_field, get_address_display(doc.get(address_field))) + @frappe.whitelist() def get_loyalty_programs(customer): ''' sets applicable loyalty program to the customer or returns a list of applicable programs ''' diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index eb223ee42c..e94e2cdd95 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -22,6 +22,7 @@ from erpnext.regional.india.utils import get_ewb_data from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice +from erpnext.stock.utils import get_incoming_rate class TestSalesInvoice(unittest.TestCase): def make(self): @@ -688,7 +689,7 @@ class TestSalesInvoice(unittest.TestCase): self.assertTrue(gle) def test_pos_gl_entry_with_perpetual_inventory(self): - make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1", + make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1") pr = make_purchase_receipt(company= "_Test Company with perpetual inventory", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1") @@ -745,7 +746,7 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(pos_return.get('payments')[0].amount, -1000) def test_pos_change_amount(self): - make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1", + make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1") pr = make_purchase_receipt(company= "_Test Company with perpetual inventory", @@ -1573,17 +1574,17 @@ class TestSalesInvoice(unittest.TestCase): }) sales_invoice = create_sales_invoice(do_not_save=1) - sales_invoice.items[0].project = item_project.project_name - sales_invoice.project = project.project_name + sales_invoice.items[0].project = item_project.name + sales_invoice.project = project.name sales_invoice.submit() expected_values = { "Debtors - _TC": { - "project": project.project_name + "project": project.name }, "Sales - _TC": { - "project": item_project.project_name + "project": item_project.name } } @@ -1770,59 +1771,82 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(target_doc.company, "_Test Company 1") self.assertEqual(target_doc.supplier, "_Test Internal Supplier") - # def test_internal_transfer_gl_entry(self): - # ## Create internal transfer account - # account = create_account(account_name="Unrealized Profit", - # parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory") + def test_internal_transfer_gl_entry(self): + ## Create internal transfer account + account = create_account(account_name="Unrealized Profit", + parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory") - # frappe.db.set_value('Company', '_Test Company with perpetual inventory', - # 'unrealized_profit_loss_account', account) + frappe.db.set_value('Company', '_Test Company with perpetual inventory', + 'unrealized_profit_loss_account', account) - # customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory", - # "_Test Company with perpetual inventory") + customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory", + "_Test Company with perpetual inventory") - # create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory", - # "_Test Company with perpetual inventory") + create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory", + "_Test Company with perpetual inventory") - # si = create_sales_invoice( - # company = "_Test Company with perpetual inventory", - # customer = customer, - # debit_to = "Debtors - TCP1", - # warehouse = "Stores - TCP1", - # income_account = "Sales - TCP1", - # expense_account = "Cost of Goods Sold - TCP1", - # cost_center = "Main - TCP1", - # currency = "INR", - # do_not_save = 1 - # ) + si = create_sales_invoice( + company = "_Test Company with perpetual inventory", + customer = customer, + debit_to = "Debtors - TCP1", + warehouse = "Stores - TCP1", + income_account = "Sales - TCP1", + expense_account = "Cost of Goods Sold - TCP1", + cost_center = "Main - TCP1", + currency = "INR", + do_not_save = 1 + ) - # si.selling_price_list = "_Test Price List Rest of the World" - # si.update_stock = 1 - # si.items[0].target_warehouse = 'Work In Progress - TCP1' - # add_taxes(si) - # si.save() - # si.submit() + si.selling_price_list = "_Test Price List Rest of the World" + si.update_stock = 1 + si.items[0].target_warehouse = 'Work In Progress - TCP1' + add_taxes(si) + si.save() - # target_doc = make_inter_company_transaction("Sales Invoice", si.name) - # target_doc.company = '_Test Company with perpetual inventory' - # target_doc.items[0].warehouse = 'Finished Goods - TCP1' - # add_taxes(target_doc) - # target_doc.save() - # target_doc.submit() + rate = 0.0 + for d in si.get('items'): + rate = get_incoming_rate({ + "item_code": d.item_code, + "warehouse": d.warehouse, + "posting_date": si.posting_date, + "posting_time": si.posting_time, + "qty": -1 * flt(d.get('stock_qty')), + "serial_no": d.serial_no, + "company": si.company, + "voucher_type": 'Sales Invoice', + "voucher_no": si.name, + "allow_zero_valuation": d.get("allow_zero_valuation") + }, raise_error_if_no_rate=False) - # si_gl_entries = [ - # ["_Test Account Excise Duty - TCP1", 0.0, 12.0, nowdate()], - # ["Unrealized Profit - TCP1", 12.0, 0.0, nowdate()] - # ] + rate = flt(rate, 2) - # check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1)) + si.submit() - # pi_gl_entries = [ - # ["_Test Account Excise Duty - TCP1", 12.0 , 0.0, nowdate()], - # ["Unrealized Profit - TCP1", 0.0, 12.0, nowdate()] - # ] + target_doc = make_inter_company_transaction("Sales Invoice", si.name) + target_doc.company = '_Test Company with perpetual inventory' + target_doc.items[0].warehouse = 'Finished Goods - TCP1' + add_taxes(target_doc) + target_doc.save() + target_doc.submit() - # check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1)) + tax_amount = flt(rate * (12/100), 2) + si_gl_entries = [ + ["_Test Account Excise Duty - TCP1", 0.0, tax_amount, nowdate()], + ["Unrealized Profit - TCP1", tax_amount, 0.0, nowdate()] + ] + + check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1)) + + pi_gl_entries = [ + ["_Test Account Excise Duty - TCP1", tax_amount , 0.0, nowdate()], + ["Unrealized Profit - TCP1", 0.0, tax_amount, nowdate()] + ] + + # Sale and Purchase both should be at valuation rate + self.assertEqual(si.items[0].rate, rate) + self.assertEqual(target_doc.items[0].rate, rate) + + check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1)) def test_eway_bill_json(self): si = make_sales_invoice_for_ewaybill() @@ -1861,23 +1885,6 @@ class TestSalesInvoice(unittest.TestCase): def test_einvoice_json(self): from erpnext.regional.india.e_invoice.utils import make_einvoice - customer_gstin = '27AACCM7806M1Z3' - customer_gstin_dtls = { - 'LegalName': '_Test Customer', 'TradeName': '_Test Customer', 'AddrLoc': '_Test City', - 'StateCode': '27', 'AddrPncd': '410038', 'AddrBno': '_Test Bldg', - 'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street' - } - company_gstin = '27AAECE4835E1ZR' - company_gstin_dtls = { - 'LegalName': '_Test Company', 'TradeName': '_Test Company', 'AddrLoc': '_Test City', - 'StateCode': '27', 'AddrPncd': '401108', 'AddrBno': '_Test Bldg', - 'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street' - } - # set cache gstin details to avoid fetching details which will require connection to GSP servers - frappe.local.gstin_cache = {} - frappe.local.gstin_cache[customer_gstin] = customer_gstin_dtls - frappe.local.gstin_cache[company_gstin] = company_gstin_dtls - si = make_sales_invoice_for_ewaybill() si.naming_series = 'INV-2020-.#####' si.items = [] @@ -1930,12 +1937,12 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(value_details['SgstVal'], total_item_sgst_value) self.assertEqual(value_details['IgstVal'], total_item_igst_value) - self.assertEqual( - value_details['TotInvVal'], - value_details['AssVal'] + value_details['CgstVal'] - + value_details['SgstVal'] + value_details['IgstVal'] + calculated_invoice_value = \ + value_details['AssVal'] + value_details['CgstVal'] \ + + value_details['SgstVal'] + value_details['IgstVal'] \ + value_details['OthChrg'] - value_details['Discount'] - ) + + self.assertTrue(value_details['TotInvVal'] - calculated_invoice_value < 0.1) self.assertEqual(value_details['TotInvVal'], si.base_grand_total) self.assertTrue(einvoice['EwbDtls']) 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 3695075798..7a98afff36 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -565,11 +565,12 @@ "print_hide": 1 }, { + "depends_on": "eval: parent.is_internal_customer && parent.update_stock", "fieldname": "target_warehouse", "fieldtype": "Link", "hidden": 1, "ignore_user_permissions": 1, - "label": "Customer Warehouse (Optional)", + "label": "Target Warehouse", "no_copy": 1, "options": "Warehouse", "print_hide": 1 @@ -815,7 +816,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-09-23 19:59:04.879322", + "modified": "2020-12-26 17:25:04.090630", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", 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 b46de6c85b..429a9f3591 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 @@ -34,6 +34,9 @@ def valdiate_taxes_and_charges_template(doc): validate_disabled(doc) + # Validate with existing taxes and charges template for unique tax category + validate_for_tax_category(doc) + for tax in doc.get("taxes"): validate_taxes_and_charges(tax) validate_inclusive_tax(tax, doc) @@ -41,3 +44,7 @@ def valdiate_taxes_and_charges_template(doc): def validate_disabled(doc): if doc.is_default and doc.disabled: frappe.throw(_("Disabled template must not be default template")) + +def validate_for_tax_category(doc): + if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0}): + frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category))) diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.js b/erpnext/accounts/doctype/shipping_rule/shipping_rule.js index d0904eec3e..8e4b806f02 100644 --- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.js +++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.js @@ -1,16 +1,18 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.ui.form.on('Shipping Rule', { - refresh: function(frm) { - frm.set_query("cost_center", function() { - return { - filters: { - company: frm.doc.company - } - } - }) +frappe.provide('erpnext.accounts.dimensions'); +frappe.ui.form.on('Shipping Rule', { + onload: function(frm) { + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); + }, + + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + }, + + refresh: function(frm) { frm.set_query("account", function() { return { filters: { diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html index 9827e00b71..8eef2adce3 100644 --- a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html +++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html @@ -152,7 +152,7 @@ {{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }} {{ frappe.utils.fmt_money(0, None, "INR") }} {{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }} - {{ frappe.utils.fmt_money(0, None, "INR") }} + {{ frappe.utils.fmt_money(value_details.OthChrg, None, "INR") }} {{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }} {{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }} diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index a36e7f8581..cb4d9b43db 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -49,12 +49,13 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum elif d.po_detail: purchase_receipt = ", ".join(po_pr_map.get(d.po_detail, [])) - expense_account = d.expense_account or aii_account_map.get(d.company) + expense_account = d.unrealized_profit_loss_account or d.expense_account \ + or aii_account_map.get(d.company) row = { 'item_code': d.item_code, - 'item_name': item_record.item_name, - 'item_group': item_record.item_group, + 'item_name': item_record.item_name if item_record else d.item_name, + 'item_group': item_record.item_group if item_record else d.item_group, 'description': d.description, 'invoice': d.parent, 'posting_date': d.posting_date, @@ -315,7 +316,9 @@ def get_items(filters, additional_query_columns): `tabPurchase Invoice Item`.`name`, `tabPurchase Invoice Item`.`parent`, `tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company, `tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total, + `tabPurchase Invoice`.unrealized_profit_loss_account, `tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description, + `tabPurchase Invoice Item`.`item_name`, `tabPurchase Invoice Item`.`item_group`, `tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`, `tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`, `tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`, diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index f54ceb0d2f..998003ac69 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -76,7 +76,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum 'company': d.company, 'sales_order': d.sales_order, 'delivery_note': d.delivery_note, - 'income_account': d.income_account, + 'income_account': d.unrealized_profit_loss_account or d.income_account, 'cost_center': d.cost_center, 'stock_qty': d.stock_qty, 'stock_uom': d.stock_uom @@ -379,6 +379,7 @@ def get_items(filters, additional_query_columns): select `tabSales Invoice Item`.name, `tabSales Invoice Item`.parent, `tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to, + `tabSales Invoice`.unrealized_profit_loss_account, `tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks, `tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total, `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description, diff --git a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py index 57a1231f5a..7195c7e0b8 100644 --- a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py +++ b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py @@ -59,23 +59,111 @@ def validate_filters(filters): def get_columns(filters): return [ - _("Payment Document") + ":: 100", - _("Payment Entry") + ":Dynamic Link/"+_("Payment Document")+":140", - _("Party Type") + "::100", - _("Party") + ":Dynamic Link/Party Type:140", - _("Posting Date") + ":Date:100", - _("Invoice") + (":Link/Purchase Invoice:130" if filters.get("payment_type") == _("Outgoing") else ":Link/Sales Invoice:130"), - _("Invoice Posting Date") + ":Date:130", - _("Payment Due Date") + ":Date:130", - _("Debit") + ":Currency:120", - _("Credit") + ":Currency:120", - _("Remarks") + "::150", - _("Age") +":Int:40", - "0-30:Currency:100", - "30-60:Currency:100", - "60-90:Currency:100", - _("90-Above") + ":Currency:100", - _("Delay in payment (Days)") + "::150" + { + "fieldname": "payment_document", + "label": _("Payment Document Type"), + "fieldtype": "Data", + "width": 100 + }, + { + "fieldname": "payment_entry", + "label": _("Payment Document"), + "fieldtype": "Dynamic Link", + "options": "payment_document", + "width": 160 + }, + { + "fieldname": "party_type", + "label": _("Party Type"), + "fieldtype": "Data", + "width": 100 + }, + { + "fieldname": "party", + "label": _("Party"), + "fieldtype": "Dynamic Link", + "options": "party_type", + "width": 160 + }, + { + "fieldname": "posting_date", + "label": _("Posting Date"), + "fieldtype": "Date", + "width": 100 + }, + { + "fieldname": "invoice", + "label": _("Invoice"), + "fieldtype": "Link", + "options": "Purchase Invoice" if filters.get("payment_type") == _("Outgoing") else "Sales Invoice", + "width": 160 + }, + { + "fieldname": "invoice_posting_date", + "label": _("Invoice Posting Date"), + "fieldtype": "Date", + "width": 100 + }, + { + "fieldname": "due_date", + "label": _("Payment Due Date"), + "fieldtype": "Date", + "width": 100 + }, + { + "fieldname": "debit", + "label": _("Debit"), + "fieldtype": "Currency", + "width": 140 + }, + { + "fieldname": "credit", + "label": _("Credit"), + "fieldtype": "Currency", + "width": 140 + }, + { + "fieldname": "remarks", + "label": _("Remarks"), + "fieldtype": "Data", + "width": 200 + }, + { + "fieldname": "age", + "label": _("Age"), + "fieldtype": "Int", + "width": 50 + }, + { + "fieldname": "range1", + "label": "0-30", + "fieldtype": "Currency", + "width": 140 + }, + { + "fieldname": "range2", + "label": "30-60", + "fieldtype": "Currency", + "width": 140 + }, + { + "fieldname": "range3", + "label": "60-90", + "fieldtype": "Currency", + "width": 140 + }, + { + "fieldname": "range4", + "label": _("90 Above"), + "fieldtype": "Currency", + "width": 140 + }, + { + "fieldname": "delay_in_payment", + "label": _("Delay in payment (Days)"), + "fieldtype": "Int", + "width": 100 + } ] def get_conditions(filters): diff --git a/erpnext/accounts/report/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py index 9399e70739..8ac749d629 100644 --- a/erpnext/accounts/report/purchase_register/purchase_register.py +++ b/erpnext/accounts/report/purchase_register/purchase_register.py @@ -14,13 +14,15 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum if not filters: filters = {} invoice_list = get_invoices(filters, additional_query_columns) - columns, expense_accounts, tax_accounts = get_columns(invoice_list, additional_table_columns) + columns, expense_accounts, tax_accounts, unrealized_profit_loss_accounts \ + = get_columns(invoice_list, additional_table_columns) if not invoice_list: msgprint(_("No record found")) return columns, invoice_list invoice_expense_map = get_invoice_expense_map(invoice_list) + internal_invoice_map = get_internal_invoice_map(invoice_list) invoice_expense_map, invoice_tax_map = get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts) invoice_po_pr_map = get_invoice_po_pr_map(invoice_list) @@ -52,10 +54,17 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum # map expense values base_net_total = 0 for expense_acc in expense_accounts: - expense_amount = flt(invoice_expense_map.get(inv.name, {}).get(expense_acc)) + if inv.is_internal_supplier and inv.company == inv.represents_company: + expense_amount = 0 + else: + expense_amount = flt(invoice_expense_map.get(inv.name, {}).get(expense_acc)) base_net_total += expense_amount row.append(expense_amount) + # Add amount in unrealized account + for account in unrealized_profit_loss_accounts: + row.append(flt(internal_invoice_map.get((inv.name, account)))) + # net total row.append(base_net_total or inv.base_net_total) @@ -96,7 +105,8 @@ def get_columns(invoice_list, additional_table_columns): "width": 80 } ] - expense_accounts = tax_accounts = expense_columns = tax_columns = [] + expense_accounts = tax_accounts = expense_columns = tax_columns = unrealized_profit_loss_accounts = \ + unrealized_profit_loss_account_columns = [] if invoice_list: expense_accounts = frappe.db.sql_list("""select distinct expense_account @@ -112,17 +122,25 @@ def get_columns(invoice_list, additional_table_columns): and parent in (%s) order by account_head""" % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) + unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account + from `tabPurchase Invoice` where docstatus = 1 and name in (%s) + and ifnull(unrealized_profit_loss_account, '') != '' + order by unrealized_profit_loss_account""" % + ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) expense_columns = [(account + ":Currency/currency:120") for account in expense_accounts] + unrealized_profit_loss_account_columns = [(account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts] + for account in tax_accounts: if account not in expense_accounts: tax_columns.append(account + ":Currency/currency:120") - columns = columns + expense_columns + [_("Net Total") + ":Currency/currency:120"] + tax_columns + \ + columns = columns + expense_columns + unrealized_profit_loss_account_columns + \ + [_("Net Total") + ":Currency/currency:120"] + tax_columns + \ [_("Total Tax") + ":Currency/currency:120", _("Grand Total") + ":Currency/currency:120", _("Rounded Total") + ":Currency/currency:120", _("Outstanding Amount") + ":Currency/currency:120"] - return columns, expense_accounts, tax_accounts + return columns, expense_accounts, tax_accounts, unrealized_profit_loss_accounts def get_conditions(filters): conditions = "" @@ -199,6 +217,19 @@ def get_invoice_expense_map(invoice_list): return invoice_expense_map +def get_internal_invoice_map(invoice_list): + unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account, + base_net_total as amount from `tabPurchase Invoice` where name in (%s) + and is_internal_supplier = 1 and company = represents_company""" % + ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) + + internal_invoice_map = {} + for d in unrealized_amount_details: + if d.unrealized_profit_loss_account: + internal_invoice_map.setdefault((d.name, d.unrealized_profit_loss_account), d.amount) + + return internal_invoice_map + def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts): tax_details = frappe.db.sql(""" select parent, account_head, case add_deduct_tax when "Add" then sum(base_tax_amount_after_discount_amount) diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index b6e61b1306..cb2c98b64a 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -15,13 +15,14 @@ def _execute(filters, additional_table_columns=None, additional_query_columns=No if not filters: filters = frappe._dict({}) invoice_list = get_invoices(filters, additional_query_columns) - columns, income_accounts, tax_accounts = get_columns(invoice_list, additional_table_columns) + columns, income_accounts, tax_accounts, unrealized_profit_loss_accounts = get_columns(invoice_list, additional_table_columns) if not invoice_list: msgprint(_("No record found")) return columns, invoice_list invoice_income_map = get_invoice_income_map(invoice_list) + internal_invoice_map = get_internal_invoice_map(invoice_list) invoice_income_map, invoice_tax_map = get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts) #Cost Center & Warehouse Map @@ -70,12 +71,22 @@ def _execute(filters, additional_table_columns=None, additional_query_columns=No # map income values base_net_total = 0 for income_acc in income_accounts: - income_amount = flt(invoice_income_map.get(inv.name, {}).get(income_acc)) + if inv.is_internal_customer and inv.company == inv.represents_company: + income_amount = 0 + else: + income_amount = flt(invoice_income_map.get(inv.name, {}).get(income_acc)) + base_net_total += income_amount row.update({ frappe.scrub(income_acc): income_amount }) + # Add amount in unrealized account + for account in unrealized_profit_loss_accounts: + row.update({ + frappe.scrub(account): flt(internal_invoice_map.get((inv.name, account))) + }) + # net total row.update({'net_total': base_net_total or inv.base_net_total}) @@ -230,6 +241,8 @@ def get_columns(invoice_list, additional_table_columns): tax_accounts = [] income_columns = [] tax_columns = [] + unrealized_profit_loss_accounts = [] + unrealized_profit_loss_account_columns = [] if invoice_list: income_accounts = frappe.db.sql_list("""select distinct income_account @@ -243,12 +256,18 @@ def get_columns(invoice_list, additional_table_columns): and parent in (%s) order by account_head""" % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) + unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account + from `tabSales Invoice` where docstatus = 1 and name in (%s) + and ifnull(unrealized_profit_loss_account, '') != '' + order by unrealized_profit_loss_account""" % + ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) + for account in income_accounts: income_columns.append({ "label": account, "fieldname": frappe.scrub(account), "fieldtype": "Currency", - "options": 'currency', + "options": "currency", "width": 120 }) @@ -258,15 +277,24 @@ def get_columns(invoice_list, additional_table_columns): "label": account, "fieldname": frappe.scrub(account), "fieldtype": "Currency", - "options": 'currency', + "options": "currency", "width": 120 }) + for account in unrealized_profit_loss_accounts: + unrealized_profit_loss_account_columns.append({ + "label": account, + "fieldname": frappe.scrub(account), + "fieldtype": "Currency", + "options": "currency", + "width": 120 + }) + net_total_column = [{ "label": _("Net Total"), "fieldname": "net_total", "fieldtype": "Currency", - "options": 'currency', + "options": "currency", "width": 120 }] @@ -301,9 +329,10 @@ def get_columns(invoice_list, additional_table_columns): } ] - columns = columns + income_columns + net_total_column + tax_columns + total_columns + columns = columns + income_columns + unrealized_profit_loss_account_columns + \ + net_total_column + tax_columns + total_columns - return columns, income_accounts, tax_accounts + return columns, income_accounts, tax_accounts, unrealized_profit_loss_accounts def get_conditions(filters): conditions = "" @@ -368,7 +397,8 @@ def get_invoices(filters, additional_query_columns): return frappe.db.sql(""" select name, posting_date, debit_to, project, customer, customer_name, owner, remarks, territory, tax_id, customer_group, - base_net_total, base_grand_total, base_rounded_total, outstanding_amount {0} + base_net_total, base_grand_total, base_rounded_total, outstanding_amount, + is_internal_customer, represents_company, company {0} from `tabSales Invoice` where docstatus = 1 %s order by posting_date desc, name desc""".format(additional_query_columns or '') % conditions, filters, as_dict=1) @@ -385,6 +415,19 @@ def get_invoice_income_map(invoice_list): return invoice_income_map +def get_internal_invoice_map(invoice_list): + unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account, + base_net_total as amount from `tabSales Invoice` where name in (%s) + and is_internal_customer = 1 and company = represents_company""" % + ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) + + internal_invoice_map = {} + for d in unrealized_amount_details: + if d.unrealized_profit_loss_account: + internal_invoice_map.setdefault((d.name, d.unrealized_profit_loss_account), d.amount) + + return internal_invoice_map + def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts): tax_details = frappe.db.sql("""select parent, account_head, sum(base_tax_amount_after_discount_amount) as tax_amount diff --git a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py index cae150c428..afbd9b4e6e 100644 --- a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py +++ b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py @@ -48,7 +48,7 @@ class CropCycle(Document): def import_disease_tasks(self, disease, start_date): disease_doc = frappe.get_doc('Disease', disease) - self.create_task(disease_doc.treatment_task, self.name, start_date) + self.create_task(disease_doc.treatment_task, self.project, start_date) def create_project(self, period, crop_tasks): project = frappe.get_doc({ diff --git a/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py b/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py index 5510d5ac02..763b4036c3 100644 --- a/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py +++ b/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py @@ -71,4 +71,4 @@ def check_task_creation(): def check_project_creation(): - return True if frappe.db.exists('Project', 'Basil from seed 2017') else False + return True if frappe.db.exists('Project', {'project_name': 'Basil from seed 2017'}) else False diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index b2318a2bc6..6f1bb28f37 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -2,6 +2,7 @@ // For license information, please see license.txt frappe.provide("erpnext.asset"); +frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on('Asset', { onload: function(frm) { @@ -32,13 +33,11 @@ frappe.ui.form.on('Asset', { }; }); - frm.set_query("cost_center", function() { - return { - "filters": { - "company": frm.doc.company, - } - }; - }); + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); + }, + + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, setup: function(frm) { diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js index a6e6974c48..79c8861bcd 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js @@ -1,6 +1,8 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt +frappe.provide("erpnext.accounts.dimensions"); + frappe.ui.form.on('Asset Value Adjustment', { setup: function(frm) { frm.add_fetch('company', 'cost_center', 'cost_center'); @@ -13,11 +15,19 @@ frappe.ui.form.on('Asset Value Adjustment', { } }); }, + onload: function(frm) { if(frm.is_new() && frm.doc.asset) { frm.trigger("set_current_asset_value"); } + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, + + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + }, + asset: function(frm) { frm.trigger("set_current_asset_value"); }, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 38532d18f3..dd0f065848 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -2,7 +2,7 @@ // License: GNU General Public License v3. See license.txt frappe.provide("erpnext.buying"); - +frappe.provide("erpnext.accounts.dimensions"); {% include 'erpnext/public/js/controllers/buying.js' %}; frappe.ui.form.on("Purchase Order", { @@ -30,6 +30,10 @@ frappe.ui.form.on("Purchase Order", { }, + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + }, + onload: function(frm) { set_schedule_date(frm); if (!frm.doc.transaction_date){ @@ -39,6 +43,8 @@ frappe.ui.form.on("Purchase Order", { erpnext.queries.setup_queries(frm, "Warehouse", function() { return erpnext.queries.warehouse(frm.doc); }); + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); } }); @@ -158,16 +164,16 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( if (doc.docstatus === 1 && !doc.inter_company_order_reference) { let me = this; - frappe.model.with_doc("Supplier", me.frm.doc.supplier, () => { - let supplier = frappe.model.get_doc("Supplier", me.frm.doc.supplier); - let internal = supplier.is_internal_supplier; - let disabled = supplier.disabled; - if (internal === 1 && disabled === 0) { - me.frm.add_custom_button("Inter Company Order", function() { - me.make_inter_company_order(me.frm); - }, __('Create')); - } - }); + let internal = me.frm.doc.is_internal_supplier; + if (internal) { + let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Sales Order" : + "Inter Company Sales Order"; + + me.frm.add_custom_button(button_label, function() { + me.make_inter_company_order(me.frm); + }, __('Create')); + } + } } @@ -347,7 +353,8 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( make_purchase_receipt: function() { frappe.model.open_mapped_doc({ method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt", - frm: cur_frm + frm: cur_frm, + freeze_message: __("Creating Purchase Receipt ...") }) }, @@ -374,7 +381,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( material_request_type: "Purchase", docstatus: 1, status: ["!=", "Stopped"], - per_ordered: ["<", 99.99], + per_ordered: ["<", 100], company: me.frm.doc.company } }) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 75da71ceff..ee2beea67f 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -134,6 +134,8 @@ "ref_sq", "column_break_74", "party_account_currency", + "is_internal_supplier", + "represents_company", "inter_company_order_reference" ], "fields": [ @@ -1101,13 +1103,28 @@ { "fieldname": "items_col_break", "fieldtype": "Column Break" + }, + { + "default": "0", + "fetch_from": "supplier.is_internal_supplier", + "fieldname": "is_internal_supplier", + "fieldtype": "Check", + "label": "Is Internal Supplier" + }, + { + "fetch_from": "supplier.represents_company", + "fieldname": "represents_company", + "fieldtype": "Link", + "label": "Represents Company", + "options": "Company", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2020-12-03 16:46:44.229351", + "modified": "2021-01-20 22:07:23.487138", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index c7efb8a1a1..d32e98e8d9 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -123,8 +123,8 @@ class PurchaseOrder(BuyingController): if self.is_subcontracted == "Yes": for item in self.items: if not item.bom: - frappe.throw(_("BOM is not specified for subcontracting item {0} at row {1}"\ - .format(item.item_code, item.idx))) + frappe.throw(_("BOM is not specified for subcontracting item {0} at row {1}") + .format(item.item_code, item.idx)) def get_schedule_dates(self): for d in self.get('items'): diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index e537771eaf..b76c3784a4 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -224,7 +224,7 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e material_request_type: "Purchase", docstatus: 1, status: ["!=", "Stopped"], - per_ordered: ["<", 99.99], + per_ordered: ["<", 100], company: me.frm.doc.company } }) @@ -280,7 +280,7 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e material_request_type: "Purchase", docstatus: 1, status: ["!=", "Stopped"], - per_ordered: ["<", 99.99] + per_ordered: ["<", 100] } }); dialog.hide(); diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 0ee9d180d9..edeb135d95 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -52,7 +52,10 @@ class Supplier(TransactionBase): self.validate_internal_supplier() def validate_internal_supplier(self): - if self.is_internal_supplier and frappe.db.get_value("Supplier", {"represents_company": self.represents_company}, "name"): + internal_supplier = frappe.db.get_value("Supplier", + {"is_internal_supplier": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name") + + if internal_supplier: frappe.throw(_("Internal Supplier for company {0} already exists").format( frappe.bold(self.represents_company))) diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js index a3b2085400..a0187b0a82 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js @@ -44,7 +44,7 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext material_request_type: "Purchase", docstatus: 1, status: ["!=", "Stopped"], - per_ordered: ["<", 99.99], + per_ordered: ["<", 100], company: me.frm.doc.company } }) diff --git a/erpnext/buying/utils.py b/erpnext/buying/utils.py index 47b48665b6..a73cb0d62e 100644 --- a/erpnext/buying/utils.py +++ b/erpnext/buying/utils.py @@ -35,9 +35,10 @@ def update_last_purchase_rate(doc, is_submit): frappe.throw(_("UOM Conversion factor is required in row {0}").format(d.idx)) # update last purchsae rate - if last_purchase_rate: - frappe.db.sql("""update `tabItem` set last_purchase_rate = %s where name = %s""", - (flt(last_purchase_rate), d.item_code)) + frappe.db.set_value('Item', d.item_code, 'last_purchase_rate', flt(last_purchase_rate)) + + + def validate_for_items(doc): items = [] diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 0f1aa23064..9d9d1b363a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -75,6 +75,9 @@ class AccountsController(TransactionBase): self.ensure_supplier_is_not_blocked() self.validate_date_with_fiscal_year() + self.validate_inter_company_reference() + + self.set_incoming_rate() if self.meta.get_field("currency"): self.calculate_taxes_and_totals() @@ -110,14 +113,20 @@ class AccountsController(TransactionBase): self.set_inter_company_account() validate_regional(self) - + validate_einvoice_fields(self) if self.doctype != 'Material Request': apply_pricing_rule_on_transaction(self) - + def before_cancel(self): validate_einvoice_fields(self) + + def on_trash(self): + # delete sl and gl entries on deletion of transaction + if frappe.db.get_single_value('Accounts Settings', 'delete_linked_ledger_entries'): + frappe.db.sql("delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name)) + frappe.db.sql("delete from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name)) def validate_deferred_start_and_end_date(self): for d in self.items: @@ -206,6 +215,17 @@ class AccountsController(TransactionBase): validate_fiscal_year(self.get(date_field), self.fiscal_year, self.company, self.meta.get_label(date_field), self) + def validate_inter_company_reference(self): + if self.doctype not in ('Purchase Invoice', 'Purchase Receipt', 'Purchase Order'): + return + + if self.is_internal_transfer(): + if not (self.get('inter_company_reference') or self.get('inter_company_invoice_reference') + or self.get('inter_company_order_reference')): + msg = _("Internal Sale or Delivery Reference missing. ") + msg += _("Please create purchase from internal sale or delivery document itself") + frappe.throw(msg, title=_("Internal Sales Reference Missing")) + def validate_due_date(self): if self.get('is_pos'): return @@ -448,8 +468,10 @@ class AccountsController(TransactionBase): account_currency = get_account_currency(gl_dict.account) if gl_dict.account and self.doctype not in ["Journal Entry", - "Period Closing Voucher", "Payment Entry"]: + "Period Closing Voucher", "Payment Entry", "Purchase Receipt", "Purchase Invoice", "Stock Entry"]: self.validate_account_currency(gl_dict.account, account_currency) + + if gl_dict.account and self.doctype not in ["Journal Entry", "Period Closing Voucher", "Payment Entry"]: set_balance_in_account_currency(gl_dict, account_currency, self.get("conversion_rate"), self.company_currency) @@ -962,9 +984,9 @@ class AccountsController(TransactionBase): It will an internal transfer if its an internal customer and representation company is same as billing company """ - if self.doctype == 'Sales Invoice': + if self.doctype in ('Sales Invoice', 'Delivery Note', 'Sales Order'): internal_party_field = 'is_internal_customer' - else: + elif self.doctype in ('Purchase Invoice', 'Purchase Receipt', 'Purchase Order'): internal_party_field = 'is_internal_supplier' if self.get(internal_party_field) and (self.represents_company == self.company): diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 6edc020701..ab1f02779b 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -44,7 +44,6 @@ class BuyingController(StockController): self.validate_items() self.set_qty_as_per_stock_uom() self.validate_stock_or_nonstock_items() - self.update_tax_category_for_internal_transfer() self.validate_warehouse() self.validate_from_warehouse() self.set_supplier_address() @@ -100,11 +99,6 @@ class BuyingController(StockController): msg = _('Tax Category has been changed to "Total" because all the Items are non-stock items') self.update_tax_category(msg) - def update_tax_category_for_internal_transfer(self): - if self.doctype == 'Purchase Invoice' and self.is_internal_transfer(): - msg = _('Tax Category has been changed to "Total" as its an internal purchase.') - self.update_tax_category(msg) - def update_tax_category(self, msg): tax_for_valuation = [d for d in self.get("taxes") if d.category in ["Valuation", "Valuation and Total"]] @@ -224,6 +218,48 @@ class BuyingController(StockController): else: item.valuation_rate = 0.0 + def set_incoming_rate(self): + if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Purchase Order"): + return + + ref_doctype_map = { + "Purchase Order": "Sales Order Item", + "Purchase Receipt": "Delivery Note Item", + "Purchase Invoice": "Sales Invoice Item", + } + + ref_doctype = ref_doctype_map.get(self.doctype) + items = self.get("items") + for d in items: + if not cint(self.get("is_return")): + # Get outgoing rate based on original item cost based on valuation method + + if not d.get(frappe.scrub(ref_doctype)): + outgoing_rate = get_incoming_rate({ + "item_code": d.item_code, + "warehouse": d.get('from_warehouse'), + "posting_date": self.get('posting_date') or self.get('transation_date'), + "posting_time": self.get('posting_time'), + "qty": -1 * flt(d.get('stock_qty')), + "serial_no": d.get('serial_no'), + "company": self.company, + "voucher_type": self.doctype, + "voucher_no": self.name, + "allow_zero_valuation": d.get("allow_zero_valuation") + }, raise_error_if_no_rate=False) + + rate = flt(outgoing_rate * d.conversion_factor, d.precision('rate')) + else: + rate = frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), 'rate') + + if self.is_internal_transfer(): + if rate != d.rate: + d.rate = rate + d.discount_percentage = 0 + d.discount_amount = 0 + frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer") + .format(d.idx), alert=1) + def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True): supplied_items_cost = 0.0 for d in self.get("supplied_items"): @@ -243,7 +279,7 @@ class BuyingController(StockController): d.amount = flt(flt(d.consumed_qty) * flt(d.rate), d.precision("amount")) supplied_items_cost += flt(d.amount) - + return supplied_items_cost def validate_for_subcontracting(self): @@ -336,7 +372,7 @@ class BuyingController(StockController): raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {}) consumed_qty = raw_material_data.get('qty', 0) - consumed_serial_nos = raw_material_data.get('serial_nos', '') + consumed_serial_nos = raw_material_data.get('serial_no', '') consumed_batch_nos = raw_material_data.get('batch_nos', '') transferred_qty = raw_material.qty @@ -559,6 +595,8 @@ class BuyingController(StockController): from_warehouse_sle = self.get_sl_entries(d, { "actual_qty": -1 * pr_qty, "warehouse": d.from_warehouse, + "outgoing_rate": d.rate, + "recalculate_rate": 1, "dependant_sle_voucher_detail_no": d.name }) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 8fe3816c24..81f0ad3fed 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -493,6 +493,41 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters): 'company': filters.get("company", "") }) +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters): + from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import get_dimension_filter_map + dimension_filters = get_dimension_filter_map() + dimension_filters = dimension_filters.get((filters.get('dimension'),filters.get('account'))) + query_filters = [] + + meta = frappe.get_meta(doctype) + if meta.is_tree: + query_filters.append(['is_group', '=', 0]) + + if meta.has_field('company'): + query_filters.append(['company', '=', filters.get('company')]) + + if txt: + query_filters.append([searchfield, 'LIKE', "%%%s%%" % txt]) + + if dimension_filters: + if dimension_filters['allow_or_restrict'] == 'Allow': + query_selector = 'in' + else: + query_selector = 'not in' + + if len(dimension_filters['allowed_dimensions']) == 1: + dimensions = tuple(dimension_filters['allowed_dimensions'] * 2) + else: + dimensions = tuple(dimension_filters['allowed_dimensions']) + + query_filters.append(['name', query_selector, dimensions]) + + output = frappe.get_all(doctype, filters=query_filters) + result = [d.name for d in output] + + return [(d,) for d in set(result)] @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs @@ -620,6 +655,34 @@ def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(query, filters) +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_healthcare_service_units(doctype, txt, searchfield, start, page_len, filters): + query = """ + select name + from `tabHealthcare Service Unit` + where + is_group = 0 + and company = {company} + and name like {txt}""".format( + company = frappe.db.escape(filters.get('company')), txt = frappe.db.escape('%{0}%'.format(txt))) + + if filters and filters.get('inpatient_record'): + from erpnext.healthcare.doctype.inpatient_medication_entry.inpatient_medication_entry import get_current_healthcare_service_unit + service_unit = get_current_healthcare_service_unit(filters.get('inpatient_record')) + + # if the patient is admitted, then appointments should be allowed against the admission service unit, + # inspite of it being an Inpatient Occupancy service unit + if service_unit: + query += " and (allow_appointments = 1 or name = {service_unit})".format(service_unit = frappe.db.escape(service_unit)) + else: + query += " and allow_appointments = 1" + else: + query += " and allow_appointments = 1" + + return frappe.db.sql(query, filters) + + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_tax_template(doctype, txt, searchfield, start, page_len, filters): diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 812021f5c8..e085048f99 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import cint, flt, cstr, comma_or, get_link_to_form +from frappe.utils import cint, flt, cstr, get_link_to_form, nowtime from frappe import _, throw from erpnext.stock.get_item_details import get_bin_details from erpnext.stock.utils import get_incoming_rate @@ -49,7 +49,6 @@ class SellingController(StockController): self.set_customer_address() self.validate_for_duplicate_items() self.validate_target_warehouse() - self.set_incoming_rate() def set_missing_values(self, for_validate=False): @@ -191,7 +190,7 @@ class SellingController(StockController): for it in self.get("items"): if not it.item_code: continue - + last_purchase_rate, is_stock_item = frappe.get_cached_value("Item", it.item_code, ["last_purchase_rate", "is_stock_item"]) last_purchase_rate_in_sales_uom = last_purchase_rate * (it.conversion_factor or 1) if flt(it.base_net_rate) < flt(last_purchase_rate_in_sales_uom): @@ -312,7 +311,7 @@ class SellingController(StockController): sales_order.update_reserved_qty(so_item_rows) def set_incoming_rate(self): - if self.doctype not in ("Delivery Note", "Sales Invoice"): + if self.doctype not in ("Delivery Note", "Sales Invoice", "Sales Order"): return items = self.get("items") + (self.get("packed_items") or []) @@ -322,15 +321,26 @@ class SellingController(StockController): d.incoming_rate = get_incoming_rate({ "item_code": d.item_code, "warehouse": d.warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "qty": -1*flt(d.qty), - "serial_no": d.serial_no, + "posting_date": self.get('posting_date') or self.get('transaction_date'), + "posting_time": self.get('posting_time') or nowtime(), + "qty": -1 * flt(d.get('stock_qty') or d.get('actual_qty')), + "serial_no": d.get('serial_no'), "company": self.company, "voucher_type": self.doctype, "voucher_no": self.name, "allow_zero_valuation": d.get("allow_zero_valuation") }, raise_error_if_no_rate=False) + + # For internal transfers use incoming rate as the valuation rate + if self.is_internal_transfer(): + rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate')) + if d.rate != rate: + d.rate = rate + d.discount_percentage = 0 + d.discount_amount = 0 + frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer") + .format(d.idx), alert=1) + elif self.get("return_against"): # Get incoming rate of return entry from reference document # based on original item cost as per valuation method @@ -391,7 +401,7 @@ class SellingController(StockController): }) if item_row.warehouse: sle.dependant_sle_voucher_detail_no = item_row.name - + return sle def set_po_nos(self, for_validate=False): diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 439997616c..4b5e347970 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -6,6 +6,7 @@ import frappe, erpnext from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate from frappe import _ import frappe.defaults +from collections import defaultdict from erpnext.accounts.utils import get_fiscal_year, check_if_stock_and_account_balance_synced from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map from erpnext.controllers.accounts_controller import AccountsController @@ -23,6 +24,8 @@ class StockController(AccountsController): self.validate_inspection() self.validate_serialized_batch() self.validate_customer_provided_item() + self.validate_internal_transfer() + self.validate_putaway_capacity() def make_gl_entries(self, gl_entries=None, from_repost=False): if self.docstatus == 2: @@ -72,6 +75,7 @@ class StockController(AccountsController): warehouse_with_no_account = [] precision = frappe.get_precision("GL Entry", "debit_in_account_currency") for item_row in voucher_details: + sle_list = sle_map.get(item_row.name) if sle_list: for sle in sle_list: @@ -216,7 +220,7 @@ class StockController(AccountsController): """, (self.doctype, self.name), as_dict=True) for sle in stock_ledger_entries: - stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle) + stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle) return stock_ledger def make_batches(self, warehouse_field): @@ -391,6 +395,84 @@ class StockController(AccountsController): if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'): d.allow_zero_valuation_rate = 1 + def validate_internal_transfer(self): + if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \ + and self.is_internal_transfer(): + self.validate_in_transit_warehouses() + self.validate_multi_currency() + self.validate_packed_items() + + def validate_in_transit_warehouses(self): + if (self.doctype == 'Sales Invoice' and self.get('update_stock')) or self.doctype == 'Delivery Note': + for item in self.get('items'): + if not item.target_warehouse: + frappe.throw(_("Row {0}: Target Warehouse is mandatory for internal transfers").format(item.idx)) + + if (self.doctype == 'Purchase Invoice' and self.get('update_stock')) or self.doctype == 'Purchase Receipt': + for item in self.get('items'): + if not item.from_warehouse: + frappe.throw(_("Row {0}: From Warehouse is mandatory for internal transfers").format(item.idx)) + + def validate_multi_currency(self): + if self.currency != self.company_currency: + frappe.throw(_("Internal transfers can only be done in company's default currency")) + + def validate_packed_items(self): + if self.doctype in ('Sales Invoice', 'Delivery Note Item') and self.get('packed_items'): + frappe.throw(_("Packed Items cannot be transferred internally")) + + def validate_putaway_capacity(self): + # if over receipt is attempted while 'apply putaway rule' is disabled + # and if rule was applied on the transaction, validate it. + from erpnext.stock.doctype.putaway_rule.putaway_rule import get_available_putaway_capacity + valid_doctype = self.doctype in ("Purchase Receipt", "Stock Entry", "Purchase Invoice", + "Stock Reconciliation") + + if self.doctype == "Purchase Invoice" and self.get("update_stock") == 0: + valid_doctype = False + + if valid_doctype: + rule_map = defaultdict(dict) + for item in self.get("items"): + warehouse_field = "t_warehouse" if self.doctype == "Stock Entry" else "warehouse" + rule = frappe.db.get_value("Putaway Rule", + { + "item_code": item.get("item_code"), + "warehouse": item.get(warehouse_field) + }, + ["name", "disable"], as_dict=True) + if rule: + if rule.get("disabled"): continue # dont validate for disabled rule + + if self.doctype == "Stock Reconciliation": + stock_qty = flt(item.qty) + else: + stock_qty = flt(item.transfer_qty) if self.doctype == "Stock Entry" else flt(item.stock_qty) + + rule_name = rule.get("name") + if not rule_map[rule_name]: + rule_map[rule_name]["warehouse"] = item.get(warehouse_field) + rule_map[rule_name]["item"] = item.get("item_code") + rule_map[rule_name]["qty_put"] = 0 + rule_map[rule_name]["capacity"] = get_available_putaway_capacity(rule_name) + rule_map[rule_name]["qty_put"] += flt(stock_qty) + + for rule, values in rule_map.items(): + if flt(values["qty_put"]) > flt(values["capacity"]): + message = self.prepare_over_receipt_message(rule, values) + frappe.throw(msg=message, title=_("Over Receipt")) + + def prepare_over_receipt_message(self, rule, values): + message = _("{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}.") \ + .format( + frappe.bold(values["qty_put"]), frappe.bold(values["item"]), + frappe.bold(values["warehouse"]), frappe.bold(values["capacity"]) + ) + message += "

" + rule_link = frappe.utils.get_link_to_form("Putaway Rule", rule) + message += _(" Please adjust the qty or edit {0} to proceed.").format(rule_link) + return message + def repost_future_sle_and_gle(self): args = frappe._dict({ "posting_date": self.posting_date, diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 8dd2e5bacb..76309f8799 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -10,6 +10,7 @@ from erpnext.controllers.accounts_controller import validate_conversion_rate, \ validate_taxes_and_charges, validate_inclusive_tax from erpnext.stock.get_item_details import _get_item_tax_template from erpnext.accounts.doctype.pricing_rule.utils import get_applied_pricing_rules +from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate class calculate_taxes_and_totals(object): def __init__(self, doc): @@ -758,3 +759,35 @@ def get_rounded_tax_amount(itemised_tax, precision): for taxes in itemised_tax.values(): for tax_account in taxes: taxes[tax_account]["tax_amount"] = flt(taxes[tax_account]["tax_amount"], precision) + +class init_landed_taxes_and_totals(object): + def __init__(self, doc): + self.doc = doc + self.tax_field = 'taxes' if self.doc.doctype == 'Landed Cost Voucher' else 'additional_costs' + self.set_account_currency() + self.set_exchange_rate() + self.set_amounts_in_company_currency() + + def set_account_currency(self): + company_currency = erpnext.get_company_currency(self.doc.company) + for d in self.doc.get(self.tax_field): + if not d.account_currency: + account_currency = frappe.db.get_value('Account', d.expense_account, 'account_currency') + d.account_currency = account_currency or company_currency + + def set_exchange_rate(self): + company_currency = erpnext.get_company_currency(self.doc.company) + for d in self.doc.get(self.tax_field): + if d.account_currency == company_currency: + d.exchange_rate = 1 + elif not d.exchange_rate or d.exchange_rate == 1 or self.doc.posting_date: + d.exchange_rate = get_exchange_rate(self.doc.posting_date, account=d.expense_account, + account_currency=d.account_currency, company=self.doc.company) + + if not d.exchange_rate: + frappe.throw(_("Row {0}: Exchange Rate is mandatory").format(d.idx)) + + def set_amounts_in_company_currency(self): + for d in self.doc.get(self.tax_field): + d.amount = flt(d.amount, d.precision("amount")) + d.base_amount = flt(d.amount * flt(d.exchange_rate), d.precision("base_amount")) \ No newline at end of file diff --git a/erpnext/crm/onboarding_step/create_opportunity/create_opportunity.json b/erpnext/crm/onboarding_step/create_opportunity/create_opportunity.json index 9f996d9e2b..0ee9317c85 100644 --- a/erpnext/crm/onboarding_step/create_opportunity/create_opportunity.json +++ b/erpnext/crm/onboarding_step/create_opportunity/create_opportunity.json @@ -8,12 +8,12 @@ "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-05-14 17:38:27.496696", + "modified": "2021-01-21 15:28:52.483839", "modified_by": "Administrator", "name": "Create Opportunity", "owner": "Administrator", "reference_document": "Opportunity", - "show_full_form": 0, + "show_full_form": 1, "title": "Create Opportunity", "validate_action": 1 } \ No newline at end of file diff --git a/erpnext/education/doctype/fee_schedule/fee_schedule.js b/erpnext/education/doctype/fee_schedule/fee_schedule.js index 75dd4469e8..65b5fa6cf2 100644 --- a/erpnext/education/doctype/fee_schedule/fee_schedule.js +++ b/erpnext/education/doctype/fee_schedule/fee_schedule.js @@ -1,6 +1,7 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt +frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on('Fee Schedule', { setup: function(frm) { frm.add_fetch('fee_structure', 'receivable_account', 'receivable_account'); @@ -8,6 +9,10 @@ frappe.ui.form.on('Fee Schedule', { frm.add_fetch('fee_structure', 'cost_center', 'cost_center'); }, + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + }, + onload: function(frm) { frm.set_query('receivable_account', function(doc) { return { @@ -50,6 +55,8 @@ frappe.ui.form.on('Fee Schedule', { } } }); + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { diff --git a/erpnext/education/doctype/fee_structure/fee_structure.js b/erpnext/education/doctype/fee_structure/fee_structure.js index b331c6d3c0..310c4105f4 100644 --- a/erpnext/education/doctype/fee_structure/fee_structure.js +++ b/erpnext/education/doctype/fee_structure/fee_structure.js @@ -1,6 +1,8 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt +frappe.provide("erpnext.accounts.dimensions"); + frappe.ui.form.on('Fee Structure', { setup: function(frm) { frm.add_fetch('company', 'default_receivable_account', 'receivable_account'); @@ -8,6 +10,10 @@ frappe.ui.form.on('Fee Structure', { frm.add_fetch('company', 'cost_center', 'cost_center'); }, + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + }, + onload: function(frm) { frm.set_query('academic_term', function() { return { @@ -35,6 +41,8 @@ frappe.ui.form.on('Fee Structure', { } }; }); + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { diff --git a/erpnext/education/doctype/fees/fees.js b/erpnext/education/doctype/fees/fees.js index aaf42b4751..ac66acd00f 100644 --- a/erpnext/education/doctype/fees/fees.js +++ b/erpnext/education/doctype/fees/fees.js @@ -1,6 +1,7 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt +frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on("Fees", { setup: function(frm) { @@ -9,15 +10,19 @@ frappe.ui.form.on("Fees", { frm.add_fetch("fee_structure", "cost_center", "cost_center"); }, - onload: function(frm){ - frm.set_query("academic_term",function(){ + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + }, + + onload: function(frm) { + frm.set_query("academic_term", function() { return{ - "filters":{ + "filters": { "academic_year": (frm.doc.academic_year) } }; }); - frm.set_query("fee_structure",function(){ + frm.set_query("fee_structure", function() { return{ "filters":{ "academic_year": (frm.doc.academic_year) @@ -45,6 +50,8 @@ frappe.ui.form.on("Fees", { if (!frm.doc.posting_date) { frm.doc.posting_date = frappe.datetime.get_today(); } + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py index 8d4b510490..66d0e5f77d 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py @@ -29,14 +29,11 @@ class PlaidConnector(): response = self.client.Item.public_token.exchange(public_token) access_token = response["access_token"] return access_token - - def get_link_token(self): + + def get_token_request(self, update_mode=False): country_codes = ["US", "CA", "FR", "IE", "NL", "ES", "GB"] if self.settings.enable_european_access else ["US", "CA"] - token_request = { + args = { "client_name": self.client_name, - "client_id": self.settings.plaid_client_id, - "secret": self.settings.plaid_secret, - "products": self.products, # only allow Plaid-supported languages and countries (LAST: Sep-19-2020) "language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en", "country_codes": country_codes, @@ -45,6 +42,20 @@ class PlaidConnector(): } } + if update_mode: + args["access_token"] = self.access_token + else: + args.update({ + "client_id": self.settings.plaid_client_id, + "secret": self.settings.plaid_secret, + "products": self.products, + }) + + return args + + def get_link_token(self, update_mode=False): + token_request = self.get_token_request(update_mode) + try: response = self.client.LinkToken.create(token_request) except InvalidRequestError: diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js index 22a4004955..bbc2ca8846 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js @@ -12,9 +12,25 @@ frappe.ui.form.on('Plaid Settings', { refresh: function (frm) { if (frm.doc.enabled) { - frm.add_custom_button('Link a new bank account', () => { + frm.add_custom_button(__('Link a new bank account'), () => { new erpnext.integrations.plaidLink(frm); }); + + frm.add_custom_button(__("Sync Now"), () => { + frappe.call({ + method: "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.enqueue_synchronization", + freeze: true, + callback: () => { + let bank_transaction_link = 'Bank Transaction'; + + frappe.msgprint({ + title: __("Sync Started"), + message: __("The sync has started in the background, please check the {0} list for new records.", [bank_transaction_link]), + alert: 1 + }); + } + }); + }).addClass("btn-primary"); } } }); @@ -30,10 +46,18 @@ erpnext.integrations.plaidLink = class plaidLink { this.product = ["auth", "transactions"]; this.plaid_env = this.frm.doc.plaid_env; this.client_name = frappe.boot.sitename; - this.token = await this.frm.call("get_link_token").then(resp => resp.message); + this.token = await this.get_link_token(); this.init_plaid(); } + async get_link_token() { + const token = await this.frm.call("get_link_token").then(resp => resp.message); + if (!token) { + frappe.throw(__('Cannot retrieve link token. Check Error Log for more information')); + } + return token; + } + init_plaid() { const me = this; me.loadScript(me.plaidUrl) @@ -78,8 +102,8 @@ erpnext.integrations.plaidLink = class plaidLink { } onScriptError(error) { - frappe.msgprint("There was an issue connecting to Plaid's authentication server"); - frappe.msgprint(error); + frappe.msgprint(__("There was an issue connecting to Plaid's authentication server. Check browser console for more information")); + console.log(error); } plaid_success(token, response) { @@ -107,4 +131,4 @@ erpnext.integrations.plaidLink = class plaidLink { }); }, __("Select a company"), __("Continue")); } -}; +}; \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index e535e81bde..70c7f3fe5d 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -166,7 +166,6 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None): related_bank = frappe.db.get_values("Bank Account", bank_account, ["bank", "integration_id"], as_dict=True) access_token = frappe.db.get_value("Bank", related_bank[0].bank, "plaid_access_token") account_id = related_bank[0].integration_id - else: access_token = frappe.db.get_value("Bank", bank, "plaid_access_token") account_id = None @@ -228,13 +227,23 @@ def new_bank_transaction(transaction): def automatic_synchronization(): settings = frappe.get_doc("Plaid Settings", "Plaid Settings") - if settings.enabled == 1 and settings.automatic_sync == 1: - plaid_accounts = frappe.get_all("Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"]) + enqueue_synchronization() - for plaid_account in plaid_accounts: - frappe.enqueue( - "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions", - bank=plaid_account.bank, - bank_account=plaid_account.name - ) +@frappe.whitelist() +def enqueue_synchronization(): + plaid_accounts = frappe.get_all("Bank Account", + filters={"integration_id": ["!=", ""]}, + fields=["name", "bank"]) + + for plaid_account in plaid_accounts: + frappe.enqueue( + "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions", + bank=plaid_account.bank, + bank_account=plaid_account.name + ) + +@frappe.whitelist() +def get_link_token_for_update(access_token): + plaid = PlaidConnector(access_token) + return plaid.get_link_token(update_mode=True) diff --git a/erpnext/exceptions.py b/erpnext/exceptions.py index d92af5d722..04291cd5bd 100644 --- a/erpnext/exceptions.py +++ b/erpnext/exceptions.py @@ -6,3 +6,5 @@ class PartyFrozen(frappe.ValidationError): pass class InvalidAccountCurrency(frappe.ValidationError): pass class InvalidCurrency(frappe.ValidationError): pass class PartyDisabled(frappe.ValidationError):pass +class InvalidAccountDimensionError(frappe.ValidationError): pass +class MandatoryAccountDimensionError(frappe.ValidationError): pass diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py index e55a1433a5..c324228467 100644 --- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py +++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py @@ -100,7 +100,6 @@ class ClinicalProcedure(Document): allow_start = self.set_actual_qty() if allow_start: self.db_set('status', 'In Progress') - insert_clinical_procedure_to_medical_record(self) return 'success' return 'insufficient stock' @@ -247,21 +246,3 @@ def make_procedure(source_name, target_doc=None): }, target_doc, set_missing_values) return doc - - -def insert_clinical_procedure_to_medical_record(doc): - subject = frappe.bold(_("Clinical Procedure conducted: ")) + cstr(doc.procedure_template) + "
" - if doc.practitioner: - subject += frappe.bold(_('Healthcare Practitioner: ')) + doc.practitioner - if subject and doc.notes: - subject += '
' + doc.notes - - medical_record = frappe.new_doc('Patient Medical Record') - medical_record.patient = doc.patient - medical_record.subject = subject - medical_record.status = 'Open' - medical_record.communication_date = doc.start_date - medical_record.reference_doctype = 'Clinical Procedure' - medical_record.reference_name = doc.name - medical_record.reference_owner = doc.owner - medical_record.save(ignore_permissions=True) diff --git a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json index b33c326313..ddf1bce492 100644 --- a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json +++ b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json @@ -19,6 +19,7 @@ "valid_days", "inpatient_settings_section", "allow_discharge_despite_unbilled_services", + "do_not_bill_inpatient_encounters", "healthcare_service_items", "inpatient_visit_charge_item", "op_consulting_charge_item", @@ -315,11 +316,17 @@ "fieldname": "allow_discharge_despite_unbilled_services", "fieldtype": "Check", "label": "Allow Discharge Despite Unbilled Healthcare Services" + }, + { + "default": "0", + "fieldname": "do_not_bill_inpatient_encounters", + "fieldtype": "Check", + "label": "Do Not Bill Patient Encounters for Inpatients" } ], "issingle": 1, "links": [], - "modified": "2021-01-04 10:19:22.329272", + "modified": "2021-01-13 09:04:35.877700", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare Settings", diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py index bba521313d..e7319085e4 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py @@ -264,7 +264,7 @@ def get_filters(entry): def get_current_healthcare_service_unit(inpatient_record): ip_record = frappe.get_doc('Inpatient Record', inpatient_record) - if ip_record.inpatient_occupancies: + if ip_record.status in ['Admitted', 'Discharge Scheduled'] and ip_record.inpatient_occupancies: return ip_record.inpatient_occupancies[-1].service_unit return diff --git a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py index e8a9444fec..8a918b0275 100644 --- a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py @@ -8,6 +8,8 @@ import unittest from frappe.utils import now_datetime, today from frappe.utils.make_random import get_random from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge +from erpnext.healthcare.doctype.lab_test.test_lab_test import create_patient_encounter +from erpnext.healthcare.utils import get_encounters_to_invoice class TestInpatientRecord(unittest.TestCase): def test_admit_and_discharge(self): @@ -42,7 +44,7 @@ class TestInpatientRecord(unittest.TestCase): def test_allow_discharge_despite_unbilled_services(self): frappe.db.sql("""delete from `tabInpatient Record`""") - setup_inpatient_settings() + setup_inpatient_settings(key="allow_discharge_despite_unbilled_services", value=1) patient = create_patient() # Schedule Admission ip_record = create_inpatient(patient) @@ -64,6 +66,35 @@ class TestInpatientRecord(unittest.TestCase): self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_record")) self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_status")) + setup_inpatient_settings(key="allow_discharge_despite_unbilled_services", value=0) + + def test_do_not_bill_patient_encounters_for_inpatients(self): + frappe.db.sql("""delete from `tabInpatient Record`""") + setup_inpatient_settings(key="do_not_bill_inpatient_encounters", value=1) + patient = create_patient() + # Schedule Admission + ip_record = create_inpatient(patient) + ip_record.expected_length_of_stay = 0 + ip_record.save(ignore_permissions = True) + + # Admit + service_unit = get_healthcare_service_unit() + admit_patient(ip_record, service_unit, now_datetime()) + + # Patient Encounter + patient_encounter = create_patient_encounter() + encounters = get_encounters_to_invoice(patient, "_Test Company") + encounter_ids = [entry.reference_name for entry in encounters] + self.assertFalse(patient_encounter.name in encounter_ids) + + # Discharge + schedule_discharge(frappe.as_json({"patient": patient})) + self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status")) + + ip_record = frappe.get_doc("Inpatient Record", ip_record.name) + mark_invoiced_inpatient_occupancy(ip_record) + discharge_patient(ip_record) + setup_inpatient_settings(key="do_not_bill_inpatient_encounters", value=0) def test_validate_overlap_admission(self): frappe.db.sql("""delete from `tabInpatient Record`""") @@ -89,9 +120,9 @@ def mark_invoiced_inpatient_occupancy(ip_record): ip_record.save(ignore_permissions = True) -def setup_inpatient_settings(): +def setup_inpatient_settings(key, value): settings = frappe.get_single("Healthcare Settings") - settings.allow_discharge_despite_unbilled_services = 1 + settings.set(key, value) settings.save() @@ -111,11 +142,15 @@ def create_inpatient(patient): return inpatient_record -def get_healthcare_service_unit(): - service_unit = get_random("Healthcare Service Unit", filters={"inpatient_occupancy": 1}) +def get_healthcare_service_unit(unit_name=None): + if not unit_name: + service_unit = get_random("Healthcare Service Unit", filters={"inpatient_occupancy": 1}) + else: + service_unit = frappe.db.exists("Healthcare Service Unit", {"healthcare_service_unit_name": unit_name}) + if not service_unit: service_unit = frappe.new_doc("Healthcare Service Unit") - service_unit.healthcare_service_unit_name = "Test Service Unit Ip Occupancy" + service_unit.healthcare_service_unit_name = unit_name or "Test Service Unit Ip Occupancy" service_unit.company = "_Test Company" service_unit.service_unit_type = get_service_unit_type() service_unit.inpatient_occupancy = 1 diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.json b/erpnext/healthcare/doctype/lab_test/lab_test.json index edf1d911aa..ac61fea3ad 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.json +++ b/erpnext/healthcare/doctype/lab_test/lab_test.json @@ -359,6 +359,7 @@ { "fieldname": "normal_test_items", "fieldtype": "Table", + "label": "Normal Test Result", "options": "Normal Test Result", "print_hide": 1 }, @@ -380,6 +381,7 @@ { "fieldname": "sensitivity_test_items", "fieldtype": "Table", + "label": "Sensitivity Test Result", "options": "Sensitivity Test Result", "print_hide": 1, "report_hide": 1 @@ -529,6 +531,7 @@ { "fieldname": "descriptive_test_items", "fieldtype": "Table", + "label": "Descriptive Test Result", "options": "Descriptive Test Result", "print_hide": 1, "report_hide": 1 @@ -549,13 +552,14 @@ { "fieldname": "organism_test_items", "fieldtype": "Table", + "label": "Organism Test Result", "options": "Organism Test Result", "print_hide": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-07-30 18:18:38.516215", + "modified": "2020-11-30 11:04:17.195848", "modified_by": "Administrator", "module": "Healthcare", "name": "Lab Test", diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.py b/erpnext/healthcare/doctype/lab_test/lab_test.py index 2db7743865..4b57cd073d 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/lab_test.py @@ -17,11 +17,9 @@ class LabTest(Document): self.validate_result_values() self.db_set('submitted_date', getdate()) self.db_set('status', 'Completed') - insert_lab_test_to_medical_record(self) def on_cancel(self): self.db_set('status', 'Cancelled') - delete_lab_test_from_medical_record(self) self.reload() def on_update(self): @@ -330,60 +328,6 @@ def get_employee_by_user_id(user_id): return frappe.get_doc('Employee', emp_id) return None -def insert_lab_test_to_medical_record(doc): - table_row = False - subject = cstr(doc.lab_test_name) - if doc.practitioner: - subject += frappe.bold(_('Healthcare Practitioner: '))+ doc.practitioner + '
' - if doc.normal_test_items: - item = doc.normal_test_items[0] - comment = '' - if item.lab_test_comment: - comment = str(item.lab_test_comment) - table_row = frappe.bold(_('Lab Test Conducted: ')) + item.lab_test_name - - if item.lab_test_event: - table_row += frappe.bold(_('Lab Test Event: ')) + item.lab_test_event - - if item.result_value: - table_row += ' ' + frappe.bold(_('Lab Test Result: ')) + item.result_value - - if item.normal_range: - table_row += ' ' + _('Normal Range: ') + item.normal_range - table_row += ' ' + comment - - elif doc.descriptive_test_items: - item = doc.descriptive_test_items[0] - - if item.lab_test_particulars and item.result_value: - table_row = item.lab_test_particulars + ' ' + item.result_value - - elif doc.sensitivity_test_items: - item = doc.sensitivity_test_items[0] - - if item.antibiotic and item.antibiotic_sensitivity: - table_row = item.antibiotic + ' ' + item.antibiotic_sensitivity - - if table_row: - subject += '
' + table_row - if doc.lab_test_comment: - subject += '
' + cstr(doc.lab_test_comment) - - medical_record = frappe.new_doc('Patient Medical Record') - medical_record.patient = doc.patient - medical_record.subject = subject - medical_record.status = 'Open' - medical_record.communication_date = doc.result_date - medical_record.reference_doctype = 'Lab Test' - medical_record.reference_name = doc.name - medical_record.reference_owner = doc.owner - medical_record.save(ignore_permissions = True) - -def delete_lab_test_from_medical_record(self): - medical_record_id = frappe.db.sql('select name from `tabPatient Medical Record` where reference_name=%s', (self.name)) - - if medical_record_id and medical_record_id[0][0]: - frappe.delete_doc('Patient Medical Record', medical_record_id[0][0]) @frappe.whitelist() def get_lab_test_prescribed(patient): diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js index 79e1775b9d..3d5073b13e 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js @@ -31,12 +31,12 @@ frappe.ui.form.on('Patient Appointment', { }; }); - frm.set_query('service_unit', function(){ + frm.set_query('service_unit', function() { return { + query: 'erpnext.controllers.queries.get_healthcare_service_units', filters: { - 'is_group': false, - 'allow_appointments': true, - 'company': frm.doc.company + company: frm.doc.company, + inpatient_record: frm.doc.inpatient_record } }; }); diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index dc820cb464..b05c673d84 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -18,6 +18,7 @@ from erpnext.healthcare.utils import check_fee_validity, get_service_item_and_pr class PatientAppointment(Document): def validate(self): self.validate_overlaps() + self.validate_service_unit() self.set_appointment_datetime() self.validate_customer_created() self.set_status() @@ -68,6 +69,19 @@ class PatientAppointment(Document): overlaps[0][1], overlaps[0][2], overlaps[0][3], overlaps[0][4]) frappe.throw(overlapping_details, title=_('Appointments Overlapping')) + def validate_service_unit(self): + if self.inpatient_record and self.service_unit: + from erpnext.healthcare.doctype.inpatient_medication_entry.inpatient_medication_entry import get_current_healthcare_service_unit + + is_inpatient_occupancy_unit = frappe.db.get_value('Healthcare Service Unit', self.service_unit, + 'inpatient_occupancy') + service_unit = get_current_healthcare_service_unit(self.inpatient_record) + if is_inpatient_occupancy_unit and service_unit != self.service_unit: + msg = _('Patient {0} is not admitted in the service unit {1}').format(frappe.bold(self.patient), frappe.bold(self.service_unit)) + '
' + msg += _('Appointment for service units with Inpatient Occupancy can only be created against the unit where patient has been admitted.') + frappe.throw(msg, title=_('Invalid Healthcare Service Unit')) + + def set_appointment_datetime(self): self.appointment_datetime = "%s %s" % (self.appointment_date, self.appointment_time or "00:00:00") diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index b681ed1a22..f7ec6f58fc 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import unittest import frappe from erpnext.healthcare.doctype.patient_appointment.patient_appointment import update_status, make_encounter -from frappe.utils import nowdate, add_days +from frappe.utils import nowdate, add_days, now_datetime from frappe.utils.make_random import get_random from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile @@ -78,6 +78,59 @@ class TestPatientAppointment(unittest.TestCase): sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent') self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'status'), 'Cancelled') + def test_appointment_booking_for_admission_service_unit(self): + from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge + from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import \ + create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy + + frappe.db.sql("""delete from `tabInpatient Record`""") + patient, medical_department, practitioner = create_healthcare_docs() + patient = create_patient() + # Schedule Admission + ip_record = create_inpatient(patient) + ip_record.expected_length_of_stay = 0 + ip_record.save(ignore_permissions = True) + + # Admit + service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy') + admit_patient(ip_record, service_unit, now_datetime()) + + appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit) + self.assertEqual(appointment.service_unit, service_unit) + + # Discharge + schedule_discharge(frappe.as_json({'patient': patient})) + ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name) + mark_invoiced_inpatient_occupancy(ip_record1) + discharge_patient(ip_record1) + + def test_invalid_healthcare_service_unit_validation(self): + from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge + from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import \ + create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy + + frappe.db.sql("""delete from `tabInpatient Record`""") + patient, medical_department, practitioner = create_healthcare_docs() + patient = create_patient() + # Schedule Admission + ip_record = create_inpatient(patient) + ip_record.expected_length_of_stay = 0 + ip_record.save(ignore_permissions = True) + + # Admit + service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy') + admit_patient(ip_record, service_unit, now_datetime()) + + appointment_service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy for Appointment') + appointment = create_appointment(patient, practitioner, nowdate(), service_unit=appointment_service_unit, save=0) + self.assertRaises(frappe.exceptions.ValidationError, appointment.save) + + # Discharge + schedule_discharge(frappe.as_json({'patient': patient})) + ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name) + mark_invoiced_inpatient_occupancy(ip_record1) + discharge_patient(ip_record1) + def create_healthcare_docs(): patient = create_patient() @@ -125,7 +178,7 @@ def create_encounter(appointment): encounter.submit() return encounter -def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0): +def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0, service_unit=None, save=1): item = create_healthcare_service_items() frappe.db.set_value('Healthcare Settings', None, 'inpatient_visit_charge_item', item) frappe.db.set_value('Healthcare Settings', None, 'op_consulting_charge_item', item) @@ -136,12 +189,15 @@ def create_appointment(patient, practitioner, appointment_date, invoice=0, proce appointment.appointment_date = appointment_date appointment.company = '_Test Company' appointment.duration = 15 + if service_unit: + appointment.service_unit = service_unit if invoice: appointment.mode_of_payment = 'Cash' appointment.paid_amount = 500 if procedure_template: appointment.procedure_template = create_clinical_procedure_template().get('name') - appointment.save(ignore_permissions=True) + if save: + appointment.save(ignore_permissions=True) return appointment def create_healthcare_service_items(): @@ -152,6 +208,7 @@ def create_healthcare_service_items(): item.item_name = 'Consulting Charges' item.item_group = 'Services' item.is_stock_item = 0 + item.stock_uom = 'Nos' item.save() return item.name diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json index 15675f4673..b646ff9ebe 100644 --- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json +++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json @@ -210,7 +210,7 @@ { "fieldname": "drug_prescription", "fieldtype": "Table", - "label": "Items", + "label": "Drug Prescription", "options": "Drug Prescription" }, { @@ -328,7 +328,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-05-16 21:00:08.644531", + "modified": "2020-11-30 10:39:00.783119", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient Encounter", diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py index 87f42491fc..cc2141790f 100644 --- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py +++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py @@ -17,10 +17,6 @@ class PatientEncounter(Document): def on_update(self): if self.appointment: frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Closed') - update_encounter_medical_record(self) - - def after_insert(self): - insert_encounter_to_medical_record(self) def on_submit(self): if self.therapies: @@ -33,8 +29,6 @@ class PatientEncounter(Document): if self.inpatient_record and self.drug_prescription: delete_ip_medication_order(self) - delete_medical_record(self) - def set_title(self): self.title = _('{0} with {1}').format(self.patient_name or self.patient, self.practitioner_name or self.practitioner)[:100] @@ -102,61 +96,7 @@ def create_therapy_plan(encounter): frappe.msgprint(_('Therapy Plan {0} created successfully.').format(frappe.bold(doc.name)), alert=True) -def insert_encounter_to_medical_record(doc): - subject = set_subject_field(doc) - medical_record = frappe.new_doc('Patient Medical Record') - medical_record.patient = doc.patient - medical_record.subject = subject - medical_record.status = 'Open' - medical_record.communication_date = doc.encounter_date - medical_record.reference_doctype = 'Patient Encounter' - medical_record.reference_name = doc.name - medical_record.reference_owner = doc.owner - medical_record.save(ignore_permissions=True) - - -def update_encounter_medical_record(encounter): - medical_record_id = frappe.db.exists('Patient Medical Record', {'reference_name': encounter.name}) - - if medical_record_id and medical_record_id[0][0]: - subject = set_subject_field(encounter) - frappe.db.set_value('Patient Medical Record', medical_record_id[0][0], 'subject', subject) - else: - insert_encounter_to_medical_record(encounter) - - -def delete_medical_record(encounter): - record = frappe.db.exists('Patient Medical Record', {'reference_name', encounter.name}) - if record: - frappe.delete_doc('Patient Medical Record', record, force=1) - def delete_ip_medication_order(encounter): record = frappe.db.exists('Inpatient Medication Order', {'patient_encounter': encounter.name}) if record: - frappe.delete_doc('Inpatient Medication Order', record, force=1) - - -def set_subject_field(encounter): - subject = frappe.bold(_('Healthcare Practitioner: ')) + encounter.practitioner + '
' - if encounter.symptoms: - subject += frappe.bold(_('Symptoms: ')) + '
' - for entry in encounter.symptoms: - subject += cstr(entry.complaint) + '
' - else: - subject += frappe.bold(_('No Symptoms')) + '
' - - if encounter.diagnosis: - subject += frappe.bold(_('Diagnosis: ')) + '
' - for entry in encounter.diagnosis: - subject += cstr(entry.diagnosis) + '
' - else: - subject += frappe.bold(_('No Diagnosis')) + '
' - - if encounter.drug_prescription: - subject += '
' + _('Drug(s) Prescribed.') - if encounter.lab_test_prescription: - subject += '
' + _('Test(s) Prescribed.') - if encounter.procedure_prescription: - subject += '
' + _('Procedure(s) Prescribed.') - - return subject + frappe.delete_doc('Inpatient Medication Order', record, force=1) \ No newline at end of file diff --git a/erpnext/healthcare/doctype/patient_history_custom_document_type/__init__.py b/erpnext/healthcare/doctype/patient_history_custom_document_type/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.json b/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.json new file mode 100644 index 0000000000..3025c7b06d --- /dev/null +++ b/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.json @@ -0,0 +1,55 @@ +{ + "actions": [], + "creation": "2020-11-25 13:40:23.054469", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "date_fieldname", + "add_edit_fields", + "selected_fields" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "selected_fields", + "fieldtype": "Code", + "label": "Selected Fields", + "read_only": 1 + }, + { + "fieldname": "add_edit_fields", + "fieldtype": "Button", + "in_list_view": 1, + "label": "Add / Edit Fields" + }, + { + "fieldname": "date_fieldname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Date Fieldname", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-11-30 13:54:37.474671", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Patient History Custom Document Type", + "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/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.py b/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.py new file mode 100644 index 0000000000..f0a1f929f4 --- /dev/null +++ b/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 PatientHistoryCustomDocumentType(Document): + pass diff --git a/erpnext/healthcare/doctype/patient_history_settings/__init__.py b/erpnext/healthcare/doctype/patient_history_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js new file mode 100644 index 0000000000..453da6a12b --- /dev/null +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js @@ -0,0 +1,133 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Patient History Settings', { + refresh: function(frm) { + frm.set_query('document_type', 'custom_doctypes', () => { + return { + filters: { + custom: 1, + is_submittable: 1, + module: 'Healthcare', + } + }; + }); + }, + + field_selector: function(frm, doc, standard=1) { + let document_fields = []; + if (doc.selected_fields) + document_fields = (JSON.parse(doc.selected_fields)).map(f => f.fieldname); + + frm.call({ + method: 'get_doctype_fields', + doc: frm.doc, + args: { + document_type: doc.document_type, + fields: document_fields + }, + freeze: true, + callback: function(r) { + if (r.message) { + let doctype = 'Patient History Custom Document Type'; + if (standard) + doctype = 'Patient History Standard Document Type'; + + frm.events.show_field_selector_dialog(frm, doc, doctype, r.message); + } + } + }); + }, + + show_field_selector_dialog: function(frm, doc, doctype, doc_fields) { + let d = new frappe.ui.Dialog({ + title: __('{0} Fields', [__(doc.document_type)]), + fields: [ + { + label: __('Select Fields'), + fieldtype: 'MultiCheck', + fieldname: 'fields', + options: doc_fields, + columns: 2 + } + ] + }); + + d.$body.prepend(` + ` + ); + + frappe.utils.setup_search(d.$body, '.unit-checkbox', '.label-area'); + + d.set_primary_action(__('Save'), () => { + let values = d.get_values().fields; + + let selected_fields = []; + + frappe.model.with_doctype(doc.document_type, function() { + for (let idx in values) { + let value = values[idx]; + + let field = frappe.get_meta(doc.document_type).fields.filter((df) => df.fieldname == value)[0]; + if (field) { + selected_fields.push({ + label: field.label, + fieldname: field.fieldname, + fieldtype: field.fieldtype + }); + } + } + + d.refresh(); + frappe.model.set_value(doctype, doc.name, 'selected_fields', JSON.stringify(selected_fields)); + }); + + d.hide(); + }); + + d.show(); + }, + + get_date_field_for_dt: function(frm, row) { + frm.call({ + method: 'get_date_field_for_dt', + doc: frm.doc, + args: { + document_type: row.document_type + }, + callback: function(data) { + if (data.message) { + frappe.model.set_value('Patient History Custom Document Type', + row.name, 'date_fieldname', data.message); + } + } + }); + } +}); + +frappe.ui.form.on('Patient History Custom Document Type', { + document_type: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.document_type) { + frm.events.get_date_field_for_dt(frm, row); + } + }, + + add_edit_fields: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.document_type) { + frm.events.field_selector(frm, row, 0); + } + } +}); + +frappe.ui.form.on('Patient History Standard Document Type', { + add_edit_fields: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.document_type) { + frm.events.field_selector(frm, row); + } + } +}); diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.json b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.json new file mode 100644 index 0000000000..143e2c91eb --- /dev/null +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.json @@ -0,0 +1,55 @@ +{ + "actions": [], + "creation": "2020-11-25 13:41:37.675518", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "standard_doctypes", + "section_break_2", + "custom_doctypes" + ], + "fields": [ + { + "fieldname": "section_break_2", + "fieldtype": "Section Break" + }, + { + "fieldname": "custom_doctypes", + "fieldtype": "Table", + "label": "Custom Document Types", + "options": "Patient History Custom Document Type" + }, + { + "fieldname": "standard_doctypes", + "fieldtype": "Table", + "label": "Standard Document Types", + "options": "Patient History Standard Document Type", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2020-11-25 13:43:38.511771", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Patient History Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py new file mode 100644 index 0000000000..2e8c994c3d --- /dev/null +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py @@ -0,0 +1,188 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import json +from frappe import _ +from frappe.utils import cstr, cint +from frappe.model.document import Document +from erpnext.healthcare.page.patient_history.patient_history import get_patient_history_doctypes + +class PatientHistorySettings(Document): + def validate(self): + self.validate_submittable_doctypes() + self.validate_date_fieldnames() + + def validate_submittable_doctypes(self): + for entry in self.custom_doctypes: + if not cint(frappe.db.get_value('DocType', entry.document_type, 'is_submittable')): + msg = _('Row #{0}: Document Type {1} is not submittable. ').format( + entry.idx, frappe.bold(entry.document_type)) + msg += _('Patient Medical Record can only be created for submittable document types.') + frappe.throw(msg) + + def validate_date_fieldnames(self): + for entry in self.custom_doctypes: + field = frappe.get_meta(entry.document_type).get_field(entry.date_fieldname) + if not field: + frappe.throw(_('Row #{0}: No such Field named {1} found in the Document Type {2}.').format( + entry.idx, frappe.bold(entry.date_fieldname), frappe.bold(entry.document_type))) + + if field.fieldtype not in ['Date', 'Datetime']: + frappe.throw(_('Row #{0}: Field {1} in Document Type {2} is not a Date / Datetime field.').format( + entry.idx, frappe.bold(entry.date_fieldname), frappe.bold(entry.document_type))) + + def get_doctype_fields(self, document_type, fields): + multicheck_fields = [] + doc_fields = frappe.get_meta(document_type).fields + + for field in doc_fields: + if field.fieldtype not in frappe.model.no_value_fields or \ + field.fieldtype in frappe.model.table_fields and not field.hidden: + multicheck_fields.append({ + 'label': field.label, + 'value': field.fieldname, + 'checked': 1 if field.fieldname in fields else 0 + }) + + return multicheck_fields + + def get_date_field_for_dt(self, document_type): + meta = frappe.get_meta(document_type) + date_fields = meta.get('fields', { + 'fieldtype': ['in', ['Date', 'Datetime']] + }) + + if date_fields: + return date_fields[0].get('fieldname') + +def create_medical_record(doc, method=None): + medical_record_required = validate_medical_record_required(doc) + if not medical_record_required: + return + + if frappe.db.exists('Patient Medical Record', { 'reference_name': doc.name }): + return + + subject = set_subject_field(doc) + date_field = get_date_field(doc.doctype) + medical_record = frappe.new_doc('Patient Medical Record') + medical_record.patient = doc.patient + medical_record.subject = subject + medical_record.status = 'Open' + medical_record.communication_date = doc.get(date_field) + medical_record.reference_doctype = doc.doctype + medical_record.reference_name = doc.name + medical_record.reference_owner = doc.owner + medical_record.save(ignore_permissions=True) + + +def update_medical_record(doc, method=None): + medical_record_required = validate_medical_record_required(doc) + if not medical_record_required: + return + + medical_record_id = frappe.db.exists('Patient Medical Record', { 'reference_name': doc.name }) + + if medical_record_id: + subject = set_subject_field(doc) + frappe.db.set_value('Patient Medical Record', medical_record_id[0][0], 'subject', subject) + else: + create_medical_record(doc) + + +def delete_medical_record(doc, method=None): + medical_record_required = validate_medical_record_required(doc) + if not medical_record_required: + return + + record = frappe.db.exists('Patient Medical Record', { 'reference_name': doc.name }) + if record: + frappe.delete_doc('Patient Medical Record', record, force=1) + + +def set_subject_field(doc): + from frappe.utils.formatters import format_value + + meta = frappe.get_meta(doc.doctype) + subject = '' + patient_history_fields = get_patient_history_fields(doc) + + for entry in patient_history_fields: + fieldname = entry.get('fieldname') + if entry.get('fieldtype') == 'Table' and doc.get(fieldname): + formatted_value = get_formatted_value_for_table_field(doc.get(fieldname), meta.get_field(fieldname)) + subject += frappe.bold(_(entry.get('label')) + ': ') + '
' + cstr(formatted_value) + '
' + + else: + if doc.get(fieldname): + formatted_value = format_value(doc.get(fieldname), meta.get_field(fieldname), doc) + subject += frappe.bold(_(entry.get('label')) + ': ') + cstr(formatted_value) + '
' + + return subject + + +def get_date_field(doctype): + dt = get_patient_history_config_dt(doctype) + + return frappe.db.get_value(dt, { 'document_type': doctype }, 'date_fieldname') + + +def get_patient_history_fields(doc): + dt = get_patient_history_config_dt(doc.doctype) + patient_history_fields = frappe.db.get_value(dt, { 'document_type': doc.doctype }, 'selected_fields') + + if patient_history_fields: + return json.loads(patient_history_fields) + + +def get_formatted_value_for_table_field(items, df): + child_meta = frappe.get_meta(df.options) + + table_head = '' + table_row = '' + html = '' + create_head = True + for item in items: + table_row += '' + for cdf in child_meta.fields: + if cdf.in_list_view: + if create_head: + table_head += '' + cdf.label + '' + if item.get(cdf.fieldname): + table_row += '' + str(item.get(cdf.fieldname)) + '' + else: + table_row += '' + create_head = False + table_row += '' + + html += "" + table_head + table_row + "
" + + return html + + +def get_patient_history_config_dt(doctype): + if frappe.db.get_value('DocType', doctype, 'custom'): + return 'Patient History Custom Document Type' + else: + return 'Patient History Standard Document Type' + + +def validate_medical_record_required(doc): + if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_setup_wizard \ + or get_module(doc) != 'Healthcare': + return False + + if doc.doctype not in get_patient_history_doctypes(): + return False + + return True + +def get_module(doc): + module = doc.meta.module + if not module: + module = frappe.db.get_value('DocType', doc.doctype, 'module') + + return module \ No newline at end of file diff --git a/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py new file mode 100644 index 0000000000..c93b788aed --- /dev/null +++ b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest +import json +from frappe.utils import getdate +from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_patient + +class TestPatientHistorySettings(unittest.TestCase): + def setUp(self): + dt = create_custom_doctype() + settings = frappe.get_single("Patient History Settings") + settings.append("custom_doctypes", { + "document_type": dt.name, + "date_fieldname": "date", + "selected_fields": json.dumps([{ + "label": "Date", + "fieldname": "date", + "fieldtype": "Date" + }, + { + "label": "Rating", + "fieldname": "rating", + "fieldtype": "Rating" + }, + { + "label": "Feedback", + "fieldname": "feedback", + "fieldtype": "Small Text" + }]) + }) + settings.save() + + def test_custom_doctype_medical_record(self): + # tests for medical record creation of standard doctypes in test_patient_medical_record.py + patient = create_patient() + doc = create_doc(patient) + + # check for medical record + medical_rec = frappe.db.exists("Patient Medical Record", {"status": "Open", "reference_name": doc.name}) + self.assertTrue(medical_rec) + + medical_rec = frappe.get_doc("Patient Medical Record", medical_rec) + expected_subject = "Date: {0}
Rating: 3
Feedback: Test Patient History Settings
".format( + frappe.utils.format_date(getdate())) + self.assertEqual(medical_rec.subject, expected_subject) + self.assertEqual(medical_rec.patient, patient) + self.assertEqual(medical_rec.communication_date, getdate()) + + +def create_custom_doctype(): + if not frappe.db.exists("DocType", "Test Patient Feedback"): + doc = frappe.get_doc({ + "doctype": "DocType", + "module": "Healthcare", + "custom": 1, + "is_submittable": 1, + "fields": [{ + "label": "Date", + "fieldname": "date", + "fieldtype": "Date" + }, + { + "label": "Patient", + "fieldname": "patient", + "fieldtype": "Link", + "options": "Patient" + }, + { + "label": "Rating", + "fieldname": "rating", + "fieldtype": "Rating" + }, + { + "label": "Feedback", + "fieldname": "feedback", + "fieldtype": "Small Text" + }], + "permissions": [{ + "role": "System Manager", + "read": 1 + }], + "name": "Test Patient Feedback", + }) + doc.insert() + return doc + else: + return frappe.get_doc("DocType", "Test Patient Feedback") + + +def create_doc(patient): + doc = frappe.get_doc({ + "doctype": "Test Patient Feedback", + "patient": patient, + "date": getdate(), + "rating": 3, + "feedback": "Test Patient History Settings" + }).insert() + doc.submit() + + return doc \ No newline at end of file diff --git a/erpnext/healthcare/doctype/patient_history_standard_document_type/__init__.py b/erpnext/healthcare/doctype/patient_history_standard_document_type/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.json b/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.json new file mode 100644 index 0000000000..b43099c4ea --- /dev/null +++ b/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.json @@ -0,0 +1,57 @@ +{ + "actions": [], + "creation": "2020-11-25 13:39:36.014814", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "date_fieldname", + "add_edit_fields", + "selected_fields" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "selected_fields", + "fieldtype": "Code", + "label": "Selected Fields", + "read_only": 1 + }, + { + "fieldname": "add_edit_fields", + "fieldtype": "Button", + "in_list_view": 1, + "label": "Add / Edit Fields" + }, + { + "fieldname": "date_fieldname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Date Fieldname", + "read_only": 1, + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-11-30 13:54:56.773325", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Patient History Standard Document Type", + "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/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.py b/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.py new file mode 100644 index 0000000000..2d94911855 --- /dev/null +++ b/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 PatientHistoryStandardDocumentType(Document): + pass diff --git a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py index 419d956425..c1d9872a01 100644 --- a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py +++ b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py @@ -18,6 +18,7 @@ class TestPatientMedicalRecord(unittest.TestCase): patient, medical_department, practitioner = create_healthcare_docs() appointment = create_appointment(patient, practitioner, nowdate(), invoice=1) encounter = create_encounter(appointment) + # check for encounter medical_rec = frappe.db.exists('Patient Medical Record', {'status': 'Open', 'reference_name': encounter.name}) self.assertTrue(medical_rec) diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.py b/erpnext/healthcare/doctype/therapy_session/therapy_session.py index c00054421d..51f267f949 100644 --- a/erpnext/healthcare/doctype/therapy_session/therapy_session.py +++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.py @@ -41,7 +41,6 @@ class TherapySession(Document): def on_submit(self): self.update_sessions_count_in_therapy_plan() - insert_session_medical_record(self) def on_update(self): if self.appointment: @@ -142,23 +141,3 @@ def get_therapy_item(therapy, item): item.reference_dt = 'Therapy Session' item.reference_dn = therapy.name return item - - -def insert_session_medical_record(doc): - subject = frappe.bold(_('Therapy: ')) + cstr(doc.therapy_type) + '
' - if doc.therapy_plan: - subject += frappe.bold(_('Therapy Plan: ')) + cstr(doc.therapy_plan) + '
' - if doc.practitioner: - subject += frappe.bold(_('Healthcare Practitioner: ')) + doc.practitioner - subject += frappe.bold(_('Total Counts Targeted: ')) + cstr(doc.total_counts_targeted) + '
' - subject += frappe.bold(_('Total Counts Completed: ')) + cstr(doc.total_counts_completed) + '
' - - medical_record = frappe.new_doc('Patient Medical Record') - medical_record.patient = doc.patient - medical_record.subject = subject - medical_record.status = 'Open' - medical_record.communication_date = doc.start_date - medical_record.reference_doctype = 'Therapy Session' - medical_record.reference_name = doc.name - medical_record.reference_owner = doc.owner - medical_record.save(ignore_permissions=True) \ No newline at end of file diff --git a/erpnext/healthcare/doctype/vital_signs/vital_signs.py b/erpnext/healthcare/doctype/vital_signs/vital_signs.py index 69d81ff4b0..35c823d739 100644 --- a/erpnext/healthcare/doctype/vital_signs/vital_signs.py +++ b/erpnext/healthcare/doctype/vital_signs/vital_signs.py @@ -12,47 +12,7 @@ class VitalSigns(Document): def validate(self): self.set_title() - def on_submit(self): - insert_vital_signs_to_medical_record(self) - - def on_cancel(self): - delete_vital_signs_from_medical_record(self) - def set_title(self): self.title = _('{0} on {1}').format(self.patient_name or self.patient, frappe.utils.format_date(self.signs_date))[:100] -def insert_vital_signs_to_medical_record(doc): - subject = set_subject_field(doc) - medical_record = frappe.new_doc('Patient Medical Record') - medical_record.patient = doc.patient - medical_record.subject = subject - medical_record.status = 'Open' - medical_record.communication_date = doc.signs_date - medical_record.reference_doctype = 'Vital Signs' - medical_record.reference_name = doc.name - medical_record.reference_owner = doc.owner - medical_record.flags.ignore_mandatory = True - medical_record.save(ignore_permissions=True) - -def delete_vital_signs_from_medical_record(doc): - medical_record = frappe.db.get_value('Patient Medical Record', {'reference_name': doc.name}) - if medical_record: - frappe.delete_doc('Patient Medical Record', medical_record) - -def set_subject_field(doc): - subject = '' - if doc.temperature: - subject += frappe.bold(_('Temperature: ')) + cstr(doc.temperature) + '
' - if doc.pulse: - subject += frappe.bold(_('Pulse: ')) + cstr(doc.pulse) + '
' - if doc.respiratory_rate: - subject += frappe.bold(_('Respiratory Rate: ')) + cstr(doc.respiratory_rate) + '
' - if doc.bp: - subject += frappe.bold(_('BP: ')) + cstr(doc.bp) + '
' - if doc.bmi: - subject += frappe.bold(_('BMI: ')) + cstr(doc.bmi) + '
' - if doc.nutrition_note: - subject += frappe.bold(_('Note: ')) + cstr(doc.nutrition_note) + '
' - - return subject diff --git a/erpnext/healthcare/page/patient_history/patient_history.css b/erpnext/healthcare/page/patient_history/patient_history.css index 865d6abee0..1bb589164e 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.css +++ b/erpnext/healthcare/page/patient_history/patient_history.css @@ -109,6 +109,11 @@ padding-right: 0px; } +.patient-history-filter { + margin-left: 35px; + width: 25%; +} + #page-medical_record .plot-wrapper { padding: 20px 15px; border-bottom: 1px solid #d1d8dd; diff --git a/erpnext/healthcare/page/patient_history/patient_history.html b/erpnext/healthcare/page/patient_history/patient_history.html index 7a9446dffd..be486c62d1 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.html +++ b/erpnext/healthcare/page/patient_history/patient_history.html @@ -1,6 +1,5 @@
-

{%= __("Select Patient") %}

@@ -11,6 +10,13 @@
+ +
+
+
+
+
+
diff --git a/erpnext/healthcare/page/patient_history/patient_history.js b/erpnext/healthcare/page/patient_history/patient_history.js index fe5b7bc488..54343aae44 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.js +++ b/erpnext/healthcare/page/patient_history/patient_history.js @@ -1,141 +1,225 @@ -frappe.provide("frappe.patient_history"); +frappe.provide('frappe.patient_history'); frappe.pages['patient_history'].on_page_load = function(wrapper) { - var me = this; - var page = frappe.ui.make_app_page({ + let me = this; + let page = frappe.ui.make_app_page({ parent: wrapper, title: 'Patient History', single_column: true }); - frappe.breadcrumbs.add("Healthcare"); + frappe.breadcrumbs.add('Healthcare'); let pid = ''; - page.main.html(frappe.render_template("patient_history", {})); - var patient = frappe.ui.form.make_control({ - parent: page.main.find(".patient"), + page.main.html(frappe.render_template('patient_history', {})); + page.main.find('.header-separator').hide(); + + let patient = frappe.ui.form.make_control({ + parent: page.main.find('.patient'), df: { - fieldtype: "Link", - options: "Patient", - fieldname: "patient", - change: function(){ - if(pid != patient.get_value() && patient.get_value()){ + fieldtype: 'Link', + options: 'Patient', + fieldname: 'patient', + placeholder: __('Select Patient'), + only_select: true, + change: function() { + let patient_id = patient.get_value(); + if (pid != patient_id && patient_id) { me.start = 0; - me.page.main.find(".patient_documents_list").html(""); - get_documents(patient.get_value(), me); - show_patient_info(patient.get_value(), me); - show_patient_vital_charts(patient.get_value(), me, "bp", "mmHg", "Blood Pressure"); + me.page.main.find('.patient_documents_list').html(''); + setup_filters(patient_id, me); + get_documents(patient_id, me); + show_patient_info(patient_id, me); + show_patient_vital_charts(patient_id, me, 'bp', 'mmHg', 'Blood Pressure'); } - pid = patient.get_value(); + pid = patient_id; } }, - only_input: true, }); patient.refresh(); - if (frappe.route_options){ + if (frappe.route_options) { patient.set_value(frappe.route_options.patient); } - this.page.main.on("click", ".btn-show-chart", function() { - var btn_show_id = $(this).attr("data-show-chart-id"), pts = $(this).attr("data-pts"); - var title = $(this).attr("data-title"); + this.page.main.on('click', '.btn-show-chart', function() { + let btn_show_id = $(this).attr('data-show-chart-id'), pts = $(this).attr('data-pts'); + let title = $(this).attr('data-title'); show_patient_vital_charts(patient.get_value(), me, btn_show_id, pts, title); }); - this.page.main.on("click", ".btn-more", function() { - var doctype = $(this).attr("data-doctype"), docname = $(this).attr("data-docname"); - if(me.page.main.find("."+docname).parent().find('.document-html').attr('data-fetched') == "1"){ - me.page.main.find("."+docname).hide(); - me.page.main.find("."+docname).parent().find('.document-html').show(); - }else{ - if(doctype && docname){ - let exclude = ["patient", "patient_name", 'patient_sex', "encounter_date"]; + this.page.main.on('click', '.btn-more', function() { + let doctype = $(this).attr('data-doctype'), docname = $(this).attr('data-docname'); + if (me.page.main.find('.'+docname).parent().find('.document-html').attr('data-fetched') == '1') { + me.page.main.find('.'+docname).hide(); + me.page.main.find('.'+docname).parent().find('.document-html').show(); + } else { + if (doctype && docname) { + let exclude = ['patient', 'patient_name', 'patient_sex', 'encounter_date']; frappe.call({ - method: "erpnext.healthcare.utils.render_doc_as_html", + method: 'erpnext.healthcare.utils.render_doc_as_html', args:{ doctype: doctype, docname: docname, exclude_fields: exclude }, + freeze: true, callback: function(r) { - if (r.message){ - me.page.main.find("."+docname).hide(); - me.page.main.find("."+docname).parent().find('.document-html').html(r.message.html+"\ -
"); - me.page.main.find("."+docname).parent().find('.document-html').show(); - me.page.main.find("."+docname).parent().find('.document-html').attr('data-fetched', "1"); + if (r.message) { + me.page.main.find('.' + docname).hide(); + + me.page.main.find('.' + docname).parent().find('.document-html').html( + `${r.message.html} +
+ + +
+ `); + + me.page.main.find('.' + docname).parent().find('.document-html').show(); + me.page.main.find('.' + docname).parent().find('.document-html').attr('data-fetched', '1'); } - }, - freeze: true + } }); } } }); - this.page.main.on("click", ".btn-less", function() { - var docname = $(this).attr("data-docname"); - me.page.main.find("."+docname).parent().find('.document-id').show(); - me.page.main.find("."+docname).parent().find('.document-html').hide(); + this.page.main.on('click', '.btn-less', function() { + let docname = $(this).attr('data-docname'); + me.page.main.find('.' + docname).parent().find('.document-id').show(); + me.page.main.find('.' + docname).parent().find('.document-html').hide(); }); me.start = 0; - me.page.main.on("click", ".btn-get-records", function(){ + me.page.main.on('click', '.btn-get-records', function() { get_documents(patient.get_value(), me); }); }; -var get_documents = function(patient, me){ +let setup_filters = function(patient, me) { + $('.doctype-filter').empty(); + frappe.xcall( + 'erpnext.healthcare.page.patient_history.patient_history.get_patient_history_doctypes' + ).then(document_types => { + let doctype_filter = frappe.ui.form.make_control({ + parent: $('.doctype-filter'), + df: { + fieldtype: 'MultiSelectList', + fieldname: 'document_type', + placeholder: __('Select Document Type'), + input_class: 'input-xs', + change: () => { + me.start = 0; + me.page.main.find('.patient_documents_list').html(''); + get_documents(patient, me, doctype_filter.get_value(), date_range_field.get_value()); + }, + get_data: () => { + return document_types.map(document_type => { + return { + description: document_type, + value: document_type + }; + }); + }, + } + }); + doctype_filter.refresh(); + + $('.date-filter').empty(); + let date_range_field = frappe.ui.form.make_control({ + df: { + fieldtype: 'DateRange', + fieldname: 'date_range', + placeholder: __('Date Range'), + input_class: 'input-xs', + change: () => { + let selected_date_range = date_range_field.get_value(); + if (selected_date_range && selected_date_range.length === 2) { + me.start = 0; + me.page.main.find('.patient_documents_list').html(''); + get_documents(patient, me, doctype_filter.get_value(), selected_date_range); + } + } + }, + parent: $('.date-filter') + }); + date_range_field.refresh(); + }); +}; + +let get_documents = function(patient, me, document_types="", selected_date_range="") { + let filters = { + name: patient, + start: me.start, + page_length: 20 + }; + if (document_types) + filters['document_types'] = document_types; + if (selected_date_range) + filters['date_range'] = selected_date_range; + frappe.call({ - "method": "erpnext.healthcare.page.patient_history.patient_history.get_feed", - args: { - name: patient, - start: me.start, - page_length: 20 - }, - callback: function (r) { - var data = r.message; - if(data.length){ + 'method': 'erpnext.healthcare.page.patient_history.patient_history.get_feed', + args: filters, + callback: function(r) { + let data = r.message; + if (data.length) { add_to_records(me, data); - }else{ - me.page.main.find(".patient_documents_list").append("


No more records..

"); - me.page.main.find(".btn-get-records").hide(); + } else { + me.page.main.find('.patient_documents_list').append(` +
+

${__('No more records..')}

+
`); + me.page.main.find('.btn-get-records').hide(); } } }); }; -var add_to_records = function(me, data){ - var details = ""; - me.page.main.find(".patient_documents_list").append(details); + + details += ''; + me.page.main.find('.patient_documents_list').append(details); me.start += data.length; - if(data.length===20){ + + if (data.length === 20) { me.page.main.find(".btn-get-records").show(); - }else{ + } else { me.page.main.find(".btn-get-records").hide(); - me.page.main.find(".patient_documents_list").append("


No more records..

"); + me.page.main.find(".patient_documents_list").append(` +
+

${__('No more records..')}

+
`); } }; -var add_date_separator = function(data) { - var date = frappe.datetime.str_to_obj(data.creation); +let add_date_separator = function(data) { + let date = frappe.datetime.str_to_obj(data.communication_date); + let pdate = ''; + let diff = frappe.datetime.get_day_diff(frappe.datetime.get_today(), frappe.datetime.obj_to_str(date)); - var diff = frappe.datetime.get_day_diff(frappe.datetime.get_today(), frappe.datetime.obj_to_str(date)); - if(diff < 1) { - var pdate = 'Today'; - } else if(diff < 2) { - pdate = 'Yesterday'; + if (diff < 1) { + pdate = __('Today'); + } else if (diff < 2) { + pdate = __('Yesterday'); } else { - pdate = frappe.datetime.global_date_format(date); + pdate = __('on ') + frappe.datetime.global_date_format(date); } data.date_sep = pdate; return data; }; -var show_patient_info = function(patient, me){ +let show_patient_info = function(patient, me) { frappe.call({ - "method": "erpnext.healthcare.doctype.patient.patient.get_patient_detail", + 'method': 'erpnext.healthcare.doctype.patient.patient.get_patient_detail', args: { patient: patient }, - callback: function (r) { - var data = r.message; - var details = ""; - if(data.image){ - details += "
"; + callback: function(r) { + let data = r.message; + let details = ''; + if (data.image) { + details += `
`; } - details += "" + data.patient_name +"
" + data.sex; - if(data.email) details += "
" + data.email; - if(data.mobile) details += "
" + data.mobile; - if(data.occupation) details += "

Occupation : " + data.occupation; - if(data.blood_group) details += "
Blood group : " + data.blood_group; - if(data.allergies) details += "

Allergies : "+ data.allergies.replace("\n", "
"); - if(data.medication) details += "
Medication : "+ data.medication.replace("\n", "
"); - if(data.alcohol_current_use) details += "

Alcohol use : "+ data.alcohol_current_use; - if(data.alcohol_past_use) details += "
Alcohol past use : "+ data.alcohol_past_use; - if(data.tobacco_current_use) details += "
Tobacco use : "+ data.tobacco_current_use; - if(data.tobacco_past_use) details += "
Tobacco past use : "+ data.tobacco_past_use; - if(data.medical_history) details += "

Medical history : "+ data.medical_history.replace("\n", "
"); - if(data.surgical_history) details += "
Surgical history : "+ data.surgical_history.replace("\n", "
"); - if(data.surrounding_factors) details += "

Occupational hazards : "+ data.surrounding_factors.replace("\n", "
"); - if(data.other_risk_factors) details += "
Other risk factors : " + data.other_risk_factors.replace("\n", "
"); - if(data.patient_details) details += "

More info : " + data.patient_details.replace("\n", "
"); - if(details){ - details = "
" + details + "
"; + details += ` ${data.patient_name}
${data.sex}`; + if (data.email) details += `
${data.email}`; + if (data.mobile) details += `
${data.mobile}`; + if (data.occupation) details += `

${__('Occupation')} : ${data.occupation}`; + if (data.blood_group) details += `
${__('Blood Group')} : ${data.blood_group}`; + if (data.allergies) details += `

${__('Allerigies')} : ${data.allergies.replace("\n", ", ")}`; + if (data.medication) details += `
${__('Medication')} : ${data.medication.replace("\n", ", ")}`; + if (data.alcohol_current_use) details += `

${__('Alcohol use')} : ${data.alcohol_current_use}`; + if (data.alcohol_past_use) details += `
${__('Alcohol past use')} : ${data.alcohol_past_use}`; + if (data.tobacco_current_use) details += `
${__('Tobacco use')} : ${data.tobacco_current_use}`; + if (data.tobacco_past_use) details += `
${__('Tobacco past use')} : ${data.tobacco_past_use}`; + if (data.medical_history) details += `

${__('Medical history')} : ${data.medical_history.replace("\n", ", ")}`; + if (data.surgical_history) details += `
${__('Surgical history')} : ${data.surgical_history.replace("\n", ", ")}`; + if (data.surrounding_factors) details += `

${__('Occupational hazards')} : ${data.surrounding_factors.replace("\n", ", ")}`; + if (data.other_risk_factors) details += `
${__('Other risk factors')} : ${data.other_risk_factors.replace("\n", ", ")}`; + if (data.patient_details) details += `

${__('More info')} : ${data.patient_details.replace("\n", ", ")}`; + + if (details) { + details = `
` + details + `
`; } - me.page.main.find(".patient_details").html(details); + me.page.main.find('.patient_details').html(details); } }); }; -var show_patient_vital_charts = function(patient, me, btn_show_id, pts, title) { +let show_patient_vital_charts = function(patient, me, btn_show_id, pts, title) { frappe.call({ - method: "erpnext.healthcare.utils.get_patient_vitals", + method: 'erpnext.healthcare.utils.get_patient_vitals', args:{ patient: patient }, callback: function(r) { - if (r.message){ - var show_chart_btns_html = "
Blood Pressure\ - Respiratory/Pulse Rate\ - Temperature\ - BMI
"; - me.page.main.find(".show_chart_btns").html(show_chart_btns_html); - var data = r.message; + if (r.message) { + let show_chart_btns_html = ` +
+ + ${__('Blood Pressure')} + + + ${__('Respiratory/Pulse Rate')} + + + ${__('Temperature')} + + + ${__('BMI')} + +
`; + + me.page.main.find('.show_chart_btns').html(show_chart_btns_html); + let data = r.message; let labels = [], datasets = []; let bp_systolic = [], bp_diastolic = [], temperature = []; let pulse = [], respiratory_rate = [], bmi = [], height = [], weight = []; - for(var i=0; i d + ' ' + pts, } }); - }else{ - me.page.main.find(".patient_vital_charts").html(""); - me.page.main.find(".show_chart_btns").html(""); + me.page.main.find('.header-separator').show(); + } else { + me.page.main.find('.patient_vital_charts').html(''); + me.page.main.find('.show_chart_btns').html(''); + me.page.main.find('.header-separator').hide(); } } }); diff --git a/erpnext/healthcare/page/patient_history/patient_history.py b/erpnext/healthcare/page/patient_history/patient_history.py index 772aa4ef5e..4cdfd64a69 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.py +++ b/erpnext/healthcare/page/patient_history/patient_history.py @@ -4,36 +4,70 @@ from __future__ import unicode_literals import frappe +import json from frappe.utils import cint from erpnext.healthcare.utils import render_docs_as_html @frappe.whitelist() -def get_feed(name, start=0, page_length=20): +def get_feed(name, document_types=None, date_range=None, start=0, page_length=20): """get feed""" - result = frappe.db.sql("""select name, owner, creation, - reference_doctype, reference_name, subject - from `tabPatient Medical Record` - where patient=%(patient)s - order by creation desc - limit %(start)s, %(page_length)s""", - { - "patient": name, - "start": cint(start), - "page_length": cint(page_length) - }, as_dict=True) + filters = get_filters(name, document_types, date_range) + + result = frappe.db.get_all('Patient Medical Record', + fields=['name', 'owner', 'communication_date', + 'reference_doctype', 'reference_name', 'subject'], + filters=filters, + order_by='communication_date DESC', + limit=cint(page_length), + start=cint(start) + ) + return result + +def get_filters(name, document_types=None, date_range=None): + filters = {'patient': name} + if document_types: + document_types = json.loads(document_types) + if len(document_types): + filters['reference_doctype'] = ['IN', document_types] + + if date_range: + try: + date_range = json.loads(date_range) + if date_range: + filters['communication_date'] = ['between', [date_range[0], date_range[1]]] + except json.decoder.JSONDecodeError: + pass + + return filters + + @frappe.whitelist() def get_feed_for_dt(doctype, docname): """get feed""" - result = frappe.db.sql("""select name, owner, modified, creation, - reference_doctype, reference_name, subject - from `tabPatient Medical Record` - where reference_name=%(docname)s and reference_doctype=%(doctype)s - order by creation desc""", - { - "docname": docname, - "doctype": doctype - }, as_dict=True) + result = frappe.db.get_all('Patient Medical Record', + fields=['name', 'owner', 'communication_date', + 'reference_doctype', 'reference_name', 'subject'], + filters={ + 'reference_doctype': doctype, + 'reference_name': docname + }, + order_by='communication_date DESC' + ) return result + + +@frappe.whitelist() +def get_patient_history_doctypes(): + document_types = [] + settings = frappe.get_single("Patient History Settings") + + for entry in settings.standard_doctypes: + document_types.append(entry.document_type) + + for entry in settings.custom_doctypes: + document_types.append(entry.document_type) + + return document_types diff --git a/erpnext/healthcare/setup.py b/erpnext/healthcare/setup.py index 06840801d3..bf4df7e4c8 100644 --- a/erpnext/healthcare/setup.py +++ b/erpnext/healthcare/setup.py @@ -16,6 +16,7 @@ def setup_healthcare(): create_healthcare_item_groups() create_sensitivity() add_healthcare_service_unit_tree_root() + setup_patient_history_settings() def create_medical_departments(): departments = [ @@ -213,3 +214,82 @@ def get_company(): if company: return company[0].name return None + +def setup_patient_history_settings(): + import json + + settings = frappe.get_single('Patient History Settings') + configuration = get_patient_history_config() + for dt, config in configuration.items(): + settings.append("standard_doctypes", { + "document_type": dt, + "date_fieldname": config[0], + "selected_fields": json.dumps(config[1]) + }) + settings.save() + +def get_patient_history_config(): + return { + "Patient Encounter": ("encounter_date", [ + {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"}, + {"label": "Symptoms", "fieldname": "symptoms", "fieldtype": "Table Multiselect"}, + {"label": "Diagnosis", "fieldname": "diagnosis", "fieldtype": "Table Multiselect"}, + {"label": "Drug Prescription", "fieldname": "drug_prescription", "fieldtype": "Table"}, + {"label": "Lab Tests", "fieldname": "lab_test_prescription", "fieldtype": "Table"}, + {"label": "Clinical Procedures", "fieldname": "procedure_prescription", "fieldtype": "Table"}, + {"label": "Therapies", "fieldname": "therapies", "fieldtype": "Table"}, + {"label": "Review Details", "fieldname": "encounter_comment", "fieldtype": "Small Text"} + ]), + "Clinical Procedure": ("start_date", [ + {"label": "Procedure Template", "fieldname": "procedure_template", "fieldtype": "Link"}, + {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"}, + {"label": "Notes", "fieldname": "notes", "fieldtype": "Small Text"}, + {"label": "Service Unit", "fieldname": "service_unit", "fieldtype": "Healthcare Service Unit"}, + {"label": "Start Time", "fieldname": "start_time", "fieldtype": "Time"}, + {"label": "Sample", "fieldname": "sample", "fieldtype": "Link"} + ]), + "Lab Test": ("result_date", [ + {"label": "Test Template", "fieldname": "template", "fieldtype": "Link"}, + {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"}, + {"label": "Test Name", "fieldname": "lab_test_name", "fieldtype": "Data"}, + {"label": "Lab Technician Name", "fieldname": "employee_name", "fieldtype": "Data"}, + {"label": "Sample ID", "fieldname": "sample", "fieldtype": "Link"}, + {"label": "Normal Test Result", "fieldname": "normal_test_items", "fieldtype": "Table"}, + {"label": "Descriptive Test Result", "fieldname": "descriptive_test_items", "fieldtype": "Table"}, + {"label": "Organism Test Result", "fieldname": "organism_test_items", "fieldtype": "Table"}, + {"label": "Sensitivity Test Result", "fieldname": "sensitivity_test_items", "fieldtype": "Table"}, + {"label": "Comments", "fieldname": "lab_test_comment", "fieldtype": "Table"} + ]), + "Therapy Session": ("start_date", [ + {"label": "Therapy Type", "fieldname": "therapy_type", "fieldtype": "Link"}, + {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"}, + {"label": "Therapy Plan", "fieldname": "therapy_plan", "fieldtype": "Link"}, + {"label": "Duration", "fieldname": "duration", "fieldtype": "Int"}, + {"label": "Location", "fieldname": "location", "fieldtype": "Link"}, + {"label": "Healthcare Service Unit", "fieldname": "service_unit", "fieldtype": "Link"}, + {"label": "Start Time", "fieldname": "start_time", "fieldtype": "Time"}, + {"label": "Exercises", "fieldname": "exercises", "fieldtype": "Table"}, + {"label": "Total Counts Targeted", "fieldname": "total_counts_targeted", "fieldtype": "Int"}, + {"label": "Total Counts Completed", "fieldname": "total_counts_completed", "fieldtype": "Int"} + ]), + "Vital Signs": ("signs_date", [ + {"label": "Body Temperature", "fieldname": "temperature", "fieldtype": "Data"}, + {"label": "Heart Rate / Pulse", "fieldname": "pulse", "fieldtype": "Data"}, + {"label": "Respiratory rate", "fieldname": "respiratory_rate", "fieldtype": "Data"}, + {"label": "Tongue", "fieldname": "tongue", "fieldtype": "Select"}, + {"label": "Abdomen", "fieldname": "abdomen", "fieldtype": "Select"}, + {"label": "Reflexes", "fieldname": "reflexes", "fieldtype": "Select"}, + {"label": "Blood Pressure", "fieldname": "bp", "fieldtype": "Data"}, + {"label": "Notes", "fieldname": "vital_signs_note", "fieldtype": "Small Text"}, + {"label": "Height (In Meter)", "fieldname": "height", "fieldtype": "Float"}, + {"label": "Weight (In Kilogram)", "fieldname": "weight", "fieldtype": "Float"}, + {"label": "BMI", "fieldname": "bmi", "fieldtype": "Float"} + ]), + "Inpatient Medication Order": ("start_date", [ + {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"}, + {"label": "Start Date", "fieldname": "start_date", "fieldtype": "Date"}, + {"label": "End Date", "fieldname": "end_date", "fieldtype": "Date"}, + {"label": "Medication Orders", "fieldname": "medication_orders", "fieldtype": "Table"}, + {"label": "Total Orders", "fieldname": "total_orders", "fieldtype": "Float"} + ]) + } \ No newline at end of file diff --git a/erpnext/healthcare/utils.py b/erpnext/healthcare/utils.py index 96282f50a9..40f7f9cabd 100644 --- a/erpnext/healthcare/utils.py +++ b/erpnext/healthcare/utils.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import math import frappe from frappe import _ +from frappe.utils.formatters import format_value from frappe.utils import time_diff_in_hours, rounded from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_income_account from erpnext.healthcare.doctype.fee_validity.fee_validity import create_fee_validity @@ -77,11 +78,13 @@ def get_appointments_to_invoice(patient, company): def get_encounters_to_invoice(patient, company): + if not isinstance(patient, str): + patient = patient.name encounters_to_invoice = [] encounters = frappe.get_list( 'Patient Encounter', fields=['*'], - filters={'patient': patient.name, 'company': company, 'invoiced': False, 'docstatus': 1} + filters={'patient': patient, 'company': company, 'invoiced': False, 'docstatus': 1} ) if encounters: for encounter in encounters: @@ -90,6 +93,10 @@ def get_encounters_to_invoice(patient, company): income_account = None service_item = None if encounter.practitioner: + if encounter.inpatient_record and \ + frappe.db.get_single_value('Healthcare Settings', 'do_not_bill_inpatient_encounters'): + continue + service_item, practitioner_charge = get_service_item_and_practitioner_charge(encounter) income_account = get_income_account(encounter.practitioner, encounter.company) @@ -642,11 +649,15 @@ def render_doc_as_html(doctype, docname, exclude_fields = []): html += "" \ + table_head + table_row + "
" continue + #on other field types add label and value to html if not df.hidden and not df.print_hide and doc.get(df.fieldname) and df.fieldname not in exclude_fields: - html += '
{0} : {1}'.format(df.label or df.fieldname, \ - doc.get(df.fieldname)) + if doc.get(df.fieldname): + formatted_value = format_value(doc.get(df.fieldname), meta.get_field(df.fieldname), doc) + html += '
{0} : {1}'.format(df.label or df.fieldname, formatted_value) + if not has_data : has_data = True + if sec_on and col_on and has_data: doc_html += section_html + html + '
' elif sec_on and not col_on and has_data: diff --git a/erpnext/hooks.py b/erpnext/hooks.py index a2d9d861bb..1c20555b82 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -221,6 +221,11 @@ standard_queries = { } doc_events = { + "*": { + "on_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.create_medical_record", + "on_update_after_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.update_medical_record", + "on_cancel": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.delete_medical_record" + }, "Stock Entry": { "on_submit": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty", "on_cancel": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty" @@ -341,7 +346,8 @@ scheduler_events = { "erpnext.selling.doctype.quotation.quotation.set_expired_status", "erpnext.healthcare.doctype.patient_appointment.patient_appointment.update_appointment_status", "erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status", - "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email" + "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email", + "erpnext.non_profit.doctype.membership.membership.set_expired_status" ], "daily_long": [ "erpnext.setup.doctype.email_digest.email_digest.send", @@ -410,9 +416,6 @@ regional_overrides = { 'Italy': { 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.italy.utils.update_itemised_tax_data', 'erpnext.controllers.accounts_controller.validate_regional': 'erpnext.regional.italy.utils.sales_invoice_validate', - }, - 'Germany': { - 'erpnext.controllers.accounts_controller.validate_regional': 'erpnext.regional.germany.accounts_controller.validate_regional', } } user_privacy_documents = [ diff --git a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py index 4e9ee3b143..336e13c9b7 100644 --- a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py +++ b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py @@ -38,7 +38,8 @@ class TestEmployeeOnboarding(unittest.TestCase): onboarding.insert() onboarding.submit() - self.assertEqual(onboarding.project, 'Employee Onboarding : Test Researcher - test@researcher.com') + project_name = frappe.db.get_value("Project", onboarding.project, "project_name") + self.assertEqual(project_name, 'Employee Onboarding : Test Researcher - test@researcher.com') # don't allow making employee if onboarding is not complete self.assertRaises(IncompleteTaskError, make_employee, onboarding.name) diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index 221300b519..629341ff2a 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -2,11 +2,21 @@ // License: GNU General Public License v3. See license.txt frappe.provide("erpnext.hr"); +frappe.provide("erpnext.accounts.dimensions"); -erpnext.hr.ExpenseClaimController = frappe.ui.form.Controller.extend({ - expense_type: function(doc, cdt, cdn) { +frappe.ui.form.on('Expense Claim', { + onload: function(frm) { + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); + }, + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + }, +}); + +frappe.ui.form.on('Expense Claim Detail', { + expense_type: function(frm, cdt, cdn) { var d = locals[cdt][cdn]; - if(!doc.company) { + if (!frm.doc.company) { d.expense_type = ""; frappe.msgprint(__("Please set the Company")); this.frm.refresh_fields(); @@ -20,7 +30,7 @@ erpnext.hr.ExpenseClaimController = frappe.ui.form.Controller.extend({ method: "erpnext.hr.doctype.expense_claim.expense_claim.get_expense_claim_account_and_cost_center", args: { "expense_claim_type": d.expense_type, - "company": doc.company + "company": frm.doc.company }, callback: function(r) { if (r.message) { @@ -32,8 +42,6 @@ erpnext.hr.ExpenseClaimController = frappe.ui.form.Controller.extend({ } }); -$.extend(cur_frm.cscript, new erpnext.hr.ExpenseClaimController({frm: cur_frm})); - cur_frm.add_fetch('employee', 'company', 'company'); cur_frm.add_fetch('employee','employee_name','employee_name'); cur_frm.add_fetch('expense_type','description','description'); @@ -167,15 +175,6 @@ frappe.ui.form.on("Expense Claim", { }; }); - frm.set_query("cost_center", "expenses", function() { - return { - filters: { - "company": frm.doc.company, - "is_group": 0 - } - }; - }); - frm.set_query("payable_account", function() { return { filters: { diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py index 4a0908d457..f9e3a441bf 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py @@ -20,35 +20,36 @@ class TestExpenseClaim(unittest.TestCase): frappe.db.sql("""delete from `tabProject` where name = "_Test Project 1" """) frappe.db.sql("update `tabExpense Claim` set project = '', task = ''") - frappe.get_doc({ + project = frappe.get_doc({ "project_name": "_Test Project 1", "doctype": "Project" - }).save() + }) + project.save() task = frappe.get_doc(dict( doctype = 'Task', subject = '_Test Project Task 1', status = 'Open', - project = '_Test Project 1' + project = project.name )).insert() task_name = task.name payable_account = get_payable_account(company_name) - make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", "_Test Project 1", task_name) + make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", project.name, task_name) self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200) - self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 200) + self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 200) - expense_claim2 = make_expense_claim(payable_account, 600, 500, company_name, "Travel Expenses - _TC4","_Test Project 1", task_name) + expense_claim2 = make_expense_claim(payable_account, 600, 500, company_name, "Travel Expenses - _TC4", project.name, task_name) self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 700) - self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 700) + self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 700) expense_claim2.cancel() self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200) - self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 200) + self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 200) def test_expense_claim_status(self): payable_account = get_payable_account(company_name) diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.json b/erpnext/hr/doctype/job_applicant/job_applicant.json index c13548ab82..1360fd1890 100644 --- a/erpnext/hr/doctype/job_applicant/job_applicant.json +++ b/erpnext/hr/doctype/job_applicant/job_applicant.json @@ -11,15 +11,24 @@ "field_order": [ "applicant_name", "email_id", + "phone_number", + "country", "status", "column_break_3", "job_title", "source", "source_name", + "applicant_rating", "section_break_6", "notes", "cover_letter", - "resume_attachment" + "resume_attachment", + "resume_link", + "section_break_16", + "currency", + "column_break_18", + "lower_range", + "upper_range" ], "fields": [ { @@ -91,12 +100,65 @@ "fieldtype": "Data", "label": "Notes", "read_only": 1 + }, + { + "fieldname": "phone_number", + "fieldtype": "Data", + "label": "Phone Number", + "options": "Phone" + }, + { + "fieldname": "country", + "fieldtype": "Link", + "label": "Country", + "options": "Country" + }, + { + "fieldname": "resume_link", + "fieldtype": "Data", + "label": "Resume Link" + }, + { + "fieldname": "applicant_rating", + "fieldtype": "Rating", + "in_list_view": 1, + "label": "Applicant Rating" + }, + { + "fieldname": "section_break_16", + "fieldtype": "Section Break", + "label": "Salary Expectation" + }, + { + "fieldname": "lower_range", + "fieldtype": "Currency", + "label": "Lower Range", + "options": "currency", + "precision": "0" + }, + { + "fieldname": "upper_range", + "fieldtype": "Currency", + "label": "Upper Range", + "options": "currency", + "precision": "0" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency" } ], "icon": "fa fa-user", "idx": 1, + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-01-13 16:19:39.113330", + "modified": "2020-09-18 12:39:02.557563", "modified_by": "Administrator", "module": "HR", "name": "Job Applicant", diff --git a/erpnext/hr/doctype/job_opening/job_opening.json b/erpnext/hr/doctype/job_opening/job_opening.json index 4437e02fc8..b8f6df6f7a 100644 --- a/erpnext/hr/doctype/job_opening/job_opening.json +++ b/erpnext/hr/doctype/job_opening/job_opening.json @@ -1,456 +1,188 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:route", - "beta": 0, - "creation": "2013-01-15 16:13:36", - "custom": 0, - "description": "Description of a Job Opening", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 0, - "engine": "InnoDB", + "actions": [], + "autoname": "field:route", + "creation": "2013-01-15 16:13:36", + "description": "Description of a Job Opening", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "job_title", + "company", + "status", + "column_break_5", + "designation", + "department", + "staffing_plan", + "planned_vacancies", + "section_break_6", + "publish", + "route", + "column_break_12", + "job_application_route", + "section_break_14", + "description", + "section_break_16", + "currency", + "lower_range", + "upper_range", + "column_break_20", + "publish_salary_range" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "job_title", - "fieldtype": "Data", - "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": "Job Title", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "job_title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Job Title", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 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": 0, - "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", + "label": "Company", + "options": "Company", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "status", - "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": 1, - "label": "Status", - "length": 0, - "no_copy": 0, - "options": "Open\nClosed", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "Open\nClosed" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_5", - "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_5", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "designation", - "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": "Designation", - "length": 0, - "no_copy": 0, - "options": "Designation", - "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": "designation", + "fieldtype": "Link", + "label": "Designation", + "options": "Designation", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "department", - "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": "Department", - "length": 0, - "no_copy": 0, - "options": "Department", - "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": "department", + "fieldtype": "Link", + "label": "Department", + "options": "Department" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "staffing_plan", - "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": "Staffing Plan", - "length": 0, - "no_copy": 0, - "options": "Staffing Plan", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "staffing_plan", + "fieldtype": "Link", + "label": "Staffing Plan", + "options": "Staffing Plan", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "staffing_plan", - "fieldname": "planned_vacancies", - "fieldtype": "Int", - "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": "Planned number of Positions", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "staffing_plan", + "fieldname": "planned_vacancies", + "fieldtype": "Int", + "label": "Planned number of Positions", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_6", - "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, - "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": "section_break_6", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "publish", - "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": "Publish on website", - "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 - }, + "default": "0", + "fieldname": "publish", + "fieldtype": "Check", + "label": "Publish on website" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "publish", - "fieldname": "route", - "fieldtype": "Data", - "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": "Route", - "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, + "depends_on": "publish", + "fieldname": "route", + "fieldtype": "Data", + "label": "Route", "unique": 1 - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Job profile, qualifications required etc.", - "fieldname": "description", - "fieldtype": "Text Editor", - "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": "Description", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "description": "Job profile, qualifications required etc.", + "fieldname": "description", + "fieldtype": "Text Editor", + "in_list_view": 1, + "label": "Description" + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_14", + "fieldtype": "Section Break" + }, + { + "collapsible": 1, + "fieldname": "section_break_16", + "fieldtype": "Section Break" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency" + }, + { + "fieldname": "lower_range", + "fieldtype": "Currency", + "label": "Lower Range", + "options": "currency", + "precision": "0" + }, + { + "fieldname": "upper_range", + "fieldtype": "Currency", + "label": "Upper Range", + "options": "currency", + "precision": "0" + }, + { + "fieldname": "column_break_20", + "fieldtype": "Column Break" + }, + { + "depends_on": "publish", + "description": "Route to the custom Job Application Webform", + "fieldname": "job_application_route", + "fieldtype": "Data", + "label": "Job Application Route" + }, + { + "default": "0", + "fieldname": "publish_salary_range", + "fieldtype": "Check", + "label": "Publish Salary Range" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-bookmark", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-05-20 15:38:44.705823", - "modified_by": "Administrator", - "module": "HR", - "name": "Job Opening", - "owner": "Administrator", + ], + "icon": "fa fa-bookmark", + "idx": 1, + "links": [], + "modified": "2020-09-18 11:23:29.488923", + "modified_by": "Administrator", + "module": "HR", + "name": "Job Opening", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Guest", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "read": 1, + "role": "Guest" } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_order": "ASC", - "track_changes": 0, - "track_seen": 0 + ], + "sort_field": "modified", + "sort_order": "ASC" } \ No newline at end of file diff --git a/erpnext/hr/doctype/job_opening/job_opening.py b/erpnext/hr/doctype/job_opening/job_opening.py index 00883d75f1..1e89767177 100644 --- a/erpnext/hr/doctype/job_opening/job_opening.py +++ b/erpnext/hr/doctype/job_opening/job_opening.py @@ -43,9 +43,8 @@ class JobOpening(WebsiteGenerator): current_count = designation_counts['employee_count'] + designation_counts['job_openings'] if self.planned_vacancies <= current_count: - frappe.throw(_("Job Openings for designation {0} already open \ - or hiring completed as per Staffing Plan {1}" - .format(self.designation, self.staffing_plan))) + frappe.throw(_("Job Openings for designation {0} already open or hiring completed as per Staffing Plan {1}").format( + self.designation, self.staffing_plan)) def get_context(self, context): context.parents = [{'route': 'jobs', 'title': _('All Jobs') }] @@ -56,7 +55,8 @@ def get_list_context(context): context.get_list = get_job_openings def get_job_openings(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by=None): - fields = ['name', 'status', 'job_title', 'description'] + fields = ['name', 'status', 'job_title', 'description', 'publish_salary_range', + 'lower_range', 'upper_range', 'currency', 'job_application_route'] filters = filters or {} filters.update({ diff --git a/erpnext/hr/doctype/job_opening/templates/job_opening_row.html b/erpnext/hr/doctype/job_opening/templates/job_opening_row.html index 5da8cc82a2..c015101600 100644 --- a/erpnext/hr/doctype/job_opening/templates/job_opening_row.html +++ b/erpnext/hr/doctype/job_opening/templates/job_opening_row.html @@ -1,9 +1,18 @@

{{ doc.job_title }}

{{ doc.description }}

+ {%- if doc.publish_salary_range -%} +

{{_("Salary range per month")}}: {{ frappe.format_value(frappe.utils.flt(doc.lower_range), currency=doc.currency) }} - {{ frappe.format_value(frappe.utils.flt(doc.upper_range), currency=doc.currency) }}

+ {% endif %}
- + {%- if doc.job_application_route -%} + {{ _("Apply Now") }} + {% else %} + + {{ _("Apply Now") }} + {% endif %}
diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py index 2c385e80f4..ab65260c09 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py @@ -88,7 +88,7 @@ def get_events(start, end, filters=None): def add_assignments(events, start, end, conditions=None): query = """select name, start_date, end_date, employee_name, - employee, docstatus + employee, docstatus, shift_type from `tabShift Assignment` where start_date >= %(start_date)s or end_date <= %(end_date)s @@ -97,18 +97,40 @@ def add_assignments(events, start, end, conditions=None): if conditions: query += conditions - for d in frappe.db.sql(query, {"start_date":start, "end_date":end}, as_dict=True): - e = { - "name": d.name, - "doctype": "Shift Assignment", - "start_date": d.start_date, - "end_date": d.end_date if d.end_date else nowdate(), - "title": cstr(d.employee_name) + ": "+ \ - cstr(d.shift_type), - "docstatus": d.docstatus - } - if e not in events: - events.append(e) + records = frappe.db.sql(query, {"start_date":start, "end_date":end}, as_dict=True) + shift_timing_map = get_shift_type_timing([d.shift_type for d in records]) + + for d in records: + daily_event_start = d.start_date + daily_event_end = d.end_date if d.end_date else getdate() + delta = timedelta(days=1) + while daily_event_start <= daily_event_end: + start_timing = frappe.utils.get_datetime(daily_event_start)+ shift_timing_map[d.shift_type]['start_time'] + end_timing = frappe.utils.get_datetime(daily_event_start)+ shift_timing_map[d.shift_type]['end_time'] + daily_event_start += delta + e = { + "name": d.name, + "doctype": "Shift Assignment", + "start_date": start_timing, + "end_date": end_timing, + "title": cstr(d.employee_name) + ": "+ \ + cstr(d.shift_type), + "docstatus": d.docstatus, + "allDay": 0 + } + if e not in events: + events.append(e) + + return events + +def get_shift_type_timing(shift_types): + shift_timing_map = {} + data = frappe.get_all("Shift Type", filters = {"name": ("IN", shift_types)}, fields = ['name', 'start_time', 'end_time']) + + for d in data: + shift_timing_map[d.name] = d + + return shift_timing_map def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=False, next_shift_direction=None): diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js b/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js index 17a986deb2..bb692e1402 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js @@ -6,14 +6,8 @@ frappe.views.calendar["Shift Assignment"] = { "start": "start_date", "end": "end_date", "id": "name", - "docstatus": 1 - }, - options: { - header: { - left: 'prev,next today', - center: 'title', - right: 'month' - } + "docstatus": 1, + "allDay": "allDay", }, get_events_method: "erpnext.hr.doctype.shift_assignment.shift_assignment.get_events" } \ No newline at end of file diff --git a/erpnext/hr/web_form/job_application/job_application.json b/erpnext/hr/web_form/job_application/job_application.json index f630570c4c..512ba5c555 100644 --- a/erpnext/hr/web_form/job_application/job_application.json +++ b/erpnext/hr/web_form/job_application/job_application.json @@ -1,86 +1,200 @@ { - "accept_payment": 0, - "allow_comments": 1, - "allow_delete": 0, - "allow_edit": 1, - "allow_incomplete": 0, - "allow_multiple": 1, - "allow_print": 0, - "amount": 0.0, - "amount_based_on_field": 0, - "creation": "2016-09-10 02:53:16.598314", - "doc_type": "Job Applicant", - "docstatus": 0, - "doctype": "Web Form", - "idx": 0, - "introduction_text": "", - "is_standard": 1, - "login_required": 0, - "max_attachment_size": 0, - "modified": "2016-12-20 00:21:44.081622", - "modified_by": "Administrator", - "module": "HR", - "name": "job-application", - "owner": "Administrator", - "published": 1, - "route": "job_application", - "show_sidebar": 1, - "sidebar_items": [], - "success_message": "Thank you for applying.", - "success_url": "/jobs", - "title": "Job Application", + "accept_payment": 0, + "allow_comments": 1, + "allow_delete": 0, + "allow_edit": 1, + "allow_incomplete": 0, + "allow_multiple": 1, + "allow_print": 0, + "amount": 0.0, + "amount_based_on_field": 0, + "apply_document_permissions": 0, + "client_script": "frappe.web_form.on('resume_link', (field, value) => {\n if (!frappe.utils.is_url(value)) {\n frappe.msgprint(__('Resume link not valid'));\n }\n});\n", + "creation": "2016-09-10 02:53:16.598314", + "doc_type": "Job Applicant", + "docstatus": 0, + "doctype": "Web Form", + "idx": 0, + "introduction_text": "", + "is_standard": 1, + "login_required": 0, + "max_attachment_size": 0, + "modified": "2020-10-07 19:27:17.143355", + "modified_by": "Administrator", + "module": "HR", + "name": "job-application", + "owner": "Administrator", + "published": 1, + "route": "job_application", + "route_to_success_link": 0, + "show_attachments": 0, + "show_in_grid": 0, + "show_sidebar": 1, + "sidebar_items": [], + "success_message": "Thank you for applying.", + "success_url": "/jobs", + "title": "Job Application", "web_form_fields": [ { - "fieldname": "job_title", - "fieldtype": "Data", - "hidden": 0, - "label": "Job Opening", - "max_length": 0, - "max_value": 0, - "options": "", - "read_only": 1, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "job_title", + "fieldtype": "Data", + "hidden": 0, + "label": "Job Opening", + "max_length": 0, + "max_value": 0, + "options": "", + "read_only": 1, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "applicant_name", - "fieldtype": "Data", - "hidden": 0, - "label": "Applicant Name", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 1 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "applicant_name", + "fieldtype": "Data", + "hidden": 0, + "label": "Applicant Name", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 1, + "show_in_filter": 0 + }, { - "fieldname": "email_id", - "fieldtype": "Data", - "hidden": 0, - "label": "Email Address", - "max_length": 0, - "max_value": 0, - "options": "Email", - "read_only": 0, - "reqd": 1 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "email_id", + "fieldtype": "Data", + "hidden": 0, + "label": "Email Address", + "max_length": 0, + "max_value": 0, + "options": "Email", + "read_only": 0, + "reqd": 1, + "show_in_filter": 0 + }, { - "fieldname": "cover_letter", - "fieldtype": "Text", - "hidden": 0, - "label": "Cover Letter", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "phone_number", + "fieldtype": "Data", + "hidden": 0, + "label": "Phone Number", + "max_length": 0, + "max_value": 0, + "options": "Phone", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "resume_attachment", - "fieldtype": "Attach", - "hidden": 0, - "label": "Resume Attachment", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 + "allow_read_on_all_link_options": 0, + "fieldname": "country", + "fieldtype": "Link", + "hidden": 0, + "label": "Country of Residence", + "max_length": 0, + "max_value": 0, + "options": "Country", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "cover_letter", + "fieldtype": "Text", + "hidden": 0, + "label": "Cover Letter", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "resume_link", + "fieldtype": "Data", + "hidden": 0, + "label": "Resume Link", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "", + "fieldtype": "Section Break", + "hidden": 0, + "label": "Expected Salary Range per month", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 1, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "currency", + "fieldtype": "Link", + "hidden": 0, + "label": "Currency", + "max_length": 0, + "max_value": 0, + "options": "Currency", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "", + "fieldtype": "Column Break", + "hidden": 0, + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "lower_range", + "fieldtype": "Currency", + "hidden": 0, + "label": "Lower Range", + "max_length": 0, + "max_value": 0, + "options": "currency", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "", + "fieldtype": "Column Break", + "hidden": 0, + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "upper_range", + "fieldtype": "Currency", + "hidden": 0, + "label": "Upper Range", + "max_length": 0, + "max_value": 0, + "options": "currency", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 } ] } \ No newline at end of file diff --git a/erpnext/loan_management/desk_page/loan/loan.json b/erpnext/loan_management/desk_page/loan/loan.json index fc59c19325..75036bd097 100644 --- a/erpnext/loan_management/desk_page/loan/loan.json +++ b/erpnext/loan_management/desk_page/loan/loan.json @@ -23,7 +23,7 @@ { "hidden": 0, "label": "Reports", - "links": "[\n {\n \"doctype\": \"Loan Repayment\",\n \"is_query_report\": true,\n \"label\": \"Loan Repayment and Closure\",\n \"name\": \"Loan Repayment and Closure\",\n \"route\": \"#query-report/Loan Repayment and Closure\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Security Pledge\",\n \"is_query_report\": true,\n \"label\": \"Loan Security Status\",\n \"name\": \"Loan Security Status\",\n \"route\": \"#query-report/Loan Security Status\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"doctype\": \"Loan Repayment\",\n \"is_query_report\": true,\n \"label\": \"Loan Repayment and Closure\",\n \"name\": \"Loan Repayment and Closure\",\n \"route\": \"#query-report/Loan Repayment and Closure\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Security Pledge\",\n \"is_query_report\": true,\n \"label\": \"Loan Security Status\",\n \"name\": \"Loan Security Status\",\n \"route\": \"#query-report/Loan Security Status\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Interest Accrual\",\n \"is_query_report\": true,\n \"label\": \"Loan Interest Report\",\n \"name\": \"Loan Interest Report\",\n \"route\": \"#query-report/Loan Interest Report\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Security\",\n \"is_query_report\": true,\n \"label\": \"Loan Security Exposure\",\n \"name\": \"Loan Security Exposure\",\n \"route\": \"#query-report/Loan Security Exposure\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Security\",\n \"is_query_report\": true,\n \"label\": \"Applicant-Wise Loan Security Exposure\",\n \"name\": \"Applicant-Wise Loan Security Exposure\",\n \"route\": \"#query-report/Applicant-Wise Loan Security Exposure\",\n \"type\": \"report\"\n }\n]" } ], "category": "Modules", @@ -38,7 +38,7 @@ "idx": 0, "is_standard": 1, "label": "Loan", - "modified": "2020-10-17 12:59:50.336085", + "modified": "2021-01-17 07:21:22.092184", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index 2e0a4d13ab..e607d4f3cb 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -202,7 +202,9 @@ def request_loan_closure(loan, posting_date=None): # checking greater than 0 as there may be some minor precision error if pending_amount < write_off_limit: - # update status as loan closure requested + # Auto create loan write off and update status as loan closure requested + write_off = make_loan_write_off(loan) + write_off.submit() frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested') else: frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount)) @@ -336,13 +338,13 @@ def create_loan_security_unpledge(unpledge_map, loan, company, applicant_type, a return unpledge_request def validate_employee_currency_with_company_currency(applicant, company): - from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_employee_currency - if not applicant: - frappe.throw(_("Please select Applicant")) - if not company: - frappe.throw(_("Please select Company")) - employee_currency = get_employee_currency(applicant) - company_currency = erpnext.get_company_currency(company) - if employee_currency != company_currency: - frappe.throw(_("Loan cannot be repayed from salary for Employee {0} because salary is processed in currency {1}") - .format(applicant, employee_currency)) + from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_employee_currency + if not applicant: + frappe.throw(_("Please select Applicant")) + if not company: + frappe.throw(_("Please select Company")) + employee_currency = get_employee_currency(applicant) + company_currency = erpnext.get_company_currency(company) + if employee_currency != company_currency: + frappe.throw(_("Loan cannot be repayed from salary for Employee {0} because salary is processed in currency {1}") + .format(applicant, employee_currency)) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 2abd7d84d9..f3c9db6233 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -321,7 +321,7 @@ class TestLoan(unittest.TestCase): self.assertEquals(sum(pledged_qty.values()), 0) amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 5)) - self.assertTrue(amounts['pending_principal_amount'] < 0) + self.assertEqual(amounts['pending_principal_amount'], 0) self.assertEquals(amounts['payable_principal_amount'], 0.0) self.assertEqual(amounts['interest_amount'], 0) @@ -473,7 +473,7 @@ class TestLoan(unittest.TestCase): self.assertEquals(loan.status, "Loan Closure Requested") amounts = calculate_amounts(loan.name, add_days(last_date, 5)) - self.assertTrue(amounts['pending_principal_amount'] < 0.0) + self.assertEqual(amounts['pending_principal_amount'], 0.0) def test_partial_unaccrued_interest_payment(self): pledge = [{ diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json index f157f0df8f..185bf7a666 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json @@ -22,6 +22,7 @@ "paid_principal_amount", "column_break_14", "interest_amount", + "total_pending_interest_amount", "paid_interest_amount", "penalty_amount", "section_break_15", @@ -172,13 +173,19 @@ "hidden": 1, "label": "Last Accrual Date", "read_only": 1 + }, + { + "fieldname": "total_pending_interest_amount", + "fieldtype": "Currency", + "label": "Total Pending Interest Amount", + "options": "Company:company:default_currency" } ], "in_create": 1, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-11-07 05:49:25.448875", + "modified": "2021-01-10 00:15:21.544140", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Interest Accrual", diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index d17f5af490..7d7992d40a 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -100,6 +100,8 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i interest_per_day = get_per_day_interest(pending_principal_amount, loan.rate_of_interest, posting_date) payable_interest = interest_per_day * no_of_days + pending_amounts = calculate_amounts(loan.name, posting_date, payment_type='Loan Closure') + args = frappe._dict({ 'loan': loan.name, 'applicant_type': loan.applicant_type, @@ -108,7 +110,8 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i 'loan_account': loan.loan_account, 'pending_principal_amount': pending_principal_amount, 'interest_amount': payable_interest, - 'penalty_amount': calculate_amounts(loan.name, posting_date)['penalty_amount'], + 'total_pending_interest_amount': pending_amounts['interest_amount'], + 'penalty_amount': pending_amounts['penalty_amount'], 'process_loan_interest': process_loan_interest, 'posting_date': posting_date, 'accrual_type': accrual_type @@ -202,6 +205,7 @@ def make_loan_interest_accrual_entry(args): loan_interest_accrual.loan_account = args.loan_account loan_interest_accrual.pending_principal_amount = flt(args.pending_principal_amount, precision) loan_interest_accrual.interest_amount = flt(args.interest_amount, precision) + loan_interest_accrual.total_pending_interest_amount = flt(args.total_pending_interest_amount, precision) loan_interest_accrual.penalty_amount = flt(args.penalty_amount, precision) loan_interest_accrual.posting_date = args.posting_date or nowdate() loan_interest_accrual.process_loan_interest_accrual = args.process_loan_interest diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py index 46a6440553..85e008ac29 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py @@ -37,10 +37,8 @@ class TestLoanInterestAccrual(unittest.TestCase): loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge) create_pledge(loan_application) - loan = create_demand_loan(self.applicant, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())) - loan.submit() first_date = '2019-10-01' @@ -50,11 +48,46 @@ class TestLoanInterestAccrual(unittest.TestCase): accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ / (days_in_year(get_datetime(first_date).year) * 100) - make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) - process_loan_interest_accrual_for_demand_loans(posting_date=last_date) - loan_interest_accural = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name}) self.assertEquals(flt(loan_interest_accural.interest_amount, 0), flt(accrued_interest_amount, 0)) + + def test_accumulated_amounts(self): + pledge = [{ + "loan_security": "Test Security 1", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge) + create_pledge(loan_application) + loan = create_demand_loan(self.applicant, "Demand Loan", loan_application, + posting_date=get_first_day(nowdate())) + loan.submit() + + first_date = '2019-10-01' + last_date = '2019-10-30' + + no_of_days = date_diff(last_date, first_date) + 1 + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ + / (days_in_year(get_datetime(first_date).year) * 100) + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + process_loan_interest_accrual_for_demand_loans(posting_date=last_date) + loan_interest_accrual = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name}) + + self.assertEquals(flt(loan_interest_accrual.interest_amount, 0), flt(accrued_interest_amount, 0)) + + next_start_date = '2019-10-31' + next_end_date = '2019-11-29' + + no_of_days = date_diff(next_end_date, next_start_date) + 1 + process = process_loan_interest_accrual_for_demand_loans(posting_date=next_end_date) + new_accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ + / (days_in_year(get_datetime(first_date).year) * 100) + + total_pending_interest_amount = flt(accrued_interest_amount + new_accrued_interest_amount, 0) + + loan_interest_accrual = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name, + 'process_loan_interest_accrual': process}) + self.assertEquals(flt(loan_interest_accrual.total_pending_interest_amount, 0), total_pending_interest_amount) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 415ba993c7..ac30c91b67 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -377,7 +377,7 @@ def get_amounts(amounts, against_loan, posting_date): amounts["penalty_amount"] = flt(penalty_amount, precision) amounts["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision) amounts["pending_accrual_entries"] = pending_accrual_entries - amounts["unaccrued_interest"] = unaccrued_interest + amounts["unaccrued_interest"] = flt(unaccrued_interest, precision) if final_due_date: amounts["due_date"] = final_due_date diff --git a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json index a55b482bd6..b6e8763756 100644 --- a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json +++ b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json @@ -7,6 +7,7 @@ "engine": "InnoDB", "field_order": [ "loan_security", + "loan_security_name", "loan_security_type", "column_break_2", "uom", @@ -79,10 +80,18 @@ "label": "Loan Security Type", "options": "Loan Security Type", "read_only": 1 + }, + { + "fetch_from": "loan_security.loan_security_name", + "fieldname": "loan_security_name", + "fieldtype": "Data", + "label": "Loan Security Name", + "read_only": 1 } ], + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-06-11 03:41:33.900340", + "modified": "2021-01-17 07:41:49.598086", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security Price", diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json index 18a97315f0..3ef53044c2 100644 --- a/erpnext/loan_management/doctype/loan_type/loan_type.json +++ b/erpnext/loan_management/doctype/loan_type/loan_type.json @@ -144,17 +144,17 @@ }, { "allow_on_submit": 1, - "description": "Pending amount that will be automatically ignored on loan closure request ", + "description": "Loan Write Off will be automatically created on loan closure request if pending amount is below this limit", "fieldname": "write_off_amount", "fieldtype": "Currency", - "label": "Write Off Amount ", + "label": "Auto Write Off Amount ", "options": "Company:company:default_currency" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-26 07:13:55.029811", + "modified": "2021-01-17 06:51:26.082879", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Type", diff --git a/erpnext/loan_management/doctype/pledge/pledge.json b/erpnext/loan_management/doctype/pledge/pledge.json index 801e3a3117..c23479c825 100644 --- a/erpnext/loan_management/doctype/pledge/pledge.json +++ b/erpnext/loan_management/doctype/pledge/pledge.json @@ -6,6 +6,7 @@ "engine": "InnoDB", "field_order": [ "loan_security", + "loan_security_name", "loan_security_type", "loan_security_code", "uom", @@ -85,11 +86,18 @@ "label": "Post Haircut Amount", "options": "Company:company:default_currency", "read_only": 1 + }, + { + "fetch_from": "loan_security.loan_security_name", + "fieldname": "loan_security_name", + "fieldtype": "Data", + "label": "Loan Security Name", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2020-11-05 10:07:15.424937", + "modified": "2021-01-17 07:41:12.452514", "modified_by": "Administrator", "module": "Loan Management", "name": "Pledge", diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json index ffc3671132..3feb3055a6 100644 --- a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json +++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json @@ -30,7 +30,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-02-01 08:14:05.845161", + "modified": "2021-01-17 03:59:14.494557", "modified_by": "Administrator", "module": "Loan Management", "name": "Process Loan Security Shortfall", @@ -45,7 +45,9 @@ "read": 1, "report": 1, "role": "System Manager", + "select": 1, "share": 1, + "submit": 1, "write": 1 }, { @@ -57,7 +59,9 @@ "read": 1, "report": 1, "role": "Loan Manager", + "select": 1, "share": 1, + "submit": 1, "write": 1 } ], diff --git a/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json index 3e7e778a25..a0b3a79b56 100644 --- a/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json +++ b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json @@ -6,6 +6,7 @@ "engine": "InnoDB", "field_order": [ "loan_security", + "loan_security_name", "qty", "loan_security_price", "amount", @@ -56,12 +57,19 @@ "label": "Post Haircut Amount", "options": "Company:company:default_currency", "read_only": 1 + }, + { + "fetch_from": "loan_security.loan_security_name", + "fieldname": "loan_security_name", + "fieldtype": "Data", + "label": "Loan Security Name", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-11-05 10:07:37.542344", + "modified": "2021-01-17 07:29:01.671722", "modified_by": "Administrator", "module": "Loan Management", "name": "Proposed Pledge", diff --git a/erpnext/loan_management/doctype/unpledge/unpledge.json b/erpnext/loan_management/doctype/unpledge/unpledge.json index 00356685eb..0091e6c43d 100644 --- a/erpnext/loan_management/doctype/unpledge/unpledge.json +++ b/erpnext/loan_management/doctype/unpledge/unpledge.json @@ -6,6 +6,7 @@ "engine": "InnoDB", "field_order": [ "loan_security", + "loan_security_name", "loan_security_type", "loan_security_code", "haircut", @@ -61,12 +62,19 @@ "fieldtype": "Percent", "label": "Haircut", "read_only": 1 + }, + { + "fetch_from": "loan_security.loan_security_name", + "fieldname": "loan_security_name", + "fieldtype": "Data", + "label": "Loan Security Name", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-11-05 10:07:28.106961", + "modified": "2021-01-17 07:36:20.212342", "modified_by": "Administrator", "module": "Loan Management", "name": "Unpledge", diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/__init__.py b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.js b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.js new file mode 100644 index 0000000000..73d60c4045 --- /dev/null +++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.js @@ -0,0 +1,16 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Applicant-Wise Loan Security Exposure"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + } + ] +}; diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.json b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.json new file mode 100644 index 0000000000..a778cd7055 --- /dev/null +++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.json @@ -0,0 +1,29 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-01-15 23:48:38.913514", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-01-15 23:48:38.913514", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Applicant-Wise Loan Security Exposure", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Loan Security", + "report_name": "Applicant-Wise Loan Security Exposure", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Loan Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py new file mode 100644 index 0000000000..ab586bc09c --- /dev/null +++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py @@ -0,0 +1,139 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import erpnext +from frappe import _ +from frappe.utils import get_datetime, flt +from six import iteritems + +def execute(filters=None): + columns = get_columns(filters) + data = get_data(filters) + return columns, data + + +def get_columns(filters): + columns = [ + {"label": _("Applicant Type"), "fieldname": "applicant_type", "options": "DocType", "width": 100}, + {"label": _("Applicant Name"), "fieldname": "applicant_name", "fieldtype": "Dynamic Link", "options": "applicant_type", "width": 150}, + {"label": _("Loan Security"), "fieldname": "loan_security", "fieldtype": "Link", "options": "Loan Security", "width": 160}, + {"label": _("Loan Security Code"), "fieldname": "loan_security_code", "fieldtype": "Data", "width": 100}, + {"label": _("Loan Security Name"), "fieldname": "loan_security_name", "fieldtype": "Data", "width": 150}, + {"label": _("Haircut"), "fieldname": "haircut", "fieldtype": "Percent", "width": 100}, + {"label": _("Loan Security Type"), "fieldname": "loan_security_type", "fieldtype": "Link", "options": "Loan Security Type", "width": 120}, + {"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80}, + {"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "width": 100}, + {"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "currency", "width": 100}, + {"label": _("Price Valid Upto"), "fieldname": "price_valid_upto", "fieldtype": "Datetime", "width": 100}, + {"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "currency", "width": 100}, + {"label": _("% Of Applicant Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100}, + {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100}, + ] + + return columns + +def get_data(filters): + data = [] + loan_security_details = get_loan_security_details(filters) + pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty(filters, + loan_security_details) + + currency = erpnext.get_company_currency(filters.get('company')) + + for key, qty in iteritems(pledge_values): + if qty: + row = {} + current_value = flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0)) + valid_upto = loan_security_details.get(key[1], {}).get('valid_upto') + + row.update(loan_security_details.get(key[1])) + row.update({ + 'applicant_type': applicant_type_map.get(key[0]), + 'applicant_name': key[0], + 'total_qty': qty, + 'current_value': current_value, + 'price_valid_upto': valid_upto, + 'portfolio_percent': flt(current_value * 100 / total_value_map.get(key[0]), 2) if total_value_map.get(key[0]) \ + else 0.0, + 'currency': currency + }) + + data.append(row) + + return data + +def get_loan_security_details(filters): + security_detail_map = {} + loan_security_price_map = {} + lsp_validity_map = {} + + loan_security_prices = frappe.db.sql(""" + SELECT loan_security, loan_security_price, valid_upto + FROM `tabLoan Security Price` t1 + WHERE valid_from >= (SELECT MAX(valid_from) FROM `tabLoan Security Price` t2 + WHERE t1.loan_security = t2.loan_security) + """, as_dict=1) + + for security in loan_security_prices: + loan_security_price_map.setdefault(security.loan_security, security.loan_security_price) + lsp_validity_map.setdefault(security.loan_security, security.valid_upto) + + loan_security_details = frappe.get_all('Loan Security', fields=['name as loan_security', + 'loan_security_code', 'loan_security_name', 'haircut', 'loan_security_type', + 'disabled']) + + for security in loan_security_details: + security.update({ + 'latest_price': flt(loan_security_price_map.get(security.loan_security)), + 'valid_upto': lsp_validity_map.get(security.loan_security) + }) + + security_detail_map.setdefault(security.loan_security, security) + + return security_detail_map + +def get_applicant_wise_total_loan_security_qty(filters, loan_security_details): + current_pledges = {} + total_value_map = {} + applicant_type_map = {} + applicant_wise_unpledges = {} + conditions = "" + + if filters.get('company'): + conditions = "AND company = %(company)s" + + unpledges = frappe.db.sql(""" + SELECT up.applicant, u.loan_security, sum(u.qty) as qty + FROM `tabLoan Security Unpledge` up, `tabUnpledge` u + WHERE u.parent = up.name + AND up.status = 'Approved' + {conditions} + GROUP BY up.applicant, u.loan_security + """.format(conditions=conditions), filters, as_dict=1) + + for unpledge in unpledges: + applicant_wise_unpledges.setdefault((unpledge.applicant, unpledge.loan_security), unpledge.qty) + + pledges = frappe.db.sql(""" + SELECT lp.applicant_type, lp.applicant, p.loan_security, sum(p.qty) as qty + FROM `tabLoan Security Pledge` lp, `tabPledge`p + WHERE p.parent = lp.name + AND lp.status = 'Pledged' + {conditions} + GROUP BY lp.applicant, p.loan_security + """.format(conditions=conditions), filters, as_dict=1) + + for security in pledges: + current_pledges.setdefault((security.applicant, security.loan_security), security.qty) + total_value_map.setdefault(security.applicant, 0.0) + applicant_type_map.setdefault(security.applicant, security.applicant_type) + + current_pledges[(security.applicant, security.loan_security)] -= \ + applicant_wise_unpledges.get((security.applicant, security.loan_security), 0.0) + + total_value_map[security.applicant] += current_pledges.get((security.applicant, security.loan_security)) \ + * loan_security_details.get(security.loan_security, {}).get('latest_price', 0) + + return current_pledges, total_value_map, applicant_type_map \ No newline at end of file diff --git a/erpnext/loan_management/report/loan_interest_report/__init__.py b/erpnext/loan_management/report/loan_interest_report/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js new file mode 100644 index 0000000000..a227b6d797 --- /dev/null +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js @@ -0,0 +1,16 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Loan Interest Report"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + } + ] +}; diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.json b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.json new file mode 100644 index 0000000000..321d6064e3 --- /dev/null +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.json @@ -0,0 +1,29 @@ +{ + "add_total_row": 1, + "columns": [], + "creation": "2021-01-10 02:03:26.742693", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-01-10 02:03:26.742693", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Interest Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Loan Interest Accrual", + "report_name": "Loan Interest Report", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Loan Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py new file mode 100644 index 0000000000..2bfe6d3c33 --- /dev/null +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py @@ -0,0 +1,183 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import erpnext +from frappe import _ +from frappe.utils import flt, getdate, add_days +from erpnext.loan_management.report.applicant_wise_loan_security_exposure.applicant_wise_loan_security_exposure \ + import get_loan_security_details + + +def execute(filters=None): + columns = get_columns(filters) + data = get_active_loan_details(filters) + return columns, data + +def get_columns(filters): + columns = [ + {"label": _("Loan"), "fieldname": "loan", "fieldtype": "Link", "options": "Loan", "width": 160}, + {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 160}, + {"label": _("Applicant Type"), "fieldname": "applicant_type", "options": "DocType", "width": 100}, + {"label": _("Applicant Name"), "fieldname": "applicant_name", "fieldtype": "Dynamic Link", "options": "applicant_type", "width": 150}, + {"label": _("Loan Type"), "fieldname": "loan_type", "fieldtype": "Link", "options": "Loan Type", "width": 100}, + {"label": _("Sanctioned Amount"), "fieldname": "sanctioned_amount", "fieldtype": "Currency", "options": "currency", "width": 120}, + {"label": _("Disbursed Amount"), "fieldname": "disbursed_amount", "fieldtype": "Currency", "options": "currency", "width": 120}, + {"label": _("Penalty Amount"), "fieldname": "penalty", "fieldtype": "Currency", "options": "currency", "width": 120}, + {"label": _("Accrued Interest"), "fieldname": "accrued_interest", "fieldtype": "Currency", "options": "currency", "width": 120}, + {"label": _("Total Repayment"), "fieldname": "total_repayment", "fieldtype": "Currency", "options": "currency", "width": 120}, + {"label": _("Principal Outstanding"), "fieldname": "principal_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120}, + {"label": _("Interest Outstanding"), "fieldname": "interest_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120}, + {"label": _("Total Outstanding"), "fieldname": "total_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120}, + {"label": _("Undue Booked Interest"), "fieldname": "undue_interest", "fieldtype": "Currency", "options": "currency", "width": 120}, + {"label": _("Interest %"), "fieldname": "rate_of_interest", "fieldtype": "Percent", "width": 100}, + {"label": _("Penalty Interest %"), "fieldname": "penalty_interest", "fieldtype": "Percent", "width": 100}, + {"label": _("Loan To Value Ratio"), "fieldname": "loan_to_value", "fieldtype": "Percent", "width": 100}, + {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100}, + ] + + return columns + +def get_active_loan_details(filters): + + filter_obj = {"status": ("!=", "Closed")} + if filters.get('company'): + filter_obj.update({'company': filters.get('company')}) + + loan_details = frappe.get_all("Loan", + fields=["name as loan", "applicant_type", "applicant as applicant_name", "loan_type", + "disbursed_amount", "rate_of_interest", "total_payment", "total_principal_paid", + "total_interest_payable", "written_off_amount", "status"], + filters=filter_obj) + + loan_list = [d.loan for d in loan_details] + + current_pledges = get_loan_wise_pledges(filters) + loan_wise_security_value = get_loan_wise_security_value(filters, current_pledges) + + sanctioned_amount_map = get_sanctioned_amount_map() + penal_interest_rate_map = get_penal_interest_rate_map() + payments = get_payments(loan_list) + accrual_map = get_interest_accruals(loan_list) + currency = erpnext.get_company_currency(filters.get('company')) + + for loan in loan_details: + loan.update({ + "sanctioned_amount": flt(sanctioned_amount_map.get(loan.applicant_name)), + "principal_outstanding": flt(loan.total_payment) - flt(loan.total_principal_paid) \ + - flt(loan.total_interest_payable) - flt(loan.written_off_amount), + "total_repayment": flt(payments.get(loan.loan)), + "accrued_interest": flt(accrual_map.get(loan.loan, {}).get("accrued_interest")), + "interest_outstanding": flt(accrual_map.get(loan.loan, {}).get("interest_outstanding")), + "penalty": flt(accrual_map.get(loan.loan, {}).get("penalty")), + "penalty_interest": penal_interest_rate_map.get(loan.loan_type), + "undue_interest": flt(accrual_map.get(loan.loan, {}).get("undue_interest")), + "loan_to_value": 0.0, + "currency": currency + }) + + loan['total_outstanding'] = loan['principal_outstanding'] + loan['interest_outstanding'] \ + + loan['penalty'] + + if loan_wise_security_value.get(loan.loan): + loan['loan_to_value'] = (loan['principal_outstanding'] * 100) / loan_wise_security_value.get(loan.loan) + + return loan_details + +def get_sanctioned_amount_map(): + return frappe._dict(frappe.get_all("Sanctioned Loan Amount", fields=["applicant", "sanctioned_amount_limit"], + as_list=1)) + +def get_payments(loans): + return frappe._dict(frappe.get_all("Loan Repayment", fields=["against_loan", "sum(amount_paid)"], + filters={"against_loan": ("in", loans)}, group_by="against_loan", as_list=1)) + +def get_interest_accruals(loans): + accrual_map = {} + + interest_accruals = frappe.get_all("Loan Interest Accrual", + fields=["loan", "interest_amount", "posting_date", "penalty_amount", + "paid_interest_amount", "accrual_type"], filters={"loan": ("in", loans)}, order_by="posting_date desc") + + for entry in interest_accruals: + accrual_map.setdefault(entry.loan, { + "accrued_interest": 0.0, + "undue_interest": 0.0, + "interest_outstanding": 0.0, + "last_accrual_date": '', + "due_date": '' + }) + + if entry.accrual_type == 'Regular': + if not accrual_map[entry.loan]['due_date']: + accrual_map[entry.loan]['due_date'] = add_days(entry.posting_date, 1) + if not accrual_map[entry.loan]['last_accrual_date']: + accrual_map[entry.loan]['last_accrual_date'] = entry.posting_date + + due_date = accrual_map[entry.loan]['due_date'] + last_accrual_date = accrual_map[entry.loan]['last_accrual_date'] + + if due_date and getdate(entry.posting_date) < getdate(due_date): + accrual_map[entry.loan]["interest_outstanding"] += entry.interest_amount - entry.paid_interest_amount + else: + accrual_map[entry.loan]['undue_interest'] += entry.interest_amount - entry.paid_interest_amount + + accrual_map[entry.loan]["accrued_interest"] += entry.interest_amount + + if last_accrual_date and getdate(entry.posting_date) == last_accrual_date: + accrual_map[entry.loan]["penalty"] = entry.penalty_amount + + return accrual_map + +def get_penal_interest_rate_map(): + return frappe._dict(frappe.get_all("Loan Type", fields=["name", "penalty_interest_rate"], as_list=1)) + +def get_loan_wise_pledges(filters): + loan_wise_unpledges = {} + current_pledges = {} + + conditions = "" + + if filters.get('company'): + conditions = "AND company = %(company)s" + + unpledges = frappe.db.sql(""" + SELECT up.loan, u.loan_security, sum(u.qty) as qty + FROM `tabLoan Security Unpledge` up, `tabUnpledge` u + WHERE u.parent = up.name + AND up.status = 'Approved' + {conditions} + GROUP BY up.loan + """.format(conditions=conditions), filters, as_dict=1) + + for unpledge in unpledges: + loan_wise_unpledges.setdefault((unpledge.loan, unpledge.loan_security), unpledge.qty) + + pledges = frappe.db.sql(""" + SELECT lp.loan, p.loan_security, sum(p.qty) as qty + FROM `tabLoan Security Pledge` lp, `tabPledge`p + WHERE p.parent = lp.name + AND lp.status = 'Pledged' + {conditions} + GROUP BY lp.loan + """.format(conditions=conditions), filters, as_dict=1) + + for security in pledges: + current_pledges.setdefault((security.loan, security.loan_security), security.qty) + current_pledges[(security.loan, security.loan_security)] -= \ + loan_wise_unpledges.get((security.loan, security.loan_security), 0.0) + + return current_pledges + +def get_loan_wise_security_value(filters, current_pledges): + loan_security_details = get_loan_security_details(filters) + loan_wise_security_value = {} + + for key in current_pledges: + qty = current_pledges.get(key) + loan_wise_security_value.setdefault(key[0], 0.0) + loan_wise_security_value[key[0]] += \ + flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0)) + + return loan_wise_security_value \ No newline at end of file diff --git a/erpnext/loan_management/report/loan_security_exposure/__init__.py b/erpnext/loan_management/report/loan_security_exposure/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.js b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.js new file mode 100644 index 0000000000..777f29624a --- /dev/null +++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.js @@ -0,0 +1,16 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Loan Security Exposure"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + } + ] +}; diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.json b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.json new file mode 100644 index 0000000000..d4dca08212 --- /dev/null +++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.json @@ -0,0 +1,29 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-01-16 08:08:01.694583", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-01-16 08:08:01.694583", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Security Exposure", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Loan Security", + "report_name": "Loan Security Exposure", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Loan Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py new file mode 100644 index 0000000000..adc8013c68 --- /dev/null +++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py @@ -0,0 +1,84 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import erpnext +from frappe import _ +from frappe.utils import flt +from six import iteritems +from erpnext.loan_management.report.applicant_wise_loan_security_exposure.applicant_wise_loan_security_exposure \ + import get_loan_security_details, get_applicant_wise_total_loan_security_qty + +def execute(filters=None): + columns = get_columns(filters) + data = get_data(filters) + return columns, data + +def get_columns(filters): + columns = [ + {"label": _("Loan Security"), "fieldname": "loan_security", "fieldtype": "Link", "options": "Loan Security", "width": 160}, + {"label": _("Loan Security Code"), "fieldname": "loan_security_code", "fieldtype": "Data", "width": 100}, + {"label": _("Loan Security Name"), "fieldname": "loan_security_name", "fieldtype": "Data", "width": 150}, + {"label": _("Haircut"), "fieldname": "haircut", "fieldtype": "Percent", "width": 100}, + {"label": _("Loan Security Type"), "fieldname": "loan_security_type", "fieldtype": "Link", "options": "Loan Security Type", "width": 120}, + {"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80}, + {"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "width": 100}, + {"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "currency", "width": 100}, + {"label": _("Price Valid Upto"), "fieldname": "price_valid_upto", "fieldtype": "Datetime", "width": 100}, + {"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "currency", "width": 100}, + {"label": _("% Of Total Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100}, + {"label": _("Pledged Applicant Count"), "fieldname": "pledged_applicant_count", "fieldtype": "Percentage", "width": 100}, + {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100}, + ] + + return columns + +def get_data(filters): + data = [] + loan_security_details = get_loan_security_details(filters) + current_pledges, total_portfolio_value = get_company_wise_loan_security_details(filters, loan_security_details) + currency = erpnext.get_company_currency(filters.get('company')) + + for security, value in iteritems(current_pledges): + if value.get('qty'): + row = {} + current_value = flt(value.get('qty', 0) * loan_security_details.get(security, {}).get('latest_price', 0)) + valid_upto = loan_security_details.get(security, {}).get('valid_upto') + + row.update(loan_security_details.get(security)) + row.update({ + 'total_qty': value.get('qty'), + 'current_value': current_value, + 'price_valid_upto': valid_upto, + 'portfolio_percent': flt(current_value * 100 / total_portfolio_value, 2), + 'pledged_applicant_count': value.get('applicant_count'), + 'currency': currency + }) + + data.append(row) + + return data + + +def get_company_wise_loan_security_details(filters, loan_security_details): + pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty(filters, + loan_security_details) + + total_portfolio_value = 0 + security_wise_map = {} + for key, qty in iteritems(pledge_values): + security_wise_map.setdefault(key[1], { + 'qty': 0.0, + 'applicant_count': 0.0 + }) + + security_wise_map[key[1]]['qty'] += qty + if qty: + security_wise_map[key[1]]['applicant_count'] += 1 + + total_portfolio_value += flt(qty * loan_security_details.get(key[1])['latest_price']) + + return security_wise_map, total_portfolio_value + + + diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json index 992ef16d64..f190cfae75 100644 --- a/erpnext/non_profit/doctype/member/member.json +++ b/erpnext/non_profit/doctype/member/member.json @@ -12,7 +12,6 @@ "membership_expiry_date", "column_break_5", "membership_type", - "email", "email_id", "image", "customer_section", @@ -64,13 +63,6 @@ "options": "Membership Type", "reqd": 1 }, - { - "fieldname": "email", - "fieldtype": "Link", - "in_list_view": 1, - "label": "User", - "options": "User" - }, { "fieldname": "image", "fieldtype": "Attach Image", @@ -178,7 +170,7 @@ ], "image_field": "image", "links": [], - "modified": "2020-09-16 23:44:13.596948", + "modified": "2020-11-09 12:12:10.174647", "modified_by": "Administrator", "module": "Non Profit", "name": "Member", diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index 25d6b53830..04b99f93f2 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -18,8 +18,6 @@ class Member(Document): def validate(self): - if self.email: - self.validate_email_type(self.email) if self.email_id: self.validate_email_type(self.email_id) @@ -57,14 +55,16 @@ class Member(Document): def make_customer_and_link(self): if self.customer: frappe.msgprint(_("A customer is already linked to this Member")) - cust = create_customer(frappe._dict({ + + customer = create_customer(frappe._dict({ 'fullname': self.member_name, - 'email': self.email_id or self.email, + 'email': self.email_id, 'phone': None })) - self.customer = cust + self.customer = customer self.save() + frappe.msgprint(_("Customer {0} has been created succesfully.").format(self.customer)) def get_or_create_member(user_details): diff --git a/erpnext/non_profit/doctype/membership/membership.js b/erpnext/non_profit/doctype/membership/membership.js index ee8a8c0a7b..573ac3319a 100644 --- a/erpnext/non_profit/doctype/membership/membership.js +++ b/erpnext/non_profit/doctype/membership/membership.js @@ -4,16 +4,25 @@ frappe.ui.form.on('Membership', { setup: function(frm) { frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => { - if (val) frm.set_df_property('razorpay_details_section', 'hidden', false); + if (val) frm.set_df_property("razorpay_details_section", "hidden", false); }) }, refresh: function(frm) { + if (frm.doc.__islocal) + return; + !frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => { - frm.call("generate_invoice", { - save: true - }).then(() => { - frm.reload_doc(); + frm.call({ + doc: frm.doc, + method: "generate_invoice", + args: {save: true}, + freeze: true, + freeze_message: __("Creating Membership Invoice"), + callback: function(r) { + if (r.invoice) + frm.reload_doc(); + } }); }); @@ -27,6 +36,6 @@ frappe.ui.form.on('Membership', { }, onload: function(frm) { - frm.add_fetch('membership_type', 'amount', 'amount'); + frm.add_fetch("membership_type", "amount", "amount"); } }); diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json index 7f218966a0..6da053f9fc 100644 --- a/erpnext/non_profit/doctype/membership/membership.json +++ b/erpnext/non_profit/doctype/membership/membership.json @@ -7,6 +7,7 @@ "engine": "InnoDB", "field_order": [ "member", + "member_name", "membership_type", "column_break_3", "membership_status", @@ -46,6 +47,8 @@ { "fieldname": "membership_status", "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, "label": "Membership Status", "options": "New\nCurrent\nExpired\nPending\nCancelled" }, @@ -122,11 +125,18 @@ "fieldtype": "Link", "label": "Invoice", "options": "Sales Invoice" + }, + { + "fetch_from": "member.member_name", + "fieldname": "member_name", + "fieldtype": "Data", + "label": "Member Name", + "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-09-19 14:28:11.532696", + "modified": "2021-01-21 16:31:20.032656", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership", @@ -158,7 +168,9 @@ } ], "restrict_to_domain": "Non Profit", + "search_fields": "member, member_name", "sort_field": "modified", "sort_order": "DESC", + "title_field": "member_name", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 7d15abaa3b..c113b80d56 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -14,33 +14,43 @@ from erpnext.non_profit.doctype.member.member import create_member from frappe import _ import erpnext - class Membership(Document): def validate(self): if not self.member or not frappe.db.exists("Member", self.member): - member_name = frappe.get_value('Member', dict(email=frappe.session.user)) + # for web forms + user_type = frappe.db.get_value("User", frappe.session.user, "user_type") + if user_type == "Website User": + self.create_member_from_website_user() + else: + frappe.throw(_("Please select a Member")) - if not member_name: - user = frappe.get_doc('User', frappe.session.user) - member = frappe.get_doc(dict( - doctype='Member', - email=frappe.session.user, - membership_type=self.membership_type, - member_name=user.get_fullname() - )).insert(ignore_permissions=True) - member_name = member.name + self.validate_membership_period() - if self.get("__islocal"): - self.member = member_name + def create_member_from_website_user(self): + member_name = frappe.get_value("Member", dict(email_id=frappe.session.user)) + if not member_name: + user = frappe.get_doc("User", frappe.session.user) + member = frappe.get_doc(dict( + doctype="Member", + email_id=frappe.session.user, + membership_type=self.membership_type, + member_name=user.get_fullname() + )).insert(ignore_permissions=True) + member_name = member.name + + if self.get("__islocal"): + self.member = member_name + + def validate_membership_period(self): # get last membership (if active) - last_membership = erpnext.get_last_membership() + last_membership = erpnext.get_last_membership(self.member) # if person applied for offline membership if last_membership and not frappe.session.user == "Administrator": # if last membership does not expire in 30 days, then do not allow to renew if getdate(add_days(last_membership.to_date, -30)) > getdate(nowdate()) : - frappe.throw(_('You can only renew if your membership expires within 30 days')) + frappe.throw(_("You can only renew if your membership expires within 30 days")) self.from_date = add_days(last_membership.to_date, 1) elif frappe.session.user == "Administrator": @@ -54,11 +64,16 @@ class Membership(Document): self.to_date = add_months(self.from_date, 1) def on_payment_authorized(self, status_changed_to=None): - if status_changed_to in ("Completed", "Authorized"): - self.load_from_db() - self.db_set('paid', 1) + if status_changed_to not in ("Completed", "Authorized"): + return + self.load_from_db() + self.db_set("paid", 1) + settings = frappe.get_doc("Membership Settings") + if settings.enable_invoicing and settings.create_for_web_forms: + self.generate_invoice(with_payment_entry=settings.make_payment_entry, save=True) - def generate_invoice(self, save=True): + + def generate_invoice(self, save=True, with_payment_entry=False): if not (self.paid or self.currency or self.amount): frappe.throw(_("The payment for this membership is not paid. To generate invoice fill the payment details")) @@ -66,34 +81,64 @@ class Membership(Document): frappe.throw(_("An invoice is already linked to this document")) member = frappe.get_doc("Member", self.member) - plan = frappe.get_doc("Membership Type", self.membership_type) - settings = frappe.get_doc("Membership Settings") - if not member.customer: frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member))) - if not settings.debit_account: - frappe.throw(_("You need to set Debit Account in Membership Settings")) - - if not settings.company: - frappe.throw(_("You need to set Default Company for invoicing in Membership Settings")) + plan = frappe.get_doc("Membership Type", self.membership_type) + settings = frappe.get_doc("Membership Settings") + self.validate_membership_type_and_settings(plan, settings) invoice = make_invoice(self, member, plan, settings) self.invoice = invoice.name + if with_payment_entry: + self.make_payment_entry(settings, invoice) + if save: self.save() return invoice + def validate_membership_type_and_settings(self, plan, settings): + settings_link = get_link_to_form("Membership Type", self.membership_type) + + if not settings.debit_account: + frappe.throw(_("You need to set Debit Account in {0}").format(settings_link)) + + if not settings.company: + frappe.throw(_("You need to set Default Company for invoicing in {0}").format(settings_link)) + + if not plan.linked_item: + frappe.throw(_("Please set a Linked Item for the Membership Type {0}").format( + get_link_to_form("Membership Type", self.membership_type))) + + def make_payment_entry(self, settings, invoice): + if not settings.payment_account: + frappe.throw(_("You need to set Payment Account in {0}").format( + get_link_to_form("Membership Type", self.membership_type))) + + from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + frappe.flags.ignore_account_permission = True + pe = get_payment_entry(dt="Sales Invoice", dn=invoice.name, bank_amount=invoice.grand_total) + frappe.flags.ignore_account_permission=False + pe.paid_to = settings.payment_account + pe.reference_no = self.name + pe.reference_date = getdate() + pe.save(ignore_permissions=True) + pe.submit() + def send_acknowlement(self): settings = frappe.get_doc("Membership Settings") if not settings.send_email: - frappe.throw(_("You need to enable Send Acknowledge Email in Membership Settings")) + frappe.throw(_("You need to enable Send Acknowledge Email in {0}").format( + get_link_to_form("Membership Settings", "Membership Settings"))) member = frappe.get_doc("Member", self.member) + if not member.email_id: + frappe.throw(_("Email address of member {0} is missing").format(frappe.utils.get_link_to_form("Member", self.member))) + plan = frappe.get_doc("Membership Type", self.membership_type) - email = member.email_id if member.email_id else member.email + email = member.email_id attachments = [frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format)] if self.invoice and settings.send_invoice: @@ -112,48 +157,56 @@ class Membership(Document): } if not frappe.flags.in_test: - frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args) + frappe.enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args) else: frappe.sendmail(**email_args) def generate_and_send_invoice(self): - invoice = self.generate_invoice(False) + self.generate_invoice(save=False) self.send_acknowlement() + def make_invoice(membership, member, plan, settings): invoice = frappe.get_doc({ - 'doctype': 'Sales Invoice', - 'customer': member.customer, - 'debit_to': settings.debit_account, - 'currency': membership.currency, - 'is_pos': 0, - 'items': [ + "doctype": "Sales Invoice", + "customer": member.customer, + "debit_to": settings.debit_account, + "currency": membership.currency, + "company": settings.company, + "is_pos": 0, + "items": [ { - 'item_code': plan.linked_item, - 'rate': membership.amount, - 'qty': 1 + "item_code": plan.linked_item, + "rate": membership.amount, + "qty": 1 } ] }) - + invoice.set_missing_values() invoice.insert(ignore_permissions=True) invoice.submit() + frappe.msgprint(_("Sales Invoice created successfully")) + return invoice + def get_member_based_on_subscription(subscription_id, email): members = frappe.get_all("Member", filters={ - 'subscription_id': subscription_id, - 'email_id': email + "subscription_id": subscription_id, + "email_id": email }, order_by="creation desc") try: - return frappe.get_doc("Member", members[0]['name']) + return frappe.get_doc("Member", members[0]["name"]) except: return None + def verify_signature(data): - signature = frappe.request.headers.get('X-Razorpay-Signature') + if frappe.flags.in_test: + return True + signature = frappe.request.headers.get("X-Razorpay-Signature") settings = frappe.get_doc("Membership Settings") key = settings.get_webhook_secret() @@ -162,6 +215,7 @@ def verify_signature(data): controller.verify_signature(data, signature, key) + @frappe.whitelist(allow_guest=True) def trigger_razorpay_subscription(*args, **kwargs): data = frappe.request.get_data(as_text=True) @@ -170,16 +224,16 @@ def trigger_razorpay_subscription(*args, **kwargs): except Exception as e: log = frappe.log_error(e, "Webhook Verification Error") notify_failure(log) - return { 'status': 'Failed', 'reason': e} + return { "status": "Failed", "reason": e} if isinstance(data, six.string_types): data = json.loads(data) data = frappe._dict(data) - subscription = data.payload.get("subscription", {}).get('entity', {}) + subscription = data.payload.get("subscription", {}).get("entity", {}) subscription = frappe._dict(subscription) - payment = data.payload.get("payment", {}).get('entity', {}) + payment = data.payload.get("payment", {}).get("entity", {}) payment = frappe._dict(payment) try: @@ -189,15 +243,15 @@ def trigger_razorpay_subscription(*args, **kwargs): member = get_member_based_on_subscription(subscription.id, payment.email) if not member: member = create_member(frappe._dict({ - 'fullname': payment.email, - 'email': payment.email, - 'plan_id': get_plan_from_razorpay_id(subscription.plan_id) + "fullname": payment.email, + "email": payment.email, + "plan_id": get_plan_from_razorpay_id(subscription.plan_id) })) member.subscription_id = subscription.id member.customer_id = payment.customer_id if subscription.notes and type(subscription.notes) == dict: - notes = '\n'.join("{}: {}".format(k, v) for k, v in subscription.notes.items()) + notes = "\n".join("{}: {}".format(k, v) for k, v in subscription.notes.items()) member.add_comment("Comment", notes) elif subscription.notes and type(subscription.notes) == str: member.add_comment("Comment", subscription.notes) @@ -227,28 +281,39 @@ def trigger_razorpay_subscription(*args, **kwargs): message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), __("Payment ID"), payment.id) log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name)) notify_failure(log) - return { 'status': 'Failed', 'reason': e} + return { "status": "Failed", "reason": e} - return { 'status': 'Success' } + return { "status": "Success" } def notify_failure(log): try: - content = """Dear System Manager, -Razorpay webhook for creating renewing membership subscription failed due to some reason. Please check the following error log linked below + content = """ + Dear System Manager, + Razorpay webhook for creating renewing membership subscription failed due to some reason. + Please check the following error log linked below + Error Log: {0} + Regards, Administrator + """.format(get_link_to_form("Error Log", log.name)) -Error Log: {0} - -Regards, -Administrator""".format(get_link_to_form("Error Log", log.name)) sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content) except: pass + def get_plan_from_razorpay_id(plan_id): - plan = frappe.get_all("Membership Type", filters={'razorpay_plan_id': plan_id}, order_by="creation desc") + plan = frappe.get_all("Membership Type", filters={"razorpay_plan_id": plan_id}, order_by="creation desc") try: - return plan[0]['name'] + return plan[0]["name"] except: return None + + +def set_expired_status(): + frappe.db.sql(""" + UPDATE + `tabMembership` SET `status` = 'Expired' + WHERE + `status` not in ('Cancelled') AND `to_date` < %s + """, (nowdate())) \ No newline at end of file diff --git a/erpnext/non_profit/doctype/membership/membership_list.js b/erpnext/non_profit/doctype/membership/membership_list.js new file mode 100644 index 0000000000..a959159899 --- /dev/null +++ b/erpnext/non_profit/doctype/membership/membership_list.js @@ -0,0 +1,15 @@ +frappe.listview_settings['Membership'] = { + get_indicator: function(doc) { + if (doc.membership_status == 'New') { + return [__('New'), 'blue', 'membership_status,=,New']; + } else if (doc.membership_status === 'Current') { + return [__('Current'), 'green', 'membership_status,=,Current']; + } else if (doc.membership_status === 'Pending') { + return [__('Pending'), 'yellow', 'membership_status,=,Pending']; + } else if (doc.membership_status === 'Expired') { + return [__('Expired'), 'grey', 'membership_status,=,Expired']; + } else { + return [__('Cancelled'), 'red', 'membership_status,=,Cancelled']; + } + } +}; diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py index b23f4062a9..ff7e6c473c 100644 --- a/erpnext/non_profit/doctype/membership/test_membership.py +++ b/erpnext/non_profit/doctype/membership/test_membership.py @@ -2,8 +2,110 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt from __future__ import unicode_literals - import unittest +import frappe +import erpnext +from erpnext.non_profit.doctype.member.member import create_member +from frappe.utils import nowdate, add_months class TestMembership(unittest.TestCase): - pass + def setUp(self): + # Get default company + company = frappe.get_doc("Company", erpnext.get_default_company()) + + # update membership settings + settings = frappe.get_doc("Membership Settings") + # Enable razorpay + settings.enable_razorpay = 1 + settings.billing_cycle = "Monthly" + settings.billing_frequency = 24 + # Enable invoicing + settings.enable_invoicing = 1 + settings.make_payment_entry = 1 + settings.company = company.name + settings.payment_account = company.default_cash_account + settings.debit_account = company.default_receivable_account + settings.save() + + # make test plan + if not frappe.db.exists("Membership Type", "_rzpy_test_milythm"): + plan = frappe.new_doc("Membership Type") + plan.membership_type = "_rzpy_test_milythm" + plan.amount = 100 + plan.razorpay_plan_id = "_rzpy_test_milythm" + plan.linked_item = create_item("_Test Item for Non Profit Membership").name + plan.insert() + else: + plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm") + + # make test member + self.member_doc = create_member(frappe._dict({ + 'fullname': "_Test_Member", + 'email': "_test_member_erpnext@example.com", + 'plan_id': plan.name + })) + self.member_doc.make_customer_and_link() + self.member = self.member_doc.name + + def test_auto_generate_invoice_and_payment_entry(self): + entry = make_membership(self.member) + + # Naive test to see if at all invoice was generated and attached to member + # In any case if details were missing, the invoicing would throw an error + invoice = entry.generate_invoice(save=True) + self.assertEqual(invoice.name, entry.invoice) + + def test_renew_within_30_days(self): + # create a membership for two months + # Should work fine + make_membership(self.member, { "from_date": nowdate() }) + make_membership(self.member, { "from_date": add_months(nowdate(), 1) }) + + from frappe.utils.user import add_role + add_role("test@example.com", "Non Profit Manager") + frappe.set_user("test@example.com") + + # create next membership with expiry not within 30 days + self.assertRaises(frappe.ValidationError, make_membership, self.member, { + "from_date": add_months(nowdate(), 2), + }) + + frappe.set_user("Administrator") + # create the same membership but as administrator + make_membership(self.member, { + "from_date": add_months(nowdate(), 2), + "to_date": add_months(nowdate(), 3), + }) + +def set_config(key, value): + frappe.db.set_value("Membership Settings", None, key, value) + +def make_membership(member, payload={}): + data = { + "doctype": "Membership", + "member": member, + "membership_status": "Current", + "membership_type": "_rzpy_test_milythm", + "currency": "INR", + "paid": 1, + "from_date": nowdate(), + "amount": 100 + } + data.update(payload) + membership = frappe.get_doc(data) + membership.insert(ignore_permissions=True, ignore_if_duplicate=True) + return membership + +def create_item(item_code): + if not frappe.db.exists("Item", item_code): + item = frappe.new_doc("Item") + item.item_code = item_code + item.item_name = item_code + item.stock_uom = "Nos" + item.description = item_code + item.item_group = "All Item Groups" + item.is_stock_item = 0 + item.save() + else: + item = frappe.get_doc("Item", item_code) + return item diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js index 1d894027b0..c95aab2a7a 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js @@ -11,7 +11,7 @@ frappe.ui.form.on("Membership Settings", { }); } - frm.set_query('inv_print_format', function(doc) { + frm.set_query("inv_print_format", function() { return { filters: { "doc_type": "Sales Invoice" @@ -19,7 +19,7 @@ frappe.ui.form.on("Membership Settings", { }; }); - frm.set_query('membership_print_format', function(doc) { + frm.set_query("membership_print_format", function() { return { filters: { "doc_type": "Membership" @@ -27,12 +27,23 @@ frappe.ui.form.on("Membership Settings", { }; }); - frm.set_query('debit_account', function(doc) { + frm.set_query("debit_account", function() { return { filters: { - 'account_type': 'Receivable', - 'is_group': 0, - 'company': frm.doc.company + "account_type": "Receivable", + "is_group": 0, + "company": frm.doc.company + } + }; + }); + + frm.set_query("payment_account", function () { + var account_types = ["Bank", "Cash"]; + return { + filters: { + "account_type": ["in", account_types], + "is_group": 0, + "company": frm.doc.company } }; }); diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 5b6bab5b0a..3887b0a2be 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -11,9 +11,12 @@ "billing_frequency", "webhook_secret", "column_break_6", - "enable_auto_invoicing", + "enable_invoicing", + "create_for_web_forms", + "make_payment_entry", "company", "debit_account", + "payment_account", "column_break_9", "send_email", "send_invoice", @@ -58,14 +61,7 @@ "label": "Invoicing" }, { - "default": "0", - "fieldname": "enable_auto_invoicing", - "fieldtype": "Check", - "label": "Enable Auto Invoicing", - "mandatory_depends_on": "eval:doc.send_invoice" - }, - { - "depends_on": "eval:doc.enable_auto_invoicing", + "depends_on": "eval:doc.enable_invoicing", "fieldname": "debit_account", "fieldtype": "Link", "label": "Debit Account", @@ -77,7 +73,7 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval:doc.enable_auto_invoicing", + "depends_on": "eval:doc.enable_invoicing", "fieldname": "company", "fieldtype": "Link", "label": "Company", @@ -86,7 +82,7 @@ }, { "default": "0", - "depends_on": "eval:doc.enable_auto_invoicing && doc.send_email", + "depends_on": "eval:doc.enable_invoicing && doc.send_email", "fieldname": "send_invoice", "fieldtype": "Check", "label": "Send Invoice with Email" @@ -119,11 +115,43 @@ "label": "Email Template", "mandatory_depends_on": "eval:doc.send_email", "options": "Email Template" + }, + { + "default": "0", + "fieldname": "enable_invoicing", + "fieldtype": "Check", + "label": "Enable Invoicing", + "mandatory_depends_on": "eval:doc.send_invoice || doc.make_payment_entry" + }, + { + "default": "0", + "depends_on": "eval:doc.enable_invoicing", + "description": "Auto creates Payment Entry for Sales Invoices created for Membership from web forms.", + "fieldname": "make_payment_entry", + "fieldtype": "Check", + "label": "Make Payment Entry" + }, + { + "depends_on": "eval:doc.make_payment_entry", + "fieldname": "payment_account", + "fieldtype": "Link", + "label": "Payment To", + "mandatory_depends_on": "eval:doc.make_payment_entry", + "options": "Account" + }, + { + "default": "0", + "depends_on": "eval:doc.enable_invoicing", + "description": "Automatically create an invoice when payment is authorized from a web form entry", + "fieldname": "create_for_web_forms", + "fieldtype": "Check", + "label": "Auto Create Invoice for Web Forms" } ], + "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-08-05 17:26:37.287395", + "modified": "2021-01-21 19:57:53.213286", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.js b/erpnext/non_profit/doctype/membership_type/membership_type.js index 43311a2c96..91a5cb74ba 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.js +++ b/erpnext/non_profit/doctype/membership_type/membership_type.js @@ -2,13 +2,21 @@ // For license information, please see license.txt frappe.ui.form.on('Membership Type', { - refresh: function(frm) { - frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => { + refresh: function (frm) { + frappe.db.get_single_value('Membership Settings', 'enable_razorpay').then(val => { if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false); }); - frappe.db.get_single_value("Membership Settings", "enable_auto_invoicing").then(val => { + frappe.db.get_single_value('Membership Settings', 'enable_invoicing').then(val => { if (val) frm.set_df_property('linked_item', 'hidden', false); }); + + frm.set_query('linked_item', () => { + return { + filters: { + is_stock_item: 0 + } + }; + }); } }); diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.py b/erpnext/non_profit/doctype/membership_type/membership_type.py index b95b04316f..022829bd3a 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.py +++ b/erpnext/non_profit/doctype/membership_type/membership_type.py @@ -5,9 +5,14 @@ from __future__ import unicode_literals from frappe.model.document import Document import frappe +from frappe import _ class MembershipType(Document): - pass + def validate(self): + if self.linked_item: + is_stock_item = frappe.db.get_value("Item", self.linked_item, "is_stock_item") + if is_stock_item: + frappe.throw(_("The Linked Item should be a service item")) def get_membership_type(razorpay_id): return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id}) \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 7b2428ebb5..f3660b3c51 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -736,8 +736,9 @@ erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02 erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.update_reason_for_resignation_in_employee -erpnext.patches.v13_0.update_custom_fields_for_shopify execute:frappe.delete_doc("Report", "Quoted Item Comparison") +erpnext.patches.v13_0.update_member_email_address +erpnext.patches.v13_0.update_custom_fields_for_shopify erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy erpnext.patches.v13_0.add_po_to_global_search @@ -745,3 +746,5 @@ erpnext.patches.v13_0.update_returned_qty_in_pr_dn erpnext.patches.v13_0.update_project_template_tasks erpnext.patches.v13_0.set_company_in_leave_ledger_entry erpnext.patches.v13_0.convert_qi_parameter_to_link_field +erpnext.patches.v13_0.setup_patient_history_settings_for_standard_doctypes +erpnext.patches.v13_0.add_naming_series_to_old_projects diff --git a/erpnext/patches/v13_0/add_naming_series_to_old_projects.py b/erpnext/patches/v13_0/add_naming_series_to_old_projects.py new file mode 100644 index 0000000000..79b67533ed --- /dev/null +++ b/erpnext/patches/v13_0/add_naming_series_to_old_projects.py @@ -0,0 +1,26 @@ +from __future__ import unicode_literals +import frappe +from frappe.custom.doctype.property_setter.property_setter import make_property_setter, delete_property_setter + +def execute(): + frappe.reload_doc("projects", "doctype", "project") + projects = frappe.db.get_all("Project", + fields=["name", "naming_series", "modified"], + filters={ + "naming_series": ["is", "not set"] + }, + order_by="timestamp(modified) asc") + + # disable set only once as the old docs must be saved + # (to bypass 'Cant change naming series' validation on save) + make_property_setter("Project", "naming_series", "set_only_once", 0, "Check") + + for entry in projects: + # need to save the doc so that users can edit old projects + doc = frappe.get_doc("Project", entry.name) + if not doc.naming_series: + doc.naming_series = "PROJ-.####" + doc.save() + + delete_property_setter("Project", "set_only_once", "naming_series") + frappe.db.commit() diff --git a/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py b/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py new file mode 100644 index 0000000000..de08aa26b3 --- /dev/null +++ b/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py @@ -0,0 +1,13 @@ +from __future__ import unicode_literals +import frappe +from erpnext.healthcare.setup import setup_patient_history_settings + +def execute(): + if "Healthcare" not in frappe.get_active_domains(): + return + + frappe.reload_doc("healthcare", "doctype", "Patient History Settings") + frappe.reload_doc("healthcare", "doctype", "Patient History Standard Document Type") + frappe.reload_doc("healthcare", "doctype", "Patient History Custom Document Type") + + setup_patient_history_settings() \ No newline at end of file diff --git a/erpnext/patches/v13_0/update_member_email_address.py b/erpnext/patches/v13_0/update_member_email_address.py new file mode 100644 index 0000000000..4056f84069 --- /dev/null +++ b/erpnext/patches/v13_0/update_member_email_address.py @@ -0,0 +1,23 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.utils.rename_field import rename_field + +def execute(): + """add value to email_id column from email""" + + if frappe.db.has_column("Member", "email"): + # Get all members + for member in frappe.db.get_all("Member", pluck="name"): + # Check if email_id already exists + if not frappe.db.get_value("Member", member, "email_id"): + # fetch email id from the user linked field email + email = frappe.db.get_value("Member", member, "email") + + # Set the value for it + frappe.db.set_value("Member", member, "email_id", email) + + if frappe.db.exists("DocType", "Membership Settings"): + rename_field("Membership Settings", "enable_auto_invoicing", "enable_invoicing") diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py index 5fa062306c..8cc27d217f 100644 --- a/erpnext/patches/v13_0/update_project_template_tasks.py +++ b/erpnext/patches/v13_0/update_project_template_tasks.py @@ -5,40 +5,43 @@ from __future__ import unicode_literals import frappe def execute(): - frappe.reload_doc("projects", "doctype", "project_template") - frappe.reload_doc("projects", "doctype", "project_template_task") - frappe.reload_doc("projects", "doctype", "project_template") - frappe.reload_doc("projects", "doctype", "task") + frappe.reload_doc("projects", "doctype", "project_template") + frappe.reload_doc("projects", "doctype", "project_template_task") + frappe.reload_doc("projects", "doctype", "task") - for template_name in frappe.db.sql(""" - select - name - from - `tabProject Template` """, - as_dict=1): - - template = frappe.get_doc("Project Template", template_name.name) - replace_tasks = False - new_tasks = [] - for task in template.tasks: - if task.subject: - replace_tasks = True - new_task = frappe.get_doc(dict( - doctype = "Task", - subject = task.subject, - start = task.start, - duration = task.duration, - task_weight = task.task_weight, - description = task.description, - is_template = 1 - )).insert() - new_tasks.append(new_task) + # Update property setter status if any + property_setter = frappe.db.get_value('Property Setter', {'doc_type': 'Task', + 'field_name': 'status', 'property': 'options'}) - if replace_tasks: - template.tasks = [] - for tsk in new_tasks: - template.append("tasks", { - "task": tsk.name, - "subject": tsk.subject - }) - template.save() \ No newline at end of file + if property_setter: + property_setter_doc = frappe.get_doc('Property Setter', {'doc_type': 'Task', + 'field_name': 'status', 'property': 'options'}) + property_setter_doc.value += "\nTemplate" + property_setter_doc.save() + + for template_name in frappe.get_all('Project Template'): + template = frappe.get_doc("Project Template", template_name.name) + replace_tasks = False + new_tasks = [] + for task in template.tasks: + if task.subject: + replace_tasks = True + new_task = frappe.get_doc(dict( + doctype = "Task", + subject = task.subject, + start = task.start, + duration = task.duration, + task_weight = task.task_weight, + description = task.description, + is_template = 1 + )).insert() + new_tasks.append(new_task) + + if replace_tasks: + template.tasks = [] + for tsk in new_tasks: + template.append("tasks", { + "task": tsk.name, + "subject": tsk.subject + }) + template.save() \ No newline at end of file diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.js b/erpnext/payroll/doctype/additional_salary/additional_salary.js index 7737e6c886..d1ed91fac7 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.js +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.js @@ -12,6 +12,8 @@ frappe.ui.form.on('Additional Salary', { } }; }); + + frm.trigger('set_earning_component'); }, employee: function(frm) { @@ -43,6 +45,19 @@ frappe.ui.form.on('Additional Salary', { }); }, + company: function(frm) { + frm.trigger('set_earning_component'); + }, + + set_earning_component: function(frm) { + if (!frm.doc.company) return; + frm.set_query("salary_component", function() { + return { + filters: {type: ["in", ["earning", "deduction"]], company: frm.doc.company} + }; + }); + }, + get_employee_currency: function(frm) { frappe.call({ method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency", diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.js b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js index 182ce0f83a..b2809b164a 100644 --- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.js +++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js @@ -10,15 +10,7 @@ frappe.ui.form.on('Employee Incentive', { } }; }); - - if (!frm.doc.company) return; - frm.set_query("salary_component", function() { - return { - query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", - filters: {type: "earning", company: frm.doc.company} - }; - }); - + frm.trigger('set_earning_component'); }, employee: function(frm) { @@ -45,11 +37,21 @@ frappe.ui.form.on('Employee Incentive', { callback: function(data) { if (data.message) { frm.set_value("company", data.message.company); + frm.trigger('set_earning_component'); } } }); }, + set_earning_component: function(frm) { + if (!frm.doc.company) return; + frm.set_query("salary_component", function() { + return { + filters: {type: "earning", company: frm.doc.company} + }; + }); + }, + get_employee_currency: function(frm) { frappe.call({ method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency", diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index 61c593d197..45f9aa170f 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -3,6 +3,8 @@ var in_progress = false; +frappe.provide("erpnext.accounts.dimensions"); + frappe.ui.form.on('Payroll Entry', { onload: function (frm) { if (!frm.doc.posting_date) { @@ -10,6 +12,7 @@ frappe.ui.form.on('Payroll Entry', { } frm.toggle_reqd(['payroll_frequency'], !frm.doc.salary_slip_based_on_timesheet); + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); frm.events.department_filters(frm); frm.events.payroll_payable_account_filters(frm); }, @@ -129,21 +132,6 @@ frappe.ui.form.on('Payroll Entry', { "company": frm.doc.company } }; - }), - frm.set_query("cost_center", function () { - return { - filters: { - "is_group": 0, - company: frm.doc.company - } - }; - }), - frm.set_query("project", function () { - return { - filters: { - company: frm.doc.company - } - }; }); }, @@ -183,6 +171,7 @@ frappe.ui.form.on('Payroll Entry', { company: function (frm) { frm.events.clear_employee_table(frm); + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, currency: function (frm) { diff --git a/erpnext/payroll/doctype/salary_detail/salary_detail.json b/erpnext/payroll/doctype/salary_detail/salary_detail.json index 5c1eb61281..393f647cc8 100644 --- a/erpnext/payroll/doctype/salary_detail/salary_detail.json +++ b/erpnext/payroll/doctype/salary_detail/salary_detail.json @@ -9,6 +9,7 @@ "abbr", "column_break_3", "amount", + "year_to_date", "section_break_5", "additional_salary", "statistical_component", @@ -226,11 +227,19 @@ { "fieldname": "column_break_24", "fieldtype": "Column Break" + }, + { + "description": "Total salary booked against this component for this employee from the beginning of the year (payroll period or fiscal year) up to the current salary slip's end date.", + "fieldname": "year_to_date", + "fieldtype": "Currency", + "label": "Year To Date", + "options": "currency", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2020-11-25 13:12:41.081106", + "modified": "2021-01-14 13:39:15.847158", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Detail", diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js index 51fb3596e9..b50c774fbe 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.js +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js @@ -138,11 +138,11 @@ frappe.ui.form.on("Salary Slip", { }, change_grid_labels: function(frm) { - frm.set_currency_labels(["amount", "default_amount", "additional_amount", "tax_on_flexible_benefit", - "tax_on_additional_salary"], frm.doc.currency, "earnings"); + let fields = ["amount", "year_to_date", "default_amount", "additional_amount", "tax_on_flexible_benefit", + "tax_on_additional_salary"]; - frm.set_currency_labels(["amount", "default_amount", "additional_amount", "tax_on_flexible_benefit", - "tax_on_additional_salary"], frm.doc.currency, "deductions"); + frm.set_currency_labels(fields, frm.doc.currency, "earnings"); + frm.set_currency_labels(fields, frm.doc.currency, "deductions"); }, refresh: function(frm) { diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json index 43deee43aa..9f9691b59d 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.json +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json @@ -584,6 +584,7 @@ "fieldtype": "Column Break" }, { + "description": "Total salary booked for this employee from the beginning of the year (payroll period or fiscal year) up to the current salary slip's end date.", "fieldname": "year_to_date", "fieldtype": "Currency", "label": "Year To Date", @@ -591,6 +592,7 @@ "read_only": 1 }, { + "description": "Total salary booked for this employee from the beginning of the month up to the current salary slip's end date.", "fieldname": "month_to_date", "fieldtype": "Currency", "label": "Month To Date", @@ -616,7 +618,7 @@ "idx": 9, "is_submittable": 1, "links": [], - "modified": "2020-12-21 23:43:44.959840", + "modified": "2021-01-14 13:37:38.180920", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Slip", diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 183ad13411..2d3bc57900 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -52,6 +52,7 @@ class SalarySlip(TransactionBase): self.calculate_net_pay() self.compute_year_to_date() self.compute_month_to_date() + self.compute_component_wise_year_to_date() if frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet"): max_working_hours = frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet") @@ -1138,16 +1139,7 @@ class SalarySlip(TransactionBase): def compute_year_to_date(self): year_to_date = 0 - payroll_period = get_payroll_period(self.start_date, self.end_date, self.company) - - if payroll_period: - period_start_date = payroll_period.start_date - period_end_date = payroll_period.end_date - else: - # get dates based on fiscal year if no payroll period exists - fiscal_year = get_fiscal_year(date=self.start_date, company=self.company, as_dict=1) - period_start_date = fiscal_year.year_start_date - period_end_date = fiscal_year.year_end_date + period_start_date, period_end_date = self.get_year_to_date_period() salary_slip_sum = frappe.get_list('Salary Slip', fields = ['sum(net_pay) as sum'], @@ -1180,6 +1172,47 @@ class SalarySlip(TransactionBase): month_to_date += self.net_pay self.month_to_date = month_to_date + def compute_component_wise_year_to_date(self): + period_start_date, period_end_date = self.get_year_to_date_period() + + for key in ('earnings', 'deductions'): + for component in self.get(key): + year_to_date = 0 + component_sum = frappe.db.sql(""" + SELECT sum(detail.amount) as sum + FROM `tabSalary Detail` as detail + INNER JOIN `tabSalary Slip` as salary_slip + ON detail.parent = salary_slip.name + WHERE + salary_slip.employee_name = %(employee_name)s + AND detail.salary_component = %(component)s + AND salary_slip.start_date >= %(period_start_date)s + AND salary_slip.end_date < %(period_end_date)s + AND salary_slip.name != %(docname)s + AND salary_slip.docstatus = 1""", + {'employee_name': self.employee_name, 'component': component.salary_component, 'period_start_date': period_start_date, + 'period_end_date': period_end_date, 'docname': self.name} + ) + + year_to_date = flt(component_sum[0][0]) if component_sum else 0.0 + year_to_date += component.amount + component.year_to_date = year_to_date + + def get_year_to_date_period(self): + payroll_period = get_payroll_period(self.start_date, self.end_date, self.company) + + if payroll_period: + period_start_date = payroll_period.start_date + period_end_date = payroll_period.end_date + else: + # get dates based on fiscal year if no payroll period exists + fiscal_year = get_fiscal_year(date=self.start_date, company=self.company, as_dict=1) + period_start_date = fiscal_year.year_start_date + period_end_date = fiscal_year.year_end_date + + return period_start_date, period_end_date + + def unlink_ref_doc_from_salary_slip(ref_no): linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip` where journal_entry=%s and docstatus < 2""", (ref_no)) diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 4368c03c2a..f58a8e58c2 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -321,6 +321,38 @@ class TestSalarySlip(unittest.TestCase): year_to_date += flt(slip.net_pay) self.assertEqual(slip.year_to_date, year_to_date) + def test_component_wise_year_to_date_computation(self): + from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure + + applicant = make_employee("test_ytd@salary.com", company="_Test Company") + + payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") + + create_tax_slab(payroll_period, allow_tax_exemption=True, currency="INR", effective_date=getdate("2019-04-01"), + company="_Test Company") + + salary_structure = make_salary_structure("Monthly Salary Structure Test for Salary Slip YTD", + "Monthly", employee=applicant, company="_Test Company", currency="INR", payroll_period=payroll_period) + + # clear salary slip for this employee + frappe.db.sql("DELETE FROM `tabSalary Slip` where employee_name = 'test_ytd@salary.com'") + + create_salary_slips_for_payroll_period(applicant, salary_structure.name, + payroll_period, deduct_random=False, num=3) + + salary_slips = frappe.get_all("Salary Slip", fields=["name"], filters={"employee_name": + "test_ytd@salary.com"}, order_by = "posting_date") + + year_to_date = dict() + for slip in salary_slips: + doc = frappe.get_doc("Salary Slip", slip.name) + for entry in doc.get("earnings"): + if not year_to_date.get(entry.salary_component): + year_to_date[entry.salary_component] = 0 + + year_to_date[entry.salary_component] += entry.amount + self.assertEqual(year_to_date[entry.salary_component], entry.year_to_date) + def test_tax_for_payroll_period(self): data = {} # test the impact of tax exemption declaration, tax exemption proof submission @@ -714,10 +746,10 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = else: return income_tax_slab_name -def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True): +def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True, num=12): deducted_dates = [] i = 0 - while i < 12: + while i < num: slip = frappe.get_doc({"doctype": "Salary Slip", "employee": employee, "salary_structure": salary_structure, "frequency": "Monthly"}) if i == 0: diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.js b/erpnext/payroll/doctype/salary_structure/salary_structure.js index ba824c5d6f..1378bf0b91 100755 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.js +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.js @@ -58,18 +58,19 @@ frappe.ui.form.on('Salary Structure', { if(!frm.doc.company) return; frm.set_query("salary_component", "earnings", function() { return { - query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", filters: {type: "earning", company: frm.doc.company} }; }); frm.set_query("salary_component", "deductions", function() { return { - query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", filters: {type: "deduction", company: frm.doc.company} }; }); }, + company: function(frm) { + frm.trigger('set_earning_deduction_component'); + }, currency: function(frm) { calculate_totals(frm.doc); @@ -117,6 +118,7 @@ frappe.ui.form.on('Salary Structure', { fields_read_only.forEach(function(field) { frappe.meta.get_docfield("Salary Detail", field, frm.doc.name).read_only = 1; }); + frm.trigger('set_earning_deduction_component'); }, assign_to_employees:function (frm) { diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py index 77914bb531..1712081550 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py @@ -207,17 +207,3 @@ def get_employees(salary_structure): return list(set([d.employee for d in employees])) -@frappe.whitelist() -@frappe.validate_and_sanitize_search_inputs -def get_earning_deduction_components(doctype, txt, searchfield, start, page_len, filters): - if len(filters) < 2: - return {} - - return frappe.db.sql(""" - select t1.salary_component - from `tabSalary Component` t1, `tabSalary Component Account` t2 - where t1.salary_component = t2.parent - and t1.type = %s - and t2.company = %s - order by salary_component - """, (filters['type'], filters['company']) ) diff --git a/erpnext/payroll/print_format/__init__.py b/erpnext/payroll/print_format/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/payroll/print_format/salary_slip_with_year_to_date/__init__.py b/erpnext/payroll/print_format/salary_slip_with_year_to_date/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/payroll/print_format/salary_slip_with_year_to_date/salary_slip_with_year_to_date.json b/erpnext/payroll/print_format/salary_slip_with_year_to_date/salary_slip_with_year_to_date.json new file mode 100644 index 0000000000..71ba37f6ed --- /dev/null +++ b/erpnext/payroll/print_format/salary_slip_with_year_to_date/salary_slip_with_year_to_date.json @@ -0,0 +1,25 @@ +{ + "absolute_value": 0, + "align_labels_right": 0, + "creation": "2021-01-14 09:56:42.393623", + "custom_format": 0, + "default_print_language": "en", + "disabled": 0, + "doc_type": "Salary Slip", + "docstatus": 0, + "doctype": "Print Format", + "font": "Default", + "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"

{{doc.name}}

\\n
\\n
\\n
\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"employee\", \"print_hide\": 0, \"label\": \"Employee\"}, {\"fieldname\": \"company\", \"print_hide\": 0, \"label\": \"Company\"}, {\"fieldname\": \"employee_name\", \"print_hide\": 0, \"label\": \"Employee Name\"}, {\"fieldname\": \"department\", \"print_hide\": 0, \"label\": \"Department\"}, {\"fieldname\": \"designation\", \"print_hide\": 0, \"label\": \"Designation\"}, {\"fieldname\": \"branch\", \"print_hide\": 0, \"label\": \"Branch\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"start_date\", \"print_hide\": 0, \"label\": \"Start Date\"}, {\"fieldname\": \"end_date\", \"print_hide\": 0, \"label\": \"End Date\"}, {\"fieldname\": \"total_working_days\", \"print_hide\": 0, \"label\": \"Working Days\"}, {\"fieldname\": \"leave_without_pay\", \"print_hide\": 0, \"label\": \"Leave Without Pay\"}, {\"fieldname\": \"payment_days\", \"print_hide\": 0, \"label\": \"Payment Days\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"earnings\", \"print_hide\": 0, \"label\": \"Earnings\", \"visible_columns\": [{\"fieldname\": \"salary_component\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"amount\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"year_to_date\", \"print_width\": \"\", \"print_hide\": 0}]}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"deductions\", \"print_hide\": 0, \"label\": \"Deductions\", \"visible_columns\": [{\"fieldname\": \"salary_component\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"amount\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"year_to_date\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"depends_on_payment_days\", \"print_width\": \"\", \"print_hide\": 0}]}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"gross_pay\", \"print_hide\": 0, \"label\": \"Gross Pay\"}, {\"fieldname\": \"total_deduction\", \"print_hide\": 0, \"label\": \"Total Deduction\"}, {\"fieldname\": \"net_pay\", \"print_hide\": 0, \"label\": \"Net Pay\"}, {\"fieldname\": \"rounded_total\", \"print_hide\": 0, \"label\": \"Rounded Total\"}, {\"fieldname\": \"total_in_words\", \"print_hide\": 0, \"label\": \"Total in words\"}, {\"fieldtype\": \"Section Break\", \"label\": \"net pay info\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"year_to_date\", \"print_hide\": 0, \"label\": \"Year To Date\"}, {\"fieldname\": \"month_to_date\", \"print_hide\": 0, \"label\": \"Month To Date\"}]", + "idx": 0, + "line_breaks": 0, + "modified": "2021-01-14 10:03:45.283725", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Salary Slip with Year to Date", + "owner": "Administrator", + "print_format_builder": 0, + "print_format_type": "Jinja", + "raw_printing": 0, + "show_section_headings": 0, + "standard": "Yes" +} \ No newline at end of file diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index f3cecd9059..3cdfcb212f 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -2,12 +2,13 @@ "actions": [], "allow_import": 1, "allow_rename": 1, - "autoname": "field:project_name", + "autoname": "naming_series:", "creation": "2013-03-07 11:55:07", "doctype": "DocType", "document_type": "Setup", "engine": "InnoDB", "field_order": [ + "naming_series", "project_name", "status", "project_type", @@ -440,13 +441,24 @@ "fieldtype": "Text", "label": "Message", "mandatory_depends_on": "collect_progress" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "no_copy": 1, + "options": "PROJ-.####", + "print_hide": 1, + "reqd": 1, + "set_only_once": 1 } ], "icon": "fa fa-puzzle-piece", "idx": 29, + "index_web_pages_for_search": 1, "links": [], "max_attachments": 4, - "modified": "2020-04-08 22:11:14.552615", + "modified": "2020-09-02 11:54:01.223620", "modified_by": "Administrator", "module": "Projects", "name": "Project", @@ -488,5 +500,6 @@ "sort_field": "modified", "sort_order": "DESC", "timeline_field": "customer", + "title_field": "project_name", "track_seen": 1 } diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index 97b67b38eb..d85c82612a 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -42,7 +42,7 @@ class TestProject(unittest.TestCase): task2 = task_exists("Test Template Task Child 1") if not task2: task2 = create_task(subject="Test Template Task Child 1", parent_task=task1.name, is_template=1, begin=1, duration=3) - + task3 = task_exists("Test Template Task Child 2") if not task3: task3 = create_task(subject="Test Template Task Child 2", parent_task=task1.name, is_template=1, begin=2, duration=3) @@ -76,7 +76,7 @@ class TestProject(unittest.TestCase): task2 = task_exists("Test Template Task with Dependency") if not task2: task2 = create_task(subject="Test Template Task with Dependency", depends_on=task1.name, is_template=1, begin=2, duration=2) - + template = make_project_template("Test Project with Template - Dependent Tasks", [task1, task2]) project = get_project(project_name, template) tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name'], dict(project=project.name), order_by='creation asc') @@ -105,6 +105,9 @@ def get_project(name, template): def make_project(args): args = frappe._dict(args) + if args.project_name and frappe.db.exists("Project", {"project_name": args.project_name}): + return frappe.get_doc("Project", {"project_name": args.project_name}) + project = frappe.get_doc(dict( doctype = 'Project', project_name = args.project_name, @@ -116,8 +119,7 @@ def make_project(args): template = make_project_template(args.project_template_name) project.project_template = template.name - if not frappe.db.exists("Project", args.project_name): - project.insert() + project.insert() return project diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index 25714f8cde..0fad5e8807 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -30,14 +30,16 @@ class TestTask(unittest.TestCase): }) def test_reschedule_dependent_task(self): + project = frappe.get_value("Project", {"project_name": "_Test Project"}) + task1 = create_task("_Test Task 1", nowdate(), add_days(nowdate(), 10)) task2 = create_task("_Test Task 2", add_days(nowdate(), 11), add_days(nowdate(), 15), task1.name) - task2.get("depends_on")[0].project = "_Test Project" + task2.get("depends_on")[0].project = project task2.save() task3 = create_task("_Test Task 3", add_days(nowdate(), 11), add_days(nowdate(), 15), task2.name) - task3.get("depends_on")[0].project = "_Test Project" + task3.get("depends_on")[0].project = project task3.save() task1.update({ @@ -104,7 +106,7 @@ def create_task(subject, start=None, end=None, depends_on=None, project=None, pa task.subject = subject task.exp_start_date = start or nowdate() task.exp_end_date = end or nowdate() - task.project = project or None if is_template else "_Test Project" + task.project = project or None if is_template else frappe.get_value("Project", {"project_name": "_Test Project"}) task.is_template = is_template task.start = begin task.duration = duration diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index a5ce44dcf2..4cb38049ff 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -89,10 +89,11 @@ class TestTimesheet(unittest.TestCase): def test_timesheet_billing_based_on_project(self): emp = make_employee("test_employee_6@salary.com") + project = frappe.get_value("Project", {"project_name": "_Test Project"}) - timesheet = make_timesheet(emp, simulate=True, billable=1, project = '_Test Project', company='_Test Company') + timesheet = make_timesheet(emp, simulate=True, billable=1, project=project, company='_Test Company') sales_invoice = create_sales_invoice(do_not_save=True) - sales_invoice.project = '_Test Project' + sales_invoice.project = project sales_invoice.submit() ts = frappe.get_doc('Timesheet', timesheet.name) diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index b068245a8b..b123af5d18 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -134,7 +134,7 @@ frappe.ui.form.on("Timesheet", { }); }, - project: function(frm) { + parent_project: function(frm) { set_project_in_timelog(frm); }, @@ -168,8 +168,8 @@ frappe.ui.form.on("Timesheet Detail", { }, time_logs_add: function(frm, cdt, cdn) { - if(frm.doc.project) { - frappe.model.set_value(cdt, cdn, 'project', frm.doc.project); + if(frm.doc.parent_project) { + frappe.model.set_value(cdt, cdn, 'project', frm.doc.parent_project); } var $trigger_again = $('.form-grid').find('.grid-row').find('.btn-open-row'); @@ -308,7 +308,9 @@ const set_employee_and_company = function(frm) { }; function set_project_in_timelog(frm) { - if(frm.doc.project){ - erpnext.utils.copy_value_in_all_rows(frm.doc, frm.doc.doctype, frm.doc.name, "time_logs", "project"); + if(frm.doc.parent_project) { + $.each(frm.doc.time_logs || [], function(i, item) { + frappe.model.set_value(item.doctype, item.name, "project", frm.doc.parent_project); + }); } } \ No newline at end of file diff --git a/erpnext/projects/doctype/timesheet/timesheet.json b/erpnext/projects/doctype/timesheet/timesheet.json index 4c2edf4f03..b28682184e 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.json +++ b/erpnext/projects/doctype/timesheet/timesheet.json @@ -15,7 +15,7 @@ "column_break_3", "salary_slip", "status", - "project", + "parent_project", "employee_detail", "employee", "employee_name", @@ -261,7 +261,7 @@ "read_only": 1 }, { - "fieldname": "project", + "fieldname": "parent_project", "fieldtype": "Link", "label": "Project", "options": "Project" @@ -271,7 +271,7 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-29 07:50:35.938231", + "modified": "2021-01-08 20:51:14.590080", "modified_by": "Administrator", "module": "Projects", "name": "Timesheet", diff --git a/erpnext/public/build.json b/erpnext/public/build.json index 2f15cbcef1..f0212db0b2 100644 --- a/erpnext/public/build.json +++ b/erpnext/public/build.json @@ -55,6 +55,8 @@ "js/item-dashboard.min.js": [ "stock/dashboard/item_dashboard.html", "stock/dashboard/item_dashboard_list.html", - "stock/dashboard/item_dashboard.js" + "stock/dashboard/item_dashboard.js", + "stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html", + "stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html" ] } diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index 29f35958e1..649eb454ac 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -31,15 +31,6 @@ 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/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index db85a3ec99..a2a723dd77 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -195,6 +195,10 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ this._super(doc, cdt, cdn); }, + batch_no: function(doc, cdt, cdn) { + this._super(doc, cdt, cdn); + }, + received_qty: function(doc, cdt, cdn) { this.calculate_accepted_qty(doc, cdt, cdn) }, @@ -516,4 +520,4 @@ erpnext.buying.get_items_from_product_bundle = function(frm) { }); dialog.show(); -} +} \ No newline at end of file diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index bed9c14141..9627600a17 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1,6 +1,8 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt +frappe.provide('erpnext.accounts.dimensions'); + erpnext.TransactionController = erpnext.taxes_and_totals.extend({ setup: function() { this._super(); @@ -103,9 +105,19 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ frappe.ui.form.on(this.frm.doctype + " Item", { items_add: function(frm, cdt, cdn) { var item = frappe.get_doc(cdt, cdn); - if(!item.warehouse && frm.doc.set_warehouse) { + if (!item.warehouse && frm.doc.set_warehouse) { item.warehouse = frm.doc.set_warehouse; } + + if (!item.target_warehouse && frm.doc.set_target_warehouse) { + item.target_warehouse = frm.doc.set_target_warehouse; + } + + if (!item.from_warehouse && frm.doc.set_from_warehouse) { + item.from_warehouse = frm.doc.set_from_warehouse; + } + + erpnext.accounts.dimensions.copy_dimension_from_first_row(frm, cdt, cdn, 'items'); } }); @@ -159,16 +171,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }; }); } - if (this.frm.fields_dict["items"].grid.get_field("cost_center")) { - this.frm.set_query("cost_center", "items", function(doc) { - return { - filters: { - "company": doc.company, - "is_group": 0 - } - }; - }); - } if (this.frm.fields_dict["items"].grid.get_field("expense_account")) { this.frm.set_query("expense_account", "items", function(doc) { @@ -233,6 +235,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }; + this.frm.trigger('set_default_internal_warehouse'); + return frappe.run_serially([ () => set_value('currency', currency), () => set_value('price_list_currency', currency), @@ -595,11 +599,21 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ return frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]) .then((r) => { if (r.message && - (r.message.has_batch_no || r.message.has_serial_no)) { + (r.message.has_batch_no || r.message.has_serial_no)) { frappe.flags.hide_serial_batch_dialog = false; } }); }, + () => { + // check if batch serial selector is disabled or not + if (show_batch_dialog && !frappe.flags.hide_serial_batch_dialog) + return frappe.db.get_single_value('Stock Settings', 'disable_serial_no_and_batch_selector') + .then((value) => { + if (value) { + frappe.flags.hide_serial_batch_dialog = true; + } + }); + }, () => { if(show_batch_dialog && !frappe.flags.hide_serial_batch_dialog) { var d = locals[cdt][cdn]; @@ -654,7 +668,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ args: item_args }, callback: function(r) { - frappe.model.set_value(item.doctype, item.name, 'rate', r.message); + frappe.model.set_value(item.doctype, item.name, 'rate', r.message * item.conversion_factor); } }); }, @@ -720,6 +734,31 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ this.calculate_taxes_and_totals(false); }, + update_stock: function() { + this.frm.trigger('set_default_internal_warehouse'); + }, + + set_default_internal_warehouse: function() { + let me = this; + if ((this.frm.doc.doctype === 'Sales Invoice' && me.frm.doc.update_stock) + || this.frm.doc.doctype == 'Delivery Note') { + if (this.frm.doc.is_internal_customer && this.frm.doc.company === this.frm.doc.represents_company) { + frappe.db.get_value('Company', this.frm.doc.company, 'default_in_transit_warehouse', function(value) { + me.frm.set_value('set_target_warehouse', value.default_in_transit_warehouse); + }); + } + } + + if ((this.frm.doc.doctype === 'Purchase Invoice' && me.frm.doc.update_stock) + || this.frm.doc.doctype == 'Purchase Receipt') { + if (this.frm.doc.is_internal_supplier && this.frm.doc.company === this.frm.doc.represents_company) { + frappe.db.get_value('Company', this.frm.doc.company, 'default_in_transit_warehouse', function(value) { + me.frm.set_value('set_from_warehouse', value.default_in_transit_warehouse); + }); + } + } + }, + company: function() { var me = this; var set_pricing = function() { @@ -806,7 +845,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) { erpnext.utils.get_shipping_address(this.frm, function(){ set_party_account(set_pricing); - }) + }); // Get default company billing address in Purchase Invoice, Order and Receipt frappe.call({ @@ -1105,6 +1144,11 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }, + batch_no: function(doc, cdt, cdn) { + let item = frappe.get_doc(cdt, cdn); + this.apply_price_list(item, true); + }, + toggle_conversion_factor: function(item) { // toggle read only property for conversion factor field if the uom and stock uom are same if(this.frm.get_field('items').grid.fields_map.conversion_factor) { @@ -1409,6 +1453,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ "pricing_rules": d.pricing_rules, "warehouse": d.warehouse, "serial_no": d.serial_no, + "batch_no": d.batch_no, "price_list_rate": d.price_list_rate, "conversion_factor": d.conversion_factor || 1.0 }); @@ -1967,6 +2012,14 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ this.autofill_warehouse(this.frm.doc.items, "warehouse", this.frm.doc.set_warehouse); }, + set_target_warehouse: function() { + this.autofill_warehouse(this.frm.doc.items, "target_warehouse", this.frm.doc.set_target_warehouse); + }, + + set_from_warehouse: function() { + this.autofill_warehouse(this.frm.doc.items, "from_warehouse", this.frm.doc.set_from_warehouse); + }, + autofill_warehouse : function (child_table, warehouse_field, warehouse) { if (warehouse && child_table && child_table.length) { let doctype = child_table[0].doctype; @@ -2031,3 +2084,35 @@ erpnext.show_serial_batch_selector = function (frm, d, callback, on_close, show_ }, show_dialog); }); } + +erpnext.apply_putaway_rule = (frm, purpose=null) => { + if (!frm.doc.company) { + frappe.throw({message: __("Please select a Company first."), title: __("Mandatory")}); + } + if (!frm.doc.items.length) return; + + frappe.call({ + method: "erpnext.stock.doctype.putaway_rule.putaway_rule.apply_putaway_rule", + args: { + doctype: frm.doctype, + items: frm.doc.items, + company: frm.doc.company, + sync: true, + purpose: purpose + }, + callback: (result) => { + if (!result.exc && result.message) { + frm.clear_table("items"); + + let items = result.message; + items.forEach((row) => { + delete row["name"]; // dont overwrite name from server side + let child = frm.add_child("items"); + Object.assign(child, row); + frm.script_manager.trigger("qty", child.doctype, child.name); + }); + frm.get_field("items").grid.refresh(); + } + } + }); +}; \ No newline at end of file diff --git a/erpnext/public/js/queries.js b/erpnext/public/js/queries.js index 560a5617da..b635adcd44 100644 --- a/erpnext/public/js/queries.js +++ b/erpnext/public/js/queries.js @@ -115,7 +115,26 @@ $.extend(erpnext.queries, { ["Warehouse", "is_group", "=",0] ] - } + }; + }, + + get_filtered_dimensions: function(doc, child_fields, dimension, company) { + let account = ''; + + child_fields.forEach((field) => { + if (!account) { + account = doc[field]; + } + }); + + return { + query: "erpnext.controllers.queries.get_filtered_dimensions", + filters: { + 'dimension': dimension, + 'account': account, + 'company': company + } + }; } }); diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 891bbe5b59..c39609bd38 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -194,15 +194,21 @@ $.extend(erpnext.utils, { add_dimensions: function(report_name, index) { let filters = frappe.query_reports[report_name].filters; - erpnext.dimension_filters.forEach((dimension) => { - let found = filters.some(el => el.fieldname === dimension['fieldname']); + frappe.call({ + method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions", + callback: function(r) { + let accounting_dimensions = r.message[0]; + accounting_dimensions.forEach((dimension) => { + let found = filters.some(el => el.fieldname === dimension['fieldname']); - if (!found) { - filters.splice(index, 0 ,{ - "fieldname": dimension["fieldname"], - "label": __(dimension["label"]), - "fieldtype": "Link", - "options": dimension["document_type"] + if (!found) { + filters.splice(index, 0, { + "fieldname": dimension["fieldname"], + "label": __(dimension["label"]), + "fieldtype": "Link", + "options": dimension["document_type"] + }); + } }); } }); diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js index b6720c05cb..96e181788e 100644 --- a/erpnext/public/js/utils/dimension_tree_filter.js +++ b/erpnext/public/js/utils/dimension_tree_filter.js @@ -1,54 +1,83 @@ -frappe.provide('frappe.ui.form'); +frappe.provide('erpnext.accounts'); -let default_dimensions = {}; +erpnext.accounts.dimensions = { + setup_dimension_filters(frm, doctype) { + this.accounting_dimensions = []; + this.default_dimensions = {}; + this.fetch_custom_dimensions(frm, doctype); + }, -let doctypes_with_dimensions = ["GL Entry", "Sales Invoice", "Purchase Invoice", "Payment Entry", "Asset", - "Expense Claim", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note", "Shipping Rule", "Loyalty Program", - "Fee Schedule", "Fee Structure", "Stock Reconciliation", "Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", - "Subscription", "Purchase Order", "Journal Entry", "Material Request", "Purchase Receipt", "Landed Cost Item", "Asset"]; + fetch_custom_dimensions(frm, doctype) { + let me = this; + frappe.call({ + method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions", + args: { + 'with_cost_center_and_project': true + }, + callback: function(r) { + me.accounting_dimensions = r.message[0]; + me.default_dimensions = r.message[1]; + me.setup_filters(frm, doctype); + } + }); + }, -let child_docs = ["Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Journal Entry Account", - "Material Request Item", "Delivery Note Item", "Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", - "Landed Cost Item", "Asset Value Adjustment", "Opening Invoice Creation Tool Item", "Subscription Plan"]; - -frappe.call({ - method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimension_filters", - callback: function(r) { - erpnext.dimension_filters = r.message[0]; - default_dimensions = r.message[1]; - } -}); - -doctypes_with_dimensions.forEach((doctype) => { - frappe.ui.form.on(doctype, { - onload: function(frm) { - erpnext.dimension_filters.forEach((dimension) => { + setup_filters(frm, doctype) { + if (this.accounting_dimensions) { + this.accounting_dimensions.forEach((dimension) => { frappe.model.with_doctype(dimension['document_type'], () => { - if(frappe.meta.has_field(dimension['document_type'], 'is_group')) { - frm.set_query(dimension['fieldname'], { - "is_group": 0 - }); - } + let parent_fields = []; + frappe.meta.get_docfields(doctype).forEach((df) => { + if (df.fieldtype === 'Link' && df.options === 'Account') { + parent_fields.push(df.fieldname); + } else if (df.fieldtype === 'Table') { + this.setup_child_filters(frm, df.options, df.fieldname, dimension['fieldname']); + } + + if (frappe.meta.has_field(doctype, dimension['fieldname'])) { + this.setup_account_filters(frm, dimension['fieldname'], parent_fields); + } + }); }); }); - }, + } + }, - company: function(frm) { - if(frm.doc.company && (Object.keys(default_dimensions || {}).length > 0) - && default_dimensions[frm.doc.company]) { - frm.trigger('update_dimension'); - } - }, + setup_child_filters(frm, doctype, parentfield, dimension) { + let fields = []; - update_dimension: function(frm) { - erpnext.dimension_filters.forEach((dimension) => { - if(frm.is_new()) { - if(frm.doc.company && Object.keys(default_dimensions || {}).length > 0 - && default_dimensions[frm.doc.company]) { + if (frappe.meta.has_field(doctype, dimension)) { + frappe.model.with_doctype(doctype, () => { + frappe.meta.get_docfields(doctype).forEach((df) => { + if (df.fieldtype === 'Link' && df.options === 'Account') { + fields.push(df.fieldname); + } + }); - let default_dimension = default_dimensions[frm.doc.company][dimension['fieldname']]; + frm.set_query(dimension, parentfield, function(doc, cdt, cdn) { + let row = locals[cdt][cdn]; + return erpnext.queries.get_filtered_dimensions(row, fields, dimension, doc.company); + }); + }); + } + }, - if(default_dimension) { + setup_account_filters(frm, dimension, fields) { + frm.set_query(dimension, function(doc) { + return erpnext.queries.get_filtered_dimensions(doc, fields, dimension, doc.company); + }); + }, + + update_dimension(frm, doctype) { + if (this.accounting_dimensions) { + this.accounting_dimensions.forEach((dimension) => { + if (frm.is_new()) { + if (frm.doc.company && Object.keys(this.default_dimensions || {}).length > 0 + && this.default_dimensions[frm.doc.company]) { + + let default_dimension = this.default_dimensions[frm.doc.company][dimension['fieldname']]; + + if (default_dimension) { if (frappe.meta.has_field(doctype, dimension['fieldname'])) { frm.set_value(dimension['fieldname'], default_dimension); } @@ -61,23 +90,14 @@ doctypes_with_dimensions.forEach((doctype) => { } }); } - }); -}); + }, -child_docs.forEach((doctype) => { - frappe.ui.form.on(doctype, { - items_add: function(frm, cdt, cdn) { - erpnext.dimension_filters.forEach((dimension) => { - var row = frappe.get_doc(cdt, cdn); - frm.script_manager.copy_from_first_row("items", row, [dimension['fieldname']]); - }); - }, - - accounts_add: function(frm, cdt, cdn) { - erpnext.dimension_filters.forEach((dimension) => { - var row = frappe.get_doc(cdt, cdn); - frm.script_manager.copy_from_first_row("accounts", row, [dimension['fieldname']]); + copy_dimension_from_first_row(frm, cdt, cdn, fieldname) { + if (frappe.meta.has_field(frm.doctype, fieldname) && this.accounting_dimensions) { + this.accounting_dimensions.forEach((dimension) => { + let row = frappe.get_doc(cdt, cdn); + frm.script_manager.copy_from_first_row(fieldname, row, [dimension['fieldname']]); }); } - }); -}); \ No newline at end of file + } +}; \ No newline at end of file diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index 770704e595..808dd5add0 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -276,6 +276,12 @@ erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) { erpnext.utils.get_shipping_address = function(frm, callback){ if (frm.doc.company) { + if (!(frm.doc.inter_com_order_reference || frm.doc.internal_invoice_reference || + frm.doc.internal_order_reference)) { + if (callback) { + return callback(); + } + } frappe.call({ method: "erpnext.accounts.custom.address.get_shipping_address", args: { diff --git a/erpnext/regional/doctype/datev_settings/datev_settings.json b/erpnext/regional/doctype/datev_settings/datev_settings.json index 713e8e34ef..f60de4c8af 100644 --- a/erpnext/regional/doctype/datev_settings/datev_settings.json +++ b/erpnext/regional/doctype/datev_settings/datev_settings.json @@ -7,13 +7,14 @@ "engine": "InnoDB", "field_order": [ "client", - "account_number_length", - "column_break_2", "client_number", - "section_break_4", + "column_break_2", + "consultant_number", "consultant", + "section_break_4", + "account_number_length", "column_break_6", - "consultant_number" + "temporary_against_account_number" ], "fields": [ { @@ -66,10 +67,17 @@ "fieldtype": "Int", "label": "Account Number Length", "reqd": 1 + }, + { + "allow_in_quick_entry": 1, + "fieldname": "temporary_against_account_number", + "fieldtype": "Data", + "label": "Temporary Against Account Number", + "reqd": 1 } ], "links": [], - "modified": "2020-11-05 17:52:11.674329", + "modified": "2020-11-19 19:00:09.088816", "modified_by": "Administrator", "module": "Regional", "name": "DATEV Settings", diff --git a/erpnext/regional/germany/accounts_controller.py b/erpnext/regional/germany/accounts_controller.py deleted file mode 100644 index 7f76493608..0000000000 --- a/erpnext/regional/germany/accounts_controller.py +++ /dev/null @@ -1,53 +0,0 @@ -import frappe -from frappe import _ -from frappe import msgprint - - -REQUIRED_FIELDS = { - "Sales Invoice": [ - { - "field_name": "company_address", - "regulation": "§ 14 Abs. 4 Nr. 1 UStG" - }, - { - "field_name": "company_tax_id", - "regulation": "§ 14 Abs. 4 Nr. 2 UStG" - }, - { - "field_name": "taxes", - "regulation": "§ 14 Abs. 4 Nr. 8 UStG" - }, - { - "field_name": "customer_address", - "regulation": "§ 14 Abs. 4 Nr. 1 UStG", - "condition": "base_grand_total > 250" - } - ] -} - - -def validate_regional(doc): - """Check if required fields for this document are present.""" - required_fields = REQUIRED_FIELDS.get(doc.doctype) - if not required_fields: - return - - meta = frappe.get_meta(doc.doctype) - field_map = {field.fieldname: field.label for field in meta.fields} - - for field in required_fields: - condition = field.get("condition") - if condition and not frappe.safe_eval(condition, doc.as_dict()): - continue - - field_name = field.get("field_name") - regulation = field.get("regulation") - if field_name and not doc.get(field_name): - missing(field_map.get(field_name), regulation) - - -def missing(field_label, regulation): - """Notify the user that a required field is missing.""" - translated_msg = _('Remember to set {field_label}. It is required by {regulation}.', context='Specific for Germany. Example: Remember to set Company Tax ID. It is required by § 14 Abs. 4 Nr. 2 UStG.') # noqa: E501 - formatted_msg = translated_msg.format(field_label=frappe.bold(_(field_label)), regulation=regulation) - msgprint(formatted_msg) diff --git a/erpnext/regional/germany/test_accounts_controller.py b/erpnext/regional/germany/test_accounts_controller.py deleted file mode 100644 index 8bd378c971..0000000000 --- a/erpnext/regional/germany/test_accounts_controller.py +++ /dev/null @@ -1,12 +0,0 @@ -import frappe -import unittest -from erpnext.regional.germany.accounts_controller import validate_regional - - -class TestAccountsController(unittest.TestCase): - - def setUp(self): - self.sales_invoice = frappe.get_last_doc('Sales Invoice') - - def test_validate_regional(self): - validate_regional(self.sales_invoice) diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 9c86cc89f5..9fa94c401f 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -18,6 +18,9 @@ erpnext.setup_einvoice_actions = (doctype) => { if (!irn && !__unsaved) { const action = () => { + if (frm.doc.__unsaved) { + frappe.throw(__('Please save the document to generate IRN.')); + } frappe.call({ method: 'erpnext.regional.india.e_invoice.utils.get_einvoice', args: { doctype, docname: name }, diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index d0cac90e4d..2366fcb9ed 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -11,6 +11,7 @@ import json import base64 import frappe import traceback +import io from frappe import _, bold from pyqrcode import create as qrcreate from frappe.integrations.utils import make_post_request, make_get_request @@ -161,9 +162,9 @@ def get_item_list(invoice): item.qty = abs(item.qty) item.discount_amount = abs(item.discount_amount * item.qty) - item.unit_rate = abs(item.base_amount / item.qty) - item.gross_amount = abs(item.base_amount) - item.taxable_value = abs(item.base_amount) + item.unit_rate = abs(item.base_net_amount / item.qty) + item.gross_amount = abs(item.base_net_amount) + item.taxable_value = abs(item.base_net_amount) item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None @@ -198,7 +199,7 @@ def update_item_taxes(invoice, item): if t.account_head in gst_accounts_list: item_tax_rate = item_tax_detail[0] # item tax amount excluding discount amount - item_tax_amount = (item_tax_rate / 100) * item.base_amount + item_tax_amount = (item_tax_rate / 100) * item.base_net_amount if t.account_head in gst_accounts.cess_account: item_tax_amount_after_discount = item_tax_detail[1] @@ -217,8 +218,14 @@ def update_item_taxes(invoice, item): def get_invoice_value_details(invoice): invoice_value_details = frappe._dict(dict()) - invoice_value_details.base_total = abs(invoice.base_total) - invoice_value_details.invoice_discount_amt = invoice.base_discount_amount + + if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount: + invoice_value_details.base_total = abs(invoice.base_total) + else: + invoice_value_details.base_total = abs(invoice.base_net_total) + + # since tax already considers discount amount + invoice_value_details.invoice_discount_amt = 0 # invoice.base_discount_amount invoice_value_details.round_off = invoice.base_rounding_adjustment invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total) invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total) @@ -244,9 +251,9 @@ def update_invoice_taxes(invoice, invoice_value_details): for tax_type in ['igst', 'cgst', 'sgst']: if t.account_head in gst_accounts[f'{tax_type}_account']: - invoice_value_details[f'total_{tax_type}_amt'] += abs(t.base_tax_amount) + invoice_value_details[f'total_{tax_type}_amt'] += abs(t.base_tax_amount_after_discount_amount) else: - invoice_value_details.total_other_charges += abs(t.base_tax_amount) + invoice_value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount) return invoice_value_details @@ -430,7 +437,7 @@ class GSPConnector(): self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn' self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice' self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin' - self.cancel_ewaybill_url = self.base_url + '/enriched/ei/api/ewayapi' + self.cancel_ewaybill_url = self.base_url + '/enriched/ewb/ewayapi?action=CANEWB' self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill' def get_credentials(self): @@ -473,7 +480,7 @@ class GSPConnector(): "data": json.dumps(data, indent=4) if isinstance(data, dict) else data, "response": json.dumps(res, indent=4) if res else None }) - request_log.insert(ignore_permissions=True) + request_log.save(ignore_permissions=True) frappe.db.commit() def fetch_auth_token(self): @@ -486,7 +493,8 @@ class GSPConnector(): res = self.make_request('post', self.authenticate_url, headers) self.e_invoice_settings.auth_token = "{} {}".format(res.get('token_type'), res.get('access_token')) self.e_invoice_settings.token_expiry = add_to_date(None, seconds=res.get('expires_in')) - self.e_invoice_settings.save() + self.e_invoice_settings.save(ignore_permissions=True) + self.e_invoice_settings.reload() except Exception: self.log_error(res) @@ -520,7 +528,7 @@ class GSPConnector(): except Exception: self.log_error() self.raise_error(True) - + @staticmethod def get_gstin_details(gstin): '''fetch and cache GSTIN details''' @@ -615,7 +623,7 @@ class GSPConnector(): except Exception: self.log_error(data) self.raise_error(True) - + def generate_eway_bill(self, **kwargs): args = frappe._dict(kwargs) @@ -664,7 +672,8 @@ class GSPConnector(): 'cancelRsnCode': reason, 'cancelRmrk': remark }, indent=4) - + headers["username"] = headers["user_name"] + del headers["user_name"] try: res = self.make_request('post', self.cancel_ewaybill_url, headers, data) if res.get('success'): @@ -757,26 +766,26 @@ class GSPConnector(): 'label': _('IRN Generated') } self.update_invoice() - + def attach_qrcode_image(self): qrcode = self.invoice.signed_qr_code doctype = self.invoice.doctype docname = self.invoice.name + filename = 'QRCode_{}.png'.format(docname).replace(os.path.sep, "__") - _file = frappe.new_doc('File') - _file.update({ - 'file_name': 'QRCode_{}.png'.format(docname.replace('/', '-')), - 'attached_to_doctype': doctype, - 'attached_to_name': docname, - 'content': 'qrcode', - 'is_private': 1 - }) - _file.insert() - frappe.db.commit() + qr_image = io.BytesIO() url = qrcreate(qrcode, error='L') - abs_file_path = os.path.abspath(_file.get_full_path()) - url.png(abs_file_path, scale=2, quiet_zone=1) - + url.png(qr_image, scale=2, quiet_zone=1) + _file = frappe.get_doc({ + "doctype": "File", + "file_name": filename, + "attached_to_doctype": doctype, + "attached_to_name": docname, + "attached_to_field": "qrcode_image", + "is_private": 1, + "content": qr_image.getvalue()}) + _file.save() + frappe.db.commit() self.invoice.qrcode_image = _file.file_url def update_invoice(self): diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 5321a9a3b5..526198424f 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -7,7 +7,7 @@ import frappe, os, json from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.permissions import add_permission, update_permission_property from erpnext.regional.india import states -from erpnext.accounts.utils import get_fiscal_year +from erpnext.accounts.utils import get_fiscal_year, FiscalYearError from frappe.utils import today def setup(company=None, patch=True): @@ -629,15 +629,20 @@ def set_salary_components(docs): def set_tax_withholding_category(company): accounts = [] + fiscal_year = None abbr = frappe.get_value("Company", company, "abbr") tds_account = frappe.get_value("Account", 'TDS Payable - {0}'.format(abbr), 'name') if company and tds_account: accounts = [dict(company=company, account=tds_account)] - fiscal_year = get_fiscal_year(today(), company=company)[0] - docs = get_tds_details(accounts, fiscal_year) + try: + fiscal_year = get_fiscal_year(today(), verbose=0, company=company)[0] + except FiscalYearError: + pass + docs = get_tds_details(accounts, fiscal_year) + for d in docs: try: doc = frappe.get_doc(d) @@ -650,11 +655,14 @@ def set_tax_withholding_category(company): if accounts: doc.append("accounts", accounts[0]) - # if fiscal year don't match with any of the already entered data, append rate row - fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year] - if not fy_exist: - doc.append("rates", d.get('rates')[0]) - + if fiscal_year: + # if fiscal year don't match with any of the already entered data, append rate row + fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year] + if not fy_exist: + doc.append("rates", d.get('rates')[0]) + + doc.flags.ignore_permissions = True + doc.flags.ignore_mandatory = True doc.save() def set_tds_account(docs, company): diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js index 87baece65d..f09d3d08ad 100644 --- a/erpnext/regional/india/taxes.js +++ b/erpnext/regional/india/taxes.js @@ -40,14 +40,12 @@ erpnext.setup_auto_gst_taxation = (doctype) => { callback: function(r) { if(r.message) { frm.set_value('taxes_and_charges', r.message.taxes_and_charges); + frm.set_value('taxes', r.message.taxes); frm.set_value('place_of_supply', r.message.place_of_supply); - } else if (frm.doc.is_internal_supplier || frm.doc.is_internal_customer) { - frm.set_value('taxes_and_charges', ''); - frm.set_value('taxes', []); } } }); } }); -}; +} diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 0d8263835d..e89885f380 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -171,7 +171,7 @@ def get_regional_address_details(party_details, doctype, company): if is_internal_transfer(party_details, doctype): party_details.taxes_and_charges = '' - party_details.taxes = '' + party_details.taxes = [] return party_details if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): diff --git a/erpnext/regional/print_format/irs_1099_form/irs_1099_form.json b/erpnext/regional/print_format/irs_1099_form/irs_1099_form.json index ce8c44a9a1..e59700f5a5 100644 --- a/erpnext/regional/print_format/irs_1099_form/irs_1099_form.json +++ b/erpnext/regional/print_format/irs_1099_form/irs_1099_form.json @@ -1,23 +1,26 @@ -[ - { - "align_labels_right": 0, - "css": "", - "custom_format": 1, - "default_print_language": "en", - "disabled": 0, - "doc_type": "Supplier", - "docstatus": 0, - "doctype": "Print Format", - "font": "Default", - "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"
\\t\\t\\t\\t

TAX Invoice
{{ doc.name }}\\t\\t\\t\\t

\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"customer_name\", \"label\": \"Customer Name\"}, {\"print_hide\": 0, \"fieldname\": \"customer_name_in_arabic\", \"label\": \"Customer Name in Arabic\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"posting_date\", \"label\": \"Date\"}, {\"fieldtype\": \"Section Break\", \"label\": \"Address\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"company\", \"label\": \"Company\"}, {\"print_hide\": 0, \"fieldname\": \"company_trn\", \"label\": \"Company TRN\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"company_address_display\", \"label\": \"Company Address\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"visible_columns\": [{\"print_hide\": 0, \"fieldname\": \"item_code\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"description\", \"print_width\": \"200px\"}, {\"print_hide\": 0, \"fieldname\": \"uom\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"tax_code\", \"print_width\": \"\"}], \"print_hide\": 0, \"fieldname\": \"items\", \"label\": \"Items\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"total\", \"label\": \"Total\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"visible_columns\": [{\"print_hide\": 0, \"fieldname\": \"charge_type\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"row_id\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"account_head\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"cost_center\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"description\", \"print_width\": \"300px\"}, {\"print_hide\": 0, \"fieldname\": \"rate\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"tax_amount\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"total\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"tax_amount_after_discount_amount\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"base_tax_amount\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"base_total\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"base_tax_amount_after_discount_amount\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"item_wise_tax_detail\", \"print_width\": \"\"}], \"print_hide\": 0, \"fieldname\": \"taxes\", \"label\": \"Sales Taxes and Charges\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"grand_total\", \"label\": \"Grand Total\"}, {\"print_hide\": 0, \"fieldname\": \"rounded_total\", \"label\": \"Rounded Total\"}, {\"print_hide\": 0, \"fieldname\": \"in_words\", \"align\": \"left\", \"label\": \"In Words\"}]", - "html": "
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n\n \n
PAYER'S name, street address, city or town, state or province, country, ZIP
or foreign postal code, and telephone no.
\n\t{{company if company else \"\"}}
\n\t{{payer_street_address if payer_street_address else \"\"}}\n
1 RentsOMB No. 1545-0115
2018
Form 1099-MISC
Miscellaneous Income
2 Royalties
3 Other Income
\n\t{{payments if payments else \"\"}}\n\t
4 Federal Income tax withheldCopy A
For
Internal Revenue
Service Center

File with Form 1096
PAYER'S TIN
\n\t{{company_tin if company_tin else \"\"}}\n\t
RECIPIENT'S TIN

\n {{tax_id if tax_id else \"None\"}}\n
Fishing boat proceeds6 Medical and health care payments
RECIPIENT'S name
\n {{supplier if supplier else \"\"}}\n
7 Nonemployee compensation
\n\t
Substitute payments in lieu of dividends or interestFor Privacy Act
and Paperwork
Reduction Act
Notice, see the
2018 General
Instructions for
Certain
Information
Returns.
Street address (including apt. no.)
\n\t{{recipient_street_address if recipient_street_address else \"\"}}\n\t
$___________$___________
9 Payer made direct sales of
$5,000 or more of consumer products
to a buyer
(recipient) for resale
10 Crop insurance proceeds
City or town, state or province, country, and ZIP or foreign postal code
\n\t{{recipient_city_state if recipient_city_state else \"\"}}\n
$___________
1112
Account number (see instructions)FACTA filing
requirement
2nd TIN not.13 Excess golden parachute payments
$___________
14 Gross proceeds paid to an
attorney
$___________
15a Section 409A deferrals15b Section 409 income16 State tax withheld17 State/Payer's state no.18 State income
$$$$
Form 1099-MISC Cat. No. 14425J www.irs.gov/Form1099MISC Department of the Treasury - Internal Revenue Service
\n
\n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n {{supplier if supplier else \"\"}}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n\n \n
PAYER'S name, street address, city or town, state or province, country, ZIP
or foreign postal code, and telephone no.
\n {{company if company else \"\"}}
\n \t{{payer_street_address if payer_street_address else \"\"}}
1 RentsOMB No. 1545-0115
2018
Form 1099-MISC
Miscellaneous Income
2 Royalties
3 Other Income
\n\t{{payments if payments else \"\"}}\n\t
4 Federal Income tax withheldCopy 1
For State Tax
Department
PAYER'S TIN
\n\t{{company_tin if company_tin else \"\"}}\n\t
RECIPIENT'S TIN
\n\t{{tax_id if tax_id else \"\"}}\n\t
Fishing boat proceeds6 Medical and health care payments
RECIPIENT'S name7 Nonemployee compensation
\n\t
Substitute payments in lieu of dividends or interest
Street address (including apt. no.)
\n\t{{recipient_street_address if recipient_street_address else \"\"}}\n\t
$___________$___________
9 Payer made direct sales of
$5,000 or more of consumer products
to a buyer
(recipient) for resale
10 Crop insurance proceeds
City or town, state or province, country, and ZIP or foreign postal code
\n\t{{recipient_city_state if recipient_city_state else \"\"}}\n\t
$___________
1112
Account number (see instructions)FACTA filing
requirement
2nd TIN not.13 Excess golden parachute payments
$___________
14 Gross proceeds paid to an
attorney
$___________
15a Section 409A deferrals15b Section 409 income16 State tax withheld17 State/Payer's state no.18 State income
$$$$
Form 1099-MISC Cat. No. 14425J www.irs.gov/Form1099MISC Department of the Treasury - Internal Revenue Service
\n
\n", - "line_breaks": 0, - "modified": "2018-10-08 14:56:56.912851", - "module": "Regional", - "name": "IRS 1099 Form", - "print_format_builder": 1, - "print_format_type": "Server", - "show_section_headings": 0, - "standard": "No" - } -] +{ + "align_labels_right": 0, + "creation": "2020-11-09 16:01:26.096002", + "css": "", + "custom_format": 1, + "default_print_language": "en", + "disabled": 0, + "doc_type": "Supplier", + "docstatus": 0, + "doctype": "Print Format", + "font": "Default", + "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"
\\t\\t\\t\\t

TAX Invoice
{{ doc.name }}\\t\\t\\t\\t

\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"customer_name\", \"label\": \"Customer Name\"}, {\"print_hide\": 0, \"fieldname\": \"customer_name_in_arabic\", \"label\": \"Customer Name in Arabic\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"posting_date\", \"label\": \"Date\"}, {\"fieldtype\": \"Section Break\", \"label\": \"Address\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"company\", \"label\": \"Company\"}, {\"print_hide\": 0, \"fieldname\": \"company_trn\", \"label\": \"Company TRN\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"company_address_display\", \"label\": \"Company Address\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"visible_columns\": [{\"print_hide\": 0, \"fieldname\": \"item_code\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"description\", \"print_width\": \"200px\"}, {\"print_hide\": 0, \"fieldname\": \"uom\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"tax_code\", \"print_width\": \"\"}], \"print_hide\": 0, \"fieldname\": \"items\", \"label\": \"Items\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"total\", \"label\": \"Total\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"visible_columns\": [{\"print_hide\": 0, \"fieldname\": \"charge_type\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"row_id\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"account_head\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"cost_center\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"description\", \"print_width\": \"300px\"}, {\"print_hide\": 0, \"fieldname\": \"rate\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"tax_amount\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"total\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"tax_amount_after_discount_amount\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"base_tax_amount\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"base_total\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"base_tax_amount_after_discount_amount\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"item_wise_tax_detail\", \"print_width\": \"\"}], \"print_hide\": 0, \"fieldname\": \"taxes\", \"label\": \"Sales Taxes and Charges\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"grand_total\", \"label\": \"Grand Total\"}, {\"print_hide\": 0, \"fieldname\": \"rounded_total\", \"label\": \"Rounded Total\"}, {\"print_hide\": 0, \"fieldname\": \"in_words\", \"align\": \"left\", \"label\": \"In Words\"}]", + "html": "
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n\n \n
PAYER'S name, street address,\n city or town, state or province, country, ZIP
or foreign postal code, and telephone no.
\n {{ company or \"\" }}
\n {{ payer_street_address or \"\" }}\n
1 RentsOMB No. 1545-0115
\n {{ fiscal_year[:2] }}\n {{ fiscal_year[-2:] }}
Form 1099-MISC\n
Miscellaneous Income
2 Royalties
3 Other Income
{{ payments or \"\" }}
4 Federal Income tax withheldCopy A
For
Internal Revenue
Service\n Center

File with Form 1096
PAYER'S TIN
{{ company_tin or \"\" }}
RECIPIENT'S TIN

{{ tax_id or \"None\" }}
Fishing boat proceeds6 Medical and health care payments
RECIPIENT'S name
{{ supplier or \"\" }}
7 Nonemployee compensation
\n
Substitute payments in lieu of dividends or interestFor Privacy Act
and Paperwork
Reduction Act
Notice, see\n the
2018 General
Instructions for
Certain
Information
Returns.
Street address (including apt. no.)
\n {{ recipient_street_address or \"\" }}\n
$___________$___________
9 Payer made direct sales of
$5,000 or more of consumer\n products
to a buyer
(recipient) for resale
10 Crop insurance proceeds
City or town, state or province, country, and ZIP or\n foreign postal code
\n {{ recipient_city_state or \"\" }}\n
$___________
1112
Account number (see instructions)FACTA filing
requirement
2nd TIN not.13 Excess golden parachute payments
$___________
14 Gross proceeds paid to an
attorney
$___________
15a Section 409A deferrals15b Section 409 income16 State tax withheld17 State/Payer's state no.18 State income
$$$$
Form 1099-MISC Cat. No. 14425J www.irs.gov/Form1099MISC Department of the\n Treasury - Internal Revenue Service
\n
\n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n {{ supplier or \"\" }}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n\n \n
PAYER'S name, street address,\n city or town, state or province, country, ZIP
or foreign postal code, and telephone no.
\n {{ company or \"\"}}\n {{ payer_street_address or \"\" }}\n
1 RentsOMB No. 1545-0115
\n {{ fiscal_year[:2] }}\n {{ fiscal_year[-2:] }}
Form 1099-MISC\n
Miscellaneous Income
2 Royalties
3 Other Income
\n {{ payments or \"\" }}\n
4 Federal Income tax withheldCopy 1
For State Tax
Department
PAYER'S TIN
\n {{ company_tin or \"\" }}\n
RECIPIENT'S TIN
\n {{ tax_id or \"\" }}\n
Fishing boat proceeds6 Medical and health care payments
RECIPIENT'S name7 Nonemployee compensation
\n
Substitute payments in lieu of dividends or interest
Street address (including apt. no.)
\n {{ recipient_street_address or \"\" }}\n
$___________$___________
9 Payer made direct sales of
$5,000 or more of consumer\n products
to a buyer
(recipient) for resale
10 Crop insurance proceeds
City or town, state or province, country, and ZIP or\n foreign postal code
\n {{ recipient_city_state or \"\" }}\n
$___________
1112
Account number (see instructions)FACTA filing
requirement
2nd TIN not.13 Excess golden parachute payments
$___________
14 Gross proceeds paid to an
attorney
$___________
15a Section 409A deferrals15b Section 409 income16 State tax withheld17 State/Payer's state no.18 State income
$$$$
Form 1099-MISC Cat. No. 14425J www.irs.gov/Form1099MISC Department of the\n Treasury - Internal Revenue Service
\n
\n", + "idx": 0, + "line_breaks": 0, + "modified": "2021-01-19 07:25:16.333666", + "modified_by": "Administrator", + "module": "Regional", + "name": "IRS 1099 Form", + "owner": "Administrator", + "print_format_builder": 1, + "print_format_type": "Jinja", + "raw_printing": 0, + "show_section_headings": 0, + "standard": "No" +} \ No newline at end of file diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 1e39c57786..cbc9478987 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -96,6 +96,8 @@ def execute(filters=None): """Entry point for frappe.""" data = [] if filters and validate(filters): + fn = 'temporary_against_account_number' + filters[fn] = frappe.get_value('DATEV Settings', filters.get('company'), fn) data = get_transactions(filters, as_dict=0) return COLUMNS, data @@ -156,11 +158,11 @@ def get_transactions(filters, as_dict=1): case gl.debit when 0 then 'H' else 'S' end as 'Soll/Haben-Kennzeichen', /* account number or, if empty, party account number */ - coalesce(acc.account_number, acc_pa.account_number) as 'Konto', + acc.account_number as 'Konto', /* against number or, if empty, party against number */ - coalesce(acc_against.account_number, acc_against_pa.account_number) as 'Gegenkonto (ohne BU-Schlüssel)', - + %(temporary_against_account_number)s as 'Gegenkonto (ohne BU-Schlüssel)', + gl.posting_date as 'Belegdatum', gl.voucher_no as 'Belegfeld 1', LEFT(gl.remarks, 60) as 'Buchungstext', @@ -171,27 +173,10 @@ def get_transactions(filters, as_dict=1): FROM `tabGL Entry` gl - /* Statistisches Konto (Debitoren/Kreditoren) */ - left join `tabParty Account` pa - on gl.against = pa.parent - and gl.company = pa.company - /* Kontonummer */ left join `tabAccount` acc on gl.account = acc.name - /* Gegenkonto-Nummer */ - left join `tabAccount` acc_against - on gl.against = acc_against.name - - /* Statistische Kontonummer */ - left join `tabAccount` acc_pa - on pa.account = acc_pa.name - - /* Statistische Gegenkonto-Nummer */ - left join `tabAccount` acc_against_pa - on pa.account = acc_against_pa.name - WHERE gl.company = %(company)s AND DATE(gl.posting_date) >= %(from_date)s AND DATE(gl.posting_date) <= %(to_date)s @@ -347,7 +332,9 @@ def download_datev_csv(filters): coa = frappe.get_value('Company', company, 'chart_of_accounts') filters['skr'] = '04' if 'SKR04' in coa else ('03' if 'SKR03' in coa else '') - filters['account_number_length'] = frappe.get_value('DATEV Settings', company, 'account_number_length') + datev_settings = frappe.get_doc('DATEV Settings', company) + filters['account_number_length'] = datev_settings.account_number_length + filters['temporary_against_account_number'] = datev_settings.temporary_against_account_number transactions = get_transactions(filters) account_names = get_account_names(filters) diff --git a/erpnext/regional/report/datev/test_datev.py b/erpnext/regional/report/datev/test_datev.py index 9529923a73..59b878e94a 100644 --- a/erpnext/regional/report/datev/test_datev.py +++ b/erpnext/regional/report/datev/test_datev.py @@ -126,7 +126,8 @@ def make_datev_settings(company): "doctype": "DATEV Settings", "client": company.name, "client_number": "12345", - "consultant_number": "67890" + "consultant_number": "67890", + "temporary_against_account_number": "9999" }).insert() @@ -137,7 +138,8 @@ class TestDatev(TestCase): self.filters = { "company": self.company.name, "from_date": today(), - "to_date": today() + "to_date": today(), + "temporary_against_account_number": "9999" } make_datev_settings(self.company) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index ad3de5f398..96dc3f728d 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -255,15 +255,16 @@ class Gstr1Report(object): for item_code, tax_amounts in item_wise_tax_detail.items(): tax_rate = tax_amounts[0] - if cgst_or_sgst: - tax_rate *= 2 - if parent not in self.cgst_sgst_invoices: - self.cgst_sgst_invoices.append(parent) + if tax_rate: + if cgst_or_sgst: + tax_rate *= 2 + if parent not in self.cgst_sgst_invoices: + self.cgst_sgst_invoices.append(parent) - rate_based_dict = self.items_based_on_tax_rate\ - .setdefault(parent, {}).setdefault(tax_rate, []) - if item_code not in rate_based_dict: - rate_based_dict.append(item_code) + rate_based_dict = self.items_based_on_tax_rate\ + .setdefault(parent, {}).setdefault(tax_rate, []) + if item_code not in rate_based_dict: + rate_based_dict.append(item_code) except ValueError: continue if unidentified_gst_accounts: diff --git a/erpnext/regional/report/irs_1099/irs_1099.js b/erpnext/regional/report/irs_1099/irs_1099.js index 2d74652cfe..070ff43f78 100644 --- a/erpnext/regional/report/irs_1099/irs_1099.js +++ b/erpnext/regional/report/irs_1099/irs_1099.js @@ -4,7 +4,7 @@ frappe.query_reports["IRS 1099"] = { "filters": [ { - "fieldname":"company", + "fieldname": "company", "label": __("Company"), "fieldtype": "Link", "options": "Company", @@ -13,7 +13,7 @@ frappe.query_reports["IRS 1099"] = { "width": 80, }, { - "fieldname":"fiscal_year", + "fieldname": "fiscal_year", "label": __("Fiscal Year"), "fieldtype": "Link", "options": "Fiscal Year", @@ -22,7 +22,7 @@ frappe.query_reports["IRS 1099"] = { "width": 80, }, { - "fieldname":"supplier_group", + "fieldname": "supplier_group", "label": __("Supplier Group"), "fieldtype": "Link", "options": "Supplier Group", @@ -32,16 +32,16 @@ frappe.query_reports["IRS 1099"] = { }, ], - onload: function(query_report) { + onload: function (query_report) { query_report.page.add_inner_button(__("Print IRS 1099 Forms"), () => { build_1099_print(query_report); }); } }; -function build_1099_print(query_report){ +function build_1099_print(query_report) { let filters = JSON.stringify(query_report.get_values()); let w = window.open('/api/method/erpnext.regional.report.irs_1099.irs_1099.irs_1099_print?' + - '&filters=' + encodeURIComponent(filters)); + '&filters=' + encodeURIComponent(filters)); // w.print(); } diff --git a/erpnext/regional/report/irs_1099/irs_1099.py b/erpnext/regional/report/irs_1099/irs_1099.py index d3509e500f..c1c8aedc9f 100644 --- a/erpnext/regional/report/irs_1099/irs_1099.py +++ b/erpnext/regional/report/irs_1099/irs_1099.py @@ -1,29 +1,34 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe import json -from frappe import _, _dict -from frappe.utils import nowdate -from frappe.utils.data import fmt_money -from erpnext.accounts.utils import get_fiscal_year + from PyPDF2 import PdfFileWriter + +import frappe +from erpnext.accounts.utils import get_fiscal_year +from frappe import _ +from frappe.utils import cstr, nowdate +from frappe.utils.data import fmt_money +from frappe.utils.jinja import render_template from frappe.utils.pdf import get_pdf from frappe.utils.print_format import read_multi_pdf -from frappe.utils.jinja import render_template + +IRS_1099_FORMS_FILE_EXTENSION = ".pdf" def execute(filters=None): - filters = filters if isinstance(filters, _dict) else _dict(filters) - + filters = filters if isinstance(filters, frappe._dict) else frappe._dict(filters) if not filters: filters.setdefault('fiscal_year', get_fiscal_year(nowdate())[0]) filters.setdefault('company', frappe.db.get_default("company")) - region = frappe.db.get_value("Company", fieldname = ["country"], filters = { "name": filters.company }) + region = frappe.db.get_value("Company", + filters={"name": filters.company}, + fieldname=["country"]) + if region != 'United States': - return [],[] + return [], [] data = [] columns = get_columns() @@ -34,20 +39,23 @@ def execute(filters=None): s.tax_id as "tax_id", SUM(gl.debit_in_account_currency) AS "payments" FROM - `tabGL Entry` gl INNER JOIN `tabSupplier` s + `tabGL Entry` gl + INNER JOIN `tabSupplier` s WHERE s.name = gl.party - AND s.irs_1099 = 1 - AND gl.fiscal_year = %(fiscal_year)s - AND gl.party_type = "Supplier" - + AND s.irs_1099 = 1 + AND gl.fiscal_year = %(fiscal_year)s + AND gl.party_type = "Supplier" GROUP BY gl.party - ORDER BY - gl.party DESC""", {"fiscal_year": filters.fiscal_year, + gl.party DESC + """, { + "fiscal_year": filters.fiscal_year, "supplier_group": filters.supplier_group, - "company": filters.company}, as_dict=True) + "company": filters.company + }, as_dict=True) + return columns, data @@ -74,7 +82,6 @@ def get_columns(): "width": 120 }, { - "fieldname": "payments", "label": _("Total Payments"), "fieldtype": "Currency", @@ -88,23 +95,32 @@ def irs_1099_print(filters): if not filters: frappe._dict({ "company": frappe.db.get_default("Company"), - "fiscal_year": frappe.db.get_default("fiscal_year")}) + "fiscal_year": frappe.db.get_default("Fiscal Year") + }) else: filters = frappe._dict(json.loads(filters)) + + fiscal_year_doc = get_fiscal_year(fiscal_year=filters.fiscal_year, as_dict=True) + fiscal_year = cstr(fiscal_year_doc.year_start_date.year) + company_address = get_payer_address_html(filters.company) company_tin = frappe.db.get_value("Company", filters.company, "tax_id") + columns, data = execute(filters) template = frappe.get_doc("Print Format", "IRS 1099 Form").html output = PdfFileWriter() + for row in data: + row["fiscal_year"] = fiscal_year row["company"] = filters.company row["company_tin"] = company_tin row["payer_street_address"] = company_address - row["recipient_street_address"], row["recipient_city_state"] = get_street_address_html("Supplier", row.supplier) + row["recipient_street_address"], row["recipient_city_state"] = get_street_address_html( + "Supplier", row.supplier) row["payments"] = fmt_money(row["payments"], precision=0, currency="USD") - frappe._dict(row) pdf = get_pdf(render_template(template, row), output=output if output else None) - frappe.local.response.filename = filters.fiscal_year + " " + filters.company + " IRS 1099 Forms" + + frappe.local.response.filename = f"{filters.fiscal_year} {filters.company} IRS 1099 Forms{IRS_1099_FORMS_FILE_EXTENSION}" frappe.local.response.filecontent = read_multi_pdf(output) frappe.local.response.type = "download" @@ -120,36 +136,45 @@ def get_payer_address_html(company): ORDER BY address_type="Postal" DESC, address_type="Billing" DESC LIMIT 1 - """, {"company": company}, as_dict=True) + """, {"company": company}, as_dict=True) + + address_display = "" if address_list: company_address = address_list[0]["name"] - return frappe.get_doc("Address", company_address).get_display() - else: - return "" + address_display = frappe.get_doc("Address", company_address).get_display() + + return address_display def get_street_address_html(party_type, party): address_list = frappe.db.sql(""" SELECT link.parent - FROM `tabDynamic Link` link, `tabAddress` address - WHERE link.parenttype = "Address" - AND link.link_name = %(party)s - ORDER BY address.address_type="Postal" DESC, + FROM + `tabDynamic Link` link, + `tabAddress` address + WHERE + link.parenttype = "Address" + AND link.link_name = %(party)s + ORDER BY + address.address_type="Postal" DESC, address.address_type="Billing" DESC LIMIT 1 - """, {"party": party}, as_dict=True) + """, {"party": party}, as_dict=True) + + street_address = city_state = "" if address_list: supplier_address = address_list[0]["parent"] doc = frappe.get_doc("Address", supplier_address) + if doc.address_line2: - street = doc.address_line1 + "
\n" + doc.address_line2 + "
\n" + street_address = doc.address_line1 + "
\n" + doc.address_line2 + "
\n" else: - street = doc.address_line1 + "
\n" - city = doc.city + ", " if doc.city else "" - city = city + doc.state + " " if doc.state else city - city = city + doc.pincode if doc.pincode else city - city += "
\n" - return street, city - else: - return "", "" + street_address = doc.address_line1 + "
\n" + + city_state = doc.city + ", " if doc.city else "" + city_state = city_state + doc.state + " " if doc.state else city_state + city_state = city_state + doc.pincode if doc.pincode else city_state + city_state += "
\n" + + return street_address, city_state diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 29214ee06d..bf8b7fc128 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -84,7 +84,10 @@ class Customer(TransactionBase): frappe.throw(_("{0} is not a company bank account").format(frappe.bold(self.default_bank_account))) def validate_internal_customer(self): - if self.is_internal_customer and frappe.db.get_value('Customer', {"represents_company": self.represents_company}, "name"): + internal_customer = frappe.db.get_value("Customer", + {"is_internal_customer": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name") + + if internal_customer: frappe.throw(_("Internal Customer for company {0} already exists").format( frappe.bold(self.represents_company))) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index d4fb07cc27..78f9df9588 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -171,8 +171,10 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( this.frm.add_custom_button(__('Request for Raw Materials'), () => this.make_raw_material_request(), __('Create')); } - // make purchase order + // Make Purchase Order + if (!this.frm.doc.is_internal_customer) { this.frm.add_custom_button(__('Purchase Order'), () => this.make_purchase_order(), __('Create')); + } // maintenance if(flt(doc.per_delivered, 2) < 100 && (order_is_maintenance || order_is_a_custom_sale)) { @@ -193,16 +195,15 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( if (doc.docstatus === 1 && !doc.inter_company_order_reference) { let me = this; - frappe.model.with_doc("Customer", me.frm.doc.customer, () => { - let customer = frappe.model.get_doc("Customer", me.frm.doc.customer); - let internal = customer.is_internal_customer; - let disabled = customer.disabled; - if (internal === 1 && disabled === 0) { - me.frm.add_custom_button("Inter Company Order", function() { - me.make_inter_company_order(); - }, __('Create')); - } - }); + let internal = me.frm.doc.is_internal_customer; + if (internal) { + let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Purchase Order" : + "Inter Company Purchase Order"; + + me.frm.add_custom_button(button_label, function() { + me.make_inter_company_order(); + }, __('Create')); + } } } // payment request diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 3d64ac3780..0a5c6651ba 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -107,6 +107,8 @@ "tc_name", "terms", "more_info", + "is_internal_customer", + "represents_company", "inter_company_order_reference", "project", "party_account_currency", @@ -1103,7 +1105,8 @@ "hide_days": 1, "hide_seconds": 1, "label": "Inter Company Order Reference", - "options": "Purchase Order" + "options": "Purchase Order", + "read_only": 1 }, { "description": "Track this Sales Order against any Project", @@ -1455,13 +1458,29 @@ "hide_seconds": 1, "label": "Skip Delivery Note", "print_hide": 1 + }, + { + "default": "0", + "fetch_from": "customer.is_internal_customer", + "fieldname": "is_internal_customer", + "fieldtype": "Check", + "label": "Is Internal Customer", + "read_only": 1 + }, + { + "fetch_from": "customer.represents_company", + "fieldname": "represents_company", + "fieldtype": "Link", + "label": "Represents Company", + "options": "Company", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2020-10-30 13:59:18.628077", + "modified": "2021-01-20 23:40:39.929296", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 7f00fca8f0..ce084646e1 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -399,6 +399,10 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ } }, + batch_no: function(doc, cdt, cdn) { + this._super(doc, cdt, cdn); + }, + qty: function(doc, cdt, cdn) { this._super(doc, cdt, cdn); diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js index 9bd03d45cb..d3c442d303 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -24,6 +24,16 @@ erpnext.stock.ItemDashboard = Class.extend({ handle_move_add($(this), "Add") }); + this.content.on('click', '.btn-edit', function() { + let item = unescape($(this).attr('data-item')); + let warehouse = unescape($(this).attr('data-warehouse')); + let company = unescape($(this).attr('data-company')); + frappe.db.get_value('Putaway Rule', + {'item_code': item, 'warehouse': warehouse, 'company': company}, 'name', (r) => { + frappe.set_route("Form", "Putaway Rule", r.name); + }); + }); + function handle_move_add(element, action) { let item = unescape(element.attr('data-item')); let warehouse = unescape(element.attr('data-warehouse')); @@ -59,7 +69,7 @@ erpnext.stock.ItemDashboard = Class.extend({ // more this.content.find('.btn-more').on('click', function() { - me.start += 20; + me.start += me.page_length; me.refresh(); }); @@ -69,33 +79,43 @@ erpnext.stock.ItemDashboard = Class.extend({ this.before_refresh(); } + let args = { + item_code: this.item_code, + warehouse: this.warehouse, + parent_warehouse: this.parent_warehouse, + item_group: this.item_group, + company: this.company, + start: this.start, + sort_by: this.sort_by, + sort_order: this.sort_order + }; + var me = this; frappe.call({ - method: 'erpnext.stock.dashboard.item_dashboard.get_data', - args: { - item_code: this.item_code, - warehouse: this.warehouse, - item_group: this.item_group, - start: this.start, - sort_by: this.sort_by, - sort_order: this.sort_order, - }, + method: this.method, + args: args, callback: function(r) { me.render(r.message); } }); }, render: function(data) { - if(this.start===0) { + if (this.start===0) { this.max_count = 0; this.result.empty(); } - var context = this.get_item_dashboard_data(data, this.max_count, true); + let context = ""; + if (this.page_name === "warehouse-capacity-summary") { + context = this.get_capacity_dashboard_data(data); + } else { + context = this.get_item_dashboard_data(data, this.max_count, true); + } + this.max_count = this.max_count; // show more button - if(data && data.length===21) { + if (data && data.length===(this.page_length + 1)) { this.content.find('.more').removeClass('hidden'); // remove the last element @@ -106,12 +126,17 @@ erpnext.stock.ItemDashboard = Class.extend({ // If not any stock in any warehouses provide a message to end user if (context.data.length > 0) { - $(frappe.render_template('item_dashboard_list', context)).appendTo(this.result); + this.content.find('.result').css('text-align', 'unset'); + $(frappe.render_template(this.template, context)).appendTo(this.result); } else { - var message = __("Currently no stock available in any warehouse"); - $(` ${message} `).appendTo(this.result); + var message = __("No Stock Available Currently"); + this.content.find('.result').css('text-align', 'center'); + + $(`
+ ${message}
`).appendTo(this.result); } }, + get_item_dashboard_data: function(data, max_count, show_item) { if(!max_count) max_count = 0; if(!data) data = []; @@ -128,8 +153,8 @@ erpnext.stock.ItemDashboard = Class.extend({ d.total_reserved, max_count); }); - var can_write = 0; - if(frappe.boot.user.can_write.indexOf("Stock Entry")>=0){ + let can_write = 0; + if (frappe.boot.user.can_write.indexOf("Stock Entry") >= 0) { can_write = 1; } @@ -138,9 +163,27 @@ erpnext.stock.ItemDashboard = Class.extend({ max_count: max_count, can_write:can_write, show_item: show_item || false + }; + }, + + get_capacity_dashboard_data: function(data) { + if (!data) data = []; + + data.forEach(function(d) { + d.color = d.percent_occupied >=80 ? "#f8814f" : "#2490ef"; + }); + + let can_write = 0; + if (frappe.boot.user.can_write.indexOf("Putaway Rule") >= 0) { + can_write = 1; } + + return { + data: data, + can_write: can_write, + }; } -}) +}); erpnext.stock.move_item = function(item, source, target, actual_qty, rate, callback) { var dialog = new frappe.ui.Dialog({ diff --git a/erpnext/stock/dashboard/warehouse_capacity_dashboard.py b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py new file mode 100644 index 0000000000..ab573e566a --- /dev/null +++ b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py @@ -0,0 +1,69 @@ +from __future__ import unicode_literals + +import frappe +from frappe.model.db_query import DatabaseQuery +from frappe.utils import nowdate +from frappe.utils import flt +from erpnext.stock.utils import get_stock_balance + +@frappe.whitelist() +def get_data(item_code=None, warehouse=None, parent_warehouse=None, + company=None, start=0, sort_by="stock_capacity", sort_order="desc"): + """Return data to render the warehouse capacity dashboard.""" + filters = get_filters(item_code, warehouse, parent_warehouse, company) + + no_permission, filters = get_warehouse_filter_based_on_permissions(filters) + if no_permission: + return [] + + capacity_data = get_warehouse_capacity_data(filters, start) + + asc_desc = -1 if sort_order == "desc" else 1 + capacity_data = sorted(capacity_data, key = lambda i: (i[sort_by] * asc_desc)) + + return capacity_data + +def get_filters(item_code=None, warehouse=None, parent_warehouse=None, + company=None): + filters = [['disable', '=', 0]] + if item_code: + filters.append(['item_code', '=', item_code]) + if warehouse: + filters.append(['warehouse', '=', warehouse]) + if company: + filters.append(['company', '=', company]) + if parent_warehouse: + lft, rgt = frappe.db.get_value("Warehouse", parent_warehouse, ["lft", "rgt"]) + warehouses = frappe.db.sql_list(""" + select name from `tabWarehouse` + where lft >=%s and rgt<=%s + """, (lft, rgt)) + filters.append(['warehouse', 'in', warehouses]) + return filters + +def get_warehouse_filter_based_on_permissions(filters): + try: + # check if user has any restrictions based on user permissions on warehouse + if DatabaseQuery('Warehouse', user=frappe.session.user).build_match_conditions(): + filters.append(['warehouse', 'in', [w.name for w in frappe.get_list('Warehouse')]]) + return False, filters + except frappe.PermissionError: + # user does not have access on warehouse + return True, [] + +def get_warehouse_capacity_data(filters, start): + capacity_data = frappe.db.get_all('Putaway Rule', + fields=['item_code', 'warehouse','stock_capacity', 'company'], + filters=filters, + limit_start=start, + limit_page_length='11' + ) + + for entry in capacity_data: + balance_qty = get_stock_balance(entry.item_code, entry.warehouse, nowdate()) or 0 + entry.update({ + 'actual_qty': balance_qty, + 'percent_occupied': flt((flt(balance_qty) / flt(entry.stock_capacity)) * 100, 0) + }) + + return capacity_data \ No newline at end of file diff --git a/erpnext/stock/desk_page/stock/stock.json b/erpnext/stock/desk_page/stock/stock.json index 9068e338c3..74cc42d1fb 100644 --- a/erpnext/stock/desk_page/stock/stock.json +++ b/erpnext/stock/desk_page/stock/stock.json @@ -8,12 +8,12 @@ { "hidden": 0, "label": "Stock Transactions", - "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Material Request\",\n \"name\": \"Material Request\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Entry\",\n \"name\": \"Stock Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"label\": \"Delivery Note\",\n \"name\": \"Delivery Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"label\": \"Purchase Receipt\",\n \"name\": \"Purchase Receipt\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Pick List\",\n \"name\": \"Pick List\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Shipment\",\n \"name\": \"Shipment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Delivery Trip\",\n \"name\": \"Delivery Trip\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Material Request\",\n \"name\": \"Material Request\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Entry\",\n \"name\": \"Stock Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"label\": \"Delivery Note\",\n \"name\": \"Delivery Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"label\": \"Purchase Receipt\",\n \"name\": \"Purchase Receipt\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Pick List\",\n \"name\": \"Pick List\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Putaway Rule\",\n \"name\": \"Putaway Rule\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Shipment\",\n \"name\": \"Shipment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Delivery Trip\",\n \"name\": \"Delivery Trip\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, "label": "Stock Reports", - "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Ledger\",\n \"name\": \"Stock Ledger\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Balance\",\n \"name\": \"Stock Balance\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Projected Qty\",\n \"name\": \"Stock Projected Qty\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Summary\",\n \"name\": \"stock-balance\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Ageing\",\n \"name\": \"Stock Ageing\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item Price Stock\",\n \"name\": \"Item Price Stock\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Ledger\",\n \"name\": \"Stock Ledger\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Balance\",\n \"name\": \"Stock Balance\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Projected Qty\",\n \"name\": \"Stock Projected Qty\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Summary\",\n \"name\": \"stock-balance\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Ageing\",\n \"name\": \"Stock Ageing\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item Price Stock\",\n \"name\": \"Item Price Stock\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Putaway Rule\"\n ],\n \"label\": \"Warehouse Capacity Summary\",\n \"name\": \"warehouse-capacity-summary\",\n \"type\": \"page\"\n }\n]" }, { "hidden": 0, @@ -58,7 +58,7 @@ "idx": 0, "is_standard": 1, "label": "Stock", - "modified": "2020-12-02 15:47:41.532942", + "modified": "2020-12-08 15:47:41.532942", "modified_by": "Administrator", "module": "Stock", "name": "Stock", diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index e41f1a8aaa..97f85bafd9 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -8,6 +8,8 @@ import unittest from erpnext.stock.doctype.batch.batch import get_batch_qty, UnableToSelectBatchError, get_batch_no from frappe.utils import cint, flt +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.stock.get_item_details import get_item_details class TestBatch(unittest.TestCase): def test_item_has_batch_enabled(self): @@ -182,7 +184,7 @@ class TestBatch(unittest.TestCase): stock_entry.cancel() current_batch_qty = flt(frappe.db.get_value("Batch", "B100", "batch_qty")) self.assertEqual(current_batch_qty, existing_batch_qty) - + @classmethod def make_new_batch_and_entry(cls, item_name, batch_name, warehouse): '''Make a new stock entry for given target warehouse and batch name of item''' @@ -252,6 +254,72 @@ class TestBatch(unittest.TestCase): return batch + def test_batch_wise_item_price(self): + if not frappe.db.get_value('Item', '_Test Batch Price Item'): + frappe.get_doc({ + 'doctype': 'Item', + 'is_stock_item': 1, + 'item_code': '_Test Batch Price Item', + 'item_group': 'Products', + 'has_batch_no': 1, + 'create_new_batch': 1 + }).insert(ignore_permissions=True) + + batch1 = create_batch('_Test Batch Price Item', 200, 1) + batch2 = create_batch('_Test Batch Price Item', 300, 1) + batch3 = create_batch('_Test Batch Price Item', 400, 0) + + args = frappe._dict({ + "item_code": "_Test Batch Price Item", + "company": "_Test Company with perpetual inventory", + "price_list": "_Test Price List", + "currency": "_Test Currency", + "doctype": "Sales Invoice", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "customer": "_Test Customer", + "name": None + }) + + #test price for batch1 + args.update({'batch_no': batch1}) + details = get_item_details(args) + self.assertEqual(details.get('price_list_rate'), 200) + + #test price for batch2 + args.update({'batch_no': batch2}) + details = get_item_details(args) + self.assertEqual(details.get('price_list_rate'), 300) + + #test price for batch3 + args.update({'batch_no': batch3}) + details = get_item_details(args) + self.assertEqual(details.get('price_list_rate'), 400) + +def create_batch(item_code, rate, create_item_price_for_batch): + pi = make_purchase_invoice(company="_Test Company with perpetual inventory", + warehouse= "Stores - TCP1", cost_center = "Main - TCP1", update_stock=1, + expense_account ="_Test Account Cost for Goods Sold - TCP1", item_code=item_code) + + batch = frappe.db.get_value('Batch', {'item': item_code, 'reference_name': pi.name}) + + if not create_item_price_for_batch: + create_price_list_for_batch(item_code, None, rate) + else: + create_price_list_for_batch(item_code, batch, rate) + + return batch + +def create_price_list_for_batch(item_code, batch, rate): + frappe.get_doc({ + 'doctype': 'Item Price', + 'item_code': '_Test Batch Price Item', + 'price_list': '_Test Price List', + 'batch_no': batch, + 'price_list_rate': rate + }).insert() + def make_new_batch(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index cb1e31b15b..334bdeac9d 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -7,6 +7,7 @@ cur_frm.add_fetch('customer', 'tax_id', 'tax_id'); frappe.provide("erpnext.stock"); frappe.provide("erpnext.stock.delivery_note"); +frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on("Delivery Note", { setup: function(frm) { @@ -76,7 +77,7 @@ frappe.ui.form.on("Delivery Note", { } }); - + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, print_without_amount: function(frm) { @@ -94,13 +95,19 @@ frappe.ui.form.on("Delivery Note", { frm.page.set_inner_btn_group_as_primary(__('Create')); } - if (frm.doc.docstatus === 1 && frm.doc.is_internal_customer && !frm.doc.inter_company_reference) { - frm.add_custom_button(__('Purchase Receipt'), function() { - frappe.model.open_mapped_doc({ - method: 'erpnext.stock.doctype.delivery_note.delivery_note.make_inter_company_purchase_receipt', - frm: frm, - }) - }, __('Create')); + if (frm.doc.docstatus == 1 && !frm.doc.inter_company_reference) { + let internal = me.frm.doc.is_internal_customer; + if (internal) { + let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Purchase Receipt" : + "Inter Company Purchase Receipt"; + + me.frm.add_custom_button(button_label, function() { + frappe.model.open_mapped_doc({ + method: 'erpnext.stock.doctype.delivery_note.delivery_note.make_inter_company_purchase_receipt', + frm: frm, + }); + }, __('Create')); + } } } }); @@ -296,15 +303,6 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend( } }) }, - - to_warehouse: function() { - let packed_items_table = this.frm.doc["packed_items"]; - this.autofill_warehouse(this.frm.doc["items"], "target_warehouse", this.frm.doc.to_warehouse); - if (packed_items_table && packed_items_table.length) { - this.autofill_warehouse(packed_items_table, "target_warehouse", this.frm.doc.to_warehouse); - } - } - }); $.extend(cur_frm.cscript, new erpnext.stock.DeliveryNoteController({frm: cur_frm})); @@ -318,6 +316,7 @@ frappe.ui.form.on('Delivery Note', { company: function(frm) { frm.trigger("unhide_account_head"); + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, unhide_account_head: function(frm) { diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index c9f8d0810e..f595aade91 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -53,7 +53,7 @@ "sec_warehouse", "set_warehouse", "col_break_warehouse", - "to_warehouse", + "set_target_warehouse", "items_section", "scan_barcode", "items", @@ -117,6 +117,7 @@ "source", "column_break5", "is_internal_customer", + "represents_company", "inter_company_reference", "per_billed", "customer_group", @@ -502,18 +503,6 @@ "fieldname": "col_break_warehouse", "fieldtype": "Column Break" }, - { - "description": "Required only for sample item.", - "fieldname": "to_warehouse", - "fieldtype": "Link", - "in_standard_filter": 1, - "label": "To Warehouse", - "no_copy": 1, - "oldfieldname": "to_warehouse", - "oldfieldtype": "Link", - "options": "Warehouse", - "print_hide": 1 - }, { "fieldname": "items_section", "fieldtype": "Section Break", @@ -1261,13 +1250,34 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "depends_on": "eval: doc.is_internal_customer", + "fieldname": "set_target_warehouse", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Set Target Warehouse", + "no_copy": 1, + "oldfieldname": "to_warehouse", + "oldfieldtype": "Link", + "options": "Warehouse", + "print_hide": 1 + }, + { + "description": "Company which internal customer represents.", + "fetch_from": "customer.represents_company", + "fieldname": "represents_company", + "fieldtype": "Link", + "label": "Represents Company", + "options": "Company", + "read_only": 1 } ], "icon": "fa fa-truck", "idx": 146, "is_submittable": 1, "links": [], - "modified": "2020-11-30 12:54:45.407289", + "modified": "2020-12-26 17:07:59.194403", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index a30cadf0a0..fa5a7fbe71 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -664,7 +664,8 @@ def make_inter_company_purchase_receipt(source_name, target_doc=None): return make_inter_company_transaction("Delivery Note", source_name, target_doc) def make_inter_company_transaction(doctype, source_name, target_doc=None): - from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_transaction, get_inter_company_details + from erpnext.accounts.doctype.sales_invoice.sales_invoice import (validate_inter_company_transaction, + get_inter_company_details, update_address, update_taxes, set_purchase_references) if doctype == 'Delivery Note': source_doc = frappe.get_doc(doctype, source_name) @@ -682,6 +683,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): def set_missing_values(source, target): target.run_method("set_missing_values") + set_purchase_references(target) if target.doctype == 'Purchase Receipt': master_doctype = 'Purchase Taxes and Charges Template' @@ -697,21 +699,35 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): if target_doc.doctype == 'Purchase Receipt': target_doc.company = details.get("company") target_doc.supplier = details.get("party") - target_doc.supplier_address = source_doc.company_address - target_doc.shipping_address = source_doc.shipping_address_name or source_doc.customer_address target_doc.buying_price_list = source_doc.selling_price_list target_doc.is_internal_supplier = 1 target_doc.inter_company_reference = source_doc.name + + # Invert the address on target doc creation + update_address(target_doc, 'supplier_address', 'address_display', source_doc.company_address) + update_address(target_doc, 'shipping_address', 'shipping_address_display', source_doc.customer_address) + + update_taxes(target_doc, party=target_doc.supplier, party_type='Supplier', company=target_doc.company, + doctype=target_doc.doctype, party_address=target_doc.supplier_address, + company_address=target_doc.shipping_address) else: target_doc.company = details.get("company") target_doc.customer = details.get("party") target_doc.company_address = source_doc.supplier_address - target_doc.shipping_address_name = source_doc.shipping_address target_doc.selling_price_list = source_doc.buying_price_list target_doc.is_internal_customer = 1 target_doc.inter_company_reference = source_doc.name - doclist = get_mapped_doc(doctype, source_name, { + # Invert the address on target doc creation + update_address(target_doc, 'company_address', 'company_address_display', source_doc.supplier_address) + update_address(target_doc, 'shipping_address_name', 'shipping_address', source_doc.shipping_address) + update_address(target_doc, 'customer_address', 'address_display', source_doc.shipping_address) + + update_taxes(target_doc, party=target_doc.customer, party_type='Customer', company=target_doc.company, + doctype=target_doc.doctype, party_address=target_doc.customer_address, + company_address=target_doc.company_address, shipping_address_name=target_doc.shipping_address_name) + + doclist = get_mapped_doc(doctype, source_name, { doctype: { "doctype": target_doctype, "postprocess": update_details, @@ -722,7 +738,10 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): doctype +" Item": { "doctype": target_doctype + " Item", "field_map": { - source_document_warehouse_field: target_document_warehouse_field + source_document_warehouse_field: target_document_warehouse_field, + 'name': 'delivery_note_item', + 'batch_no': 'batch_no', + 'serial_no': 'serial_no' }, "field_no_map": [ "warehouse" diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index 4bbf3de594..9de088df0e 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -458,7 +458,7 @@ "fieldname": "warehouse", "fieldtype": "Link", "in_list_view": 1, - "label": "From Warehouse", + "label": "Warehouse", "oldfieldname": "warehouse", "oldfieldtype": "Link", "options": "Warehouse", @@ -467,11 +467,12 @@ "width": "100px" }, { + "depends_on": "eval:parent.is_internal_customer", "fieldname": "target_warehouse", "fieldtype": "Link", "hidden": 1, "ignore_user_permissions": 1, - "label": "Customer Warehouse (Optional)", + "label": "Target Warehouse", "no_copy": 1, "options": "Warehouse", "print_hide": 1 @@ -748,7 +749,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-12-07 19:59:27.119856", + "modified": "2020-12-26 17:31:27.029803", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index faeeb578fe..ec32b0f044 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -384,7 +384,10 @@ $.extend(erpnext.item, { ' + __("Stock Levels") + ''); erpnext.item.item_dashboard = new erpnext.stock.ItemDashboard({ parent: section, - item_code: frm.doc.name + item_code: frm.doc.name, + page_length: 20, + method: 'erpnext.stock.dashboard.item_dashboard.get_data', + template: 'item_dashboard_list' }); erpnext.item.item_dashboard.refresh(); }); diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index d07b3dc4fe..fcf7c2608e 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -106,9 +106,9 @@ "item_tax_section_break", "taxes", "inspection_criteria", + "quality_inspection_template", "inspection_required_before_purchase", "inspection_required_before_delivery", - "quality_inspection_template", "manufacturing", "default_bom", "is_sub_contracted_item", @@ -814,7 +814,6 @@ "label": "Inspection Required before Delivery" }, { - "depends_on": "eval:(doc.inspection_required_before_purchase || doc.inspection_required_before_delivery)", "fieldname": "quality_inspection_template", "fieldtype": "Link", "label": "Quality Inspection Template", @@ -1069,7 +1068,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 1, - "modified": "2020-08-07 14:24:58.384992", + "modified": "2021-01-25 20:49:50.222976", "modified_by": "Administrator", "module": "Stock", "name": "Item", @@ -1131,4 +1130,4 @@ "sort_order": "DESC", "title_field": "item_name", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/stock/doctype/item_price/item_price.js b/erpnext/stock/doctype/item_price/item_price.js index 2729f4b15e..e4db0480db 100644 --- a/erpnext/stock/doctype/item_price/item_price.js +++ b/erpnext/stock/doctype/item_price/item_price.js @@ -15,5 +15,13 @@ frappe.ui.form.on("Item Price", { frm.set_df_property("bulk_import_help", "options", '' + __("Import in Bulk") + ''); + + frm.set_query('batch_no', function() { + return { + filters: { + 'item': frm.doc.item_code + } + } + }); } }); diff --git a/erpnext/stock/doctype/item_price/item_price.json b/erpnext/stock/doctype/item_price/item_price.json index 5f62381f8b..83177b372a 100644 --- a/erpnext/stock/doctype/item_price/item_price.json +++ b/erpnext/stock/doctype/item_price/item_price.json @@ -18,6 +18,7 @@ "price_list", "customer", "supplier", + "batch_no", "column_break_3", "buying", "selling", @@ -47,31 +48,41 @@ "oldfieldtype": "Select", "options": "Item", "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "uom", "fieldtype": "Link", "label": "UOM", - "options": "UOM" + "options": "UOM", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "description": "Quantity that must be bought or sold per UOM", "fieldname": "packing_unit", "fieldtype": "Int", - "label": "Packing Unit" + "label": "Packing Unit", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_17", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "item_name", "fieldtype": "Data", "in_list_view": 1, "label": "Item Name", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fetch_from": "item_code.brand", @@ -79,19 +90,25 @@ "fieldtype": "Read Only", "in_list_view": 1, "label": "Brand", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "item_description", "fieldtype": "Text", "label": "Item Description", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "price_list_details", "fieldtype": "Section Break", "label": "Price List", - "options": "fa fa-tags" + "options": "fa fa-tags", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "price_list", @@ -100,7 +117,9 @@ "in_standard_filter": 1, "label": "Price List", "options": "Price List", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -108,37 +127,49 @@ "fieldname": "customer", "fieldtype": "Link", "label": "Customer", - "options": "Customer" + "options": "Customer", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.buying == 1", "fieldname": "supplier", "fieldtype": "Link", "label": "Supplier", - "options": "Supplier" + "options": "Supplier", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_3", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "buying", "fieldtype": "Check", "label": "Buying", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "selling", "fieldtype": "Check", "label": "Selling", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "item_details", "fieldtype": "Section Break", - "options": "fa fa-tag" + "options": "fa fa-tag", + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -146,11 +177,15 @@ "fieldtype": "Link", "label": "Currency", "options": "Currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_br_1", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "price_list_rate", @@ -162,53 +197,80 @@ "oldfieldname": "ref_rate", "oldfieldtype": "Currency", "options": "currency", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_15", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "Today", "fieldname": "valid_from", "fieldtype": "Date", - "label": "Valid From" + "label": "Valid From", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "lead_time_days", "fieldtype": "Int", - "label": "Lead Time in days" + "label": "Lead Time in days", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_18", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "valid_upto", "fieldtype": "Date", - "label": "Valid Upto" + "label": "Valid Upto", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_24", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "note", "fieldtype": "Text", - "label": "Note" + "label": "Note", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "reference", "fieldtype": "Data", "in_list_view": 1, - "label": "Reference" + "label": "Reference", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "batch_no", + "fieldtype": "Link", + "label": "Batch No", + "options": "Batch", + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-flag", "idx": 1, + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-07-06 22:31:32.943475", + "modified": "2020-12-08 18:12:15.395772", "modified_by": "Administrator", "module": "Stock", "name": "Item Price", diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index bed5ea9ab6..e82a19b0dc 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -54,7 +54,8 @@ class ItemPrice(Document): "valid_upto", "packing_unit", "customer", - "supplier",]: + "supplier", + "batch_no"]: if self.get(field): conditions += " and {0} = %({0})s ".format(field) else: @@ -68,7 +69,7 @@ class ItemPrice(Document): self.as_dict(),) if price_list_rate: - frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty, and Dates."), ItemPriceDuplicateItem,) + frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, Batch, UOM, Qty, and Dates."), ItemPriceDuplicateItem,) def before_save(self): if self.selling: diff --git a/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json b/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json index b24d621c31..c77b993167 100644 --- a/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json +++ b/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2013-02-22 01:28:02", "doctype": "DocType", "document_type": "Document", @@ -29,6 +30,8 @@ "options": "Item", "read_only": 1, "reqd": 1, + "show_days": 1, + "show_seconds": 1, "width": "100px" }, { @@ -41,6 +44,8 @@ "print_width": "300px", "read_only": 1, "reqd": 1, + "show_days": 1, + "show_seconds": 1, "width": "120px" }, { @@ -50,7 +55,9 @@ "no_copy": 1, "options": "Purchase Invoice\nPurchase Receipt", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "receipt_document", @@ -59,25 +66,33 @@ "no_copy": 1, "options": "receipt_document_type", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_break2", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "qty", "fieldtype": "Float", "in_list_view": 1, "label": "Qty", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "rate", "fieldtype": "Currency", "label": "Rate", "options": "Company:company:default_currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "amount", @@ -88,14 +103,19 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "read_only": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "applicable_charges", "fieldtype": "Currency", "in_list_view": 1, "label": "Applicable Charges", - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", + "read_only_depends_on": "eval:parent.distribute_charges_based_on != 'Distribute Manually'", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "purchase_receipt_item", @@ -104,22 +124,30 @@ "label": "Purchase Receipt Item", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 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 }, { "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", @@ -128,12 +156,15 @@ "fieldtype": "Check", "hidden": 1, "label": "Is Fixed Asset", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 } ], "idx": 1, "istable": 1, - "modified": "2020-09-18 17:26:09.703215", + "links": [], + "modified": "2021-01-25 23:09:23.322282", "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Item", diff --git a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json index 64331c7d57..4fcdb4c10c 100644 --- a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json +++ b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json @@ -6,8 +6,11 @@ "engine": "InnoDB", "field_order": [ "expense_account", + "account_currency", + "exchange_rate", "description", "col_break3", + "base_amount", "amount" ], "fields": [ @@ -28,7 +31,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Amount", - "options": "Company:company:default_currency", + "options": "account_currency", "reqd": 1 }, { @@ -38,13 +41,33 @@ "in_list_view": 1, "label": "Expense Account", "mandatory_depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))", - "options": "Account", - "print_hide": 1 + "options": "Account" + }, + { + "fieldname": "account_currency", + "fieldtype": "Link", + "label": "Account Currency", + "options": "Currency", + "read_only": 1 + }, + { + "fieldname": "exchange_rate", + "fieldtype": "Float", + "label": "Exchange Rate", + "precision": "9" + }, + { + "fieldname": "base_amount", + "fieldtype": "Currency", + "label": "Base Amount", + "options": "Company:company:default_currency", + "read_only": 1 } ], + "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-12-04 00:22:14.373312", + "modified": "2020-12-26 01:07:23.233604", "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Taxes and Charges", diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js index 5de1352518..1abbc35334 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js @@ -1,6 +1,7 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt +{% include 'erpnext/stock/landed_taxes_and_charges_common.js' %}; frappe.provide("erpnext.stock"); @@ -29,20 +30,9 @@ erpnext.stock.LandedCostVoucher = erpnext.stock.StockController.extend({ this.frm.add_fetch("receipt_document", "supplier", "supplier"); this.frm.add_fetch("receipt_document", "posting_date", "posting_date"); this.frm.add_fetch("receipt_document", "base_grand_total", "grand_total"); - - this.frm.set_query("expense_account", "taxes", function() { - return { - query: "erpnext.controllers.queries.tax_account_query", - filters: { - "account_type": ["Tax", "Chargeable", "Income Account", "Expenses Included In Valuation", "Expenses Included In Asset Valuation"], - "company": me.frm.doc.company - } - }; - }); - }, - refresh: function(frm) { + refresh: function() { var help_content = `

@@ -72,6 +62,11 @@ erpnext.stock.LandedCostVoucher = erpnext.stock.StockController.extend({
`; set_field_options("landed_cost_help", help_content); + + if (this.frm.doc.company) { + let company_currency = frappe.get_doc(":Company", this.frm.doc.company).default_currency; + this.frm.set_currency_labels(["total_taxes_and_charges"], company_currency); + } }, get_items_from_purchase_receipts: function() { @@ -97,34 +92,36 @@ erpnext.stock.LandedCostVoucher = erpnext.stock.StockController.extend({ set_total_taxes_and_charges: function() { var total_taxes_and_charges = 0.0; $.each(this.frm.doc.taxes || [], function(i, d) { - total_taxes_and_charges += flt(d.amount) + total_taxes_and_charges += flt(d.base_amount); }); - cur_frm.set_value("total_taxes_and_charges", total_taxes_and_charges); + this.frm.set_value("total_taxes_and_charges", total_taxes_and_charges); }, set_applicable_charges_for_item: function() { var me = this; if(this.frm.doc.taxes.length) { - var total_item_cost = 0.0; var based_on = this.frm.doc.distribute_charges_based_on.toLowerCase(); - $.each(this.frm.doc.items || [], function(i, d) { - total_item_cost += flt(d[based_on]) - }); - var total_charges = 0.0; - $.each(this.frm.doc.items || [], function(i, item) { - item.applicable_charges = flt(item[based_on]) * flt(me.frm.doc.total_taxes_and_charges) / flt(total_item_cost) - item.applicable_charges = flt(item.applicable_charges, precision("applicable_charges", item)) - total_charges += item.applicable_charges - }); + if (based_on != 'distribute manually') { + $.each(this.frm.doc.items || [], function(i, d) { + total_item_cost += flt(d[based_on]) + }); - if (total_charges != this.frm.doc.total_taxes_and_charges){ - var diff = this.frm.doc.total_taxes_and_charges - flt(total_charges) - this.frm.doc.items.slice(-1)[0].applicable_charges += diff + var total_charges = 0.0; + $.each(this.frm.doc.items || [], function(i, item) { + item.applicable_charges = flt(item[based_on]) * flt(me.frm.doc.total_taxes_and_charges) / flt(total_item_cost) + item.applicable_charges = flt(item.applicable_charges, precision("applicable_charges", item)) + total_charges += item.applicable_charges + }); + + if (total_charges != this.frm.doc.total_taxes_and_charges){ + var diff = this.frm.doc.total_taxes_and_charges - flt(total_charges) + this.frm.doc.items.slice(-1)[0].applicable_charges += diff + } + refresh_field("items"); } - refresh_field("items"); } }, distribute_charges_based_on: function (frm) { @@ -134,7 +131,16 @@ erpnext.stock.LandedCostVoucher = erpnext.stock.StockController.extend({ items_remove: () => { this.trigger('set_applicable_charges_for_item'); } - }); cur_frm.script_manager.make(erpnext.stock.LandedCostVoucher); + +frappe.ui.form.on('Landed Cost Taxes and Charges', { + expense_account: function(frm, cdt, cdn) { + frm.events.set_account_currency(frm, cdt, cdn); + }, + + amount: function(frm, cdt, cdn) { + frm.events.set_base_amount(frm, cdt, cdn); + } +}); diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json index 01492807de..059f925184 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "naming_series:", "creation": "2014-07-11 11:33:42.547339", "doctype": "DocType", @@ -7,6 +8,9 @@ "field_order": [ "naming_series", "company", + "column_break_2", + "posting_date", + "section_break_5", "purchase_receipts", "purchase_receipt_items", "get_items_from_purchase_receipts", @@ -30,7 +34,9 @@ "options": "MAT-LCV-.YYYY.-", "print_hide": 1, "reqd": 1, - "set_only_once": 1 + "set_only_once": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "company", @@ -40,24 +46,32 @@ "label": "Company", "options": "Company", "remember_last_selected_value": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "purchase_receipts", "fieldtype": "Table", "label": "Purchase Receipts", "options": "Landed Cost Purchase Receipt", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "purchase_receipt_items", "fieldtype": "Section Break", - "label": "Purchase Receipt Items" + "label": "Purchase Receipt Items", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "get_items_from_purchase_receipts", "fieldtype": "Button", - "label": "Get Items From Purchase Receipts" + "label": "Get Items From Purchase Receipts", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "items", @@ -65,42 +79,56 @@ "label": "Purchase Receipt Items", "no_copy": 1, "options": "Landed Cost Item", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "sec_break1", "fieldtype": "Section Break", - "label": "Applicable Charges" + "label": "Applicable Charges", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes", "fieldtype": "Table", "label": "Taxes and Charges", "options": "Landed Cost Taxes and Charges", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_9", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_taxes_and_charges", "fieldtype": "Currency", - "label": "Total Taxes and Charges", + "label": "Total Taxes and Charges (Company Currency)", "options": "Company:company:default_currency", "read_only": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_break1", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "distribute_charges_based_on", "fieldtype": "Select", "label": "Distribute Charges Based On", - "options": "Qty\nAmount", - "reqd": 1 + "options": "Qty\nAmount\nDistribute Manually", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "amended_from", @@ -109,21 +137,51 @@ "no_copy": 1, "options": "Landed Cost Voucher", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "sec_break2", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "landed_cost_help", "fieldtype": "HTML", - "label": "Landed Cost Help" + "label": "Landed Cost Help", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "hide_border": 1, + "show_days": 1, + "show_seconds": 1 } ], "icon": "icon-usd", + "index_web_pages_for_search": 1, "is_submittable": 1, - "modified": "2019-11-21 15:34:10.846093", + "links": [], + "modified": "2021-01-25 23:07:30.468423", "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Voucher", diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index 9ec6b8946c..69a8bf19d3 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -9,6 +9,7 @@ from frappe.model.meta import get_field_precision from frappe.model.document import Document from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.accounts.doctype.account.account import get_account_currency +from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals class LandedCostVoucher(Document): def get_items_from_purchase_receipts(self): @@ -39,13 +40,15 @@ class LandedCostVoucher(Document): def validate(self): self.check_mandatory() + self.validate_purchase_receipts() + init_landed_taxes_and_totals(self) + self.set_total_taxes_and_charges() if not self.get("items"): self.get_items_from_purchase_receipts() - else: - self.validate_applicable_charges_for_item() - self.validate_purchase_receipts() - self.validate_expense_accounts() - self.set_total_taxes_and_charges() + + self.set_applicable_charges_on_item() + self.validate_applicable_charges_for_item() + def check_mandatory(self): if not self.get("purchase_receipts"): @@ -73,21 +76,37 @@ class LandedCostVoucher(Document): frappe.throw(_("Row {0}: Cost center is required for an item {1}") .format(item.idx, item.item_code)) - def validate_expense_accounts(self): - company_currency = erpnext.get_company_currency(self.company) - for account in self.taxes: - if get_account_currency(account.expense_account) != company_currency: - frappe.throw(_("Row {}: Expense account currency should be same as company's default currency.").format(account.idx) - + _("Please select expense account with account currency as {}.").format(frappe.bold(company_currency)), - title=_("Invalid Account Currency")) - def set_total_taxes_and_charges(self): - self.total_taxes_and_charges = sum([flt(d.amount) for d in self.get("taxes")]) + self.total_taxes_and_charges = sum([flt(d.base_amount) for d in self.get("taxes")]) + + def set_applicable_charges_on_item(self): + if self.get('taxes') and self.distribute_charges_based_on != 'Distribute Manually': + total_item_cost = 0.0 + total_charges = 0.0 + item_count = 0 + based_on_field = frappe.scrub(self.distribute_charges_based_on) + + for item in self.get('items'): + total_item_cost += item.get(based_on_field) + + for item in self.get('items'): + item.applicable_charges = flt(flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)), + item.precision('applicable_charges')) + total_charges += item.applicable_charges + item_count += 1 + + if total_charges != self.total_taxes_and_charges: + diff = self.total_taxes_and_charges - total_charges + self.get('items')[item_count - 1].applicable_charges += diff def validate_applicable_charges_for_item(self): based_on = self.distribute_charges_based_on.lower() - total = sum([flt(d.get(based_on)) for d in self.get("items")]) + if based_on != 'distribute manually': + total = sum([flt(d.get(based_on)) for d in self.get("items")]) + else: + # consider for proportion while distributing manually + total = sum([flt(d.get('applicable_charges')) for d in self.get("items")]) if not total: frappe.throw(_("Total {0} for all items is zero, may be you should change 'Distribute Charges Based On'").format(based_on)) @@ -153,13 +172,13 @@ class LandedCostVoucher(Document): docs = frappe.db.get_all('Asset', filters={ receipt_document_type: item.receipt_document, 'item_code': item.item_code }, fields=['name', 'docstatus']) if not docs or len(docs) != item.qty: - frappe.throw(_('There are not enough asset created or linked to {0}.').format(item.receipt_document) - + _('Please create or link {0} Assets with respective document.').format(item.qty)) + frappe.throw(_('There are not enough asset created or linked to {0}. Please create or link {1} Assets with respective document.').format( + item.receipt_document, item.qty)) if docs: for d in docs: if d.docstatus == 1: - frappe.throw(_('{0} {1} has submitted Assets. Remove Item {2} from table to continue.') - .format(item.receipt_document_type, frappe.bold(item.receipt_document), frappe.bold(item.item_code))) + frappe.throw(_('{2} {0} has submitted Assets. Remove Item {1} from table to continue.').format( + item.receipt_document, item.item_code, item.receipt_document_type)) def update_rate_in_serial_no_for_non_asset_items(self, receipt_document): for item in receipt_document.get("items"): 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 b97213e4fb..144101c67d 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 @@ -10,6 +10,7 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \ import get_gl_entries, test_records as pr_test_records, make_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 class TestLandedCostVoucher(unittest.TestCase): def test_landed_cost_voucher(self): @@ -162,8 +163,8 @@ class TestLandedCostVoucher(unittest.TestCase): lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, 123.22) - self.assertEqual(lcv.items[0].applicable_charges, 41.07) - self.assertEqual(lcv.items[2].applicable_charges, 41.08) + self.assertEqual(flt(lcv.items[0].applicable_charges, 2), 41.07) + self.assertEqual(flt(lcv.items[2].applicable_charges, 2), 41.08) def test_multiple_landed_cost_voucher_against_pr(self): pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", @@ -206,6 +207,46 @@ class TestLandedCostVoucher(unittest.TestCase): self.assertEqual(pr.items[0].landed_cost_voucher_amount, 100) self.assertEqual(pr.items[1].landed_cost_voucher_amount, 100) + def test_multi_currency_lcv(self): + ## Create USD Shipping charges_account + usd_shipping = create_account(account_name="Shipping Charges USD", + parent_account="Duties and Taxes - TCP1", company="_Test Company with perpetual inventory", + account_currency="USD") + + pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", + supplier_warehouse = "Stores - TCP1") + pr.submit() + + lcv = make_landed_cost_voucher(company = pr.company, receipt_document_type = "Purchase Receipt", + receipt_document=pr.name, charges=100, do_not_save=True) + + lcv.append("taxes", { + "description": "Shipping Charges", + "expense_account": usd_shipping, + "amount": 10 + }) + + lcv.save() + lcv.submit() + pr.load_from_db() + + # Considering exchange rate from USD to INR as 62.9 + self.assertEqual(lcv.total_taxes_and_charges, 729) + self.assertEqual(pr.items[0].landed_cost_voucher_amount, 729) + + gl_entries = frappe.get_all("GL Entry", fields=["account", "credit", "credit_in_account_currency"], + filters={"voucher_no": pr.name, "account": ("in", ["Shipping Charges USD - TCP1", "Expenses Included In Valuation - TCP1"])}) + + expected_gl_entries = { + "Shipping Charges USD - TCP1": [629, 10], + "Expenses Included In Valuation - TCP1": [100, 100] + } + + for entry in gl_entries: + amounts = expected_gl_entries.get(entry.account) + self.assertEqual(entry.credit, amounts[0]) + self.assertEqual(entry.credit_in_account_currency, amounts[1]) + def make_landed_cost_voucher(** args): args = frappe._dict(args) ref_doc = frappe.get_doc(args.receipt_document_type, args.receipt_document) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 01edd99e9d..527b0d3ea9 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -2,6 +2,7 @@ // License: GNU General Public License v3. See license.txt // eslint-disable-next-line +frappe.provide("erpnext.accounts.dimensions"); {% include 'erpnext/public/js/controllers/buying.js' %}; frappe.ui.form.on('Material Request', { @@ -66,6 +67,12 @@ frappe.ui.form.on('Material Request', { filters: {'company': doc.company} }; }); + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); + }, + + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, onload_post_render: function(frm) { diff --git a/erpnext/stock/doctype/material_request/material_request_list.js b/erpnext/stock/doctype/material_request/material_request_list.js index 0d7095875c..de7a3d05bf 100644 --- a/erpnext/stock/doctype/material_request/material_request_list.js +++ b/erpnext/stock/doctype/material_request/material_request_list.js @@ -1,9 +1,10 @@ frappe.listview_settings['Material Request'] = { add_fields: ["material_request_type", "status", "per_ordered", "per_received", "transfer_status"], get_indicator: function(doc) { - if(doc.status=="Stopped") { + var precision = frappe.defaults.get_default("float_precision"); + if (doc.status=="Stopped") { return [__("Stopped"), "red", "status,=,Stopped"]; - } else if(doc.transfer_status && doc.docstatus != 2) { + } else if (doc.transfer_status && doc.docstatus != 2) { if (doc.transfer_status == "Not Started") { return [__("Not Started"), "orange"]; } else if (doc.transfer_status == "In Transit") { @@ -11,14 +12,14 @@ frappe.listview_settings['Material Request'] = { } else if (doc.transfer_status == "Completed") { return [__("Completed"), "green"]; } - } else if(doc.docstatus==1 && flt(doc.per_ordered, 2) == 0) { + } else if (doc.docstatus==1 && flt(doc.per_ordered, precision) == 0) { return [__("Pending"), "orange", "per_ordered,=,0"]; - } else if(doc.docstatus==1 && flt(doc.per_ordered, 2) < 100) { + } else if (doc.docstatus==1 && flt(doc.per_ordered, precision) < 100) { return [__("Partially ordered"), "yellow", "per_ordered,<,100"]; - } else if(doc.docstatus==1 && flt(doc.per_ordered, 2) == 100) { - if (doc.material_request_type == "Purchase" && flt(doc.per_received, 2) < 100 && flt(doc.per_received, 2) > 0) { + } else if (doc.docstatus==1 && flt(doc.per_ordered, precision) == 100) { + if (doc.material_request_type == "Purchase" && flt(doc.per_received, precision) < 100 && flt(doc.per_received, precision) > 0) { return [__("Partially Received"), "yellow", "per_received,<,100"]; - } else if (doc.material_request_type == "Purchase" && flt(doc.per_received, 2) == 100) { + } else if (doc.material_request_type == "Purchase" && flt(doc.per_received, precision) == 100) { return [__("Received"), "green", "per_received,=,100"]; } else if (doc.material_request_type == "Purchase") { return [__("Ordered"), "green", "per_ordered,=,100"]; diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 0a29fa05e1..72a3a5e67c 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -424,6 +424,7 @@ class TestMaterialRequest(unittest.TestCase): "basic_rate": 1.0 }) se_doc.get("items")[1].update({ + "item_code": "_Test Item Home Desktop 100", "qty": 3.0, "transfer_qty": 3.0, "s_warehouse": "_Test Warehouse 1 - _TC", @@ -534,7 +535,7 @@ class TestMaterialRequest(unittest.TestCase): mr = make_material_request(item_code='_Test FG Item', material_request_type='Manufacture', uom="_Test UOM 1", conversion_factor=12) - + requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC') self.assertEqual(requested_qty, existing_requested_qty + 120) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index bc1d81d356..57cc3504a9 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -46,6 +46,8 @@ frappe.ui.form.on("Purchase Receipt", { erpnext.queries.setup_queries(frm, "Warehouse", function() { return erpnext.queries.warehouse(frm.doc); }); + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { @@ -75,6 +77,7 @@ frappe.ui.form.on("Purchase Receipt", { company: function(frm) { frm.trigger("toggle_display_account_head"); + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, toggle_display_account_head: function(frm) { @@ -213,6 +216,10 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend }); }, + apply_putaway_rule: function() { + if (this.frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(this.frm); + } + }); // for backward compatibility: combine new and previous states diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 5bb3095708..32d349f303 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -21,6 +21,7 @@ "posting_date", "posting_time", "set_posting_time", + "apply_putaway_rule", "is_return", "return_against", "section_addresses", @@ -47,6 +48,7 @@ "set_warehouse", "rejected_warehouse", "col_break_warehouse", + "set_from_warehouse", "is_subcontracted", "supplier_warehouse", "items_section", @@ -114,6 +116,7 @@ "per_returned", "is_internal_supplier", "inter_company_reference", + "represents_company", "subscription_detail", "auto_repeat", "printing_settings", @@ -1086,7 +1089,9 @@ "fieldname": "inter_company_reference", "fieldtype": "Link", "label": "Inter Company Reference", + "no_copy": 1, "options": "Delivery Note", + "print_hide": 1, "read_only": 1 }, { @@ -1106,6 +1111,12 @@ "label": "Billing Address", "read_only": 1 }, + { + "default": "0", + "fieldname": "apply_putaway_rule", + "fieldtype": "Check", + "label": "Apply Putaway Rule" + }, { "depends_on": "eval:!doc.__islocal", "fieldname": "per_returned", @@ -1114,13 +1125,29 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "depends_on": "eval: doc.is_internal_supplier", + "description": "Sets 'From Warehouse' in each row of the items table.", + "fieldname": "set_from_warehouse", + "fieldtype": "Link", + "label": "Set From Warehouse", + "options": "Warehouse" + }, + { + "fetch_from": "supplier.represents_company", + "fieldname": "represents_company", + "fieldtype": "Link", + "label": "Represents Company", + "options": "Company", + "read_only": 1 } ], "icon": "fa fa-truck", "idx": 261, "is_submittable": 1, "links": [], - "modified": "2020-11-30 12:54:23.278500", + "modified": "2020-12-26 20:49:39.106049", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index f833fc75c0..550c849c5d 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -83,6 +83,12 @@ class PurchaseReceipt(BuyingController): } ]) + def before_validate(self): + from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule + + if self.get("items") and self.apply_putaway_rule and not self.get("is_return"): + apply_putaway_rule(self.doctype, self.get("items"), self.company) + def validate(self): self.validate_posting_time() super(PurchaseReceipt, self).validate() @@ -103,6 +109,7 @@ class PurchaseReceipt(BuyingController): if getdate(self.posting_date) > getdate(nowdate()): throw(_("Posting Date cannot be future date")) + def validate_cwip_accounts(self): for item in self.get('items'): if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category): @@ -281,12 +288,15 @@ class PurchaseReceipt(BuyingController): # Amount added through landed-cost-voucher if d.landed_cost_voucher_amount and landed_cost_entries: for account, amount in iteritems(landed_cost_entries[(d.item_code, d.name)]): + account_currency = get_account_currency(account) gl_entries.append(self.get_gl_dict({ "account": account, + "account_currency": account_currency, "against": warehouse_account[d.warehouse]["account"], "cost_center": d.cost_center, "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(amount), + "credit": flt(amount["base_amount"]), + "credit_in_account_currency": flt(amount["amount"]), "project": d.project }, item=d)) @@ -408,7 +418,7 @@ class PurchaseReceipt(BuyingController): if warehouse_with_no_account: frappe.msgprint(_("No accounting entries for the following warehouses") + ": \n" + "\n".join(warehouse_with_no_account)) - + return process_gl_map(gl_entries) def get_asset_gl_entry(self, gl_entries): @@ -721,7 +731,13 @@ def get_item_account_wise_additional_cost(purchase_document): for lcv in landed_cost_vouchers: landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", lcv.parent) - based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on) + + #Use amount field for total item cost for manually cost distributed LCVs + if landed_cost_voucher_doc.distribute_charges_based_on == 'Distribute Manually': + based_on_field = 'amount' + else: + based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on) + total_item_cost = 0 for item in landed_cost_voucher_doc.items: @@ -731,9 +747,16 @@ def get_item_account_wise_additional_cost(purchase_document): if item.receipt_document == purchase_document: for account in landed_cost_voucher_doc.taxes: item_account_wise_cost.setdefault((item.item_code, item.purchase_receipt_item), {}) - item_account_wise_cost[(item.item_code, item.purchase_receipt_item)].setdefault(account.expense_account, 0.0) - item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account] += \ + item_account_wise_cost[(item.item_code, item.purchase_receipt_item)].setdefault(account.expense_account, { + "amount": 0.0, + "base_amount": 0.0 + }) + + item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account]["amount"] += \ account.amount * item.get(based_on_field) / total_item_cost + item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account]["base_amount"] += \ + account.base_amount * item.get(based_on_field) / total_item_cost + return item_account_wise_cost diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index f99ca89f79..ca58ab2823 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1011,6 +1011,7 @@ def make_purchase_receipt(**args): pr.currency = args.currency or "INR" pr.is_return = args.is_return pr.return_against = args.return_against + pr.apply_putaway_rule = args.apply_putaway_rule qty = args.qty or 5 received_qty = args.received_qty or qty rejected_qty = args.rejected_qty or flt(received_qty) - flt(qty) @@ -1026,6 +1027,7 @@ def make_purchase_receipt(**args): "rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" if rejected_qty != 0 else "", "rate": args.rate if args.rate != None else 50, "conversion_factor": args.conversion_factor or 1.0, + "stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0), "serial_no": args.serial_no, "stock_uom": args.stock_uom or "_Test UOM", "uom": uom, diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 871b255b06..e99119202e 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -76,6 +76,8 @@ "purchase_order_item", "material_request_item", "purchase_receipt_item", + "delivery_note_item", + "putaway_rule", "section_break_45", "allow_zero_valuation_rate", "bom", @@ -818,11 +820,12 @@ "read_only": 1 }, { + "depends_on": "eval:parent.is_internal_supplier", "fieldname": "from_warehouse", "fieldtype": "Link", "hidden": 1, "ignore_user_permissions": 1, - "label": "Supplier Warehouse", + "label": "From Warehouse", "options": "Warehouse" }, { @@ -839,6 +842,15 @@ "fieldname": "image_column", "fieldtype": "Column Break" }, + { + "fieldname": "putaway_rule", + "fieldtype": "Link", + "label": "Putaway Rule", + "no_copy": 1, + "options": "Putaway Rule", + "print_hide": 1, + "read_only": 1 + }, { "fieldname": "tracking_section", "fieldtype": "Section Break" @@ -861,12 +873,20 @@ "fieldtype": "Float", "label": "Received Qty in Stock UOM", "print_hide": 1 + }, + { + "fieldname": "delivery_note_item", + "fieldtype": "Data", + "label": "Delivery Note Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-12-07 10:00:38.204294", + "modified": "2020-12-26 16:50:56.479347", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/putaway_rule/__init__.py b/erpnext/stock/doctype/putaway_rule/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.js b/erpnext/stock/doctype/putaway_rule/putaway_rule.js new file mode 100644 index 0000000000..e0569206ef --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.js @@ -0,0 +1,43 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Putaway Rule', { + setup: function(frm) { + frm.set_query("warehouse", function() { + return { + "filters": { + "company": frm.doc.company, + "is_group": 0 + } + }; + }); + }, + + uom: function(frm) { + if (frm.doc.item_code && frm.doc.uom) { + return frm.call({ + method: "erpnext.stock.get_item_details.get_conversion_factor", + args: { + item_code: frm.doc.item_code, + uom: frm.doc.uom + }, + callback: function(r) { + if (!r.exc) { + let stock_capacity = flt(frm.doc.capacity) * flt(r.message.conversion_factor); + frm.set_value('conversion_factor', r.message.conversion_factor); + frm.set_value('stock_capacity', stock_capacity); + } + } + }); + } + }, + + capacity: function(frm) { + let stock_capacity = flt(frm.doc.capacity) * flt(frm.doc.conversion_factor); + frm.set_value('stock_capacity', stock_capacity); + } + + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.json b/erpnext/stock/doctype/putaway_rule/putaway_rule.json new file mode 100644 index 0000000000..a003f4986f --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json @@ -0,0 +1,160 @@ +{ + "actions": [], + "autoname": "PUT-.####", + "creation": "2020-11-09 11:39:46.489501", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "disable", + "item_code", + "item_name", + "warehouse", + "priority", + "col_break_capacity", + "company", + "capacity", + "uom", + "conversion_factor", + "stock_uom", + "stock_capacity" + ], + "fields": [ + { + "fieldname": "item_code", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Item", + "options": "Item", + "reqd": 1 + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Warehouse", + "options": "Warehouse", + "reqd": 1 + }, + { + "fieldname": "col_break_capacity", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "capacity", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Capacity", + "reqd": 1 + }, + { + "fetch_from": "item_code.stock_uom", + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "read_only": 1 + }, + { + "default": "1", + "fieldname": "priority", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Priority" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "default": "0", + "depends_on": "eval:!doc.__islocal", + "fieldname": "disable", + "fieldtype": "Check", + "label": "Disable" + }, + { + "fieldname": "uom", + "fieldtype": "Link", + "label": "UOM", + "no_copy": 1, + "options": "UOM" + }, + { + "fieldname": "stock_capacity", + "fieldtype": "Float", + "label": "Capacity in Stock UOM", + "no_copy": 1, + "read_only": 1 + }, + { + "default": "1", + "fieldname": "conversion_factor", + "fieldtype": "Float", + "label": "Conversion Factor", + "no_copy": 1, + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-11-25 20:39:19.973437", + "modified_by": "Administrator", + "module": "Stock", + "name": "Putaway Rule", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock User", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "item_code", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py new file mode 100644 index 0000000000..ea26caced0 --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -0,0 +1,235 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import copy +import json +from collections import defaultdict +from six import string_types +from frappe import _ +from frappe.utils import flt, floor, nowdate, cint +from frappe.model.document import Document +from erpnext.stock.utils import get_stock_balance +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + +class PutawayRule(Document): + def validate(self): + self.validate_duplicate_rule() + self.validate_warehouse_and_company() + self.validate_capacity() + self.validate_priority() + self.set_stock_capacity() + + def validate_duplicate_rule(self): + existing_rule = frappe.db.exists("Putaway Rule", {"item_code": self.item_code, "warehouse": self.warehouse}) + if existing_rule and existing_rule != self.name: + frappe.throw(_("Putaway Rule already exists for Item {0} in Warehouse {1}.") + .format(frappe.bold(self.item_code), frappe.bold(self.warehouse)), + title=_("Duplicate")) + + def validate_priority(self): + if self.priority < 1: + frappe.throw(_("Priority cannot be lesser than 1."), title=_("Invalid Priority")) + + def validate_warehouse_and_company(self): + company = frappe.db.get_value("Warehouse", self.warehouse, "company") + if company != self.company: + frappe.throw(_("Warehouse {0} does not belong to Company {1}.") + .format(frappe.bold(self.warehouse), frappe.bold(self.company)), + title=_("Invalid Warehouse")) + + def validate_capacity(self): + stock_uom = frappe.db.get_value("Item", self.item_code, "stock_uom") + balance_qty = get_stock_balance(self.item_code, self.warehouse, nowdate()) + + if flt(self.stock_capacity) < flt(balance_qty): + frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} {2}.") + .format(self.item_code, frappe.bold(balance_qty), stock_uom), + title=_("Insufficient Capacity")) + + if not self.capacity: + frappe.throw(_("Capacity must be greater than 0"), title=_("Invalid")) + + def set_stock_capacity(self): + self.stock_capacity = (flt(self.conversion_factor) or 1) * flt(self.capacity) + +@frappe.whitelist() +def get_available_putaway_capacity(rule): + stock_capacity, item_code, warehouse = frappe.db.get_value("Putaway Rule", rule, + ["stock_capacity", "item_code", "warehouse"]) + balance_qty = get_stock_balance(item_code, warehouse, nowdate()) + free_space = flt(stock_capacity) - flt(balance_qty) + return free_space if free_space > 0 else 0 + +@frappe.whitelist() +def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): + """ Applies Putaway Rule on line items. + + items: List of Purchase Receipt/Stock Entry Items + company: Company in the Purchase Receipt/Stock Entry + doctype: Doctype to apply rule on + purpose: Purpose of Stock Entry + sync (optional): Sync with client side only for client side calls + """ + if isinstance(items, string_types): + items = json.loads(items) + + items_not_accomodated, updated_table = [], [] + item_wise_rules = defaultdict(list) + + for item in items: + if isinstance(item, dict): + item = frappe._dict(item) + + source_warehouse = item.get("s_warehouse") + serial_nos = get_serial_nos(item.get("serial_no")) + item.conversion_factor = flt(item.conversion_factor) or 1.0 + pending_qty, item_code = flt(item.qty), item.item_code + pending_stock_qty = flt(item.transfer_qty) if doctype == "Stock Entry" else flt(item.stock_qty) + uom_must_be_whole_number = frappe.db.get_value('UOM', item.uom, 'must_be_whole_number') + + if not pending_qty or not item_code: + updated_table = add_row(item, pending_qty, source_warehouse or item.warehouse, updated_table) + continue + + at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse) + + if not rules: + warehouse = source_warehouse or item.warehouse + if at_capacity: + # rules available, but no free space + items_not_accomodated.append([item_code, pending_qty]) + else: + updated_table = add_row(item, pending_qty, warehouse, updated_table) + continue + + # maintain item/item-warehouse wise rules, to handle if item is entered twice + # in the table, due to different price, etc. + key = item_code + if doctype == "Stock Entry" and purpose == "Material Transfer" and source_warehouse: + key = (item_code, source_warehouse) + + if not item_wise_rules[key]: + item_wise_rules[key] = rules + + for rule in item_wise_rules[key]: + if pending_stock_qty > 0 and rule.free_space: + stock_qty_to_allocate = flt(rule.free_space) if pending_stock_qty >= flt(rule.free_space) else pending_stock_qty + qty_to_allocate = stock_qty_to_allocate / item.conversion_factor + + if uom_must_be_whole_number: + qty_to_allocate = floor(qty_to_allocate) + stock_qty_to_allocate = qty_to_allocate * item.conversion_factor + + if not qty_to_allocate: break + + updated_table = add_row(item, qty_to_allocate, rule.warehouse, updated_table, + rule.name, serial_nos=serial_nos) + + pending_stock_qty -= stock_qty_to_allocate + pending_qty -= qty_to_allocate + rule["free_space"] -= stock_qty_to_allocate + + if not pending_stock_qty > 0: break + + # if pending qty after applying all rules, add row without warehouse + if pending_stock_qty > 0: + items_not_accomodated.append([item.item_code, pending_qty]) + + if items_not_accomodated: + show_unassigned_items_message(items_not_accomodated) + + items[:] = updated_table if updated_table else items # modify items table + + if sync and json.loads(sync): # sync with client side + return items + +def get_ordered_putaway_rules(item_code, company, source_warehouse=None): + """Returns an ordered list of putaway rules to apply on an item.""" + filters = { + "item_code": item_code, + "company": company, + "disable": 0 + } + if source_warehouse: + filters.update({"warehouse": ["!=", source_warehouse]}) + + rules = frappe.get_all("Putaway Rule", + fields=["name", "item_code", "stock_capacity", "priority", "warehouse"], + filters=filters, + order_by="priority asc, capacity desc") + + if not rules: + return False, None + + vacant_rules = [] + for rule in rules: + balance_qty = get_stock_balance(rule.item_code, rule.warehouse, nowdate()) + free_space = flt(rule.stock_capacity) - flt(balance_qty) + if free_space > 0: + rule["free_space"] = free_space + vacant_rules.append(rule) + + if not vacant_rules: + # After iterating through rules, if no rules are left + # then there is not enough space left in any rule + return True, None + + vacant_rules = sorted(vacant_rules, key = lambda i: (i['priority'], -i['free_space'])) + + return False, vacant_rules + +def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=None): + new_updated_table_row = copy.deepcopy(item) + new_updated_table_row.idx = 1 if not updated_table else cint(updated_table[-1].idx) + 1 + new_updated_table_row.name = None + new_updated_table_row.qty = to_allocate + + if item.doctype == "Stock Entry Detail": + new_updated_table_row.t_warehouse = warehouse + new_updated_table_row.transfer_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) + else: + new_updated_table_row.stock_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) + new_updated_table_row.warehouse = warehouse + new_updated_table_row.rejected_qty = 0 + new_updated_table_row.received_qty = to_allocate + + if rule: + new_updated_table_row.putaway_rule = rule + if serial_nos: + new_updated_table_row.serial_no = get_serial_nos_to_allocate(serial_nos, to_allocate) + + updated_table.append(new_updated_table_row) + return updated_table + +def show_unassigned_items_message(items_not_accomodated): + msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

" + formatted_item_rows = "" + + for entry in items_not_accomodated: + item_link = frappe.utils.get_link_to_form("Item", entry[0]) + formatted_item_rows += """ + {0} + {1} + """.format(item_link, frappe.bold(entry[1])) + + msg += """ + + + + + + {2} +
{0}{1}
+ """.format(_("Item"), _("Unassigned Qty"), formatted_item_rows) + + frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True) + +def get_serial_nos_to_allocate(serial_nos, to_allocate): + if serial_nos: + allocated_serial_nos = serial_nos[0: cint(to_allocate)] + serial_nos[:] = serial_nos[cint(to_allocate):] # pop out allocated serial nos and modify list + return "\n".join(allocated_serial_nos) if allocated_serial_nos else "" + else: return "" \ No newline at end of file diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js new file mode 100644 index 0000000000..725e91ee8d --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js @@ -0,0 +1,18 @@ +frappe.listview_settings['Putaway Rule'] = { + add_fields: ["disable"], + get_indicator: (doc) => { + if (doc.disable) { + return [__("Disabled"), "darkgrey", "disable,=,1"]; + } else { + return [__("Active"), "blue", "disable,=,0"]; + } + }, + + reports: [ + { + name: 'Warehouse Capacity Summary', + report_type: 'Page', + route: 'warehouse-capacity-summary' + } + ] +}; diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py new file mode 100644 index 0000000000..86f7dc3e08 --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py @@ -0,0 +1,389 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals +import frappe +import unittest +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.get_item_details import get_conversion_factor +from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry +from erpnext.stock.doctype.batch.test_batch import make_new_batch +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + +class TestPutawayRule(unittest.TestCase): + def setUp(self): + if not frappe.db.exists("Item", "_Rice"): + make_item("_Rice", { + 'is_stock_item': 1, + 'has_batch_no' : 1, + 'create_new_batch': 1, + 'stock_uom': 'Kg' + }) + + if not frappe.db.exists("Warehouse", {"warehouse_name": "Rack 1"}): + create_warehouse("Rack 1") + if not frappe.db.exists("Warehouse", {"warehouse_name": "Rack 2"}): + create_warehouse("Rack 2") + + self.warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) + self.warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) + + if not frappe.db.exists("UOM", "Bag"): + new_uom = frappe.new_doc("UOM") + new_uom.uom_name = "Bag" + new_uom.save() + + def test_putaway_rules_priority(self): + """Test if rule is applied by priority, irrespective of free space.""" + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, + uom="Kg") + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=300, + uom="Kg", priority=2) + + pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, + do_not_submit=1) + self.assertEqual(len(pr.items), 2) + self.assertEqual(pr.items[0].qty, 200) + self.assertEqual(pr.items[0].warehouse, self.warehouse_1) + self.assertEqual(pr.items[1].qty, 100) + self.assertEqual(pr.items[1].warehouse, self.warehouse_2) + + pr.delete() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_with_same_priority(self): + """Test if rule with more free space is applied, + among two rules with same priority and capacity.""" + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=500, + uom="Kg") + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=500, + uom="Kg") + + # out of 500 kg capacity, occupy 100 kg in warehouse_1 + stock_receipt = make_stock_entry(item_code="_Rice", target=self.warehouse_1, qty=100, basic_rate=50) + + pr = make_purchase_receipt(item_code="_Rice", qty=700, apply_putaway_rule=1, + do_not_submit=1) + self.assertEqual(len(pr.items), 2) + self.assertEqual(pr.items[0].qty, 500) + # warehouse_2 has 500 kg free space, it is given priority + self.assertEqual(pr.items[0].warehouse, self.warehouse_2) + self.assertEqual(pr.items[1].qty, 200) + # warehouse_1 has 400 kg free space, it is given less priority + self.assertEqual(pr.items[1].warehouse, self.warehouse_1) + + stock_receipt.cancel() + pr.delete() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_with_insufficient_capacity(self): + """Test if qty exceeding capacity, is handled.""" + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=100, + uom="Kg") + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=200, + uom="Kg") + + pr = make_purchase_receipt(item_code="_Rice", qty=350, apply_putaway_rule=1, + do_not_submit=1) + self.assertEqual(len(pr.items), 2) + self.assertEqual(pr.items[0].qty, 200) + self.assertEqual(pr.items[0].warehouse, self.warehouse_2) + self.assertEqual(pr.items[1].qty, 100) + self.assertEqual(pr.items[1].warehouse, self.warehouse_1) + # total 300 assigned, 50 unassigned + + pr.delete() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_multi_uom(self): + """Test rules applied on uom other than stock uom.""" + item = frappe.get_doc("Item", "_Rice") + if not frappe.db.get_value("UOM Conversion Detail", {"parent": "_Rice", "uom": "Bag"}): + item.append("uoms", { + "uom": "Bag", + "conversion_factor": 1000 + }) + item.save() + + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=3, + uom="Bag") + self.assertEqual(rule_1.stock_capacity, 3000) + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=4, + uom="Bag") + self.assertEqual(rule_2.stock_capacity, 4000) + + # populate 'Rack 1' with 1 Bag, making the free space 2 Bags + stock_receipt = make_stock_entry(item_code="_Rice", target=self.warehouse_1, qty=1000, basic_rate=50) + + pr = make_purchase_receipt(item_code="_Rice", qty=6, uom="Bag", stock_uom="Kg", + conversion_factor=1000, apply_putaway_rule=1, do_not_submit=1) + self.assertEqual(len(pr.items), 2) + self.assertEqual(pr.items[0].qty, 4) + self.assertEqual(pr.items[0].warehouse, self.warehouse_2) + self.assertEqual(pr.items[1].qty, 2) + self.assertEqual(pr.items[1].warehouse, self.warehouse_1) + + stock_receipt.cancel() + pr.delete() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_multi_uom_whole_uom(self): + """Test if whole UOMs are handled.""" + item = frappe.get_doc("Item", "_Rice") + if not frappe.db.get_value("UOM Conversion Detail", {"parent": "_Rice", "uom": "Bag"}): + item.append("uoms", { + "uom": "Bag", + "conversion_factor": 1000 + }) + item.save() + + frappe.db.set_value("UOM", "Bag", "must_be_whole_number", 1) + + # Putaway Rule in different UOM + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=1, + uom="Bag") + self.assertEqual(rule_1.stock_capacity, 1000) + # Putaway Rule in Stock UOM + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=500) + self.assertEqual(rule_2.stock_capacity, 500) + # total capacity is 1500 Kg + + pr = make_purchase_receipt(item_code="_Rice", qty=2, uom="Bag", stock_uom="Kg", + conversion_factor=1000, apply_putaway_rule=1, do_not_submit=1) + self.assertEqual(len(pr.items), 1) + self.assertEqual(pr.items[0].qty, 1) + self.assertEqual(pr.items[0].warehouse, self.warehouse_1) + # leftover space was for 500 kg (0.5 Bag) + # Since Bag is a whole UOM, 1(out of 2) Bag will be unassigned + + pr.delete() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_with_reoccurring_item(self): + """Test rules on same item entered multiple times with different rate.""" + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, + uom="Kg") + # total capacity is 200 Kg + + pr = make_purchase_receipt(item_code="_Rice", qty=100, apply_putaway_rule=1, + do_not_submit=1) + pr.append("items", { + "item_code": "_Rice", + "warehouse": "_Test Warehouse - _TC", + "qty": 200, + "uom": "Kg", + "stock_uom": "Kg", + "stock_qty": 200, + "received_qty": 200, + "rate": 100, + "conversion_factor": 1.0, + }) # same item entered again in PR but with different rate + pr.save() + self.assertEqual(len(pr.items), 2) + self.assertEqual(pr.items[0].qty, 100) + self.assertEqual(pr.items[0].warehouse, self.warehouse_1) + self.assertEqual(pr.items[0].putaway_rule, rule_1.name) + # same rule applied to second item row + # with previous assignment considered + self.assertEqual(pr.items[1].qty, 100) # 100 unassigned in second row from 200 + self.assertEqual(pr.items[1].warehouse, self.warehouse_1) + self.assertEqual(pr.items[1].putaway_rule, rule_1.name) + + pr.delete() + rule_1.delete() + + def test_validate_over_receipt_in_warehouse(self): + """Test if overreceipt is blocked in the presence of putaway rules.""" + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, + uom="Kg") + + pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, + do_not_submit=1) + self.assertEqual(len(pr.items), 1) + self.assertEqual(pr.items[0].qty, 200) # 100 is unassigned fro 300 Kg + self.assertEqual(pr.items[0].warehouse, self.warehouse_1) + self.assertEqual(pr.items[0].putaway_rule, rule_1.name) + + # force overreceipt and disable apply putaway rule in PR + pr.items[0].qty = 300 + pr.items[0].stock_qty = 300 + pr.apply_putaway_rule = 0 + self.assertRaises(frappe.ValidationError, pr.save) + + pr.delete() + rule_1.delete() + + def test_putaway_rule_on_stock_entry_material_transfer(self): + """Test if source warehouse is considered while applying rules.""" + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, + uom="Kg") # higher priority + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=100, + uom="Kg", priority=2) + + stock_entry = make_stock_entry(item_code="_Rice", source=self.warehouse_1, qty=200, + target="_Test Warehouse - _TC", purpose="Material Transfer", + apply_putaway_rule=1, do_not_submit=1) + + stock_entry_item = stock_entry.get("items")[0] + + # since source warehouse is Rack 1, rule 1 (for Rack 1) will be avoided + # even though it has more free space and higher priority + self.assertEqual(stock_entry_item.t_warehouse, self.warehouse_2) + self.assertEqual(stock_entry_item.qty, 100) # unassigned 100 out of 200 Kg + self.assertEqual(stock_entry_item.putaway_rule, rule_2.name) + + stock_entry.delete() + rule_1.delete() + rule_2.delete() + + def test_putaway_rule_on_stock_entry_material_transfer_reoccuring_item(self): + """Test if reoccuring item is correctly considered.""" + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=300, + uom="Kg") + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=600, + uom="Kg", priority=2) + + # create SE with first row having source warehouse as Rack 2 + stock_entry = make_stock_entry(item_code="_Rice", source=self.warehouse_2, qty=200, + target="_Test Warehouse - _TC", purpose="Material Transfer", + apply_putaway_rule=1, do_not_submit=1) + + # Add rows with source warehouse as Rack 1 + stock_entry.extend("items", [ + { + "item_code": "_Rice", + "s_warehouse": self.warehouse_1, + "t_warehouse": "_Test Warehouse - _TC", + "qty": 100, + "basic_rate": 50, + "conversion_factor": 1.0, + "transfer_qty": 100 + }, + { + "item_code": "_Rice", + "s_warehouse": self.warehouse_1, + "t_warehouse": "_Test Warehouse - _TC", + "qty": 200, + "basic_rate": 60, + "conversion_factor": 1.0, + "transfer_qty": 200 + } + ]) + + stock_entry.save() + + # since source warehouse was Rack 2, exclude rule_2 + self.assertEqual(stock_entry.items[0].t_warehouse, self.warehouse_1) + self.assertEqual(stock_entry.items[0].qty, 200) + self.assertEqual(stock_entry.items[0].putaway_rule, rule_1.name) + + # since source warehouse was Rack 1, exclude rule_1 even though it has + # higher priority + self.assertEqual(stock_entry.items[1].t_warehouse, self.warehouse_2) + self.assertEqual(stock_entry.items[1].qty, 100) + self.assertEqual(stock_entry.items[1].putaway_rule, rule_2.name) + + self.assertEqual(stock_entry.items[2].t_warehouse, self.warehouse_2) + self.assertEqual(stock_entry.items[2].qty, 200) + self.assertEqual(stock_entry.items[2].putaway_rule, rule_2.name) + + stock_entry.delete() + rule_1.delete() + rule_2.delete() + + def test_putaway_rule_on_stock_entry_material_transfer_batch_serial_item(self): + """Test if batch and serial items are split correctly.""" + if not frappe.db.exists("Item", "Water Bottle"): + make_item("Water Bottle", { + "is_stock_item": 1, + "has_batch_no" : 1, + "create_new_batch": 1, + "has_serial_no": 1, + "serial_no_series": "BOTTL-.####", + "stock_uom": "Nos" + }) + + rule_1 = create_putaway_rule(item_code="Water Bottle", warehouse=self.warehouse_1, capacity=3, + uom="Nos") + rule_2 = create_putaway_rule(item_code="Water Bottle", warehouse=self.warehouse_2, capacity=2, + uom="Nos") + + make_new_batch(batch_id="BOTTL-BATCH-1", item_code="Water Bottle") + + pr = make_purchase_receipt(item_code="Water Bottle", qty=5, do_not_submit=1) + pr.items[0].batch_no = "BOTTL-BATCH-1" + pr.save() + pr.submit() + + serial_nos = frappe.get_list("Serial No", filters={"purchase_document_no": pr.name, "status": "Active"}) + serial_nos = [d.name for d in serial_nos] + + stock_entry = make_stock_entry(item_code="Water Bottle", source="_Test Warehouse - _TC", qty=5, + target="Finished Goods - _TC", purpose="Material Transfer", + apply_putaway_rule=1, do_not_save=1) + stock_entry.items[0].batch_no = "BOTTL-BATCH-1" + stock_entry.items[0].serial_no = "\n".join(serial_nos) + stock_entry.save() + + self.assertEqual(stock_entry.items[0].t_warehouse, self.warehouse_1) + self.assertEqual(stock_entry.items[0].qty, 3) + self.assertEqual(stock_entry.items[0].putaway_rule, rule_1.name) + self.assertEqual(stock_entry.items[0].serial_no, "\n".join(serial_nos[:3])) + self.assertEqual(stock_entry.items[0].batch_no, "BOTTL-BATCH-1") + + self.assertEqual(stock_entry.items[1].t_warehouse, self.warehouse_2) + self.assertEqual(stock_entry.items[1].qty, 2) + self.assertEqual(stock_entry.items[1].putaway_rule, rule_2.name) + self.assertEqual(stock_entry.items[1].serial_no, "\n".join(serial_nos[3:])) + self.assertEqual(stock_entry.items[1].batch_no, "BOTTL-BATCH-1") + + stock_entry.delete() + pr.cancel() + rule_1.delete() + rule_2.delete() + + def test_putaway_rule_on_stock_entry_material_receipt(self): + """Test if rules are applied in Stock Entry of type Receipt.""" + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, + uom="Kg") # more capacity + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=100, + uom="Kg") + + stock_entry = make_stock_entry(item_code="_Rice", qty=100, + target="_Test Warehouse - _TC", purpose="Material Receipt", + apply_putaway_rule=1, do_not_submit=1) + + stock_entry_item = stock_entry.get("items")[0] + + self.assertEqual(stock_entry_item.t_warehouse, self.warehouse_1) + self.assertEqual(stock_entry_item.qty, 100) + self.assertEqual(stock_entry_item.putaway_rule, rule_1.name) + + stock_entry.delete() + rule_1.delete() + rule_2.delete() + +def create_putaway_rule(**args): + args = frappe._dict(args) + putaway = frappe.new_doc("Putaway Rule") + + putaway.disable = args.disable or 0 + putaway.company = args.company or "_Test Company" + putaway.item_code = args.item or args.item_code or "_Test Item" + putaway.warehouse = args.warehouse + putaway.priority = args.priority or 1 + putaway.capacity = args.capacity or 1 + putaway.stock_uom = frappe.db.get_value("Item", putaway.item_code, "stock_uom") + putaway.uom = args.uom or putaway.stock_uom + putaway.conversion_factor = get_conversion_factor(putaway.item_code, putaway.uom)['conversion_factor'] + + if not args.do_not_save: + putaway.save() + + return putaway \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index f75e8b727d..726118d06d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1,9 +1,20 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt frappe.provide("erpnext.stock"); +frappe.provide("erpnext.accounts.dimensions"); + +{% include 'erpnext/stock/landed_taxes_and_charges_common.js' %}; frappe.ui.form.on('Stock Entry', { setup: function(frm) { + frm.set_indicator_formatter('item_code', function(doc) { + if (!doc.s_warehouse) { + return 'blue'; + } else { + return (doc.qty<=doc.actual_qty) ? 'green' : 'orange'; + } + }); + frm.set_query('work_order', function() { return { filters: [ @@ -86,17 +97,9 @@ frappe.ui.form.on('Stock Entry', { } }); - frm.set_query("expense_account", "additional_costs", function() { - return { - query: "erpnext.controllers.queries.tax_account_query", - filters: { - "account_type": ["Tax", "Chargeable", "Income Account", "Expenses Included In Valuation", "Expenses Included In Asset Valuation"], - "company": frm.doc.company - } - }; - }); frm.add_fetch("bom_no", "inspection_required", "inspection_required"); + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, setup_quality_inspection: function(frm) { @@ -312,6 +315,8 @@ frappe.ui.form.on('Stock Entry', { frm.set_value("letter_head", company_doc.default_letter_head); } frm.trigger("toggle_display_account_head"); + + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); } }, @@ -547,7 +552,7 @@ frappe.ui.form.on('Stock Entry', { calculate_total_additional_costs: function(frm) { const total_additional_costs = frappe.utils.sum( - (frm.doc.additional_costs || []).map(function(c) { return flt(c.amount); }) + (frm.doc.additional_costs || []).map(function(c) { return flt(c.base_amount); }) ); frm.set_value("total_additional_costs", @@ -580,8 +585,12 @@ frappe.ui.form.on('Stock Entry', { } }); } + }, + + apply_putaway_rule: function (frm) { + if (frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(frm, frm.doc.purpose); } -}) +}); frappe.ui.form.on('Stock Entry Detail', { qty: function(frm, cdt, cdn) { @@ -722,8 +731,18 @@ var validate_sample_quantity = function(frm, cdt, cdn) { }; frappe.ui.form.on('Landed Cost Taxes and Charges', { - amount: function(frm) { - frm.events.calculate_amount(frm); + amount: function(frm, cdt, cdn) { + frm.events.set_base_amount(frm, cdt, cdn); + + // Adding this check because same table in used in LCV + // This causes an error if you try to post an LCV immediately after a Stock Entry + if (frm.doc.doctype == 'Stock Entry') { + frm.events.calculate_amount(frm); + } + }, + + expense_account: function(frm, cdt, cdn) { + frm.events.set_account_currency(frm, cdt, cdn); } }); @@ -771,15 +790,6 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ } } - this.frm.set_indicator_formatter('item_code', - function(doc) { - if (!doc.s_warehouse) { - return 'blue'; - } else { - return (doc.qty<=doc.actual_qty) ? "green" : "orange" - } - }) - this.frm.add_fetch("purchase_order", "supplier", "supplier"); frappe.dynamic_link = { doc: this.frm.doc, fieldname: 'supplier', doctype: 'Supplier' } diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 5aed08102c..98c047a09e 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -27,6 +27,7 @@ "set_posting_time", "inspection_required", "from_bom", + "apply_putaway_rule", "sb1", "bom_no", "fg_completed_qty", @@ -640,6 +641,13 @@ "fieldtype": "Check", "label": "Add to Transit", "no_copy": 1 + }, + { + "default": "0", + "depends_on": "eval:in_list([\"Material Transfer\", \"Material Receipt\"], doc.purpose)", + "fieldname": "apply_putaway_rule", + "fieldtype": "Check", + "label": "Apply Putaway Rule" } ], "icon": "fa fa-file-text", @@ -647,7 +655,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-09-09 12:59:02.508943", + "modified": "2020-12-09 14:58:13.267321", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 9b8b3ba6c5..d77b70ff14 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -19,6 +19,7 @@ from frappe.model.mapper import get_mapped_doc from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit, get_serial_nos from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import OpeningEntryAccountError from erpnext.accounts.general_ledger import process_gl_map +from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals import json from six import string_types, itervalues, iteritems @@ -42,6 +43,14 @@ class StockEntry(StockController): for item in self.get("items"): item.update(get_bin_details(item.item_code, item.s_warehouse)) + def before_validate(self): + from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule + apply_rule = self.apply_putaway_rule and (self.purpose in ["Material Transfer", "Material Receipt"]) + + if self.get("items") and apply_rule: + apply_putaway_rule(self.doctype, self.get("items"), self.company, + purpose=self.purpose) + def validate(self): self.pro_doc = frappe._dict() if self.work_order: @@ -79,6 +88,7 @@ class StockEntry(StockController): self.validate_serialized_batch() self.set_actual_qty() self.calculate_rate_and_amount() + self.validate_putaway_capacity() def on_submit(self): self.update_stock_ledger() @@ -186,7 +196,7 @@ class StockEntry(StockController): and (sed.t_warehouse is null or sed.t_warehouse = '')""", self.project, as_list=1) amount = amount[0][0] if amount else 0 - additional_costs = frappe.db.sql(""" select ifnull(sum(sed.amount), 0) + additional_costs = frappe.db.sql(""" select ifnull(sum(sed.base_amount), 0) from `tabStock Entry` se, `tabLanded Cost Taxes and Charges` sed where @@ -436,6 +446,7 @@ class StockEntry(StockController): def calculate_rate_and_amount(self, reset_outgoing_rate=True, raise_error_if_no_rate=True): self.set_basic_rate(reset_outgoing_rate, raise_error_if_no_rate) + init_landed_taxes_and_totals(self) self.distribute_additional_costs() self.update_valuation_rate() self.set_total_incoming_outgoing_value() @@ -524,7 +535,7 @@ class StockEntry(StockController): if not any([d.item_code for d in self.items if d.t_warehouse]): self.additional_costs = [] - self.total_additional_costs = sum([flt(t.amount) for t in self.get("additional_costs")]) + self.total_additional_costs = sum([flt(t.base_amount) for t in self.get("additional_costs")]) if self.purpose in ("Repack", "Manufacture"): incoming_items_cost = sum([flt(t.basic_amount) for t in self.get("items") if t.is_finished_item]) @@ -764,13 +775,19 @@ class StockEntry(StockController): for d in self.get("items"): if d.t_warehouse: item_account_wise_additional_cost.setdefault((d.item_code, d.name), {}) - item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, 0.0) + item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, { + "amount": 0.0, + "base_amount": 0.0 + }) multiply_based_on = d.basic_amount if total_basic_amount else d.qty - item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account] += \ + item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["amount"] += \ flt(t.amount * multiply_based_on) / divide_based_on + item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["base_amount"] += \ + flt(t.base_amount * multiply_based_on) / divide_based_on + if item_account_wise_additional_cost: for d in self.get("items"): for account, amount in iteritems(item_account_wise_additional_cost.get((d.item_code, d.name), {})): @@ -781,7 +798,8 @@ class StockEntry(StockController): "against": d.expense_account, "cost_center": d.cost_center, "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": amount + "credit_in_account_currency": flt(amount["amount"]), + "credit": flt(amount["base_amount"]) }, item=d)) gl_entries.append(self.get_gl_dict({ @@ -789,7 +807,7 @@ class StockEntry(StockController): "against": account, "cost_center": d.cost_center, "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": -1 * amount # put it as negative credit instead of debit purposefully + "credit": -1 * amount['base_amount'] # put it as negative credit instead of debit purposefully }, item=d)) return process_gl_map(gl_entries) @@ -1337,9 +1355,6 @@ class StockEntry(StockController): frappe.MappingMismatchError) elif self.purpose == "Material Transfer" and self.add_to_transit: continue - elif mreq_item.warehouse != (item.s_warehouse if self.purpose == "Material Issue" else item.t_warehouse): - frappe.throw(_("Warehouse for row {0} does not match Material Request").format(item.idx), - frappe.MappingMismatchError) def validate_batch(self): if self.purpose in ["Material Transfer for Manufacture", "Manufacture", "Repack", "Send to Subcontractor"]: diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py index b78c6be983..b12a8547fe 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py @@ -53,6 +53,8 @@ def make_stock_entry(**args): args.target = args.to_warehouse if args.item_code: args.item = args.item_code + if args.apply_putaway_rule: + s.apply_putaway_rule = args.apply_putaway_rule if isinstance(args.qty, string_types): if '.' in args.qty: @@ -118,7 +120,8 @@ def make_stock_entry(**args): "t_warehouse": args.target, "qty": args.qty, "basic_rate": args.rate or args.basic_rate, - "conversion_factor": 1.0, + "conversion_factor": args.conversion_factor or 1.0, + "transfer_qty": flt(args.qty) * (flt(args.conversion_factor) or 1.0), "serial_no": args.serial_no, 'batch_no': args.batch_no, 'cost_center': args.cost_center, diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index b78ae6d79b..988ae92969 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-03-29 18:22:12", "doctype": "DocType", @@ -65,6 +66,7 @@ "against_stock_entry", "ste_detail", "po_detail", + "putaway_rule", "column_break_51", "reference_purchase_receipt", "quality_inspection" @@ -495,6 +497,16 @@ "fieldtype": "Check", "label": "Set Basic Rate Manually" }, + { + "depends_on": "eval:in_list([\"Material Transfer\", \"Material Receipt\"], parent.purpose)", + "fieldname": "putaway_rule", + "fieldtype": "Link", + "label": "Putaway Rule", + "no_copy": 1, + "options": "Putaway Rule", + "print_hide": 1, + "read_only": 1 + }, { "fieldname": "quantity_section", "fieldtype": "Section Break", @@ -526,7 +538,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-12-23 17:55:03.384138", + "modified": "2020-12-30 15:00:44.489442", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index e2121fce3f..ac4ed5e75d 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -2,6 +2,7 @@ // License: GNU General Public License v3. See license.txt frappe.provide("erpnext.stock"); +frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on("Stock Reconciliation", { onload: function(frm) { @@ -26,6 +27,12 @@ frappe.ui.form.on("Stock Reconciliation", { if (!frm.doc.expense_account) { frm.trigger("set_expense_account"); } + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); + }, + + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, refresh: function(frm) { diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 5b40292ea8..f0a90f9754 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -30,6 +30,7 @@ class StockReconciliation(StockController): self.validate_data() self.validate_expense_account() self.set_total_qty_and_amount() + self.validate_putaway_capacity() if self._action=="submit": self.make_batches('warehouse') diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 3ff396ba77..84af57b48d 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -16,6 +16,7 @@ "action_if_quality_inspection_is_not_submitted", "show_barcode_field", "clean_description_html", + "disable_serial_no_and_batch_selector", "section_break_7", "auto_insert_price_list_rate_if_missing", "allow_negative_stock", @@ -227,6 +228,12 @@ "fieldname": "control_historical_stock_transactions_section", "fieldtype": "Section Break", "label": "Control Historical Stock Transactions" + }, + { + "default": "0", + "fieldname": "disable_serial_no_and_batch_selector", + "fieldtype": "Check", + "label": "Disable Serial No And Batch Selector" } ], "icon": "icon-cog", @@ -234,7 +241,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-12-29 12:53:31.162247", + "modified": "2021-01-18 13:15:38.352796", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index bf45251c9d..dfe8fea67b 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -674,6 +674,8 @@ def get_item_price(args, item_code, ignore_party=False): and price_list=%(price_list)s and ifnull(uom, '') in ('', %(uom)s)""" + conditions += "and ifnull(batch_no, '') in ('', %(batch_no)s)" + if not ignore_party: if args.get("customer"): conditions += " and customer=%(customer)s" @@ -692,7 +694,7 @@ def get_item_price(args, item_code, ignore_party=False): return frappe.db.sql(""" select name, price_list_rate, uom from `tabItem Price` {conditions} - order by valid_from desc, uom desc """.format(conditions=conditions), args) + order by valid_from desc, batch_no desc, uom desc """.format(conditions=conditions), args) def get_price_list_rate_for(args, item_code): """ @@ -711,6 +713,7 @@ def get_price_list_rate_for(args, item_code): "uom": args.get('uom'), "transaction_date": args.get('transaction_date'), "posting_date": args.get('posting_date'), + "batch_no": args.get('batch_no') } item_price_data = 0 diff --git a/erpnext/stock/landed_taxes_and_charges_common.js b/erpnext/stock/landed_taxes_and_charges_common.js new file mode 100644 index 0000000000..f3f61963a8 --- /dev/null +++ b/erpnext/stock/landed_taxes_and_charges_common.js @@ -0,0 +1,62 @@ +let document_list = ['Landed Cost Voucher', 'Stock Entry']; + +document_list.forEach((doctype) => { + frappe.ui.form.on(doctype, { + refresh: function(frm) { + let tax_field = frm.doc.doctype == 'Landed Cost Voucher' ? 'taxes' : 'additional_costs'; + frm.set_query("expense_account", tax_field, function() { + return { + filters: { + "account_type": ['in', ["Tax", "Chargeable", "Income Account", "Expenses Included In Valuation", "Expenses Included In Asset Valuation"]], + "company": frm.doc.company + } + }; + }); + }, + + set_account_currency: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.expense_account) { + frappe.db.get_value('Account', row.expense_account, 'account_currency', function(value) { + frappe.model.set_value(cdt, cdn, "account_currency", value.account_currency); + frm.events.set_exchange_rate(frm, cdt, cdn); + }); + } + }, + + set_exchange_rate: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; + + if (row.account_currency == company_currency) { + row.exchange_rate = 1; + frm.set_df_property('taxes', 'hidden', 1, row.name, 'exchange_rate'); + } else if (!row.exchange_rate || row.exchange_rate == 1) { + frm.set_df_property('taxes', 'hidden', 0, row.name, 'exchange_rate'); + frappe.call({ + method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_exchange_rate", + args: { + posting_date: frm.doc.posting_date, + account: row.expense_account, + account_currency: row.account_currency, + company: frm.doc.company + }, + callback: function(r) { + if (r.message) { + frappe.model.set_value(cdt, cdn, "exchange_rate", r.message); + } + } + }); + } + + frm.refresh_field('taxes'); + }, + + set_base_amount: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + frappe.model.set_value(cdt, cdn, "base_amount", + flt(flt(row.amount)*row.exchange_rate, precision("base_amount", row))); + } + }); +}); + diff --git a/erpnext/stock/page/stock_balance/stock_balance.js b/erpnext/stock/page/stock_balance/stock_balance.js index da21c6bc64..bddffd465e 100644 --- a/erpnext/stock/page/stock_balance/stock_balance.js +++ b/erpnext/stock/page/stock_balance/stock_balance.js @@ -65,6 +65,9 @@ frappe.pages['stock-balance'].on_page_load = function(wrapper) { frappe.require('assets/js/item-dashboard.min.js', function() { page.item_dashboard = new erpnext.stock.ItemDashboard({ parent: page.main, + page_length: 20, + method: 'erpnext.stock.dashboard.item_dashboard.get_data', + template: 'item_dashboard_list' }) page.item_dashboard.before_refresh = function() { diff --git a/erpnext/stock/page/warehouse_capacity_summary/__init__.py b/erpnext/stock/page/warehouse_capacity_summary/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html new file mode 100644 index 0000000000..90112c78a8 --- /dev/null +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html @@ -0,0 +1,40 @@ +{% for d in data %} +
+
+ + +
+ {{ d.stock_capacity }} +
+
+ {{ d.actual_qty }} +
+
+
+
+
+
+
+
+ {{ d.percent_occupied }}% +
+ {% if can_write %} +
+
+ {% endif %} +
+
+{% endfor %} \ No newline at end of file diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js new file mode 100644 index 0000000000..b610e7dd58 --- /dev/null +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js @@ -0,0 +1,120 @@ +frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { + var page = frappe.ui.make_app_page({ + parent: wrapper, + title: 'Warehouse Capacity Summary', + single_column: true + }); + page.set_secondary_action('Refresh', () => page.capacity_dashboard.refresh(), 'octicon octicon-sync'); + page.start = 0; + + page.company_field = page.add_field({ + fieldname: 'company', + label: __('Company'), + fieldtype: 'Link', + options: 'Company', + reqd: 1, + default: frappe.defaults.get_default("company"), + change: function() { + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + page.warehouse_field = page.add_field({ + fieldname: 'warehouse', + label: __('Warehouse'), + fieldtype: 'Link', + options: 'Warehouse', + change: function() { + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + page.item_field = page.add_field({ + fieldname: 'item_code', + label: __('Item'), + fieldtype: 'Link', + options: 'Item', + change: function() { + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + page.parent_warehouse_field = page.add_field({ + fieldname: 'parent_warehouse', + label: __('Parent Warehouse'), + fieldtype: 'Link', + options: 'Warehouse', + get_query: function() { + return { + filters: { + "is_group": 1 + } + }; + }, + change: function() { + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + page.sort_selector = new frappe.ui.SortSelector({ + parent: page.wrapper.find('.page-form'), + args: { + sort_by: 'stock_capacity', + sort_order: 'desc', + options: [ + {fieldname: 'stock_capacity', label: __('Capacity (Stock UOM)')}, + {fieldname: 'percent_occupied', label: __('% Occupied')}, + {fieldname: 'actual_qty', label: __('Balance Qty (Stock ')} + ] + }, + change: function(sort_by, sort_order) { + page.capacity_dashboard.sort_by = sort_by; + page.capacity_dashboard.sort_order = sort_order; + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + frappe.require('assets/js/item-dashboard.min.js', function() { + $(frappe.render_template('warehouse_capacity_summary_header')).appendTo(page.main); + + page.capacity_dashboard = new erpnext.stock.ItemDashboard({ + page_name: "warehouse-capacity-summary", + page_length: 10, + parent: page.main, + sort_by: 'stock_capacity', + sort_order: 'desc', + method: 'erpnext.stock.dashboard.warehouse_capacity_dashboard.get_data', + template: 'warehouse_capacity_summary' + }); + + page.capacity_dashboard.before_refresh = function() { + this.item_code = page.item_field.get_value(); + this.warehouse = page.warehouse_field.get_value(); + this.parent_warehouse = page.parent_warehouse_field.get_value(); + this.company = page.company_field.get_value(); + }; + + page.capacity_dashboard.refresh(); + + let setup_click = function(doctype) { + page.main.on('click', 'a[data-type="'+ doctype.toLowerCase() +'"]', function() { + var name = $(this).attr('data-name'); + var field = page[doctype.toLowerCase() + '_field']; + if (field.get_value()===name) { + frappe.set_route('Form', doctype, name); + } else { + field.set_input(name); + page.capacity_dashboard.refresh(); + } + }); + }; + + setup_click('Item'); + setup_click('Warehouse'); + }); +}; \ No newline at end of file diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.json b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.json new file mode 100644 index 0000000000..a6e5b45332 --- /dev/null +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.json @@ -0,0 +1,26 @@ +{ + "content": null, + "creation": "2020-11-25 12:07:54.056208", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2020-11-25 11:07:54.056208", + "modified_by": "Administrator", + "module": "Stock", + "name": "warehouse-capacity-summary", + "owner": "Administrator", + "page_name": "Warehouse Capacity Summary", + "roles": [ + { + "role": "Stock User" + }, + { + "role": "Stock Manager" + } + ], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 0, + "title": "Warehouse Capacity Summary" +} \ No newline at end of file diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html new file mode 100644 index 0000000000..acaf180a90 --- /dev/null +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html @@ -0,0 +1,19 @@ +
+
+
+ Warehouse +
+
+ Item +
+
+ Stock Capacity +
+
+ Balance Stock Qty +
+
+ % Occupied +
+
+
\ No newline at end of file diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 8aaf7abcbe..ff603fcfb3 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -233,7 +233,8 @@ def get_stock_ledger_entries(filters): from `tabItem` {item_conditions}) item where item_code = item.name and company = %(company)s and - posting_date <= %(to_date)s + posting_date <= %(to_date)s and + is_cancelled != 1 {sle_conditions} order by posting_date, posting_time, sle.creation, actual_qty""" #nosec .format(item_conditions=get_item_conditions(filters), diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 5b9ada0ee5..2b2a7a202d 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -41,7 +41,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc if sle.get("actual_qty") or sle.get("voucher_type")=="Stock Reconciliation": sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher) - + args = sle_doc.as_dict() update_bin(args, allow_negative_stock, via_landed_cost_voucher) @@ -65,7 +65,7 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False): def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negative_stock=False, via_landed_cost_voucher=False): if not args and voucher_type and voucher_no: args = get_args_for_voucher(voucher_type, voucher_no) - + distinct_item_warehouses = [(d.item_code, d.warehouse) for d in args] i = 0 @@ -80,7 +80,7 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat for item_wh, new_sle in iteritems(obj.new_items): if item_wh not in distinct_item_warehouses: args.append(new_sle) - + i += 1 def get_args_for_voucher(voucher_type, voucher_no): @@ -127,7 +127,7 @@ class update_entries_after(object): self.initialize_previous_data(self.args) self.build() - + def get_precision(self): company_base_currency = frappe.get_cached_value('Company', self.company, "default_currency") self.precision = get_field_precision(frappe.get_meta("Stock Ledger Entry").get_field("stock_value"), @@ -213,13 +213,13 @@ class update_entries_after(object): # includes current entry! args = self.data[self.args.warehouse].previous_sle \ or frappe._dict({"item_code": self.item_code, "warehouse": self.args.warehouse}) - + return list(self.get_sle_after_datetime(args)) def get_dependent_entries_to_fix(self, entries_to_fix, sle): dependant_sle = get_sle_by_voucher_detail_no(sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name) - + if not dependant_sle: return elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse: @@ -251,7 +251,7 @@ class update_entries_after(object): # Get dynamic incoming/outgoing rate self.get_dynamic_incoming_outgoing_rate(sle) - + if sle.serial_no: self.get_serialized_values(sle) self.wh_data.qty_after_transaction += flt(sle.actual_qty) @@ -329,7 +329,7 @@ class update_entries_after(object): rate = get_rate_for_return(sle.voucher_type, sle.voucher_no, sle.item_code, voucher_detail_no=sle.voucher_detail_no) else: if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"): - rate_field = "valuation_rate" + rate_field = "valuation_rate" else: rate_field = "incoming_rate" @@ -344,7 +344,7 @@ class update_entries_after(object): ref_doctype = "Packed Item" else: ref_doctype = "Purchase Receipt Item Supplied" - + rate = frappe.db.get_value(ref_doctype, {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code}, rate_field) @@ -374,7 +374,7 @@ class update_entries_after(object): stock_entry.db_update() for d in stock_entry.items: d.db_update() - + def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate): # Update item's incoming rate on transaction item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code") @@ -487,7 +487,6 @@ class update_entries_after(object): self.wh_data.valuation_rate = new_stock_value / new_stock_qty else: self.wh_data.valuation_rate = sle.outgoing_rate - else: if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate: self.wh_data.valuation_rate = sle.outgoing_rate @@ -631,7 +630,7 @@ class update_entries_after(object): frappe.throw(message, NegativeStockError, title='Insufficient Stock') else: raise NegativeStockError(message) - + def update_bin(self): # update bin for each warehouse for warehouse, data in iteritems(self.data): @@ -766,7 +765,7 @@ def update_qty_in_future_sle(args, allow_negative_stock=None): frappe.db.sql(""" update `tabStock Ledger Entry` set qty_after_transaction = qty_after_transaction + {qty} - where + where item_code = %(item_code)s and warehouse = %(warehouse)s and voucher_no != %(voucher_no)s @@ -794,7 +793,7 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=None): frappe.get_desk_link('Warehouse', args.warehouse), sle[0]["posting_date"], sle[0]["posting_time"], frappe.get_desk_link(sle[0]["voucher_type"], sle[0]["voucher_no"])) - + frappe.throw(message, NegativeStockError, title='Insufficient Stock') def get_future_sle_with_negative_qty(args): @@ -803,7 +802,7 @@ def get_future_sle_with_negative_qty(args): qty_after_transaction, posting_date, posting_time, voucher_type, voucher_no from `tabStock Ledger Entry` - where + where item_code = %(item_code)s and warehouse = %(warehouse)s and voucher_no != %(voucher_no)s diff --git a/erpnext/support/desk_page/support/support.json b/erpnext/support/desk_page/support/support.json index 18cf87ab0b..dba2b1463f 100644 --- a/erpnext/support/desk_page/support/support.json +++ b/erpnext/support/desk_page/support/support.json @@ -28,7 +28,7 @@ { "hidden": 0, "label": "Reports", - "links": "[\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"First Response Time for Issues\",\n \"name\": \"First Response Time for Issues\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"Issue Summary\",\n \"name\": \"Issue Summary\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"First Response Time for Issues\",\n \"name\": \"First Response Time for Issues\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"Issue Analytics\",\n \"name\": \"Issue Analytics\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"Issue Summary\",\n \"name\": \"Issue Summary\",\n \"type\": \"report\"\n }\n]" } ], "category": "Modules", @@ -43,7 +43,7 @@ "idx": 0, "is_standard": 1, "label": "Support", - "modified": "2020-10-12 18:40:22.252915", + "modified": "2021-01-13 20:15:03.064256", "modified_by": "Administrator", "module": "Support", "name": "Support", diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 62b39cced5..1ac295919b 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -214,7 +214,10 @@ class Issue(Document): def before_insert(self): if frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): - self.set_response_and_resolution_time() + if frappe.flags.in_test: + self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) + else: + 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, diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index c962dc6b31..483bb155db 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -135,15 +135,19 @@ class TestIssue(unittest.TestCase): self.assertEqual(flt(issue.total_hold_time, 2), 2700) -def make_issue(creation=None, customer=None, index=0): +def make_issue(creation=None, customer=None, index=0, priority=None, issue_type=None): issue = frappe.get_doc({ "doctype": "Issue", "subject": "Service Level Agreement Issue {0}".format(index), "customer": customer, "raised_by": "test@example.com", "description": "Service Level Agreement Issue", + "issue_type": issue_type, + "priority": priority, "creation": creation, - "service_level_agreement_creation": creation + "opening_date": creation, + "service_level_agreement_creation": creation, + "company": "_Test Company" }).insert(ignore_permissions=True) return issue diff --git a/erpnext/support/report/issue_analytics/__init__.py b/erpnext/support/report/issue_analytics/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/support/report/issue_analytics/issue_analytics.js b/erpnext/support/report/issue_analytics/issue_analytics.js new file mode 100644 index 0000000000..f87b2c2ddd --- /dev/null +++ b/erpnext/support/report/issue_analytics/issue_analytics.js @@ -0,0 +1,141 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Issue Analytics"] = { + "filters": [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + fieldname: "based_on", + label: __("Based On"), + fieldtype: "Select", + options: ["Customer", "Issue Type", "Issue Priority", "Assigned To"], + default: "Customer", + reqd: 1 + }, + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.defaults.get_global_default("year_start_date"), + reqd: 1 + }, + { + fieldname:"to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.defaults.get_global_default("year_end_date"), + reqd: 1 + }, + { + fieldname: "range", + label: __("Range"), + fieldtype: "Select", + options: [ + { "value": "Weekly", "label": __("Weekly") }, + { "value": "Monthly", "label": __("Monthly") }, + { "value": "Quarterly", "label": __("Quarterly") }, + { "value": "Yearly", "label": __("Yearly") } + ], + default: "Monthly", + reqd: 1 + }, + { + fieldname: "status", + label: __("Status"), + fieldtype: "Select", + options:[ + {label: __('Open'), value: 'Open'}, + {label: __('Replied'), value: 'Replied'}, + {label: __('Resolved'), value: 'Resolved'}, + {label: __('Closed'), value: 'Closed'} + ] + }, + { + fieldname: "priority", + label: __("Issue Priority"), + fieldtype: "Link", + options: "Issue Priority" + }, + { + fieldname: "customer", + label: __("Customer"), + fieldtype: "Link", + options: "Customer" + }, + { + fieldname: "project", + label: __("Project"), + fieldtype: "Link", + options: "Project" + }, + { + fieldname: "assigned_to", + label: __("Assigned To"), + fieldtype: "Link", + options: "User" + } + ], + after_datatable_render: function(datatable_obj) { + $(datatable_obj.wrapper).find(".dt-row-0").find('input[type=checkbox]').click(); + }, + get_datatable_options(options) { + return Object.assign(options, { + checkboxColumn: true, + events: { + onCheckRow: function(data) { + if (data && data.length) { + row_name = data[2].content; + row_values = data.slice(3).map(function(column) { + return column.content; + }) + entry = { + 'name': row_name, + 'values': row_values + } + + let raw_data = frappe.query_report.chart.data; + let new_datasets = raw_data.datasets; + + var found = false; + + for(var i=0; i < new_datasets.length; i++){ + if (new_datasets[i].name == row_name){ + found = true; + new_datasets.splice(i,1); + break; + } + } + + if (!found){ + new_datasets.push(entry); + } + + let new_data = { + labels: raw_data.labels, + datasets: new_datasets + } + + setTimeout(() => { + frappe.query_report.chart.update(new_data) + },500) + + + setTimeout(() => { + frappe.query_report.chart.draw(true); + }, 1000) + + frappe.query_report.raw_chart_data = new_data; + } + }, + } + }); + } +}; \ No newline at end of file diff --git a/erpnext/support/report/issue_analytics/issue_analytics.json b/erpnext/support/report/issue_analytics/issue_analytics.json new file mode 100644 index 0000000000..dd18498d1d --- /dev/null +++ b/erpnext/support/report/issue_analytics/issue_analytics.json @@ -0,0 +1,26 @@ +{ + "add_total_row": 1, + "columns": [], + "creation": "2020-10-09 19:52:10.227317", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2020-10-11 19:43:19.358625", + "modified_by": "Administrator", + "module": "Support", + "name": "Issue Analytics", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Issue", + "report_name": "Issue Analytics", + "report_type": "Script Report", + "roles": [ + { + "role": "Support Team" + } + ] +} \ No newline at end of file diff --git a/erpnext/support/report/issue_analytics/issue_analytics.py b/erpnext/support/report/issue_analytics/issue_analytics.py new file mode 100644 index 0000000000..0b629151a6 --- /dev/null +++ b/erpnext/support/report/issue_analytics/issue_analytics.py @@ -0,0 +1,222 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import json +from six import iteritems +from frappe import _, scrub +from frappe.utils import getdate, flt, add_to_date, add_days +from erpnext.accounts.utils import get_fiscal_year + +def execute(filters=None): + return IssueAnalytics(filters).run() + +class IssueAnalytics(object): + def __init__(self, filters=None): + """Issue Analytics Report""" + self.filters = frappe._dict(filters or {}) + self.get_period_date_ranges() + + def run(self): + self.get_columns() + self.get_data() + self.get_chart_data() + + return self.columns, self.data, None, self.chart + + def get_columns(self): + self.columns = [] + + if self.filters.based_on == 'Customer': + self.columns.append({ + 'label': _('Customer'), + 'options': 'Customer', + 'fieldname': 'customer', + 'fieldtype': 'Link', + 'width': 200 + }) + + elif self.filters.based_on == 'Assigned To': + self.columns.append({ + 'label': _('User'), + 'fieldname': 'user', + 'fieldtype': 'Link', + 'options': 'User', + 'width': 200 + }) + + elif self.filters.based_on == 'Issue Type': + self.columns.append({ + 'label': _('Issue Type'), + 'fieldname': 'issue_type', + 'fieldtype': 'Link', + 'options': 'Issue Type', + 'width': 200 + }) + + elif self.filters.based_on == 'Issue Priority': + self.columns.append({ + 'label': _('Issue Priority'), + 'fieldname': 'priority', + 'fieldtype': 'Link', + 'options': 'Issue Priority', + 'width': 200 + }) + + for end_date in self.periodic_daterange: + period = self.get_period(end_date) + self.columns.append({ + 'label': _(period), + 'fieldname': scrub(period), + 'fieldtype': 'Int', + 'width': 120 + }) + + self.columns.append({ + 'label': _('Total'), + 'fieldname': 'total', + 'fieldtype': 'Int', + 'width': 120 + }) + + def get_data(self): + self.get_issues() + self.get_rows() + + def get_period(self, date): + months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + + if self.filters.range == 'Weekly': + period = 'Week ' + str(date.isocalendar()[1]) + elif self.filters.range == 'Monthly': + period = str(months[date.month - 1]) + elif self.filters.range == 'Quarterly': + period = 'Quarter ' + str(((date.month - 1) // 3) + 1) + else: + year = get_fiscal_year(date, self.filters.company) + period = str(year[0]) + + if getdate(self.filters.from_date).year != getdate(self.filters.to_date).year and self.filters.range != 'Yearly': + period += ' ' + str(date.year) + + return period + + def get_period_date_ranges(self): + from dateutil.relativedelta import relativedelta, MO + from_date, to_date = getdate(self.filters.from_date), getdate(self.filters.to_date) + + increment = { + 'Monthly': 1, + 'Quarterly': 3, + 'Half-Yearly': 6, + 'Yearly': 12 + }.get(self.filters.range, 1) + + if self.filters.range in ['Monthly', 'Quarterly']: + from_date = from_date.replace(day=1) + elif self.filters.range == 'Yearly': + from_date = get_fiscal_year(from_date)[1] + else: + from_date = from_date + relativedelta(from_date, weekday=MO(-1)) + + self.periodic_daterange = [] + for dummy in range(1, 53): + if self.filters.range == 'Weekly': + period_end_date = add_days(from_date, 6) + else: + period_end_date = add_to_date(from_date, months=increment, days=-1) + + if period_end_date > to_date: + period_end_date = to_date + + self.periodic_daterange.append(period_end_date) + + from_date = add_days(period_end_date, 1) + if period_end_date == to_date: + break + + def get_issues(self): + filters = self.get_common_filters() + self.field_map = { + 'Customer': 'customer', + 'Issue Type': 'issue_type', + 'Issue Priority': 'priority', + 'Assigned To': '_assign' + } + + self.entries = frappe.db.get_all('Issue', + fields=[self.field_map.get(self.filters.based_on), 'name', 'opening_date'], + filters=filters, + debug=1 + ) + + def get_common_filters(self): + filters = {} + filters['opening_date'] = ('between', [self.filters.from_date, self.filters.to_date]) + + if self.filters.get('assigned_to'): + filters['_assign'] = ('like', '%' + self.filters.get('assigned_to') + '%') + + for entry in ['company', 'status', 'priority', 'customer', 'project']: + if self.filters.get(entry): + filters[entry] = self.filters.get(entry) + + return filters + + def get_rows(self): + self.data = [] + self.get_periodic_data() + + for entity, period_data in iteritems(self.issue_periodic_data): + if self.filters.based_on == 'Customer': + row = {'customer': entity} + elif self.filters.based_on == 'Assigned To': + row = {'user': entity} + elif self.filters.based_on == 'Issue Type': + row = {'issue_type': entity} + elif self.filters.based_on == 'Issue Priority': + row = {'priority': entity} + + total = 0 + for end_date in self.periodic_daterange: + period = self.get_period(end_date) + amount = flt(period_data.get(period, 0.0)) + row[scrub(period)] = amount + total += amount + + row['total'] = total + + self.data.append(row) + + def get_periodic_data(self): + self.issue_periodic_data = frappe._dict() + + for d in self.entries: + period = self.get_period(d.get('opening_date')) + + if self.filters.based_on == 'Assigned To': + if d._assign: + for entry in json.loads(d._assign): + self.issue_periodic_data.setdefault(entry, frappe._dict()).setdefault(period, 0.0) + self.issue_periodic_data[entry][period] += 1 + + else: + field = self.field_map.get(self.filters.based_on) + value = d.get(field) + if not value: + value = _('Not Specified') + + self.issue_periodic_data.setdefault(value, frappe._dict()).setdefault(period, 0.0) + self.issue_periodic_data[value][period] += 1 + + def get_chart_data(self): + length = len(self.columns) + labels = [d.get('label') for d in self.columns[1:length-1]] + self.chart = { + 'data': { + 'labels': labels, + 'datasets': [] + }, + 'type': 'line' + } \ No newline at end of file diff --git a/erpnext/support/report/issue_analytics/test_issue_analytics.py b/erpnext/support/report/issue_analytics/test_issue_analytics.py new file mode 100644 index 0000000000..432906db9b --- /dev/null +++ b/erpnext/support/report/issue_analytics/test_issue_analytics.py @@ -0,0 +1,211 @@ +from __future__ import unicode_literals +import unittest +import frappe +from frappe.utils import getdate, add_months +from erpnext.support.report.issue_analytics.issue_analytics import execute +from erpnext.support.doctype.issue.test_issue import make_issue, create_customer +from erpnext.support.doctype.service_level_agreement.test_service_level_agreement import create_service_level_agreements_for_issues +from frappe.desk.form.assign_to import add as add_assignment + +months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + +class TestIssueAnalytics(unittest.TestCase): + @classmethod + def setUpClass(self): + frappe.db.sql("delete from `tabIssue` where company='_Test Company'") + frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1) + + current_month_date = getdate() + last_month_date = add_months(current_month_date, -1) + self.current_month = str(months[current_month_date.month - 1]).lower() + '_' + str(current_month_date.year) + self.last_month = str(months[last_month_date.month - 1]).lower() + '_' + str(last_month_date.year) + + def test_issue_analytics(self): + create_service_level_agreements_for_issues() + create_issue_types() + create_records() + + self.compare_result_for_customer() + self.compare_result_for_issue_type() + self.compare_result_for_issue_priority() + self.compare_result_for_assignment() + + def compare_result_for_customer(self): + filters = { + 'company': '_Test Company', + 'based_on': 'Customer', + 'from_date': add_months(getdate(), -1), + 'to_date': getdate(), + 'range': 'Monthly' + } + + report = execute(filters) + + expected_data = [ + { + 'customer': '__Test Customer 2', + self.last_month: 1.0, + self.current_month: 0.0, + 'total': 1.0 + }, + { + 'customer': '__Test Customer 1', + self.last_month: 0.0, + self.current_month: 1.0, + 'total': 1.0 + }, + { + 'customer': '__Test Customer', + self.last_month: 1.0, + self.current_month: 1.0, + 'total': 2.0 + } + ] + + self.assertEqual(expected_data, report[1]) # rows + self.assertEqual(len(report[0]), 4) # cols + + def compare_result_for_issue_type(self): + filters = { + 'company': '_Test Company', + 'based_on': 'Issue Type', + 'from_date': add_months(getdate(), -1), + 'to_date': getdate(), + 'range': 'Monthly' + } + + report = execute(filters) + + expected_data = [ + { + 'issue_type': 'Discomfort', + self.last_month: 1.0, + self.current_month: 0.0, + 'total': 1.0 + }, + { + 'issue_type': 'Service Request', + self.last_month: 0.0, + self.current_month: 1.0, + 'total': 1.0 + }, + { + 'issue_type': 'Bug', + self.last_month: 1.0, + self.current_month: 1.0, + 'total': 2.0 + } + ] + + self.assertEqual(expected_data, report[1]) # rows + self.assertEqual(len(report[0]), 4) # cols + + def compare_result_for_issue_priority(self): + filters = { + 'company': '_Test Company', + 'based_on': 'Issue Priority', + 'from_date': add_months(getdate(), -1), + 'to_date': getdate(), + 'range': 'Monthly' + } + + report = execute(filters) + + expected_data = [ + { + 'priority': 'Medium', + self.last_month: 1.0, + self.current_month: 1.0, + 'total': 2.0 + }, + { + 'priority': 'Low', + self.last_month: 1.0, + self.current_month: 0.0, + 'total': 1.0 + }, + { + 'priority': 'High', + self.last_month: 0.0, + self.current_month: 1.0, + 'total': 1.0 + } + ] + + self.assertEqual(expected_data, report[1]) # rows + self.assertEqual(len(report[0]), 4) # cols + + def compare_result_for_assignment(self): + filters = { + 'company': '_Test Company', + 'based_on': 'Assigned To', + 'from_date': add_months(getdate(), -1), + 'to_date': getdate(), + 'range': 'Monthly' + } + + report = execute(filters) + + expected_data = [ + { + 'user': 'test@example.com', + self.last_month: 1.0, + self.current_month: 1.0, + 'total': 2.0 + }, + { + 'user': 'test1@example.com', + self.last_month: 2.0, + self.current_month: 1.0, + 'total': 3.0 + } + ] + + self.assertEqual(expected_data, report[1]) # rows + self.assertEqual(len(report[0]), 4) # cols + + +def create_issue_types(): + for entry in ['Bug', 'Service Request', 'Discomfort']: + if not frappe.db.exists('Issue Type', entry): + frappe.get_doc({ + 'doctype': 'Issue Type', + '__newname': entry + }).insert() + + +def create_records(): + create_customer("__Test Customer", "_Test SLA Customer Group", "__Test SLA Territory") + create_customer("__Test Customer 1", "_Test SLA Customer Group", "__Test SLA Territory") + create_customer("__Test Customer 2", "_Test SLA Customer Group", "__Test SLA Territory") + + current_month_date = getdate() + last_month_date = add_months(current_month_date, -1) + + issue = make_issue(current_month_date, "__Test Customer", 2, "High", "Bug") + add_assignment({ + "assign_to": ["test@example.com"], + "doctype": "Issue", + "name": issue.name + }) + + issue = make_issue(last_month_date, "__Test Customer", 2, "Low", "Bug") + add_assignment({ + "assign_to": ["test1@example.com"], + "doctype": "Issue", + "name": issue.name + }) + + issue = make_issue(current_month_date, "__Test Customer 1", 2, "Medium", "Service Request") + add_assignment({ + "assign_to": ["test1@example.com"], + "doctype": "Issue", + "name": issue.name + }) + + issue = make_issue(last_month_date, "__Test Customer 2", 2, "Medium", "Discomfort") + add_assignment({ + "assign_to": ["test@example.com", "test1@example.com"], + "doctype": "Issue", + "name": issue.name + }) \ No newline at end of file diff --git a/erpnext/templates/generators/job_opening.html b/erpnext/templates/generators/job_opening.html index f92e72eaa7..c562db3c25 100644 --- a/erpnext/templates/generators/job_opening.html +++ b/erpnext/templates/generators/job_opening.html @@ -13,10 +13,21 @@ {%- if description -%}
{{ description }}
{% endif %} + +{%- if publish_salary_range -%} +
{{_("Salary range per month")}}: {{ frappe.format_value(frappe.utils.flt(lower_range), currency=currency) }} - {{ frappe.format_value(frappe.utils.flt(upper_range), currency=currency) }}
+{% endif %} +

- + {{ _("Apply Now") }} + {% else %} + {{ _("Apply Now") }} + {% endif %}

{% endblock %} diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html index ea6b00fc58..5d8ee5cab6 100644 --- a/erpnext/templates/includes/macros.html +++ b/erpnext/templates/includes/macros.html @@ -40,7 +40,7 @@
{% if card.image %} -
+
{% endif %}
{{ card.title }}