From 9d829532420689bb2d4692b77180f3c47c54e3b5 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Thu, 6 Aug 2020 23:58:56 +0530 Subject: [PATCH 001/295] fix: Opportunity Status fix --- erpnext/selling/doctype/quotation/quotation.py | 17 ++++++++--------- .../selling/doctype/sales_order/sales_order.py | 1 - 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 449a968a4f9..01479a16540 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -25,7 +25,6 @@ class Quotation(SellingController): def validate(self): super(Quotation, self).validate() self.set_status() - self.update_opportunity() self.validate_uom_is_integer("stock_uom", "qty") self.validate_valid_till() self.set_customer_name() @@ -50,20 +49,20 @@ class Quotation(SellingController): lead_name, company_name = frappe.db.get_value("Lead", self.party_name, ["lead_name", "company_name"]) self.customer_name = company_name or lead_name - def update_opportunity(self): + def update_opportunity(self, status): for opportunity in list(set([d.prevdoc_docname for d in self.get("items")])): if opportunity: - self.update_opportunity_status(opportunity) + self.update_opportunity_status(status, opportunity) if self.opportunity: - self.update_opportunity_status() + self.update_opportunity_status(status) - def update_opportunity_status(self, opportunity=None): + def update_opportunity_status(self, status, opportunity=None): if not opportunity: opportunity = self.opportunity opp = frappe.get_doc("Opportunity", opportunity) - opp.status = None + opp.status = status opp.set_status(update=True) def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None): @@ -82,7 +81,7 @@ class Quotation(SellingController): else: frappe.throw(_("Invalid lost reason {0}, please create a new lost reason").format(frappe.bold(reason.get('lost_reason')))) - self.update_opportunity() + self.update_opportunity('Lost') self.update_lead() self.save() @@ -95,7 +94,7 @@ class Quotation(SellingController): self.company, self.base_grand_total, self) #update enquiry status - self.update_opportunity() + self.update_opportunity('Quotation') self.update_lead() def on_cancel(self): @@ -105,7 +104,7 @@ class Quotation(SellingController): #update enquiry status self.set_status(update=True) - self.update_opportunity() + self.update_opportunity('Open') self.update_lead() def print_other_charges(self,docname): diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index ffb66354fa0..f17af69e5be 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -159,7 +159,6 @@ class SalesOrder(SellingController): frappe.throw(_("Quotation {0} is cancelled").format(quotation)) doc.set_status(update=True) - doc.update_opportunity() def validate_drop_ship(self): for d in self.get('items'): From 288ced24dbf771988b1c601d2e6d5f0fde07bd12 Mon Sep 17 00:00:00 2001 From: michellealva Date: Sun, 30 Aug 2020 19:33:27 +0530 Subject: [PATCH 002/295] feat: Add Naming Series for Project DocType --- erpnext/projects/doctype/project/project.json | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index f3cecd9059b..122a1a96f4e 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,22 @@ "fieldtype": "Text", "label": "Message", "mandatory_depends_on": "collect_progress" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "hidden": 1, + "label": "Series", + "options": "PROJ.####", + "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-08-30 19:32:40.050707", "modified_by": "Administrator", "module": "Projects", "name": "Project", @@ -488,5 +498,6 @@ "sort_field": "modified", "sort_order": "DESC", "timeline_field": "customer", + "title_field": "project_name", "track_seen": 1 -} +} \ No newline at end of file From f46c1c5164b44e7f8ce3ad65a90189688f291740 Mon Sep 17 00:00:00 2001 From: michellealva Date: Sun, 30 Aug 2020 21:37:45 +0530 Subject: [PATCH 003/295] fix: Change property of field --- erpnext/projects/doctype/project/project.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index 122a1a96f4e..c91b01eaa9f 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -445,9 +445,9 @@ { "fieldname": "naming_series", "fieldtype": "Select", - "hidden": 1, "label": "Series", "options": "PROJ.####", + "reqd": 1, "set_only_once": 1 } ], @@ -456,7 +456,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 4, - "modified": "2020-08-30 19:32:40.050707", + "modified": "2020-08-30 21:36:45.915818", "modified_by": "Administrator", "module": "Projects", "name": "Project", From f51cf9f23e0e41e6db5b5538ba2c4f75b24bfe74 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 2 Sep 2020 11:43:36 +0530 Subject: [PATCH 004/295] fix: Crop Cycle Test --- erpnext/agriculture/doctype/crop_cycle/crop_cycle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py index cae150c428a..afbd9b4e6e0 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({ From 099d6718c914198475153bf6498cc57ed6fd6396 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 2 Sep 2020 11:54:55 +0530 Subject: [PATCH 005/295] fix: Dont Copy or Print Naming Series --- erpnext/projects/doctype/project/project.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index c91b01eaa9f..8ed68888541 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -446,7 +446,9 @@ "fieldname": "naming_series", "fieldtype": "Select", "label": "Series", + "no_copy": 1, "options": "PROJ.####", + "print_hide": 1, "reqd": 1, "set_only_once": 1 } @@ -456,7 +458,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 4, - "modified": "2020-08-30 21:36:45.915818", + "modified": "2020-09-02 11:54:01.223620", "modified_by": "Administrator", "module": "Projects", "name": "Project", From d8c38249e03e2206c41cd86c64d31cae431845e5 Mon Sep 17 00:00:00 2001 From: Michelle Alva <50285544+michellealva@users.noreply.github.com> Date: Thu, 3 Sep 2020 09:04:21 +0530 Subject: [PATCH 006/295] fix: Change naming series Co-authored-by: Himanshu --- erpnext/projects/doctype/project/project.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index 8ed68888541..3cdfcb212f5 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -447,7 +447,7 @@ "fieldtype": "Select", "label": "Series", "no_copy": 1, - "options": "PROJ.####", + "options": "PROJ-.####", "print_hide": 1, "reqd": 1, "set_only_once": 1 @@ -502,4 +502,4 @@ "timeline_field": "customer", "title_field": "project_name", "track_seen": 1 -} \ No newline at end of file +} From 73d944da21045d1f6387b5fc583e37e37850c30d Mon Sep 17 00:00:00 2001 From: Anupam Date: Tue, 13 Oct 2020 18:11:05 +0530 Subject: [PATCH 007/295] fix: review changes --- erpnext/selling/doctype/quotation/quotation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 7c55d7742f8..3157982d528 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -62,7 +62,7 @@ class Quotation(SellingController): opportunity = self.opportunity opp = frappe.get_doc("Opportunity", opportunity) - opp.status = status + opp.set_status(status=status) opp.set_status(update=True) def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None): From e927224fbc45b54929825b015ad7da161bc984d9 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 4 Nov 2020 19:57:47 +0530 Subject: [PATCH 008/295] feat: update membership setting doctype * rename enable_auto_invoicing to enable_invoicing * add option to make_payment entry --- .../membership_settings.json | 42 +++++++++++++------ .../membership_type/membership_type.js | 4 +- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 5b6bab5b0a0..a70c3c4b8ae 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -11,9 +11,11 @@ "billing_frequency", "webhook_secret", "column_break_6", - "enable_auto_invoicing", + "enable_invoicing", + "make_payment_entry", "company", "debit_account", + "payment_account", "column_break_9", "send_email", "send_invoice", @@ -58,14 +60,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 +72,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 +81,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 +114,34 @@ "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", + "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" } ], + "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-08-05 17:26:37.287395", + "modified": "2020-11-04 19:51:21.990595", "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 43311a2c965..94ccdd83345 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.js +++ b/erpnext/non_profit/doctype/membership_type/membership_type.js @@ -2,12 +2,12 @@ // For license information, please see license.txt frappe.ui.form.on('Membership Type', { - refresh: function(frm) { + 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); }); } From d75ff1a93e562ac5e22dc5afd2aef20fc8c62ab1 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 4 Nov 2020 20:17:33 +0530 Subject: [PATCH 009/295] feat: generate invoice on payment authorized --- erpnext/non_profit/doctype/membership/membership.py | 11 ++++++++--- .../membership_settings/membership_settings.json | 10 +++++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 4c85cb60e8b..97de63b052e 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -54,9 +54,14 @@ 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): if not (self.paid or self.currency or self.amount): diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index a70c3c4b8ae..a25f5ffbc22 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -12,6 +12,7 @@ "webhook_secret", "column_break_6", "enable_invoicing", + "create_for_web_forms", "make_payment_entry", "company", "debit_account", @@ -136,12 +137,19 @@ "label": "Payment To", "mandatory_depends_on": "eval:doc.make_payment_entry", "options": "Account" + }, + { + "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": "Data", + "label": "Auto Create Invoice for Web Forms" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-11-04 19:51:21.990595", + "modified": "2020-11-04 20:19:55.163749", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", From 9d9fa74e6b8dd5db1f89bc6bf92809c0fba29eda Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 9 Nov 2020 12:29:03 +0530 Subject: [PATCH 010/295] refactor(member): drop email column * remove email column * update controller methods * add patch to add value from email to email_id --- erpnext/non_profit/doctype/member/member.json | 10 +--------- .../doctype/membership/membership.py | 8 ++++++-- erpnext/patches.txt | 3 ++- .../v13_0/update_member_email_address.py | 19 +++++++++++++++++++ 4 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 erpnext/patches/v13_0/update_member_email_address.py diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json index 992ef16d644..f190cfae755 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/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 97de63b052e..36f68bc00c4 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -24,7 +24,7 @@ class Membership(Document): user = frappe.get_doc('User', frappe.session.user) member = frappe.get_doc(dict( doctype='Member', - email=frappe.session.user, + email_id=frappe.session.user, membership_type=self.membership_type, member_name=user.get_fullname() )).insert(ignore_permissions=True) @@ -97,8 +97,12 @@ class Membership(Document): frappe.throw(_("You need to enable Send Acknowledge Email in 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: diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 34dbdd0bd51..a9cd25fd420 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -733,4 +733,5 @@ erpnext.patches.v13_0.print_uom_after_quantity_patch erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail erpnext.patches.v13_0.update_reason_for_resignation_in_employee -execute:frappe.delete_doc("Report", "Quoted Item Comparison") \ No newline at end of file +execute:frappe.delete_doc("Report", "Quoted Item Comparison") +erpnext.patches.v13_0.update_member_email_address \ 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 00000000000..da7828adbcb --- /dev/null +++ b/erpnext/patches/v13_0/update_member_email_address.py @@ -0,0 +1,19 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe + +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) From e0f4dd0643a9ef59d81d70d35050f7e51cfcdc1d Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 9 Nov 2020 12:29:27 +0530 Subject: [PATCH 011/295] fix: fieldtype for auto_create_for_web_forms --- .../doctype/membership_settings/membership_settings.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index a25f5ffbc22..961a9b9b3b1 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -139,17 +139,18 @@ "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": "Data", + "fieldtype": "Check", "label": "Auto Create Invoice for Web Forms" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-11-04 20:19:55.163749", + "modified": "2020-11-09 12:28:49.972434", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", From 286ec04197e6cad7aac9a95d9c8996bc44006252 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 9 Nov 2020 12:53:00 +0530 Subject: [PATCH 012/295] test(membership): setup test defaults --- .../doctype/membership/membership.py | 2 + .../doctype/membership/test_membership.py | 47 ++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 36f68bc00c4..ae4df4a3747 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -162,6 +162,8 @@ def get_member_based_on_subscription(subscription_id, email): return None def verify_signature(data): + if frappe.flags.in_test: + return True signature = frappe.request.headers.get('X-Razorpay-Signature') settings = frappe.get_doc("Membership Settings") diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py index b23f4062a97..b62f19bd0de 100644 --- a/erpnext/non_profit/doctype/membership/test_membership.py +++ b/erpnext/non_profit/doctype/membership/test_membership.py @@ -2,8 +2,51 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt from __future__ import unicode_literals - import unittest +from erpnext.non_profit.doctype.member.member import create_member +from erpnext.stock.doctype.item.test_item import create_item 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_to = company.default_cash_account + settings.debit_account = company.default_receivable_account + settings.save() + + # make test plan + plan = frappe.new_doc("Membership Type") + plan.amount = 100 + plan.razorpay_plan_id = "_rzpy_test_milythm" + plan.linked_item = create_item("_Test Item for Non Profit Membership") + plan.insert() + + # make test member + self.member_doc = create_member(frappe._dict({ + 'fullname': "_Test_Member", + 'email': "_test_member_erpnext@example.com", + 'plan_id': plan.name + })) + + def test_auto_generate_invoice_and_payment_entry(self): + pass + + def test_renew within_30_days(self): + pass + + def test_from_to_dates(self): + pass + + def test_razorpay_webook(self): + pass From c04321e64586ec7f466bb848530712320f4bfbe8 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 9 Nov 2020 13:29:46 +0530 Subject: [PATCH 013/295] test(membership): add test for invoicing and validation --- .../doctype/membership/membership.py | 16 +++- .../doctype/membership/test_membership.py | 78 ++++++++++++++++--- 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index ae4df4a3747..ac3b89a8d02 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -86,6 +86,20 @@ class Membership(Document): invoice = make_invoice(self, member, plan, settings) self.invoice = invoice.name + if with_payment_entry: + if not settings.payment_account: + frappe.throw(_("You need to set Payment Account in Membership Settings")) + + 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() + if save: self.save() @@ -97,7 +111,7 @@ class Membership(Document): frappe.throw(_("You need to enable Send Acknowledge Email in 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))) diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py index b62f19bd0de..6e4885d013c 100644 --- a/erpnext/non_profit/doctype/membership/test_membership.py +++ b/erpnext/non_profit/doctype/membership/test_membership.py @@ -3,7 +3,10 @@ # 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, getdate, add_months from erpnext.stock.doctype.item.test_item import create_item class TestMembership(unittest.TestCase): @@ -21,15 +24,16 @@ class TestMembership(unittest.TestCase): settings.enable_invoicing = 1 settings.make_payment_entry = 1 settings.company = company.name - settings.payment_to = company.default_cash_account + settings.payment_account = company.default_cash_account settings.debit_account = company.default_receivable_account settings.save() # make test plan 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") + plan.linked_item = create_item("_Test Item for Non Profit Membership").name plan.insert() # make test member @@ -38,15 +42,71 @@ class TestMembership(unittest.TestCase): '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): - pass + entry = make_membership(self.member) - def test_renew within_30_days(self): - pass + # 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) + # entry.delete() - def test_from_to_dates(self): - pass + # # Remove customer + # old_customer = self.member_doc.customer + # self.member_doc.customer = None + # self.member_doc.save() - def test_razorpay_webook(self): - pass + # entry = make_membership(self.member) + # self.assertRaises(frappe.ValidationError, entry.generate_invoice) + + # # Add customer value back + # self.member_doc.customer = old_customer + # self.member_doc.save() + + # # Remove company + # set_config(company, None) + # self.assertRaises(frappe.ValidationError, entry.generate_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 + new_entry = 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 \ No newline at end of file From 7e1cdf9b978ffdb6713a2e2cade4ac7307b73533 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 9 Nov 2020 14:01:11 +0530 Subject: [PATCH 014/295] feat(breaking): update get_last_membership to fetch correct details --- erpnext/__init__.py | 14 ++++---------- .../non_profit/doctype/membership/membership.py | 2 +- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 38d8a62f07f..5a5c448026e 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/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index ac3b89a8d02..7c83a4e0da1 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -34,7 +34,7 @@ class Membership(Document): self.member = member_name # 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": From 12fafa3e7a20bb8a2ad54ea43f8e3d2146bd30b5 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 9 Nov 2020 14:01:22 +0530 Subject: [PATCH 015/295] chore: remove validation for old member field --- erpnext/non_profit/doctype/member/member.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index 44b975e9e9d..7fc4f225aae 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) From 723e220a3409150de11c4b74a7bbb5911060382b Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 9 Nov 2020 14:01:52 +0530 Subject: [PATCH 016/295] chore: remove commented code --- .../doctype/membership/test_membership.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py index 6e4885d013c..ce31b919562 100644 --- a/erpnext/non_profit/doctype/membership/test_membership.py +++ b/erpnext/non_profit/doctype/membership/test_membership.py @@ -52,23 +52,6 @@ class TestMembership(unittest.TestCase): # 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) - # entry.delete() - - # # Remove customer - # old_customer = self.member_doc.customer - # self.member_doc.customer = None - # self.member_doc.save() - - # entry = make_membership(self.member) - # self.assertRaises(frappe.ValidationError, entry.generate_invoice) - - # # Add customer value back - # self.member_doc.customer = old_customer - # self.member_doc.save() - - # # Remove company - # set_config(company, None) - # self.assertRaises(frappe.ValidationError, entry.generate_invoice) def test_renew_within_30_days(self): # create a membership for two months From 8b6370bb45232d0e39508a1329258388c4d47439 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 15 Nov 2020 22:41:36 +0530 Subject: [PATCH 017/295] feat: Add accounting dimension filter doctype --- .../accounting_dimension_filter/__init__.py | 0 .../accounting_dimension_filter.js | 32 ++++++ .../accounting_dimension_filter.json | 100 ++++++++++++++++++ .../accounting_dimension_filter.py | 63 +++++++++++ .../test_accounting_dimension_filter.py | 10 ++ .../doctype/allowed_dimension/__init__.py | 0 .../allowed_dimension/allowed_dimension.json | 43 ++++++++ .../allowed_dimension/allowed_dimension.py | 10 ++ .../doctype/applicable_on_account/__init__.py | 0 .../applicable_on_account.json | 35 ++++++ .../applicable_on_account.py | 10 ++ 11 files changed, 303 insertions(+) create mode 100644 erpnext/accounts/doctype/accounting_dimension_filter/__init__.py create mode 100644 erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js create mode 100644 erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json create mode 100644 erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py create mode 100644 erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py create mode 100644 erpnext/accounts/doctype/allowed_dimension/__init__.py create mode 100644 erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json create mode 100644 erpnext/accounts/doctype/allowed_dimension/allowed_dimension.py create mode 100644 erpnext/accounts/doctype/applicable_on_account/__init__.py create mode 100644 erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json create mode 100644 erpnext/accounts/doctype/applicable_on_account/applicable_on_account.py 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 00000000000..e69de29bb2d 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 00000000000..6c254fcfb73 --- /dev/null +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js @@ -0,0 +1,32 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Accounting Dimension Filter', { + onload: function(frm) { + frappe.db.get_list('Accounting Dimension', + {fields: ['name']}).then((res) => { + let options = ['Cost Center', 'Project']; + + res.forEach((dimension) => { + options.push(dimension.name); + }); + + frm.set_df_property('accounting_dimension', 'options', options); + }); + }, + + 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"); + }, +}); + +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 00000000000..e626a09ce0c --- /dev/null +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json @@ -0,0 +1,100 @@ +{ + "actions": [], + "autoname": "format:{accounting_dimension}-{#####}", + "creation": "2020-11-08 18:28:11.906146", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "accounting_dimension", + "column_break_2", + "allow_or_restrict", + "section_break_4", + "accounts", + "column_break_6", + "dimensions" + ], + "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": "Accounts", + "options": "Applicable On Account", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "depends_on": "eval:doc.accounting_dimension", + "fieldname": "dimensions", + "fieldtype": "Table", + "label": "Dimensions", + "options": "Allowed Dimension", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-11-14 18:02:02.616932", + "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 00000000000..ccfafd96ed3 --- /dev/null +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py @@ -0,0 +1,63 @@ +# -*- 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, ad.fieldname + FROM + `tabApplicable On Account` a, `tabAllowed Dimension` d, + `tabAccounting Dimension Filter` p, `tabAccounting Dimension` ad + WHERE + p.name = a.parent + AND p.name = d.parent + AND (p.accounting_dimension = ad.name + OR p.accounting_dimension in ('Cost Center', 'Project')) + """, as_dict=1) + + dimension_filter_map = {} + account_filter_map = {} + + for f in filters: + if f.accounting_dimension in ('Cost Center', 'Project'): + f.fieldname = scrub(f.accounting_dimension) + + build_map(dimension_filter_map, f.fieldname, f.applicable_on_account, f.dimension_value, + f.allow_or_restrict) + + return dimension_filter_map + +def build_map(map_object, dimension, account, filter_value, allow_or_restrict): + map_object.setdefault((dimension, account), { + 'allowed_dimensions': [], + '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 00000000000..c271a25fbd8 --- /dev/null +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestAccountingDimensionFilter(unittest.TestCase): + pass diff --git a/erpnext/accounts/doctype/allowed_dimension/__init__.py b/erpnext/accounts/doctype/allowed_dimension/__init__.py new file mode 100644 index 00000000000..e69de29bb2d 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 00000000000..20024b03226 --- /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-14 19:54:03.269016", + "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 00000000000..c2afc1a2621 --- /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 00000000000..e69de29bb2d 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 00000000000..8305da2ba08 --- /dev/null +++ b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json @@ -0,0 +1,35 @@ +{ + "actions": [], + "creation": "2020-11-08 18:20:00.944449", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "applicable_on_account" + ], + "fields": [ + { + "fieldname": "applicable_on_account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Applicable On Account", + "options": "Account", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-11-14 16:54:06.756883", + "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 00000000000..0fccaf302fb --- /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 From 96e874bfda3e93a48613765c7433824587fb0360 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 15 Nov 2020 22:43:01 +0530 Subject: [PATCH 018/295] fix: dimension filter query --- .../accounting_dimension.py | 14 +++- erpnext/controllers/queries.py | 47 ++++++++++++++ .../public/js/utils/dimension_tree_filter.js | 65 ++++++++++++++----- 3 files changed, 109 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index f888d9e038a..b9d4da289ee 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_dimension_filters(with_costcenter_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_costcenter_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/controllers/queries.py b/erpnext/controllers/queries.py index 8fe3816c24a..015807d5639 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -493,6 +493,53 @@ 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'))) + group_condition = '' + company_condition = '' + + meta = frappe.get_meta(doctype) + + if meta.is_tree: + group_condition = 'and is_group = 0 ' + + if meta.has_field('company'): + company_condition = 'and company = %s ' % (frappe.db.escape(filters.get('company'))) + + 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']) + + result = frappe.db.sql("""SELECT name from `tab{doctype}` where + name {query_selector} {restricted} + {group_condition} {company_condition} + and {key} LIKE %(txt)s""".format( + doctype=doctype, query_selector=query_selector, restricted=dimensions, + group_condition = group_condition, + company_condition = company_condition, + key=searchfield), { + 'txt': '%' + txt + '%' + }) + + return result + else: + return frappe.db.sql(""" + SELECT name from `tab{doctype}` where + {key} LIKE %(txt)s {group_condition} {company_condition}""" + .format(doctype=doctype, key=searchfield, + group_condition=group_condition, company_condition=company_condition), + { 'txt': '%' + txt + '%'}) @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js index b6720c05cb2..34b563553e1 100644 --- a/erpnext/public/js/utils/dimension_tree_filter.js +++ b/erpnext/public/js/utils/dimension_tree_filter.js @@ -5,14 +5,18 @@ let default_dimensions = {}; 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"]; + "Subscription", "Purchase Order", "Journal Entry", "Material Request", "Purchase Receipt", "Asset", "Asset Value Adjustment"]; 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"]; + "Landed Cost Item", "Asset Value Adjustment", "Opening Invoice Creation Tool Item", "Subscription Plan", + "Sales Taxes and Charges", "Purchase Taxes and Charges"]; frappe.call({ method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimension_filters", + args: { + 'with_costcenter_and_project': true + }, callback: function(r) { erpnext.dimension_filters = r.message[0]; default_dimensions = r.message[1]; @@ -24,11 +28,16 @@ doctypes_with_dimensions.forEach((doctype) => { onload: function(frm) { erpnext.dimension_filters.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') { + setup_child_filters(frm, df.options, df.fieldname, dimension['fieldname']); + }; + + setup_account_filters(frm, dimension['fieldname'], parent_fields); + }); }); }); }, @@ -67,17 +76,41 @@ 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']]); - }); + copy_dimension(frm, cdt, cdn, "items"); }, 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(frm, cdt, cdn, "accounts"); } }); -}); \ No newline at end of file +}); + +let copy_dimension = function(frm, cdt, cdn, fieldname) { + erpnext.dimension_filters.forEach((dimension) => { + let row = frappe.get_doc(cdt, cdn); + frm.script_manager.copy_from_first_row(fieldname, row, [dimension['fieldname']]); + }); +} + +let setup_child_filters = function(frm, doctype, parentfield, dimension) { + let fields = []; + + frappe.model.with_doctype(doctype, () => { + frappe.meta.get_docfields(doctype).forEach((df) => { + if (df.fieldtype === 'Link' && df.options === 'Account') { + fields.push(df.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); + }); + }); +} + +let setup_account_filters = function(frm, dimension, fields) { + frm.set_query(dimension, function(doc) { + return erpnext.queries.get_filtered_dimensions(doc, fields, dimension, doc.company); + }); +} \ No newline at end of file From 6e5748e2a3344eeead1a7b1f86258de233276d02 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 15 Nov 2020 22:43:48 +0530 Subject: [PATCH 019/295] fix: dimension filter query --- erpnext/public/js/queries.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/erpnext/public/js/queries.js b/erpnext/public/js/queries.js index 560a5617da5..7b7a9df1ac0 100644 --- a/erpnext/public/js/queries.js +++ b/erpnext/public/js/queries.js @@ -116,6 +116,25 @@ $.extend(erpnext.queries, { ] } + }, + + 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 + } + } } }); From f916bc048ff08a4bbae87e37bd8215c35ac32f8a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 15 Nov 2020 22:44:39 +0530 Subject: [PATCH 020/295] fix: Remove cost center query from doctypes --- erpnext/accounts/doctype/budget/budget.js | 12 ++---------- .../accounts/doctype/journal_entry/journal_entry.js | 9 --------- .../doctype/loyalty_program/loyalty_program.js | 8 -------- .../accounts/doctype/payment_entry/payment_entry.js | 9 --------- .../doctype/purchase_invoice/purchase_invoice.js | 9 --------- .../accounts/doctype/sales_invoice/sales_invoice.js | 9 --------- .../accounts/doctype/shipping_rule/shipping_rule.js | 8 -------- erpnext/assets/doctype/asset/asset.js | 8 -------- erpnext/hr/doctype/expense_claim/expense_claim.js | 9 --------- .../payroll/doctype/payroll_entry/payroll_entry.js | 9 +-------- erpnext/public/js/controllers/accounts.js | 9 --------- erpnext/public/js/controllers/transaction.js | 10 ---------- 12 files changed, 3 insertions(+), 106 deletions(-) diff --git a/erpnext/accounts/doctype/budget/budget.js b/erpnext/accounts/doctype/budget/budget.js index cadf1e7e0ca..48cc493522a 100644 --- a/erpnext/accounts/doctype/budget/budget.js +++ b/erpnext/accounts/doctype/budget/budget.js @@ -3,14 +3,6 @@ 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: { @@ -18,7 +10,7 @@ frappe.ui.form.on('Budget', { } } }) - + frm.set_query("account", "accounts", function() { return { filters: { @@ -28,7 +20,7 @@ frappe.ui.form.on('Budget', { } } }) - + frm.set_query("monthly_distribution", function() { return { filters: { diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index ff12967155f..d60a7b76cc4 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -222,15 +222,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]; diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.js b/erpnext/accounts/doctype/loyalty_program/loyalty_program.js index 524a671801b..0d2b8cbf151 100644 --- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.js +++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.js @@ -46,14 +46,6 @@ 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")); }, diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index e1174717382..ea5487d5754 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -88,15 +88,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"]; diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 1d41d0fa2a9..3c07ee75cbb 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -501,15 +501,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/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 502e65ed8d0..e27bd2fa3ac 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -571,15 +571,6 @@ frappe.ui.form.on('Sales Invoice', { }; }); - frm.set_query("cost_center", function() { - return { - filters: { - company: frm.doc.company, - is_group: 0 - } - }; - }); - frm.custom_make_buttons = { 'Delivery Note': 'Delivery', 'Sales Invoice': 'Sales Return', diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.js b/erpnext/accounts/doctype/shipping_rule/shipping_rule.js index d0904eec3e3..7cfbfed1388 100644 --- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.js +++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.js @@ -3,14 +3,6 @@ frappe.ui.form.on('Shipping Rule', { refresh: function(frm) { - frm.set_query("cost_center", function() { - return { - filters: { - company: frm.doc.company - } - } - }) - frm.set_query("account", function() { return { filters: { diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 7ad164a8b9b..3af3948fac6 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -31,14 +31,6 @@ frappe.ui.form.on('Asset', { } }; }); - - frm.set_query("cost_center", function() { - return { - "filters": { - "company": frm.doc.company, - } - }; - }); }, setup: function(frm) { diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index 221300b519a..cbafd7d3ac0 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -167,15 +167,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/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index 1abc869c539..96006158b68 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -113,14 +113,7 @@ frappe.ui.form.on('Payroll Entry', { } }; }), - frm.set_query("cost_center", function () { - return { - filters: { - "is_group": 0, - company: frm.doc.company - } - }; - }), + frm.set_query("project", function () { return { filters: { diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index 6e97d811fc1..45c494e3e56 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/transaction.js b/erpnext/public/js/controllers/transaction.js index 1358a4bd088..ec6b3dc6df1 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -159,16 +159,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) { From 1f9b03345dd6360e5b8fc2df98948cebe1e53a1e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 16 Nov 2020 09:55:35 +0530 Subject: [PATCH 021/295] fix: Remove project filter --- erpnext/accounts/doctype/budget/budget.js | 8 -------- erpnext/payroll/doctype/payroll_entry/payroll_entry.js | 10 +--------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/erpnext/accounts/doctype/budget/budget.js b/erpnext/accounts/doctype/budget/budget.js index 48cc493522a..1b793982472 100644 --- a/erpnext/accounts/doctype/budget/budget.js +++ b/erpnext/accounts/doctype/budget/budget.js @@ -3,14 +3,6 @@ frappe.ui.form.on('Budget', { onload: function(frm) { - frm.set_query("project", function() { - return { - filters: { - company: frm.doc.company - } - } - }) - frm.set_query("account", "accounts", function() { return { filters: { diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index 96006158b68..bc34d6c3b15 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -112,15 +112,7 @@ frappe.ui.form.on('Payroll Entry', { "company": frm.doc.company } }; - }), - - frm.set_query("project", function () { - return { - filters: { - company: frm.doc.company - } - }; - }); + }) }, payroll_frequency: function (frm) { From 9e9ea965824c8562d915d3983641b0df1bafec15 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 16 Nov 2020 12:03:47 +0530 Subject: [PATCH 022/295] fix: linting --- .../accounting_dimension_filter.js | 10 +++++----- .../accounting_dimension_filter.py | 1 - erpnext/payroll/doctype/payroll_entry/payroll_entry.js | 2 +- erpnext/public/js/queries.js | 2 +- erpnext/public/js/utils/dimension_tree_filter.js | 9 ++++----- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js index 6c254fcfb73..3e880d3ca68 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js @@ -5,13 +5,13 @@ frappe.ui.form.on('Accounting Dimension Filter', { onload: function(frm) { frappe.db.get_list('Accounting Dimension', {fields: ['name']}).then((res) => { - let options = ['Cost Center', 'Project']; + let options = ['Cost Center', 'Project']; - res.forEach((dimension) => { - options.push(dimension.name); - }); + res.forEach((dimension) => { + options.push(dimension.name); + }); - frm.set_df_property('accounting_dimension', 'options', options); + frm.set_df_property('accounting_dimension', 'options', options); }); }, diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py index ccfafd96ed3..0dcf1164238 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py @@ -44,7 +44,6 @@ def get_dimension_filter_map(): """, as_dict=1) dimension_filter_map = {} - account_filter_map = {} for f in filters: if f.accounting_dimension in ('Cost Center', 'Project'): diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index bc34d6c3b15..d32fdbcaf16 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -112,7 +112,7 @@ frappe.ui.form.on('Payroll Entry', { "company": frm.doc.company } }; - }) + }); }, payroll_frequency: function (frm) { diff --git a/erpnext/public/js/queries.js b/erpnext/public/js/queries.js index 7b7a9df1ac0..98f1b504ccc 100644 --- a/erpnext/public/js/queries.js +++ b/erpnext/public/js/queries.js @@ -134,7 +134,7 @@ $.extend(erpnext.queries, { 'account': account, 'company': company } - } + }; } }); diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js index 34b563553e1..7a42fb56148 100644 --- a/erpnext/public/js/utils/dimension_tree_filter.js +++ b/erpnext/public/js/utils/dimension_tree_filter.js @@ -1,5 +1,4 @@ frappe.provide('frappe.ui.form'); - let default_dimensions = {}; let doctypes_with_dimensions = ["GL Entry", "Sales Invoice", "Purchase Invoice", "Payment Entry", "Asset", @@ -34,7 +33,7 @@ doctypes_with_dimensions.forEach((doctype) => { parent_fields.push(df.fieldname); } else if (df.fieldtype === 'Table') { setup_child_filters(frm, df.options, df.fieldname, dimension['fieldname']); - }; + } setup_account_filters(frm, dimension['fieldname'], parent_fields); }); @@ -90,7 +89,7 @@ let copy_dimension = function(frm, cdt, cdn, fieldname) { let row = frappe.get_doc(cdt, cdn); frm.script_manager.copy_from_first_row(fieldname, row, [dimension['fieldname']]); }); -} +}; let setup_child_filters = function(frm, doctype, parentfield, dimension) { let fields = []; @@ -107,10 +106,10 @@ let setup_child_filters = function(frm, doctype, parentfield, dimension) { return erpnext.queries.get_filtered_dimensions(row, fields, dimension, doc.company); }); }); -} +}; let setup_account_filters = function(frm, dimension, fields) { frm.set_query(dimension, function(doc) { return erpnext.queries.get_filtered_dimensions(doc, fields, dimension, doc.company); }); -} \ No newline at end of file +}; \ No newline at end of file From d51c953c22a2de6824fbe88e5d9989db19220d1b Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 16 Nov 2020 12:48:40 +0530 Subject: [PATCH 023/295] chore: Fix Tests, use project name instead of project_name field --- .../accounts/doctype/budget/test_budget.py | 15 +++++++--- .../journal_entry/test_journal_entry.py | 23 +++++++++------ .../purchase_invoice/test_purchase_invoice.py | 29 +++++++++++-------- .../sales_invoice/test_sales_invoice.py | 14 ++++----- .../doctype/crop_cycle/test_crop_cycle.py | 2 +- .../test_employee_onboarding.py | 3 +- .../expense_claim/test_expense_claim.py | 17 ++++++----- .../projects/doctype/project/test_project.py | 4 +++ erpnext/projects/doctype/task/test_task.py | 8 +++-- .../doctype/timesheet/test_timesheet.py | 5 ++-- 10 files changed, 73 insertions(+), 47 deletions(-) diff --git a/erpnext/accounts/doctype/budget/test_budget.py b/erpnext/accounts/doctype/budget/test_budget.py index 0f115f9cc20..62d17c4e697 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 > 10: month = 10 + project = frappe.get_value("Project", {"project_name": "_Test Project"}) for i in range(month): 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/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py index 53c07583d8e..402ea5085e5 100644 --- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py @@ -168,7 +168,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) @@ -307,15 +307,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" @@ -325,10 +330,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/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 2e5a7142a33..26acad3a4fb 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -436,26 +436,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(self): set_perpetual_inventory() @@ -874,17 +879,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/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 9660c9570e2..29b1ea2f9f0 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1571,7 +1571,7 @@ class TestSalesInvoice(unittest.TestCase): for gle in gl_entries: self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) - + def test_sales_invoice_with_project_link(self): from erpnext.projects.doctype.project.test_project import make_project @@ -1587,17 +1587,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 } } @@ -1605,9 +1605,9 @@ class TestSalesInvoice(unittest.TestCase): debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s order by account asc""", sales_invoice.name, as_dict=1) - + self.assertTrue(gl_entries) - + for gle in gl_entries: self.assertEqual(expected_values[gle.account]["project"], gle.project) diff --git a/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py b/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py index 5510d5ac020..763b4036c3a 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/hr/doctype/employee_onboarding/test_employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py index 4e9ee3b143a..336e13c9b77 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/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py index 6e97f0513d6..d9b472cce7f 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py @@ -19,35 +19,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/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index 0c4f6f1bdfe..f31225e36b6 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -47,6 +47,10 @@ def get_project(name): 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}) + if args.project_template_name: template = make_project_template(args.project_template_name) else: diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index 47a28fd1114..6ad8a19532b 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, sa task.subject = subject task.exp_start_date = start or nowdate() task.exp_end_date = end or nowdate() - task.project = project or "_Test Project" + task.project = project or frappe.get_value("Project", {"project_name": "_Test Project"}) if save: task.save() else: diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index a5ce44dcf24..4cb38049ff4 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) From 0c6319194e7fb498e9639d6efb66cbf1a629df89 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 16 Nov 2020 13:20:19 +0530 Subject: [PATCH 024/295] fix: GL Entry validation for dimensions --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index def9ed6803e..1ac607940fc 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -13,6 +13,8 @@ from erpnext.accounts.utils import get_account_currency from erpnext.accounts.utils import get_fiscal_year from erpnext.exceptions import InvalidAccountCurrency 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): @@ -37,6 +39,7 @@ class GLEntry(Document): def on_update_with_args(self, adv_adj, update_outstanding = 'Yes'): 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) @@ -91,6 +94,21 @@ 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['allow_or_restrict'] == 'Allow': + if self.get(dimension) and self.get(dimension) not in value['allowed_dimensions']: + frappe.throw(_("Invalid value {0} for account {1}").format( + frappe.bold(self.get(dimension)), frappe.bold(self.account))) + else: + if self.get(dimension) and self.get(dimension) in value['allowed_dimensions']: + frappe.throw(_("Invalid value {0} for account {1}").format( + frappe.bold(self.get(dimension)), frappe.bold(self.account))) def check_pl_account(self): if self.is_opening=='Yes' and \ From d82c0f3beafe43146f396dce9593edd4a9d69557 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 16 Nov 2020 20:32:16 +0530 Subject: [PATCH 025/295] fix: Add disable and mandatory check for accounting dimension filters --- .../accounting_dimension_filter.js | 37 ++++++++++++++++++- .../accounting_dimension_filter.json | 23 +++++++++++- .../accounting_dimension_filter.py | 8 ++-- .../allowed_dimension/allowed_dimension.json | 3 +- .../applicable_on_account.json | 15 +++++++- erpnext/accounts/doctype/gl_entry/gl_entry.py | 6 ++- 6 files changed, 80 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js index 3e880d3ca68..c8c32d58bd3 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js @@ -3,16 +3,48 @@ frappe.ui.form.on('Accounting Dimension Filter', { onload: function(frm) { + frm.set_query('applicable_on_account', 'accounts', function() { + return { + filters : { + 'company': frm.doc.company + } + } + }); + frappe.db.get_list('Accounting Dimension', - {fields: ['name']}).then((res) => { + {fields: ['document_type']}).then((res) => { let options = ['Cost Center', 'Project']; res.forEach((dimension) => { - options.push(dimension.name); + options.push(dimension.document_type); }); frm.set_df_property('accounting_dimension', 'options', options); }); + + frm.trigger('setup_filters'); + }, + + setup_filters: function(frm) { + let filters = {}; + + frappe.model.with_doctype(frm.doc.accounting_dimension, function() { + if (frm.doc.accounting_dimension) { + 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) { @@ -20,6 +52,7 @@ frappe.ui.form.on('Accounting Dimension Filter', { let row = frm.add_child("dimensions"); row.accounting_dimension = frm.doc.accounting_dimension; frm.refresh_field("dimensions"); + frm.trigger('setup_filters'); }, }); diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json index e626a09ce0c..c1190a395fe 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json @@ -7,8 +7,10 @@ "engine": "InnoDB", "field_order": [ "accounting_dimension", - "column_break_2", "allow_or_restrict", + "column_break_2", + "company", + "disabled", "section_break_4", "accounts", "column_break_6", @@ -70,11 +72,28 @@ "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 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-14 18:02:02.616932", + "modified": "2020-11-16 17:27:40.292860", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting Dimension Filter", diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py index 0dcf1164238..210b2c8ea89 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py @@ -32,12 +32,13 @@ def get_dimension_filter_map(): filters = frappe.db.sql( """ SELECT a.applicable_on_account, d.dimension_value, p.accounting_dimension, - p.allow_or_restrict, ad.fieldname + p.allow_or_restrict, ad.fieldname, a.is_mandatory FROM `tabApplicable On Account` a, `tabAllowed Dimension` d, `tabAccounting Dimension Filter` p, `tabAccounting Dimension` ad WHERE p.name = a.parent + AND p.disabled = 0 AND p.name = d.parent AND (p.accounting_dimension = ad.name OR p.accounting_dimension in ('Cost Center', 'Project')) @@ -50,13 +51,14 @@ def get_dimension_filter_map(): 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.allow_or_restrict, f.is_mandatory) return dimension_filter_map -def build_map(map_object, dimension, account, filter_value, allow_or_restrict): +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/allowed_dimension/allowed_dimension.json b/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json index 20024b03226..c2d34b3b7e6 100644 --- a/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json +++ b/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json @@ -22,6 +22,7 @@ "fieldname": "dimension_value", "fieldtype": "Dynamic Link", "in_list_view": 1, + "label": "Applicable Dimension", "options": "accounting_dimension", "show_days": 1, "show_seconds": 1 @@ -30,7 +31,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-11-14 19:54:03.269016", + "modified": "2020-11-16 17:41:50.422843", "modified_by": "Administrator", "module": "Accounts", "name": "Allowed Dimension", diff --git a/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json index 8305da2ba08..5c809515c29 100644 --- a/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json +++ b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json @@ -5,7 +5,8 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "applicable_on_account" + "applicable_on_account", + "is_mandatory" ], "fields": [ { @@ -17,12 +18,22 @@ "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-14 16:54:06.756883", + "modified": "2020-11-16 13:36:59.129672", "modified_by": "Administrator", "module": "Accounts", "name": "Applicable On Account", diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 1ac607940fc..b3caf6a82e8 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -77,11 +77,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): @@ -101,6 +99,10 @@ class GLEntry(Document): 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))) + 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 account {1}").format( From 4bd52b48424c2fbe02d5e00ca5ddd3ce4665eb44 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 16 Nov 2020 23:01:36 +0530 Subject: [PATCH 026/295] fix: Add test case --- .../test_accounting_dimension.py | 64 ++++++++-------- .../accounting_dimension_filter.py | 10 +-- .../test_accounting_dimension_filter.py | 75 ++++++++++++++++++- 3 files changed, 110 insertions(+), 39 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py index 104880f6f34..b5375e106fe 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"}): + 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() def disable_dimension(): dimension1 = frappe.get_doc("Accounting Dimension", "Department") diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py index 210b2c8ea89..440073b7230 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py @@ -32,23 +32,21 @@ def get_dimension_filter_map(): filters = frappe.db.sql( """ SELECT a.applicable_on_account, d.dimension_value, p.accounting_dimension, - p.allow_or_restrict, ad.fieldname, a.is_mandatory + p.allow_or_restrict, a.is_mandatory FROM `tabApplicable On Account` a, `tabAllowed Dimension` d, - `tabAccounting Dimension Filter` p, `tabAccounting Dimension` ad + `tabAccounting Dimension Filter` p WHERE p.name = a.parent AND p.disabled = 0 AND p.name = d.parent - AND (p.accounting_dimension = ad.name - OR p.accounting_dimension in ('Cost Center', 'Project')) + """, as_dict=1) dimension_filter_map = {} for f in filters: - if f.accounting_dimension in ('Cost Center', 'Project'): - f.fieldname = scrub(f.accounting_dimension) + 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) 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 index c271a25fbd8..feb0af1bd59 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -3,8 +3,79 @@ # See license.txt from __future__ import unicode_literals -# import frappe +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 class TestAccountingDimensionFilter(unittest.TestCase): - pass + def setUp(self): + create_accounting_dimension_filter() + + def test_allowed_dimension_validation(self): + si = create_sales_invoice(do_not_save=1) + si.items[0].cost_center = 'Main - _TC' + si.save() + + self.assertRaises(frappe.ValidationError, si.submit) + + def test_mandatory_dimension_validation(self): + si = create_sales_invoice(do_not_save=1) + si.items[0].location = '' + si.save() + + self.assertRaises(frappe.ValidationError, si.submit) + + def tearDown(self): + disable_dimension_filter() + +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 3 - _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': 'Location'}): + frappe.get_doc({ + 'doctype': 'Accounting Dimension Filter', + 'accounting_dimension': 'Location', + 'allow_or_restrict': 'Allow', + 'company': '_Test Company', + 'accounts': [{ + 'applicable_on_account': 'Sales - _TC', + 'is_mandatory': 1 + }], + 'dimensions': [{ + 'accounting_dimension': 'Location', + 'dimension_value': 'Block 1' + }] + }).insert() + else: + doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Location'}) + doc.disabled = 0 + doc.save() + +def disable_dimension_filter(): + doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'}) + doc.disabled = 0 + doc.save() + + doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Location'}) + doc.disabled = 0 + doc.save() From 6456c3dc244c0b294748707ed08e558826f3ab65 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 17 Nov 2020 11:10:00 +0530 Subject: [PATCH 027/295] fix: Linting and test cases --- .../accounting_dimension/test_accounting_dimension.py | 8 ++++---- .../accounting_dimension_filter.js | 4 ++-- .../test_accounting_dimension_filter.py | 3 ++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py index b5375e106fe..fc1d7e344af 100644 --- a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py @@ -75,14 +75,14 @@ def create_dimension(): frappe.set_user("Administrator") if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}): - dimension = frappe.get_doc({ + frappe.get_doc({ "doctype": "Accounting Dimension", "document_type": "Department", }).insert() else: - dimension1 = frappe.get_doc("Accounting Dimension", "Department") - dimension1.disabled = 0 - dimension1.save() + 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({ diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js index c8c32d58bd3..994ee44354d 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js @@ -8,7 +8,7 @@ frappe.ui.form.on('Accounting Dimension Filter', { filters : { 'company': frm.doc.company } - } + }; }); frappe.db.get_list('Accounting Dimension', @@ -41,7 +41,7 @@ frappe.ui.form.on('Accounting Dimension Filter', { frm.set_query('dimension_value', 'dimensions', function() { return { filters: filters - } + }; }); } }); 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 index feb0af1bd59..02fd75e7595 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -10,6 +10,7 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp class TestAccountingDimensionFilter(unittest.TestCase): def setUp(self): + create_dimension() create_accounting_dimension_filter() def test_allowed_dimension_validation(self): @@ -42,7 +43,7 @@ def create_accounting_dimension_filter(): }], 'dimensions': [{ 'accounting_dimension': 'Cost Center', - 'dimension_value': '_Test Cost Center 3 - _TC' + 'dimension_value': '_Test Cost Center 2 - _TC' }] }).insert() else: From 8b4d92d4fcc8ffbd92366dd63bd66122719156d2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 17 Nov 2020 13:19:29 +0530 Subject: [PATCH 028/295] fix: Disable filters after test --- .../test_accounting_dimension_filter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 02fd75e7595..801786b6e96 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -74,9 +74,9 @@ def create_accounting_dimension_filter(): def disable_dimension_filter(): doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'}) - doc.disabled = 0 + doc.disabled = 1 doc.save() doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Location'}) - doc.disabled = 0 + doc.disabled = 1 doc.save() From 350972ece4e80b66d02f7943acdc2cb13f277f6f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 17 Nov 2020 13:51:58 +0530 Subject: [PATCH 029/295] fix: Account filters --- .../accounting_dimension_filter.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js index 994ee44354d..f0362d31403 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js @@ -28,8 +28,8 @@ frappe.ui.form.on('Accounting Dimension Filter', { setup_filters: function(frm) { let filters = {}; - frappe.model.with_doctype(frm.doc.accounting_dimension, function() { - if (frm.doc.accounting_dimension) { + 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; } @@ -43,8 +43,8 @@ frappe.ui.form.on('Accounting Dimension Filter', { filters: filters }; }); - } - }); + }); + } }, accounting_dimension: function(frm) { From 8aeb340dc8bd87651a73bbfdef9e3d2c681de878 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Thu, 19 Nov 2020 19:18:48 +0530 Subject: [PATCH 030/295] fix: add remarks to sales invoice --- .../doctype/sales_invoice/sales_invoice.py | 9 ++++-- erpnext/patches.txt | 1 + .../v12_0/update_sales_invoice_remarks.py | 32 +++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 erpnext/patches/v12_0/update_sales_invoice_remarks.py diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index af6c6968dc1..0530aa2d234 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe, erpnext import frappe.defaults -from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate, get_link_to_form +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 frappe.model.mapper import get_mapped_doc @@ -535,7 +535,12 @@ class SalesInvoice(SellingController): self.against_income_account = ','.join(against_acc) def add_remarks(self): - if not self.remarks: self.remarks = 'No Remarks' + if not self.remarks: + if self.po_no and self.po_date: + self.remarks = _("Against Customer Order {0} dated {1}").format(self.po_no, + formatdate(self.po_date)) + else: + self.remarks = _("No Remarks") def validate_auto_set_posting_time(self): # Don't auto set the posting date and time if invoice is amended diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 25be8841174..4a38cb3ab80 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -735,3 +735,4 @@ erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail 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.v12_0.update_sales_invoice_remarks \ No newline at end of file diff --git a/erpnext/patches/v12_0/update_sales_invoice_remarks.py b/erpnext/patches/v12_0/update_sales_invoice_remarks.py new file mode 100644 index 00000000000..7e8feaaca6c --- /dev/null +++ b/erpnext/patches/v12_0/update_sales_invoice_remarks.py @@ -0,0 +1,32 @@ +from __future__ import unicode_literals +import frappe + +from frappe import _ +from frappe.utils import formatdate + +def execute(): + si_list = frappe.db.get_all('Sales Invoice', filters = { + 'docstatus': 1, + 'remarks': 'No Remarks', + 'po_no' : ['!=', ''], + 'po_date' : ['!=', ''] + }, + fields = ['name', 'po_no', 'po_date'] + ) + + for doc in si_list: + remarks = _("Against Customer Order {0} dated {1}").format(doc.po_no, + formatdate(doc.po_date)) + + frappe.db.set_value('Sales Invoice', doc.name, 'remarks', remarks) + + gl_entry_list = frappe.db.get_all('GL Entry', filters = { + 'voucher_type': 'Sales Invoice', + 'remarks': 'No Remarks', + 'voucher_no' : doc.name + }, + fields = ['name'] + ) + + for entry in gl_entry_list: + frappe.db.set_value('GL Entry', entry.name, 'remarks', remarks) \ No newline at end of file From 6c17b84caef6300f3872ce5b5db031e0ec7501fd Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 Nov 2020 13:42:16 +0530 Subject: [PATCH 031/295] fix: Replace raw query with ORM --- erpnext/controllers/queries.py | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 015807d5639..e3aac9aba85 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -499,16 +499,17 @@ 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'))) - group_condition = '' - company_condition = '' + query_filters = [] meta = frappe.get_meta(doctype) - if meta.is_tree: - group_condition = 'and is_group = 0 ' + query_filters.append(['is_group', '=', 0]) if meta.has_field('company'): - company_condition = 'and company = %s ' % (frappe.db.escape(filters.get('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': @@ -521,25 +522,12 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters) else: dimensions = tuple(dimension_filters['allowed_dimensions']) - result = frappe.db.sql("""SELECT name from `tab{doctype}` where - name {query_selector} {restricted} - {group_condition} {company_condition} - and {key} LIKE %(txt)s""".format( - doctype=doctype, query_selector=query_selector, restricted=dimensions, - group_condition = group_condition, - company_condition = company_condition, - key=searchfield), { - 'txt': '%' + txt + '%' - }) + query_filters.append(['name', query_selector, dimensions]) - return result - else: - return frappe.db.sql(""" - SELECT name from `tab{doctype}` where - {key} LIKE %(txt)s {group_condition} {company_condition}""" - .format(doctype=doctype, key=searchfield, - group_condition=group_condition, company_condition=company_condition), - { 'txt': '%' + txt + '%'}) + 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 From df065f7044df675d161314992a2eaeec83971839 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 Nov 2020 13:44:52 +0530 Subject: [PATCH 032/295] fix: form layout and naming fixes --- .../accounting_dimension.js | 1 - .../accounting_dimension.py | 4 +-- .../accounting_dimension_filter.js | 7 +++++- .../accounting_dimension_filter.json | 10 ++++---- .../accounting_dimension_filter.py | 25 +++++++++---------- 5 files changed, 25 insertions(+), 22 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js index 9a6c3893393..65c5ff1ceaf 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 b9d4da289ee..52e9ff8b764 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(with_costcenter_and_project=False): +def get_dimensions(with_cost_center_and_project=False): dimension_filters = frappe.db.sql(""" SELECT label, fieldname, document_type FROM `tabAccounting Dimension` @@ -214,7 +214,7 @@ def get_dimension_filters(with_costcenter_and_project=False): FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p WHERE c.parent = p.name""", as_dict=1) - if with_costcenter_and_project: + if with_cost_center_and_project: dimension_filters.extend([ { 'fieldname': 'cost_center', diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js index f0362d31403..a2526e92c36 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js @@ -2,10 +2,15 @@ // 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'); + } + }, onload: function(frm) { frm.set_query('applicable_on_account', 'accounts', function() { return { - filters : { + filters: { 'company': frm.doc.company } }; diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json index c1190a395fe..7736b2dffb2 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json @@ -7,10 +7,10 @@ "engine": "InnoDB", "field_order": [ "accounting_dimension", - "allow_or_restrict", + "disabled", "column_break_2", "company", - "disabled", + "allow_or_restrict", "section_break_4", "accounts", "column_break_6", @@ -57,7 +57,7 @@ { "fieldname": "accounts", "fieldtype": "Table", - "label": "Accounts", + "label": "Applicable On Account", "options": "Applicable On Account", "reqd": 1, "show_days": 1, @@ -67,7 +67,7 @@ "depends_on": "eval:doc.accounting_dimension", "fieldname": "dimensions", "fieldtype": "Table", - "label": "Dimensions", + "label": "Applicable Dimension", "options": "Allowed Dimension", "reqd": 1, "show_days": 1, @@ -93,7 +93,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-16 17:27:40.292860", + "modified": "2020-11-24 12:34:42.458713", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting Dimension Filter", diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py index 440073b7230..6aef9caa747 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py @@ -29,19 +29,18 @@ class AccountingDimensionFilter(Document): 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) + 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 = {} From 4e991fe668daafe8488560fddef806cbc540373d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 Nov 2020 13:46:07 +0530 Subject: [PATCH 033/295] fix: Define specific exceptions and fix tests --- .../test_accounting_dimension_filter.py | 27 ++++++++++++------- erpnext/accounts/doctype/gl_entry/gl_entry.py | 8 +++--- erpnext/exceptions.py | 2 ++ 3 files changed, 23 insertions(+), 14 deletions(-) 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 index 801786b6e96..f67e1de4044 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -6,7 +6,8 @@ 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 +from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import create_dimension, disable_dimension +from erpnext.exceptions import InvalidAccountDimension, MandatoryDimension class TestAccountingDimensionFilter(unittest.TestCase): def setUp(self): @@ -16,19 +17,25 @@ class TestAccountingDimensionFilter(unittest.TestCase): def test_allowed_dimension_validation(self): si = create_sales_invoice(do_not_save=1) si.items[0].cost_center = 'Main - _TC' + si.location = 'Block 1' si.save() - self.assertRaises(frappe.ValidationError, si.submit) + self.assertRaises(InvalidAccountDimension, si.submit) def test_mandatory_dimension_validation(self): si = create_sales_invoice(do_not_save=1) - si.items[0].location = '' + 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(frappe.ValidationError, si.submit) + self.assertRaises(MandatoryDimension, si.submit) def tearDown(self): disable_dimension_filter() + disable_dimension() def create_accounting_dimension_filter(): if not frappe.db.get_value('Accounting Dimension Filter', @@ -52,10 +59,10 @@ def create_accounting_dimension_filter(): doc.save() if not frappe.db.get_value('Accounting Dimension Filter', - {'accounting_dimension': 'Location'}): + {'accounting_dimension': 'Department'}): frappe.get_doc({ 'doctype': 'Accounting Dimension Filter', - 'accounting_dimension': 'Location', + 'accounting_dimension': 'Department', 'allow_or_restrict': 'Allow', 'company': '_Test Company', 'accounts': [{ @@ -63,12 +70,12 @@ def create_accounting_dimension_filter(): 'is_mandatory': 1 }], 'dimensions': [{ - 'accounting_dimension': 'Location', - 'dimension_value': 'Block 1' + 'accounting_dimension': 'Department', + 'dimension_value': '_Test Department - _TC' }] }).insert() else: - doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Location'}) + doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Department'}) doc.disabled = 0 doc.save() @@ -77,6 +84,6 @@ def disable_dimension_filter(): doc.disabled = 1 doc.save() - doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Location'}) + doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Department'}) doc.disabled = 1 doc.save() diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index b3caf6a82e8..f586de82e35 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -11,7 +11,7 @@ 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, InvalidAccountDimension, MandatoryDimension 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 @@ -101,16 +101,16 @@ class GLEntry(Document): 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))) + frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), MandatoryDimension) 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 account {1}").format( - frappe.bold(self.get(dimension)), frappe.bold(self.account))) + frappe.bold(self.get(dimension)), frappe.bold(self.account)), InvalidAccountDimension) else: if self.get(dimension) and self.get(dimension) in value['allowed_dimensions']: frappe.throw(_("Invalid value {0} for account {1}").format( - frappe.bold(self.get(dimension)), frappe.bold(self.account))) + frappe.bold(self.get(dimension)), frappe.bold(self.account)), InvalidAccountDimension) def check_pl_account(self): if self.is_opening=='Yes' and \ diff --git a/erpnext/exceptions.py b/erpnext/exceptions.py index d92af5d7227..dcf3d6bad1a 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 InvalidAccountDimension(frappe.ValidationError): pass +class MandatoryDimension(frappe.ValidationError): pass From 92b3449789689fc71dbd2ea3df7a2f7553cb0dec Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 Nov 2020 14:01:57 +0530 Subject: [PATCH 034/295] fix: Label changes --- .../accounts/doctype/allowed_dimension/allowed_dimension.json | 3 +-- .../doctype/applicable_on_account/applicable_on_account.json | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json b/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json index c2d34b3b7e6..7fe2a3c647e 100644 --- a/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json +++ b/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json @@ -22,7 +22,6 @@ "fieldname": "dimension_value", "fieldtype": "Dynamic Link", "in_list_view": 1, - "label": "Applicable Dimension", "options": "accounting_dimension", "show_days": 1, "show_seconds": 1 @@ -31,7 +30,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-11-16 17:41:50.422843", + "modified": "2020-11-23 09:56:19.744200", "modified_by": "Administrator", "module": "Accounts", "name": "Allowed Dimension", diff --git a/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json index 5c809515c29..95e98d0b673 100644 --- a/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json +++ b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json @@ -13,7 +13,7 @@ "fieldname": "applicable_on_account", "fieldtype": "Link", "in_list_view": 1, - "label": "Applicable On Account", + "label": "Accounts", "options": "Account", "reqd": 1, "show_days": 1, @@ -33,7 +33,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-11-16 13:36:59.129672", + "modified": "2020-11-22 19:55:13.324136", "modified_by": "Administrator", "module": "Accounts", "name": "Applicable On Account", From 6b9dda5f3eb7af999456ffbf4cc69c255c2f09b2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 Nov 2020 14:49:10 +0530 Subject: [PATCH 035/295] fix: Move filter setup to doctype controllers --- erpnext/accounts/doctype/budget/budget.js | 7 +- .../doctype/journal_entry/journal_entry.js | 5 + .../loyalty_program/loyalty_program.js | 7 + .../opening_invoice_creation_tool.js | 3 + .../doctype/payment_entry/payment_entry.js | 4 + .../doctype/pos_profile/pos_profile.js | 3 + .../purchase_invoice/purchase_invoice.js | 7 + .../doctype/sales_invoice/sales_invoice.js | 8 +- .../doctype/shipping_rule/shipping_rule.js | 10 ++ erpnext/assets/doctype/asset/asset.js | 7 + .../asset_value_adjustment.js | 10 ++ .../doctype/purchase_order/purchase_order.js | 8 +- .../doctype/fee_schedule/fee_schedule.js | 7 + .../doctype/fee_structure/fee_structure.js | 8 + erpnext/education/doctype/fees/fees.js | 7 + .../hr/doctype/expense_claim/expense_claim.js | 20 ++- .../doctype/payroll_entry/payroll_entry.js | 5 + erpnext/public/js/controllers/transaction.js | 4 + .../public/js/utils/dimension_tree_filter.js | 164 ++++++++---------- .../doctype/delivery_note/delivery_note.js | 4 +- .../material_request/material_request.js | 7 + .../purchase_receipt/purchase_receipt.js | 3 + .../stock/doctype/stock_entry/stock_entry.js | 4 + .../stock_reconciliation.js | 7 + 24 files changed, 218 insertions(+), 101 deletions(-) diff --git a/erpnext/accounts/doctype/budget/budget.js b/erpnext/accounts/doctype/budget/budget.js index 1b793982472..e60bc60475e 100644 --- a/erpnext/accounts/doctype/budget/budget.js +++ b/erpnext/accounts/doctype/budget/budget.js @@ -1,5 +1,6 @@ // 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) { @@ -11,7 +12,7 @@ frappe.ui.form.on('Budget', { is_group: 0 } } - }) + }); frm.set_query("monthly_distribution", function() { return { @@ -19,7 +20,9 @@ frappe.ui.form.on('Budget', { fiscal_year: frm.doc.fiscal_year } } - }) + }); + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index d60a7b76cc4..37b03f3f0e0 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() { @@ -397,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/loyalty_program/loyalty_program.js b/erpnext/accounts/doctype/loyalty_program/loyalty_program.js index 0d2b8cbf151..f90f86728de 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 = @@ -47,11 +49,16 @@ frappe.ui.form.on('Loyalty Program', { }); 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 3ce5701823e..c087980798c 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 ea5487d5754..4318aea2bda 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) { @@ -158,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/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js index 558e21c13aa..668cf016d35 100755 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.js +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js @@ -48,6 +48,8 @@ frappe.ui.form.on('POS Profile', { } }; }); + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { @@ -58,6 +60,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 3c07ee75cbb..3fa6ee76e61 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -16,6 +16,11 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ }); } }, + + company: function() { + erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype); + }, + onload: function() { this._super(); @@ -31,6 +36,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) { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index e27bd2fa3ac..b89cecfab5e 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) { diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.js b/erpnext/accounts/doctype/shipping_rule/shipping_rule.js index 7cfbfed1388..8e4b806f02d 100644 --- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.js +++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.js @@ -1,7 +1,17 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt +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 { diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 3af3948fac6..e9c8aff678d 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) { @@ -31,6 +32,12 @@ frappe.ui.form.on('Asset', { } }; }); + + 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 a6e6974c48d..79c8861bcdc 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 2f52a9e0355..92e2708eab6 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); } }); diff --git a/erpnext/education/doctype/fee_schedule/fee_schedule.js b/erpnext/education/doctype/fee_schedule/fee_schedule.js index 75dd4469e84..65b5fa6cf23 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 b331c6d3c0e..310c4105f47 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 aaf42b47517..433bd64d2fb 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,6 +10,10 @@ frappe.ui.form.on("Fees", { 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("academic_term",function(){ return{ @@ -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/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index cbafd7d3ac0..e399b22f90f 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'); diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index d32fdbcaf16..410840771cf 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) { @@ -17,6 +19,8 @@ frappe.ui.form.on('Payroll Entry', { } }; }); + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { @@ -122,6 +126,7 @@ frappe.ui.form.on('Payroll Entry', { company: function (frm) { frm.events.clear_employee_table(frm); + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, department: function (frm) { diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index ec6b3dc6df1..3f293782b41 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(); @@ -106,6 +108,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ if(!item.warehouse && frm.doc.set_warehouse) { item.warehouse = frm.doc.set_warehouse; } + + erpnext.accounts.dimensions.copy_dimension_from_first_row(frm, cdt, cdn, 'items'); } }); diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js index 7a42fb56148..c79736d0e19 100644 --- a/erpnext/public/js/utils/dimension_tree_filter.js +++ b/erpnext/public/js/utils/dimension_tree_filter.js @@ -1,60 +1,79 @@ -frappe.provide('frappe.ui.form'); -let default_dimensions = {}; +frappe.provide('erpnext.accounts'); -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", "Asset", "Asset Value Adjustment"]; - -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", - "Sales Taxes and Charges", "Purchase Taxes and Charges"]; - -frappe.call({ - method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimension_filters", - args: { - 'with_costcenter_and_project': true +erpnext.accounts.dimensions = { + setup_dimension_filters(frm, doctype) { + this.accounting_dimensions = []; + this.default_dimensions = {}; + this.fetch_custom_dimensions(frm, doctype); }, - 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) => { - frappe.model.with_doctype(dimension['document_type'], () => { - 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') { - setup_child_filters(frm, df.options, df.fieldname, dimension['fieldname']); - } + 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); + } + }); + }, - setup_account_filters(frm, dimension['fieldname'], parent_fields); - }); + setup_filters(frm, doctype) { + this.accounting_dimensions.forEach((dimension) => { + frappe.model.with_doctype(dimension['document_type'], () => { + 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 (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); + } + }); + + 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); + }); + }); + } + }, + + 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(default_dimensions || {}).length > 0 - && default_dimensions[frm.doc.company]) { + if(frm.doc.company && Object.keys(this.default_dimensions || {}).length > 0 + && this.default_dimensions[frm.doc.company]) { - let default_dimension = default_dimensions[frm.doc.company][dimension['fieldname']]; + let default_dimension = this.default_dimensions[frm.doc.company][dimension['fieldname']]; if(default_dimension) { if (frappe.meta.has_field(doctype, dimension['fieldname'])) { @@ -69,47 +88,14 @@ doctypes_with_dimensions.forEach((doctype) => { } }); } - }); -}); + }, -child_docs.forEach((doctype) => { - frappe.ui.form.on(doctype, { - items_add: function(frm, cdt, cdn) { - copy_dimension(frm, cdt, cdn, "items"); - }, - - accounts_add: function(frm, cdt, cdn) { - copy_dimension(frm, cdt, cdn, "accounts"); + copy_dimension_from_first_row(frm, cdt, cdn, fieldname) { + if (frappe.meta.has_field(frm.doctype, fieldname)) { + this.accounting_dimensions.forEach((dimension) => { + let row = frappe.get_doc(cdt, cdn); + frm.script_manager.copy_from_first_row(fieldname, row, [dimension['fieldname']]); + }); } - }); -}); - -let copy_dimension = function(frm, cdt, cdn, fieldname) { - erpnext.dimension_filters.forEach((dimension) => { - let row = frappe.get_doc(cdt, cdn); - frm.script_manager.copy_from_first_row(fieldname, row, [dimension['fieldname']]); - }); -}; - -let setup_child_filters = function(frm, doctype, parentfield, dimension) { - let fields = []; - - frappe.model.with_doctype(doctype, () => { - frappe.meta.get_docfields(doctype).forEach((df) => { - if (df.fieldtype === 'Link' && df.options === 'Account') { - fields.push(df.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); - }); - }); -}; - -let setup_account_filters = function(frm, dimension, fields) { - frm.set_query(dimension, function(doc) { - return erpnext.queries.get_filtered_dimensions(doc, fields, dimension, doc.company); - }); -}; \ No newline at end of file + } +} \ No newline at end of file diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 251a26a592e..ccc719c26d0 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) { @@ -75,7 +76,7 @@ frappe.ui.form.on("Delivery Note", { } }); - + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, print_without_amount: function(frm) { @@ -305,6 +306,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/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 01edd99e9d2..527b0d3ea93 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/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index bc1d81d3565..d998729c479 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) { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 91217582ca4..80f18a63f56 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1,6 +1,7 @@ // 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"); frappe.ui.form.on('Stock Entry', { setup: function(frm) { @@ -97,6 +98,7 @@ frappe.ui.form.on('Stock Entry', { }); 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 +314,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); } }, diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index e2121fce3f2..be9404d9c8c 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) { From 2c114053ad3087ad12a8d8a084cabb817b126066 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 27 Nov 2020 15:05:28 +0530 Subject: [PATCH 036/295] feat: Patient History Settings --- .../__init__.py | 0 .../patient_history_custom_document_type.json | 46 ++++++++++ .../patient_history_custom_document_type.py | 10 +++ .../patient_history_settings/__init__.py | 0 .../patient_history_settings.js | 85 +++++++++++++++++++ .../patient_history_settings.json | 55 ++++++++++++ .../patient_history_settings.py | 10 +++ .../test_patient_history_settings.py | 10 +++ .../__init__.py | 0 ...atient_history_standard_document_type.json | 47 ++++++++++ .../patient_history_standard_document_type.py | 10 +++ 11 files changed, 273 insertions(+) create mode 100644 erpnext/healthcare/doctype/patient_history_custom_document_type/__init__.py create mode 100644 erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.json create mode 100644 erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.py create mode 100644 erpnext/healthcare/doctype/patient_history_settings/__init__.py create mode 100644 erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js create mode 100644 erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.json create mode 100644 erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py create mode 100644 erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py create mode 100644 erpnext/healthcare/doctype/patient_history_standard_document_type/__init__.py create mode 100644 erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.json create mode 100644 erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.py 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 00000000000..e69de29bb2d 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 00000000000..a158075e7b9 --- /dev/null +++ b/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.json @@ -0,0 +1,46 @@ +{ + "actions": [], + "creation": "2020-11-25 13:40:23.054469", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "select_fields", + "selected_fields" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "select_fields", + "fieldtype": "Button", + "in_list_view": 1, + "label": "Select Fields" + }, + { + "fieldname": "selected_fields", + "fieldtype": "Code", + "label": "selected_fields" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-11-25 14:19:33.637543", + "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 00000000000..f0a1f929f45 --- /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 00000000000..e69de29bb2d 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 00000000000..155476e2b10 --- /dev/null +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js @@ -0,0 +1,85 @@ +// 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, + module: 'Healthcare' + } + }; + }); + }, + + field_selector: function(frm, doc) { + let document_fields = (JSON.parse(doc.selected_fields)).map(f => f.fieldname); + let d = new frappe.ui.Dialog({ + title: __('{0} Fields', [__(doc.document_type)]), + fields: [ + { + label: __('Select Fields'), + fieldtype: 'MultiCheck', + fieldname: 'fields', + options: frm.events.get_doctype_fields(frm, doc.document_type, document_fields), + columns: 2 + } + ] + }); + + d.set_primary_action(__('Save'), () => { + let values = d.get_values().fields; + + let selected_fields = []; + + for (let idx in values) { + let value = values[idx]; + + let field = frappe.meta.get_docfield(doc.document_type, value); + if (field) { + selected_fields.push({ + label: field.label, + fieldname: field.fieldname + }); + } + } + + frappe.model.set_value('Patient History Custom Document Type', doc.name, 'selected_fields', JSON.stringify(selected_fields)); + d.hide(); + }); + + d.show(); + }, + + get_doctype_fields(frm, document_type, fields) { + let multiselect_fields = []; + + frappe.model.with_doctype(document_type, () => { + // get doctype fields + frappe.get_doc('DocType', document_type).fields.forEach(field => { + if (!in_list(frappe.model.no_value_type, field.fieldtype) && !field.hidden) { + multiselect_fields.push({ + label: field.label, + value: field.fieldname, + checked: in_list(fields, field.fieldname) + }); + } + }); + }); + + return multiselect_fields; + } +}); + +frappe.ui.form.on('Patient History Custom Document Type', { + select_fields: function(frm) { + let doc = frm.selected_doc; + + if (!doc.document_type) + frappe.throw(__('Select the Document Type first.')) + + frm.events.field_selector(frm, doc); + } + +}); 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 00000000000..143e2c91eb5 --- /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 00000000000..27cbf2fc60d --- /dev/null +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.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 PatientHistorySettings(Document): + pass 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 00000000000..548c423670f --- /dev/null +++ b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestPatientHistorySettings(unittest.TestCase): + pass 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 00000000000..e69de29bb2d 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 00000000000..ec40d893eba --- /dev/null +++ b/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.json @@ -0,0 +1,47 @@ +{ + "actions": [], + "creation": "2020-11-25 13:39:36.014814", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "select_fields", + "selected_fields" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "select_fields", + "fieldtype": "Button", + "in_list_view": 1, + "label": "Select Fields" + }, + { + "fieldname": "selected_fields", + "fieldtype": "Code", + "label": "Selected Fields" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-11-25 14:19:53.708991", + "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 00000000000..2d94911855a --- /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 From f2932d720882433e18c690be130a7d919a0570d6 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 28 Nov 2020 19:19:58 +0530 Subject: [PATCH 037/295] feat: set date field in Settings for Patient Medical Record --- .../patient_history_custom_document_type.json | 24 ++++++++++++------- .../patient_history_settings.js | 13 ++++------ .../patient_history_settings.py | 17 +++++++++++-- ...atient_history_standard_document_type.json | 16 ++++++------- 4 files changed, 44 insertions(+), 26 deletions(-) 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 index a158075e7b9..7986e48ced7 100644 --- 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 @@ -6,7 +6,8 @@ "engine": "InnoDB", "field_order": [ "document_type", - "select_fields", + "date_fieldname", + "add_edit_fields", "selected_fields" ], "fields": [ @@ -18,22 +19,29 @@ "options": "DocType", "reqd": 1 }, - { - "fieldname": "select_fields", - "fieldtype": "Button", - "in_list_view": 1, - "label": "Select Fields" - }, { "fieldname": "selected_fields", "fieldtype": "Code", "label": "selected_fields" + }, + { + "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-25 14:19:33.637543", + "modified": "2020-11-28 19:04:48.323164", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient History Custom Document Type", diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js index 155476e2b10..ca2707f6a60 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js @@ -73,13 +73,10 @@ frappe.ui.form.on('Patient History Settings', { }); frappe.ui.form.on('Patient History Custom Document Type', { - select_fields: function(frm) { - let doc = frm.selected_doc; - - if (!doc.document_type) - frappe.throw(__('Select the Document Type first.')) - - frm.events.field_selector(frm, doc); + 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.py b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py index 27cbf2fc60d..9e876e8c959 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py @@ -3,8 +3,21 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe +from frappe import _ from frappe.model.document import Document class PatientHistorySettings(Document): - pass + def validate(self): + self.validate_date_fieldnames() + + 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))) \ No newline at end of file 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 index ec40d893eba..ef4fc2bfe1e 100644 --- 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 @@ -6,7 +6,7 @@ "engine": "InnoDB", "field_order": [ "document_type", - "select_fields", + "add_edit_fields", "selected_fields" ], "fields": [ @@ -19,22 +19,22 @@ "read_only": 1, "reqd": 1 }, - { - "fieldname": "select_fields", - "fieldtype": "Button", - "in_list_view": 1, - "label": "Select Fields" - }, { "fieldname": "selected_fields", "fieldtype": "Code", "label": "Selected Fields" + }, + { + "fieldname": "add_edit_fields", + "fieldtype": "Button", + "in_list_view": 1, + "label": "Add / Edit Fields" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-11-25 14:19:53.708991", + "modified": "2020-11-28 18:57:30.446348", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient History Standard Document Type", From c91e03c8911286cb50731dd7064501b9c80474a9 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 28 Nov 2020 20:24:06 +0530 Subject: [PATCH 038/295] feat: Create Patient Medical Record on configured doctype submission --- .../patient_history_settings.js | 6 ++- .../patient_history_settings.py | 53 ++++++++++++++++++- erpnext/hooks.py | 4 ++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js index ca2707f6a60..60926eeb119 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js @@ -40,7 +40,8 @@ frappe.ui.form.on('Patient History Settings', { if (field) { selected_fields.push({ label: field.label, - fieldname: field.fieldname + fieldname: field.fieldname, + fieldtype: field.fieldtype }); } } @@ -58,7 +59,8 @@ frappe.ui.form.on('Patient History Settings', { frappe.model.with_doctype(document_type, () => { // get doctype fields frappe.get_doc('DocType', document_type).fields.forEach(field => { - if (!in_list(frappe.model.no_value_type, field.fieldtype) && !field.hidden) { + if (!in_list(frappe.model.no_value_type, field.fieldtype) || + in_list(frappe.model.table_fields, field.fieldtype) && !field.hidden) { multiselect_fields.push({ label: field.label, value: field.fieldname, diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py index 9e876e8c959..af8c6f45574 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ +from frappe.utils import cstr from frappe.model.document import Document class PatientHistorySettings(Document): @@ -20,4 +21,54 @@ class PatientHistorySettings(Document): 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))) \ No newline at end of file + entry.idx, frappe.bold(entry.date_fieldname), frappe.bold(entry.document_type))) + + +def create_medical_record(doc, method=None): + if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_setup_wizard or \ + frappe.db.get_value('Doctype', doc.doctype, 'module') != 'Healthcare': + 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 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 doc.get(fieldname): + formated_value = format_value(doc.get(fieldname), meta.get_field(fieldname), doc) + subject += frappe.bold(_(entry.get('label')) + ': ') + cstr(formated_value) + subject += '
' + + return subject + + +def get_date_field(doctype): + return frappe.db.get_value('Patient History Custom Document Type', + { 'document_type': doctype }, 'date_fieldname') + + +def get_patient_history_fields(doc): + import json + patient_history_fields = frappe.db.get_value('Patient History Custom Document Type', + { 'document_type': doc.doctype }, 'selected_fields') + + if patient_history_fields: + return json.loads(patient_history_fields) + diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 741176f33f4..4ee42c7559d 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -221,6 +221,10 @@ standard_queries = { } doc_events = { + "*": { + "on_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.create_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" From a75f79762f224f8b38d2877b7a596cc93d9647fd Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 28 Nov 2020 23:09:07 +0530 Subject: [PATCH 039/295] feat: Link for individual documents in Patient History --- .../page/patient_history/patient_history.html | 1 - .../page/patient_history/patient_history.js | 19 ++++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/erpnext/healthcare/page/patient_history/patient_history.html b/erpnext/healthcare/page/patient_history/patient_history.html index 7a9446dffd7..60f4501fed1 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") %}

diff --git a/erpnext/healthcare/page/patient_history/patient_history.js b/erpnext/healthcare/page/patient_history/patient_history.js index fe5b7bc4883..3e6d790ca73 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.js +++ b/erpnext/healthcare/page/patient_history/patient_history.js @@ -16,6 +16,8 @@ frappe.pages['patient_history'].on_page_load = function(wrapper) { fieldtype: "Link", options: "Patient", fieldname: "patient", + placeholder: __('Select Patient'), + only_select: true, change: function(){ if(pid != patient.get_value() && patient.get_value()){ me.start = 0; @@ -27,7 +29,6 @@ frappe.pages['patient_history'].on_page_load = function(wrapper) { pid = patient.get_value(); } }, - only_input: true, }); patient.refresh(); @@ -120,7 +121,11 @@ var add_to_records = function(me, data){ data[i].imgsrc = false; } var time_line_heading = data[i].practitioner ? `${data[i].practitioner} ` : ``; - time_line_heading += data[i].reference_doctype + " - "+ data[i].reference_name; + time_line_heading += data[i].reference_doctype + " - " + + ` + ${data[i].reference_name} + ` + details += `
  • `; @@ -135,7 +140,7 @@ var add_to_records = function(me, data){ } details += `
    - `+time_line_heading+` on + `+time_line_heading+` ${data[i].date_sep} @@ -172,11 +177,11 @@ var add_date_separator = function(data) { var diff = frappe.datetime.get_day_diff(frappe.datetime.get_today(), frappe.datetime.obj_to_str(date)); if(diff < 1) { - var pdate = 'Today'; + var pdate = __('Today'); } else if(diff < 2) { - pdate = 'Yesterday'; + pdate = __('Yesterday'); } else { - pdate = frappe.datetime.global_date_format(date); + pdate = __('on ') + frappe.datetime.global_date_format(date); } data.date_sep = pdate; return data; @@ -227,7 +232,7 @@ var show_patient_vital_charts = function(patient, me, btn_show_id, pts, title) { }, callback: function(r) { if (r.message){ - var show_chart_btns_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 get_documents = function(patient, me) { frappe.call({ - "method": "erpnext.healthcare.page.patient_history.patient_history.get_feed", + '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){ + 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.creation); + 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) { + if (diff < 1) { + pdate = __('Today'); + } else if (diff < 2) { pdate = __('Yesterday'); } else { pdate = __('on ') + frappe.datetime.global_date_format(date); @@ -187,107 +212,118 @@ var add_date_separator = function(data) { 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 = ""; - me.page.main.find(".show_chart_btns").html(show_chart_btns_html); - var data = r.message; + if (r.message) { + let show_chart_btns_html = ` + `; + + 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(""); + } else { + me.page.main.find('.patient_vital_charts').html(''); + me.page.main.find('.show_chart_btns').html(''); } } }); From fc1e352adf3f7efbbf243a029714d3c8eef0720f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 29 Nov 2020 22:38:14 +0530 Subject: [PATCH 041/295] feat: Doctype filter for Patient History --- .../page/patient_history/patient_history.css | 5 ++ .../page/patient_history/patient_history.html | 8 +++ .../page/patient_history/patient_history.js | 58 ++++++++++++++---- .../page/patient_history/patient_history.py | 59 ++++++++++++------- 4 files changed, 98 insertions(+), 32 deletions(-) diff --git a/erpnext/healthcare/page/patient_history/patient_history.css b/erpnext/healthcare/page/patient_history/patient_history.css index 865d6abee00..1bb589164e6 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 60f4501fed1..00b38e7258d 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.html +++ b/erpnext/healthcare/page/patient_history/patient_history.html @@ -10,6 +10,14 @@
    +
    +
    +
    +
    +
    +
    +
    +
    diff --git a/erpnext/healthcare/page/patient_history/patient_history.js b/erpnext/healthcare/page/patient_history/patient_history.js index 5f1851fb0f3..40b86fdff4d 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.js +++ b/erpnext/healthcare/page/patient_history/patient_history.js @@ -20,14 +20,16 @@ frappe.pages['patient_history'].on_page_load = function(wrapper) { placeholder: __('Select Patient'), only_select: true, change: function(){ - if (pid != patient.get_value() && patient.get_value()) { + 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'); + 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; } }, }); @@ -93,14 +95,48 @@ frappe.pages['patient_history'].on_page_load = function(wrapper) { }); }; -let get_documents = function(patient, me) { +let setup_filters = function(patient, me) { + 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()); + }, + get_data: () => { + return document_types.map(document_type => { + return { + description: document_type, + value: document_type + }; + }); + }, + } + }); + doctype_filter.refresh(); + }) +} + +let get_documents = function(patient, me, document_types="") { + let filters = { + name: patient, + start: me.start, + page_length: 20 + }; + if (document_types) + filters['document_types'] = document_types; + frappe.call({ 'method': 'erpnext.healthcare.page.patient_history.patient_history.get_feed', - args: { - name: patient, - start: me.start, - page_length: 20 - }, + args: filters, callback: function(r) { let data = r.message; if (data.length) { diff --git a/erpnext/healthcare/page/patient_history/patient_history.py b/erpnext/healthcare/page/patient_history/patient_history.py index 772aa4ef5eb..c04b3761970 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.py +++ b/erpnext/healthcare/page/patient_history/patient_history.py @@ -4,36 +4,53 @@ 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, 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 = {'patient': name} + if document_types: + document_types = json.loads(document_types) + filters['reference_doctype'] = ['IN', document_types] + + result = frappe.db.get_all('Patient Medical Record', + fields=['name', 'owner', 'creation', + 'reference_doctype', 'reference_name', 'subject'], + filters=filters, + order_by='creation DESC', + limit=cint(page_length), + start=cint(start) + ) + return result @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', 'creation', + 'reference_doctype', 'reference_name', 'subject'], + filters={ + 'reference_doctype': doctype, + 'reference_name': docname + }, + order_by='creation 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 \ No newline at end of file From 4af3d4e4bb745f35549ce3b638981206c1f84a3e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 29 Nov 2020 22:43:56 +0530 Subject: [PATCH 042/295] fix: feed not visible when filter is reset --- erpnext/healthcare/page/patient_history/patient_history.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/healthcare/page/patient_history/patient_history.py b/erpnext/healthcare/page/patient_history/patient_history.py index c04b3761970..b8494c9e587 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.py +++ b/erpnext/healthcare/page/patient_history/patient_history.py @@ -14,7 +14,8 @@ def get_feed(name, document_types=None, start=0, page_length=20): filters = {'patient': name} if document_types: document_types = json.loads(document_types) - filters['reference_doctype'] = ['IN', document_types] + if len(document_types): + filters['reference_doctype'] = ['IN', document_types] result = frappe.db.get_all('Patient Medical Record', fields=['name', 'owner', 'creation', From 4d6d115a4d43aa3b7c6ebf2604998a2a4728050c Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 29 Nov 2020 23:16:12 +0530 Subject: [PATCH 043/295] feat: date range filter for Patient History --- .../page/patient_history/patient_history.html | 3 +- .../page/patient_history/patient_history.js | 27 +++++++++++++++-- .../page/patient_history/patient_history.py | 30 ++++++++++++++----- 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/erpnext/healthcare/page/patient_history/patient_history.html b/erpnext/healthcare/page/patient_history/patient_history.html index 00b38e7258d..deaaa97868c 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.html +++ b/erpnext/healthcare/page/patient_history/patient_history.html @@ -14,8 +14,7 @@
    -
    -
    +
    diff --git a/erpnext/healthcare/page/patient_history/patient_history.js b/erpnext/healthcare/page/patient_history/patient_history.js index 40b86fdff4d..5a6295b7079 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.js +++ b/erpnext/healthcare/page/patient_history/patient_history.js @@ -109,7 +109,7 @@ let setup_filters = function(patient, me) { change: () => { me.start = 0; me.page.main.find('.patient_documents_list').html(''); - get_documents(patient, me, doctype_filter.get_value()); + get_documents(patient, me, doctype_filter.get_value(), date_range_field.get_value()); }, get_data: () => { return document_types.map(document_type => { @@ -122,10 +122,29 @@ let setup_filters = function(patient, me) { } }); doctype_filter.refresh(); - }) + + 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="") { +let get_documents = function(patient, me, document_types="", selected_date_range="") { let filters = { name: patient, start: me.start, @@ -133,6 +152,8 @@ let get_documents = function(patient, me, document_types="") { }; 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', diff --git a/erpnext/healthcare/page/patient_history/patient_history.py b/erpnext/healthcare/page/patient_history/patient_history.py index b8494c9e587..de8a5771d2f 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.py +++ b/erpnext/healthcare/page/patient_history/patient_history.py @@ -9,13 +9,9 @@ from frappe.utils import cint from erpnext.healthcare.utils import render_docs_as_html @frappe.whitelist() -def get_feed(name, document_types=None, start=0, page_length=20): +def get_feed(name, document_types=None, date_range=None, start=0, page_length=20): """get feed""" - filters = {'patient': name} - if document_types: - document_types = json.loads(document_types) - if len(document_types): - filters['reference_doctype'] = ['IN', document_types] + filters = get_filters(name, document_types, date_range) result = frappe.db.get_all('Patient Medical Record', fields=['name', 'owner', 'creation', @@ -28,6 +24,25 @@ def get_feed(name, document_types=None, start=0, page_length=20): 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['creation'] = ['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""" @@ -43,6 +58,7 @@ def get_feed_for_dt(doctype, docname): return result + @frappe.whitelist() def get_patient_history_doctypes(): document_types = [] @@ -54,4 +70,4 @@ def get_patient_history_doctypes(): for entry in settings.custom_doctypes: document_types.append(entry.document_type) - return document_types \ No newline at end of file + return document_types From 06e7cc2c35bf526f5a7bfba81d645ed4028a276b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 30 Nov 2020 12:01:48 +0530 Subject: [PATCH 044/295] fix: Handle table field rendering in Patient Medical record --- .../patient_history_settings.py | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py index af8c6f45574..759fcadef26 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py @@ -51,10 +51,16 @@ def set_subject_field(doc): for entry in patient_history_fields: fieldname = entry.get('fieldname') - if doc.get(fieldname): - formated_value = format_value(doc.get(fieldname), meta.get_field(fieldname), doc) - subject += frappe.bold(_(entry.get('label')) + ': ') + cstr(formated_value) - subject += '
    ' + 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) + + subject += '
    ' return subject @@ -72,3 +78,27 @@ def get_patient_history_fields(doc): 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 From f3df5c9f3cec4db7092c301d03897eca3acc67e0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 30 Nov 2020 12:16:28 +0530 Subject: [PATCH 045/295] feat: patch to setup standard doctype config in Patient History Settings --- .../healthcare/doctype/lab_test/lab_test.json | 6 +- .../patient_encounter/patient_encounter.json | 4 +- ...atient_history_standard_document_type.json | 11 ++- erpnext/healthcare/setup.py | 80 +++++++++++++++++++ erpnext/patches.txt | 1 + ..._history_settings_for_standard_doctypes.py | 9 +++ 6 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.json b/erpnext/healthcare/doctype/lab_test/lab_test.json index edf1d911aac..ac61fea3ad7 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/patient_encounter/patient_encounter.json b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json index 15675f4673f..b646ff9ebe6 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_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 index ef4fc2bfe1e..9c9d0cb4cd8 100644 --- 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 @@ -6,6 +6,7 @@ "engine": "InnoDB", "field_order": [ "document_type", + "date_fieldname", "add_edit_fields", "selected_fields" ], @@ -29,12 +30,20 @@ "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-28 18:57:30.446348", + "modified": "2020-11-30 12:15:14.940935", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient History Standard Document Type", diff --git a/erpnext/healthcare/setup.py b/erpnext/healthcare/setup.py index 06840801d37..bf4df7e4c88 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/patches.txt b/erpnext/patches.txt index 25be8841174..fcb63ed6e4d 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -735,3 +735,4 @@ erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail 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.setup_patient_history_settings_for_standard_doctypes 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 00000000000..3332be05613 --- /dev/null +++ b/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py @@ -0,0 +1,9 @@ +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 + + setup_patient_history_settings() \ No newline at end of file From 4097e89f8b80140389eec6b29aea337d3a7835f0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 30 Nov 2020 12:48:39 +0530 Subject: [PATCH 046/295] feat: Standard doctype config for Patient History --- .../patient_history_settings.js | 17 ++++++++++++-- .../patient_history_settings.py | 22 ++++++++++++++----- erpnext/hooks.py | 1 + 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js index 60926eeb119..c3d0dce6756 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js @@ -13,7 +13,7 @@ frappe.ui.form.on('Patient History Settings', { }); }, - field_selector: function(frm, doc) { + field_selector: function(frm, doc, standard=1) { let document_fields = (JSON.parse(doc.selected_fields)).map(f => f.fieldname); let d = new frappe.ui.Dialog({ title: __('{0} Fields', [__(doc.document_type)]), @@ -45,8 +45,12 @@ frappe.ui.form.on('Patient History Settings', { }); } } + let doctype = 'Patient History Custom Document Type'; + if (standard) + doctype = 'Patient History Standard Document Type'; - frappe.model.set_value('Patient History Custom Document Type', doc.name, 'selected_fields', JSON.stringify(selected_fields)); + + frappe.model.set_value(doctype, doc.name, 'selected_fields', JSON.stringify(selected_fields)); d.hide(); }); @@ -75,6 +79,15 @@ frappe.ui.form.on('Patient History Settings', { }); frappe.ui.form.on('Patient History Custom Document Type', { + add_edit_fields: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.document_type) { + frm.events.field_selector(frm, row, standard=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) { diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py index 759fcadef26..20b062e909e 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py @@ -7,6 +7,7 @@ import frappe from frappe import _ from frappe.utils import cstr 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): @@ -29,6 +30,9 @@ def create_medical_record(doc, method=None): frappe.db.get_value('Doctype', doc.doctype, 'module') != 'Healthcare': return + if doc.doctype not in get_patient_history_doctypes(): + return + subject = set_subject_field(doc) date_field = get_date_field(doc.doctype) medical_record = frappe.new_doc('Patient Medical Record') @@ -66,14 +70,15 @@ def set_subject_field(doc): def get_date_field(doctype): - return frappe.db.get_value('Patient History Custom Document Type', - { 'document_type': doctype }, 'date_fieldname') + dt = get_patient_history_config_dt(doctype) + + return frappe.db.get_value(dt, { 'document_type': doctype }, 'date_fieldname') def get_patient_history_fields(doc): import json - patient_history_fields = frappe.db.get_value('Patient History Custom Document Type', - { 'document_type': doc.doctype }, 'selected_fields') + 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) @@ -99,6 +104,13 @@ def get_formatted_value_for_table_field(items, df): create_head = False table_row += '' - html += "" + table_head + 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' diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 4ee42c7559d..aa5291aa0e7 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -223,6 +223,7 @@ standard_queries = { doc_events = { "*": { "on_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.create_medical_record", + "on_cancel": "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": { From c0fcc807d338ad614aa76f7ea03d2b804760da77 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 30 Nov 2020 13:22:01 +0530 Subject: [PATCH 047/295] feat: hooks for updating and deleting medical records --- .../patient_history_settings.py | 41 +++++++++++++++++-- erpnext/hooks.py | 2 +- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py index 20b062e909e..367c34f1e86 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py @@ -26,11 +26,11 @@ class PatientHistorySettings(Document): def create_medical_record(doc, method=None): - if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_setup_wizard or \ - frappe.db.get_value('Doctype', doc.doctype, 'module') != 'Healthcare': + medical_record_required = validate_medical_record_required(doc) + if not medical_record_required: return - if doc.doctype not in get_patient_history_doctypes(): + if frappe.db.exists('Patient Medical Record', { 'reference_name': doc.name }): return subject = set_subject_field(doc) @@ -46,6 +46,30 @@ def create_medical_record(doc, method=None): 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 @@ -114,3 +138,14 @@ def get_patient_history_config_dt(doctype): 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 \ + frappe.db.get_value('Doctype', doc.doctype, 'module') != 'Healthcare': + return False + + if doc.doctype not in get_patient_history_doctypes(): + return False + + return True \ No newline at end of file diff --git a/erpnext/hooks.py b/erpnext/hooks.py index aa5291aa0e7..51c169f4003 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -223,7 +223,7 @@ standard_queries = { doc_events = { "*": { "on_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.create_medical_record", - "on_cancel": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.update_medical_record", + "on_update": "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": { From c538a4a31d2c9f829a308e03a01d9eac5d537d0e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 30 Nov 2020 13:22:34 +0530 Subject: [PATCH 048/295] refactor: remove medical record creation methods from individual doctypes --- .../clinical_procedure/clinical_procedure.py | 19 ------ .../healthcare/doctype/lab_test/lab_test.py | 56 ----------------- .../patient_encounter/patient_encounter.py | 62 +------------------ .../therapy_session/therapy_session.py | 21 ------- .../doctype/vital_signs/vital_signs.py | 40 ------------ 5 files changed, 1 insertion(+), 197 deletions(-) diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py index e55a1433a51..c3242284674 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/lab_test/lab_test.py b/erpnext/healthcare/doctype/lab_test/lab_test.py index 2db77438653..4b57cd073d0 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_encounter/patient_encounter.py b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py index 87f42491fce..cc2141790f7 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/therapy_session/therapy_session.py b/erpnext/healthcare/doctype/therapy_session/therapy_session.py index 85d09701774..f8a8e0c8a19 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_cancel(self): self.update_sessions_count_in_therapy_plan(on_cancel=True) @@ -135,23 +134,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 69d81ff4b08..35c823d739c 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 From 5e3c51bf7d157a34baed3e791f8bb40052587654 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 30 Nov 2020 13:35:00 +0530 Subject: [PATCH 049/295] refactor: format value using standard formatters --- erpnext/healthcare/utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/healthcare/utils.py b/erpnext/healthcare/utils.py index 96282f50a92..6b495a4eac9 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 @@ -642,11 +643,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: From 4ee293d2f45c257fa4b103adc1af088e68d4dd5f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 30 Nov 2020 14:12:48 +0530 Subject: [PATCH 050/295] fix: handle non-configured fields --- .../patient_history_custom_document_type.json | 5 +++-- .../patient_history_settings.js | 12 ++++++++---- .../patient_history_standard_document_type.json | 5 +++-- 3 files changed, 14 insertions(+), 8 deletions(-) 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 index 7986e48ced7..3025c7b06d7 100644 --- 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 @@ -22,7 +22,8 @@ { "fieldname": "selected_fields", "fieldtype": "Code", - "label": "selected_fields" + "label": "Selected Fields", + "read_only": 1 }, { "fieldname": "add_edit_fields", @@ -41,7 +42,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-11-28 19:04:48.323164", + "modified": "2020-11-30 13:54:37.474671", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient History Custom Document Type", diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js index c3d0dce6756..ee363462ef8 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js @@ -14,7 +14,11 @@ frappe.ui.form.on('Patient History Settings', { }, field_selector: function(frm, doc, standard=1) { - let document_fields = (JSON.parse(doc.selected_fields)).map(f => f.fieldname); + let document_fields = []; + if (doc.selected_fields) + document_fields = (JSON.parse(doc.selected_fields)).map(f => f.fieldname); + + let doctype_fields = frm.events.get_doctype_fields(frm, doc.document_type, document_fields); let d = new frappe.ui.Dialog({ title: __('{0} Fields', [__(doc.document_type)]), fields: [ @@ -22,7 +26,7 @@ frappe.ui.form.on('Patient History Settings', { label: __('Select Fields'), fieldtype: 'MultiCheck', fieldname: 'fields', - options: frm.events.get_doctype_fields(frm, doc.document_type, document_fields), + options: doctype_fields, columns: 2 } ] @@ -49,7 +53,7 @@ frappe.ui.form.on('Patient History Settings', { if (standard) doctype = 'Patient History Standard Document Type'; - + d.refresh(); frappe.model.set_value(doctype, doc.name, 'selected_fields', JSON.stringify(selected_fields)); d.hide(); }); @@ -82,7 +86,7 @@ frappe.ui.form.on('Patient History Custom Document Type', { add_edit_fields: function(frm, cdt, cdn) { let row = locals[cdt][cdn]; if (row.document_type) { - frm.events.field_selector(frm, row, standard=0); + frm.events.field_selector(frm, row, 0); } } }); 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 index 9c9d0cb4cd8..b43099c4ea9 100644 --- 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 @@ -23,7 +23,8 @@ { "fieldname": "selected_fields", "fieldtype": "Code", - "label": "Selected Fields" + "label": "Selected Fields", + "read_only": 1 }, { "fieldname": "add_edit_fields", @@ -43,7 +44,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-11-30 12:15:14.940935", + "modified": "2020-11-30 13:54:56.773325", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient History Standard Document Type", From ed3fc20731ae7e2279fabf6274a7660ca520a2e7 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 30 Nov 2020 14:56:24 +0530 Subject: [PATCH 051/295] fix: sider issues --- .../page/patient_history/patient_history.js | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/erpnext/healthcare/page/patient_history/patient_history.js b/erpnext/healthcare/page/patient_history/patient_history.js index 5a6295b7079..d509ea22a2d 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.js +++ b/erpnext/healthcare/page/patient_history/patient_history.js @@ -24,7 +24,7 @@ frappe.pages['patient_history'].on_page_load = function(wrapper) { if (pid != patient_id && patient_id) { me.start = 0; me.page.main.find('.patient_documents_list').html(''); - setup_filters(patient_id, me) + 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'); @@ -90,7 +90,7 @@ frappe.pages['patient_history'].on_page_load = function(wrapper) { 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); }); }; @@ -142,7 +142,7 @@ let setup_filters = function(patient, me) { }); date_range_field.refresh(); }); -} +}; let get_documents = function(patient, me, document_types="", selected_date_range="") { let filters = { @@ -176,7 +176,7 @@ let get_documents = function(patient, me, document_types="", selected_date_range let add_to_records = function(me, data) { let details = "
    `; } details += ` ${data.patient_name}
    ${data.sex}`; - if (data.email) details += `
    ${data.email}` + 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}`; @@ -299,7 +299,7 @@ let show_patient_info = function(patient, me) { 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){ + if (details) { details = `
    ` + details + `
    `; } me.page.main.find('.patient_details').html(details); @@ -337,7 +337,7 @@ let show_patient_vital_charts = function(patient, me, btn_show_id, pts, title) { let bp_systolic = [], bp_diastolic = [], temperature = []; let pulse = [], respiratory_rate = [], bmi = [], height = [], weight = []; - for(let i=0; i Date: Mon, 30 Nov 2020 15:00:42 +0530 Subject: [PATCH 052/295] refactor: show Patient History feed as per configured date instead of creation --- .../healthcare/page/patient_history/patient_history.js | 2 +- .../healthcare/page/patient_history/patient_history.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/healthcare/page/patient_history/patient_history.js b/erpnext/healthcare/page/patient_history/patient_history.js index d509ea22a2d..9c44d63b965 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.js +++ b/erpnext/healthcare/page/patient_history/patient_history.js @@ -254,7 +254,7 @@ let add_to_records = function(me, data) { }; let add_date_separator = function(data) { - let date = frappe.datetime.str_to_obj(data.creation); + 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)); diff --git a/erpnext/healthcare/page/patient_history/patient_history.py b/erpnext/healthcare/page/patient_history/patient_history.py index de8a5771d2f..4cdfd64a697 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.py +++ b/erpnext/healthcare/page/patient_history/patient_history.py @@ -14,10 +14,10 @@ def get_feed(name, document_types=None, date_range=None, start=0, page_length=20 filters = get_filters(name, document_types, date_range) result = frappe.db.get_all('Patient Medical Record', - fields=['name', 'owner', 'creation', + fields=['name', 'owner', 'communication_date', 'reference_doctype', 'reference_name', 'subject'], filters=filters, - order_by='creation DESC', + order_by='communication_date DESC', limit=cint(page_length), start=cint(start) ) @@ -36,7 +36,7 @@ def get_filters(name, document_types=None, date_range=None): try: date_range = json.loads(date_range) if date_range: - filters['creation'] = ['between', [date_range[0], date_range[1]]] + filters['communication_date'] = ['between', [date_range[0], date_range[1]]] except json.decoder.JSONDecodeError: pass @@ -47,13 +47,13 @@ def get_filters(name, document_types=None, date_range=None): def get_feed_for_dt(doctype, docname): """get feed""" result = frappe.db.get_all('Patient Medical Record', - fields=['name', 'owner', 'creation', + fields=['name', 'owner', 'communication_date', 'reference_doctype', 'reference_name', 'subject'], filters={ 'reference_doctype': doctype, 'reference_name': docname }, - order_by='creation DESC' + order_by='communication_date DESC' ) return result From b5b8c5474a14c394fae3ddb4074d7205bc3dfbbb Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 30 Nov 2020 16:09:15 +0530 Subject: [PATCH 053/295] refactor: move call to fetch doctype fields to server-side --- .../patient_history_settings.js | 47 +++++++++---------- .../patient_history_settings.py | 17 ++++++- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js index ee363462ef8..bf3c5b954e5 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js @@ -18,7 +18,27 @@ frappe.ui.form.on('Patient History Settings', { if (doc.selected_fields) document_fields = (JSON.parse(doc.selected_fields)).map(f => f.fieldname); - let doctype_fields = frm.events.get_doctype_fields(frm, doc.document_type, document_fields); + 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: [ @@ -26,7 +46,7 @@ frappe.ui.form.on('Patient History Settings', { label: __('Select Fields'), fieldtype: 'MultiCheck', fieldname: 'fields', - options: doctype_fields, + options: doc_fields, columns: 2 } ] @@ -49,9 +69,6 @@ frappe.ui.form.on('Patient History Settings', { }); } } - let doctype = 'Patient History Custom Document Type'; - if (standard) - doctype = 'Patient History Standard Document Type'; d.refresh(); frappe.model.set_value(doctype, doc.name, 'selected_fields', JSON.stringify(selected_fields)); @@ -59,26 +76,6 @@ frappe.ui.form.on('Patient History Settings', { }); d.show(); - }, - - get_doctype_fields(frm, document_type, fields) { - let multiselect_fields = []; - - frappe.model.with_doctype(document_type, () => { - // get doctype fields - frappe.get_doc('DocType', document_type).fields.forEach(field => { - if (!in_list(frappe.model.no_value_type, field.fieldtype) || - in_list(frappe.model.table_fields, field.fieldtype) && !field.hidden) { - multiselect_fields.push({ - label: field.label, - value: field.fieldname, - checked: in_list(fields, field.fieldname) - }); - } - }); - }); - - return multiselect_fields; } }); diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py index 367c34f1e86..9f18c6bbf52 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe +import json from frappe import _ from frappe.utils import cstr from frappe.model.document import Document @@ -24,6 +25,21 @@ class PatientHistorySettings(Document): 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 create_medical_record(doc, method=None): medical_record_required = validate_medical_record_required(doc) @@ -100,7 +116,6 @@ def get_date_field(doctype): def get_patient_history_fields(doc): - import json dt = get_patient_history_config_dt(doc.doctype) patient_history_fields = frappe.db.get_value(dt, { 'document_type': doc.doctype }, 'selected_fields') From f6756838ba63075e16ca5c4b3601d2eebe143d33 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 30 Nov 2020 17:51:56 +0530 Subject: [PATCH 054/295] fix: patch --- .../doctype/patient_appointment/test_patient_appointment.py | 1 + .../setup_patient_history_settings_for_standard_doctypes.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index eeed157291e..f8b7f7f2f02 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -148,6 +148,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/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py b/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py index 3332be05613..de08aa26b3b 100644 --- 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 @@ -3,7 +3,11 @@ import frappe from erpnext.healthcare.setup import setup_patient_history_settings def execute(): - if 'Healthcare' not in frappe.get_active_domains(): + 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 From ab5053ef9cbd5ce27f9253a3409bd7be24f597e2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 2 Dec 2020 12:34:40 +0530 Subject: [PATCH 055/295] fix: Accounting dimension import in PCV --- .../doctype/period_closing_voucher/period_closing_voucher.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 7dd5b017703..a74fa062b6a 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) From 59820004b8b1084511664142e29e8d8dbde9e9c6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 2 Dec 2020 12:34:59 +0530 Subject: [PATCH 056/295] fix: Exception naming --- .../test_accounting_dimension_filter.py | 6 +++--- erpnext/accounts/doctype/gl_entry/gl_entry.py | 8 ++++---- erpnext/exceptions.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) 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 index f67e1de4044..fa700e115ce 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -7,7 +7,7 @@ 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 InvalidAccountDimension, MandatoryDimension +from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError class TestAccountingDimensionFilter(unittest.TestCase): def setUp(self): @@ -20,7 +20,7 @@ class TestAccountingDimensionFilter(unittest.TestCase): si.location = 'Block 1' si.save() - self.assertRaises(InvalidAccountDimension, si.submit) + self.assertRaises(InvalidAccountDimensionError, si.submit) def test_mandatory_dimension_validation(self): si = create_sales_invoice(do_not_save=1) @@ -31,7 +31,7 @@ class TestAccountingDimensionFilter(unittest.TestCase): si.items[0].cost_center = '_Test Cost Center 2 - _TC' si.save() - self.assertRaises(MandatoryDimension, si.submit) + self.assertRaises(MandatoryAccountDimensionError, si.submit) def tearDown(self): disable_dimension_filter() diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index f586de82e35..fd6f01e345f 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -11,7 +11,7 @@ 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, InvalidAccountDimension, MandatoryDimension +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 @@ -101,16 +101,16 @@ class GLEntry(Document): 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)), MandatoryDimension) + 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 account {1}").format( - frappe.bold(self.get(dimension)), frappe.bold(self.account)), InvalidAccountDimension) + frappe.bold(self.get(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 account {1}").format( - frappe.bold(self.get(dimension)), frappe.bold(self.account)), InvalidAccountDimension) + frappe.bold(self.get(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError) def check_pl_account(self): if self.is_opening=='Yes' and \ diff --git a/erpnext/exceptions.py b/erpnext/exceptions.py index dcf3d6bad1a..04291cd5bd1 100644 --- a/erpnext/exceptions.py +++ b/erpnext/exceptions.py @@ -6,5 +6,5 @@ class PartyFrozen(frappe.ValidationError): pass class InvalidAccountCurrency(frappe.ValidationError): pass class InvalidCurrency(frappe.ValidationError): pass class PartyDisabled(frappe.ValidationError):pass -class InvalidAccountDimension(frappe.ValidationError): pass -class MandatoryDimension(frappe.ValidationError): pass +class InvalidAccountDimensionError(frappe.ValidationError): pass +class MandatoryAccountDimensionError(frappe.ValidationError): pass From 7b2d518059f1a131a0ece9e6872c5ed8c4a93d04 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 4 Dec 2020 11:28:26 +0530 Subject: [PATCH 057/295] fix: Dimension filters in accounting reports --- erpnext/public/js/utils.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 891bbe5b598..2635d47f886 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"] + }); + } }); } }); From 7fef622b136c4e7da72651a03c120878e2a6d386 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Fri, 4 Dec 2020 19:18:36 +0530 Subject: [PATCH 058/295] fix: drop ship partial order fixed --- erpnext/selling/doctype/sales_order/sales_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index ae227e0110e..3e1c82f9616 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -831,7 +831,7 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=[], tar for supplier in suppliers: po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")}) - if len(po) == 0: + if len(po) == 0 or any( item.get("delivered_by_supplier") == 1 for item in selected_items): doc = get_mapped_doc("Sales Order", source_name, { "Sales Order": { "doctype": "Purchase Order", From c32ac223ed181d3a1a122775b93d6e4672d2e38f Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 7 Dec 2020 14:53:15 +0530 Subject: [PATCH 059/295] feat: adding task field in project template --- .../project_template_task.json | 195 ++---------------- erpnext/projects/doctype/task/task.json | 21 +- 2 files changed, 32 insertions(+), 184 deletions(-) diff --git a/erpnext/projects/doctype/project_template_task/project_template_task.json b/erpnext/projects/doctype/project_template_task/project_template_task.json index 8644d897bb0..80c510db1b0 100644 --- a/erpnext/projects/doctype/project_template_task/project_template_task.json +++ b/erpnext/projects/doctype/project_template_task/project_template_task.json @@ -1,203 +1,32 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, + "actions": [], "creation": "2019-02-18 17:24:41.830096", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "task" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "subject", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, + "fieldname": "task", + "fieldtype": "Link", "in_list_view": 1, - "in_standard_filter": 0, - "label": "Subject", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "start", - "fieldtype": "Int", - "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": "Begin On (Days)", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "duration", - "fieldtype": "Int", - "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": "Duration (Days)", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "task_weight", - "fieldtype": "Float", - "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": "Task Weight", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "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, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Task", + "options": "Task", + "reqd": 1 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, "istable": 1, - "max_attachments": 0, - "modified": "2019-02-18 18:30:22.688966", + "links": [], + "modified": "2020-12-07 13:28:40.961810", "modified_by": "Administrator", "module": "Projects", "name": "Project Template Task", - "name_case": "", "owner": "Administrator", "permissions": [], "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json index 27f1a71a528..a9e3d9bc0fe 100644 --- a/erpnext/projects/doctype/task/task.json +++ b/erpnext/projects/doctype/task/task.json @@ -12,6 +12,7 @@ "issue", "type", "is_group", + "is_template", "column_break0", "status", "priority", @@ -22,9 +23,11 @@ "sb_timeline", "exp_start_date", "expected_time", + "start", "column_break_11", "exp_end_date", "progress", + "duration", "is_milestone", "sb_details", "description", @@ -360,6 +363,22 @@ "label": "Completed By", "no_copy": 1, "options": "User" + }, + { + "default": "0", + "fieldname": "is_template", + "fieldtype": "Check", + "label": "Is Template" + }, + { + "fieldname": "start", + "fieldtype": "Int", + "label": "Begin On (Days)" + }, + { + "fieldname": "duration", + "fieldtype": "Int", + "label": "Duration (Days)" } ], "icon": "fa fa-check", @@ -367,7 +386,7 @@ "is_tree": 1, "links": [], "max_attachments": 5, - "modified": "2020-07-03 12:36:04.960457", + "modified": "2020-12-07 13:26:53.614689", "modified_by": "Administrator", "module": "Projects", "name": "Task", From 5597e386b437ce7fda30960005d64d5fcc00d6a9 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 7 Dec 2020 15:53:45 +0530 Subject: [PATCH 060/295] fix: added test for drop ship partial order --- .../doctype/sales_order/test_sales_order.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index a33d401b572..1d11ad6a7e4 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -772,6 +772,56 @@ class TestSalesOrder(unittest.TestCase): so.load_from_db() so.cancel() + def test_drop_shipping_partial_order(self): + from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order_for_default_supplier, \ + update_status as so_update_status + + # make items + po_item1 = make_item("_Test Item for Drop Shipping 1", {"is_stock_item": 1, "delivered_by_supplier": 1}) + po_item2 = make_item("_Test Item for Drop Shipping 2", {"is_stock_item": 1, "delivered_by_supplier": 1}) + + so_items = [ + { + "item_code": po_item1.item_code, + "warehouse": "", + "qty": 2, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier' + }, + { + "item_code": po_item2.item_code, + "warehouse": "_Test Warehouse - _TC", + "qty": 2, + "rate": 300, + "conversion_factor": 1.0 + } + ] + + # create so and po + so = make_sales_order(item_list=so_items, do_not_submit=True) + so.submit() + + # create po for only one item + po = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]]) + po.submit() + + self.assertEqual(so.customer, po.customer) + self.assertEqual(po.items[0].sales_order, so.name) + self.assertEqual(po.items[0].item_code, po_item.item_code) + #test po_item length + self.assertEqual(len(po.items), 1) + + # create po for remaining item + po = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[1]]) + po.submit() + + # teardown + so_update_status("Draft", so.name) + po.cancel() + so.load_from_db() + so.cancel() + def test_reserved_qty_for_closing_so(self): bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, fields=["reserved_qty"]) From 89624ad6226cdd09a413589b1b87a73607cf9d6d Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 7 Dec 2020 16:50:03 +0530 Subject: [PATCH 061/295] fix: sider issue fixed --- erpnext/selling/doctype/sales_order/test_sales_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 1d11ad6a7e4..a0af49982b8 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -808,7 +808,7 @@ class TestSalesOrder(unittest.TestCase): self.assertEqual(so.customer, po.customer) self.assertEqual(po.items[0].sales_order, so.name) - self.assertEqual(po.items[0].item_code, po_item.item_code) + self.assertEqual(po.items[0].item_code, po_item1.item_code) #test po_item length self.assertEqual(len(po.items), 1) From 32e77d73cf992551fc6bf77a7c2607deed1aa7a2 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 7 Dec 2020 17:39:37 +0530 Subject: [PATCH 062/295] fix: added supplier to second item in test --- erpnext/selling/doctype/sales_order/test_sales_order.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index a0af49982b8..0ca58121f41 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -791,10 +791,11 @@ class TestSalesOrder(unittest.TestCase): }, { "item_code": po_item2.item_code, - "warehouse": "_Test Warehouse - _TC", + "warehouse": "", "qty": 2, - "rate": 300, - "conversion_factor": 1.0 + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier' } ] From 71b1a0ca7dd3e6cd4cbc91d50e6ffe4b62737ef0 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 7 Dec 2020 19:09:29 +0530 Subject: [PATCH 063/295] fix: cancelling both test po created --- .../doctype/sales_order/test_sales_order.py | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 0ca58121f41..5954602bea6 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -804,22 +804,24 @@ class TestSalesOrder(unittest.TestCase): so.submit() # create po for only one item - po = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]]) - po.submit() + po1 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]]) + po1.submit() - self.assertEqual(so.customer, po.customer) - self.assertEqual(po.items[0].sales_order, so.name) - self.assertEqual(po.items[0].item_code, po_item1.item_code) - #test po_item length - self.assertEqual(len(po.items), 1) + self.assertEqual(so.customer, po1.customer) + self.assertEqual(po1.items[0].sales_order, so.name) + self.assertEqual(po1.items[0].item_code, po_item1.item_code) + #test po item length + self.assertEqual(len(po1.items), 1) # create po for remaining item - po = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[1]]) - po.submit() + po2 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[1]]) + po2.submit() # teardown so_update_status("Draft", so.name) - po.cancel() + + po1.cancel() + po2.cancel() so.load_from_db() so.cancel() From c82b52855cf43774e78d54a7fcd41c62884ac47a Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 8 Dec 2020 11:07:59 +0530 Subject: [PATCH 064/295] fix: template filter for tasks --- .../doctype/project_template/project_template.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/projects/doctype/project_template/project_template.js b/erpnext/projects/doctype/project_template/project_template.js index d7a876dfbd3..acc6849f2bb 100644 --- a/erpnext/projects/doctype/project_template/project_template.js +++ b/erpnext/projects/doctype/project_template/project_template.js @@ -5,4 +5,13 @@ frappe.ui.form.on('Project Template', { // refresh: function(frm) { // } + setup: function (frm) { + me.frm.set_query("task", "tasks", function (doc, cdt, cdn) { + return { + filters: { + "is_template": 1 + } + } + }); + } }); From 91e1136d2c5d368256ac0934dca3b83ef3a71ff6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 8 Dec 2020 22:53:03 +0530 Subject: [PATCH 065/295] feat: Batch wise item pricing --- erpnext/public/js/controllers/transaction.js | 5 +++++ erpnext/stock/doctype/item_price/item_price.py | 5 +++-- erpnext/stock/get_item_details.py | 4 ++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 7f08cd1359f..3dcec892e42 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1103,6 +1103,11 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }, + batch_no: function(doc, cdt, cdn) { + let item = frappe.get_doc(cdt, cdn); + this.apply_pricing_rule(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) { diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index bed5ea9ab66..e82a19b0dc0 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/get_item_details.py b/erpnext/stock/get_item_details.py index 08f7a83b893..a5f8e5f0a05 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -680,6 +680,9 @@ def get_item_price(args, item_code, ignore_party=False): else: conditions += "and (customer is null or customer = '') and (supplier is null or supplier = '')" + if args.get('batch_no'): + conditions += "and batch_no = %(batch_no)s" + if args.get('transaction_date'): conditions += """ and %(transaction_date)s between ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31')""" @@ -709,6 +712,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 From bc0a2859e44feecec1611fab0e4b6f4867ba1a4c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 9 Dec 2020 12:40:09 +0530 Subject: [PATCH 066/295] fix: Rate for items with no batch --- erpnext/public/js/controllers/buying.js | 4 + erpnext/public/js/controllers/transaction.js | 3 +- erpnext/selling/sales_common.js | 4 + .../stock/doctype/item_price/item_price.json | 116 ++++++++++++++---- erpnext/stock/get_item_details.py | 7 +- 5 files changed, 102 insertions(+), 32 deletions(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index e5be4997c1d..a537b8633d7 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -194,6 +194,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) }, diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 3dcec892e42..4a9ecd7784f 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1105,7 +1105,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ batch_no: function(doc, cdt, cdn) { let item = frappe.get_doc(cdt, cdn); - this.apply_pricing_rule(item, true); + this.apply_price_list(item, true); }, toggle_conversion_factor: function(item) { @@ -1412,6 +1412,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 }); diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 7f00fca8f05..ce084646e15 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/doctype/item_price/item_price.json b/erpnext/stock/doctype/item_price/item_price.json index 5f62381f8b3..83177b372ad 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/get_item_details.py b/erpnext/stock/get_item_details.py index a5f8e5f0a05..2d2abd71aa1 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -672,6 +672,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" @@ -680,9 +682,6 @@ def get_item_price(args, item_code, ignore_party=False): else: conditions += "and (customer is null or customer = '') and (supplier is null or supplier = '')" - if args.get('batch_no'): - conditions += "and batch_no = %(batch_no)s" - if args.get('transaction_date'): conditions += """ and %(transaction_date)s between ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31')""" @@ -693,7 +692,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): """ From 811629a9616f411eb40b0d0adb301dc5f0ff8cf3 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Wed, 9 Dec 2020 15:52:05 +0530 Subject: [PATCH 067/295] feat: tasks in project template and tests --- erpnext/projects/doctype/project/project.py | 38 ++++++-- .../projects/doctype/project/test_project.py | 92 +++++++++++++++---- .../project_template/test_project_template.py | 49 +++++----- erpnext/projects/doctype/task/test_task.py | 6 +- 4 files changed, 126 insertions(+), 59 deletions(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 5bbd29c4c42..04a0fb6c4f0 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -55,16 +55,34 @@ class Project(Document): # create tasks from template for task in template.tasks: - frappe.get_doc(dict( - doctype = 'Task', - subject = task.subject, - project = self.name, - status = 'Open', - exp_start_date = add_days(self.expected_start_date, task.start), - exp_end_date = add_days(self.expected_start_date, task.start + task.duration), - description = task.description, - task_weight = task.task_weight - )).insert() + template_task_details = frappe.get_doc("Task", task.task) + project_task = self.create_task_from_template(template_task_details) + + if template_task_details.depends_on: + for child_task in template_task_details.depends_on: + child_task_details = frappe.get_doc("Task",child_task.task) + self.create_task_from_template(child_task_details, project_task) + + def create_task_from_template(self, task_details, project_task=None): + doc = frappe.get_doc(dict( + doctype = 'Task', + subject = task_details.subject, + project = self.name, + status = 'Open', + exp_start_date = add_days(self.expected_start_date, task_details.start), + exp_end_date = add_days(self.expected_start_date, task_details.start + task_details.duration), + description = task_details.description, + task_weight = task_details.task_weight, + type = task_details.type, + issue = task_details.issue, + is_group = task_details.is_group + )) + if task_details.parent_task and project_task: + doc.parent_task = project_task.name + if not task_details.is_group: + doc.depends_on = task_details.depends_on + doc.insert() + return doc def is_row_updated(self, row, existing_task_data, fields): if self.get("__islocal") or not existing_task_data: return True diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index 0c4f6f1bdfe..ce95b056141 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -7,33 +7,87 @@ import frappe, unittest test_records = frappe.get_test_records('Project') test_ignore = ["Sales Order"] -from erpnext.projects.doctype.project_template.test_project_template import get_project_template, make_project_template +from erpnext.projects.doctype.project_template.test_project_template import make_project_template from erpnext.projects.doctype.project.project import set_project_status - -from frappe.utils import getdate +from erpnext.projects.doctype.task.test_task import create_task +from frappe.utils import getdate, nowdate, add_days class TestProject(unittest.TestCase): - def test_project_with_template(self): - frappe.db.sql('delete from tabTask where project = "Test Project with Template"') - frappe.delete_doc('Project', 'Test Project with Template') + def test_project_with_template_having_no_parent_and_depend_tasks(self): + """ + Test Action: Basic Test of a Project created from template. The template has a single task. + """ + frappe.db.sql('delete from tabTask where project = "Test Project with Templ - no parent and dependend tasks"') + frappe.delete_doc('Project', 'Test Project with Templ - no parent and dependend tasks') - project = get_project('Test Project with Template') + if not frappe.db.exists("Task", "Test Temp Task with no parent and dependency"): + task1 = create_task(subject="Test Temp Task with no parent and dependency", is_template=1, begin=0, duration=3) + template = make_project_template("Test Project Template - no parent and dependend tasks", [task1]) + project = get_project("Test Project with Templ - no parent and dependend tasks", template) tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') - task1 = tasks[0] - self.assertEqual(task1.subject, 'Task 1') - self.assertEqual(task1.description, 'Task 1 description') - self.assertEqual(getdate(task1.exp_start_date), getdate('2019-01-01')) - self.assertEqual(getdate(task1.exp_end_date), getdate('2019-01-04')) + self.assertEqual(task[0].subject, 'Test Temp Task with no parent and dependency') + self.assertEqual(getdate(task[0].exp_end_date), add_days(nowdate() + 0 + 3)) + self.assertEqual(len(tasks), 1) - self.assertEqual(len(tasks), 4) - task4 = tasks[3] - self.assertEqual(task4.subject, 'Task 4') - self.assertEqual(getdate(task4.exp_end_date), getdate('2019-01-06')) + def test_project_template_having_parent_child_tasks(self): -def get_project(name): - template = get_project_template() + frappe.db.sql('delete from tabTask where project = "Test Project with Templ - tasks with parent-child"') + frappe.delete_doc('Project', 'Test Project with Templ - tasks with parent-child') + + if not frappe.db.exists("Task", "Test Temp Task parent"): + task1 = create_task(subject="Test Temp Task parent", is_group=1, is_template=1, begin=1, duration=1) + + if not frappe.db.exists("Task", "Test Temp Task child 1"): + task2 = create_task(subject="Test Temp Task child 1", parent_task=task1.name, is_template=1, begin=1, duration=3) + + if not frappe.db.exists("Task", "Test Temp Task child 2"): + task3 = create_task(subject="Test Temp Task child 2", parent_task=task1.name, is_template=1, begin=2, duration=3) + + template = make_project_template("Test Project Template - tasks with parent-child", [task1, task2, task3]) + project = get_project("Test Project with Templ - tasks with parent-child", template) + tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') + + self.assertEqual(task[0].subject, 'Test Temp Task parent') + self.assertEqual(getdate(task[0].exp_end_date), add_days(nowdate()+ 1 + 1)) + print(task[0].depends_on) + + self.assertEqual(task[1].subject, 'Test Temp Task child 1') + self.assertEqual(getdate(task[1].exp_end_date), add_days(nowdate()+ 1 + 3)) + self.assertEqual(task[1].parent_task, task[0].name) + + self.assertEqual(task[2].subject, 'Test Temp Task child 2') + self.assertEqual(getdate(task[2].exp_end_date), add_days(nowdate()+ 2 + 3)) + self.assertEqual(task[2].parent_task, task[0].name) + + self.assertEqual(len(tasks), 3) + + def test_project_template_having_dependent_tasks(self): + + frappe.db.sql('delete from tabTask where project = "Test Project with Templ - dependent tasks"') + frappe.delete_doc('Project', 'Test Project with Templ - dependent tasks') + + if not frappe.db.exists("Task", "Test Temp Task for dependency"): + task1 = create_task(subject="Test Temp Task for dependency", is_template=1, begin=3, duration=1) + + if not frappe.db.exists("Task", "Test Temp Task with dependency"): + task2 = create_task(subject="Test Temp Task with dependency", depends_on=task1.name, is_template=1, begin=2, duration=2) + + template = make_project_template("Test Project Template - tasks with parent-child", [task1, task2]) + project = get_project("Test Project with Templ - tasks with parent-child", template) + tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') + + self.assertEqual(task[0].subject, 'Test Temp Task for dependency') + self.assertEqual(getdate(task[0].exp_end_date), add_days(nowdate()+ 3 + 1)) + + self.assertEqual(task[1].subject, 'Test Temp Task with dependency') + self.assertEqual(getdate(task[1].exp_end_date), add_days(nowdate()+ 2 + 2)) + self.assertEqual(task[1].depends_on, task[0].name) + + self.assertEqual(len(tasks), 2) + +def get_project(name, template): project = frappe.get_doc(dict( doctype = 'Project', @@ -49,8 +103,6 @@ def make_project(args): args = frappe._dict(args) if args.project_template_name: template = make_project_template(args.project_template_name) - else: - template = get_project_template() project = frappe.get_doc(dict( doctype = 'Project', diff --git a/erpnext/projects/doctype/project_template/test_project_template.py b/erpnext/projects/doctype/project_template/test_project_template.py index 2c5831a5dc9..dd98d02c021 100644 --- a/erpnext/projects/doctype/project_template/test_project_template.py +++ b/erpnext/projects/doctype/project_template/test_project_template.py @@ -5,44 +5,37 @@ from __future__ import unicode_literals import frappe import unittest +from erpnext.projects.doctype.task.test_task import create_task class TestProjectTemplate(unittest.TestCase): pass -def get_project_template(): - if not frappe.db.exists('Project Template', 'Test Project Template'): - frappe.get_doc(dict( - doctype = 'Project Template', - name = 'Test Project Template', - tasks = [ - dict(subject='Task 1', description='Task 1 description', - start=0, duration=3), - dict(subject='Task 2', description='Task 2 description', - start=0, duration=2), - dict(subject='Task 3', description='Task 3 description', - start=2, duration=4), - dict(subject='Task 4', description='Task 4 description', - start=3, duration=2), - ] - )).insert() - - return frappe.get_doc('Project Template', 'Test Project Template') - -def make_project_template(project_template_name, project_tasks=[]): +def get_project_template(project_template_name="Test Project Template", project_tasks=[]): if not frappe.db.exists('Project Template', project_template_name): frappe.get_doc(dict( doctype = 'Project Template', name = project_template_name, tasks = project_tasks or [ - dict(subject='Task 1', description='Task 1 description', - start=0, duration=3), - dict(subject='Task 2', description='Task 2 description', - start=0, duration=2), - dict(subject='Task 3', description='Task 3 description', - start=2, duration=4), - dict(subject='Task 4', description='Task 4 description', - start=3, duration=2), + create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3), + create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2), + create_task(subject="_Test Template Task 3", is_template=1, begin=2, duration=4), + create_task(subject="_Test Template Task 4", is_template=1, begin=3, duration=2), ] )).insert() + return frappe.get_doc('Project Template', project_template_name) + +def make_project_template(project_template_name, project_tasks=[]): + if not frappe.db.exists('Project Template', project_template_name): + doc = frappe.get_doc(dict( + doctype = 'Project Template', + name = project_template_name, + tasks = project_tasks or [ + create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3), + create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2), + ] + )) + print("doc",doc.tasks) + doc.insert() + return frappe.get_doc('Project Template', project_template_name) \ No newline at end of file diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index 47a28fd1114..181a2dc3162 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -97,7 +97,7 @@ class TestTask(unittest.TestCase): self.assertEqual(frappe.db.get_value("Task", task.name, "status"), "Overdue") -def create_task(subject, start=None, end=None, depends_on=None, project=None, save=True): +def create_task(subject, start=None, end=None, depends_on=None, project=None, parent_task=None, is_group=0, is_template=0, begin=0, duration=0, save=True): if not frappe.db.exists("Task", subject): task = frappe.new_doc('Task') task.status = "Open" @@ -105,6 +105,10 @@ def create_task(subject, start=None, end=None, depends_on=None, project=None, sa task.exp_start_date = start or nowdate() task.exp_end_date = end or nowdate() task.project = project or "_Test Project" + task.is_template = is_template, + task.start = begin + task.duration = duration, + task.is_group = is_group if save: task.save() else: From c41fb7556b37d80f41db48643bd1e3bffec7ee03 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 9 Dec 2020 15:55:39 +0530 Subject: [PATCH 068/295] fix: Add test case for batch pricing --- erpnext/stock/doctype/batch/test_batch.py | 70 ++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index c2a3d3c151f..b0d30a765da 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -9,6 +9,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.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory +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): @@ -187,7 +189,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''' @@ -257,6 +259,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'): + doc = 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) From f936e8a334bb8abab78d9938d6808d9e8688a6ee Mon Sep 17 00:00:00 2001 From: pateljannat Date: Wed, 9 Dec 2020 17:09:22 +0530 Subject: [PATCH 069/295] fix: sider issues fixed --- .../projects/doctype/project/test_project.py | 33 +++++++++---------- .../project_template/project_template.js | 4 +-- .../project_template/test_project_template.py | 6 ++-- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index ce95b056141..52f877b8b75 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -27,8 +27,8 @@ class TestProject(unittest.TestCase): project = get_project("Test Project with Templ - no parent and dependend tasks", template) tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') - self.assertEqual(task[0].subject, 'Test Temp Task with no parent and dependency') - self.assertEqual(getdate(task[0].exp_end_date), add_days(nowdate() + 0 + 3)) + self.assertEqual(tasks[0].subject, 'Test Temp Task with no parent and dependency') + self.assertEqual(getdate(tasks[0].exp_end_date), add_days(nowdate() + 0 + 3)) self.assertEqual(len(tasks), 1) def test_project_template_having_parent_child_tasks(self): @@ -48,18 +48,17 @@ class TestProject(unittest.TestCase): template = make_project_template("Test Project Template - tasks with parent-child", [task1, task2, task3]) project = get_project("Test Project with Templ - tasks with parent-child", template) tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') + print(tasks, type(tasks), len(tasks)) + self.assertEqual(tasks[0].subject, 'Test Temp Task parent') + self.assertEqual(getdate(tasks[0].exp_end_date), add_days(nowdate()+ 1 + 1)) - self.assertEqual(task[0].subject, 'Test Temp Task parent') - self.assertEqual(getdate(task[0].exp_end_date), add_days(nowdate()+ 1 + 1)) - print(task[0].depends_on) + self.assertEqual(tasks[1].subject, 'Test Temp Task child 1') + self.assertEqual(getdate(tasks[1].exp_end_date), add_days(nowdate()+ 1 + 3)) + self.assertEqual(tasks[1].parent_task, tasks[0].name) - self.assertEqual(task[1].subject, 'Test Temp Task child 1') - self.assertEqual(getdate(task[1].exp_end_date), add_days(nowdate()+ 1 + 3)) - self.assertEqual(task[1].parent_task, task[0].name) - - self.assertEqual(task[2].subject, 'Test Temp Task child 2') - self.assertEqual(getdate(task[2].exp_end_date), add_days(nowdate()+ 2 + 3)) - self.assertEqual(task[2].parent_task, task[0].name) + self.assertEqual(tasks[2].subject, 'Test Temp Task child 2') + self.assertEqual(getdate(tasks[2].exp_end_date), add_days(nowdate()+ 2 + 3)) + self.assertEqual(tasks[2].parent_task, tasks[0].name) self.assertEqual(len(tasks), 3) @@ -78,12 +77,12 @@ class TestProject(unittest.TestCase): project = get_project("Test Project with Templ - tasks with parent-child", template) tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') - self.assertEqual(task[0].subject, 'Test Temp Task for dependency') - self.assertEqual(getdate(task[0].exp_end_date), add_days(nowdate()+ 3 + 1)) + self.assertEqual(tasks[0].subject, 'Test Temp Task for dependency') + self.assertEqual(getdate(tasks[0].exp_end_date), add_days(nowdate()+ 3 + 1)) - self.assertEqual(task[1].subject, 'Test Temp Task with dependency') - self.assertEqual(getdate(task[1].exp_end_date), add_days(nowdate()+ 2 + 2)) - self.assertEqual(task[1].depends_on, task[0].name) + self.assertEqual(tasks[1].subject, 'Test Temp Task with dependency') + self.assertEqual(getdate(tasks[1].exp_end_date), add_days(nowdate()+ 2 + 2)) + self.assertEqual(tasks[1].depends_on, tasks[0].name) self.assertEqual(len(tasks), 2) diff --git a/erpnext/projects/doctype/project_template/project_template.js b/erpnext/projects/doctype/project_template/project_template.js index acc6849f2bb..7668df3e139 100644 --- a/erpnext/projects/doctype/project_template/project_template.js +++ b/erpnext/projects/doctype/project_template/project_template.js @@ -6,12 +6,12 @@ frappe.ui.form.on('Project Template', { // } setup: function (frm) { - me.frm.set_query("task", "tasks", function (doc, cdt, cdn) { + frm.set_query("task", "tasks", function () { return { filters: { "is_template": 1 } - } + }; }); } }); diff --git a/erpnext/projects/doctype/project_template/test_project_template.py b/erpnext/projects/doctype/project_template/test_project_template.py index dd98d02c021..379365f9998 100644 --- a/erpnext/projects/doctype/project_template/test_project_template.py +++ b/erpnext/projects/doctype/project_template/test_project_template.py @@ -27,15 +27,13 @@ def get_project_template(project_template_name="Test Project Template", project_ def make_project_template(project_template_name, project_tasks=[]): if not frappe.db.exists('Project Template', project_template_name): - doc = frappe.get_doc(dict( + frappe.get_doc(dict( doctype = 'Project Template', name = project_template_name, tasks = project_tasks or [ create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3), create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2), ] - )) - print("doc",doc.tasks) - doc.insert() + )).insert() return frappe.get_doc('Project Template', project_template_name) \ No newline at end of file From 249595a2d8c4e314e079fe7d775124028982aaf6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 9 Dec 2020 18:13:23 +0530 Subject: [PATCH 070/295] fix: Sider Issues --- erpnext/stock/doctype/batch/test_batch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index b0d30a765da..81c443579c6 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -261,7 +261,7 @@ class TestBatch(unittest.TestCase): def test_batch_wise_item_price(self): if not frappe.db.get_value('Item', '_Test Batch Price Item'): - doc = frappe.get_doc({ + frappe.get_doc({ 'doctype': 'Item', 'is_stock_item': 1, 'item_code': '_Test Batch Price Item', From 52d37edf98b6e5a9972d03ffd54e1547ef204651 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 9 Dec 2020 17:57:29 +0100 Subject: [PATCH 071/295] feat: separate equity tree in CoA SKR04 --- ..._kontenplan_SKR04_with_account_number.json | 229 +++++++++++------- 1 file changed, 138 insertions(+), 91 deletions(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json index 3fc109bfd67..849df18c6f9 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json @@ -910,98 +910,8 @@ }, "is_group": 1 }, - "Passiva": { + "Passiva - Verbindlichkeiten": { "root_type": "Liability", - "A - Eigenkapital": { - "account_type": "Equity", - "is_group": 1, - "I - Gezeichnetes Kapital": { - "account_type": "Equity", - "is_group": 1, - "Gezeichnetes Kapital": { - "account_type": "Equity", - "account_number": "2900" - }, - "Ausstehende Einlagen auf das gezeichnete Kapital": { - "account_number": "2910", - "is_group": 1 - } - }, - "II - Kapitalr\u00fccklage": { - "account_type": "Equity", - "is_group": 1, - "Kapitalr\u00fccklage": { - "account_number": "2920" - } - }, - "III - Gewinnr\u00fccklagen": { - "account_type": "Equity", - "1 - gesetzliche R\u00fccklage": { - "account_type": "Equity", - "is_group": 1, - "Gesetzliche R\u00fccklage": { - "account_number": "2930" - } - }, - "2 - R\u00fccklage f. Anteile an einem herrschenden oder mehrheitlich beteiligten Unternehmen": { - "account_type": "Equity", - "is_group": 1 - }, - "3 - satzungsm\u00e4\u00dfige R\u00fccklagen": { - "account_type": "Equity", - "is_group": 1, - "Satzungsm\u00e4\u00dfige R\u00fccklagen": { - "account_number": "2950" - } - }, - "4 - andere Gewinnr\u00fccklagen": { - "account_type": "Equity", - "is_group": 1, - "Gewinnr\u00fccklagen aus den \u00dcbergangsvorschriften BilMoG": { - "is_group": 1, - "Gewinnr\u00fccklagen (BilMoG)": { - "account_number": "2963" - }, - "Gewinnr\u00fccklagen aus Zuschreibung Sachanlageverm\u00f6gen (BilMoG)": { - "account_number": "2964" - }, - "Gewinnr\u00fccklagen aus Zuschreibung Finanzanlageverm\u00f6gen (BilMoG)": { - "account_number": "2965" - }, - "Gewinnr\u00fccklagen aus Aufl\u00f6sung der Sonderposten mit R\u00fccklageanteil (BilMoG)": { - "account_number": "2966" - } - }, - "Latente Steuern (Gewinnr\u00fccklage Haben) aus erfolgsneutralen Verrechnungen": { - "account_number": "2967" - }, - "Latente Steuern (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": { - "account_number": "2968" - }, - "Rechnungsabgrenzungsposten (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": { - "account_number": "2969" - } - }, - "is_group": 1 - }, - "IV - Gewinnvortrag/Verlustvortrag": { - "account_type": "Equity", - "is_group": 1, - "Gewinnvortrag vor Verwendung": { - "account_number": "2970" - }, - "Verlustvortrag vor Verwendung": { - "account_number": "2978" - } - }, - "V - Jahres\u00fcberschu\u00df/Jahresfehlbetrag": { - "account_type": "Equity", - "is_group": 1 - }, - "Einlagen stiller Gesellschafter": { - "account_number": "9295" - } - }, "B - R\u00fcckstellungen": { "is_group": 1, "1 - R\u00fcckstellungen f. Pensionen und \u00e4hnliche Verplicht.": { @@ -1618,6 +1528,143 @@ }, "is_group": 1 }, + "Passiva - Eigenkapital": { + "root_type": "Equity", + "A - Eigenkapital": { + "account_type": "Equity", + "is_group": 1, + "I - Gezeichnetes Kapital": { + "account_type": "Equity", + "is_group": 1, + "Gezeichnetes Kapital": { + "account_number": "2900", + "account_type": "Equity" + }, + "Gesch\u00e4ftsguthaben der verbleibenden Mitglieder": { + "account_number": "2901" + }, + "Gesch\u00e4ftsguthaben der ausscheidenden Mitglieder": { + "account_number": "2902" + }, + "Gesch\u00e4ftsguthaben aus gek\u00fcndigten Gesch\u00e4ftsanteilen": { + "account_number": "2903" + }, + "R\u00fcckst\u00e4ndige f\u00e4llige Einzahlungen auf Gesch\u00e4ftsanteile, vermerkt": { + "account_number": "2906" + }, + "Gegenkonto R\u00fcckst\u00e4ndige f\u00e4llige Einzahlungen auf Gesch\u00e4ftsanteile, vermerkt": { + "account_number": "2907" + }, + "Kapitalerh\u00f6hung aus Gesellschaftsmitteln": { + "account_number": "2908" + }, + "Ausstehende Einlagen auf das gezeichnete Kapital, nicht eingefordert": { + "account_number": "2910" + } + }, + "II - Kapitalr\u00fccklage": { + "account_type": "Equity", + "is_group": 1, + "Kapitalr\u00fccklage": { + "account_number": "2920" + }, + "Kapitalr\u00fccklage durch Ausgabe von Anteilen \u00fcber Nennbetrag": { + "account_number": "2925" + }, + "Kapitalr\u00fccklage durch Ausgabe von Schuldverschreibungen": { + "account_number": "2926" + }, + "Kapitalr\u00fccklage durch Zuzahlungen gegen Gew\u00e4hrung eines Vorzugs": { + "account_number": "2927" + }, + "Kapitalr\u00fccklage durch Zuzahlungen in das Eigenkapital": { + "account_number": "2928" + }, + "Nachschusskapital (Gegenkonto 1299)": { + "account_number": "2929" + } + }, + "III - Gewinnr\u00fccklagen": { + "account_type": "Equity", + "1 - gesetzliche R\u00fccklage": { + "account_type": "Equity", + "is_group": 1, + "Gesetzliche R\u00fccklage": { + "account_number": "2930" + } + }, + "2 - R\u00fccklage f. Anteile an einem herrschenden oder mehrheitlich beteiligten Unternehmen": { + "account_type": "Equity", + "is_group": 1, + "R\u00fccklage f. Anteile an einem herrschenden oder mehrheitlich beteiligten Unternehmen": { + "account_number": "2935" + } + }, + "3 - satzungsm\u00e4\u00dfige R\u00fccklagen": { + "account_type": "Equity", + "is_group": 1, + "Satzungsm\u00e4\u00dfige R\u00fccklagen": { + "account_number": "2950" + } + }, + "4 - andere Gewinnr\u00fccklagen": { + "account_type": "Equity", + "is_group": 1, + "Andere Gewinnr\u00fccklagen": { + "account_number": "2960" + }, + "Andere Gewinnr\u00fccklagen aus dem Erwerb eigener Anteile": { + "account_number": "2961" + }, + "Eigenkapitalanteil von Wertaufholungen": { + "account_number": "2962" + }, + "Gewinnr\u00fccklagen aus den \u00dcbergangsvorschriften BilMoG": { + "is_group": 1, + "Gewinnr\u00fccklagen (BilMoG)": { + "account_number": "2963" + }, + "Gewinnr\u00fccklagen aus Zuschreibung Sachanlageverm\u00f6gen (BilMoG)": { + "account_number": "2964" + }, + "Gewinnr\u00fccklagen aus Zuschreibung Finanzanlageverm\u00f6gen (BilMoG)": { + "account_number": "2965" + }, + "Gewinnr\u00fccklagen aus Aufl\u00f6sung der Sonderposten mit R\u00fccklageanteil (BilMoG)": { + "account_number": "2966" + } + }, + "Latente Steuern (Gewinnr\u00fccklage Haben) aus erfolgsneutralen Verrechnungen": { + "account_number": "2967" + }, + "Latente Steuern (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": { + "account_number": "2968" + }, + "Rechnungsabgrenzungsposten (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": { + "account_number": "2969" + } + }, + "is_group": 1 + }, + "IV - Gewinnvortrag/Verlustvortrag": { + "account_type": "Equity", + "is_group": 1, + "Gewinnvortrag vor Verwendung": { + "account_number": "2970" + }, + "Verlustvortrag vor Verwendung": { + "account_number": "2978" + } + }, + "V - Jahres\u00fcberschu\u00df/Jahresfehlbetrag": { + "account_type": "Equity", + "is_group": 1 + }, + "Einlagen stiller Gesellschafter": { + "account_number": "9295" + } + } + }, "1 - Umsatzerl\u00f6se": { "root_type": "Income", "is_group": 1, From e15ef1e19f124436d8705d633a02388a1107056f Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 10 Dec 2020 20:55:25 +0530 Subject: [PATCH 072/295] fix: corrected tests --- erpnext/projects/doctype/project/project.py | 35 +++++++----- .../projects/doctype/project/test_project.py | 57 ++++++++++++------- .../project_template/test_project_template.py | 19 ++++--- erpnext/projects/doctype/task/test_task.py | 8 +-- 4 files changed, 71 insertions(+), 48 deletions(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 04a0fb6c4f0..dfb54a2f77f 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -54,17 +54,15 @@ class Project(Document): self.project_type = template.project_type # create tasks from template + project_tasks = [] for task in template.tasks: template_task_details = frappe.get_doc("Task", task.task) - project_task = self.create_task_from_template(template_task_details) + project_tasks.append(self.create_task_from_template(template_task_details)) - if template_task_details.depends_on: - for child_task in template_task_details.depends_on: - child_task_details = frappe.get_doc("Task",child_task.task) - self.create_task_from_template(child_task_details, project_task) + #self.dependency_mapping(template.tasks, project_tasks) - def create_task_from_template(self, task_details, project_task=None): - doc = frappe.get_doc(dict( + def create_task_from_template(self, task_details): + return frappe.get_doc(dict( doctype = 'Task', subject = task_details.subject, project = self.name, @@ -75,14 +73,21 @@ class Project(Document): task_weight = task_details.task_weight, type = task_details.type, issue = task_details.issue, - is_group = task_details.is_group - )) - if task_details.parent_task and project_task: - doc.parent_task = project_task.name - if not task_details.is_group: - doc.depends_on = task_details.depends_on - doc.insert() - return doc + is_group = task_details.is_group, + start = task_details.start, + duration = task_details.duration + )).insert() + + """ def dependency_mapping(self, template_tasks, project_tasks): + for tmp_task in template_tasks: + for prj_task in project_tasks: + if tmp_task.subject == prj_task.subject: + if tmp_task.depends_on and not prj_task.depends_on: + for child_task in tmp_task.depends_on: + child_task_detai + prj_task.depends_on = tmp_task.depends_on + """ + def is_row_updated(self, row, existing_task_data, fields): if self.get("__islocal") or not existing_task_data: return True diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index 52f877b8b75..f9bb1b3ac4d 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -20,15 +20,16 @@ class TestProject(unittest.TestCase): frappe.db.sql('delete from tabTask where project = "Test Project with Templ - no parent and dependend tasks"') frappe.delete_doc('Project', 'Test Project with Templ - no parent and dependend tasks') - if not frappe.db.exists("Task", "Test Temp Task with no parent and dependency"): - task1 = create_task(subject="Test Temp Task with no parent and dependency", is_template=1, begin=0, duration=3) + task1 = task_exists("Test Temp Task with no parent and dependency") + if not task1: + task1 = create_task(subject="Test Temp Task with no parent and dependency", is_template=1, begin=5, duration=3) template = make_project_template("Test Project Template - no parent and dependend tasks", [task1]) project = get_project("Test Project with Templ - no parent and dependend tasks", template) tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') self.assertEqual(tasks[0].subject, 'Test Temp Task with no parent and dependency') - self.assertEqual(getdate(tasks[0].exp_end_date), add_days(nowdate() + 0 + 3)) + self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, tasks[0])) self.assertEqual(len(tasks), 1) def test_project_template_having_parent_child_tasks(self): @@ -36,28 +37,31 @@ class TestProject(unittest.TestCase): frappe.db.sql('delete from tabTask where project = "Test Project with Templ - tasks with parent-child"') frappe.delete_doc('Project', 'Test Project with Templ - tasks with parent-child') - if not frappe.db.exists("Task", "Test Temp Task parent"): + task1 = task_exists("Test Temp Task parent") + if not task1: task1 = create_task(subject="Test Temp Task parent", is_group=1, is_template=1, begin=1, duration=1) - if not frappe.db.exists("Task", "Test Temp Task child 1"): + task2 = task_exists("Test Temp Task child 1") + if not task2: task2 = create_task(subject="Test Temp Task child 1", parent_task=task1.name, is_template=1, begin=1, duration=3) - if not frappe.db.exists("Task", "Test Temp Task child 2"): + task3 = task_exists("Test Temp Task child 2") + if not task3: task3 = create_task(subject="Test Temp Task child 2", parent_task=task1.name, is_template=1, begin=2, duration=3) - template = make_project_template("Test Project Template - tasks with parent-child", [task1, task2, task3]) + template = make_project_template("Test Project Template - tasks with parent-child", [task1]) project = get_project("Test Project with Templ - tasks with parent-child", template) tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') - print(tasks, type(tasks), len(tasks)) + print(tasks[0].duration) self.assertEqual(tasks[0].subject, 'Test Temp Task parent') - self.assertEqual(getdate(tasks[0].exp_end_date), add_days(nowdate()+ 1 + 1)) + self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, tasks[0])) self.assertEqual(tasks[1].subject, 'Test Temp Task child 1') - self.assertEqual(getdate(tasks[1].exp_end_date), add_days(nowdate()+ 1 + 3)) + self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, tasks[1])) self.assertEqual(tasks[1].parent_task, tasks[0].name) self.assertEqual(tasks[2].subject, 'Test Temp Task child 2') - self.assertEqual(getdate(tasks[2].exp_end_date), add_days(nowdate()+ 2 + 3)) + self.assertEqual(getdate(tasks[2].exp_end_date), calculate_end_date(project, tasks[2])) self.assertEqual(tasks[2].parent_task, tasks[0].name) self.assertEqual(len(tasks), 3) @@ -67,22 +71,24 @@ class TestProject(unittest.TestCase): frappe.db.sql('delete from tabTask where project = "Test Project with Templ - dependent tasks"') frappe.delete_doc('Project', 'Test Project with Templ - dependent tasks') - if not frappe.db.exists("Task", "Test Temp Task for dependency"): + task1 = task_exists("Test Temp Task for dependency") + if not task1: task1 = create_task(subject="Test Temp Task for dependency", is_template=1, begin=3, duration=1) - if not frappe.db.exists("Task", "Test Temp Task with dependency"): + task2 = task_exists("Test Temp Task with dependency") + if not task2: task2 = create_task(subject="Test Temp Task with dependency", depends_on=task1.name, is_template=1, begin=2, duration=2) - template = make_project_template("Test Project Template - tasks with parent-child", [task1, task2]) + template = make_project_template("Test Project with Templ - dependent tasks", [task2]) project = get_project("Test Project with Templ - tasks with parent-child", template) tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') - self.assertEqual(tasks[0].subject, 'Test Temp Task for dependency') - self.assertEqual(getdate(tasks[0].exp_end_date), add_days(nowdate()+ 3 + 1)) + self.assertEqual(tasks[0].subject, 'Test Temp Task with dependency') + self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, tasks[0])) + self.assertEqual(tasks[0].depends_on, tasks[1].name) - self.assertEqual(tasks[1].subject, 'Test Temp Task with dependency') - self.assertEqual(getdate(tasks[1].exp_end_date), add_days(nowdate()+ 2 + 2)) - self.assertEqual(tasks[1].depends_on, tasks[0].name) + self.assertEqual(tasks[1].subject, 'Test Temp Task for dependency') + self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, tasks[1]) ) self.assertEqual(len(tasks), 2) @@ -93,7 +99,7 @@ def get_project(name, template): project_name = name, status = 'Open', project_template = template.name, - expected_start_date = '2019-01-01' + expected_start_date = nowdate() )).insert() return project @@ -114,4 +120,13 @@ def make_project(args): if not frappe.db.exists("Project", args.project_name): project.insert() - return project \ No newline at end of file + return project + +def task_exists(subject): + result = frappe.db.get_list("Task", filters={"subject": subject},fields=["name"]) + if not len(result): + return False + return frappe.get_doc("Task", result[0].name) + +def calculate_end_date(project, task): + return getdate(add_days(project.expected_start_date, task.start + task.duration)) \ No newline at end of file diff --git a/erpnext/projects/doctype/project_template/test_project_template.py b/erpnext/projects/doctype/project_template/test_project_template.py index 379365f9998..6c6b78368ed 100644 --- a/erpnext/projects/doctype/project_template/test_project_template.py +++ b/erpnext/projects/doctype/project_template/test_project_template.py @@ -17,9 +17,7 @@ def get_project_template(project_template_name="Test Project Template", project_ name = project_template_name, tasks = project_tasks or [ create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3), - create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2), - create_task(subject="_Test Template Task 3", is_template=1, begin=2, duration=4), - create_task(subject="_Test Template Task 4", is_template=1, begin=3, duration=2), + create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2) ] )).insert() @@ -27,13 +25,18 @@ def get_project_template(project_template_name="Test Project Template", project_ def make_project_template(project_template_name, project_tasks=[]): if not frappe.db.exists('Project Template', project_template_name): - frappe.get_doc(dict( - doctype = 'Project Template', - name = project_template_name, - tasks = project_tasks or [ + project_tasks = project_tasks or [ create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3), create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2), ] - )).insert() + doc = frappe.get_doc(dict( + doctype = 'Project Template', + name = project_template_name + )) + for task in project_tasks: + doc.append("tasks",{ + "task": task.name + }) + doc.insert() return frappe.get_doc('Project Template', project_template_name) \ No newline at end of file diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index 181a2dc3162..d43d132e80e 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -104,11 +104,12 @@ 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 "_Test Project" - task.is_template = is_template, + task.project = project + task.is_template = is_template task.start = begin - task.duration = duration, + task.duration = duration task.is_group = is_group + task.parent_task = parent_task if save: task.save() else: @@ -120,5 +121,4 @@ def create_task(subject, start=None, end=None, depends_on=None, project=None, pa }) if save: task.save() - return task From fa72671929581c318cd0b828b07477987199a003 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Fri, 11 Dec 2020 11:16:54 +0530 Subject: [PATCH 073/295] fix: partial order for drop ship --- .../doctype/sales_order/sales_order.py | 77 +++++++++---------- 1 file changed, 35 insertions(+), 42 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 3e1c82f9616..2379a304bbc 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -830,52 +830,45 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=[], tar frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order.")) for supplier in suppliers: - po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")}) - if len(po) == 0 or any( item.get("delivered_by_supplier") == 1 for item in selected_items): - doc = get_mapped_doc("Sales Order", source_name, { - "Sales Order": { - "doctype": "Purchase Order", - "field_no_map": [ - "address_display", - "contact_display", - "contact_mobile", - "contact_email", - "contact_person", - "taxes_and_charges", - "shipping_address" - ], - "validation": { - "docstatus": ["=", 1] - } - }, - "Sales Order Item": { - "doctype": "Purchase Order Item", - "field_map": [ - ["name", "sales_order_item"], - ["parent", "sales_order"], - ["stock_uom", "stock_uom"], - ["uom", "uom"], - ["conversion_factor", "conversion_factor"], - ["delivery_date", "schedule_date"] - ], - "field_no_map": [ - "rate", - "price_list_rate", - "item_tax_template" - ], - "postprocess": update_item, - "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map + doc = get_mapped_doc("Sales Order", source_name, { + "Sales Order": { + "doctype": "Purchase Order", + "field_no_map": [ + "address_display", + "contact_display", + "contact_mobile", + "contact_email", + "contact_person", + "taxes_and_charges", + "shipping_address" + ], + "validation": { + "docstatus": ["=", 1] } - }, target_doc, set_missing_values) + }, + "Sales Order Item": { + "doctype": "Purchase Order Item", + "field_map": [ + ["name", "sales_order_item"], + ["parent", "sales_order"], + ["stock_uom", "stock_uom"], + ["uom", "uom"], + ["conversion_factor", "conversion_factor"], + ["delivery_date", "schedule_date"] + ], + "field_no_map": [ + "rate", + "price_list_rate", + "item_tax_template" + ], + "postprocess": update_item, + "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map + } + }, target_doc, set_missing_values) - doc.insert() - else: - suppliers =[] - if suppliers: + doc.insert() frappe.db.commit() return doc - else: - frappe.msgprint(_("Purchase Order already created for all Sales Order items")) @frappe.whitelist() def make_purchase_order(source_name, selected_items=[], target_doc=None): From 06961a261e8560c591e2828e1733a90720fc4872 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Fri, 11 Dec 2020 11:46:43 +0530 Subject: [PATCH 074/295] fix: conflicts --- erpnext/selling/doctype/sales_order/sales_order.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 2379a304bbc..5d341b746a6 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -840,7 +840,8 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=[], tar "contact_email", "contact_person", "taxes_and_charges", - "shipping_address" + "shipping_address", + "terms" ], "validation": { "docstatus": ["=", 1] @@ -859,7 +860,10 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=[], tar "field_no_map": [ "rate", "price_list_rate", - "item_tax_template" + "item_tax_template", + "discount_percentage", + "discount_amount", + "pricing_rules" ], "postprocess": update_item, "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map From f9751f1f95e9f69701da999bcb342c6b620bcf95 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 14 Dec 2020 16:20:02 +0530 Subject: [PATCH 075/295] feat: project template having dependent tasks --- erpnext/projects/doctype/project/project.py | 33 +- .../project_template/project_template.py | 23 +- erpnext/projects/doctype/task/task.py | 451 +++++++++--------- 3 files changed, 282 insertions(+), 225 deletions(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index dfb54a2f77f..2d3339773a1 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -55,11 +55,13 @@ class Project(Document): # create tasks from template project_tasks = [] + tmp_task_details = [] for task in template.tasks: template_task_details = frappe.get_doc("Task", task.task) + tmp_task_details.append(template_task_details) project_tasks.append(self.create_task_from_template(template_task_details)) - #self.dependency_mapping(template.tasks, project_tasks) + self.dependency_mapping(tmp_task_details, project_tasks) def create_task_from_template(self, task_details): return frappe.get_doc(dict( @@ -78,16 +80,33 @@ class Project(Document): duration = task_details.duration )).insert() - """ def dependency_mapping(self, template_tasks, project_tasks): + def dependency_mapping(self, template_tasks, project_tasks): for tmp_task in template_tasks: for prj_task in project_tasks: if tmp_task.subject == prj_task.subject: - if tmp_task.depends_on and not prj_task.depends_on: - for child_task in tmp_task.depends_on: - child_task_detai - prj_task.depends_on = tmp_task.depends_on - """ + self.check_depends_on_value(tmp_task, prj_task, project_tasks) + self.check_for_parent_tasks(tmp_task, prj_task, project_tasks) + def check_depends_on_value(self, tmp_task, prj_task, project_tasks): + if tmp_task.depends_on and not prj_task.depends_on: + for child_task in tmp_task.depends_on: + child_task_subject = frappe.db.get_value("Task", child_task.task, "subject") + corresponding_prj_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks)) + if len(corresponding_prj_task): + prj_task.append("depends_on",{ + "task": corresponding_prj_task[0].name + }) + prj_task.save() + + def check_for_parent_tasks(self, tmp_task, prj_task, project_tasks): + if tmp_task.parent_task and not prj_task.parent_task: + parent_task_subject = frappe.db.get_value("Task", tmp_task.parent_task, "subject") + corresponding_prj_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks)) + if len(corresponding_prj_task): + prj_task.parent_task = corresponding_prj_task[0].name + print(prj_task.name, prj_task.parent_task, corresponding_prj_task[0].name) + prj_task.save() + print(prj_task.name, corresponding_prj_task[0].name) def is_row_updated(self, row, existing_task_data, fields): if self.get("__islocal") or not existing_task_data: return True diff --git a/erpnext/projects/doctype/project_template/project_template.py b/erpnext/projects/doctype/project_template/project_template.py index ac78135fc42..1beebf7a258 100644 --- a/erpnext/projects/doctype/project_template/project_template.py +++ b/erpnext/projects/doctype/project_template/project_template.py @@ -3,8 +3,27 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe from frappe.model.document import Document +from frappe import _ class ProjectTemplate(Document): - pass + + def validate(self): + self.validate_dependencies() + + def validate_dependencies(self): + for task in self.tasks: + task_details = frappe.get_doc("Task", task.task) + if task_details.depends_on: + for dependency_task in task_details.depends_on: + if not self.check_dependent_task_presence(dependency_task.task): + task_details_format = """{0}""".format(task_details.name) + dependency_task_format = """{0}""".format(dependency_task.task) + frappe.throw(_("Task {0} depends on Task {1}. Please add Task {1} to the Tasks list.").format(frappe.bold(task_details_format), frappe.bold(dependency_task_format))) + + def check_dependent_task_presence(self, task): + for task_details in self.tasks: + if task_details.task == task: + return True + return False diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index fb84094ffe6..072a848f263 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -17,291 +17,310 @@ class CircularReferenceError(frappe.ValidationError): pass class EndDateCannotBeGreaterThanProjectEndDateError(frappe.ValidationError): pass class Task(NestedSet): - nsm_parent_field = 'parent_task' + nsm_parent_field = 'parent_task' - def get_feed(self): - return '{0}: {1}'.format(_(self.status), self.subject) + def get_feed(self): + return '{0}: {1}'.format(_(self.status), self.subject) - def get_customer_details(self): - cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer) - if cust: - ret = {'customer_name': cust and cust[0][0] or ''} - return ret + def get_customer_details(self): + cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer) + if cust: + ret = {'customer_name': cust and cust[0][0] or ''} + return ret - def validate(self): - self.validate_dates() - self.validate_parent_project_dates() - self.validate_progress() - self.validate_status() - self.update_depends_on() + def validate(self): + self.validate_dates() + self.validate_parent_project_dates() + self.validate_progress() + self.validate_status() + self.update_depends_on() + self.validate_dependencies_for_template_task() - def validate_dates(self): - if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date): - frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \ - frappe.bold("Expected End Date"))) + def validate_dates(self): + if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date): + frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \ + frappe.bold("Expected End Date"))) - if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date): - frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \ - frappe.bold("Actual End Date"))) + if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date): + frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \ + frappe.bold("Actual End Date"))) - def validate_parent_project_dates(self): - if not self.project or frappe.flags.in_test: - return + def validate_parent_project_dates(self): + if not self.project or frappe.flags.in_test: + return - expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date") + expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date") - if expected_end_date: - validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected") - validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual") + if expected_end_date: + validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected") + validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual") - def validate_status(self): - if self.status!=self.get_db_value("status") and self.status == "Completed": - for d in self.depends_on: - if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"): - frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task))) + def validate_status(self): + if self.status!=self.get_db_value("status") and self.status == "Completed": + for d in self.depends_on: + if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"): + frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task))) - close_all_assignments(self.doctype, self.name) + close_all_assignments(self.doctype, self.name) - def validate_progress(self): - if flt(self.progress or 0) > 100: - frappe.throw(_("Progress % for a task cannot be more than 100.")) + def validate_progress(self): + if flt(self.progress or 0) > 100: + frappe.throw(_("Progress % for a task cannot be more than 100.")) - if flt(self.progress) == 100: - self.status = 'Completed' + if flt(self.progress) == 100: + self.status = 'Completed' - if self.status == 'Completed': - self.progress = 100 + if self.status == 'Completed': + self.progress = 100 - def update_depends_on(self): - depends_on_tasks = self.depends_on_tasks or "" - for d in self.depends_on: - if d.task and not d.task in depends_on_tasks: - depends_on_tasks += d.task + "," - self.depends_on_tasks = depends_on_tasks + def validate_dependencies_for_template_task(self): + if self.is_template: + self.validate_parent_template_task() + self.validate_depends_on_tasks() + + def validate_parent_template_task(self): + if self.parent_task: + if not frappe.db.get_value("Task", self.parent_task, "is_template"): + parent_task_format = """{0}""".format(self.parent_task) + frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format)) + + def validate_depends_on_tasks(self): + if self.depends_on: + for task in self.depends_on: + if not frappe.db.get_value("Task", task.task, "is_template"): + dependent_task_format = """{0}""".format(task.task) + frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format)) - def update_nsm_model(self): - frappe.utils.nestedset.update_nsm(self) + def update_depends_on(self): + depends_on_tasks = self.depends_on_tasks or "" + for d in self.depends_on: + if d.task and not d.task in depends_on_tasks: + depends_on_tasks += d.task + "," + self.depends_on_tasks = depends_on_tasks - def on_update(self): - self.update_nsm_model() - self.check_recursion() - self.reschedule_dependent_tasks() - self.update_project() - self.unassign_todo() - self.populate_depends_on() + def update_nsm_model(self): + frappe.utils.nestedset.update_nsm(self) - def unassign_todo(self): - if self.status == "Completed": - close_all_assignments(self.doctype, self.name) - if self.status == "Cancelled": - clear(self.doctype, self.name) + def on_update(self): + self.update_nsm_model() + self.check_recursion() + self.reschedule_dependent_tasks() + self.update_project() + self.unassign_todo() + self.populate_depends_on() - def update_total_expense_claim(self): - self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim` - where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0] + def unassign_todo(self): + if self.status == "Completed": + close_all_assignments(self.doctype, self.name) + if self.status == "Cancelled": + clear(self.doctype, self.name) - def update_time_and_costing(self): - tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date, - sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount, - sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1""" - ,self.name, as_dict=1)[0] - if self.status == "Open": - self.status = "Working" - self.total_costing_amount= tl.total_costing_amount - self.total_billing_amount= tl.total_billing_amount - self.actual_time= tl.time - self.act_start_date= tl.start_date - self.act_end_date= tl.end_date + def update_total_expense_claim(self): + self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim` + where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0] - def update_project(self): - if self.project and not self.flags.from_project: - frappe.get_cached_doc("Project", self.project).update_project() + def update_time_and_costing(self): + tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date, + sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount, + sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1""" + ,self.name, as_dict=1)[0] + if self.status == "Open": + self.status = "Working" + self.total_costing_amount= tl.total_costing_amount + self.total_billing_amount= tl.total_billing_amount + self.actual_time= tl.time + self.act_start_date= tl.start_date + self.act_end_date= tl.end_date - def check_recursion(self): - if self.flags.ignore_recursion_check: return - check_list = [['task', 'parent'], ['parent', 'task']] - for d in check_list: - task_list, count = [self.name], 0 - while (len(task_list) > count ): - tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " % - (d[0], d[1], '%s'), cstr(task_list[count])) - count = count + 1 - for b in tasks: - if b[0] == self.name: - frappe.throw(_("Circular Reference Error"), CircularReferenceError) - if b[0]: - task_list.append(b[0]) + def update_project(self): + if self.project and not self.flags.from_project: + frappe.get_cached_doc("Project", self.project).update_project() - if count == 15: - break + def check_recursion(self): + if self.flags.ignore_recursion_check: return + check_list = [['task', 'parent'], ['parent', 'task']] + for d in check_list: + task_list, count = [self.name], 0 + while (len(task_list) > count ): + tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " % + (d[0], d[1], '%s'), cstr(task_list[count])) + count = count + 1 + for b in tasks: + if b[0] == self.name: + frappe.throw(_("Circular Reference Error"), CircularReferenceError) + if b[0]: + task_list.append(b[0]) - def reschedule_dependent_tasks(self): - end_date = self.exp_end_date or self.act_end_date - if end_date: - for task_name in frappe.db.sql(""" - select name from `tabTask` as parent - where parent.project = %(project)s - and parent.name in ( - select parent from `tabTask Depends On` as child - where child.task = %(task)s and child.project = %(project)s) - """, {'project': self.project, 'task':self.name }, as_dict=1): - task = frappe.get_doc("Task", task_name.name) - if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open": - task_duration = date_diff(task.exp_end_date, task.exp_start_date) - task.exp_start_date = add_days(end_date, 1) - task.exp_end_date = add_days(task.exp_start_date, task_duration) - task.flags.ignore_recursion_check = True - task.save() + if count == 15: + break - def has_webform_permission(self): - project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user") - if project_user: - return True + def reschedule_dependent_tasks(self): + end_date = self.exp_end_date or self.act_end_date + if end_date: + for task_name in frappe.db.sql(""" + select name from `tabTask` as parent + where parent.project = %(project)s + and parent.name in ( + select parent from `tabTask Depends On` as child + where child.task = %(task)s and child.project = %(project)s) + """, {'project': self.project, 'task':self.name }, as_dict=1): + task = frappe.get_doc("Task", task_name.name) + if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open": + task_duration = date_diff(task.exp_end_date, task.exp_start_date) + task.exp_start_date = add_days(end_date, 1) + task.exp_end_date = add_days(task.exp_start_date, task_duration) + task.flags.ignore_recursion_check = True + task.save() - def populate_depends_on(self): - if self.parent_task: - parent = frappe.get_doc('Task', self.parent_task) - if not self.name in [row.task for row in parent.depends_on]: - parent.append("depends_on", { - "doctype": "Task Depends On", - "task": self.name, - "subject": self.subject - }) - parent.save() + def has_webform_permission(self): + project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user") + if project_user: + return True - def on_trash(self): - if check_if_child_exists(self.name): - throw(_("Child Task exists for this Task. You can not delete this Task.")) + def populate_depends_on(self): + if self.parent_task: + parent = frappe.get_doc('Task', self.parent_task) + if not self.name in [row.task for row in parent.depends_on]: + parent.append("depends_on", { + "doctype": "Task Depends On", + "task": self.name, + "subject": self.subject + }) + parent.save() - self.update_nsm_model() + def on_trash(self): + if check_if_child_exists(self.name): + throw(_("Child Task exists for this Task. You can not delete this Task.")) - def after_delete(self): - self.update_project() + self.update_nsm_model() - def update_status(self): - if self.status not in ('Cancelled', 'Completed') and self.exp_end_date: - from datetime import datetime - if self.exp_end_date < datetime.now().date(): - self.db_set('status', 'Overdue', update_modified=False) - self.update_project() + def after_delete(self): + self.update_project() + + def update_status(self): + if self.status not in ('Cancelled', 'Completed') and self.exp_end_date: + from datetime import datetime + if self.exp_end_date < datetime.now().date(): + self.db_set('status', 'Overdue', update_modified=False) + self.update_project() @frappe.whitelist() def check_if_child_exists(name): - child_tasks = frappe.get_all("Task", filters={"parent_task": name}) - child_tasks = [get_link_to_form("Task", task.name) for task in child_tasks] - return child_tasks + child_tasks = frappe.get_all("Task", filters={"parent_task": name}) + child_tasks = [get_link_to_form("Task", task.name) for task in child_tasks] + return child_tasks @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_project(doctype, txt, searchfield, start, page_len, filters): - from erpnext.controllers.queries import get_match_cond - return frappe.db.sql(""" select name from `tabProject` - where %(key)s like %(txt)s - %(mcond)s - order by name - limit %(start)s, %(page_len)s""" % { - 'key': searchfield, - 'txt': frappe.db.escape('%' + txt + '%'), - 'mcond':get_match_cond(doctype), - 'start': start, - 'page_len': page_len - }) + from erpnext.controllers.queries import get_match_cond + return frappe.db.sql(""" select name from `tabProject` + where %(key)s like %(txt)s + %(mcond)s + order by name + limit %(start)s, %(page_len)s""" % { + 'key': searchfield, + 'txt': frappe.db.escape('%' + txt + '%'), + 'mcond':get_match_cond(doctype), + 'start': start, + 'page_len': page_len + }) @frappe.whitelist() def set_multiple_status(names, status): - names = json.loads(names) - for name in names: - task = frappe.get_doc("Task", name) - task.status = status - task.save() + names = json.loads(names) + for name in names: + task = frappe.get_doc("Task", name) + task.status = status + task.save() def set_tasks_as_overdue(): - tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"]) - for task in tasks: - if task.status == "Pending Review": - if getdate(task.review_date) > getdate(today()): - continue - frappe.get_doc("Task", task.name).update_status() + tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"]) + for task in tasks: + if task.status == "Pending Review": + if getdate(task.review_date) > getdate(today()): + continue + frappe.get_doc("Task", task.name).update_status() @frappe.whitelist() def make_timesheet(source_name, target_doc=None, ignore_permissions=False): - def set_missing_values(source, target): - target.append("time_logs", { - "hours": source.actual_time, - "completed": source.status == "Completed", - "project": source.project, - "task": source.name - }) + def set_missing_values(source, target): + target.append("time_logs", { + "hours": source.actual_time, + "completed": source.status == "Completed", + "project": source.project, + "task": source.name + }) - doclist = get_mapped_doc("Task", source_name, { - "Task": { - "doctype": "Timesheet" - } - }, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions) + doclist = get_mapped_doc("Task", source_name, { + "Task": { + "doctype": "Timesheet" + } + }, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions) - return doclist + return doclist @frappe.whitelist() def get_children(doctype, parent, task=None, project=None, is_root=False): - filters = [['docstatus', '<', '2']] + filters = [['docstatus', '<', '2']] - if task: - filters.append(['parent_task', '=', task]) - elif parent and not is_root: - # via expand child - filters.append(['parent_task', '=', parent]) - else: - filters.append(['ifnull(`parent_task`, "")', '=', '']) + if task: + filters.append(['parent_task', '=', task]) + elif parent and not is_root: + # via expand child + filters.append(['parent_task', '=', parent]) + else: + filters.append(['ifnull(`parent_task`, "")', '=', '']) - if project: - filters.append(['project', '=', project]) + if project: + filters.append(['project', '=', project]) - tasks = frappe.get_list(doctype, fields=[ - 'name as value', - 'subject as title', - 'is_group as expandable' - ], filters=filters, order_by='name') + tasks = frappe.get_list(doctype, fields=[ + 'name as value', + 'subject as title', + 'is_group as expandable' + ], filters=filters, order_by='name') - # return tasks - return tasks + # return tasks + return tasks @frappe.whitelist() def add_node(): - from frappe.desk.treeview import make_tree_args - args = frappe.form_dict - args.update({ - "name_field": "subject" - }) - args = make_tree_args(**args) + from frappe.desk.treeview import make_tree_args + args = frappe.form_dict + args.update({ + "name_field": "subject" + }) + args = make_tree_args(**args) - if args.parent_task == 'All Tasks' or args.parent_task == args.project: - args.parent_task = None + if args.parent_task == 'All Tasks' or args.parent_task == args.project: + args.parent_task = None - frappe.get_doc(args).insert() + frappe.get_doc(args).insert() @frappe.whitelist() def add_multiple_tasks(data, parent): - data = json.loads(data) - new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""} - new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or "" + data = json.loads(data) + new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""} + new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or "" - for d in data: - if not d.get("subject"): continue - new_doc['subject'] = d.get("subject") - new_task = frappe.get_doc(new_doc) - new_task.insert() + for d in data: + if not d.get("subject"): continue + new_doc['subject'] = d.get("subject") + new_task = frappe.get_doc(new_doc) + new_task.insert() def on_doctype_update(): - frappe.db.add_index("Task", ["lft", "rgt"]) + frappe.db.add_index("Task", ["lft", "rgt"]) def validate_project_dates(project_end_date, task, task_start, task_end, actual_or_expected_date): - if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0: - frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date)) + if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0: + frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date)) - if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0: - frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date)) + if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0: + frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date)) From e64718b2ae7a7a92e8e542e1361437cd030e4015 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 15 Dec 2020 09:16:27 +0530 Subject: [PATCH 076/295] fix: selecting salary component (#24121) --- .../doctype/additional_salary/additional_salary.js | 8 -------- .../doctype/employee_incentive/employee_incentive.js | 4 ++-- .../payroll/doctype/salary_structure/salary_structure.js | 7 +++---- .../payroll/doctype/salary_structure/salary_structure.py | 2 +- 4 files changed, 6 insertions(+), 15 deletions(-) diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.js b/erpnext/payroll/doctype/additional_salary/additional_salary.js index 0784de93eb1..7737e6c8869 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.js +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.js @@ -12,14 +12,6 @@ frappe.ui.form.on('Additional Salary', { } }; }); - - if (!frm.doc.currency) return; - frm.set_query("salary_component", function() { - return { - query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", - filters: {currency: frm.doc.currency, company: frm.doc.company} - }; - }); }, employee: function(frm) { diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.js b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js index 85d1c54a221..182ce0f83a6 100644 --- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.js +++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js @@ -11,11 +11,11 @@ frappe.ui.form.on('Employee Incentive', { }; }); - if (!frm.doc.currency) return; + 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", currency: frm.doc.currency, company: frm.doc.company} + filters: {type: "earning", company: frm.doc.company} }; }); diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.js b/erpnext/payroll/doctype/salary_structure/salary_structure.js index 7daae49c587..ba824c5d6fa 100755 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.js +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.js @@ -55,17 +55,17 @@ frappe.ui.form.on('Salary Structure', { }, set_earning_deduction_component: function(frm) { - if(!frm.doc.currency && !frm.doc.company) return; + 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", currency: frm.doc.currency, company: frm.doc.company} + 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", currency: frm.doc.currency, company: frm.doc.company} + filters: {type: "deduction", company: frm.doc.company} }; }); }, @@ -74,7 +74,6 @@ frappe.ui.form.on('Salary Structure', { currency: function(frm) { calculate_totals(frm.doc); frm.trigger("set_dynamic_labels") - frm.trigger('set_earning_deduction_component'); frm.refresh() }, diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py index 877e41d93c5..77914bb5319 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py @@ -210,7 +210,7 @@ def get_employees(salary_structure): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_earning_deduction_components(doctype, txt, searchfield, start, page_len, filters): - if len(filters) < 3: + if len(filters) < 2: return {} return frappe.db.sql(""" From 58e8e06ab7e1965fa4c37d0df9f986d58b095776 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 15 Dec 2020 09:17:17 +0530 Subject: [PATCH 077/295] fix: retention filters (#24123) * fix: retention filters * fix: slider --- erpnext/payroll/doctype/retention_bonus/retention_bonus.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/retention_bonus/retention_bonus.js b/erpnext/payroll/doctype/retention_bonus/retention_bonus.js index 6fe8ccad46b..f8bb40a9cb8 100644 --- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.js +++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.js @@ -4,9 +4,13 @@ frappe.ui.form.on('Retention Bonus', { setup: function(frm) { frm.set_query("employee", function() { + if (!frm.doc.company) { + frappe.msgprint(__("Please Select Company First")); + } return { filters: { - "status": "Active" + "status": "Active", + "company": frm.doc.company } }; }); From 89d14fdf6877f021057a23289a8a6c7e05fa061a Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 15 Dec 2020 09:31:30 +0530 Subject: [PATCH 078/295] fix: minor ui changes (#24125) * fix: minor ui changes * fix: slider --- .../employee_advance/employee_advance.js | 39 +++++++++++-------- .../employee_benefit_application.json | 7 +++- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.js b/erpnext/hr/doctype/employee_advance/employee_advance.js index 7056adf2083..5037ceb489e 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.js +++ b/erpnext/hr/doctype/employee_advance/employee_advance.js @@ -18,13 +18,18 @@ frappe.ui.form.on('Employee Advance', { if (!frm.doc.employee) { frappe.msgprint(__("Please select employee first")); } - var company_currency = erpnext.get_currency(frm.doc.company); + let company_currency = erpnext.get_currency(frm.doc.company); + let currencies = [company_currency]; + if (frm.doc.currency && (frm.doc.currency != company_currency)) { + currencies.push(frm.doc.currency); + } + return { filters: { "root_type": "Asset", "is_group": 0, "company": frm.doc.company, - "account_currency": ["in", [frm.doc.currency, company_currency]], + "account_currency": ["in", currencies], } }; }); @@ -181,21 +186,23 @@ frappe.ui.form.on('Employee Advance', { }, currency: function(frm) { - var from_currency = frm.doc.currency; - var company_currency; - if (!frm.doc.company) { - company_currency = erpnext.get_currency(frappe.defaults.get_default("Company")); - } else { - company_currency = erpnext.get_currency(frm.doc.company); + if (frm.doc.currency) { + var from_currency = frm.doc.currency; + var company_currency; + if (!frm.doc.company) { + company_currency = erpnext.get_currency(frappe.defaults.get_default("Company")); + } else { + company_currency = erpnext.get_currency(frm.doc.company); + } + if (from_currency != company_currency) { + frm.events.set_exchange_rate(frm, from_currency, company_currency); + } else { + frm.set_value("exchange_rate", 1.0); + frm.set_df_property('exchange_rate', 'hidden', 1); + frm.set_df_property("exchange_rate", "description", "" ); + } + frm.refresh_fields(); } - if (from_currency != company_currency) { - frm.events.set_exchange_rate(frm, from_currency, company_currency); - } else { - frm.set_value("exchange_rate", 1.0); - frm.set_df_property('exchange_rate', 'hidden', 1); - frm.set_df_property("exchange_rate", "description", "" ); - } - frm.refresh_fields(); }, set_exchange_rate: function(frm, from_currency, company_currency) { diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json index 9a5a463152e..4c45580bf01 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json @@ -23,6 +23,7 @@ "employee_benefits", "totals", "total_amount", + "column_break", "pro_rata_dispensed_amount" ], "fields": [ @@ -139,11 +140,15 @@ "label": "Company", "options": "Company", "reqd": 1 + }, + { + "fieldname": "column_break", + "fieldtype": "Column Break" } ], "is_submittable": 1, "links": [], - "modified": "2020-11-25 11:49:05.095101", + "modified": "2020-12-14 15:52:08.566418", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Benefit Application", From 85213fa8cbcbadbfa97848433e5a15fda0220dd7 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 15 Dec 2020 09:32:02 +0530 Subject: [PATCH 079/295] fix(Asset): set current asset value before calculating difference amount (#24119) --- .../doctype/asset_value_adjustment/asset_value_adjustment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index c2579ebf708..74ca62ffdad 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -13,8 +13,8 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g class AssetValueAdjustment(Document): def validate(self): self.validate_date() - self.set_difference_amount() self.set_current_asset_value() + self.set_difference_amount() def on_submit(self): self.make_depreciation_entry() From f2206c27e75ad743ec73cc2332bed47917727689 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Tue, 15 Dec 2020 05:05:16 +0100 Subject: [PATCH 080/295] fix: allow other github links in same PR (#23995) --- .github/helper/documentation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/helper/documentation.py b/.github/helper/documentation.py index b603ed5e53d..9cc4663c394 100644 --- a/.github/helper/documentation.py +++ b/.github/helper/documentation.py @@ -21,8 +21,8 @@ def docs_link_exists(body): if word.startswith('http') and uri_validator(word): parsed_url = urlparse(word) if parsed_url.netloc == "github.com": - _, org, repo, _type, ref = parsed_url.path.split('/') - if org == "frappe" and repo in docs_repos: + parts = parsed_url.path.split('/') + if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos: return True From 23f0debf8807eeac894cfb5d628fddb80bf0fd72 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 15 Dec 2020 10:00:21 +0530 Subject: [PATCH 081/295] fix: tests --- erpnext/projects/doctype/project/test_project.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index f9bb1b3ac4d..ea54774d52d 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -79,16 +79,16 @@ class TestProject(unittest.TestCase): if not task2: task2 = create_task(subject="Test Temp Task with dependency", depends_on=task1.name, is_template=1, begin=2, duration=2) - template = make_project_template("Test Project with Templ - dependent tasks", [task2]) - project = get_project("Test Project with Templ - tasks with parent-child", template) + template = make_project_template("Test Project with Templ - dependent tasks", [task1, task2]) + project = get_project("Test Project with Templ - dependent tasks", template) tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') - self.assertEqual(tasks[0].subject, 'Test Temp Task with dependency') - self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, tasks[0])) - self.assertEqual(tasks[0].depends_on, tasks[1].name) + self.assertEqual(tasks[1].subject, 'Test Temp Task with dependency') + self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, tasks[1])) + self.assertTrue(tasks[1].depends_on_tasks.find(tasks[0].name) >= 0 ) - self.assertEqual(tasks[1].subject, 'Test Temp Task for dependency') - self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, tasks[1]) ) + self.assertEqual(tasks[0].subject, 'Test Temp Task for dependency') + self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, tasks[0]) ) self.assertEqual(len(tasks), 2) From caf67e608f871adca275e001dddc96c96af4ea77 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 15 Dec 2020 10:00:31 +0530 Subject: [PATCH 082/295] fix: tests --- erpnext/projects/doctype/project/project.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 2d3339773a1..5a9375a0e6c 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -82,31 +82,29 @@ class Project(Document): def dependency_mapping(self, template_tasks, project_tasks): for tmp_task in template_tasks: - for prj_task in project_tasks: - if tmp_task.subject == prj_task.subject: - self.check_depends_on_value(tmp_task, prj_task, project_tasks) - self.check_for_parent_tasks(tmp_task, prj_task, project_tasks) + prj_task = list(filter(lambda x: x.subject == tmp_task.subject, project_tasks))[0] + self.check_depends_on_value(tmp_task, prj_task, project_tasks) + self.check_for_parent_tasks(tmp_task, prj_task, project_tasks) def check_depends_on_value(self, tmp_task, prj_task, project_tasks): - if tmp_task.depends_on and not prj_task.depends_on: - for child_task in tmp_task.depends_on: + if tmp_task.get("depends_on") and not prj_task.get("depends_on"): + for child_task in tmp_task.get("depends_on"): child_task_subject = frappe.db.get_value("Task", child_task.task, "subject") corresponding_prj_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks)) if len(corresponding_prj_task): prj_task.append("depends_on",{ "task": corresponding_prj_task[0].name }) + print(prj_task.name) prj_task.save() def check_for_parent_tasks(self, tmp_task, prj_task, project_tasks): - if tmp_task.parent_task and not prj_task.parent_task: - parent_task_subject = frappe.db.get_value("Task", tmp_task.parent_task, "subject") + if tmp_task.get("parent_task") and not prj_task.get("parent_task"): + parent_task_subject = frappe.db.get_value("Task", tmp_task.get("parent_task"), "subject") corresponding_prj_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks)) if len(corresponding_prj_task): prj_task.parent_task = corresponding_prj_task[0].name - print(prj_task.name, prj_task.parent_task, corresponding_prj_task[0].name) prj_task.save() - print(prj_task.name, corresponding_prj_task[0].name) def is_row_updated(self, row, existing_task_data, fields): if self.get("__islocal") or not existing_task_data: return True From a6fef7ae6bbdba8a4f922ebdfbf337033c41ac4d Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 15 Dec 2020 11:50:18 +0530 Subject: [PATCH 083/295] feat: parent-child relation tasks --- erpnext/projects/doctype/project/project.py | 2 +- erpnext/projects/doctype/project/test_project.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 5a9375a0e6c..13e72fec8a2 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -83,6 +83,7 @@ class Project(Document): def dependency_mapping(self, template_tasks, project_tasks): for tmp_task in template_tasks: prj_task = list(filter(lambda x: x.subject == tmp_task.subject, project_tasks))[0] + prj_task = frappe.get_doc("Task", prj_task.name) self.check_depends_on_value(tmp_task, prj_task, project_tasks) self.check_for_parent_tasks(tmp_task, prj_task, project_tasks) @@ -95,7 +96,6 @@ class Project(Document): prj_task.append("depends_on",{ "task": corresponding_prj_task[0].name }) - print(prj_task.name) prj_task.save() def check_for_parent_tasks(self, tmp_task, prj_task, project_tasks): diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index ea54774d52d..c3f56b8e860 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -49,10 +49,10 @@ class TestProject(unittest.TestCase): if not task3: task3 = create_task(subject="Test Temp Task child 2", parent_task=task1.name, is_template=1, begin=2, duration=3) - template = make_project_template("Test Project Template - tasks with parent-child", [task1]) + template = make_project_template("Test Project Template - tasks with parent-child", [task1, task2, task3]) project = get_project("Test Project with Templ - tasks with parent-child", template) tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') - print(tasks[0].duration) + self.assertEqual(tasks[0].subject, 'Test Temp Task parent') self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, tasks[0])) From c553453825d826da24516ffbde05fe2be1c3b938 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Tue, 15 Dec 2020 16:29:10 +0530 Subject: [PATCH 084/295] fix: user is not a field (#24129) --- erpnext/non_profit/doctype/member/member.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index 44b975e9e9d..25d6b538300 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -59,7 +59,7 @@ class Member(Document): frappe.msgprint(_("A customer is already linked to this Member")) cust = create_customer(frappe._dict({ 'fullname': self.member_name, - 'email': self.email_id or self.user, + 'email': self.email_id or self.email, 'phone': None })) @@ -177,4 +177,4 @@ def register_member(fullname, email, rzpay_plan_id, subscription_id, pan=None, m mobile=mobile )) - return member.name \ No newline at end of file + return member.name From 29778e2fba4b1f073fdfc048f784f755c57a1eeb Mon Sep 17 00:00:00 2001 From: Leela vadlamudi Date: Tue, 15 Dec 2020 21:23:17 +0530 Subject: [PATCH 085/295] feat: Voice Call Settings doctype added (#24126) --- erpnext/public/js/telephony.js | 2 +- .../doctype/voice_call_settings/__init__.py | 0 .../test_voice_call_settings.py | 10 ++ .../voice_call_settings.js | 8 ++ .../voice_call_settings.json | 124 ++++++++++++++++++ .../voice_call_settings.py | 10 ++ 6 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 erpnext/telephony/doctype/voice_call_settings/__init__.py create mode 100644 erpnext/telephony/doctype/voice_call_settings/test_voice_call_settings.py create mode 100644 erpnext/telephony/doctype/voice_call_settings/voice_call_settings.js create mode 100644 erpnext/telephony/doctype/voice_call_settings/voice_call_settings.json create mode 100644 erpnext/telephony/doctype/voice_call_settings/voice_call_settings.py diff --git a/erpnext/public/js/telephony.js b/erpnext/public/js/telephony.js index bd7f8903066..f9caadeed7f 100644 --- a/erpnext/public/js/telephony.js +++ b/erpnext/public/js/telephony.js @@ -20,4 +20,4 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlData.extend( { }); } } -}); \ No newline at end of file +}); diff --git a/erpnext/telephony/doctype/voice_call_settings/__init__.py b/erpnext/telephony/doctype/voice_call_settings/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/telephony/doctype/voice_call_settings/test_voice_call_settings.py b/erpnext/telephony/doctype/voice_call_settings/test_voice_call_settings.py new file mode 100644 index 00000000000..85d6adda093 --- /dev/null +++ b/erpnext/telephony/doctype/voice_call_settings/test_voice_call_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestVoiceCallSettings(unittest.TestCase): + pass diff --git a/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.js b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.js new file mode 100644 index 00000000000..4a61b612d00 --- /dev/null +++ b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Voice Call Settings', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.json b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.json new file mode 100644 index 00000000000..25e55a22dce --- /dev/null +++ b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.json @@ -0,0 +1,124 @@ +{ + "actions": [], + "autoname": "field:user", + "creation": "2020-12-08 16:52:40.590146", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user", + "call_receiving_device", + "column_break_3", + "greeting_message", + "agent_busy_message", + "agent_unavailable_message" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "label": "User", + "options": "User", + "permlevel": 1, + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "greeting_message", + "fieldtype": "Data", + "label": "Greeting Message" + }, + { + "fieldname": "agent_busy_message", + "fieldtype": "Data", + "label": "Agent Busy Message" + }, + { + "fieldname": "agent_unavailable_message", + "fieldtype": "Data", + "label": "Agent Unavailable Message" + }, + { + "default": "Computer", + "fieldname": "call_receiving_device", + "fieldtype": "Select", + "label": "Call Receiving Device", + "options": "Computer\nPhone" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-12-14 18:49:34.600194", + "modified_by": "Administrator", + "module": "Telephony", + "name": "Voice Call Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "delete": 1, + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "delete": 1, + "email": 1, + "export": 1, + "permlevel": 2, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "permlevel": 2, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.py b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.py new file mode 100644 index 00000000000..ad3bbf1784d --- /dev/null +++ b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.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 VoiceCallSettings(Document): + pass From 96a5e4effa54bb87c7700b0a060c2a119e02a0ac Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 16 Dec 2020 13:00:55 +0530 Subject: [PATCH 086/295] fix: Tax template update on customer address change --- erpnext/regional/india/taxes.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js index b70b2ec48cc..87baece65d3 100644 --- a/erpnext/regional/india/taxes.js +++ b/erpnext/regional/india/taxes.js @@ -12,6 +12,9 @@ erpnext.setup_auto_gst_taxation = (doctype) => { tax_category: function(frm) { frm.trigger('get_tax_template'); }, + customer_address: function(frm) { + frm.trigger('get_tax_template'); + }, get_tax_template: function(frm) { if (!frm.doc.company) return; From 87b477a31126e478c2bcc77861975e015474bc6a Mon Sep 17 00:00:00 2001 From: pateljannat Date: Wed, 16 Dec 2020 13:37:21 +0530 Subject: [PATCH 087/295] feat: patch for project template tasks --- erpnext/patches.txt | 1 + .../v13_0/update_project_template_tasks.py | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 erpnext/patches/v13_0/update_project_template_tasks.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 86ac613ae5b..435511210bc 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -741,3 +741,4 @@ 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 erpnext.patches.v13_0.update_returned_qty_in_pr_dn +erpnext.patches.v13_0.update_project_template_tasks diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py new file mode 100644 index 00000000000..55f0ff45057 --- /dev/null +++ b/erpnext/patches/v13_0/update_project_template_tasks.py @@ -0,0 +1,32 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + templates = frappe.get_list("Project Template", fields = ["name"]) + for template_name in templates: + template = frappe.get_doc("Project Template", template_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.name) + if replace_tasks: + template.tasks = [] + for tsk in new_tasks: + template.append("tasks", { + "task": tsk + }) + template.save() \ No newline at end of file From 9962ba86d0db913d53bf87736e1fdd2194436f09 Mon Sep 17 00:00:00 2001 From: "hasnain2808@gmail.com" Date: Wed, 16 Dec 2020 14:41:04 +0530 Subject: [PATCH 088/295] fix: charts not displaying when tree_type changed --- .../purchase_analytics/purchase_analytics.js | 72 +++++++++-------- .../report/sales_analytics/sales_analytics.js | 79 ++++++++++--------- 2 files changed, 81 insertions(+), 70 deletions(-) diff --git a/erpnext/buying/report/purchase_analytics/purchase_analytics.js b/erpnext/buying/report/purchase_analytics/purchase_analytics.js index e17973c337b..7ee9f2c372a 100644 --- a/erpnext/buying/report/purchase_analytics/purchase_analytics.js +++ b/erpnext/buying/report/purchase_analytics/purchase_analytics.js @@ -75,62 +75,66 @@ frappe.query_reports["Purchase Analytics"] = { return Object.assign(options, { checkboxColumn: true, events: { - onCheckRow: function(data) { + onCheckRow: function (data) { + if (!data) return; + + const data_doctype = $( + data[2].html + )[0].attributes.getNamedItem("data-doctype").value; + const tree_type = frappe.query_report.filters[0].value; + if (data_doctype != tree_type) return; + row_name = data[2].content; length = data.length; - var tree_type = frappe.query_report.filters[0].value; - - if(tree_type == "Supplier" || tree_type == "Item") { - row_values = data.slice(4,length-1).map(function (column) { - return column.content; - }) - } - else { - row_values = data.slice(3,length-1).map(function (column) { - return column.content; - }) + if (tree_type == "Supplier" || tree_type == "Item") { + row_values = data + .slice(4, length - 1) + .map(function (column) { + return column.content; + }); + } else { + row_values = data + .slice(3, length - 1) + .map(function (column) { + return column.content; + }); } - entry = { - 'name':row_name, - 'values':row_values - } + entry = { + name: row_name, + values: row_values, + }; let raw_data = frappe.query_report.chart.data; let new_datasets = raw_data.datasets; - var found = false; + let found = false; - for(var i=0; i < new_datasets.length;i++){ - if(new_datasets[i].name == row_name){ + for (let i = 0; i < new_datasets.length; i++) { + if (new_datasets[i].name == row_name) { found = true; - new_datasets.splice(i,1); + new_datasets.splice(i, 1); break; } } - if(!found){ + 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) + datasets: new_datasets, + }; + chart_options = { + data: new_data, + type: "line", + }; + frappe.query_report.render_chart(chart_options); frappe.query_report.raw_chart_data = new_data; }, - } + }, }); } } diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.js b/erpnext/selling/report/sales_analytics/sales_analytics.js index 0e565a3fb6f..aad6bfd5ef1 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.js +++ b/erpnext/selling/report/sales_analytics/sales_analytics.js @@ -74,67 +74,74 @@ frappe.query_reports["Sales Analytics"] = { return Object.assign(options, { checkboxColumn: true, events: { - onCheckRow: function(data) { + onCheckRow: function (data) { + if (!data) return; + + const data_doctype = $( + data[2].html + )[0].attributes.getNamedItem("data-doctype").value; + const tree_type = frappe.query_report.filters[0].value; + if (data_doctype != tree_type) return; + row_name = data[2].content; length = data.length; - var tree_type = frappe.query_report.filters[0].value; - - if(tree_type == "Customer") { - row_values = data.slice(4,length-1).map(function (column) { - return column.content; - }) + if (tree_type == "Customer") { + row_values = data + .slice(4, length - 1) + .map(function (column) { + return column.content; + }); } else if (tree_type == "Item") { - row_values = data.slice(5,length-1).map(function (column) { - return column.content; - }) - } - else { - row_values = data.slice(3,length-1).map(function (column) { - return column.content; - }) + row_values = data + .slice(5, length - 1) + .map(function (column) { + return column.content; + }); + } else { + row_values = data + .slice(3, length - 1) + .map(function (column) { + return column.content; + }); } entry = { - 'name':row_name, - 'values':row_values - } + name: row_name, + values: row_values, + }; let raw_data = frappe.query_report.chart.data; let new_datasets = raw_data.datasets; - var found = false; + let found = false; - for(var i=0; i < new_datasets.length;i++){ - if(new_datasets[i].name == row_name){ + for (let i = 0; i < new_datasets.length; i++) { + if (new_datasets[i].name == row_name) { found = true; - new_datasets.splice(i,1); + new_datasets.splice(i, 1); break; } } - if(!found){ + 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) + datasets: new_datasets, + }; + chart_options = { + data: new_data, + type: "line", + }; + frappe.query_report.render_chart(chart_options); frappe.query_report.raw_chart_data = new_data; }, - } - }) + }, + }); }, } From b184d43e757f2982aab9b900943c789920609f8c Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 16 Dec 2020 14:51:42 +0530 Subject: [PATCH 089/295] refactor: Auto Repeat next schedule date function params (#23959) * refactor: Auto Repeat next schedule date function params * refactor: Auto Repeat next schedule date function params --- erpnext/selling/doctype/sales_order/sales_order.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 04d85e575cc..accf59ebc45 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -418,8 +418,7 @@ class SalesOrder(SellingController): def on_recurring(self, reference_doc, auto_repeat_doc): def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date): - delivery_date = get_next_schedule_date(ref_doc_delivery_date, - auto_repeat_doc.frequency, auto_repeat_doc.start_date, cint(auto_repeat_doc.repeat_on_day)) + delivery_date = auto_repeat_doc.get_next_schedule_date(schedule_date=ref_doc_delivery_date) if delivery_date <= transaction_date: delivery_date_diff = frappe.utils.date_diff(ref_doc_delivery_date, red_doc_transaction_date) From 924f99bead32bb4eba656019a15931f797eb04ae Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 16 Dec 2020 15:42:50 +0530 Subject: [PATCH 090/295] fix: Help message --- .../accounting_dimension_filter.js | 12 ++++++++++++ .../accounting_dimension_filter.json | 19 +++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js index a2526e92c36..74b7b516763 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js @@ -6,6 +6,18 @@ frappe.ui.form.on('Accounting Dimension Filter', { 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() { diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json index 7736b2dffb2..c0327ad0ad8 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json @@ -14,7 +14,9 @@ "section_break_4", "accounts", "column_break_6", - "dimensions" + "dimensions", + "section_break_10", + "dimension_filter_help" ], "fields": [ { @@ -89,11 +91,24 @@ "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-11-24 12:34:42.458713", + "modified": "2020-12-16 15:27:23.659285", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting Dimension Filter", From 5a8a52b9c6739f25bbd3b128402cb5a19b40afc0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 16 Dec 2020 15:54:06 +0530 Subject: [PATCH 091/295] fix: Therapy Type and Therapy Plan field visibility in Patient Appointment --- .../patient_appointment/patient_appointment.json | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json index ac35acc21ac..35600e48092 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json @@ -23,9 +23,9 @@ "procedure_template", "get_procedure_from_encounter", "procedure_prescription", + "therapy_plan", "therapy_type", "get_prescribed_therapies", - "therapy_plan", "practitioner", "practitioner_name", "department", @@ -284,7 +284,7 @@ "report_hide": 1 }, { - "depends_on": "eval:doc.patient;", + "depends_on": "eval:doc.patient && doc.therapy_plan;", "fieldname": "therapy_type", "fieldtype": "Link", "label": "Therapy", @@ -292,17 +292,16 @@ "set_only_once": 1 }, { - "depends_on": "eval:doc.patient && doc.__islocal;", + "depends_on": "eval:doc.patient && doc.therapy_plan && doc.__islocal;", "fieldname": "get_prescribed_therapies", "fieldtype": "Button", "label": "Get Prescribed Therapies" }, { - "depends_on": "eval: doc.patient && doc.therapy_type", + "depends_on": "eval: doc.patient;", "fieldname": "therapy_plan", "fieldtype": "Link", "label": "Therapy Plan", - "mandatory_depends_on": "eval: doc.patient && doc.therapy_type", "options": "Therapy Plan" }, { @@ -348,7 +347,7 @@ } ], "links": [], - "modified": "2020-05-21 03:04:21.400893", + "modified": "2020-12-16 13:16:58.578503", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient Appointment", From f2a431d86615a5f3c509605dfb58c62adb24b366 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 16 Dec 2020 16:09:58 +0530 Subject: [PATCH 092/295] fix: filter Therapy Types and Therapy Plan in Patient Appointment --- .../patient_appointment.js | 30 +++++++++++++++++++ .../patient_appointment.py | 13 +++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js index 2d6b64532b1..79e1775b9db 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js @@ -22,6 +22,7 @@ frappe.ui.form.on('Patient Appointment', { filters: {'status': 'Active'} }; }); + frm.set_query('practitioner', function() { return { filters: { @@ -29,6 +30,7 @@ frappe.ui.form.on('Patient Appointment', { } }; }); + frm.set_query('service_unit', function(){ return { filters: { @@ -39,6 +41,16 @@ frappe.ui.form.on('Patient Appointment', { }; }); + frm.set_query('therapy_plan', function() { + return { + filters: { + 'patient': frm.doc.patient + } + }; + }); + + frm.trigger('set_therapy_type_filter'); + if (frm.is_new()) { frm.page.set_primary_action(__('Check Availability'), function() { if (!frm.doc.patient) { @@ -136,6 +148,24 @@ frappe.ui.form.on('Patient Appointment', { } }, + therapy_plan: function(frm) { + frm.trigger('set_therapy_type_filter'); + }, + + set_therapy_type_filter: function(frm) { + if (frm.doc.therapy_plan) { + frm.call('get_therapy_types').then(r => { + frm.set_query('therapy_type', function() { + return { + filters: { + 'name': ['in', r.message] + } + }; + }); + }); + } + }, + therapy_type: function(frm) { if (frm.doc.therapy_type) { frappe.db.get_value('Therapy Type', frm.doc.therapy_type, 'default_duration', (r) => { diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index e685b20a8c8..dc820cb464e 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -91,6 +91,17 @@ class PatientAppointment(Document): if fee_validity: frappe.msgprint(_('{0} has fee validity till {1}').format(self.patient, fee_validity.valid_till)) + def get_therapy_types(self): + if not self.therapy_plan: + return + + therapy_types = [] + doc = frappe.get_doc('Therapy Plan', self.therapy_plan) + for entry in doc.therapy_plan_details: + therapy_types.append(entry.therapy_type) + + return therapy_types + @frappe.whitelist() def check_payment_fields_reqd(patient): @@ -145,7 +156,7 @@ def invoice_appointment(appointment_doc): sales_invoice.flags.ignore_mandatory = True sales_invoice.save(ignore_permissions=True) sales_invoice.submit() - frappe.msgprint(_('Sales Invoice {0} created'.format(sales_invoice.name)), alert=True) + frappe.msgprint(_('Sales Invoice {0} created').format(sales_invoice.name), alert=True) frappe.db.set_value('Patient Appointment', appointment_doc.name, 'invoiced', 1) frappe.db.set_value('Patient Appointment', appointment_doc.name, 'ref_sales_invoice', sales_invoice.name) From d44f45c57be854c1c6c625ffccf86b56203c3dd7 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Wed, 16 Dec 2020 16:28:09 +0530 Subject: [PATCH 093/295] fix: sider issues --- erpnext/projects/doctype/project/test_project.py | 7 ++++--- erpnext/projects/doctype/task/task.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index c3f56b8e860..ce56a50b4e2 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -106,17 +106,18 @@ def get_project(name, template): def make_project(args): args = frappe._dict(args) - if args.project_template_name: - template = make_project_template(args.project_template_name) project = frappe.get_doc(dict( doctype = 'Project', project_name = args.project_name, status = 'Open', - project_template = template.name, expected_start_date = args.start_date )) + if args.project_template_name: + template = make_project_template(args.project_template_name) + project.project_template = template.name + if not frappe.db.exists("Project", args.project_name): project.insert() diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 072a848f263..80b764ba4f0 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -94,7 +94,7 @@ class Task(NestedSet): def update_depends_on(self): depends_on_tasks = self.depends_on_tasks or "" for d in self.depends_on: - if d.task and not d.task in depends_on_tasks: + if d.task and d.task not in depends_on_tasks: depends_on_tasks += d.task + "," self.depends_on_tasks = depends_on_tasks @@ -180,7 +180,7 @@ class Task(NestedSet): def populate_depends_on(self): if self.parent_task: parent = frappe.get_doc('Task', self.parent_task) - if not self.name in [row.task for row in parent.depends_on]: + if self.name not in [row.task for row in parent.depends_on]: parent.append("depends_on", { "doctype": "Task Depends On", "task": self.name, From ff59f18012c0587c5c76ef3bdb74c72ebcff4957 Mon Sep 17 00:00:00 2001 From: prssanna Date: Wed, 28 Oct 2020 11:02:02 +0530 Subject: [PATCH 094/295] fix: override field_map for job card gantt --- .../doctype/job_card/job_card_calendar.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card_calendar.js b/erpnext/manufacturing/doctype/job_card/job_card_calendar.js index cf07698ad6a..f4877fdca0b 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card_calendar.js +++ b/erpnext/manufacturing/doctype/job_card/job_card_calendar.js @@ -8,7 +8,17 @@ frappe.views.calendar["Job Card"] = { "allDay": "allDay", "progress": "progress" }, - gantt: true, + gantt: { + field_map: { + "start": "started_time", + "end": "started_time", + "id": "name", + "title": "subject", + "color": "color", + "allDay": "allDay", + "progress": "progress" + } + }, filters: [ { "fieldtype": "Link", From 23d6afe43a83fc60fecc6d9009ea271e1faf0e6e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 16 Dec 2020 18:21:08 +0530 Subject: [PATCH 095/295] fix: Auto Repeat Import (#24157) --- erpnext/selling/doctype/sales_order/sales_order.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index accf59ebc45..9388e0927e1 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -14,7 +14,6 @@ from erpnext.stock.stock_balance import update_bin_qty, get_reserved_qty from frappe.desk.notifications import clear_doctype_notifications from frappe.contacts.doctype.address.address import get_company_address from erpnext.controllers.selling_controller import SellingController -from frappe.automation.doctype.auto_repeat.auto_repeat import get_next_schedule_date from erpnext.selling.doctype.customer.customer import check_credit_limit from erpnext.stock.doctype.item.item import get_item_defaults from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults From 2528d5ee15a5ce9d5d9634eec016946b1416154d Mon Sep 17 00:00:00 2001 From: pateljannat Date: Wed, 16 Dec 2020 18:29:49 +0530 Subject: [PATCH 096/295] fix: tests --- erpnext/projects/doctype/task/test_task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index d43d132e80e..aded78b8574 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -104,7 +104,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 + task.project = project or "_Test Project" task.is_template = is_template task.start = begin task.duration = duration From 21168eab7f5922c7ea653b0883a68b5151acccb5 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 16 Dec 2020 19:39:34 +0530 Subject: [PATCH 097/295] fix: Remove patch for setting next date in Subscription (#24158) --- erpnext/patches.txt | 1 - .../v9_0/fix_subscription_next_date.py | 48 ------------------- 2 files changed, 49 deletions(-) delete mode 100644 erpnext/patches/v9_0/fix_subscription_next_date.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 86ac613ae5b..9e33014c38e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -450,7 +450,6 @@ erpnext.patches.v8_9.set_member_party_type erpnext.patches.v9_0.add_user_to_child_table_in_pos_profile erpnext.patches.v9_0.set_schedule_date_for_material_request_and_purchase_order erpnext.patches.v9_0.student_admission_childtable_migrate -erpnext.patches.v9_0.fix_subscription_next_date #2017-10-23 erpnext.patches.v9_0.add_healthcare_domain erpnext.patches.v9_0.set_variant_item_description erpnext.patches.v9_0.set_uoms_in_variant_field diff --git a/erpnext/patches/v9_0/fix_subscription_next_date.py b/erpnext/patches/v9_0/fix_subscription_next_date.py deleted file mode 100644 index 4595c8dc998..00000000000 --- a/erpnext/patches/v9_0/fix_subscription_next_date.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (c) 2017, Frappe and Contributors -# License: GNU General Public License v3. See license.txt - -from __future__ import unicode_literals -import frappe -from frappe.utils import getdate -from frappe.automation.doctype.auto_repeat.auto_repeat import get_next_schedule_date - -def execute(): - frappe.reload_doc('accounts', 'doctype', 'subscription') - fields = ["name", "reference_doctype", "reference_document", - "start_date", "frequency", "repeat_on_day"] - - for d in fields: - if not frappe.db.has_column('Subscription', d): - return - - doctypes = ('Purchase Order', 'Sales Order', 'Purchase Invoice', 'Sales Invoice') - for data in frappe.get_all('Subscription', - fields = fields, - filters = {'reference_doctype': ('in', doctypes), 'docstatus': 1}): - - recurring_id = frappe.db.get_value(data.reference_doctype, data.reference_document, "recurring_id") - if recurring_id: - frappe.db.sql("update `tab{0}` set subscription=%s where recurring_id=%s" - .format(data.reference_doctype), (data.name, recurring_id)) - - date_field = 'transaction_date' - if data.reference_doctype in ['Sales Invoice', 'Purchase Invoice']: - date_field = 'posting_date' - - start_date = frappe.db.get_value(data.reference_doctype, data.reference_document, date_field) - - if start_date and getdate(start_date) != getdate(data.start_date): - last_ref_date = frappe.db.sql(""" - select {0} - from `tab{1}` - where subscription=%s and docstatus < 2 - order by creation desc - limit 1 - """.format(date_field, data.reference_doctype), data.name)[0][0] - - next_schedule_date = get_next_schedule_date(last_ref_date, data.frequency, data.repeat_on_day) - - frappe.db.set_value("Subscription", data.name, { - "start_date": start_date, - "next_schedule_date": next_schedule_date - }, None) \ No newline at end of file From c9f63accddd6a7b24d6c3ff257d8fba3395c8c94 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 17 Dec 2020 00:12:17 +0530 Subject: [PATCH 098/295] fix: do not manufacture same serial no multiple times --- erpnext/stock/doctype/serial_no/serial_no.py | 24 +++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 295149e2387..25ce2d59695 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -6,7 +6,7 @@ import frappe import json from frappe.model.naming import make_autoname -from frappe.utils import cint, cstr, flt, add_days, nowdate, getdate +from frappe.utils import cint, cstr, flt, add_days, nowdate, getdate, get_link_to_form from erpnext.stock.get_item_details import get_reserved_qty_for_so from frappe import _, ValidationError @@ -244,7 +244,7 @@ def validate_serial_no(sle, item_det): for serial_no in serial_nos: if frappe.db.exists("Serial No", serial_no): sr = frappe.db.get_value("Serial No", serial_no, ["name", "item_code", "batch_no", "sales_order", - "delivery_document_no", "delivery_document_type", "warehouse", + "delivery_document_no", "delivery_document_type", "warehouse", "purchase_document_type", "purchase_document_no", "company"], as_dict=1) if sr and cint(sle.actual_qty) < 0 and sr.warehouse != sle.warehouse: @@ -256,9 +256,10 @@ def validate_serial_no(sle, item_det): frappe.throw(_("Serial No {0} does not belong to Item {1}").format(serial_no, sle.item_code), SerialNoItemError) - if cint(sle.actual_qty) > 0 and has_duplicate_serial_no(sr, sle): - frappe.throw(_("Serial No {0} has already been received").format(serial_no), - SerialNoDuplicateError) + if cint(sle.actual_qty) > 0 and has_serial_no_exists(sr, sle): + doc_name = frappe.bold(get_link_to_form(sr.purchase_document_type, sr.purchase_document_no)) + frappe.throw(_("Serial No {0} has already been received in the {1} #{2}") + .format(frappe.bold(serial_no), sr.purchase_document_type, doc_name), SerialNoDuplicateError) if (sr.delivery_document_no and sle.voucher_type not in ['Stock Entry', 'Stock Reconciliation'] and sle.voucher_type == sr.delivery_document_type): @@ -349,7 +350,7 @@ def validate_so_serial_no(sr, sales_order): frappe.throw(_("""{0} Serial No {1} cannot be delivered""") .format(msg, sr.name)) -def has_duplicate_serial_no(sn, sle): +def has_serial_no_exists(sn, sle): if (sn.warehouse and not sle.skip_serial_no_validaiton and sle.voucher_type != 'Stock Reconciliation'): return True @@ -359,12 +360,13 @@ def has_duplicate_serial_no(sn, sle): status = False if sn.purchase_document_no: - if sle.voucher_type in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"] and \ - sn.delivery_document_type not in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"]: + if (sle.voucher_type in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"] and + sn.delivery_document_type not in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"]): status = True - if status and sle.voucher_type == 'Stock Entry' and \ - frappe.db.get_value('Stock Entry', sle.voucher_no, 'purpose') != 'Material Receipt': + # If status is receipt then system will allow to in-ward the delivered serial no + if (status and sle.voucher_type == 'Stock Entry' and + frappe.db.get_value('Stock Entry', sle.voucher_no, 'purpose') == 'Material Receipt'): status = False return status @@ -420,7 +422,7 @@ def auto_make_serial_nos(args): if is_new: created_numbers.append(sr.name) - form_links = list(map(lambda d: frappe.utils.get_link_to_form('Serial No', d), created_numbers)) + form_links = list(map(lambda d: get_link_to_form('Serial No', d), created_numbers)) # Setting up tranlated title field for all cases singular_title = _("Serial Number Created") From f2bff8e220a26b1ed9a662e010b7e15fe91df73e Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 17 Dec 2020 11:54:59 +0530 Subject: [PATCH 099/295] fix: patch relaod doctype --- erpnext/patches/v13_0/update_project_template_tasks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py index 55f0ff45057..df1886f616c 100644 --- a/erpnext/patches/v13_0/update_project_template_tasks.py +++ b/erpnext/patches/v13_0/update_project_template_tasks.py @@ -5,9 +5,10 @@ from __future__ import unicode_literals import frappe def execute(): + frappe.reload_doctype("Project Template") templates = frappe.get_list("Project Template", fields = ["name"]) for template_name in templates: - template = frappe.get_doc("Project Template", template_name) + template = frappe.get_doc("Project Template", template_name.name) replace_tasks = False new_tasks = [] for task in template.tasks: From 1872e2c1ac429439de1c8d1f52c79b41e9dd7fdd Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 17 Dec 2020 14:29:52 +0530 Subject: [PATCH 100/295] fix: wrap assignees in a list --- erpnext/crm/doctype/appointment/appointment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 63efeb3cb61..2009ebf7cba 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -126,7 +126,7 @@ class Appointment(Document): add_assignemnt({ 'doctype': self.doctype, 'name': self.name, - 'assign_to': existing_assignee + 'assign_to': [existing_assignee] }) return if self._assign: @@ -139,7 +139,7 @@ class Appointment(Document): add_assignemnt({ 'doctype': self.doctype, 'name': self.name, - 'assign_to': agent + 'assign_to': [agent] }) break From 2dbb1d6bc72b28542eec44878c28f1eed069bcca Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 17 Dec 2020 15:49:52 +0530 Subject: [PATCH 101/295] fix: indentation --- erpnext/patches/v13_0/update_project_template_tasks.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py index df1886f616c..8dd0181eceb 100644 --- a/erpnext/patches/v13_0/update_project_template_tasks.py +++ b/erpnext/patches/v13_0/update_project_template_tasks.py @@ -5,10 +5,9 @@ from __future__ import unicode_literals import frappe def execute(): - frappe.reload_doctype("Project Template") - templates = frappe.get_list("Project Template", fields = ["name"]) - for template_name in templates: + for template_name in frappe.db.sql(""" select name from `tabProject Template` """, as_dict=1): template = frappe.get_doc("Project Template", template_name.name) + print(template.tasks) replace_tasks = False new_tasks = [] for task in template.tasks: From 611b42733b82a3ab737fde001f7ca9246f1e6870 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 17 Dec 2020 14:49:48 +0530 Subject: [PATCH 102/295] fix: leave policy dashboard fix and roles --- .../doctype/leave_policy/leave_policy_dashboard.py | 14 +------------- .../leave_policy_assignment.json | 5 ++++- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py index ff5dc2ff3e0..e0ec4be2dce 100644 --- a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py +++ b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py @@ -4,22 +4,10 @@ from frappe import _ def get_data(): return { 'fieldname': 'leave_policy', - 'non_standard_fieldnames': { - 'Employee Grade': 'default_leave_policy' - }, 'transactions': [ - { - 'label': _('Employees'), - 'items': ['Employee', 'Employee Grade'] - }, { 'label': _('Leaves'), 'items': ['Leave Allocation'] }, ] - } - - - - - \ No newline at end of file + } \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json index ecebb3b7d6c..bbb42227154 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json @@ -111,7 +111,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-10-15 15:18:15.227848", + "modified": "2020-12-17 16:27:20.311060", "modified_by": "Administrator", "module": "HR", "name": "Leave Policy Assignment", @@ -127,6 +127,7 @@ "report": 1, "role": "HR Manager", "share": 1, + "submit": 1, "write": 1 }, { @@ -139,6 +140,7 @@ "report": 1, "role": "HR User", "share": 1, + "submit": 1, "write": 1 }, { @@ -151,6 +153,7 @@ "report": 1, "role": "System Manager", "share": 1, + "submit": 1, "write": 1 } ], From 09f0e9111d6fb79868c58f55ead0f18351c6d216 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 17 Dec 2020 17:20:21 +0530 Subject: [PATCH 103/295] fix: patch --- erpnext/patches/v13_0/update_project_template_tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py index 8dd0181eceb..0bcd1d3f3a5 100644 --- a/erpnext/patches/v13_0/update_project_template_tasks.py +++ b/erpnext/patches/v13_0/update_project_template_tasks.py @@ -5,9 +5,9 @@ from __future__ import unicode_literals import frappe def execute(): + frappe.reload_doc("projects", "doctype", "project_template”) for template_name in frappe.db.sql(""" select name from `tabProject Template` """, as_dict=1): template = frappe.get_doc("Project Template", template_name.name) - print(template.tasks) replace_tasks = False new_tasks = [] for task in template.tasks: From 5a06908bbc247607e61d18f8fd848e1bceaa6e11 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Dec 2020 17:38:53 +0530 Subject: [PATCH 104/295] fix: Add breadccrumbs to item group page --- erpnext/templates/generators/item_group.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html index 40a064fc768..74b2ae3c515 100644 --- a/erpnext/templates/generators/item_group.html +++ b/erpnext/templates/generators/item_group.html @@ -1,5 +1,9 @@ {% extends "templates/web.html" %} +{% block breadcrumbs %} + {% include "templates/includes/breadcrumbs.html" %} +{% endblock %} + {% block header %}

    {{ name }}

    {% endblock %} {% block page_content %} From 79b71462cbdec8fabbb20f80f7e258bb55a65620 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 17 Dec 2020 18:21:34 +0530 Subject: [PATCH 105/295] fix: patch --- erpnext/patches/v13_0/update_project_template_tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py index 0bcd1d3f3a5..1303efd93fb 100644 --- a/erpnext/patches/v13_0/update_project_template_tasks.py +++ b/erpnext/patches/v13_0/update_project_template_tasks.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe def execute(): - frappe.reload_doc("projects", "doctype", "project_template”) + frappe.reload_doc("projects", "doctype", "project_template") 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 From b074334dcff6d337351809fc991f01489424929e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Dec 2020 18:46:59 +0530 Subject: [PATCH 106/295] fix: Typo in tax category doctype query --- erpnext/regional/india/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index f8520c2d003..f256a66266d 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -53,7 +53,7 @@ def validate_gstin_for_india(doc, method): .format(doc.gst_state_number)) def validate_tax_category(doc, method): - if doc.get('gst_state') and frappe.db.get_value('Tax category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state}): + if doc.get('gst_state') and frappe.db.get_value('Tax Category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state}): if doc.is_inter_state: frappe.throw(_("Inter State tax category for GST State {0} already exists").format(doc.gst_state)) else: From b8e656512e8ab34601149c3f3ca0f9831441545a Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 17 Dec 2020 20:22:06 +0530 Subject: [PATCH 107/295] fix: test cleanup --- .../project_template/test_project_template.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/erpnext/projects/doctype/project_template/test_project_template.py b/erpnext/projects/doctype/project_template/test_project_template.py index 6c6b78368ed..95663cdcbbb 100644 --- a/erpnext/projects/doctype/project_template/test_project_template.py +++ b/erpnext/projects/doctype/project_template/test_project_template.py @@ -10,19 +10,6 @@ from erpnext.projects.doctype.task.test_task import create_task class TestProjectTemplate(unittest.TestCase): pass -def get_project_template(project_template_name="Test Project Template", project_tasks=[]): - if not frappe.db.exists('Project Template', project_template_name): - frappe.get_doc(dict( - doctype = 'Project Template', - name = project_template_name, - tasks = project_tasks or [ - create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3), - create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2) - ] - )).insert() - - return frappe.get_doc('Project Template', project_template_name) - def make_project_template(project_template_name, project_tasks=[]): if not frappe.db.exists('Project Template', project_template_name): project_tasks = project_tasks or [ From 04f48a011d343cad2dbe667b68408db6d523982e Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Fri, 18 Dec 2020 01:31:00 +0530 Subject: [PATCH 108/295] feat: Add year_to_date field --- erpnext/payroll/doctype/salary_slip/salary_slip.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json index 386618cf083..b64e5a08fe6 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.json +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json @@ -69,6 +69,7 @@ "net_pay_info", "net_pay", "base_net_pay", + "year_to_date", "column_break_53", "rounded_total", "base_rounded_total", @@ -578,13 +579,18 @@ { "fieldname": "column_break_69", "fieldtype": "Column Break" + }, + { + "fieldname": "year_to_date", + "fieldtype": "Currency", + "label": "Year To Date" } ], "icon": "fa fa-file-text", "idx": 9, "is_submittable": 1, "links": [], - "modified": "2020-10-21 23:02:59.400249", + "modified": "2020-12-17 21:51:19.612940", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Slip", From 9f1e018e4f868220a985af6784c1940a67d47b82 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Fri, 18 Dec 2020 01:35:27 +0530 Subject: [PATCH 109/295] feat: Compute year_to_date --- .../doctype/salary_slip/salary_slip.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 20365b191d0..27de46acc30 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -35,6 +35,9 @@ class SalarySlip(TransactionBase): def autoname(self): self.name = make_autoname(self.series) + def before_save(self): + self.compute_year_to_date() + def validate(self): self.status = self.get_status() self.validate_dates() @@ -1125,6 +1128,26 @@ class SalarySlip(TransactionBase): self.gross_pay += self.earnings[i].amount self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) + def compute_year_to_date(self): + year_to_date = 0 + fiscal_year = frappe.get_list('Fiscal Year', + fields = ['year','year_start_date','year_end_date'], + filters= {'year_start_date' : ['<=', self.start_date], + 'year_end_date' : ['>=', self.end_date] + })[0] + salary_slips_from_current_fiscal_year = frappe.get_list('Salary Slip', + fields = ['employee_name', 'start_date', 'end_date', 'net_pay'], + filters = {'employee_name' : self.employee_name, + 'start_date' : ['>=', fiscal_year.year_start_date], + 'end_date' : ['<=', fiscal_year.year_end_date] + }) + + for salary_slip in salary_slips_from_current_fiscal_year: + year_to_date += salary_slip.net_pay + + year_to_date += self.net_pay + self.year_to_date = year_to_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)) @@ -1135,4 +1158,4 @@ def unlink_ref_doc_from_salary_slip(ref_no): def generate_password_for_pdf(policy_template, employee): employee = frappe.get_doc("Employee", employee) - return policy_template.format(**employee.as_dict()) + return policy_template.format(**employee.as_dict()) \ No newline at end of file From 6afa83f2c7ac8a71c05959e21884ccab2733fa14 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 18 Dec 2020 11:05:41 +0530 Subject: [PATCH 110/295] fix(requirements): update to latest pandas --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c4f9171fcaa..678cf74fef0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ braintree==3.57.1 frappe gocardless-pro==1.11.0 googlemaps==3.1.1 -pandas==1.0.5 +pandas>=1.0.5 plaid-python==6.0.0 pycountry==19.8.18 PyGithub==1.44.1 From a81519f5571f25d15c47e20eefce257c18c3f1ff Mon Sep 17 00:00:00 2001 From: Afshan Date: Fri, 18 Dec 2020 11:16:01 +0530 Subject: [PATCH 111/295] fix: error popup for submitted doc --- .../payroll/doctype/salary_slip/salary_slip.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js index f7e22c63879..abe873d8393 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.js +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js @@ -214,14 +214,16 @@ frappe.ui.form.on('Salary Slip Timesheet', { }); var calculate_totals = function(frm) { - if (frm.doc.earnings || frm.doc.deductions) { - frappe.call({ - method: "set_totals", - doc: frm.doc, - callback: function() { - frm.refresh_fields(); - } - }); + if (frm.doc.docstatus === 0) { + if (frm.doc.earnings || frm.doc.deductions) { + frappe.call({ + method: "set_totals", + doc: frm.doc, + callback: function() { + frm.refresh_fields(); + } + }); + } } }; From 89a02d7d3f705742bc95e654e2909a821e883e45 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Fri, 18 Dec 2020 14:59:20 +0530 Subject: [PATCH 112/295] feat: Changed Fiscal Year to Payroll Period --- .../doctype/salary_slip/salary_slip.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 27de46acc30..0ea0684a8ff 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -35,9 +35,6 @@ class SalarySlip(TransactionBase): def autoname(self): self.name = make_autoname(self.series) - def before_save(self): - self.compute_year_to_date() - def validate(self): self.status = self.get_status() self.validate_dates() @@ -52,6 +49,7 @@ class SalarySlip(TransactionBase): self.get_working_days_details(lwp = self.leave_without_pay) self.calculate_net_pay() + self.compute_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") @@ -1130,19 +1128,21 @@ class SalarySlip(TransactionBase): def compute_year_to_date(self): year_to_date = 0 - fiscal_year = frappe.get_list('Fiscal Year', - fields = ['year','year_start_date','year_end_date'], - filters= {'year_start_date' : ['<=', self.start_date], - 'year_end_date' : ['>=', self.end_date] + payroll_period = frappe.get_list('Payroll Period', + fields = ['start_date','end_date','company'], + filters= {'start_date' : ['<=', self.start_date], + 'end_date' : ['>=', self.end_date], + 'company' : self.company })[0] - salary_slips_from_current_fiscal_year = frappe.get_list('Salary Slip', + salary_slips_from_current_payroll_period = frappe.get_list('Salary Slip', fields = ['employee_name', 'start_date', 'end_date', 'net_pay'], filters = {'employee_name' : self.employee_name, - 'start_date' : ['>=', fiscal_year.year_start_date], - 'end_date' : ['<=', fiscal_year.year_end_date] + 'start_date' : ['>=', payroll_period.start_date], + 'end_date' : ['<=', payroll_period.end_date], + 'name' : ['!=', self.name] }) - for salary_slip in salary_slips_from_current_fiscal_year: + for salary_slip in salary_slips_from_current_payroll_period: year_to_date += salary_slip.net_pay year_to_date += self.net_pay From d6277cdc7f08f14081b7e425f8a901472c4a73cb Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 18 Dec 2020 21:37:19 +0530 Subject: [PATCH 113/295] feat: Value Based and Numeric Quality Inspection - Acceptance Formula is optional - Choose between Value based and Numeric QI - If numeric, select single or multiple readings - Added Min, Max and Mean Values for numeric inspection to avoid formula usage - Deprecated code cleanup in js file --- .../item_quality_inspection_parameter.json | 54 +++++++++- .../quality_inspection/quality_inspection.js | 102 +++++++++--------- .../quality_inspection.json | 4 +- .../quality_inspection/quality_inspection.py | 98 +++++++++++++---- .../quality_inspection_reading.json | 93 ++++++++++++++-- .../quality_inspection_template.py | 4 +- 6 files changed, 268 insertions(+), 87 deletions(-) diff --git a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json index 888bc2de474..f4501281579 100644 --- a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json +++ b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json @@ -8,8 +8,14 @@ "field_order": [ "specification", "value", + "value_based", + "single_reading", "column_break_3", - "acceptance_formula" + "formula_based_criteria", + "acceptance_formula", + "min_value", + "max_value", + "mean_value" ], "fields": [ { @@ -24,10 +30,11 @@ "width": "200px" }, { + "depends_on": "eval:(!doc.formula_based_criteria && doc.value_based)", "fieldname": "value", "fieldtype": "Data", "in_list_view": 1, - "label": "Acceptance Criteria", + "label": "Acceptance Criteria Value", "oldfieldname": "value", "oldfieldtype": "Data" }, @@ -36,17 +43,56 @@ "fieldtype": "Column Break" }, { - "description": "Simple Python formula based on numeric Readings.
    Example 1: reading_1 > 0.2 and reading_1 < 0.5
    \nExample 2: (reading_1 + reading_2) / 2 < 10", + "depends_on": "formula_based_criteria", + "description": "Simple Python formula applied on Reading fields.
    Numeric eg.: reading_1 > 0.2 and reading_1 < 0.5
    \nValue based eg.: reading_value in (\"A\", \"B\", \"C)", "fieldname": "acceptance_formula", "fieldtype": "Code", "in_list_view": 1, "label": "Acceptance Criteria Formula" + }, + { + "default": "0", + "fieldname": "formula_based_criteria", + "fieldtype": "Check", + "label": "Formula Based Criteria" + }, + { + "default": "0", + "depends_on": "eval:!doc.value_based", + "fieldname": "single_reading", + "fieldtype": "Check", + "label": "Single Reading" + }, + { + "depends_on": "eval:(!doc.formula_based_criteria && !doc.single_reading && !doc.value_based)", + "fieldname": "mean_value", + "fieldtype": "Float", + "label": "Mean Value" + }, + { + "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)", + "fieldname": "min_value", + "fieldtype": "Float", + "label": "Minimum Value" + }, + { + "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)", + "fieldname": "max_value", + "fieldtype": "Float", + "label": "Maximum Value" + }, + { + "default": "0", + "description": "Non-numeric Inspection.", + "fieldname": "value_based", + "fieldtype": "Check", + "label": "Value Based" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-11-16 16:33:42.421842", + "modified": "2020-12-18 21:03:29.828723", "modified_by": "Administrator", "module": "Stock", "name": "Item Quality Inspection Parameter", diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js index 376848afaa4..f0bf9aed802 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js @@ -4,6 +4,54 @@ cur_frm.cscript.refresh = cur_frm.cscript.inspection_type; frappe.ui.form.on("Quality Inspection", { + setup: function(frm) { + frm.set_query("batch_no", function() { + return { + filters: { + "item": frm.doc.item_code + } + } + }); + + // Serial No based on item_code + frm.set_query("item_serial_no", function() { + var filters = {}; + if (frm.doc.item_code) { + filters = { + 'item_code': frm.doc.item_code + } + } + return { filters: filters } + }); + + // item code based on GRN/DN + frm.set_query("item_code", function(doc) { + let doctype = doc.reference_type; + + if (doc.reference_type !== "Job Card") { + doctype = (doc.reference_type == "Stock Entry") ? + "Stock Entry Detail" : doc.reference_type + " Item"; + } + + if (doc.reference_type && doc.reference_name) { + let filters = { + "from": doctype, + "inspection_type": doc.inspection_type + }; + + if (doc.reference_type == doctype) + filters["reference_name"] = doc.reference_name; + else + filters["parent"] = doc.reference_name; + + return { + query: "erpnext.stock.doctype.quality_inspection.quality_inspection.item_query", + filters: filters + }; + } + }); + }, + item_code: function(frm) { if (frm.doc.item_code) { return frm.call({ @@ -26,55 +74,5 @@ frappe.ui.form.on("Quality Inspection", { } }); } - } -}) - -// item code based on GRN/DN -cur_frm.fields_dict['item_code'].get_query = function(doc, cdt, cdn) { - let doctype = doc.reference_type; - - if (doc.reference_type !== "Job Card") { - doctype = (doc.reference_type == "Stock Entry") ? - "Stock Entry Detail" : doc.reference_type + " Item"; - } - - if (doc.reference_type && doc.reference_name) { - let filters = { - "from": doctype, - "inspection_type": doc.inspection_type - }; - - if (doc.reference_type == doctype) - filters["reference_name"] = doc.reference_name; - else - filters["parent"] = doc.reference_name; - - return { - query: "erpnext.stock.doctype.quality_inspection.quality_inspection.item_query", - filters: filters - }; - } -}, - -// Serial No based on item_code -cur_frm.fields_dict['item_serial_no'].get_query = function(doc, cdt, cdn) { - var filters = {}; - if (doc.item_code) { - filters = { - 'item_code': doc.item_code - } - } - return { filters: filters } -} - -cur_frm.set_query("batch_no", function(doc) { - return { - filters: { - "item": doc.item_code - } - } -}) - -cur_frm.add_fetch('item_code', 'item_name', 'item_name'); -cur_frm.add_fetch('item_code', 'description', 'description'); - + }, +}) \ No newline at end of file diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json index f6d76194d94..edfe7e98b2e 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json @@ -136,6 +136,7 @@ "width": "50%" }, { + "fetch_from": "item_code.item_name", "fieldname": "item_name", "fieldtype": "Data", "in_global_search": 1, @@ -143,6 +144,7 @@ "read_only": 1 }, { + "fetch_from": "item_code.description", "fieldname": "description", "fieldtype": "Small Text", "label": "Description", @@ -236,7 +238,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-11-19 17:06:05.409963", + "modified": "2020-12-18 19:59:55.710300", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection", diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index ae4eb9b9956..a7a023bcbf3 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -6,7 +6,7 @@ import frappe from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc from frappe import _ -from frappe.utils import flt +from frappe.utils import flt, cint from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template \ import get_template_details @@ -16,7 +16,7 @@ class QualityInspection(Document): self.get_item_specification_details() if self.readings: - self.set_status_based_on_acceptance_formula() + self.inspect_and_set_status() def get_item_specification_details(self): if not self.quality_inspection_template: @@ -29,9 +29,7 @@ class QualityInspection(Document): parameters = get_template_details(self.quality_inspection_template) for d in parameters: child = self.append('readings', {}) - child.specification = d.specification - child.value = d.value - child.acceptance_formula = d.acceptance_formula + child.update(d) child.status = "Accepted" def get_quality_inspection_template(self): @@ -76,28 +74,84 @@ class QualityInspection(Document): """.format(parent_doc=self.reference_type, child_doc=doctype), (quality_inspection, self.modified, self.reference_name, self.item_code)) - def set_status_based_on_acceptance_formula(self): + def inspect_and_set_status(self): for reading in self.readings: - if not reading.acceptance_formula: continue + if reading.formula_based_criteria: + self.set_status_based_on_acceptance_formula(reading) + else: + self.set_status_based_on_acceptance_values(reading) + + def set_status_based_on_acceptance_values(self, reading): + if cint(reading.value_based): + result = reading.get("reading_value") == reading.get("value") + else: + # numeric readings + if cint(reading.single_reading): + reading_1 = flt(reading.get("reading_1")) + result = flt(reading.get("min_value")) <= reading_1 <= flt(reading.get("max_value")) + else: + result = self.min_max_criteria_passed(reading) and self.mean_criteria_passed(reading) + + reading.status = "Accepted" if result else "Rejected" + + def min_max_criteria_passed(self, reading): + """Determine whether all readings fall in the acceptable range.""" + for i in range(1, 11): + reading_field = reading.get("reading_" + str(i)) + if reading_field is not None: + result = flt(reading.get("min_value")) <= flt(reading_field) <= flt(reading.get("max_value")) + if not result: return False + return True + + def mean_criteria_passed(self, reading): + """Determine whether mean of all readings is acceptable.""" + if reading.get("mean_value"): + from statistics import mean + readings_list = [] - condition = reading.acceptance_formula - data = {} for i in range(1, 11): - field = "reading_" + str(i) - data[field] = flt(reading.get(field)) or 0 + reading_value = reading.get("reading_" + str(i)) + if reading_value is not None: + readings_list.append(flt(reading_value)) - try: - result = frappe.safe_eval(condition, None, data) - reading.status = "Accepted" if result else "Rejected" - except SyntaxError: - frappe.throw(_("Row #{0}: Acceptance Criteria Formula is incorrect.").format(reading.idx), - title=_("Invalid Formula")) - except NameError as e: - field = frappe.bold(e.args[0].split()[1]) - frappe.throw(_("Row #{0}: {1} is not a valid reading field. Please refer to the field description.") - .format(reading.idx, field), - title=_("Invalid Formula")) + actual_mean = mean(readings_list) if readings_list else 0 + return True if actual_mean == reading.get("mean_value") else False + return True # no mean value, nothing to check + + def set_status_based_on_acceptance_formula(self, reading): + if not reading.acceptance_formula: + frappe.throw(_("Row #{0}: Acceptance Criteria Formula is required.").format(reading.idx), + title=_("Missing Formula")) + + condition = reading.acceptance_formula + data = self.get_formula_evaluation_data(reading) + + try: + result = frappe.safe_eval(condition, None, data) + reading.status = "Accepted" if result else "Rejected" + except NameError as e: + field = frappe.bold(e.args[0].split()[1]) + frappe.throw(_("Row #{0}: {1} is not a valid reading field. Please refer to the field description.") + .format(reading.idx, field), + title=_("Invalid Formula")) + except Exception: + frappe.throw(_("Row #{0}: Acceptance Criteria Formula is incorrect.").format(reading.idx), + title=_("Invalid Formula")) + + def get_formula_evaluation_data(self, reading): + data = {} + if cint(reading.value_based): + data = {"reading_value": reading.get("reading_value")} + else: + # numeric readings + data = {"reading_1": flt(reading.get("reading_1"))} + if not cint(reading.single_reading): + # if multiple numeric readings add all readings to data + for i in range(2, 11): + field = "reading_" + str(i) + data[field] = flt(reading.get(field)) + return data @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs diff --git a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json index c1976dd1fb5..db95fabee0b 100644 --- a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json +++ b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json @@ -7,21 +7,30 @@ "engine": "InnoDB", "field_order": [ "specification", - "value", "status", + "value", + "value_based", "column_break_4", + "formula_based_criteria", "acceptance_formula", + "min_value", + "max_value", + "mean_value", "section_break_3", + "reading_value", + "section_break_14", + "single_reading", + "section_break_12", "reading_1", "reading_2", "reading_3", - "column_break_10", "reading_4", + "column_break_10", "reading_5", "reading_6", - "column_break_14", "reading_7", "reading_8", + "column_break_14", "reading_9", "reading_10" ], @@ -38,10 +47,11 @@ }, { "columns": 2, + "depends_on": "eval:(!doc.formula_based_criteria && doc.value_based)", "fieldname": "value", "fieldtype": "Data", "in_list_view": 1, - "label": "Acceptance Criteria", + "label": "Acceptance Criteria Value", "oldfieldname": "value", "oldfieldtype": "Data" }, @@ -56,6 +66,7 @@ }, { "columns": 1, + "depends_on": "eval:!doc.single_reading", "fieldname": "reading_2", "fieldtype": "Data", "in_list_view": 1, @@ -65,6 +76,7 @@ }, { "columns": 1, + "depends_on": "eval:!doc.single_reading", "fieldname": "reading_3", "fieldtype": "Data", "in_list_view": 1, @@ -73,6 +85,7 @@ "oldfieldtype": "Data" }, { + "depends_on": "eval:!doc.single_reading", "fieldname": "reading_4", "fieldtype": "Data", "label": "Reading 4", @@ -80,6 +93,7 @@ "oldfieldtype": "Data" }, { + "depends_on": "eval:!doc.single_reading", "fieldname": "reading_5", "fieldtype": "Data", "label": "Reading 5", @@ -87,6 +101,7 @@ "oldfieldtype": "Data" }, { + "depends_on": "eval:!doc.single_reading", "fieldname": "reading_6", "fieldtype": "Data", "label": "Reading 6", @@ -94,6 +109,7 @@ "oldfieldtype": "Data" }, { + "depends_on": "eval:!doc.single_reading", "fieldname": "reading_7", "fieldtype": "Data", "label": "Reading 7", @@ -101,6 +117,7 @@ "oldfieldtype": "Data" }, { + "depends_on": "eval:!doc.single_reading", "fieldname": "reading_8", "fieldtype": "Data", "label": "Reading 8", @@ -108,6 +125,7 @@ "oldfieldtype": "Data" }, { + "depends_on": "eval:!doc.single_reading", "fieldname": "reading_9", "fieldtype": "Data", "label": "Reading 9", @@ -115,6 +133,7 @@ "oldfieldtype": "Data" }, { + "depends_on": "eval:!doc.single_reading", "fieldname": "reading_10", "fieldtype": "Data", "label": "Reading 10", @@ -133,15 +152,18 @@ "options": "Accepted\nRejected" }, { + "depends_on": "value_based", "fieldname": "section_break_3", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Value Based Inspection" }, { "fieldname": "column_break_4", "fieldtype": "Column Break" }, { - "description": "Simple Python formula based on numeric Readings.
    Example 1: reading_1 > 0.2 and reading_1 < 0.5
    \nExample 2: (reading_1 + reading_2) / 2 < 10", + "depends_on": "formula_based_criteria", + "description": "Simple Python formula applied on Reading fields.
    Numeric eg.: reading_1 > 0.2 and reading_1 < 0.5
    \nValue based eg.: reading_value in (\"A\", \"B\", \"C)", "fieldname": "acceptance_formula", "fieldtype": "Code", "label": "Acceptance Criteria Formula" @@ -153,12 +175,69 @@ { "fieldname": "column_break_14", "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "formula_based_criteria", + "fieldtype": "Check", + "label": "Formula Based Criteria" + }, + { + "depends_on": "eval:(!doc.formula_based_criteria && !doc.single_reading && !doc.value_based)", + "fieldname": "mean_value", + "fieldtype": "Float", + "label": "Mean Value" + }, + { + "default": "0", + "fieldname": "single_reading", + "fieldtype": "Check", + "label": "Single Reading" + }, + { + "depends_on": "eval:!doc.value_based", + "fieldname": "section_break_12", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)", + "description": "Applied on each reading.", + "fieldname": "min_value", + "fieldtype": "Float", + "label": "Minimum Value" + }, + { + "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)", + "description": "Applied on each reading.", + "fieldname": "max_value", + "fieldtype": "Float", + "label": "Maximum Value" + }, + { + "default": "0", + "description": "Non-numeric Inspection.", + "fieldname": "value_based", + "fieldtype": "Check", + "label": "Value Based" + }, + { + "depends_on": "value_based", + "fieldname": "reading_value", + "fieldtype": "Data", + "label": "Reading Value" + }, + { + "depends_on": "eval:!doc.value_based", + "fieldname": "section_break_14", + "fieldtype": "Section Break", + "label": "Numeric Inspection" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-11-16 16:34:29.947856", + "modified": "2020-12-18 21:02:04.865777", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection Reading", diff --git a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py index e2848469b88..7dd0febc203 100644 --- a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py +++ b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py @@ -13,6 +13,8 @@ def get_template_details(template): if not template: return [] return frappe.get_all('Item Quality Inspection Parameter', - fields=["specification", "value", "acceptance_formula"], + fields=["specification", "value", "acceptance_formula", + "value_based", "formula_based_criteria", "single_reading", + "min_value", "max_value", "mean_value"], filters={'parenttype': 'Quality Inspection Template', 'parent': template}, order_by="idx") \ No newline at end of file From 1b1df6b6bcabedcf27ebc5c1cc4f2c0fac1e73b8 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Fri, 18 Dec 2020 23:51:05 +0530 Subject: [PATCH 114/295] feat: Add month_to_date field --- erpnext/payroll/doctype/salary_slip/salary_slip.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json index b64e5a08fe6..5141868adb7 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.json +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json @@ -73,6 +73,7 @@ "column_break_53", "rounded_total", "base_rounded_total", + "month_to_date", "section_break_55", "total_in_words", "column_break_69", @@ -583,14 +584,21 @@ { "fieldname": "year_to_date", "fieldtype": "Currency", - "label": "Year To Date" + "label": "Year To Date(Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "month_to_date", + "fieldtype": "Currency", + "label": "Month To Date" } ], "icon": "fa fa-file-text", "idx": 9, "is_submittable": 1, "links": [], - "modified": "2020-12-17 21:51:19.612940", + "modified": "2020-12-18 23:23:10.484574", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Slip", From 59fbf702dad1c4231d19fe8cfb4970862e4aaad0 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Fri, 18 Dec 2020 23:52:11 +0530 Subject: [PATCH 115/295] feat: Compute month_to_date --- .../doctype/salary_slip/salary_slip.py | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 0ea0684a8ff..e86a7fc3158 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -50,6 +50,7 @@ class SalarySlip(TransactionBase): self.calculate_net_pay() self.compute_year_to_date() + self.compute_month_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,8 +1139,7 @@ class SalarySlip(TransactionBase): fields = ['employee_name', 'start_date', 'end_date', 'net_pay'], filters = {'employee_name' : self.employee_name, 'start_date' : ['>=', payroll_period.start_date], - 'end_date' : ['<=', payroll_period.end_date], - 'name' : ['!=', self.name] + 'end_date' : ['<', self.start_date] }) for salary_slip in salary_slips_from_current_payroll_period: @@ -1148,6 +1148,22 @@ class SalarySlip(TransactionBase): year_to_date += self.net_pay self.year_to_date = year_to_date + def compute_month_to_date(self): + month_to_date = 0 + date = datetime.datetime.strptime(self.start_date,"%Y-%m-%d") + first_day_of_the_month = "1-" + str(date.month) + "-" + str(date.year) + salary_slips_from_this_month = frappe.get_list('Salary Slip', + fields = ['employee_name', 'start_date', 'net_pay'], + filters = {'employee_name' : self.employee_name, + 'start_date' : ['>=', first_day_of_the_month], + 'end_date' : ['<', self.start_date] + }) + for salary_slip in salary_slips_from_this_month: + month_to_date += salary_slip.net_pay + + month_to_date += self.net_pay + self.month_to_date = month_to_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)) From ddd9fe49fca4f05f31bc97e462a478aa52477e2d Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Fri, 18 Dec 2020 23:58:05 +0530 Subject: [PATCH 116/295] feat: Add month_to_date field --- erpnext/payroll/doctype/salary_slip/salary_slip.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json index 5141868adb7..d981a39953d 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.json +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json @@ -591,14 +591,16 @@ { "fieldname": "month_to_date", "fieldtype": "Currency", - "label": "Month To Date" + "label": "Month To Date(Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 9, "is_submittable": 1, "links": [], - "modified": "2020-12-18 23:23:10.484574", + "modified": "2020-12-18 23:57:41.042954", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Slip", From 0c4f97368d05f2277ccca54b583be0a104acf7a9 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 21 Dec 2020 11:44:48 +0530 Subject: [PATCH 117/295] chore: UX improvement - Removed 'single reading' checkbox, unnecessary - Removed 'Mean' field and added computed mean to formula data - Changed 'Value Based' to 'Non-Numeric' - Re-arranged fields --- .../item_quality_inspection_parameter.json | 43 +++++------- .../quality_inspection/quality_inspection.py | 57 +++++++--------- .../quality_inspection_reading.json | 65 +++++-------------- .../quality_inspection_template.py | 3 +- 4 files changed, 58 insertions(+), 110 deletions(-) diff --git a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json index f4501281579..9b980a1e013 100644 --- a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json +++ b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json @@ -8,14 +8,12 @@ "field_order": [ "specification", "value", - "value_based", - "single_reading", + "non_numeric", "column_break_3", - "formula_based_criteria", - "acceptance_formula", "min_value", "max_value", - "mean_value" + "formula_based_criteria", + "acceptance_formula" ], "fields": [ { @@ -27,10 +25,10 @@ "oldfieldtype": "Data", "print_width": "200px", "reqd": 1, - "width": "200px" + "width": "100px" }, { - "depends_on": "eval:(!doc.formula_based_criteria && doc.value_based)", + "depends_on": "eval:(!doc.formula_based_criteria && doc.non_numeric)", "fieldname": "value", "fieldtype": "Data", "in_list_view": 1, @@ -44,10 +42,9 @@ }, { "depends_on": "formula_based_criteria", - "description": "Simple Python formula applied on Reading fields.
    Numeric eg.: reading_1 > 0.2 and reading_1 < 0.5
    \nValue based eg.: reading_value in (\"A\", \"B\", \"C)", + "description": "Simple Python formula applied on Reading fields.
    Numeric eg. 1: reading_1 > 0.2 and reading_1 < 0.5
    \nNumeric eg. 2: mean > 3.5 (mean of populated fields)
    \nValue based eg.: reading_value in (\"A\", \"B\", \"C)", "fieldname": "acceptance_formula", "fieldtype": "Code", - "in_list_view": 1, "label": "Acceptance Criteria Formula" }, { @@ -57,42 +54,32 @@ "label": "Formula Based Criteria" }, { - "default": "0", - "depends_on": "eval:!doc.value_based", - "fieldname": "single_reading", - "fieldtype": "Check", - "label": "Single Reading" - }, - { - "depends_on": "eval:(!doc.formula_based_criteria && !doc.single_reading && !doc.value_based)", - "fieldname": "mean_value", - "fieldtype": "Float", - "label": "Mean Value" - }, - { - "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)", + "depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)", "fieldname": "min_value", "fieldtype": "Float", + "in_list_view": 1, "label": "Minimum Value" }, { - "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)", + "depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)", "fieldname": "max_value", "fieldtype": "Float", + "in_list_view": 1, "label": "Maximum Value" }, { "default": "0", - "description": "Non-numeric Inspection.", - "fieldname": "value_based", + "fieldname": "non_numeric", "fieldtype": "Check", - "label": "Value Based" + "in_list_view": 1, + "label": "Non-Numeric", + "width": "80px" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-12-18 21:03:29.828723", + "modified": "2020-12-21 11:37:55.387677", "modified_by": "Administrator", "module": "Stock", "name": "Item Quality Inspection Parameter", diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index a7a023bcbf3..f582658d871 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -79,46 +79,27 @@ class QualityInspection(Document): if reading.formula_based_criteria: self.set_status_based_on_acceptance_formula(reading) else: + # if not formula based check acceptance values set self.set_status_based_on_acceptance_values(reading) def set_status_based_on_acceptance_values(self, reading): - if cint(reading.value_based): + if cint(reading.non_numeric): result = reading.get("reading_value") == reading.get("value") else: # numeric readings - if cint(reading.single_reading): - reading_1 = flt(reading.get("reading_1")) - result = flt(reading.get("min_value")) <= reading_1 <= flt(reading.get("max_value")) - else: - result = self.min_max_criteria_passed(reading) and self.mean_criteria_passed(reading) + result = self.min_max_criteria_passed(reading) reading.status = "Accepted" if result else "Rejected" def min_max_criteria_passed(self, reading): """Determine whether all readings fall in the acceptable range.""" for i in range(1, 11): - reading_field = reading.get("reading_" + str(i)) - if reading_field is not None: - result = flt(reading.get("min_value")) <= flt(reading_field) <= flt(reading.get("max_value")) + reading_value = reading.get("reading_" + str(i)) + if reading_value is not None and reading_value.strip(): + result = flt(reading.get("min_value")) <= flt(reading_value) <= flt(reading.get("max_value")) if not result: return False return True - def mean_criteria_passed(self, reading): - """Determine whether mean of all readings is acceptable.""" - if reading.get("mean_value"): - from statistics import mean - readings_list = [] - - for i in range(1, 11): - reading_value = reading.get("reading_" + str(i)) - if reading_value is not None: - readings_list.append(flt(reading_value)) - - actual_mean = mean(readings_list) if readings_list else 0 - return True if actual_mean == reading.get("mean_value") else False - - return True # no mean value, nothing to check - def set_status_based_on_acceptance_formula(self, reading): if not reading.acceptance_formula: frappe.throw(_("Row #{0}: Acceptance Criteria Formula is required.").format(reading.idx), @@ -141,18 +122,30 @@ class QualityInspection(Document): def get_formula_evaluation_data(self, reading): data = {} - if cint(reading.value_based): + if cint(reading.non_numeric): data = {"reading_value": reading.get("reading_value")} else: # numeric readings - data = {"reading_1": flt(reading.get("reading_1"))} - if not cint(reading.single_reading): - # if multiple numeric readings add all readings to data - for i in range(2, 11): - field = "reading_" + str(i) - data[field] = flt(reading.get(field)) + for i in range(1, 11): + field = "reading_" + str(i) + data[field] = flt(reading.get(field)) + data["mean"] = self.calculate_mean(reading) + return data + def calculate_mean(self, reading): + """Calculate mean of all non-empty readings.""" + from statistics import mean + readings_list = [] + + for i in range(1, 11): + reading_value = reading.get("reading_" + str(i)) + if reading_value is not None and reading_value.strip(): + readings_list.append(flt(reading_value)) + + actual_mean = mean(readings_list) if readings_list else 0 + return actual_mean + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def item_query(doctype, txt, searchfield, start, page_len, filters): diff --git a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json index db95fabee0b..0792f26d2ab 100644 --- a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json +++ b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json @@ -9,18 +9,15 @@ "specification", "status", "value", - "value_based", + "non_numeric", "column_break_4", - "formula_based_criteria", - "acceptance_formula", "min_value", "max_value", - "mean_value", + "formula_based_criteria", + "acceptance_formula", "section_break_3", "reading_value", "section_break_14", - "single_reading", - "section_break_12", "reading_1", "reading_2", "reading_3", @@ -47,7 +44,7 @@ }, { "columns": 2, - "depends_on": "eval:(!doc.formula_based_criteria && doc.value_based)", + "depends_on": "eval:(!doc.formula_based_criteria && doc.non_numeric)", "fieldname": "value", "fieldtype": "Data", "in_list_view": 1, @@ -66,7 +63,6 @@ }, { "columns": 1, - "depends_on": "eval:!doc.single_reading", "fieldname": "reading_2", "fieldtype": "Data", "in_list_view": 1, @@ -76,7 +72,6 @@ }, { "columns": 1, - "depends_on": "eval:!doc.single_reading", "fieldname": "reading_3", "fieldtype": "Data", "in_list_view": 1, @@ -85,7 +80,6 @@ "oldfieldtype": "Data" }, { - "depends_on": "eval:!doc.single_reading", "fieldname": "reading_4", "fieldtype": "Data", "label": "Reading 4", @@ -93,7 +87,6 @@ "oldfieldtype": "Data" }, { - "depends_on": "eval:!doc.single_reading", "fieldname": "reading_5", "fieldtype": "Data", "label": "Reading 5", @@ -101,7 +94,6 @@ "oldfieldtype": "Data" }, { - "depends_on": "eval:!doc.single_reading", "fieldname": "reading_6", "fieldtype": "Data", "label": "Reading 6", @@ -109,7 +101,6 @@ "oldfieldtype": "Data" }, { - "depends_on": "eval:!doc.single_reading", "fieldname": "reading_7", "fieldtype": "Data", "label": "Reading 7", @@ -117,7 +108,6 @@ "oldfieldtype": "Data" }, { - "depends_on": "eval:!doc.single_reading", "fieldname": "reading_8", "fieldtype": "Data", "label": "Reading 8", @@ -125,7 +115,6 @@ "oldfieldtype": "Data" }, { - "depends_on": "eval:!doc.single_reading", "fieldname": "reading_9", "fieldtype": "Data", "label": "Reading 9", @@ -133,7 +122,6 @@ "oldfieldtype": "Data" }, { - "depends_on": "eval:!doc.single_reading", "fieldname": "reading_10", "fieldtype": "Data", "label": "Reading 10", @@ -152,7 +140,7 @@ "options": "Accepted\nRejected" }, { - "depends_on": "value_based", + "depends_on": "non_numeric", "fieldname": "section_break_3", "fieldtype": "Section Break", "label": "Value Based Inspection" @@ -163,7 +151,7 @@ }, { "depends_on": "formula_based_criteria", - "description": "Simple Python formula applied on Reading fields.
    Numeric eg.: reading_1 > 0.2 and reading_1 < 0.5
    \nValue based eg.: reading_value in (\"A\", \"B\", \"C)", + "description": "Simple Python formula applied on Reading fields.
    Numeric eg. 1: reading_1 > 0.2 and reading_1 < 0.5
    \nNumeric eg. 2: mean > 3.5 (mean of populated fields)
    \nValue based eg.: reading_value in (\"A\", \"B\", \"C)", "fieldname": "acceptance_formula", "fieldtype": "Code", "label": "Acceptance Criteria Formula" @@ -183,61 +171,42 @@ "label": "Formula Based Criteria" }, { - "depends_on": "eval:(!doc.formula_based_criteria && !doc.single_reading && !doc.value_based)", - "fieldname": "mean_value", - "fieldtype": "Float", - "label": "Mean Value" - }, - { - "default": "0", - "fieldname": "single_reading", - "fieldtype": "Check", - "label": "Single Reading" - }, - { - "depends_on": "eval:!doc.value_based", - "fieldname": "section_break_12", - "fieldtype": "Section Break", - "hide_border": 1 - }, - { - "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)", + "depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)", "description": "Applied on each reading.", "fieldname": "min_value", "fieldtype": "Float", "label": "Minimum Value" }, { - "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)", + "depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)", "description": "Applied on each reading.", "fieldname": "max_value", "fieldtype": "Float", "label": "Maximum Value" }, { - "default": "0", - "description": "Non-numeric Inspection.", - "fieldname": "value_based", - "fieldtype": "Check", - "label": "Value Based" - }, - { - "depends_on": "value_based", + "depends_on": "non_numeric", "fieldname": "reading_value", "fieldtype": "Data", "label": "Reading Value" }, { - "depends_on": "eval:!doc.value_based", + "depends_on": "eval:!doc.non_numeric", "fieldname": "section_break_14", "fieldtype": "Section Break", "label": "Numeric Inspection" + }, + { + "default": "0", + "fieldname": "non_numeric", + "fieldtype": "Check", + "label": "Non-Numeric" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-12-18 21:02:04.865777", + "modified": "2020-12-21 11:36:24.885019", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection Reading", diff --git a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py index 7dd0febc203..c5a7974a732 100644 --- a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py +++ b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py @@ -14,7 +14,6 @@ def get_template_details(template): return frappe.get_all('Item Quality Inspection Parameter', fields=["specification", "value", "acceptance_formula", - "value_based", "formula_based_criteria", "single_reading", - "min_value", "max_value", "mean_value"], + "non_numeric", "formula_based_criteria", "min_value", "max_value"], filters={'parenttype': 'Quality Inspection Template', 'parent': template}, order_by="idx") \ No newline at end of file From 68f91c96400226254875016d0e0b95bdc3816580 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 21 Dec 2020 12:24:45 +0530 Subject: [PATCH 118/295] chore: Added tests for new ux - Test for value based inspection - tweaks in test for formula based inspection - tweaks in create_quality_inspection as status in child row is auto set now --- .../test_quality_inspection.py | 59 ++++++++++++++++--- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index 2c40009426e..d0bfb466e05 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -44,24 +44,61 @@ class TestQualityInspection(unittest.TestCase): qa.delete() dn.delete() + def test_value_based_qi_readings(self): + # Test QI based on acceptance values (Non formula) + dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) + readings = [{ + "specification": "Iron Content", # numeric reading + "min_value": 0.1, + "max_value": 0.9, + "reading_1": "0.4" + }, + { + "specification": "Particle Inspection Needed", # non-numeric reading + "non_numeric": 1, + "value": "Yes", + "reading_value": "Yes" + }] + + qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, + readings=readings, do_not_save=True) + qa.save() + + # status must be auto set as per formula + self.assertEqual(qa.readings[0].status, "Accepted") + self.assertEqual(qa.readings[1].status, "Accepted") + + qa.delete() + dn.delete() + def test_formula_based_qi_readings(self): dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) readings = [{ - "specification": "Iron Content", + "specification": "Iron Content", # numeric reading + "formula_based_criteria": 1, "acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50", - "reading_1": 0.4 + "reading_1": "0.4" }, { - "specification": "Calcium Content", + "specification": "Calcium Content", # numeric reading + "formula_based_criteria": 1, "acceptance_formula": "reading_1 > 0.20 and reading_1 < 0.50", - "reading_1": 0.7 + "reading_1": "0.7" }, { - "specification": "Mg Content", - "acceptance_formula": "(reading_1 + reading_2 + reading_3) / 3 < 0.9", - "reading_1": 0.5, - "reading_2": 0.7, + "specification": "Mg Content", # numeric reading + "formula_based_criteria": 1, + "acceptance_formula": "mean < 0.9", + "reading_1": "0.5", + "reading_2": "0.7", "reading_3": "random text" # check if random string input causes issues + }, + { + "specification": "Calcium Content", # non-numeric reading + "formula_based_criteria": 1, + "non_numeric": 1, + "acceptance_formula": "reading_value in ('Grade A', 'Grade B', 'Grade C')", + "reading_value": "Grade B" }] qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, @@ -72,6 +109,7 @@ class TestQualityInspection(unittest.TestCase): self.assertEqual(qa.readings[0].status, "Accepted") self.assertEqual(qa.readings[1].status, "Rejected") self.assertEqual(qa.readings[2].status, "Accepted") + self.assertEqual(qa.readings[3].status, "Accepted") qa.delete() dn.delete() @@ -86,8 +124,11 @@ def create_quality_inspection(**args): qa.item_code = args.item_code or "_Test Item with QA" qa.sample_size = 1 qa.inspected_by = frappe.session.user + qa.status = args.status or "Accepted" - readings = args.readings or {"specification": "Size", "status": args.status} + readings = args.readings or {"specification": "Size", "min_value": 0, "max_value": 10} + if args.status == "Rejected": + readings["reading_1"] = "12" # status is auto set in child on save if isinstance(readings, list): for entry in readings: From 0e222173ea431b46344f7b73866390c15e80106e Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 21 Dec 2020 13:44:03 +0530 Subject: [PATCH 119/295] fix: don't set primary action if workflow is set --- erpnext/payroll/doctype/payroll_entry/payroll_entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index cb48abbc363..31abaf40bf2 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -39,7 +39,7 @@ frappe.ui.form.on('Payroll Entry', { } ).toggleClass('btn-primary', !(frm.doc.employees || []).length); } - if ((frm.doc.employees || []).length) { + if ((frm.doc.employees || []).length && !frappe.model.has_workflow(frm.doctype)) { frm.page.clear_primary_action(); frm.page.set_primary_action(__('Create Salary Slips'), () => { frm.save('Submit').then(()=>{ From eae31f02cc1a5254292b7c621513e70b91d10b22 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 21 Dec 2020 13:58:44 +0530 Subject: [PATCH 120/295] fix: Sider (missing semi-colons) --- .../doctype/quality_inspection/quality_inspection.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js index f0bf9aed802..2ec8a070052 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js @@ -10,18 +10,18 @@ frappe.ui.form.on("Quality Inspection", { filters: { "item": frm.doc.item_code } - } + }; }); // Serial No based on item_code frm.set_query("item_serial_no", function() { - var filters = {}; + let filters = {}; if (frm.doc.item_code) { filters = { 'item_code': frm.doc.item_code - } + }; } - return { filters: filters } + return { filters: filters }; }); // item code based on GRN/DN @@ -75,4 +75,4 @@ frappe.ui.form.on("Quality Inspection", { }); } }, -}) \ No newline at end of file +}); \ No newline at end of file From a77b8c9fcc1ed7a2a44d2cdf3a3b50c5d8a4366f Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 21 Dec 2020 14:45:50 +0530 Subject: [PATCH 121/295] Repost item valuation (#24031) * feat: Reposting logic for future finished/transferred item * feat: added fields to identify needs to recalculate rate while reposting * refactor: Set rate for outgoing and finished items * refactor: Arranged fields in Stock Entry item table and added fields to identify finished and scrap item * refactor: Arranged fields in Stock Entry item table and added fields to identify finished and scrap item * refactor: Get outgoing rate for purchase return * refactor: Get incoming rate for sales return * test: Added tests for reposting valuation of transferred/finished/returned items * feat: added incoming rate field in DN, SI and Packed Item table * feat: get incoming rate for returned item * fix: no error while getting valuation rate in stock entry * fix: update stock ledger for DN and SI * feat: update item valuation rate in PR and PI based on supplied items cost * feat: SLE reposting logic for sales return and subcontracted item with test cases * feat: update qty in future sle * feat: repost future sle and gle via Repost Item Valuation * fix: Skip unwanted function calling while reposting * fix: repost sle for specific item and warehouse * test: Modified tests for backdated stock reco * fix: ignore cancelled sle in few methods * feat: role allowed to do backdated entry * feat: Show reposting status on stock valuation related reports * fix: minor fixes * fix: fixed sider issues * fix: serial no fix related to immutable ledger * fix: Test cases fixes related to perpetual inventory * fix: Test cases fixed * fix: Fixed reposting on cancel and test cases * feat: Restart reposting item valuation * refactor: Code cleanup using small functions and test case fixes * fix: minor fixes * fix: Raise on error while reposting item valuation * fix: minor fix * fix: Tests fixed * fix: skip some validation ig gle made from reposting * fix: test fixes * fix: debugging stock and account validation * fix: debugging stock and account validation * fix: debugging travis for stock and account sync validation * fix: debugging travis * fix: debugging travis * fix: debugging travis --- .../accounts/doctype/account/test_account.py | 2 +- .../doctype/coupon_code/test_coupon_code.py | 50 +- erpnext/accounts/doctype/gl_entry/gl_entry.py | 24 +- .../journal_entry/test_journal_entry.py | 68 +- .../loyalty_program/test_loyalty_program.py | 2 - .../purchase_invoice/purchase_invoice.py | 19 +- .../purchase_invoice/test_purchase_invoice.py | 72 +-- .../doctype/sales_invoice/sales_invoice.py | 17 +- .../doctype/sales_invoice/test_records.json | 3 +- .../sales_invoice/test_sales_invoice.py | 128 ++-- .../sales_invoice_item.json | 30 +- erpnext/accounts/general_ledger.py | 38 +- erpnext/accounts/utils.py | 24 +- .../purchase_order_item.json | 2 +- erpnext/controllers/buying_controller.py | 129 ++-- .../controllers/sales_and_purchase_return.py | 42 ++ erpnext/controllers/selling_controller.py | 116 ++-- erpnext/controllers/stock_controller.py | 101 +-- .../doctype/work_order/test_work_order.py | 2 - .../sales_order_item/sales_order_item.json | 2 +- .../setup/doctype/company/test_records.json | 18 +- erpnext/stock/doctype/batch/test_batch.py | 5 - erpnext/stock/doctype/bin/bin.py | 14 +- .../doctype/delivery_note/delivery_note.py | 4 +- .../delivery_note/test_delivery_note.py | 7 +- .../delivery_note_item.json | 11 +- erpnext/stock/doctype/item/test_records.json | 10 + .../item_alternative/test_item_alternative.py | 2 - .../landed_cost_taxes_and_charges.json | 8 +- .../landed_cost_voucher.py | 18 +- .../test_landed_cost_voucher.py | 12 +- .../material_request/test_material_request.py | 3 - .../doctype/packed_item/packed_item.json | 18 +- .../purchase_receipt/purchase_receipt.py | 4 +- .../purchase_receipt/test_purchase_receipt.py | 188 +++--- .../purchase_receipt_item.json | 2 +- .../doctype/repost_item_valuation/__init__.py | 0 .../repost_item_valuation.js | 52 ++ .../repost_item_valuation.json | 215 +++++++ .../repost_item_valuation.py | 89 +++ .../test_repost_item_valuation.py | 10 + erpnext/stock/doctype/serial_no/serial_no.py | 33 +- .../stock/doctype/serial_no/test_serial_no.py | 3 - .../stock/doctype/stock_entry/stock_entry.js | 25 +- .../doctype/stock_entry/stock_entry.json | 3 +- .../stock/doctype/stock_entry/stock_entry.py | 303 +++++---- .../doctype/stock_entry/test_stock_entry.py | 83 +-- .../stock_entry_detail.json | 74 ++- .../stock_ledger_entry.json | 62 +- .../stock_ledger_entry/stock_ledger_entry.py | 45 +- .../test_stock_ledger_entry.py | 395 +++++++++++- .../stock_reconciliation.py | 4 +- .../test_stock_reconciliation.py | 47 +- .../stock_settings/stock_settings.json | 31 +- .../stock/doctype/warehouse/test_warehouse.py | 61 +- erpnext/stock/doctype/warehouse/warehouse.py | 1 - .../report/stock_analytics/stock_analytics.py | 2 + .../report/stock_balance/stock_balance.py | 3 +- .../stock/report/stock_ledger/stock_ledger.py | 3 +- .../stock_projected_qty.py | 3 +- ...rehouse_wise_item_balance_age_and_value.py | 2 + erpnext/stock/stock_balance.py | 18 +- erpnext/stock/stock_ledger.py | 597 +++++++++++++----- erpnext/stock/utils.py | 11 +- 64 files changed, 2336 insertions(+), 1034 deletions(-) create mode 100644 erpnext/stock/doctype/repost_item_valuation/__init__.py create mode 100644 erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js create mode 100644 erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json create mode 100644 erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py create mode 100644 erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py index 0605d89a7e2..113bea00645 100644 --- a/erpnext/accounts/doctype/account/test_account.py +++ b/erpnext/accounts/doctype/account/test_account.py @@ -172,7 +172,7 @@ class TestAccount(unittest.TestCase): frappe.delete_doc("Account", doc) -def _make_test_records(verbose): +def _make_test_records(verbose=None): from frappe.test_runner import make_test_objects accounts = [ diff --git a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py index 340b9dd58ad..622bd33e20a 100644 --- a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py +++ b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py @@ -28,22 +28,22 @@ def test_create_test_data(): "item_group": "_Test Item Group", "item_name": "_Test Tesla Car", "apply_warehouse_wise_reorder_level": 0, - "warehouse":"Stores - TCP1", + "warehouse":"Stores - _TC", "gst_hsn_code": "999800", "valuation_rate": 5000, "standard_rate":5000, "item_defaults": [{ - "company": "_Test Company with perpetual inventory", - "default_warehouse": "Stores - TCP1", + "company": "_Test Company", + "default_warehouse": "Stores - _TC", "default_price_list":"_Test Price List", - "expense_account": "Cost of Goods Sold - TCP1", - "buying_cost_center": "Main - TCP1", - "selling_cost_center": "Main - TCP1", - "income_account": "Sales - TCP1" + "expense_account": "Cost of Goods Sold - _TC", + "buying_cost_center": "Main - _TC", + "selling_cost_center": "Main - _TC", + "income_account": "Sales - _TC" }], "show_in_website": 1, "route":"-test-tesla-car", - "website_warehouse": "Stores - TCP1" + "website_warehouse": "Stores - _TC" }) item.insert() # create test item price @@ -65,12 +65,12 @@ def test_create_test_data(): "items": [{ "item_code": "_Test Tesla Car" }], - "warehouse":"Stores - TCP1", + "warehouse":"Stores - _TC", "coupon_code_based":1, "selling": 1, "rate_or_discount": "Discount Percentage", "discount_percentage": 30, - "company": "_Test Company with perpetual inventory", + "company": "_Test Company", "currency":"INR", "for_price_list":"_Test Price List" }) @@ -85,7 +85,7 @@ def test_create_test_data(): }) sales_partner.insert() # create test item coupon code - if not frappe.db.exists("Coupon Code","SAVE30"): + if not frappe.db.exists("Coupon Code", "SAVE30"): coupon_code = frappe.get_doc({ "doctype": "Coupon Code", "coupon_name":"SAVE30", @@ -102,35 +102,27 @@ class TestCouponCode(unittest.TestCase): test_create_test_data() def tearDown(self): - frappe.set_user("Administrator") + frappe.set_user("Administrator") - def test_1_check_coupon_code_used_before_so(self): - coupon_code = frappe.get_doc("Coupon Code", frappe.db.get_value("Coupon Code", {"coupon_name":"SAVE30"})) - # reset used coupon code count - coupon_code.used=0 - coupon_code.save() - # check no coupon code is used before sales order is made - self.assertEqual(coupon_code.get("used"),0) + def test_sales_order_with_coupon_code(self): + frappe.db.set_value("Coupon Code", "SAVE30", "used", 0) - def test_2_sales_order_with_coupon_code(self): - so = make_sales_order(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', - customer="_Test Customer", selling_price_list="_Test Price List", item_code="_Test Tesla Car", rate=5000,qty=1, + so = make_sales_order(company='_Test Company', warehouse='Stores - _TC', + customer="_Test Customer", selling_price_list="_Test Price List", + item_code="_Test Tesla Car", rate=5000, qty=1, do_not_submit=True) - so = frappe.get_doc('Sales Order', so.name) - # check item price before coupon code is applied self.assertEqual(so.items[0].rate, 5000) + so.coupon_code='SAVE30' so.sales_partner='_Test Coupon Partner' so.save() + # check item price after coupon code is applied self.assertEqual(so.items[0].rate, 3500) + so.submit() - - def test_3_check_coupon_code_used_after_so(self): - doc = frappe.get_doc("Coupon Code", frappe.db.get_value("Coupon Code", {"coupon_name":"SAVE30"})) - # check no coupon code is used before sales order is made - self.assertEqual(doc.get("used"),1) + self.assertEqual(frappe.db.get_value("Coupon Code", "SAVE30", "used"), 1) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index def9ed6803e..c4412749080 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -30,20 +30,22 @@ class GLEntry(Document): self.pl_must_have_cost_center() self.validate_cost_center() - self.check_pl_account() - self.validate_party() - self.validate_currency() + if not self.flags.from_repost: + self.check_pl_account() + self.validate_party() + self.validate_currency() - def on_update_with_args(self, adv_adj, update_outstanding = 'Yes'): - self.validate_account_details(adv_adj) - self.validate_dimensions_for_pl_and_bs() + def on_update_with_args(self, adv_adj, update_outstanding = 'Yes', from_repost=False): + if not from_repost: + self.validate_account_details(adv_adj) + self.validate_dimensions_for_pl_and_bs() validate_frozen_account(self.account, adv_adj) validate_balance_type(self.account, adv_adj) # Update outstanding amt on against voucher if self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees'] \ - and self.against_voucher and update_outstanding == 'Yes': + and self.against_voucher and update_outstanding == 'Yes' and not from_repost: update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type, self.against_voucher) @@ -106,8 +108,8 @@ class GLEntry(Document): from tabAccount where name=%s""", self.account, as_dict=1)[0] if ret.is_group==1: - frappe.throw(_('''{0} {1}: Account {2} is a Group Account and group accounts cannot be used in - transactions''').format(self.voucher_type, self.voucher_no, self.account)) + frappe.throw(_('''{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions''') + .format(self.voucher_type, self.voucher_no, self.account)) if ret.docstatus==2: frappe.throw(_("{0} {1}: Account {2} is inactive") @@ -136,8 +138,8 @@ class GLEntry(Document): .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))) + 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) diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py index 53c07583d8e..1d2eacdb80c 100644 --- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py @@ -75,54 +75,40 @@ class TestJournalEntry(unittest.TestCase): elif test_voucher.doctype in ["Sales Order", "Purchase Order"]: # if test_voucher is a Sales Order/Purchase Order, test error on cancellation of test_voucher + frappe.db.set_value("Accounts Settings", "Accounts Settings", + "unlink_advance_payment_on_cancelation_of_order", 0) submitted_voucher = frappe.get_doc(test_voucher.doctype, test_voucher.name) self.assertRaises(frappe.LinkExistsError, submitted_voucher.cancel) def test_jv_against_stock_account(self): - from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory - set_perpetual_inventory() + company = "_Test Company with perpetual inventory" + stock_account = get_inventory_account(company) - jv = frappe.copy_doc({ - "cheque_date": nowdate(), - "cheque_no": "33", - "company": "_Test Company with perpetual inventory", - "doctype": "Journal Entry", - "accounts": [ - { - "account": "Debtors - TCP1", - "party_type": "Customer", - "party": "_Test Customer", - "credit_in_account_currency": 400.0, - "debit_in_account_currency": 0.0, - "doctype": "Journal Entry Account", - "parentfield": "accounts", - "cost_center": "Main - TCP1" - }, - { - "account": "_Test Bank - TCP1", - "credit_in_account_currency": 0.0, - "debit_in_account_currency": 400.0, - "doctype": "Journal Entry Account", - "parentfield": "accounts", - "cost_center": "Main - TCP1" - } - ], - "naming_series": "_T-Journal Entry-", - "posting_date": nowdate(), - "user_remark": "test", - "voucher_type": "Bank Entry" - }) - - jv.get("accounts")[0].update({ - "account": get_inventory_account('_Test Company with perpetual inventory'), - "company": "_Test Company with perpetual inventory", - "party_type": None, - "party": None + jv = frappe.new_doc("Journal Entry") + jv.company = company + jv.posting_date = nowdate() + jv.append("accounts", { + "account": stock_account, + "cost_center": "Main - TCP1", + "debit_in_account_currency": 100 }) + + jv.append("accounts", { + "account": "Stock Adjustment - TCP1", + "credit_in_account_currency": 100, + "cost_center": "Main - TCP1", + }) + jv.insert() - self.assertRaises(StockAccountInvalidTransaction, jv.submit) - jv.cancel() - set_perpetual_inventory(0) + from erpnext.accounts.utils import get_stock_and_account_balance + account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(stock_account, nowdate(), company) + + if account_bal == stock_bal: + self.assertRaises(StockAccountInvalidTransaction, jv.submit) + frappe.db.rollback() + else: + jv.submit() + jv.cancel() def test_multi_currency(self): jv = make_journal_entry("_Test Bank USD - _TC", diff --git a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py index 5278d8b2412..31994885aa6 100644 --- a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py +++ b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py @@ -8,12 +8,10 @@ import unittest from frappe.utils import today, cint, flt, getdate from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details_with_points from erpnext.accounts.party import get_dashboard_info -from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory class TestLoyaltyProgram(unittest.TestCase): @classmethod def setUpClass(self): - set_perpetual_inventory(0) # create relevant item, customer, loyalty program, etc create_records() diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index d94d261c6bc..b52678e8d3b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -410,10 +410,13 @@ class PurchaseInvoice(BuyingController): # this sequence because outstanding may get -negative self.make_gl_entries() + if self.update_stock == 1: + self.repost_future_sle_and_gle() + self.update_project() update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) - def make_gl_entries(self, gl_entries=None): + def make_gl_entries(self, gl_entries=None, from_repost=False): if not gl_entries: gl_entries = self.get_gl_entries() @@ -421,7 +424,7 @@ class PurchaseInvoice(BuyingController): update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes" if self.docstatus == 1: - make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False) + make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False, from_repost=from_repost) elif self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) @@ -436,9 +439,11 @@ class PurchaseInvoice(BuyingController): self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company) if self.auto_accounting_for_stock: self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed") + self.expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") else: self.stock_received_but_not_billed = None - self.expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") + self.expenses_included_in_valuation = None + self.negative_expense_to_be_booked = 0.0 gl_entries = [] @@ -452,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) @@ -994,11 +999,15 @@ 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') unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference) - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') def update_project(self): project_list = [] diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index f2499d24b5b..c0506ba97f6 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -9,8 +9,7 @@ import frappe.model from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from frappe.utils import cint, flt, today, nowdate, add_days, getdate import frappe.defaults -from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory, \ - test_records as pr_test_records, make_purchase_receipt, get_taxes +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt, get_taxes from erpnext.controllers.accounts_controller import get_payment_terms from erpnext.exceptions import InvalidCurrency from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction @@ -33,13 +32,10 @@ class TestPurchaseInvoice(unittest.TestCase): def test_gl_entries_without_perpetual_inventory(self): frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC") - wrapper = frappe.copy_doc(test_records[0]) - set_perpetual_inventory(0, wrapper.company) - self.assertTrue(not cint(erpnext.is_perpetual_inventory_enabled(wrapper.company))) - wrapper.insert() - wrapper.submit() - wrapper.load_from_db() - dl = wrapper + pi = frappe.copy_doc(test_records[0]) + self.assertTrue(not cint(erpnext.is_perpetual_inventory_enabled(pi.company))) + pi.insert() + pi.submit() expected_gl_entries = { "_Test Payable - _TC": [0, 1512.0], @@ -54,12 +50,16 @@ class TestPurchaseInvoice(unittest.TestCase): "Round Off - _TC": [0, 0.3] } gl_entries = frappe.db.sql("""select account, debit, credit from `tabGL Entry` - where voucher_type = 'Purchase Invoice' and voucher_no = %s""", dl.name, as_dict=1) + where voucher_type = 'Purchase Invoice' and voucher_no = %s""", pi.name, as_dict=1) for d in gl_entries: self.assertEqual([d.debit, d.credit], expected_gl_entries.get(d.account)) def test_gl_entries_with_perpetual_inventory(self): - pi = make_purchase_invoice(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1", get_taxes_and_charges=True, qty=10) + pi = make_purchase_invoice(company="_Test Company with perpetual inventory", + warehouse= "Stores - TCP1", cost_center = "Main - TCP1", + expense_account ="_Test Account Cost for Goods Sold - TCP1", + get_taxes_and_charges=True, qty=10) + self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pi.company)), 1) self.check_gle_for_pi(pi.name) @@ -198,8 +198,6 @@ class TestPurchaseInvoice(unittest.TestCase): pr = make_purchase_receipt(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", get_taxes_and_charges=True,) - self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pr.company)), 1) - pi = make_purchase_invoice(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1", get_taxes_and_charges=True, qty=10,do_not_save= "True") for d in pi.items: @@ -247,17 +245,11 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertRaises(frappe.CannotChangeConstantError, pi.save) - def test_gl_entries_with_aia_for_non_stock_items(self): - pi = frappe.copy_doc(test_records[1]) - set_perpetual_inventory(1, pi.company) - self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pi.company)), 1) - pi.get("items")[0].item_code = "_Test Non Stock Item" - pi.get("items")[0].expense_account = "_Test Account Cost for Goods Sold - _TC" - pi.get("taxes").pop(0) - pi.get("taxes").pop(1) - pi.insert() - pi.submit() - pi.load_from_db() + def test_gl_entries_for_non_stock_items_with_perpetual_inventory(self): + pi = make_purchase_invoice(item_code = "_Test Non Stock Item", + company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1", + cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1") + self.assertTrue(pi.status, "Unpaid") gl_entries = frappe.db.sql("""select account, debit, credit @@ -265,17 +257,15 @@ class TestPurchaseInvoice(unittest.TestCase): order by account asc""", pi.name, as_dict=1) self.assertTrue(gl_entries) - expected_values = sorted([ - ["_Test Payable - _TC", 0, 620], - ["_Test Account Cost for Goods Sold - _TC", 500.0, 0], - ["_Test Account VAT - _TC", 120.0, 0], - ]) + expected_values = [ + ["_Test Account Cost for Goods Sold - TCP1", 250.0, 0], + ["Creditors - TCP1", 0, 250] + ] for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[i][0], gle.account) self.assertEqual(expected_values[i][1], gle.debit) self.assertEqual(expected_values[i][2], gle.credit) - set_perpetual_inventory(0, pi.company) def test_purchase_invoice_calculation(self): pi = frappe.copy_doc(test_records[0]) @@ -457,12 +447,13 @@ class TestPurchaseInvoice(unittest.TestCase): pi.cancel() self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), existing_purchase_cost) - def test_return_purchase_invoice(self): - set_perpetual_inventory() + def test_return_purchase_invoice_with_perpetual_inventory(self): + pi = make_purchase_invoice(company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1", + cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1") - pi = make_purchase_invoice() - - return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2) + return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2, + company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1", + cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1") # check gl entries for return @@ -473,19 +464,15 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertTrue(gl_entries) expected_values = { - "Creditors - _TC": [100.0, 0.0], - "Stock Received But Not Billed - _TC": [0.0, 100.0], + "Creditors - TCP1": [100.0, 0.0], + "Stock Received But Not Billed - TCP1": [0.0, 100.0], } for gle in gl_entries: self.assertEqual(expected_values[gle.account][0], gle.debit) self.assertEqual(expected_values[gle.account][1], gle.credit) - set_perpetual_inventory(0) - def test_multi_currency_gle(self): - set_perpetual_inventory(0) - pi = make_purchase_invoice(supplier="_Test Supplier USD", credit_to="_Test Payable USD - _TC", currency="USD", conversion_rate=50) @@ -640,10 +627,9 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(len(pi.get("supplied_items")), 2) rm_supp_cost = sum([d.amount for d in pi.get("supplied_items")]) - self.assertEqual(pi.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2)) + self.assertEqual(flt(pi.get("items")[0].rm_supp_cost, 2), flt(rm_supp_cost, 2)) def test_rejected_serial_no(self): - set_perpetual_inventory(0) pi = make_purchase_invoice(item_code="_Test Serialized Item With Series", received_qty=2, qty=1, rejected_qty=1, rate=500, update_stock=1, rejected_warehouse = "_Test Rejected Warehouse - _TC") diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index ca6f22cc30b..50734c865cd 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -179,6 +179,9 @@ class SalesInvoice(SellingController): # this sequence because outstanding may get -ve self.make_gl_entries() + + if self.update_stock == 1: + self.repost_future_sle_and_gle() if not self.is_return: self.update_billing_status_for_zero_amount_refdoc("Delivery Note") @@ -258,6 +261,10 @@ class SalesInvoice(SellingController): self.update_stock_ledger() self.make_gl_entries_on_cancel() + + if self.update_stock == 1: + self.repost_future_sle_and_gle() + frappe.db.set(self, 'status', 'Cancelled') if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction": @@ -279,7 +286,7 @@ class SalesInvoice(SellingController): if "Healthcare" in active_domains: manage_invoice_submit_cancel(self, "on_cancel") - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') def update_status_updater_args(self): if cint(self.update_stock): @@ -722,22 +729,20 @@ class SalesInvoice(SellingController): if d.delivery_note and frappe.db.get_value("Delivery Note", d.delivery_note, "docstatus") != 1: throw(_("Delivery Note {0} is not submitted").format(d.delivery_note)) - def make_gl_entries(self, gl_entries=None): - from erpnext.accounts.general_ledger import make_reverse_gl_entries + def make_gl_entries(self, gl_entries=None, from_repost=False): + from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company) if not gl_entries: gl_entries = self.get_gl_entries() if gl_entries: - from erpnext.accounts.general_ledger import make_gl_entries - # if POS and amount is written off, updating outstanding amt after posting all gl entries update_outstanding = "No" if (cint(self.is_pos) or self.write_off_account or cint(self.redeem_loyalty_points)) else "Yes" if self.docstatus == 1: - make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False) + make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False, from_repost=from_repost) elif self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) diff --git a/erpnext/accounts/doctype/sales_invoice/test_records.json b/erpnext/accounts/doctype/sales_invoice/test_records.json index 11ebe6a573a..ee6419db20a 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_records.json +++ b/erpnext/accounts/doctype/sales_invoice/test_records.json @@ -17,7 +17,8 @@ "description": "138-CMS Shoe", "doctype": "Sales Invoice Item", "income_account": "Sales - _TC", - "expense_account": "_Test Account Cost for Goods Sold - _TC", + "expense_account": "_Test Account Cost for Goods Sold - _TC", + "item_code": "138-CMS Shoe", "item_name": "138-CMS Shoe", "parentfield": "items", "qty": 1.0, diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 22a4f336547..ceb79079893 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -10,7 +10,6 @@ from frappe.model.dynamic_links import get_dynamic_link_map from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile -from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError from frappe.model.naming import make_autoname @@ -659,7 +658,6 @@ class TestSalesInvoice(unittest.TestCase): def test_sales_invoice_gl_entry_without_perpetual_inventory(self): si = frappe.copy_doc(test_records[1]) - set_perpetual_inventory(0, si.company) si.insert() si.submit() @@ -815,7 +813,6 @@ class TestSalesInvoice(unittest.TestCase): frappe.db.sql("delete from `tabPOS Profile`") def test_pos_si_without_payment(self): - set_perpetual_inventory() make_pos_profile() pos = copy.deepcopy(test_records[1]) @@ -829,9 +826,8 @@ class TestSalesInvoice(unittest.TestCase): self.assertRaises(frappe.ValidationError, si.submit) def test_sales_invoice_gl_entry_with_perpetual_inventory_no_item_code(self): - set_perpetual_inventory() - - si = frappe.get_doc(test_records[1]) + si = create_sales_invoice(company="_Test Company with perpetual inventory", debit_to = "Debtors - TCP1", + income_account="Sales - TCP1", cost_center = "Main - TCP1", do_not_save=True) si.get("items")[0].item_code = None si.insert() si.submit() @@ -842,24 +838,16 @@ class TestSalesInvoice(unittest.TestCase): self.assertTrue(gl_entries) expected_values = dict((d[0], d) for d in [ - [si.debit_to, 630.0, 0.0], - [test_records[1]["items"][0]["income_account"], 0.0, 500.0], - [test_records[1]["taxes"][0]["account_head"], 0.0, 80.0], - [test_records[1]["taxes"][1]["account_head"], 0.0, 50.0], + ["Debtors - TCP1", 100.0, 0.0], + ["Sales - TCP1", 0.0, 100.0] ]) for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account][0], gle.account) self.assertEqual(expected_values[gle.account][1], gle.debit) self.assertEqual(expected_values[gle.account][2], gle.credit) - set_perpetual_inventory(0) - def test_sales_invoice_gl_entry_with_perpetual_inventory_non_stock_item(self): - set_perpetual_inventory() - si = frappe.get_doc(test_records[1]) - si.get("items")[0].item_code = "_Test Non Stock Item" - si.insert() - si.submit() + si = create_sales_invoice(item="_Test Non Stock Item") gl_entries = frappe.db.sql("""select account, debit, credit from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s @@ -867,17 +855,14 @@ class TestSalesInvoice(unittest.TestCase): self.assertTrue(gl_entries) expected_values = dict((d[0], d) for d in [ - [si.debit_to, 630.0, 0.0], - [test_records[1]["items"][0]["income_account"], 0.0, 500.0], - [test_records[1]["taxes"][0]["account_head"], 0.0, 80.0], - [test_records[1]["taxes"][1]["account_head"], 0.0, 50.0], + [si.debit_to, 100.0, 0.0], + [test_records[1]["items"][0]["income_account"], 0.0, 100.0] ]) for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account][0], gle.account) self.assertEqual(expected_values[gle.account][1], gle.debit) self.assertEqual(expected_values[gle.account][2], gle.credit) - set_perpetual_inventory(0) def _insert_purchase_receipt(self): from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import test_records \ @@ -1106,7 +1091,6 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(si.grand_total, 859.43) def test_multi_currency_gle(self): - set_perpetual_inventory(0) si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", currency="USD", conversion_rate=50) @@ -1776,64 +1760,69 @@ class TestSalesInvoice(unittest.TestCase): si.submit() target_doc = make_inter_company_transaction("Sales Invoice", si.name) + target_doc.items[0].update({ + "expense_account": "Cost of Goods Sold - _TC1", + "cost_center": "Main - _TC1", + "warehouse": "Stores - _TC1" + }) target_doc.submit() 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() + # si.submit() - 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() + # 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() - si_gl_entries = [ - ["_Test Account Excise Duty - TCP1", 0.0, 12.0, nowdate()], - ["Unrealized Profit - TCP1", 12.0, 0.0, nowdate()] - ] + # si_gl_entries = [ + # ["_Test Account Excise Duty - TCP1", 0.0, 12.0, nowdate()], + # ["Unrealized Profit - TCP1", 12.0, 0.0, nowdate()] + # ] - check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1)) + # check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1)) - pi_gl_entries = [ - ["_Test Account Excise Duty - TCP1", 12.0 , 0.0, nowdate()], - ["Unrealized Profit - TCP1", 0.0, 12.0, nowdate()] - ] + # pi_gl_entries = [ + # ["_Test Account Excise Duty - TCP1", 12.0 , 0.0, nowdate()], + # ["Unrealized Profit - TCP1", 0.0, 12.0, nowdate()] + # ] - check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1)) + # check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1)) def test_eway_bill_json(self): if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'): @@ -1991,14 +1980,19 @@ def create_sales_invoice(**args): si.append("items", { "item_code": args.item or args.item_code or "_Test Item", + "item_name": args.item_name or "_Test Item", + "description": args.description or "_Test Item", "gst_hsn_code": "999800", "warehouse": args.warehouse or "_Test Warehouse - _TC", "qty": args.qty or 1, + "uom": args.uom or "Nos", + "stock_uom": args.uom or "Nos", "rate": args.rate if args.get("rate") is not None else 100, "income_account": args.income_account or "Sales - _TC", "expense_account": args.expense_account or "Cost of Goods Sold - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC", - "serial_no": args.serial_no + "serial_no": args.serial_no, + "conversion_factor": 1 }) if not args.do_not_save: 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 fb3dd6a92a1..36950757989 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-06-04 11:02:19", "doctype": "DocType", @@ -51,6 +52,7 @@ "column_break_24", "base_net_rate", "base_net_amount", + "incoming_rate", "drop_ship", "delivered_by_supplier", "accounting", @@ -792,20 +794,28 @@ "options": "Project" }, { - "depends_on": "eval:parent.update_stock == 1", - "fieldname": "sales_invoice_item", - "fieldtype": "Data", - "ignore_user_permissions": 1, - "label": "Sales Invoice Item", - "no_copy": 1, - "print_hide": 1, - "read_only": 1 - } + "depends_on": "eval:parent.update_stock == 1", + "fieldname": "sales_invoice_item", + "fieldtype": "Data", + "ignore_user_permissions": 1, + "label": "Sales Invoice Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "incoming_rate", + "fieldtype": "Currency", + "label": "Incoming Rate", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-08-20 11:24:41.749986", + "modified": "2020-09-23 19:59:04.879322", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 9a091bf57bc..c7f0c8781c0 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -15,13 +15,13 @@ class ClosedAccountingPeriod(frappe.ValidationError): pass class StockAccountInvalidTransaction(frappe.ValidationError): pass class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError): pass -def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, update_outstanding='Yes'): +def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, update_outstanding='Yes', from_repost=False): if gl_map: if not cancel: validate_accounting_period(gl_map) gl_map = process_gl_map(gl_map, merge_entries) if gl_map and len(gl_map) > 1: - save_entries(gl_map, adv_adj, update_outstanding) + save_entries(gl_map, adv_adj, update_outstanding, from_repost) else: frappe.throw(_("Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction.")) else: @@ -119,8 +119,9 @@ def check_if_in_list(gle, gl_map, dimensions=None): if same_head: return e -def save_entries(gl_map, adv_adj, update_outstanding): - validate_cwip_accounts(gl_map) +def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False): + if not from_repost: + validate_cwip_accounts(gl_map) round_off_debit_credit(gl_map) @@ -128,24 +129,24 @@ def save_entries(gl_map, adv_adj, update_outstanding): check_freezing_date(gl_map[0]["posting_date"], adv_adj) for entry in gl_map: - make_entry(entry, adv_adj, update_outstanding) + make_entry(entry, adv_adj, update_outstanding, from_repost) - # check against budget - validate_expense_against_budget(entry) - - validate_account_for_perpetual_inventory(gl_map) + if not from_repost: + validate_account_for_perpetual_inventory(gl_map) -def make_entry(args, adv_adj, update_outstanding): +def make_entry(args, adv_adj, update_outstanding, from_repost=False): gle = frappe.new_doc("GL Entry") gle.update(args) gle.flags.ignore_permissions = 1 + gle.flags.from_repost = from_repost gle.insert() - gle.run_method("on_update_with_args", adv_adj, update_outstanding) + gle.run_method("on_update_with_args", adv_adj, update_outstanding, from_repost) gle.submit() # check against budget - validate_expense_against_budget(args) + if not from_repost: + validate_expense_against_budget(args) def validate_account_for_perpetual_inventory(gl_map): if cint(erpnext.is_perpetual_inventory_enabled(gl_map[0].company)): @@ -161,7 +162,7 @@ def validate_account_for_perpetual_inventory(gl_map): # Always use current date to get stock and account balance as there can future entries for # other items account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account, - getdate(), gl_map[0].company) + gl_map[0].posting_date, gl_map[0].company) if gl_map[0].voucher_type=="Journal Entry": # In case of Journal Entry, there are no corresponding SL entries, @@ -176,8 +177,8 @@ def validate_account_for_perpetual_inventory(gl_map): currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency")) diff = flt(stock_bal - account_bal, precision) - error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses.").format( - stock_bal, account_bal, frappe.bold(account)) + error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses on {3}.").format( + stock_bal, account_bal, frappe.bold(account), gl_map[0].posting_date) error_resolution = _("Please create adjustment Journal Entry for amount {0} ").format(frappe.bold(diff)) stock_adjustment_account = frappe.db.get_value("Company",gl_map[0].company,"stock_adjustment_account") @@ -185,9 +186,10 @@ def validate_account_for_perpetual_inventory(gl_map): db_or_cr_stock_adjustment_account = ('debit_in_account_currency' if diff < 0 else 'credit_in_account_currency') journal_entry_args = { - 'accounts':[ - {'account': account, db_or_cr_warehouse_account : abs(diff)}, - {'account': stock_adjustment_account, db_or_cr_stock_adjustment_account : abs(diff) }] + 'accounts':[ + {'account': account, db_or_cr_warehouse_account : abs(diff)}, + {'account': stock_adjustment_account, db_or_cr_stock_adjustment_account : abs(diff)} + ] } frappe.msgprint(msg="""{0}

    {1}

    """.format(error_reason, error_resolution), diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 550aaef4040..540ac841823 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -928,7 +928,7 @@ def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for if expected_gle: if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle): _delete_gl_entries(voucher_type, voucher_no) - voucher_obj.make_gl_entries(gl_entries=expected_gle, repost_future_gle=False, from_repost=True) + voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True) else: _delete_gl_entries(voucher_type, voucher_no) @@ -947,7 +947,10 @@ def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, f for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no from `tabStock Ledger Entry` sle - where timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s) {condition} + where + timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s) + and is_cancelled = 0 + {condition} order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""".format(condition=condition), tuple([posting_date, posting_time] + values), as_dict=True): future_stock_vouchers.append([d.voucher_type, d.voucher_no]) @@ -964,3 +967,20 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date): gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d) return gl_entries + +def compare_existing_and_expected_gle(existing_gle, expected_gle): + matched = True + for entry in expected_gle: + account_existed = False + for e in existing_gle: + if entry.account == e.account: + account_existed = True + if entry.account == e.account and entry.against_account == e.against_account \ + and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) \ + and (entry.debit != e.debit or entry.credit != e.credit): + matched = False + break + if not account_existed: + matched = False + break + return matched \ No newline at end of file diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 10db240a446..c691e9f9f85 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -732,7 +732,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-10-30 11:59:47.670951", + "modified": "2020-12-07 11:59:47.670951", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 286c4f44510..dc61870df30 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -16,6 +16,8 @@ from frappe.contacts.doctype.address.address import get_address_display from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.controllers.stock_controller import StockController +from erpnext.controllers.sales_and_purchase_return import get_rate_for_return +from erpnext.stock.utils import get_incoming_rate class BuyingController(StockController): def __setup__(self): @@ -63,7 +65,7 @@ class BuyingController(StockController): self.set_landed_cost_voucher_amount() if self.doctype in ("Purchase Receipt", "Purchase Invoice"): - self.update_valuation_rate("items") + self.update_valuation_rate() def set_missing_values(self, for_validate=False): super(BuyingController, self).set_missing_values(for_validate) @@ -177,7 +179,7 @@ class BuyingController(StockController): self.in_words = money_in_words(amount, self.currency) # update valuation rate - def update_valuation_rate(self, parentfield): + def update_valuation_rate(self, reset_outgoing_rate=True): """ item_tax_amount is the total tax amount applied on that item stored for valuation @@ -188,7 +190,7 @@ class BuyingController(StockController): stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0 last_item_idx = 1 - for d in self.get(parentfield): + for d in self.get("items"): if d.item_code and d.item_code in stock_and_asset_items: stock_and_asset_items_qty += flt(d.qty) stock_and_asset_items_amount += flt(d.base_net_amount) @@ -198,7 +200,7 @@ class BuyingController(StockController): if d.category in ["Valuation", "Valuation and Total"]]) valuation_amount_adjustment = total_valuation_amount - for i, item in enumerate(self.get(parentfield)): + for i, item in enumerate(self.get("items")): if item.item_code and item.qty and item.item_code in stock_and_asset_items: item_proportion = flt(item.base_net_amount) / stock_and_asset_items_amount if stock_and_asset_items_amount \ else flt(item.qty) / stock_and_asset_items_qty @@ -216,16 +218,34 @@ class BuyingController(StockController): item.conversion_factor = get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0 qty_in_stock_uom = flt(item.qty * item.conversion_factor) - rm_supp_cost = flt(item.rm_supp_cost) if self.doctype in ["Purchase Receipt", "Purchase Invoice"] else 0.0 - - landed_cost_voucher_amount = flt(item.landed_cost_voucher_amount) \ - if self.doctype in ["Purchase Receipt", "Purchase Invoice"] else 0.0 - - item.valuation_rate = ((item.base_net_amount + item.item_tax_amount + rm_supp_cost - + landed_cost_voucher_amount) / qty_in_stock_uom) + item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate) + item.valuation_rate = ((item.base_net_amount + item.item_tax_amount + item.rm_supp_cost + + flt(item.landed_cost_voucher_amount)) / qty_in_stock_uom) else: item.valuation_rate = 0.0 + 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"): + if d.reference_name == item_row_id: + if reset_outgoing_rate and frappe.db.get_value('Item', d.rm_item_code, 'is_stock_item'): + rate = get_incoming_rate({ + "item_code": d.rm_item_code, + "warehouse": self.supplier_warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "qty": -1 * d.consumed_qty, + "serial_no": d.serial_no + }) + + if rate > 0: + d.rate = rate + + d.amount = flt(d.consumed_qty) * flt(d.rate) + supplied_items_cost += flt(d.amount) + + return supplied_items_cost + def validate_for_subcontracting(self): if not self.is_subcontracted and self.sub_contracted_items: frappe.throw(_("Please enter 'Is Subcontracted' as Yes or No")) @@ -352,35 +372,17 @@ class BuyingController(StockController): else: self.append_raw_material_to_be_backflushed(item, raw_material, qty) - def append_raw_material_to_be_backflushed(self, fg_item_doc, raw_material_data, qty): + def append_raw_material_to_be_backflushed(self, fg_item_row, raw_material_data, qty): rm = self.append('supplied_items', {}) rm.update(raw_material_data) if not rm.main_item_code: - rm.main_item_code = fg_item_doc.item_code + rm.main_item_code = fg_item_row.item_code - rm.reference_name = fg_item_doc.name + rm.reference_name = fg_item_row.name rm.required_qty = qty rm.consumed_qty = qty - if not raw_material_data.get('non_stock_item'): - from erpnext.stock.utils import get_incoming_rate - rm.rate = get_incoming_rate({ - "item_code": raw_material_data.rm_item_code, - "warehouse": self.supplier_warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "qty": -1 * qty, - "serial_no": rm.serial_no - }) - - if not rm.rate: - rm.rate = get_valuation_rate(raw_material_data.rm_item_code, self.supplier_warehouse, - self.doctype, self.name, currency=self.company_currency, company=self.company) - - rm.amount = qty * flt(rm.rate) - fg_item_doc.rm_supp_cost += rm.amount - def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table): exploded_item = 1 if hasattr(item, 'include_exploded_items'): @@ -389,7 +391,7 @@ class BuyingController(StockController): bom_items = get_items_from_bom(item.item_code, item.bom, exploded_item) used_alternative_items = [] - if self.doctype == 'Purchase Receipt' and item.purchase_order: + if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order: used_alternative_items = get_used_alternative_items(purchase_order = item.purchase_order) raw_materials_cost = 0 @@ -406,7 +408,7 @@ class BuyingController(StockController): reserve_warehouse = None conversion_factor = item.conversion_factor - if (self.doctype == 'Purchase Receipt' and item.purchase_order and + if (self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order and bom_item.item_code in used_alternative_items): alternative_item_data = used_alternative_items.get(bom_item.item_code) bom_item.item_code = alternative_item_data.item_code @@ -434,9 +436,7 @@ class BuyingController(StockController): rm.rm_item_code = bom_item.item_code rm.stock_uom = bom_item.stock_uom rm.required_qty = required_qty - if self.doctype == "Purchase Order" and not rm.reserve_warehouse: - rm.reserve_warehouse = reserve_warehouse - + rm.rate = bom_item.rate rm.conversion_factor = conversion_factor if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: @@ -444,29 +444,8 @@ class BuyingController(StockController): rm.description = bom_item.description if item.batch_no and frappe.db.get_value("Item", rm.rm_item_code, "has_batch_no") and not rm.batch_no: rm.batch_no = item.batch_no - - # get raw materials rate - if self.doctype == "Purchase Receipt": - from erpnext.stock.utils import get_incoming_rate - rm.rate = get_incoming_rate({ - "item_code": bom_item.item_code, - "warehouse": self.supplier_warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "qty": -1 * required_qty, - "serial_no": rm.serial_no - }) - if not rm.rate: - rm.rate = get_valuation_rate(bom_item.item_code, self.supplier_warehouse, - self.doctype, self.name, currency=self.company_currency, company = self.company) - else: - rm.rate = bom_item.rate - - rm.amount = required_qty * flt(rm.rate) - raw_materials_cost += flt(rm.amount) - - if self.doctype in ("Purchase Receipt", "Purchase Invoice"): - item.rm_supp_cost = raw_materials_cost + elif not rm.reserve_warehouse: + rm.reserve_warehouse = reserve_warehouse def cleanup_raw_materials_supplied(self, parent_items, raw_material_table): """Remove all those child items which are no longer present in main item table""" @@ -579,7 +558,8 @@ class BuyingController(StockController): or (cint(self.is_return) and self.docstatus==2)): from_warehouse_sle = self.get_sl_entries(d, { "actual_qty": -1 * pr_qty, - "warehouse": d.from_warehouse + "warehouse": d.from_warehouse, + "dependant_sle_voucher_detail_no": d.name }) sl_entries.append(from_warehouse_sle) @@ -589,28 +569,20 @@ class BuyingController(StockController): "serial_no": cstr(d.serial_no).strip() }) if self.is_return: - filters = { - "voucher_type": self.doctype, - "voucher_no": self.return_against, - "item_code": d.item_code - } - - if (self.doctype == "Purchase Invoice" and self.update_stock - and d.get("purchase_invoice_item")): - filters["voucher_detail_no"] = d.purchase_invoice_item - elif self.doctype == "Purchase Receipt" and d.get("purchase_receipt_item"): - filters["voucher_detail_no"] = d.purchase_receipt_item - - original_incoming_rate = frappe.db.get_value("Stock Ledger Entry", filters, "incoming_rate") + outgoing_rate = get_rate_for_return(self.doctype, self.name, d.item_code, self.return_against, item_row=d) sle.update({ - "outgoing_rate": original_incoming_rate + "outgoing_rate": outgoing_rate, + "recalculate_rate": 1 }) + if d.from_warehouse: + sle.dependant_sle_voucher_detail_no = d.name else: val_rate_db_precision = 6 if cint(self.precision("valuation_rate", d)) <= 6 else 9 incoming_rate = flt(d.valuation_rate, val_rate_db_precision) sle.update({ - "incoming_rate": incoming_rate + "incoming_rate": incoming_rate, + "recalculate_rate": 1 if (self.is_subcontracted and d.bom) or d.from_warehouse else 0 }) sl_entries.append(sle) @@ -618,7 +590,8 @@ class BuyingController(StockController): or (cint(self.is_return) and self.docstatus==1)): from_warehouse_sle = self.get_sl_entries(d, { "actual_qty": -1 * pr_qty, - "warehouse": d.from_warehouse + "warehouse": d.from_warehouse, + "recalculate_rate": 1 }) sl_entries.append(from_warehouse_sle) @@ -666,6 +639,7 @@ class BuyingController(StockController): "item_code": d.rm_item_code, "warehouse": self.supplier_warehouse, "actual_qty": -1*flt(d.consumed_qty), + "dependant_sle_voucher_detail_no": d.reference_name })) def on_submit(self): @@ -857,6 +831,7 @@ class BuyingController(StockController): else: validate_item_type(self, "is_purchase_item", "purchase") + def get_items_from_bom(item_code, bom, exploded_item=1): doctype = "BOM Item" if not exploded_item else "BOM Explosion Item" diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 5299b25601d..8f65c31f3d1 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -365,3 +365,45 @@ def make_return_doc(doctype, source_name, target_doc=None): }, target_doc, set_missing_values) return doclist + +def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None, item_row=None, voucher_detail_no=None): + if not return_against: + return_against = frappe.get_cached_value(voucher_type, voucher_no, "return_against") + + return_against_item_field = get_return_against_item_fields(voucher_type) + + filters = get_filters(voucher_type, voucher_no, voucher_detail_no, + return_against, item_code, return_against_item_field, item_row) + + if voucher_type in ("Purchase Receipt", "Purchase Invoice"): + select_field = "incoming_rate" + else: + select_field = "abs(stock_value_difference / actual_qty)" + + return flt(frappe.db.get_value("Stock Ledger Entry", filters, select_field)) + +def get_return_against_item_fields(voucher_type): + return_against_item_fields = { + "Purchase Receipt": "purchase_receipt_item", + "Purchase Invoice": "purchase_invoice_item", + "Delivery Note": "dn_detail", + "Sales Invoice": "sales_invoice_item" + } + return return_against_item_fields[voucher_type] + +def get_filters(voucher_type, voucher_no, voucher_detail_no, return_against, item_code, return_against_item_field, item_row): + filters = { + "voucher_type": voucher_type, + "voucher_no": return_against, + "item_code": item_code + } + + if item_row: + reference_voucher_detail_no = item_row.get(return_against_item_field) + else: + reference_voucher_detail_no = frappe.db.get_value(voucher_type + " Item", voucher_detail_no, return_against_item_field) + + if reference_voucher_detail_no: + filters["voucher_detail_no"] = reference_voucher_detail_no + + return filters \ No newline at end of file diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 4dbd7bfa186..85cfb951fcc 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -13,6 +13,7 @@ from frappe.contacts.doctype.address.address import get_address_display from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.controllers.stock_controller import StockController +from erpnext.controllers.sales_and_purchase_return import get_rate_for_return class SellingController(StockController): def __setup__(self): @@ -48,6 +49,7 @@ 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): @@ -230,7 +232,8 @@ class SellingController(StockController): 'voucher_type': self.doctype, 'allow_zero_valuation': d.allow_zero_valuation_rate, 'sales_invoice_item': d.get("sales_invoice_item"), - 'delivery_note_item': d.get("dn_detail") + 'dn_detail': d.get("dn_detail"), + 'incoming_rate': p.incoming_rate })) else: il.append(frappe._dict({ @@ -248,7 +251,8 @@ class SellingController(StockController): 'voucher_type': self.doctype, 'allow_zero_valuation': d.allow_zero_valuation_rate, 'sales_invoice_item': d.get("sales_invoice_item"), - 'delivery_note_item': d.get("dn_detail") + 'dn_detail': d.get("dn_detail"), + 'incoming_rate': d.incoming_rate })) return il @@ -307,69 +311,89 @@ 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"): + return + + items = self.get("items") + (self.get("packed_items") or []) + for d in items: + if not cint(self.get("is_return")): + # Get incoming rate based on original item cost based on valuation method + 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, + "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) + elif self.get("return_against"): + # Get incoming rate of return entry from reference document + # based on original item cost as per valuation method + d.incoming_rate = get_rate_for_return(self.doctype, self.name, d.item_code, self.return_against, item_row=d) + def update_stock_ledger(self): self.update_reserved_qty() sl_entries = [] + # Loop over items and packed items table for d in self.get_item_list(): if frappe.get_cached_value("Item", d.item_code, "is_stock_item") == 1 and flt(d.qty): if flt(d.conversion_factor)==0.0: d.conversion_factor = get_conversion_factor(d.item_code, d.uom).get("conversion_factor") or 1.0 - return_rate = 0 - if cint(self.is_return) and self.return_against and self.docstatus==1: - against_document_no = (d.get("sales_invoice_item") - if self.doctype == "Sales Invoice" else d.get("delivery_note_item")) - return_rate = self.get_incoming_rate_for_return(d.item_code, - self.return_against, against_document_no) - - # On cancellation or if return entry submission, make stock ledger entry for + # On cancellation or return entry submission, make stock ledger entry for # target warehouse first, to update serial no values properly if d.warehouse and ((not cint(self.is_return) and self.docstatus==1) or (cint(self.is_return) and self.docstatus==2)): - sl_entries.append(self.get_sl_entries(d, { - "actual_qty": -1*flt(d.qty), - "incoming_rate": return_rate - })) + sl_entries.append(self.get_sle_for_source_warehouse(d)) if d.target_warehouse: - target_warehouse_sle = self.get_sl_entries(d, { - "actual_qty": flt(d.qty), - "warehouse": d.target_warehouse - }) - - if self.docstatus == 1: - if not cint(self.is_return): - args = frappe._dict({ - "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, - "company": d.company, - "voucher_type": d.voucher_type, - "voucher_no": d.name, - "allow_zero_valuation": d.allow_zero_valuation - }) - target_warehouse_sle.update({ - "incoming_rate": get_incoming_rate(args) - }) - else: - target_warehouse_sle.update({ - "outgoing_rate": return_rate - }) - sl_entries.append(target_warehouse_sle) + sl_entries.append(self.get_sle_for_target_warehouse(d)) if d.warehouse and ((not cint(self.is_return) and self.docstatus==2) or (cint(self.is_return) and self.docstatus==1)): - sl_entries.append(self.get_sl_entries(d, { - "actual_qty": -1*flt(d.qty), - "incoming_rate": return_rate - })) + sl_entries.append(self.get_sle_for_source_warehouse(d)) + self.make_sl_entries(sl_entries) + def get_sle_for_source_warehouse(self, item_row): + sle = self.get_sl_entries(item_row, { + "actual_qty": -1*flt(item_row.qty), + "incoming_rate": item_row.incoming_rate, + "recalculate_rate": cint(self.is_return) + }) + if item_row.target_warehouse and not cint(self.is_return): + sle.dependant_sle_voucher_detail_no = item_row.name + + return sle + + def get_sle_for_target_warehouse(self, item_row): + sle = self.get_sl_entries(item_row, { + "actual_qty": flt(item_row.qty), + "warehouse": item_row.target_warehouse + }) + + if self.docstatus == 1: + if not cint(self.is_return): + sle.update({ + "incoming_rate": item_row.incoming_rate, + "recalculate_rate": 1 + }) + else: + sle.update({ + "outgoing_rate": item_row.incoming_rate + }) + if item_row.warehouse: + sle.dependant_sle_voucher_detail_no = item_row.name + + return sle + def set_po_nos(self, for_validate=False): if self.doctype == 'Sales Invoice' and hasattr(self, "items"): if for_validate and self.po_no: @@ -463,4 +487,4 @@ def set_default_income_account_for_item(obj): for d in obj.get("items"): if d.item_code: if getattr(d, "income_account", None): - set_item_default(d.item_code, obj.company, 'income_account', d.income_account) + set_item_default(d.item_code, obj.company, 'income_account', d.income_account) \ No newline at end of file diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 683d7f77b55..51c063c2c0b 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -24,7 +24,7 @@ class StockController(AccountsController): self.validate_serialized_batch() self.validate_customer_provided_item() - def make_gl_entries(self, gl_entries=None): + def make_gl_entries(self, gl_entries=None, from_repost=False): if self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) @@ -34,12 +34,12 @@ class StockController(AccountsController): if self.docstatus==1: if not gl_entries: gl_entries = self.get_gl_entries(warehouse_account) - make_gl_entries(gl_entries) + make_gl_entries(gl_entries, from_repost=from_repost) elif self.doctype in ['Purchase Receipt', 'Purchase Invoice'] and self.docstatus == 1: gl_entries = [] gl_entries = self.get_asset_gl_entry(gl_entries) - make_gl_entries(gl_entries) + make_gl_entries(gl_entries, from_repost=from_repost) def validate_serialized_batch(self): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -70,7 +70,6 @@ class StockController(AccountsController): gl_list = [] 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) @@ -125,7 +124,7 @@ class StockController(AccountsController): if warehouse_with_no_account: for wh in warehouse_with_no_account: if frappe.db.get_value("Warehouse", wh, "company"): - frappe.throw(_("Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}.").format(wh, self.company)) + frappe.throw(_("Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}.").format(wh, self.company)) return process_gl_map(gl_list) @@ -309,23 +308,6 @@ class StockController(AccountsController): return serialized_items - def get_incoming_rate_for_return(self, item_code, against_document, against_document_no=None): - incoming_rate = 0.0 - cond = '' - if against_document and item_code: - if against_document_no: - cond = " and voucher_detail_no = %s" %(frappe.db.escape(against_document_no)) - - incoming_rate = frappe.db.sql("""select abs(stock_value_difference / actual_qty) - from `tabStock Ledger Entry` - where voucher_type = %s and voucher_no = %s - and item_code = %s {0} limit 1""".format(cond), - (self.doctype, against_document, item_code)) - - incoming_rate = incoming_rate[0][0] if incoming_rate else 0.0 - - return incoming_rate - def validate_warehouse(self): from erpnext.stock.utils import validate_warehouse_company @@ -409,19 +391,64 @@ class StockController(AccountsController): if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'): d.allow_zero_valuation_rate = 1 -def compare_existing_and_expected_gle(existing_gle, expected_gle): - matched = True - for entry in expected_gle: - account_existed = False - for e in existing_gle: - if entry.account == e.account: - account_existed = True - if entry.account == e.account and entry.against_account == e.against_account \ - and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) \ - and (entry.debit != e.debit or entry.credit != e.credit): - matched = False - break - if not account_existed: - matched = False + def repost_future_sle_and_gle(self): + args = frappe._dict({ + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "voucher_type": self.doctype, + "voucher_no": self.name, + "company": self.company + }) + + if check_if_future_sle_exists(args): + create_repost_item_valuation_entry(args) + +def check_if_future_sle_exists(args): + sl_entries = frappe.db.get_all("Stock Ledger Entry", + filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no}, + fields=["item_code", "warehouse"], + order_by="creation asc") + + distinct_item_warehouses = list(set([(d.item_code, d.warehouse) for d in sl_entries])) + + sle_exists = False + for item_code, warehouse in distinct_item_warehouses: + args.update({ + "item_code": item_code, + "warehouse": warehouse + }) + if get_sle(args): + sle_exists = True break - return matched + return sle_exists + +def get_sle(args): + return frappe.db.sql(""" + select name + from `tabStock Ledger Entry` + where + item_code=%(item_code)s + and warehouse=%(warehouse)s + and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s) + and voucher_no != %(voucher_no)s + and is_cancelled = 0 + limit 1 + """, args) + +def create_repost_item_valuation_entry(args): + args = frappe._dict(args) + repost_entry = frappe.new_doc("Repost Item Valuation") + repost_entry.based_on = args.based_on + if not args.based_on: + repost_entry.based_on = 'Transaction' if args.voucher_no else "Item and Warehouse" + repost_entry.voucher_type = args.voucher_type + repost_entry.voucher_no = args.voucher_no + repost_entry.item_code = args.item_code + repost_entry.warehouse = args.warehouse + repost_entry.posting_date = args.posting_date + repost_entry.posting_time = args.posting_time + repost_entry.company = args.company + repost_entry.allow_zero_rate = args.allow_zero_rate + repost_entry.flags.ignore_links = True + repost_entry.save() + repost_entry.submit() \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 2bf3fbf75e9..ce9699e1b3c 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -6,7 +6,6 @@ from __future__ import unicode_literals import unittest import frappe from frappe.utils import flt, time_diff_in_hours, now, add_months, cint, today -from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry, ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError, CapacityError) from erpnext.stock.doctype.stock_entry import test_stock_entry @@ -18,7 +17,6 @@ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse class TestWorkOrder(unittest.TestCase): def setUp(self): - set_perpetual_inventory(0) self.warehouse = '_Test Warehouse 2 - _TC' self.item = '_Test Item' diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index eff17f8bc78..159655b74bb 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -785,7 +785,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-05-29 20:54:32.309460", + "modified": "2020-012-07 20:54:32.309460", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/setup/doctype/company/test_records.json b/erpnext/setup/doctype/company/test_records.json index 21302417d2b..9e55702ddc9 100644 --- a/erpnext/setup/doctype/company/test_records.json +++ b/erpnext/setup/doctype/company/test_records.json @@ -7,7 +7,8 @@ "doctype": "Company", "domain": "Manufacturing", "chart_of_accounts": "Standard", - "default_holiday_list": "_Test Holiday List" + "default_holiday_list": "_Test Holiday List", + "enable_perpetual_inventory": 0 }, { "abbr": "_TC1", @@ -17,7 +18,8 @@ "doctype": "Company", "domain": "Retail", "chart_of_accounts": "Standard", - "default_holiday_list": "_Test Holiday List" + "default_holiday_list": "_Test Holiday List", + "enable_perpetual_inventory": 0 }, { "abbr": "_TC2", @@ -27,7 +29,8 @@ "doctype": "Company", "domain": "Retail", "chart_of_accounts": "Standard", - "default_holiday_list": "_Test Holiday List" + "default_holiday_list": "_Test Holiday List", + "enable_perpetual_inventory": 0 }, { "abbr": "_TC3", @@ -38,7 +41,8 @@ "doctype": "Company", "domain": "Manufacturing", "chart_of_accounts": "Standard", - "default_holiday_list": "_Test Holiday List" + "default_holiday_list": "_Test Holiday List", + "enable_perpetual_inventory": 0 }, { "abbr": "_TC4", @@ -50,7 +54,8 @@ "doctype": "Company", "domain": "Manufacturing", "chart_of_accounts": "Standard", - "default_holiday_list": "_Test Holiday List" + "default_holiday_list": "_Test Holiday List", + "enable_perpetual_inventory": 0 }, { "abbr": "_TC5", @@ -61,7 +66,8 @@ "doctype": "Company", "domain": "Manufacturing", "chart_of_accounts": "Standard", - "default_holiday_list": "_Test Holiday List" + "default_holiday_list": "_Test Holiday List", + "enable_perpetual_inventory": 0 }, { "abbr": "TCP1", diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index c2a3d3c151f..e41f1a8aaaf 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -8,13 +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.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory class TestBatch(unittest.TestCase): - - def setUp(self): - set_perpetual_inventory(0) - def test_item_has_batch_enabled(self): self.assertRaises(ValidationError, frappe.get_doc({ "doctype": "Batch", diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 7acdec728b6..ab19b77ad8e 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -16,22 +16,30 @@ class Bin(Document): def update_stock(self, args, allow_negative_stock=False, via_landed_cost_voucher=False): '''Called from erpnext.stock.utils.update_bin''' self.update_qty(args) - if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation": - from erpnext.stock.stock_ledger import update_entries_after + from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle if not args.get("posting_date"): args["posting_date"] = nowdate() + if args.get("is_cancelled") and via_landed_cost_voucher: + return + + # Reposts only current voucher SL Entries + # Updates valuation rate, stock value, stock queue for current transaction update_entries_after({ "item_code": self.item_code, "warehouse": self.warehouse, "posting_date": args.get("posting_date"), "posting_time": args.get("posting_time"), + "voucher_type": args.get("voucher_type"), "voucher_no": args.get("voucher_no"), - "sle_id": args.sle_id + "sle_id": args.name }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) + # Update qty_after_transaction in future SLEs of this item and warehouse + update_qty_in_future_sle(args) + def update_qty(self, args): # update the stock values (for current quantities) if args.get("voucher_type")=="Stock Reconciliation": diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 3f3407e3501..1a6a5550927 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -217,6 +217,7 @@ class DeliveryNote(SellingController): # because updating reserved qty in bin depends upon updated delivered qty in SO self.update_stock_ledger() self.make_gl_entries() + self.repost_future_sle_and_gle() def on_cancel(self): super(DeliveryNote, self).on_cancel() @@ -234,7 +235,8 @@ class DeliveryNote(SellingController): self.cancel_packing_slips() self.make_gl_entries_on_cancel() - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') + self.repost_future_sle_and_gle() + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') def check_credit_limit(self): from erpnext.selling.doctype.customer.customer import check_credit_limit diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 6b4663a688a..559f8be0dea 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -10,8 +10,7 @@ import frappe.defaults from frappe.utils import cint, nowdate, nowtime, cstr, add_days, flt, today from erpnext.stock.stock_ledger import get_previous_sle from erpnext.accounts.utils import get_balance_on -from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \ - import get_gl_entries, set_perpetual_inventory +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice, make_delivery_trip from erpnext.stock.doctype.stock_entry.test_stock_entry \ import make_stock_entry, make_serialized_item, get_qty_after_transaction @@ -24,9 +23,6 @@ from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse from erpnext.stock.doctype.item.test_item import create_item class TestDeliveryNote(unittest.TestCase): - def setUp(self): - set_perpetual_inventory(0) - def test_over_billing_against_dn(self): frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) @@ -43,7 +39,6 @@ class TestDeliveryNote(unittest.TestCase): def test_delivery_note_no_gl_entry(self): company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company') - set_perpetual_inventory(0, company) make_stock_entry(target="_Test Warehouse - _TC", qty=5, basic_rate=100) stock_queue = json.loads(get_previous_sle({ 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 7b471874af7..4bbf3de5940 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -56,6 +56,7 @@ "base_net_rate", "base_net_amount", "billed_amt", + "incoming_rate", "item_weight_details", "weight_per_unit", "total_weight", @@ -732,16 +733,22 @@ "depends_on": "returned_qty", "fieldname": "returned_qty", "fieldtype": "Float", - "label": "Returned Qty in Stock UOM", + "label": "Returned Qty in Stock UOM" + }, + { + "fieldname": "incoming_rate", + "fieldtype": "Currency", + "label": "Incoming Rate", "no_copy": 1, "print_hide": 1, "read_only": 1 } ], "idx": 1, + "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-07-31 20:12:43.054342", + "modified": "2020-12-07 19:59:27.119856", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json index 9ca887c77e3..8f437b13f0d 100644 --- a/erpnext/stock/doctype/item/test_records.json +++ b/erpnext/stock/doctype/item/test_records.json @@ -458,5 +458,15 @@ "item_tax_template": "_Test Item Tax Template 1" } ] + }, + { + "description": "_Test", + "doctype": "Item", + "is_stock_item": 1, + "item_code": "138-CMS Shoe", + "item_group": "_Test Item Group", + "item_name": "138-CMS Shoe", + "stock_uom": "_Test UOM", + "gst_hsn_code": "999800" } ] diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py index f045e4f9114..d5700fe5147 100644 --- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py @@ -12,11 +12,9 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt, make_rm_stock_entry import unittest -from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory class TestItemAlternative(unittest.TestCase): def setUp(self): - set_perpetual_inventory(0) make_items() def test_alternative_item_for_subcontract_rm(self): 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 0cc243d4cb5..64331c7d578 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 @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2014-07-11 11:51:00.453717", "doctype": "DocType", "editable_grid": 1, @@ -31,16 +32,19 @@ "reqd": 1 }, { + "depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))", "fieldname": "expense_account", "fieldtype": "Link", "in_list_view": 1, "label": "Expense Account", + "mandatory_depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))", "options": "Account", - "reqd": 1 + "print_hide": 1 } ], "istable": 1, - "modified": "2019-09-30 18:28:32.070655", + "links": [], + "modified": "2020-12-04 00:22:14.373312", "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Taxes and Charges", 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 bc3d3266add..9ec6b8946cc 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -77,9 +77,9 @@ class LandedCostVoucher(Document): company_currency = erpnext.get_company_currency(self.company) for account in self.taxes: if get_account_currency(account.expense_account) != company_currency: - frappe.throw(msg=_(""" Row {0}: Expense account currency should be same as company's default currency. - Please select expense account with account currency as {1}""") - .format(account.idx, frappe.bold(company_currency)), title=_("Invalid Account 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")]) @@ -121,7 +121,7 @@ class LandedCostVoucher(Document): doc.set_landed_cost_voucher_amount() # set valuation amount in pr item - doc.update_valuation_rate("items") + doc.update_valuation_rate(reset_outgoing_rate=False) # db_update will update and save landed_cost_voucher_amount and voucher_amount in PR for item in doc.get("items"): @@ -143,6 +143,7 @@ class LandedCostVoucher(Document): doc.docstatus = 1 doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True) doc.make_gl_entries() + doc.repost_future_sle_and_gle() def validate_asset_qty_and_status(self, receipt_document_type, receipt_document): for item in self.get('items'): @@ -152,14 +153,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}. \ - Please create or link {1} Assets with respective document.').format(item.receipt_document, 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)) if docs: for d in docs: if d.docstatus == 1: - 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)) + 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))) 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 3f2c5daf669..b97213e4fba 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 @@ -7,7 +7,7 @@ import unittest import frappe from frappe.utils import flt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \ - import set_perpetual_inventory, get_gl_entries, test_records as pr_test_records, make_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 @@ -27,7 +27,7 @@ class TestLandedCostVoucher(unittest.TestCase): }, fieldname=["qty_after_transaction", "stock_value"], as_dict=1) - submit_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) + create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) pr_lc_value = frappe.db.get_value("Purchase Receipt Item", {"parent": pr.name}, "landed_cost_voucher_amount") self.assertEqual(pr_lc_value, 25.0) @@ -89,7 +89,7 @@ class TestLandedCostVoucher(unittest.TestCase): }, fieldname=["qty_after_transaction", "stock_value"], as_dict=1) - submit_landed_cost_voucher("Purchase Invoice", pi.name, pi.company) + create_landed_cost_voucher("Purchase Invoice", pi.name, pi.company) pi_lc_value = frappe.db.get_value("Purchase Invoice Item", {"parent": pi.name}, "landed_cost_voucher_amount") @@ -137,7 +137,7 @@ class TestLandedCostVoucher(unittest.TestCase): serial_no_rate = frappe.db.get_value("Serial No", "SN001", "purchase_rate") - submit_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) + create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) serial_no = frappe.db.get_value("Serial No", "SN001", ["warehouse", "purchase_rate"], as_dict=1) @@ -160,7 +160,7 @@ class TestLandedCostVoucher(unittest.TestCase): }) pr.submit() - lcv = submit_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, 123.22) + 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) @@ -236,7 +236,7 @@ def make_landed_cost_voucher(** args): return lcv -def submit_landed_cost_voucher(receipt_document_type, receipt_document, company, charges=50): +def create_landed_cost_voucher(receipt_document_type, receipt_document, company, charges=50): ref_doc = frappe.get_doc(receipt_document_type, receipt_document) lcv = frappe.new_doc("Landed Cost Voucher") diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 19924b16363..0a29fa05e1a 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -12,9 +12,6 @@ from erpnext.stock.doctype.material_request.material_request \ from erpnext.stock.doctype.item.test_item import create_item class TestMaterialRequest(unittest.TestCase): - def setUp(self): - erpnext.set_perpetual_inventory(0) - def test_make_purchase_order(self): mr = frappe.copy_doc(test_records[0]).insert() diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index 2ac5c426c03..f1d7f8c8c9e 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2013-02-22 01:28:00", "doctype": "DocType", "editable_grid": 1, @@ -14,6 +15,7 @@ "target_warehouse", "column_break_9", "qty", + "uom", "section_break_9", "serial_no", "column_break_11", @@ -23,7 +25,7 @@ "actual_qty", "projected_qty", "column_break_16", - "uom", + "incoming_rate", "page_break", "prevdoc_doctype", "parent_detail_docname" @@ -199,11 +201,21 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "incoming_rate", + "fieldtype": "Currency", + "label": "Incoming Rate", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, + "index_web_pages_for_search": 1, "istable": 1, - "modified": "2019-11-26 20:09:59.400960", + "links": [], + "modified": "2020-09-24 09:25:13.050151", "modified_by": "Administrator", "module": "Stock", "name": "Packed Item", @@ -212,4 +224,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 97e0fa738cd..226064bae78 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -181,6 +181,7 @@ class PurchaseReceipt(BuyingController): update_serial_nos_after_submit(self, "items") self.make_gl_entries() + self.repost_future_sle_and_gle() def check_next_docstatus(self): submit_rv = frappe.db.sql("""select t1.name @@ -209,7 +210,8 @@ class PurchaseReceipt(BuyingController): # because updating ordered qty in bin depends upon updated ordered qty in PO self.update_stock_ledger() self.make_gl_entries_on_cancel() - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') + self.repost_future_sle_and_gle() + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') self.delete_auto_created_batches() def get_current_stock(self): diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 9b8eeed1a12..83012d355ff 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -9,14 +9,15 @@ import frappe.defaults from frappe.utils import cint, flt, cstr, today, random_string, add_days from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice from erpnext.stock.doctype.item.test_item import create_item -from erpnext import set_perpetual_inventory from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.stock.doctype.item.test_item import make_item from six import iteritems +from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + class TestPurchaseReceipt(unittest.TestCase): def setUp(self): - set_perpetual_inventory(0) frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) def test_reverse_purchase_receipt_sle(self): @@ -112,6 +113,8 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertFalse(get_gl_entries("Purchase Receipt", pr.name)) + pr.cancel() + def test_batched_serial_no_purchase(self): item = frappe.db.exists("Item", {'item_name': 'Batched Serialized Item'}) if not item: @@ -183,22 +186,30 @@ class TestPurchaseReceipt(unittest.TestCase): rm_supp_cost = sum([d.amount for d in pr.get("supplied_items")]) self.assertEqual(pr.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2)) + + pr.cancel() def test_subcontracting_gle_fg_item_rate_zero(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - set_perpetual_inventory() frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM") - make_stock_entry(item_code="_Test Item", target="Work In Progress - TCP1", qty=100, basic_rate=100, company="_Test Company with perpetual inventory") - make_stock_entry(item_code="_Test Item Home Desktop 100", target="Work In Progress - TCP1", + + se1 = make_stock_entry(item_code="_Test Item", target="Work In Progress - TCP1", qty=100, basic_rate=100, company="_Test Company with perpetual inventory") + + se2 = make_stock_entry(item_code="_Test Item Home Desktop 100", target="Work In Progress - TCP1", + qty=100, basic_rate=100, company="_Test Company with perpetual inventory") + pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=0, is_subcontracted="Yes", - company="_Test Company with perpetual inventory", warehouse='Stores - TCP1', supplier_warehouse='Work In Progress - TCP1') + company="_Test Company with perpetual inventory", warehouse='Stores - TCP1', + supplier_warehouse='Work In Progress - TCP1') gl_entries = get_gl_entries("Purchase Receipt", pr.name) self.assertFalse(gl_entries) - set_perpetual_inventory(0) + pr.cancel() + se1.cancel() + se2.cancel() def test_subcontracting_over_receipt(self): """ @@ -216,13 +227,13 @@ class TestPurchaseReceipt(unittest.TestCase): item_code = "_Test Subcontracted FG Item 1" make_subcontracted_item(item_code=item_code) - po = create_purchase_order(item_code=item_code, qty=1, + po = create_purchase_order(item_code=item_code, qty=1, include_exploded_items=0, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") #stock raw materials in a warehouse before transfer - make_stock_entry(target="_Test Warehouse - _TC", - item_code = "Test Extra Item 1", qty=1, basic_rate=100) - make_stock_entry(target="_Test Warehouse - _TC", + se1 = make_stock_entry(target="_Test Warehouse - _TC", + item_code = "Test Extra Item 1", qty=10, basic_rate=100) + se2 = make_stock_entry(target="_Test Warehouse - _TC", item_code = "_Test FG Item", qty=1, basic_rate=100) rm_items = [ { @@ -254,6 +265,13 @@ class TestPurchaseReceipt(unittest.TestCase): pr1.submit() self.assertRaises(frappe.ValidationError, pr2.submit) + pr1.cancel() + se.cancel() + se1.cancel() + se2.cancel() + po.reload() + po.cancel() + def test_serial_no_supplier(self): pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) self.assertEqual(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "supplier"), @@ -284,6 +302,8 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(frappe.db.get_value("Serial No", serial_no, "warehouse"), pr.get("items")[0].rejected_warehouse) + pr.cancel() + def test_purchase_return_partial(self): pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1") @@ -371,6 +391,9 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(pr.per_returned, 100) self.assertEqual(pr.status, 'Return Issued') + return_pr.cancel() + pr.cancel() + def test_purchase_return_for_rejected_qty(self): from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse @@ -388,6 +411,9 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(actual_qty, -2) + return_pr.cancel() + pr.cancel() + def test_purchase_return_for_serialized_items(self): def _check_serial_no_values(serial_no, field_values): @@ -415,6 +441,10 @@ class TestPurchaseReceipt(unittest.TestCase): "delivery_document_no": return_pr.name }) + return_pr.cancel() + pr.reload() + pr.cancel() + def test_purchase_return_for_multi_uom(self): item_code = "_Test Purchase Return For Multi-UOM" if not frappe.db.exists('Item', item_code): @@ -431,6 +461,9 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(abs(return_pr.items[0].stock_qty), 1.0) + return_pr.cancel() + pr.cancel() + def test_closed_purchase_receipt(self): from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_purchase_receipt_status @@ -440,6 +473,9 @@ class TestPurchaseReceipt(unittest.TestCase): update_purchase_receipt_status(pr.name, "Closed") self.assertEqual(frappe.db.get_value("Purchase Receipt", pr.name, "status"), "Closed") + pr.reload() + pr.cancel() + def test_pr_billing_status(self): # PO -> PR1 -> PI and PO -> PI and PO -> PR2 from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order @@ -482,6 +518,16 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(pr2.per_billed, 80) self.assertEqual(pr2.status, "To Bill") + pr2.cancel() + pi2.reload() + pi2.cancel() + pi1.reload() + pi1.cancel() + pr1.reload() + pr1.cancel() + po.reload() + po.cancel() + def test_serial_no_against_purchase_receipt(self): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -509,6 +555,8 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(serial_no, frappe.db.get_value("Serial No", {"purchase_document_type": "Purchase Receipt", "purchase_document_no": new_pr_doc.name}, "name")) + new_pr_doc.cancel() + def test_not_accept_duplicate_serial_no(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note @@ -519,16 +567,19 @@ class TestPurchaseReceipt(unittest.TestCase): item_code = item.name serial_no = random_string(5) - make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no) - create_delivery_note(item_code=item_code, qty=1, serial_no=serial_no) + pr1 = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no) + dn = create_delivery_note(item_code=item_code, qty=1, serial_no=serial_no) - pr = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no, do_not_submit=True) - self.assertRaises(SerialNoDuplicateError, pr.submit) + pr2 = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no, do_not_submit=True) + self.assertRaises(SerialNoDuplicateError, pr2.submit) se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1, serial_no=serial_no, basic_rate=100, do_not_submit=True) self.assertRaises(SerialNoDuplicateError, se.submit) + dn.cancel() + pr1.cancel() + def test_auto_asset_creation(self): asset_item = "Test Asset Item" @@ -549,7 +600,7 @@ class TestPurchaseReceipt(unittest.TestCase): 'company_name': '_Test Company', 'fixed_asset_account': '_Test Fixed Asset - _TC', 'accumulated_depreciation_account': '_Test Accumulated Depreciations - _TC', - 'depreciation_expense_account': '_Test Depreciation - _TC' + 'depreciation_expense_account': '_Test Depreciations - _TC' }] }).insert() @@ -568,6 +619,8 @@ class TestPurchaseReceipt(unittest.TestCase): location = frappe.db.get_value('Asset', assets[0].name, 'location') self.assertEquals(location, "Test Location") + pr.cancel() + def test_purchase_return_with_submitted_asset(self): from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return @@ -594,6 +647,9 @@ class TestPurchaseReceipt(unittest.TestCase): pr_return.submit() + pr_return.cancel() + pr.cancel() + def test_purchase_receipt_cost_center(self): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center cost_center = "_Test Cost Center for BS Account - TCP1" @@ -605,7 +661,8 @@ class TestPurchaseReceipt(unittest.TestCase): 'location_name': 'Test Location' }).insert() - pr = make_purchase_receipt(cost_center=cost_center, company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1") + pr = make_purchase_receipt(cost_center=cost_center, company="_Test Company with perpetual inventory", + warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1") stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse) gl_entries = get_gl_entries("Purchase Receipt", pr.name) @@ -623,6 +680,8 @@ class TestPurchaseReceipt(unittest.TestCase): for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) + pr.cancel() + def test_purchase_receipt_cost_center_with_balance_sheet_account(self): if not frappe.db.exists('Location', 'Test Location'): frappe.get_doc({ @@ -648,6 +707,8 @@ class TestPurchaseReceipt(unittest.TestCase): for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) + pr.cancel() + def test_make_purchase_invoice_from_pr_for_returned_qty(self): from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order, create_pr_against_po @@ -663,6 +724,12 @@ class TestPurchaseReceipt(unittest.TestCase): pi = make_purchase_invoice(pr.name) self.assertEquals(pi.items[0].qty, 3) + pr1.cancel() + pr.reload() + pr.cancel() + po.reload() + po.cancel() + def test_make_purchase_invoice_from_pr_with_returned_qty_duplicate_items(self): pr1 = make_purchase_receipt(qty=8, do_not_submit=True) pr1.append("items", { @@ -689,8 +756,14 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEquals(pi2.items[0].qty, 2) self.assertEquals(pi2.items[1].qty, 1) + pr2.cancel() + pi1.cancel() + pr1.reload() + pr1.cancel() + def test_stock_transfer_from_purchase_receipt(self): - pr1 = make_purchase_receipt(warehouse = 'Work In Progress - TCP1', company="_Test Company with perpetual inventory") + pr1 = make_purchase_receipt(warehouse = 'Work In Progress - TCP1', + company="_Test Company with perpetual inventory") pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", do_not_save=1) @@ -713,18 +786,20 @@ class TestPurchaseReceipt(unittest.TestCase): for sle in sl_entries: self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty) - def test_stock_transfer_from_purchase_receipt_with_valuation(self): - warehouse = frappe.get_doc('Warehouse', 'Work In Progress - TCP1') - warehouse.account = '_Test Account Stock In Hand - TCP1' - warehouse.save() + pr.cancel() + pr1.cancel() - pr1 = make_purchase_receipt(warehouse = 'Work In Progress - TCP1', + def test_stock_transfer_from_purchase_receipt_with_valuation(self): + create_warehouse("_Test Warehouse for Valuation", company="_Test Company with perpetual inventory", + properties={"account": '_Test Account Stock In Hand - TCP1'}) + + pr1 = make_purchase_receipt(warehouse = '_Test Warehouse for Valuation - TCP1', company="_Test Company with perpetual inventory") pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", do_not_save=1) - pr.items[0].from_warehouse = 'Work In Progress - TCP1' + pr.items[0].from_warehouse = '_Test Warehouse for Valuation - TCP1' pr.supplier_warehouse = '' @@ -749,7 +824,7 @@ class TestPurchaseReceipt(unittest.TestCase): ] expected_sle = { - 'Work In Progress - TCP1': -5, + '_Test Warehouse for Valuation - TCP1': -5, 'Stores - TCP1': 5 } @@ -761,60 +836,9 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(gle.debit, expected_gle[i][1]) self.assertEqual(gle.credit, expected_gle[i][2]) - warehouse.account = '' - warehouse.save() + pr.cancel() + pr1.cancel() - def test_backdated_purchase_receipt(self): - # make purchase receipt for default company - make_purchase_receipt(company="_Test Company 4", warehouse="Stores - _TC4") - - # try to make another backdated PR - posting_date = add_days(today(), -1) - pr = make_purchase_receipt(company="_Test Company 4", warehouse="Stores - _TC4", - do_not_submit=True) - - pr.set_posting_time = 1 - pr.posting_date = posting_date - pr.save() - - self.assertRaises(frappe.ValidationError, pr.submit) - - # make purchase receipt for other company backdated - pr = make_purchase_receipt(company="_Test Company 5", warehouse="Stores - _TC5", - do_not_submit=True) - - pr.set_posting_time = 1 - pr.posting_date = posting_date - pr.submit() - - # Allowed to submit for other company's PR - self.assertEqual(pr.docstatus, 1) - - def test_backdated_purchase_receipt_for_same_company_different_warehouse(self): - # make purchase receipt for default company - make_purchase_receipt(company="_Test Company 4", warehouse="Stores - _TC4") - - # try to make another backdated PR - posting_date = add_days(today(), -1) - pr = make_purchase_receipt(company="_Test Company 4", warehouse="Stores - _TC4", - do_not_submit=True) - - pr.set_posting_time = 1 - pr.posting_date = posting_date - pr.save() - - self.assertRaises(frappe.ValidationError, pr.submit) - - # make purchase receipt for other company backdated - pr = make_purchase_receipt(company="_Test Company 4", warehouse="Finished Goods - _TC4", - do_not_submit=True) - - pr.set_posting_time = 1 - pr.posting_date = posting_date - pr.submit() - - # Allowed to submit for other company's PR - self.assertEqual(pr.docstatus, 1) def test_subcontracted_pr_for_multi_transfer_batches(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry @@ -877,6 +901,12 @@ class TestPurchaseReceipt(unittest.TestCase): update_backflush_based_on("BOM") + pr.delete() + se.cancel() + ste2.cancel() + ste1.cancel() + po.cancel() + def get_sl_entries(voucher_type, voucher_no): return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s @@ -972,6 +1002,8 @@ def make_purchase_receipt(**args): pr.posting_date = args.posting_date or today() if args.posting_time: pr.posting_time = args.posting_time + if args.posting_date or args.posting_time: + pr.set_posting_time = 1 pr.company = args.company or "_Test Company" pr.supplier = args.supplier or "_Test Supplier" pr.is_subcontracted = args.is_subcontracted or "No" 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 84c64aa8f85..871b255b06a 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -866,7 +866,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-11-02 10:00:38.204294", + "modified": "2020-12-07 10:00:38.204294", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/repost_item_valuation/__init__.py b/erpnext/stock/doctype/repost_item_valuation/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js new file mode 100644 index 00000000000..e429cd5e304 --- /dev/null +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js @@ -0,0 +1,52 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Repost Item Valuation', { + setup: function(frm) { + frm.set_query("warehouse", () => { + let filters = { + 'is_group': 0 + }; + if (frm.doc.company) filters['company'] = frm.doc.company; + return {filters: filters}; + }); + + frm.set_query("voucher_type", () => { + return { + filters: { + name: ['in', ['Purchase Receipt', 'Purchase Invoice', 'Delivery Note', + 'Sales Invoice', 'Stock Entry', 'Stock Reconciliation']] + } + }; + }); + + if (frm.doc.company) { + frm.set_query("voucher_no", () => { + return { + filters: { + company: frm.doc.company + } + }; + }); + } + }, + refresh: function(frm) { + if (frm.doc.status == "Failed") { + frm.add_custom_button(__('Restart'), function () { + frm.trigger("restart_reposting"); + }).addClass("btn-primary"); + } + }, + + restart_reposting: function(frm) { + frappe.call({ + method: "restart_reposting", + doc: frm.doc, + callback: function(r) { + if (!r.exc) { + frm.refresh(); + } + } + }); + } +}); diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json new file mode 100644 index 00000000000..071fc86d9b3 --- /dev/null +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json @@ -0,0 +1,215 @@ +{ + "actions": [], + "autoname": "REPOST-ITEM-VAL-.######", + "creation": "2020-10-22 22:27:07.742161", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "based_on", + "voucher_type", + "voucher_no", + "item_code", + "warehouse", + "posting_date", + "posting_time", + "column_break_5", + "status", + "company", + "allow_negative_stock", + "via_landed_cost_voucher", + "allow_zero_rate", + "amended_from", + "error_section", + "error_log" + ], + "fields": [ + { + "depends_on": "eval:doc.based_on=='Item and Warehouse'", + "fieldname": "item_code", + "fieldtype": "Link", + "label": "Item Code", + "mandatory_depends_on": "eval:doc.based_on=='Item and Warehouse'", + "options": "Item" + }, + { + "depends_on": "eval:doc.based_on=='Item and Warehouse'", + "fieldname": "warehouse", + "fieldtype": "Link", + "label": "Warehouse", + "mandatory_depends_on": "eval:doc.based_on=='Item and Warehouse'", + "options": "Warehouse" + }, + { + "fetch_from": "voucher_no.posting_date", + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date", + "reqd": 1 + }, + { + "fetch_from": "voucher_no.posting_time", + "fieldname": "posting_time", + "fieldtype": "Time", + "label": "Posting Time" + }, + { + "default": "Queued", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "no_copy": 1, + "options": "Queued\nIn Progress\nCompleted\nFailed", + "read_only": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Repost Item Valuation", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.status=='Failed'", + "fieldname": "error_section", + "fieldtype": "Section Break", + "label": "Error" + }, + { + "fieldname": "error_log", + "fieldtype": "Long Text", + "label": "Error Log", + "no_copy": 1, + "read_only": 1 + }, + { + "fetch_from": "warehouse.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "depends_on": "eval:doc.based_on=='Transaction'", + "fieldname": "voucher_type", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Voucher Type", + "mandatory_depends_on": "eval:doc.based_on=='Transaction'", + "options": "DocType" + }, + { + "depends_on": "eval:doc.based_on=='Transaction'", + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Voucher No", + "mandatory_depends_on": "eval:doc.based_on=='Transaction'", + "options": "voucher_type" + }, + { + "default": "Transaction", + "fieldname": "based_on", + "fieldtype": "Select", + "label": "Based On", + "options": "Transaction\nItem and Warehouse", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "allow_negative_stock", + "fieldtype": "Check", + "label": "Allow Negative Stock" + }, + { + "default": "0", + "fieldname": "via_landed_cost_voucher", + "fieldtype": "Check", + "label": "Via Landed Cost Voucher" + }, + { + "default": "0", + "fieldname": "allow_zero_rate", + "fieldtype": "Check", + "label": "Allow Zero Rate" + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2020-12-10 07:52:12.476589", + "modified_by": "Administrator", + "module": "Stock", + "name": "Repost Item Valuation", + "owner": "Administrator", + "permissions": [ + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py new file mode 100644 index 00000000000..a942f2edda7 --- /dev/null +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -0,0 +1,89 @@ +# -*- 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, erpnext +from frappe.model.document import Document +from frappe.utils import cint +from erpnext.stock.stock_ledger import repost_future_sle +from erpnext.accounts.utils import update_gl_entries_after + + +class RepostItemValuation(Document): + def validate(self): + self.set_status() + self.reset_field_values() + self.set_company() + + def reset_field_values(self): + if self.based_on == 'Transaction': + self.item_code = None + self.warehouse = None + else: + self.voucher_type = None + self.voucher_no = None + + def set_company(self): + if self.voucher_type and self.voucher_no: + self.company = frappe.get_cached_value(self.voucher_type, self.voucher_no, "company") + elif self.warehouse: + self.company = frappe.get_cached_value("Warehouse", self.warehouse, "company") + + def set_status(self, status=None): + if not status: + status = 'Queued' + self.db_set('status', status) + + def on_submit(self): + frappe.enqueue(repost, timeout=1800, queue='long', + job_name='repost_sle', now=frappe.flags.in_test, doc=self) + + def restart_reposting(self): + self.set_status('Queued') + frappe.enqueue(repost, timeout=1800, queue='long', + job_name='repost_sle', now=True, doc=self) + +def repost(doc): + try: + doc.set_status('In Progress') + frappe.db.commit() + + repost_sl_entries(doc) + repost_gl_entries(doc) + doc.set_status('Completed') + except Exception: + frappe.db.rollback() + traceback = frappe.get_traceback() + frappe.log_error(traceback) + frappe.db.set_value(doc.doctype, doc.name, 'error_log', traceback) + doc.set_status('Failed') + raise + finally: + frappe.db.commit() + +def repost_sl_entries(doc): + if doc.based_on == 'Transaction': + repost_future_sle(voucher_type=doc.voucher_type, voucher_no=doc.voucher_no, + allow_negative_stock=doc.allow_negative_stock, via_landed_cost_voucher=doc.via_landed_cost_voucher) + else: + repost_future_sle(args=[frappe._dict({ + "item_code": doc.item_code, + "warehouse": doc.warehouse, + "posting_date": doc.posting_date, + "posting_time": doc.posting_time + })], allow_negative_stock=doc.allow_negative_stock, via_landed_cost_voucher=doc.via_landed_cost_voucher) + +def repost_gl_entries(doc): + if not cint(erpnext.is_perpetual_inventory_enabled(doc.company)): + return + + if doc.based_on == 'Transaction': + ref_doc = frappe.get_doc(doc.voucher_type, doc.voucher_no) + items, warehouses = ref_doc.get_items_and_warehouses() + else: + items = [doc.item_code] + warehouses = [doc.warehouse] + + update_gl_entries_after(doc.posting_date, doc.posting_time, + warehouses, items, company=doc.company) \ No newline at end of file diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py new file mode 100644 index 00000000000..13ceb68669c --- /dev/null +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestRepostItemValuation(unittest.TestCase): + pass diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 295149e2387..39ccf49c81c 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -134,17 +134,13 @@ class SerialNo(StockController): sle_dict = self.get_stock_ledger_entries(serial_no) if sle_dict: if sle_dict.get("incoming", []): - sle_list = [sle for sle in sle_dict["incoming"] if sle.is_cancelled == 0] - if sle_list: - entries["purchase_sle"] = sle_list[0] + entries["purchase_sle"] = sle_dict["incoming"][0] if len(sle_dict.get("incoming", [])) - len(sle_dict.get("outgoing", [])) > 0: entries["last_sle"] = sle_dict["incoming"][0] else: entries["last_sle"] = sle_dict["outgoing"][0] - sle_list = [sle for sle in sle_dict["outgoing"] if sle.is_cancelled == 0] - if sle_list: - entries["delivery_sle"] = sle_list[0] + entries["delivery_sle"] = sle_dict["outgoing"][0] return entries @@ -155,11 +151,12 @@ class SerialNo(StockController): for sle in frappe.db.sql(""" SELECT voucher_type, voucher_no, - posting_date, posting_time, incoming_rate, actual_qty, serial_no, is_cancelled + posting_date, posting_time, incoming_rate, actual_qty, serial_no FROM `tabStock Ledger Entry` WHERE item_code=%s AND company = %s + AND is_cancelled = 0 AND (serial_no = %s OR serial_no like %s OR serial_no like %s @@ -179,7 +176,7 @@ class SerialNo(StockController): def on_trash(self): sl_entries = frappe.db.sql("""select serial_no from `tabStock Ledger Entry` - where serial_no like %s and item_code=%s""", + where serial_no like %s and item_code=%s and is_cancelled=0""", ("%%%s%%" % self.name, self.item_code), as_dict=True) # Find the exact match @@ -229,7 +226,7 @@ def validate_serial_no(sle, item_det): if serial_nos: frappe.throw(_("Item {0} is not setup for Serial Nos. Column must be blank").format(sle.item_code), SerialNoNotRequiredError) - else: + elif not sle.is_cancelled: if serial_nos: if cint(sle.actual_qty) != flt(sle.actual_qty): frappe.throw(_("Serial No {0} quantity {1} cannot be a fraction").format(sle.item_code, sle.actual_qty)) @@ -247,10 +244,6 @@ def validate_serial_no(sle, item_det): "delivery_document_no", "delivery_document_type", "warehouse", "purchase_document_no", "company"], as_dict=1) - if sr and cint(sle.actual_qty) < 0 and sr.warehouse != sle.warehouse: - frappe.throw(_("Cannot cancel {0} {1} because Serial No {2} does not belong to the warehouse {3}") - .format(sle.voucher_type, sle.voucher_no, serial_no, sle.warehouse), SerialNoWarehouseError) - if sr.item_code!=sle.item_code: if not allow_serial_nos_with_different_item(serial_no, sle): frappe.throw(_("Serial No {0} does not belong to Item {1}").format(serial_no, @@ -277,7 +270,7 @@ def validate_serial_no(sle, item_det): frappe.throw(_("Serial No {0} does not belong to Batch {1}").format(serial_no, sle.batch_no), SerialNoBatchError) - if not sr.warehouse: + if not sle.is_cancelled and not sr.warehouse: frappe.throw(_("Serial No {0} does not belong to any Warehouse") .format(serial_no), SerialNoWarehouseError) @@ -327,6 +320,12 @@ def validate_serial_no(sle, item_det): elif cint(sle.actual_qty) < 0 or not item_det.serial_no_series: frappe.throw(_("Serial Nos Required for Serialized Item {0}").format(sle.item_code), SerialNoRequiredError) + elif serial_nos: + for serial_no in serial_nos: + sr = frappe.db.get_value("Serial No", serial_no, ["name", "warehouse"], as_dict=1) + if sr and cint(sle.actual_qty) < 0 and sr.warehouse != sle.warehouse: + frappe.throw(_("Cannot cancel {0} {1} because Serial No {2} does not belong to the warehouse {3}") + .format(sle.voucher_type, sle.voucher_no, serial_no, sle.warehouse)) def validate_material_transfer_entry(sle_doc): sle_doc.update({ @@ -334,7 +333,7 @@ def validate_material_transfer_entry(sle_doc): "skip_serial_no_validaiton": False }) - if (sle_doc.voucher_type == "Stock Entry" and + if (sle_doc.voucher_type == "Stock Entry" and not sle_doc.is_cancelled and frappe.get_cached_value("Stock Entry", sle_doc.voucher_no, "purpose") == "Material Transfer"): if sle_doc.actual_qty < 0: sle_doc.skip_update_serial_no = True @@ -379,7 +378,7 @@ def allow_serial_nos_with_different_item(sle_serial_no, sle): stock_entry = frappe.get_cached_doc("Stock Entry", sle.voucher_no) if stock_entry.purpose in ("Repack", "Manufacture"): for d in stock_entry.get("items"): - if d.serial_no and (d.s_warehouse or d.t_warehouse): + if d.serial_no and (d.s_warehouse if not sle.is_cancelled else d.t_warehouse): serial_nos = get_serial_nos(d.serial_no) if sle_serial_no in serial_nos: allow_serial_nos = True @@ -388,7 +387,7 @@ def allow_serial_nos_with_different_item(sle_serial_no, sle): def update_serial_nos(sle, item_det): if sle.skip_update_serial_no: return - if not sle.serial_no and cint(sle.actual_qty) > 0 \ + if not sle.is_cancelled and not sle.serial_no and cint(sle.actual_qty) > 0 \ and item_det.has_serial_no == 1 and item_det.serial_no_series: serial_nos = get_auto_serial_nos(item_det.serial_no_series, sle.actual_qty) frappe.db.set(sle, "serial_no", serial_nos) diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py index ab061076e52..ed70790b2ca 100644 --- a/erpnext/stock/doctype/serial_no/test_serial_no.py +++ b/erpnext/stock/doctype/serial_no/test_serial_no.py @@ -12,7 +12,6 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse -from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory test_dependencies = ["Item"] test_records = frappe.get_test_records('Serial No') @@ -38,8 +37,6 @@ class TestSerialNo(unittest.TestCase): self.assertTrue(SerialNoCannotCannotChangeError, sr.save) def test_inter_company_transfer(self): - set_perpetual_inventory(0, "_Test Company 1") - set_perpetual_inventory(0) se = make_serialized_item(target_warehouse="_Test Warehouse - _TC") serial_nos = get_serial_nos(se.get("items")[0].serial_no) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 27fcbb7e2a5..98116ec1832 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -510,22 +510,31 @@ frappe.ui.form.on('Stock Entry', { calculate_amount: function(frm) { frm.events.calculate_total_additional_costs(frm); - - const total_basic_amount = frappe.utils.sum( - (frm.doc.items || []).map(function(i) { return i.t_warehouse ? flt(i.basic_amount) : 0; }) - ); - + let total_basic_amount = 0; + if (in_list(["Repack", "Manufacture"], frm.doc.purpose)) { + total_basic_amount = frappe.utils.sum( + (frm.doc.items || []).map(function(i) { + return i.is_finished_item ? flt(i.basic_amount) : 0; + }) + ); + } else { + total_basic_amount = frappe.utils.sum( + (frm.doc.items || []).map(function(i) { + return i.t_warehouse ? flt(i.basic_amount) : 0; + }) + ); + } + for (let i in frm.doc.items) { let item = frm.doc.items[i]; - if (item.t_warehouse && total_basic_amount) { + if (((in_list(["Repack", "Manufacture"], frm.doc.purpose) && item.is_finished_item) || item.t_warehouse) && total_basic_amount) { item.additional_cost = (flt(item.basic_amount) / total_basic_amount) * frm.doc.total_additional_costs; } else { item.additional_cost = 0; } - item.amount = flt(item.basic_amount + flt(item.additional_cost), - precision("amount", item)); + item.amount = flt(item.basic_amount + flt(item.additional_cost), precision("amount", item)); if (flt(item.transfer_qty)) { item.valuation_rate = flt(flt(item.basic_rate) + (flt(item.additional_cost) / flt(item.transfer_qty)), diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 61e0df67238..5aed08102c7 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -644,9 +644,10 @@ ], "icon": "fa fa-file-text", "idx": 1, + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-08-11 19:10:07.954981", + "modified": "2020-09-09 12:59:02.508943", "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 32d7e6eb34c..afdb54ceaa2 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -18,7 +18,7 @@ from erpnext.stock.utils import get_bin 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 import json from six import string_types, itervalues, iteritems @@ -58,6 +58,7 @@ class StockEntry(StockController): self.validate_warehouse() self.validate_work_order() self.validate_bom() + self.mark_finished_and_scrap_items() self.validate_finished_goods() self.validate_with_material_request() self.validate_batch() @@ -75,13 +76,11 @@ class StockEntry(StockController): else: set_batch_nos(self, 's_warehouse') - self.set_incoming_rate() self.validate_serialized_batch() self.set_actual_qty() - self.calculate_rate_and_amount(update_finished_item_rate=False) + self.calculate_rate_and_amount() def on_submit(self): - self.update_stock_ledger() update_serial_nos_after_submit(self, "items") @@ -89,11 +88,15 @@ class StockEntry(StockController): self.validate_purchase_order() if self.purchase_order and self.purpose == "Send to Subcontractor": self.update_purchase_order_supplied_items() + self.make_gl_entries() + + self.repost_future_sle_and_gle() self.update_cost_in_project() self.validate_reserved_serial_no_consumption() self.update_transferred_qty() self.update_quality_inspection() + if self.work_order and self.purpose == "Manufacture": self.update_so_in_serial_number() @@ -113,9 +116,10 @@ class StockEntry(StockController): self.update_work_order() self.update_stock_ledger() - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') self.make_gl_entries_on_cancel() + self.repost_future_sle_and_gle() self.update_cost_in_project() self.update_transferred_qty() self.update_quality_inspection() @@ -256,11 +260,10 @@ class StockEntry(StockController): def validate_fg_completed_qty(self): if self.purpose == "Manufacture" and self.work_order: - production_item = frappe.get_value('Work Order', self.work_order, 'production_item') - for item in self.items: - if item.item_code == production_item and item.t_warehouse and item.qty != self.fg_completed_qty: + for d in self.items: + if d.is_finished_item and d.qty != self.fg_completed_qty: frappe.throw(_("Finished product quantity {0} and For Quantity {1} cannot be different") - .format(item.qty, self.fg_completed_qty)) + .format(d.qty, self.fg_completed_qty)) def validate_difference_account(self): if not cint(erpnext.is_perpetual_inventory_enabled(self.company)): @@ -382,21 +385,6 @@ class StockEntry(StockController): frappe.throw(_("Stock Entries already created for Work Order ") + self.work_order + ":" + ", ".join(other_ste), DuplicateEntryForWorkOrderError) - def set_incoming_rate(self): - if self.purpose == "Repack": - self.set_basic_rate_for_finished_goods() - - for d in self.items: - if d.s_warehouse: - args = self.get_args_for_incoming_rate(d) - d.basic_rate = get_incoming_rate(args) - elif d.allow_zero_valuation_rate and not d.s_warehouse: - d.basic_rate = 0.0 - elif d.t_warehouse and not d.basic_rate: - d.basic_rate = get_valuation_rate(d.item_code, d.t_warehouse, - self.doctype, self.name, d.allow_zero_valuation_rate, - currency=erpnext.get_company_currency(self.company), company=self.company) - def set_actual_qty(self): allow_negative_stock = cint(frappe.db.get_value("Stock Settings", None, "allow_negative_stock")) @@ -432,57 +420,64 @@ class StockEntry(StockController): d.serial_no = transferred_serial_no def get_stock_and_rate(self): + """ + Updates rate and availability of all the items. + Called from Update Rate and Availability button. + """ self.set_work_order_details() self.set_transfer_qty() self.set_actual_qty() self.calculate_rate_and_amount() - def calculate_rate_and_amount(self, force=False, - update_finished_item_rate=True, raise_error_if_no_rate=True): - self.set_basic_rate(force, update_finished_item_rate, raise_error_if_no_rate) + 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) self.distribute_additional_costs() self.update_valuation_rate() self.set_total_incoming_outgoing_value() self.set_total_amount() - def set_basic_rate(self, force=False, update_finished_item_rate=True, raise_error_if_no_rate=True): - """get stock and incoming rate on posting date""" - raw_material_cost = 0.0 - scrap_material_cost = 0.0 - fg_basic_rate = 0.0 + def set_basic_rate(self, reset_outgoing_rate=True, raise_error_if_no_rate=True): + """ + Set rate for outgoing, scrapped and finished items + """ + # Set rate for outgoing items + outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate) + # Set basic rate for incoming items for d in self.get('items'): - if d.t_warehouse: fg_basic_rate = flt(d.basic_rate) - args = self.get_args_for_incoming_rate(d) + if d.s_warehouse or d.set_basic_rate_manually: continue - # get basic rate - if not d.bom_no: - if (not flt(d.basic_rate) and not d.allow_zero_valuation_rate) or d.s_warehouse or force: - basic_rate = flt(get_incoming_rate(args, raise_error_if_no_rate), self.precision("basic_rate", d)) - if basic_rate > 0: - d.basic_rate = basic_rate + if d.allow_zero_valuation_rate: + d.basic_rate = 0.0 + elif d.is_finished_item: + if self.purpose == "Manufacture": + d.basic_rate = self.get_basic_rate_for_manufactured_item(d.transfer_qty, outgoing_items_cost) + elif self.purpose == "Repack": + d.basic_rate = self.get_basic_rate_for_repacked_items(d.transfer_qty, outgoing_items_cost) + + if not d.basic_rate and not d.allow_zero_valuation_rate: + d.basic_rate = get_valuation_rate(d.item_code, d.t_warehouse, + self.doctype, self.name, d.allow_zero_valuation_rate, + currency=erpnext.get_company_currency(self.company), company=self.company, + raise_error_if_no_rate=raise_error_if_no_rate) + + d.basic_rate = flt(d.basic_rate, d.precision("basic_rate")) + d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount")) + + def set_rate_for_outgoing_items(self, reset_outgoing_rate=True): + outgoing_items_cost = 0.0 + for d in self.get('items'): + if d.s_warehouse: + if reset_outgoing_rate: + args = self.get_args_for_incoming_rate(d) + rate = get_incoming_rate(args) + if rate > 0: + d.basic_rate = rate d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount")) if not d.t_warehouse: - raw_material_cost += flt(d.basic_amount) - - # get scrap items basic rate - if d.bom_no: - if not flt(d.basic_rate) and not d.allow_zero_valuation_rate and \ - getattr(self, "pro_doc", frappe._dict()).scrap_warehouse == d.t_warehouse: - basic_rate = flt(get_incoming_rate(args, raise_error_if_no_rate), - self.precision("basic_rate", d)) - if basic_rate > 0: - d.basic_rate = basic_rate - d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount")) - - if getattr(self, "pro_doc", frappe._dict()).scrap_warehouse == d.t_warehouse: - - scrap_material_cost += flt(d.basic_amount) - - number_of_fg_items = len([t.t_warehouse for t in self.get("items") if t.t_warehouse]) - if (fg_basic_rate == 0.0 and number_of_fg_items == 1) or update_finished_item_rate: - self.set_basic_rate_for_finished_goods(raw_material_cost, scrap_material_cost) + outgoing_items_cost += flt(d.basic_amount) + return outgoing_items_cost def get_args_for_incoming_rate(self, item): return frappe._dict({ @@ -498,44 +493,44 @@ class StockEntry(StockController): "allow_zero_valuation": item.allow_zero_valuation_rate, }) - def set_basic_rate_for_finished_goods(self, raw_material_cost=0, scrap_material_cost=0): - total_fg_qty = 0 - if not raw_material_cost and self.get("items"): - raw_material_cost = sum([flt(row.basic_amount) for row in self.items - if row.s_warehouse and not row.t_warehouse]) + def get_basic_rate_for_repacked_items(self, finished_item_qty, outgoing_items_cost): + finished_items = [d.item_code for d in self.get("items") if d.is_finished_item] + if len(finished_items) == 1: + return flt(outgoing_items_cost / finished_item_qty) + else: + unique_finished_items = set(finished_items) + if len(unique_finished_items) == 1: + total_fg_qty = sum([flt(d.transfer_qty) for d in self.items if d.is_finished_item]) + return flt(outgoing_items_cost / total_fg_qty) - total_fg_qty = sum([flt(row.qty) for row in self.items - if row.t_warehouse and not row.s_warehouse]) + def get_basic_rate_for_manufactured_item(self, finished_item_qty, outgoing_items_cost=0): + scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item]) - if self.purpose in ["Manufacture", "Repack"]: - for d in self.get("items"): - if (d.transfer_qty and (d.bom_no or d.t_warehouse) - and (getattr(self, "pro_doc", frappe._dict()).scrap_warehouse != d.t_warehouse)): + # Get raw materials cost from BOM if multiple material consumption entries + if frappe.db.get_single_value("Manufacturing Settings", "material_consumption"): + bom_items = self.get_bom_raw_materials(finished_item_qty) + outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()]) - if (self.work_order and self.purpose == "Manufacture" - and frappe.db.get_single_value("Manufacturing Settings", "material_consumption")): - bom_items = self.get_bom_raw_materials(d.transfer_qty) - raw_material_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()]) - - if raw_material_cost and self.purpose == "Manufacture": - d.basic_rate = flt((raw_material_cost - scrap_material_cost) / flt(d.transfer_qty), d.precision("basic_rate")) - d.basic_amount = flt((raw_material_cost - scrap_material_cost), d.precision("basic_amount")) - elif self.purpose == "Repack" and total_fg_qty and not d.set_basic_rate_manually: - d.basic_rate = flt(raw_material_cost) / flt(total_fg_qty) - d.basic_amount = d.basic_rate * flt(d.qty) + return flt(outgoing_items_cost - scrap_items_cost) def distribute_additional_costs(self): - if self.purpose == "Material Issue": + # If no incoming items, set additional costs blank + 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")]) - total_basic_amount = sum([flt(t.basic_amount) for t in self.get("items") if t.t_warehouse]) - for d in self.get("items"): - if d.t_warehouse and total_basic_amount: - d.additional_cost = (flt(d.basic_amount) / total_basic_amount) * self.total_additional_costs - else: - d.additional_cost = 0 + 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]) + else: + incoming_items_cost = sum([flt(t.basic_amount) for t in self.get("items") if t.t_warehouse]) + + if incoming_items_cost: + for d in self.get("items"): + if (self.purpose in ("Repack", "Manufacture") and d.is_finished_item) or d.t_warehouse: + d.additional_cost = (flt(d.basic_amount) / incoming_items_cost) * self.total_additional_costs + else: + d.additional_cost = 0 def update_valuation_rate(self): for d in self.get("items"): @@ -638,71 +633,115 @@ class StockEntry(StockController): item_code = d.original_item or d.item_code validate_bom_no(item_code, d.bom_no) + def mark_finished_and_scrap_items(self): + if self.purpose in ("Repack", "Manufacture"): + if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]): + return + + finished_item = self.get_finished_item() + + for d in self.items: + if d.t_warehouse and not d.s_warehouse: + if self.purpose=="Repack" or d.item_code == finished_item: + d.is_finished_item = 1 + else: + d.is_scrap_item = 1 + else: + d.is_finished_item = 0 + d.is_scrap_item = 0 + + def get_finished_item(self): + finished_item = None + if self.work_order: + finished_item = frappe.db.get_value("Work Order", self.work_order, "production_item") + elif self.bom_no: + finished_item = frappe.db.get_value("BOM", self.bom_no, "item") + + return finished_item + def validate_finished_goods(self): """validation: finished good quantity should be same as manufacturing quantity""" if not self.work_order: return - items_with_target_warehouse = [] - allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", - "overproduction_percentage_for_work_order")) - production_item, wo_qty = frappe.db.get_value("Work Order", self.work_order, ["production_item", "qty"]) + number_of_finished_items = 0 for d in self.get('items'): - if (self.purpose != "Send to Subcontractor" and d.bom_no - and flt(d.transfer_qty) > flt(self.fg_completed_qty) and d.item_code == production_item): - frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}"). \ - format(d.idx, d.transfer_qty, self.fg_completed_qty)) + if d.is_finished_item: + if d.item_code != production_item: + frappe.throw(_("Finished Item {0} does not match with Work Order {1}") + .format(d.item_code, self.work_order)) + elif flt(d.transfer_qty) > flt(self.fg_completed_qty): + frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}"). \ + format(d.idx, d.transfer_qty, self.fg_completed_qty)) + number_of_finished_items += 1 - if self.work_order and self.purpose == "Manufacture" and d.t_warehouse: - items_with_target_warehouse.append(d.item_code) + if number_of_finished_items > 1: + frappe.throw(_("Multiple items cannot be marked as finished item")) + + if self.purpose == "Manufacture": + allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", + "overproduction_percentage_for_work_order")) - if self.work_order and self.purpose == "Manufacture": allowed_qty = wo_qty + (allowance_percentage/100 * wo_qty) if self.fg_completed_qty > allowed_qty: frappe.throw(_("For quantity {0} should not be greater than work order quantity {1}") .format(flt(self.fg_completed_qty), wo_qty)) - if production_item not in items_with_target_warehouse: - frappe.throw(_("Finished Item {0} must be entered for Manufacture type entry") - .format(production_item)) - def update_stock_ledger(self): sl_entries = [] + finished_item_row = self.get_finished_item_row() - # make sl entries for source warehouse first, then do for target warehouse - for d in self.get('items'): - if cstr(d.s_warehouse): - sl_entries.append(self.get_sl_entries(d, { - "warehouse": cstr(d.s_warehouse), - "actual_qty": -flt(d.transfer_qty), - "incoming_rate": 0 - })) - - for d in self.get('items'): - if cstr(d.t_warehouse): - sl_entries.append(self.get_sl_entries(d, { - "warehouse": cstr(d.t_warehouse), - "actual_qty": flt(d.transfer_qty), - "incoming_rate": flt(d.valuation_rate) - })) - - # On cancellation, make stock ledger entry for - # target warehouse first, to update serial no values properly - - # if cstr(d.s_warehouse) and self.docstatus == 2: - # sl_entries.append(self.get_sl_entries(d, { - # "warehouse": cstr(d.s_warehouse), - # "actual_qty": -flt(d.transfer_qty), - # "incoming_rate": 0 - # })) + # make sl entries for source warehouse first + self.get_sle_for_source_warehouse(sl_entries, finished_item_row) + # SLE for target warehouse + self.get_sle_for_target_warehouse(sl_entries, finished_item_row) + + # reverse sl entries if cancel if self.docstatus == 2: sl_entries.reverse() self.make_sl_entries(sl_entries) + def get_finished_item_row(self): + finished_item_row = None + if self.purpose in ("Manufacture", "Repack"): + for d in self.get('items'): + if d.is_finished_item: + finished_item_row = d + + return finished_item_row + + def get_sle_for_source_warehouse(self, sl_entries, finished_item_row): + for d in self.get('items'): + if cstr(d.s_warehouse): + sle = self.get_sl_entries(d, { + "warehouse": cstr(d.s_warehouse), + "actual_qty": -flt(d.transfer_qty), + "incoming_rate": 0 + }) + if cstr(d.t_warehouse): + sle.dependant_sle_voucher_detail_no = d.name + elif finished_item_row and (finished_item_row.item_code != d.item_code or finished_item_row.t_warehouse != d.s_warehouse): + sle.dependant_sle_voucher_detail_no = finished_item_row.name + + sl_entries.append(sle) + + def get_sle_for_target_warehouse(self, sl_entries, finished_item_row): + for d in self.get('items'): + if cstr(d.t_warehouse): + sle = self.get_sl_entries(d, { + "warehouse": cstr(d.t_warehouse), + "actual_qty": flt(d.transfer_qty), + "incoming_rate": flt(d.valuation_rate) + }) + if cstr(d.s_warehouse) or (finished_item_row and d.name == finished_item_row.name): + sle.recalculate_rate = 1 + + sl_entries.append(sle) + def get_gl_entries(self, warehouse_account): gl_entries = super(StockEntry, self).get_gl_entries(warehouse_account) @@ -747,7 +786,7 @@ class StockEntry(StockController): "credit": -1 * amount # put it as negative credit instead of debit purposefully }, item=d)) - return gl_entries + return process_gl_map(gl_entries) def update_work_order(self): def _validate_work_order(pro_doc): @@ -996,6 +1035,7 @@ class StockEntry(StockController): "stock_uom": item.stock_uom, "expense_account": item.get("expense_account"), "cost_center": item.get("buying_cost_center"), + "is_finished_item": 1 } }, bom_no = self.bom_no) @@ -1034,6 +1074,7 @@ class StockEntry(StockController): for item in itervalues(item_dict): item.from_warehouse = "" + item.is_scrap_item = 1 return item_dict def get_unconsumed_raw_materials(self): @@ -1246,6 +1287,8 @@ class StockEntry(StockController): se_child.subcontracted_item = item_dict[d].get("main_item_code") se_child.cost_center = (item_dict[d].get("cost_center") or get_default_cost_center(item_dict[d], company = self.company)) + se_child.is_finished_item = item_dict[d].get("is_finished_item", 0) + se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0) for field in ["idx", "po_detail", "original_item", "expense_account", "description", "item_name"]: diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 9b6744ca3c0..1a641855aa2 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -6,7 +6,6 @@ import frappe, unittest import frappe.defaults from frappe.utils import flt, nowdate, nowtime from erpnext.stock.doctype.serial_no.serial_no import * -from erpnext import set_perpetual_inventory from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError from erpnext.stock.stock_ledger import get_previous_sle from frappe.permissions import add_user_permission, remove_user_permission @@ -32,7 +31,6 @@ def get_sle(**args): class TestStockEntry(unittest.TestCase): def tearDown(self): frappe.set_user("Administrator") - set_perpetual_inventory(0) def test_fifo(self): frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) @@ -213,7 +211,6 @@ class TestStockEntry(unittest.TestCase): def test_repack_no_change_in_valuation(self): company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company') - set_perpetual_inventory(0, company) make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100) make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", @@ -235,8 +232,6 @@ class TestStockEntry(unittest.TestCase): order by account desc""", repack.name, as_dict=1) self.assertFalse(gl_entries) - set_perpetual_inventory(0, repack.company) - def test_repack_with_additional_costs(self): company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') @@ -474,7 +469,6 @@ class TestStockEntry(unittest.TestCase): def test_warehouse_company_validation(self): company = frappe.db.get_value('Warehouse', '_Test Warehouse 2 - _TC1', 'company') - set_perpetual_inventory(0, company) frappe.get_doc("User", "test2@example.com")\ .add_roles("Sales User", "Sales Manager", "Stock User", "Stock Manager") frappe.set_user("test2@example.com") @@ -500,7 +494,7 @@ class TestStockEntry(unittest.TestCase): st1 = frappe.copy_doc(test_records[0]) st1.company = "_Test Company 1" - set_perpetual_inventory(0, st1.company) + frappe.set_user("test@example.com") st1.get("items")[0].t_warehouse="_Test Warehouse 2 - _TC1" self.assertRaises(frappe.PermissionError, st1.insert) @@ -698,47 +692,54 @@ class TestStockEntry(unittest.TestCase): repack.insert() self.assertRaises(frappe.ValidationError, repack.submit) - def test_material_consumption(self): - from erpnext.manufacturing.doctype.work_order.work_order \ - import make_stock_entry as _make_stock_entry - bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item 2", - "is_default": 1, "docstatus": 1}) + # def test_material_consumption(self): + # frappe.db.set_value("Manufacturing Settings", None, "backflush_raw_materials_based_on", "BOM") + # frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0") - work_order = frappe.new_doc("Work Order") - work_order.update({ - "company": "_Test Company", - "fg_warehouse": "_Test Warehouse 1 - _TC", - "production_item": "_Test FG Item 2", - "bom_no": bom_no, - "qty": 4.0, - "stock_uom": "_Test UOM", - "wip_warehouse": "_Test Warehouse - _TC", - "additional_operating_cost": 1000 - }) - work_order.insert() - work_order.submit() + # from erpnext.manufacturing.doctype.work_order.work_order \ + # import make_stock_entry as _make_stock_entry + # bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item 2", + # "is_default": 1, "docstatus": 1}) - make_stock_entry(item_code="_Test Serialized Item With Series", target="_Test Warehouse - _TC", qty=50, basic_rate=100) - make_stock_entry(item_code="_Test Item 2", target="_Test Warehouse - _TC", qty=50, basic_rate=20) + # work_order = frappe.new_doc("Work Order") + # work_order.update({ + # "company": "_Test Company", + # "fg_warehouse": "_Test Warehouse 1 - _TC", + # "production_item": "_Test FG Item 2", + # "bom_no": bom_no, + # "qty": 4.0, + # "stock_uom": "_Test UOM", + # "wip_warehouse": "_Test Warehouse - _TC", + # "additional_operating_cost": 1000, + # "use_multi_level_bom": 1 + # }) + # work_order.insert() + # work_order.submit() - item_quantity = { - '_Test Item': 10.0, - '_Test Item 2': 12.0, - '_Test Serialized Item With Series': 6.0 - } + # make_stock_entry(item_code="_Test Serialized Item With Series", target="_Test Warehouse - _TC", qty=50, basic_rate=100) + # make_stock_entry(item_code="_Test Item 2", target="_Test Warehouse - _TC", qty=50, basic_rate=20) - stock_entry = frappe.get_doc(_make_stock_entry(work_order.name, "Material Consumption for Manufacture", 2)) - for d in stock_entry.get('items'): - self.assertEqual(item_quantity.get(d.item_code), d.qty) + # item_quantity = { + # '_Test Item': 2.0, + # '_Test Item 2': 12.0, + # '_Test Serialized Item With Series': 6.0 + # } + + # stock_entry = frappe.get_doc(_make_stock_entry(work_order.name, "Material Consumption for Manufacture", 2)) + # for d in stock_entry.get('items'): + # self.assertEqual(item_quantity.get(d.item_code), d.qty) def test_customer_provided_parts_se(self): create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0) - se = make_stock_entry(item_code='CUST-0987', purpose = 'Material Receipt', qty=4, to_warehouse = "_Test Warehouse - _TC") + se = make_stock_entry(item_code='CUST-0987', purpose = 'Material Receipt', + qty=4, to_warehouse = "_Test Warehouse - _TC") self.assertEqual(se.get("items")[0].allow_zero_valuation_rate, 1) self.assertEqual(se.get("items")[0].amount, 0) def test_gle_for_opening_stock_entry(self): - mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1", company="_Test Company with perpetual inventory",qty=50, basic_rate=100, expense_account="Stock Adjustment - TCP1", is_opening="Yes", do_not_save=True) + mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1", + company="_Test Company with perpetual inventory", qty=50, basic_rate=100, + expense_account="Stock Adjustment - TCP1", is_opening="Yes", do_not_save=True) self.assertRaises(OpeningEntryAccountError, mr.save) @@ -759,8 +760,8 @@ class TestStockEntry(unittest.TestCase): "company":"_Test Company with perpetual inventory", "items":[ { - "item_code":"Basil Leaves", - "description":"Basil Leaves", + "item_code":"_Test Item", + "description":"_Test Item", "qty": 1, "basic_rate": 0, "uom":"Nos", @@ -769,8 +770,8 @@ class TestStockEntry(unittest.TestCase): "cost_center": "Main - TCP1" }, { - "item_code":"Basil Leaves", - "description":"Basil Leaves", + "item_code":"_Test Item", + "description":"_Test Item", "qty": 2, "basic_rate": 0, "uom":"Nos", 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 79e8f9af8fc..6fe60298eeb 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -13,8 +13,10 @@ "t_warehouse", "sec_break1", "item_code", - "col_break2", "item_name", + "col_break2", + "is_finished_item", + "is_scrap_item", "subcontracted_item", "section_break_8", "description", @@ -22,35 +24,37 @@ "item_group", "image", "image_view", - "quantity_and_rate", - "set_basic_rate_manually", + "quantity_section", "qty", - "basic_rate", - "basic_amount", - "additional_cost", - "amount", - "valuation_rate", - "col_break3", - "uom", - "conversion_factor", - "stock_uom", "transfer_qty", "retain_sample", + "column_break_20", + "uom", + "stock_uom", + "conversion_factor", "sample_quantity", + "rates_section", + "basic_rate", + "additional_cost", + "valuation_rate", + "allow_zero_valuation_rate", + "col_break3", + "set_basic_rate_manually", + "basic_amount", + "amount", "serial_no_batch", "serial_no", "col_break4", "batch_no", - "quality_inspection", "accounting", "expense_account", - "col_break5", "accounting_dimensions_section", "cost_center", + "project", "dimension_col_break", "more_info", - "allow_zero_valuation_rate", "actual_qty", + "transferred_qty", "bom_no", "allow_alternative_item", "col_break6", @@ -62,9 +66,8 @@ "ste_detail", "po_detail", "column_break_51", - "transferred_qty", "reference_purchase_receipt", - "project" + "quality_inspection" ], "fields": [ { @@ -159,11 +162,6 @@ "options": "image", "print_hide": 1 }, - { - "fieldname": "quantity_and_rate", - "fieldtype": "Section Break", - "label": "Quantity and Rate" - }, { "bold": 1, "fieldname": "qty", @@ -321,10 +319,6 @@ "options": "Account", "print_hide": 1 }, - { - "fieldname": "col_break5", - "fieldtype": "Column Break" - }, { "default": ":Company", "depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))", @@ -335,6 +329,7 @@ "print_hide": 1 }, { + "collapsible": 1, "fieldname": "more_info", "fieldtype": "Section Break", "label": "More Information" @@ -456,6 +451,7 @@ "read_only": 1 }, { + "collapsible": 1, "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", "label": "Accounting Dimensions" @@ -498,6 +494,32 @@ "fieldname": "set_basic_rate_manually", "fieldtype": "Check", "label": "Set Basic Rate Manually" + }, + { + "fieldname": "quantity_section", + "fieldtype": "Section Break", + "label": "Quantity" + }, + { + "fieldname": "column_break_20", + "fieldtype": "Column Break" + }, + { + "fieldname": "rates_section", + "fieldtype": "Section Break", + "label": "Rates" + }, + { + "default": "0", + "fieldname": "is_scrap_item", + "fieldtype": "Check", + "label": "Is Scrap Item" + }, + { + "default": "0", + "fieldname": "is_finished_item", + "fieldtype": "Check", + "label": "Is Finished Item" } ], "idx": 1, diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json index fda17e08ab3..2463a21ed61 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json @@ -8,26 +8,33 @@ "engine": "InnoDB", "field_order": [ "item_code", - "serial_no", - "batch_no", "warehouse", "posting_date", "posting_time", + "column_break_6", "voucher_type", "voucher_no", "voucher_detail_no", + "dependant_sle_voucher_detail_no", + "recalculate_rate", + "section_break_11", "actual_qty", + "qty_after_transaction", "incoming_rate", "outgoing_rate", - "stock_uom", - "qty_after_transaction", + "column_break_17", "valuation_rate", "stock_value", "stock_value_difference", "stock_queue", - "project", + "section_break_21", "company", + "stock_uom", + "project", + "batch_no", + "column_break_26", "fiscal_year", + "serial_no", "is_cancelled", "to_rename" ], @@ -50,7 +57,6 @@ { "fieldname": "serial_no", "fieldtype": "Long Text", - "in_list_view": 1, "label": "Serial No", "print_width": "100px", "read_only": 1, @@ -59,7 +65,6 @@ { "fieldname": "batch_no", "fieldtype": "Data", - "in_list_view": 1, "label": "Batch No", "oldfieldname": "batch_no", "oldfieldtype": "Data", @@ -119,6 +124,7 @@ "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "in_filter": 1, + "in_list_view": 1, "in_standard_filter": 1, "label": "Voucher No", "oldfieldname": "voucher_no", @@ -142,6 +148,7 @@ "fieldname": "actual_qty", "fieldtype": "Float", "in_filter": 1, + "in_list_view": 1, "label": "Actual Quantity", "oldfieldname": "actual_qty", "oldfieldtype": "Currency", @@ -152,6 +159,7 @@ { "fieldname": "incoming_rate", "fieldtype": "Currency", + "in_list_view": 1, "label": "Incoming Rate", "oldfieldname": "incoming_rate", "oldfieldtype": "Currency", @@ -217,13 +225,11 @@ { "fieldname": "stock_queue", "fieldtype": "Text", - "hidden": 1, "label": "Stock Queue (FIFO)", "oldfieldname": "fcfs_stack", "oldfieldtype": "Text", "print_hide": 1, - "read_only": 1, - "report_hide": 1 + "read_only": 1 }, { "fieldname": "project", @@ -269,14 +275,48 @@ "hidden": 1, "label": "To Rename", "search_index": 1 + }, + { + "fieldname": "dependant_sle_voucher_detail_no", + "fieldtype": "Data", + "label": "Dependant SLE Voucher Detail No" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_11", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_17", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_21", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_26", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "recalculate_rate", + "fieldtype": "Check", + "label": "Recalculate Incoming/Outgoing Rate", + "no_copy": 1, + "read_only": 1 } ], "hide_toolbar": 1, "icon": "fa fa-list", "idx": 1, "in_create": 1, + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-04-23 05:57:03.985520", + "modified": "2020-09-07 11:10:35.318872", "modified_by": "Administrator", "module": "Stock", "name": "Stock Ledger Entry", diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index bb356f694a4..a5c303ccb4d 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -10,8 +10,10 @@ from frappe.model.document import Document from datetime import date from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock from erpnext.accounts.utils import get_fiscal_year +from frappe.core.doctype.role.role import get_users class StockFreezeError(frappe.ValidationError): pass +class BackDatedStockTransaction(frappe.ValidationError): pass exclude_from_linked_with = True @@ -34,7 +36,6 @@ class StockLedgerEntry(Document): self.validate_and_set_fiscal_year() self.block_transactions_against_group_warehouse() self.validate_with_last_transaction_posting_time() - self.validate_future_posting() def on_submit(self): self.check_stock_frozen_date() @@ -48,7 +49,7 @@ class StockLedgerEntry(Document): def calculate_batch_qty(self): if self.batch_no: batch_qty = frappe.db.get_value("Stock Ledger Entry", - {"docstatus": 1, "batch_no": self.batch_no}, + {"docstatus": 1, "batch_no": self.batch_no, "is_cancelled": 0}, "sum(actual_qty)") or 0 frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty) @@ -88,14 +89,14 @@ class StockLedgerEntry(Document): # check if batch number is required if self.voucher_type != 'Stock Reconciliation': - if item_det.has_batch_no ==1: + if item_det.has_batch_no == 1: batch_item = self.item_code if self.item_code == item_det.item_name else self.item_code + ":" + item_det.item_name if not self.batch_no: frappe.throw(_("Batch number is mandatory for Item {0}").format(batch_item)) elif not frappe.db.get_value("Batch",{"item": self.item_code, "name": self.batch_no}): frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item)) - elif item_det.has_batch_no ==0 and self.batch_no: + elif item_det.has_batch_no == 0 and self.batch_no and self.is_cancelled == 0: frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code)) if item_det.has_variants: @@ -142,28 +143,28 @@ class StockLedgerEntry(Document): is_group_warehouse(self.warehouse) def validate_with_last_transaction_posting_time(self): - last_transaction_time = frappe.db.sql(""" - select MAX(timestamp(posting_date, posting_time)) as posting_time - from `tabStock Ledger Entry` - where docstatus = 1 and item_code = %s - and warehouse = %s""", (self.item_code, self.warehouse))[0][0] + authorized_role = frappe.db.get_single_value("Stock Settings", "role_allowed_to_create_edit_back_dated_transactions") + if authorized_role: + authorized_users = get_users(authorized_role) + if authorized_users and frappe.session.user not in authorized_users: + last_transaction_time = frappe.db.sql(""" + select MAX(timestamp(posting_date, posting_time)) as posting_time + from `tabStock Ledger Entry` + where docstatus = 1 and item_code = %s + and warehouse = %s""", (self.item_code, self.warehouse))[0][0] - cur_doc_posting_datetime = "%s %s" % (self.posting_date, self.get("posting_time") or "00:00:00") + cur_doc_posting_datetime = "%s %s" % (self.posting_date, self.get("posting_time") or "00:00:00") - if last_transaction_time and get_datetime(cur_doc_posting_datetime) < get_datetime(last_transaction_time): - msg = _("Last Stock Transaction for item {0} under warehouse {1} was on {2}.").format(frappe.bold(self.item_code), - frappe.bold(self.warehouse), frappe.bold(last_transaction_time)) + if last_transaction_time and get_datetime(cur_doc_posting_datetime) < get_datetime(last_transaction_time): + msg = _("Last Stock Transaction for item {0} under warehouse {1} was on {2}.").format(frappe.bold(self.item_code), + frappe.bold(self.warehouse), frappe.bold(last_transaction_time)) - msg += "

    " + _("Stock Transactions for Item {0} under warehouse {1} cannot be posted before this time.").format( - frappe.bold(self.item_code), frappe.bold(self.warehouse)) + msg += "

    " + _("You are not authorized to make/edit Stock Transactions for Item {0} under warehouse {1} before this time.").format( + frappe.bold(self.item_code), frappe.bold(self.warehouse)) - msg += "

    " + _("Please remove this item and try to submit again or update the posting time.") - frappe.throw(msg, title=_("Backdated Stock Entry")) - - def validate_future_posting(self): - if date_diff(self.posting_date, getdate()) > 0: - msg = _("Posting future stock transactions are not allowed due to Immutable Ledger") - frappe.throw(msg, title=_("Future Posting Not Allowed")) + msg += "

    " + _("Please contact any of the following users to {} this transaction.") + msg += "
    " + "
    ".join(authorized_users) + frappe.throw(msg, BackDatedStockTransaction, title=_("Backdated Stock Entry")) def on_doctype_update(): if not frappe.db.has_index('tabStock Ledger Entry', 'posting_sort_index'): diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index 04dae83447b..59f1f3961b6 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -5,8 +5,397 @@ from __future__ import unicode_literals import frappe import unittest - -# test_records = frappe.get_test_records('Stock Ledger Entry') +from frappe.utils import today, add_days +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation \ + import create_stock_reconciliation +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.stock_ledger import get_previous_sle +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import create_landed_cost_voucher +from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note +from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import BackDatedStockTransaction class TestStockLedgerEntry(unittest.TestCase): - pass + def setUp(self): + items = create_items() + + # delete SLE and BINs for all items + frappe.db.sql("delete from `tabStock Ledger Entry` where item_code in (%s)" % (', '.join(['%s']*len(items))), items) + frappe.db.sql("delete from `tabBin` where item_code in (%s)" % (', '.join(['%s']*len(items))), items) + + def test_item_cost_reposting(self): + company = "_Test Company" + + # _Test Item for Reposting at Stores warehouse on 10-04-2020: Qty = 50, Rate = 100 + create_stock_reconciliation( + item_code="_Test Item for Reposting", + warehouse="Stores - _TC", + qty=50, + rate=100, + company=company, + expense_account = "Stock Adjustment - _TC", + posting_date='2020-04-10', + posting_time='14:00' + ) + + # _Test Item for Reposting at FG warehouse on 20-04-2020: Qty = 10, Rate = 200 + create_stock_reconciliation( + item_code="_Test Item for Reposting", + warehouse="Finished Goods - _TC", + qty=10, + rate=200, + company=company, + expense_account = "Stock Adjustment - _TC", + posting_date='2020-04-20', + posting_time='14:00' + ) + + # _Test Item for Reposting transferred from Stores to FG warehouse on 30-04-2020 + make_stock_entry( + item_code="_Test Item for Reposting", + source="Stores - _TC", + target="Finished Goods - _TC", + company=company, + qty=10, + expense_account="Stock Adjustment - _TC", + posting_date='2020-04-30', + posting_time='14:00' + ) + target_wh_sle = get_previous_sle({ + "item_code": "_Test Item for Reposting", + "warehouse": "Finished Goods - _TC", + "posting_date": '2020-04-30', + "posting_time": '14:00' + }) + + self.assertEqual(target_wh_sle.get("valuation_rate"), 150) + + # Repack entry on 5-5-2020 + repack = create_repack_entry(company=company, posting_date='2020-05-05', posting_time='14:00') + + finished_item_sle = get_previous_sle({ + "item_code": "_Test Finished Item for Reposting", + "warehouse": "Finished Goods - _TC", + "posting_date": '2020-05-05', + "posting_time": '14:00' + }) + self.assertEqual(finished_item_sle.get("incoming_rate"), 540) + self.assertEqual(finished_item_sle.get("valuation_rate"), 540) + + # Reconciliation for _Test Item for Reposting at Stores on 12-04-2020: Qty = 50, Rate = 150 + create_stock_reconciliation( + item_code="_Test Item for Reposting", + warehouse="Stores - _TC", + qty=50, + rate=150, + company=company, + expense_account = "Stock Adjustment - _TC", + posting_date='2020-04-12', + posting_time='14:00' + ) + + + # Check valuation rate of finished goods warehouse after back-dated entry at Stores + target_wh_sle = get_previous_sle({ + "item_code": "_Test Item for Reposting", + "warehouse": "Finished Goods - _TC", + "posting_date": '2020-04-30', + "posting_time": '14:00' + }) + self.assertEqual(target_wh_sle.get("incoming_rate"), 150) + self.assertEqual(target_wh_sle.get("valuation_rate"), 175) + + # Check valuation rate of repacked item after back-dated entry at Stores + finished_item_sle = get_previous_sle({ + "item_code": "_Test Finished Item for Reposting", + "warehouse": "Finished Goods - _TC", + "posting_date": '2020-05-05', + "posting_time": '14:00' + }) + self.assertEqual(finished_item_sle.get("incoming_rate"), 790) + self.assertEqual(finished_item_sle.get("valuation_rate"), 790) + + # Check updated rate in Repack entry + repack.reload() + self.assertEqual(repack.items[0].get("basic_rate"), 150) + self.assertEqual(repack.items[1].get("basic_rate"), 750) + + def test_purchase_return_valuation_reposting(self): + pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-10', + warehouse="Stores - _TC", item_code="_Test Item for Reposting", qty=5, rate=100) + + return_pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-15', + warehouse="Stores - _TC", item_code="_Test Item for Reposting", is_return=1, return_against=pr.name, qty=-2) + + # check sle + outgoing_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", + "voucher_no": return_pr.name}, ["outgoing_rate", "stock_value_difference"]) + + self.assertEqual(outgoing_rate, 100) + self.assertEqual(stock_value_difference, -200) + + create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) + + outgoing_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", + "voucher_no": return_pr.name}, ["outgoing_rate", "stock_value_difference"]) + + self.assertEqual(outgoing_rate, 110) + self.assertEqual(stock_value_difference, -220) + + def test_sales_return_valuation_reposting(self): + company = "_Test Company" + item_code="_Test Item for Reposting" + + # Purchase Return: Qty = 5, Rate = 100 + pr = make_purchase_receipt(company=company, posting_date='2020-04-10', + warehouse="Stores - _TC", item_code=item_code, qty=5, rate=100) + + #Delivery Note: Qty = 5, Rate = 150 + dn = create_delivery_note(item_code=item_code, qty=5, rate=150, warehouse="Stores - _TC", + company=company, expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC") + + # check outgoing_rate for DN + outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note", + "voucher_no": dn.name}, "stock_value_difference") / 5) + + self.assertEqual(dn.items[0].incoming_rate, 100) + self.assertEqual(outgoing_rate, 100) + + # Return Entry: Qty = -2, Rate = 150 + return_dn = create_delivery_note(is_return=1, return_against=dn.name, item_code=item_code, qty=-2, rate=150, + company=company, warehouse="Stores - _TC", expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC") + + # check incoming rate for Return entry + incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": return_dn.name}, + ["incoming_rate", "stock_value_difference"]) + + self.assertEqual(return_dn.items[0].incoming_rate, 100) + self.assertEqual(incoming_rate, 100) + self.assertEqual(stock_value_difference, 200) + + #------------------------------- + + # Landed Cost Voucher to update the rate of incoming Purchase Return: Additional cost = 50 + lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) + + # check outgoing_rate for DN after reposting + outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note", + "voucher_no": dn.name}, "stock_value_difference") / 5) + self.assertEqual(outgoing_rate, 110) + + dn.reload() + self.assertEqual(dn.items[0].incoming_rate, 110) + + # check incoming rate for Return entry after reposting + incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": return_dn.name}, + ["incoming_rate", "stock_value_difference"]) + + self.assertEqual(incoming_rate, 110) + self.assertEqual(stock_value_difference, 220) + + return_dn.reload() + self.assertEqual(return_dn.items[0].incoming_rate, 110) + + # Cleanup data + return_dn.cancel() + dn.cancel() + lcv.cancel() + pr.cancel() + + def test_reposting_of_sales_return_for_packed_item(self): + company = "_Test Company" + packed_item_code="_Test Item for Reposting" + bundled_item = "_Test Bundled Item for Reposting" + create_product_bundle_item(bundled_item, [[packed_item_code, 4]]) + + # Purchase Return: Qty = 50, Rate = 100 + pr = make_purchase_receipt(company=company, posting_date='2020-04-10', + warehouse="Stores - _TC", item_code=packed_item_code, qty=50, rate=100) + + #Delivery Note: Qty = 5, Rate = 150 + dn = create_delivery_note(item_code=bundled_item, qty=5, rate=150, warehouse="Stores - _TC", + company=company, expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC") + + # check outgoing_rate for DN + outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note", + "voucher_no": dn.name}, "stock_value_difference") / 20) + + self.assertEqual(dn.packed_items[0].incoming_rate, 100) + self.assertEqual(outgoing_rate, 100) + + # Return Entry: Qty = -2, Rate = 150 + return_dn = create_delivery_note(is_return=1, return_against=dn.name, item_code=bundled_item, qty=-2, rate=150, + company=company, warehouse="Stores - _TC", expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC") + + # check incoming rate for Return entry + incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": return_dn.name}, + ["incoming_rate", "stock_value_difference"]) + + self.assertEqual(return_dn.packed_items[0].incoming_rate, 100) + self.assertEqual(incoming_rate, 100) + self.assertEqual(stock_value_difference, 800) + + #------------------------------- + + # Landed Cost Voucher to update the rate of incoming Purchase Return: Additional cost = 50 + lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) + + # check outgoing_rate for DN after reposting + outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note", + "voucher_no": dn.name}, "stock_value_difference") / 20) + self.assertEqual(outgoing_rate, 101) + + dn.reload() + self.assertEqual(dn.packed_items[0].incoming_rate, 101) + + # check incoming rate for Return entry after reposting + incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": return_dn.name}, + ["incoming_rate", "stock_value_difference"]) + + self.assertEqual(incoming_rate, 101) + self.assertEqual(stock_value_difference, 808) + + return_dn.reload() + self.assertEqual(return_dn.packed_items[0].incoming_rate, 101) + + # Cleanup data + return_dn.cancel() + dn.cancel() + lcv.cancel() + pr.cancel() + + def test_sub_contracted_item_costing(self): + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + + company = "_Test Company" + rm_item_code="_Test Item for Reposting" + subcontracted_item = "_Test Subcontracted Item for Reposting" + + frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM") + make_bom(item = subcontracted_item, raw_materials =[rm_item_code], currency="INR") + + # Purchase raw materials on supplier warehouse: Qty = 50, Rate = 100 + pr = make_purchase_receipt(company=company, posting_date='2020-04-10', + warehouse="Stores - _TC", item_code=rm_item_code, qty=10, rate=100) + + # Purchase Receipt for subcontracted item + pr1 = make_purchase_receipt(company=company, posting_date='2020-04-20', + warehouse="Finished Goods - _TC", supplier_warehouse="Stores - _TC", + item_code=subcontracted_item, qty=10, rate=20, is_subcontracted="Yes") + + self.assertEqual(pr1.items[0].valuation_rate, 120) + + # Update raw material's valuation via LCV, Additional cost = 50 + lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) + + pr1.reload() + self.assertEqual(pr1.items[0].valuation_rate, 125) + + # check outgoing_rate for DN after reposting + incoming_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", + "voucher_no": pr1.name, "item_code": subcontracted_item}, "incoming_rate") + self.assertEqual(incoming_rate, 125) + + # cleanup data + pr1.cancel() + lcv.cancel() + pr.cancel() + + def test_back_dated_entry_not_allowed(self): + # Back dated stock transactions are only allowed to stock managers + frappe.db.set_value("Stock Settings", None, + "role_allowed_to_create_edit_back_dated_transactions", "Stock Manager") + + # Set User with Stock User role but not Stock Manager + frappe.set_user("test@example.com") + user = frappe.get_doc("User", "test@example.com") + user.add_roles("Stock User") + user.remove_roles("Stock Manager") + + stock_entry_on_today = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100) + back_dated_se_1 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100, + posting_date=add_days(today(), -1), do_not_submit=True) + + # Block back-dated entry + self.assertRaises(BackDatedStockTransaction, back_dated_se_1.submit) + + user.add_roles("Stock Manager") + + # Back dated entry allowed to Stock Manager + back_dated_se_2 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100, + posting_date=add_days(today(), -1)) + + back_dated_se_2.cancel() + stock_entry_on_today.cancel() + + frappe.db.set_value("Stock Settings", None, "role_allowed_to_create_edit_back_dated_transactions", None) + frappe.set_user("Administrator") + + +def create_repack_entry(**args): + args = frappe._dict(args) + repack = frappe.new_doc("Stock Entry") + repack.stock_entry_type = "Repack" + repack.company = args.company or "_Test Company" + repack.posting_date = args.posting_date + repack.set_posting_time = 1 + repack.append("items", { + "item_code": "_Test Item for Reposting", + "s_warehouse": "Stores - _TC", + "qty": 5, + "conversion_factor": 1, + "expense_account": "Stock Adjustment - _TC", + "cost_center": "Main - _TC" + }) + + repack.append("items", { + "item_code": "_Test Finished Item for Reposting", + "t_warehouse": "Finished Goods - _TC", + "qty": 1, + "conversion_factor": 1, + "expense_account": "Stock Adjustment - _TC", + "cost_center": "Main - _TC" + }) + + repack.append("additional_costs", { + "expense_account": "Freight and Forwarding Charges - _TC", + "description": "transport cost", + "amount": 40 + }) + + repack.save() + repack.submit() + + return repack + +def create_product_bundle_item(new_item_code, packed_items): + if not frappe.db.exists("Product Bundle", new_item_code): + item = frappe.new_doc("Product Bundle") + item.new_item_code = new_item_code + + for d in packed_items: + item.append("items", { + "item_code": d[0], + "qty": d[1] + }) + + item.save() + +def create_items(): + items = ["_Test Item for Reposting", "_Test Finished Item for Reposting", + "_Test Subcontracted Item for Reposting", "_Test Bundled Item for Reposting"] + for d in items: + properties = {"valuation_method": "FIFO"} + if d == "_Test Bundled Item for Reposting": + properties.update({"is_stock_item": 0}) + elif d == "_Test Subcontracted Item for Reposting": + properties.update({"is_sub_contracted_item": 1}) + + make_item(d, properties=properties) + + return items \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 00b8f69c083..5b40292ea8f 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -37,14 +37,16 @@ class StockReconciliation(StockController): def on_submit(self): self.update_stock_ledger() self.make_gl_entries() + self.repost_future_sle_and_gle() from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit update_serial_nos_after_submit(self, "items") def on_cancel(self): - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') self.make_sle_on_cancel() self.make_gl_entries_on_cancel() + self.repost_future_sle_and_gle() def remove_items_with_no_change(self): """Remove items if qty or rate is not changed""" diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 23d48d4ac76..088456f8651 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -8,12 +8,11 @@ from __future__ import unicode_literals import frappe, unittest from frappe.utils import flt, nowdate, nowtime from erpnext.accounts.utils import get_stock_and_account_balance -from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.item.test_item import create_item -from erpnext.stock.utils import get_stock_balance, get_incoming_rate, get_available_serial_nos, get_stock_value_on +from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos class TestStockReconciliation(unittest.TestCase): @@ -29,16 +28,17 @@ class TestStockReconciliation(unittest.TestCase): self._test_reco_sle_gle("Moving Average") def _test_reco_sle_gle(self, valuation_method): - insert_existing_sle(warehouse='Stores - TCP1') + se1, se2, se3 = insert_existing_sle(warehouse='Stores - TCP1') company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') # [[qty, valuation_rate, posting_date, # posting_time, expected_stock_value, bin_qty, bin_valuation]] + input_data = [ - [50, 1000], - [25, 900], - ["", 1000], - [20, ""], - [0, ""] + [50, 1000, "2012-12-26", "12:00"], + [25, 900, "2012-12-26", "12:00"], + ["", 1000, "2012-12-20", "12:05"], + [20, "", "2012-12-26", "12:05"], + [0, "", "2012-12-31", "12:10"] ] for d in input_data: @@ -47,13 +47,13 @@ class TestStockReconciliation(unittest.TestCase): last_sle = get_previous_sle({ "item_code": "_Test Item", "warehouse": "Stores - TCP1", - "posting_date": nowdate(), - "posting_time": nowtime() + "posting_date": d[2], + "posting_time": d[3] }) # submit stock reconciliation stock_reco = create_stock_reconciliation(qty=d[0], rate=d[1], - posting_date=nowdate(), posting_time=nowtime(), warehouse="Stores - TCP1", + posting_date=d[2], posting_time=d[3], warehouse="Stores - TCP1", company=company, expense_account = "Stock Adjustment - TCP1") # check stock value @@ -81,10 +81,15 @@ class TestStockReconciliation(unittest.TestCase): stock_reco.cancel() + se3.cancel() + se2.cancel() + se1.cancel() + def test_get_items(self): - create_warehouse("_Test Warehouse Group 1", {"is_group": 1}) + create_warehouse("_Test Warehouse Group 1", + {"is_group": 1, "company": "_Test Company", "parent_warehouse": "All Warehouses - _TC"}) create_warehouse("_Test Warehouse Ledger 1", - {"is_group": 0, "parent_warehouse": "_Test Warehouse Group 1 - _TC"}) + {"is_group": 0, "parent_warehouse": "_Test Warehouse Group 1 - _TC", "company": "_Test Company"}) create_item("_Test Stock Reco Item", is_stock_item=1, valuation_rate=100, warehouse="_Test Warehouse Ledger 1 - _TC", opening_stock=100) @@ -95,8 +100,6 @@ class TestStockReconciliation(unittest.TestCase): [items[0]["item_code"], items[0]["warehouse"], items[0]["qty"]]) def test_stock_reco_for_serialized_item(self): - set_perpetual_inventory() - to_delete_records = [] to_delete_serial_nos = [] @@ -148,8 +151,6 @@ class TestStockReconciliation(unittest.TestCase): stock_doc.cancel() def test_stock_reco_for_batch_item(self): - set_perpetual_inventory() - to_delete_records = [] to_delete_serial_nos = [] @@ -196,15 +197,17 @@ class TestStockReconciliation(unittest.TestCase): def insert_existing_sle(warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - make_stock_entry(posting_date=nowdate(), posting_time=nowtime(), item_code="_Test Item", + se1 = make_stock_entry(posting_date="2012-12-15", posting_time="02:00", item_code="_Test Item", target=warehouse, qty=10, basic_rate=700) - make_stock_entry(posting_date=nowdate(), posting_time=nowtime(), item_code="_Test Item", + se2 = make_stock_entry(posting_date="2012-12-25", posting_time="03:00", item_code="_Test Item", source=warehouse, qty=15) - make_stock_entry(posting_date=nowdate(), posting_time=nowtime(), item_code="_Test Item", + se3 = make_stock_entry(posting_date="2013-01-05", posting_time="07:00", item_code="_Test Item", target=warehouse, qty=15, basic_rate=1200) + return se1, se2, se3 + def create_batch_or_serial_no_items(): create_warehouse("_Test Warehouse for Stock Reco1", {"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"}) @@ -256,6 +259,10 @@ def create_stock_reconciliation(**args): return sr def set_valuation_method(item_code, valuation_method): + existing_valuation_method = get_valuation_method(item_code) + if valuation_method == existing_valuation_method: + return + frappe.db.set_value("Item", item_code, "valuation_method", valuation_method) for warehouse in frappe.get_all("Warehouse", filters={"company": "_Test Company"}, fields=["name", "is_group"]): diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index a1666579d12..859aea2eb60 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -28,7 +28,9 @@ "inter_warehouse_transfer_settings_section", "allow_from_dn", "allow_from_pr", - "freeze_stock_entries", + "control_historical_stock_transactions_section", + "role_allowed_to_create_edit_back_dated_transactions", + "column_break_26", "stock_frozen_upto", "stock_frozen_upto_days", "stock_auth_role", @@ -156,21 +158,20 @@ "label": "Notify by Email on Creation of Automatic Material Request" }, { - "fieldname": "freeze_stock_entries", - "fieldtype": "Section Break", - "label": "Freeze Stock Entries" - }, - { + "description": "No stock transactions can be created or modified before this date.", "fieldname": "stock_frozen_upto", "fieldtype": "Date", "label": "Stock Frozen Upto" }, { + "description": "Stock transactions that are older than the mentioned days cannot be modified.", "fieldname": "stock_frozen_upto_days", "fieldtype": "Int", "label": "Freeze Stocks Older Than (Days)" }, { + "depends_on": "eval:(doc.stock_frozen_upto || doc.stock_frozen_upto_days)", + "description": "The users with this Role are allowed to create/modify a stock transaction, even though the transaction is frozen.", "fieldname": "stock_auth_role", "fieldtype": "Link", "label": "Role Allowed to Edit Frozen Stock", @@ -210,6 +211,22 @@ "fieldname": "allow_from_pr", "fieldtype": "Check", "label": "Allow Material Transfer from Purchase Receipt to Purchase Invoice" + }, + { + "description": "If mentioned, the system will allow only the users with this Role to create or modify any stock transaction earlier than the latest stock transaction for a specific item and warehouse. If set as blank, it allows all users to create/edit back-dated transactions.", + "fieldname": "role_allowed_to_create_edit_back_dated_transactions", + "fieldtype": "Link", + "label": "Role Allowed to Create/Edit Back-dated Transactions", + "options": "User" + }, + { + "fieldname": "column_break_26", + "fieldtype": "Column Break" + }, + { + "fieldname": "control_historical_stock_transactions_section", + "fieldtype": "Section Break", + "label": "Control Historical Stock Transactions" } ], "icon": "icon-cog", @@ -217,7 +234,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-11-23 15:26:54.225608", + "modified": "2020-11-23 22:26:54.225608", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py index 3101e8af4c7..95478f61f0a 100644 --- a/erpnext/stock/doctype/warehouse/test_warehouse.py +++ b/erpnext/stock/doctype/warehouse/test_warehouse.py @@ -10,13 +10,10 @@ from frappe.test_runner import make_test_records import erpnext from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry -from erpnext import set_perpetual_inventory from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account - test_records = frappe.get_test_records('Warehouse') - class TestWarehouse(unittest.TestCase): def setUp(self): if not frappe.get_value('Item', '_Test Item'): @@ -37,63 +34,63 @@ class TestWarehouse(unittest.TestCase): self.assertEqual(child_warehouse.is_group, 0) def test_warehouse_renaming(self): - set_perpetual_inventory(1) - create_warehouse("Test Warehouse for Renaming 1") - account = get_inventory_account("_Test Company", "Test Warehouse for Renaming 1 - _TC") + create_warehouse("Test Warehouse for Renaming 1", company="_Test Company with perpetual inventory") + account = get_inventory_account("_Test Company with perpetual inventory", "Test Warehouse for Renaming 1 - TCP1") self.assertTrue(frappe.db.get_value("Warehouse", filters={"account": account})) # Rename with abbr - if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 2 - _TC"): - frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 2 - _TC") - frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 1 - _TC", "Test Warehouse for Renaming 2 - _TC") + if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 2 - TCP1"): + frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1") + frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 1 - TCP1", "Test Warehouse for Renaming 2 - TCP1") self.assertTrue(frappe.db.get_value("Warehouse", - filters={"account": "Test Warehouse for Renaming 1 - _TC"})) + filters={"account": "Test Warehouse for Renaming 1 - TCP1"})) # Rename without abbr - if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 3 - _TC"): - frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 3 - _TC") + if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 3 - TCP1"): + frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1") - frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 2 - _TC", "Test Warehouse for Renaming 3") + frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1", "Test Warehouse for Renaming 3") self.assertTrue(frappe.db.get_value("Warehouse", - filters={"account": "Test Warehouse for Renaming 1 - _TC"})) + filters={"account": "Test Warehouse for Renaming 1 - TCP1"})) # Another rename with multiple dashes - if frappe.db.exists("Warehouse", "Test - Warehouse - Company - _TC"): - frappe.delete_doc("Warehouse", "Test - Warehouse - Company - _TC") - frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 3 - _TC", "Test - Warehouse - Company") + if frappe.db.exists("Warehouse", "Test - Warehouse - Company - TCP1"): + frappe.delete_doc("Warehouse", "Test - Warehouse - Company - TCP1") + frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1", "Test - Warehouse - Company") def test_warehouse_merging(self): - set_perpetual_inventory(1) + company = "_Test Company with perpetual inventory" + create_warehouse("Test Warehouse for Merging 1", company=company, + properties={"parent_warehouse": "All Warehouses - TCP1"}) + create_warehouse("Test Warehouse for Merging 2", company=company, + properties={"parent_warehouse": "All Warehouses - TCP1"}) - create_warehouse("Test Warehouse for Merging 1") - create_warehouse("Test Warehouse for Merging 2") - - make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 1 - _TC", - qty=1, rate=100) - make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 2 - _TC", - qty=1, rate=100) + make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 1 - TCP1", + qty=1, rate=100, company=company) + make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 2 - TCP1", + qty=1, rate=100, company=company) existing_bin_qty = ( cint(frappe.db.get_value("Bin", - {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 1 - _TC"}, "actual_qty")) + {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 1 - TCP1"}, "actual_qty")) + cint(frappe.db.get_value("Bin", - {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - _TC"}, "actual_qty")) + {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty")) ) - frappe.rename_doc("Warehouse", "Test Warehouse for Merging 1 - _TC", - "Test Warehouse for Merging 2 - _TC", merge=True) + frappe.rename_doc("Warehouse", "Test Warehouse for Merging 1 - TCP1", + "Test Warehouse for Merging 2 - TCP1", merge=True) - self.assertFalse(frappe.db.exists("Warehouse", "Test Warehouse for Merging 1 - _TC")) + self.assertFalse(frappe.db.exists("Warehouse", "Test Warehouse for Merging 1 - TCP1")) bin_qty = frappe.db.get_value("Bin", - {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - _TC"}, "actual_qty") + {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty") self.assertEqual(bin_qty, existing_bin_qty) self.assertTrue(frappe.db.get_value("Warehouse", - filters={"account": "Test Warehouse for Merging 2 - _TC"})) + filters={"account": "Test Warehouse for Merging 2 - TCP1"})) def create_warehouse(warehouse_name, properties=None, company=None): if not company: diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index cd86be31150..6c84f168fd4 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -29,7 +29,6 @@ class Warehouse(NestedSet): self.set_onload('account', account) load_address_and_contact(self) - def on_update(self): self.update_nsm_model() diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.py b/erpnext/stock/report/stock_analytics/stock_analytics.py index 54eefdfaaa4..0cc8ca48aac 100644 --- a/erpnext/stock/report/stock_analytics/stock_analytics.py +++ b/erpnext/stock/report/stock_analytics/stock_analytics.py @@ -7,9 +7,11 @@ from frappe import _, scrub from frappe.utils import getdate, flt from erpnext.stock.report.stock_balance.stock_balance import (get_items, get_stock_ledger_entries, get_item_details) from erpnext.accounts.utils import get_fiscal_year +from erpnext.stock.utils import is_reposting_item_valuation_in_progress from six import iteritems def execute(filters=None): + is_reposting_item_valuation_in_progress() filters = frappe._dict(filters or {}) columns = get_columns(filters) data = get_data(filters) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index ccd01001bb7..e5d4d626c47 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -7,12 +7,13 @@ from frappe import _ from frappe.utils import flt, cint, getdate, now, date_diff from erpnext.stock.utils import add_additional_uom_columns from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition - +from erpnext.stock.utils import is_reposting_item_valuation_in_progress from erpnext.stock.report.stock_ageing.stock_ageing import get_fifo_queue, get_average_age from six import iteritems def execute(filters=None): + is_reposting_item_valuation_in_progress() if not filters: filters = {} validate_filters(filters) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 86af5e0c868..7b5701a9932 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -5,11 +5,12 @@ from __future__ import unicode_literals import frappe from frappe.utils import cint, flt -from erpnext.stock.utils import update_included_uom_in_report +from erpnext.stock.utils import update_included_uom_in_report, is_reposting_item_valuation_in_progress from frappe import _ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos def execute(filters=None): + is_reposting_item_valuation_in_progress() include_uom = filters.get("include_uom") columns = get_columns() items = get_items(filters) diff --git a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py index c8efb1637f9..1183e41d041 100644 --- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py +++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py @@ -5,9 +5,10 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.utils import flt, today -from erpnext.stock.utils import update_included_uom_in_report +from erpnext.stock.utils import update_included_uom_in_report, is_reposting_item_valuation_in_progress def execute(filters=None): + is_reposting_item_valuation_in_progress() filters = frappe._dict(filters or {}) include_uom = filters.get("include_uom") columns = get_columns() diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py index ebcb106b02a..04f7d347ba8 100644 --- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py +++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py @@ -11,9 +11,11 @@ from frappe.utils import flt, cint, getdate from erpnext.stock.report.stock_balance.stock_balance import (get_item_details, get_item_reorder_details, get_item_warehouse_map, get_items, get_stock_ledger_entries) from erpnext.stock.report.stock_ageing.stock_ageing import get_fifo_queue, get_average_age +from erpnext.stock.utils import is_reposting_item_valuation_in_progress from six import iteritems def execute(filters=None): + is_reposting_item_valuation_in_progress() if not filters: filters = {} validate_filters(filters) diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index b5ae1b78eb4..8ba1f1ca5c7 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -6,6 +6,7 @@ import frappe from frappe.utils import flt, cstr, nowdate, nowtime from erpnext.stock.utils import update_bin from erpnext.stock.stock_ledger import update_entries_after +from erpnext.controllers.stock_controller import create_repost_item_valuation_entry def repost(only_actual=False, allow_negative_stock=False, allow_zero_rate=False, only_bin=False): """ @@ -56,12 +57,18 @@ def repost_stock(item_code, warehouse, allow_zero_rate=False, update_bin_qty(item_code, warehouse, qty_dict) def repost_actual_qty(item_code, warehouse, allow_zero_rate=False, allow_negative_stock=False): - update_entries_after({ "item_code": item_code, "warehouse": warehouse }, - allow_zero_rate=allow_zero_rate, allow_negative_stock=allow_negative_stock) + create_repost_item_valuation_entry({ + "item_code": item_code, + "warehouse": warehouse, + "posting_date": "1900-01-01", + "posting_time": "00:01", + "allow_negative_stock": allow_negative_stock, + "allow_zero_rate": allow_zero_rate + }) def get_balance_qty_from_sle(item_code, warehouse): balance_qty = frappe.db.sql("""select qty_after_transaction from `tabStock Ledger Entry` - where item_code=%s and warehouse=%s + where item_code=%s and warehouse=%s and is_cancelled=0 order by posting_date desc, posting_time desc, creation desc limit 1""", (item_code, warehouse)) @@ -191,7 +198,7 @@ def set_stock_balance_as_per_serial_no(item_code=None, posting_date=None, postin print(d[0], d[1], d[2], serial_nos[0][0]) sle = frappe.db.sql("""select valuation_rate, company from `tabStock Ledger Entry` - where item_code = %s and warehouse = %s + where item_code = %s and warehouse = %s and is_cancelled = 0 order by posting_date desc limit 1""", (d[0], d[1])) sle_dict = { @@ -223,7 +230,8 @@ def set_stock_balance_as_per_serial_no(item_code=None, posting_date=None, postin }) update_bin(args) - update_entries_after({ + + create_repost_item_valuation_entry({ "item_code": d[0], "warehouse": d[1], "posting_date": posting_date, diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index f4490f1b01e..5b9ada0ee56 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -5,9 +5,10 @@ from __future__ import unicode_literals import frappe, erpnext from frappe import _ from frappe.utils import cint, flt, cstr, now, now_datetime +from frappe.model.meta import get_field_precision from erpnext.stock.utils import get_valuation_method, get_incoming_outgoing_rate_for_cancel +from erpnext.stock.utils import get_bin import json - from six import iteritems # future reposting @@ -25,32 +26,23 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc set_as_cancel(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no')) for sle in sl_entries: - sle_id = None - if via_landed_cost_voucher or cancel: - sle['posting_date'] = now_datetime().strftime('%Y-%m-%d') - sle['posting_time'] = now_datetime().strftime('%H:%M:%S.%f') + if cancel: + sle['actual_qty'] = -flt(sle.get('actual_qty')) - if cancel: - sle['actual_qty'] = -flt(sle.get('actual_qty')) - - if sle['actual_qty'] < 0 and not sle.get('outgoing_rate'): - sle['outgoing_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code, - sle.voucher_type, sle.voucher_no, sle.voucher_detail_no) - sle['incoming_rate'] = 0.0 - - if sle['actual_qty'] > 0 and not sle.get('incoming_rate'): - sle['incoming_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code, - sle.voucher_type, sle.voucher_no, sle.voucher_detail_no) - sle['outgoing_rate'] = 0.0 + if sle['actual_qty'] < 0 and not sle.get('outgoing_rate'): + sle['outgoing_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code, + sle.voucher_type, sle.voucher_no, sle.voucher_detail_no) + sle['incoming_rate'] = 0.0 + if sle['actual_qty'] > 0 and not sle.get('incoming_rate'): + sle['incoming_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code, + sle.voucher_type, sle.voucher_no, sle.voucher_detail_no) + sle['outgoing_rate'] = 0.0 if sle.get("actual_qty") or sle.get("voucher_type")=="Stock Reconciliation": - sle_id = make_entry(sle, allow_negative_stock, via_landed_cost_voucher) - - args = sle.copy() - args.update({ - "sle_id": sle_id - }) + 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) @@ -68,8 +60,36 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False): sle.via_landed_cost_voucher = via_landed_cost_voucher sle.insert() sle.submit() - return sle.name + return sle +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 + while i < len(args): + obj = update_entries_after({ + "item_code": args[i].item_code, + "warehouse": args[i].warehouse, + "posting_date": args[i].posting_date, + "posting_time": args[i].posting_time + }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) + + 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): + return frappe.db.get_all("Stock Ledger Entry", + filters={"voucher_type": voucher_type, "voucher_no": voucher_no}, + fields=["item_code", "warehouse", "posting_date", "posting_time"], + order_by="creation asc", + group_by="item_code, warehouse" + ) class update_entries_after(object): """ @@ -86,141 +106,299 @@ class update_entries_after(object): } """ def __init__(self, args, allow_zero_rate=False, allow_negative_stock=None, via_landed_cost_voucher=False, verbose=1): - from frappe.model.meta import get_field_precision - - self.exceptions = [] + self.exceptions = {} self.verbose = verbose self.allow_zero_rate = allow_zero_rate - self.allow_negative_stock = allow_negative_stock self.via_landed_cost_voucher = via_landed_cost_voucher - if not self.allow_negative_stock: - self.allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", - "allow_negative_stock")) + self.allow_negative_stock = allow_negative_stock \ + or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) - self.args = args - for key, value in iteritems(args): - setattr(self, key, value) + self.args = frappe._dict(args) + self.item_code = args.get("item_code") + if self.args.sle_id: + self.args['name'] = self.args.sle_id - self.previous_sle = self.get_sle_before_datetime() - self.previous_sle = self.previous_sle[0] if self.previous_sle else frappe._dict() + self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company") + self.get_precision() + self.valuation_method = get_valuation_method(self.item_code) + self.new_items = {} + + self.data = frappe._dict() + 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"), + currency=company_base_currency) + + def initialize_previous_data(self, args): + """ + Get previous sl entries for current item for each related warehouse + and assigns into self.data dict + + :Data Structure: + + self.data = { + warehouse1: { + 'previus_sle': {}, + 'qty_after_transaction': 10, + 'valuation_rate': 100, + 'stock_value': 1000, + 'prev_stock_value': 1000, + 'stock_queue': '[[10, 100]]', + 'stock_value_difference': 1000 + } + } + + """ + self.data.setdefault(args.warehouse, frappe._dict()) + warehouse_dict = self.data[args.warehouse] + previous_sle = self.get_sle_before_datetime(args) + warehouse_dict.previous_sle = previous_sle for key in ("qty_after_transaction", "valuation_rate", "stock_value"): - setattr(self, key, flt(self.previous_sle.get(key))) + setattr(warehouse_dict, key, flt(previous_sle.get(key))) - self.company = frappe.db.get_value("Warehouse", self.warehouse, "company") - self.precision = get_field_precision(frappe.get_meta("Stock Ledger Entry").get_field("stock_value"), - currency=frappe.get_cached_value('Company', self.company, "default_currency")) + warehouse_dict.update({ + "prev_stock_value": previous_sle.stock_value or 0.0, + "stock_queue": json.loads(previous_sle.stock_queue or "[]"), + "stock_value_difference": 0.0 + }) - self.prev_stock_value = self.previous_sle.stock_value or 0.0 - self.stock_queue = json.loads(self.previous_sle.stock_queue or "[]") - self.valuation_method = get_valuation_method(self.item_code) - self.stock_value_difference = 0.0 - self.build(args.get('sle_id')) - - def build(self, sle_id): - if sle_id: - sle = get_sle_by_id(sle_id) - self.process_sle(sle) + def build(self): + if self.args.get("sle_id"): + self.process_sle_against_current_voucher() else: - # includes current entry! - entries_to_fix = self.get_sle_after_datetime() - for sle in entries_to_fix: + entries_to_fix = self.get_future_entries_to_fix() + + i = 0 + while i < len(entries_to_fix): + sle = entries_to_fix[i] + i += 1 + self.process_sle(sle) + if sle.dependant_sle_voucher_detail_no: + self.get_dependent_entries_to_fix(entries_to_fix, sle) + if self.exceptions: self.raise_exceptions() self.update_bin() - def update_bin(self): - # update bin - bin_name = frappe.db.get_value("Bin", { - "item_code": self.item_code, - "warehouse": self.warehouse - }) + def process_sle_against_current_voucher(self): + sl_entries = self.get_sle_against_current_voucher() + for sle in sl_entries: + self.process_sle(sle) - if not bin_name: - bin_doc = frappe.get_doc({ - "doctype": "Bin", - "item_code": self.item_code, - "warehouse": self.warehouse - }) - bin_doc.insert(ignore_permissions=True) - else: - bin_doc = frappe.get_doc("Bin", bin_name) + def get_sle_against_current_voucher(self): + return frappe.db.sql(""" + select + *, timestamp(posting_date, posting_time) as "timestamp" + from + `tabStock Ledger Entry` + where + item_code = %(item_code)s + and warehouse = %(warehouse)s + and voucher_type = %(voucher_type)s + and voucher_no = %(voucher_no)s + order by + creation ASC + for update + """, self.args, as_dict=1) - bin_doc.update({ - "valuation_rate": self.valuation_rate, - "actual_qty": self.qty_after_transaction, - "stock_value": self.stock_value - }) - bin_doc.flags.via_stock_ledger_entry = True + def get_future_entries_to_fix(self): + # 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)) - bin_doc.save(ignore_permissions=True) + 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: + return + elif dependant_sle.item_code != self.item_code \ + and (dependant_sle.item_code, dependant_sle.warehouse) not in self.new_items: + self.new_items[(dependant_sle.item_code, dependant_sle.warehouse)] = dependant_sle + return + + self.initialize_previous_data(dependant_sle) + + args = self.data[dependant_sle.warehouse].previous_sle \ + or frappe._dict({"item_code": self.item_code, "warehouse": dependant_sle.warehouse}) + future_sle_for_dependant = list(self.get_sle_after_datetime(args)) + + entries_to_fix.extend(future_sle_for_dependant) + entries_to_fix = sorted(entries_to_fix, key=lambda k: k['timestamp']) def process_sle(self, sle): + # previous sle data for this warehouse + self.wh_data = self.data[sle.warehouse] + if (sle.serial_no and not self.via_landed_cost_voucher) or not cint(self.allow_negative_stock): # validate negative stock for serialized items, fifo valuation # or when negative stock is not allowed for moving average if not self.validate_negative_stock(sle): - self.qty_after_transaction += flt(sle.actual_qty) + self.wh_data.qty_after_transaction += flt(sle.actual_qty) return + # Get dynamic incoming/outgoing rate + self.get_dynamic_incoming_outgoing_rate(sle) + if sle.serial_no: self.get_serialized_values(sle) - self.qty_after_transaction += flt(sle.actual_qty) + self.wh_data.qty_after_transaction += flt(sle.actual_qty) if sle.voucher_type == "Stock Reconciliation": - self.qty_after_transaction = sle.qty_after_transaction + self.wh_data.qty_after_transaction = sle.qty_after_transaction - self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate) + self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate) else: if sle.voucher_type=="Stock Reconciliation" and not sle.batch_no: # assert - self.valuation_rate = sle.valuation_rate - self.qty_after_transaction = sle.qty_after_transaction - self.stock_queue = [[self.qty_after_transaction, self.valuation_rate]] - self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate) + self.wh_data.valuation_rate = sle.valuation_rate + self.wh_data.qty_after_transaction = sle.qty_after_transaction + self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]] + self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate) else: if self.valuation_method == "Moving Average": self.get_moving_average_values(sle) - self.qty_after_transaction += flt(sle.actual_qty) - self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate) + self.wh_data.qty_after_transaction += flt(sle.actual_qty) + self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate) else: self.get_fifo_values(sle) - self.qty_after_transaction += flt(sle.actual_qty) - self.stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in self.stock_queue)) + self.wh_data.qty_after_transaction += flt(sle.actual_qty) + self.wh_data.stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in self.wh_data.stock_queue)) # rounding as per precision - self.stock_value = flt(self.stock_value, self.precision) - - stock_value_difference = self.stock_value - self.prev_stock_value - - self.prev_stock_value = self.stock_value + self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision) + stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value + self.wh_data.prev_stock_value = self.wh_data.stock_value # update current sle - sle.qty_after_transaction = self.qty_after_transaction - sle.valuation_rate = self.valuation_rate - sle.stock_value = self.stock_value - sle.stock_queue = json.dumps(self.stock_queue) + sle.qty_after_transaction = self.wh_data.qty_after_transaction + sle.valuation_rate = self.wh_data.valuation_rate + sle.stock_value = self.wh_data.stock_value + sle.stock_queue = json.dumps(self.wh_data.stock_queue) sle.stock_value_difference = stock_value_difference sle.doctype="Stock Ledger Entry" frappe.get_doc(sle).db_update() + self.update_outgoing_rate_on_transaction(sle) + def validate_negative_stock(self, sle): """ validate negative stock for entries current datetime onwards will not consider cancelled entries """ - diff = self.qty_after_transaction + flt(sle.actual_qty) + diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) if diff < 0 and abs(diff) > 0.0001: # negative stock! exc = sle.copy().update({"diff": diff}) - self.exceptions.append(exc) + self.exceptions.setdefault(sle.warehouse, []).append(exc) return False else: return True + def get_dynamic_incoming_outgoing_rate(self, sle): + # Get updated incoming/outgoing rate from transaction + if sle.recalculate_rate: + rate = self.get_incoming_outgoing_rate_from_transaction(sle) + + if flt(sle.actual_qty) >= 0: + sle.incoming_rate = rate + else: + sle.outgoing_rate = rate + + def get_incoming_outgoing_rate_from_transaction(self, sle): + rate = 0 + # Material Transfer, Repack, Manufacturing + if sle.voucher_type == "Stock Entry": + rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate") + # Sales and Purchase Return + elif sle.voucher_type in ("Purchase Receipt", "Purchase Invoice", "Delivery Note", "Sales Invoice"): + if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"): + from erpnext.controllers.sales_and_purchase_return import get_rate_for_return # don't move this import to top + 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" + else: + rate_field = "incoming_rate" + + # check in item table + item_code, incoming_rate = frappe.db.get_value(sle.voucher_type + " Item", + sle.voucher_detail_no, ["item_code", rate_field]) + + if item_code == sle.item_code: + rate = incoming_rate + else: + if sle.voucher_type in ("Delivery Note", "Sales Invoice"): + 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) + + return rate + + def update_outgoing_rate_on_transaction(self, sle): + """ + Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return + In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount + """ + if sle.actual_qty and sle.voucher_detail_no: + outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty) + + if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry": + self.update_rate_on_stock_entry(sle, outgoing_rate) + elif sle.voucher_type in ("Delivery Note", "Sales Invoice"): + self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate) + elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"): + self.update_rate_on_purchase_receipt(sle, outgoing_rate) + + def update_rate_on_stock_entry(self, sle, outgoing_rate): + frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate) + + # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount + stock_entry = frappe.get_doc("Stock Entry", sle.voucher_no) + stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False) + 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") + if item_code == sle.item_code: + frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate) + else: + # packed item + frappe.db.set_value("Packed Item", + {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code}, + "incoming_rate", outgoing_rate) + + def update_rate_on_purchase_receipt(self, sle, outgoing_rate): + if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no): + frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate) + else: + frappe.db.set_value("Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate) + + # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice + if frappe.db.get_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"): + doc = frappe.get_cached_doc(sle.voucher_type, sle.voucher_no) + doc.update_valuation_rate(reset_outgoing_rate=False) + for d in (doc.items + doc.supplied_items): + d.db_update() + def get_serialized_values(self, sle): incoming_rate = flt(sle.incoming_rate) actual_qty = flt(sle.actual_qty) @@ -228,7 +406,7 @@ class update_entries_after(object): if incoming_rate < 0: # wrong incoming rate - incoming_rate = self.valuation_rate + incoming_rate = self.wh_data.valuation_rate stock_value_change = 0 if incoming_rate: @@ -236,22 +414,25 @@ class update_entries_after(object): elif actual_qty < 0: # In case of delivery/stock issue, get average purchase rate # of serial nos of current entry - outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos) - stock_value_change = -1 * outgoing_value + if not sle.is_cancelled: + outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos) + stock_value_change = -1 * outgoing_value + else: + stock_value_change = actual_qty * sle.outgoing_rate - new_stock_qty = self.qty_after_transaction + actual_qty + new_stock_qty = self.wh_data.qty_after_transaction + actual_qty if new_stock_qty > 0: - new_stock_value = (self.qty_after_transaction * self.valuation_rate) + stock_value_change + new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + stock_value_change if new_stock_value >= 0: # calculate new valuation rate only if stock value is positive # else it remains the same as that of previous entry - self.valuation_rate = new_stock_value / new_stock_qty + self.wh_data.valuation_rate = new_stock_value / new_stock_qty - if not self.valuation_rate and sle.voucher_detail_no: + if not self.wh_data.valuation_rate and sle.voucher_detail_no: allow_zero_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no) if not allow_zero_rate: - self.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, + self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, sle.voucher_type, sle.voucher_no, self.allow_zero_rate, currency=erpnext.get_company_currency(sle.company)) @@ -287,39 +468,39 @@ class update_entries_after(object): def get_moving_average_values(self, sle): actual_qty = flt(sle.actual_qty) - new_stock_qty = flt(self.qty_after_transaction) + actual_qty + new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty if new_stock_qty >= 0: if actual_qty > 0: - if flt(self.qty_after_transaction) <= 0: - self.valuation_rate = sle.incoming_rate + if flt(self.wh_data.qty_after_transaction) <= 0: + self.wh_data.valuation_rate = sle.incoming_rate else: - new_stock_value = (self.qty_after_transaction * self.valuation_rate) + \ + new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + \ (actual_qty * sle.incoming_rate) - self.valuation_rate = new_stock_value / new_stock_qty + self.wh_data.valuation_rate = new_stock_value / new_stock_qty elif sle.outgoing_rate: if new_stock_qty: - new_stock_value = (self.qty_after_transaction * self.valuation_rate) + \ + new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + \ (actual_qty * sle.outgoing_rate) - self.valuation_rate = new_stock_value / new_stock_qty + self.wh_data.valuation_rate = new_stock_value / new_stock_qty else: - self.valuation_rate = sle.outgoing_rate + self.wh_data.valuation_rate = sle.outgoing_rate else: - if flt(self.qty_after_transaction) >= 0 and sle.outgoing_rate: - self.valuation_rate = sle.outgoing_rate + if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate: + self.wh_data.valuation_rate = sle.outgoing_rate - if not self.valuation_rate and actual_qty > 0: - self.valuation_rate = sle.incoming_rate + if not self.wh_data.valuation_rate and actual_qty > 0: + self.wh_data.valuation_rate = sle.incoming_rate # Get valuation rate from previous SLE or Item master, if item does not have the # allow zero valuration rate flag set - if not self.valuation_rate and sle.voucher_detail_no: + if not self.wh_data.valuation_rate and sle.voucher_detail_no: allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no) if not allow_zero_valuation_rate: - self.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, + self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, sle.voucher_type, sle.voucher_no, self.allow_zero_rate, currency=erpnext.get_company_currency(sle.company)) @@ -329,22 +510,22 @@ class update_entries_after(object): outgoing_rate = flt(sle.outgoing_rate) if actual_qty > 0: - if not self.stock_queue: - self.stock_queue.append([0, 0]) + if not self.wh_data.stock_queue: + self.wh_data.stock_queue.append([0, 0]) # last row has the same rate, just updated the qty - if self.stock_queue[-1][1]==incoming_rate: - self.stock_queue[-1][0] += actual_qty + if self.wh_data.stock_queue[-1][1]==incoming_rate: + self.wh_data.stock_queue[-1][0] += actual_qty else: - if self.stock_queue[-1][0] > 0: - self.stock_queue.append([actual_qty, incoming_rate]) + if self.wh_data.stock_queue[-1][0] > 0: + self.wh_data.stock_queue.append([actual_qty, incoming_rate]) else: - qty = self.stock_queue[-1][0] + actual_qty - self.stock_queue[-1] = [qty, incoming_rate] + qty = self.wh_data.stock_queue[-1][0] + actual_qty + self.wh_data.stock_queue[-1] = [qty, incoming_rate] else: qty_to_pop = abs(actual_qty) while qty_to_pop: - if not self.stock_queue: + if not self.wh_data.stock_queue: # Get valuation rate from last sle if exists or from valuation rate field in item master allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no) if not allow_zero_valuation_rate: @@ -354,35 +535,35 @@ class update_entries_after(object): else: _rate = 0 - self.stock_queue.append([0, _rate]) + self.wh_data.stock_queue.append([0, _rate]) index = None if outgoing_rate > 0: # Find the entry where rate matched with outgoing rate - for i, v in enumerate(self.stock_queue): + for i, v in enumerate(self.wh_data.stock_queue): if v[1] == outgoing_rate: index = i break # If no entry found with outgoing rate, collapse stack if index == None: - new_stock_value = sum((d[0]*d[1] for d in self.stock_queue)) - qty_to_pop*outgoing_rate - new_stock_qty = sum((d[0] for d in self.stock_queue)) - qty_to_pop - self.stock_queue = [[new_stock_qty, new_stock_value/new_stock_qty if new_stock_qty > 0 else outgoing_rate]] + new_stock_value = sum((d[0]*d[1] for d in self.wh_data.stock_queue)) - qty_to_pop*outgoing_rate + new_stock_qty = sum((d[0] for d in self.wh_data.stock_queue)) - qty_to_pop + self.wh_data.stock_queue = [[new_stock_qty, new_stock_value/new_stock_qty if new_stock_qty > 0 else outgoing_rate]] break else: index = 0 # select first batch or the batch with same rate - batch = self.stock_queue[index] + batch = self.wh_data.stock_queue[index] if qty_to_pop >= batch[0]: # consume current batch qty_to_pop = qty_to_pop - batch[0] - self.stock_queue.pop(index) - if not self.stock_queue and qty_to_pop: + self.wh_data.stock_queue.pop(index) + if not self.wh_data.stock_queue and qty_to_pop: # stock finished, qty still remains to be withdrawn # negative stock, keep in as a negative batch - self.stock_queue.append([-qty_to_pop, outgoing_rate or batch[1]]) + self.wh_data.stock_queue.append([-qty_to_pop, outgoing_rate or batch[1]]) break else: @@ -391,14 +572,14 @@ class update_entries_after(object): batch[0] = batch[0] - qty_to_pop qty_to_pop = 0 - stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in self.stock_queue)) - stock_qty = sum((flt(batch[0]) for batch in self.stock_queue)) + stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in self.wh_data.stock_queue)) + stock_qty = sum((flt(batch[0]) for batch in self.wh_data.stock_queue)) if stock_qty: - self.valuation_rate = stock_value / flt(stock_qty) + self.wh_data.valuation_rate = stock_value / flt(stock_qty) - if not self.stock_queue: - self.stock_queue.append([0, sle.incoming_rate or sle.outgoing_rate or self.valuation_rate]) + if not self.wh_data.stock_queue: + self.wh_data.stock_queue.append([0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]) def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no): ref_item_dt = "" @@ -413,39 +594,56 @@ class update_entries_after(object): else: return 0 - def get_sle_before_datetime(self): + def get_sle_before_datetime(self, args): """get previous stock ledger entry before current time-bucket""" - if self.args.get('sle_id'): - self.args['name'] = self.args.get('sle_id') + sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False) + sle = sle[0] if sle else frappe._dict() + return sle - return get_stock_ledger_entries(self.args, "<=", "desc", "limit 1", for_update=False) - - def get_sle_after_datetime(self): + def get_sle_after_datetime(self, args): """get Stock Ledger Entries after a particular datetime, for reposting""" - return get_stock_ledger_entries(self.previous_sle or frappe._dict({ - "item_code": self.args.get("item_code"), "warehouse": self.args.get("warehouse") }), - ">", "asc", for_update=True, check_serial_no=False) + return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False) def raise_exceptions(self): - deficiency = min(e["diff"] for e in self.exceptions) + msg_list = [] + for warehouse, exceptions in iteritems(self.exceptions): + deficiency = min(e["diff"] for e in exceptions) - if ((self.exceptions[0]["voucher_type"], self.exceptions[0]["voucher_no"]) in - frappe.local.flags.currently_saving): + if ((exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]) in + frappe.local.flags.currently_saving): - msg = _("{0} units of {1} needed in {2} to complete this transaction.").format( - abs(deficiency), frappe.get_desk_link('Item', self.item_code), - frappe.get_desk_link('Warehouse', self.warehouse)) - else: - msg = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( - abs(deficiency), frappe.get_desk_link('Item', self.item_code), - frappe.get_desk_link('Warehouse', self.warehouse), - self.exceptions[0]["posting_date"], self.exceptions[0]["posting_time"], - frappe.get_desk_link(self.exceptions[0]["voucher_type"], self.exceptions[0]["voucher_no"])) + msg = _("{0} units of {1} needed in {2} to complete this transaction.").format( + abs(deficiency), frappe.get_desk_link('Item', self.item_code), + frappe.get_desk_link('Warehouse', warehouse)) + else: + msg = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( + abs(deficiency), frappe.get_desk_link('Item', self.item_code), + frappe.get_desk_link('Warehouse', warehouse), + exceptions[0]["posting_date"], exceptions[0]["posting_time"], + frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"])) - if self.verbose: - frappe.throw(msg, NegativeStockError, title='Insufficient Stock') - else: - raise NegativeStockError(msg) + if msg: + msg_list.append(msg) + + if msg_list: + message = "\n\n".join(msg_list) + if self.verbose: + 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): + bin_doc = get_bin(self.item_code, warehouse) + + bin_doc.update({ + "valuation_rate": data.valuation_rate, + "actual_qty": data.qty_after_transaction, + "stock_value": data.stock_value + }) + bin_doc.flags.via_stock_ledger_entry = True + bin_doc.save(ignore_permissions=True) def get_previous_sle(args, for_update=False): """ @@ -489,6 +687,7 @@ def get_stock_ledger_entries(previous_sle, operator=None, select *, timestamp(posting_date, posting_time) as "timestamp" from `tabStock Ledger Entry` where item_code = %%(item_code)s + and is_cancelled = 0 %(conditions)s order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s %(limit)s %(for_update)s""" % { @@ -498,10 +697,11 @@ def get_stock_ledger_entries(previous_sle, operator=None, "order": order }, previous_sle, as_dict=1, debug=debug) -def get_sle_by_id(sle_id): - return frappe.db.get_all('Stock Ledger Entry', - fields=['*', 'timestamp(posting_date, posting_time) as timestamp'], - filters={'name': sle_id})[0] +def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None): + return frappe.db.get_value('Stock Ledger Entry', + {'voucher_detail_no': voucher_detail_no, 'name': ['!=', excluded_sle]}, + ['item_code', 'warehouse', 'posting_date', 'posting_time', 'timestamp(posting_date, posting_time) as timestamp'], + as_dict=1) def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, allow_zero_rate=False, currency=None, company=None, raise_error_if_no_rate=True): @@ -529,7 +729,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, voucher_no, voucher_type)) if last_valuation_rate: - return flt(last_valuation_rate[0][0]) # as there is previous records, it might come with zero rate + return flt(last_valuation_rate[0][0]) # If negative stock allowed, and item delivered without any incoming entry, # system does not found any SLE, then take valuation rate from Item @@ -561,3 +761,54 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, frappe.throw(msg=msg, title=_("Valuation Rate Missing")) return valuation_rate + +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 + item_code = %(item_code)s + and warehouse = %(warehouse)s + and voucher_no != %(voucher_no)s + and is_cancelled = 0 + and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s) + or ( + timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s) + and creation > %(creation)s + ) + ) + """.format(qty=args.actual_qty), args) + + validate_negative_qty_in_future_sle(args, allow_negative_stock) + +def validate_negative_qty_in_future_sle(args, allow_negative_stock=None): + allow_negative_stock = allow_negative_stock \ + or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) + + if args.actual_qty < 0 and not allow_negative_stock: + sle = get_future_sle_with_negative_qty(args) + if sle: + message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( + abs(sle[0]["qty_after_transaction"]), + frappe.get_desk_link('Item', args.item_code), + 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): + return frappe.db.sql(""" + select + qty_after_transaction, posting_date, posting_time, + voucher_type, voucher_no + from `tabStock Ledger Entry` + where + item_code = %(item_code)s + and warehouse = %(warehouse)s + and voucher_no != %(voucher_no)s + and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s) + and is_cancelled = 0 + and qty_after_transaction < 0 + limit 1 + """, args, as_dict=1) \ No newline at end of file diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index f9ac25443ea..4ea7e4fcd6e 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -63,6 +63,7 @@ def get_stock_value_on(warehouse=None, posting_date=None, item_code=None): SELECT item_code, stock_value, name, warehouse FROM `tabStock Ledger Entry` sle WHERE posting_date <= %s {0} + and is_cancelled = 0 ORDER BY timestamp(posting_date, posting_time) DESC, creation DESC """.format(condition), values, as_dict=1) @@ -211,7 +212,7 @@ def get_incoming_rate(args, raise_error_if_no_rate=True): currency=erpnext.get_company_currency(args.get('company')), company=args.get('company'), raise_error_if_no_rate=raise_error_if_no_rate) - return in_rate + return flt(in_rate) def get_avg_purchase_rate(serial_nos): """get average value of serial numbers""" @@ -375,4 +376,10 @@ def get_incoming_outgoing_rate_for_cancel(item_code, voucher_type, voucher_no, v outgoing_rate = outgoing_rate[0][0] if outgoing_rate else 0.0 - return outgoing_rate \ No newline at end of file + return outgoing_rate + +def is_reposting_item_valuation_in_progress(): + reposting_in_progress = frappe.db.exists("Repost Item Valuation", + {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) + if reposting_in_progress: + frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1) \ No newline at end of file From 9466e42e7095f7f4ff32230ab7dace6642455ba9 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 21 Dec 2020 20:52:20 +0530 Subject: [PATCH 122/295] fix: change request modifications --- .../v13_0/update_project_template_tasks.py | 8 +- erpnext/projects/doctype/project/project.py | 51 +++++------ .../projects/doctype/project/test_project.py | 87 ++++++++++--------- .../project_template/project_template.py | 5 +- erpnext/projects/doctype/task/task.json | 4 +- erpnext/projects/doctype/task/test_task.py | 2 +- 6 files changed, 82 insertions(+), 75 deletions(-) diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py index 1303efd93fb..26c42592816 100644 --- a/erpnext/patches/v13_0/update_project_template_tasks.py +++ b/erpnext/patches/v13_0/update_project_template_tasks.py @@ -6,7 +6,13 @@ import frappe def execute(): frappe.reload_doc("projects", "doctype", "project_template") - for template_name in frappe.db.sql(""" select name from `tabProject Template` """, as_dict=1): + 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 = [] diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 13e72fec8a2..2cdfb7af444 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -59,8 +59,8 @@ class Project(Document): for task in template.tasks: template_task_details = frappe.get_doc("Task", task.task) tmp_task_details.append(template_task_details) - project_tasks.append(self.create_task_from_template(template_task_details)) - + task = self.create_task_from_template(template_task_details) + project_tasks.append(task) self.dependency_mapping(tmp_task_details, project_tasks) def create_task_from_template(self, task_details): @@ -75,36 +75,33 @@ class Project(Document): task_weight = task_details.task_weight, type = task_details.type, issue = task_details.issue, - is_group = task_details.is_group, - start = task_details.start, - duration = task_details.duration + is_group = task_details.is_group )).insert() def dependency_mapping(self, template_tasks, project_tasks): - for tmp_task in template_tasks: - prj_task = list(filter(lambda x: x.subject == tmp_task.subject, project_tasks))[0] - prj_task = frappe.get_doc("Task", prj_task.name) - self.check_depends_on_value(tmp_task, prj_task, project_tasks) - self.check_for_parent_tasks(tmp_task, prj_task, project_tasks) + for template_task in template_tasks: + project_task = list(filter(lambda x: x.subject == template_task.subject, project_tasks))[0] + if template_task.get("depends_on") and not project_task.get("depends_on"): + self.check_depends_on_value(template_task, project_task, project_tasks) + if template_task.get("parent_task") and not project_task.get("parent_task"): + self.check_for_parent_tasks(template_task, project_task, project_tasks) - def check_depends_on_value(self, tmp_task, prj_task, project_tasks): - if tmp_task.get("depends_on") and not prj_task.get("depends_on"): - for child_task in tmp_task.get("depends_on"): - child_task_subject = frappe.db.get_value("Task", child_task.task, "subject") - corresponding_prj_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks)) - if len(corresponding_prj_task): - prj_task.append("depends_on",{ - "task": corresponding_prj_task[0].name - }) - prj_task.save() + def check_depends_on_value(self, template_task, project_task, project_tasks): + for child_task in template_task.get("depends_on"): + child_task_subject = frappe.db.get_value("Task", child_task.task, "subject") + corresponding_project_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks)) + if len(corresponding_project_task): + project_task.append("depends_on",{ + "task": corresponding_project_task[0].name + }) + project_task.save() - def check_for_parent_tasks(self, tmp_task, prj_task, project_tasks): - if tmp_task.get("parent_task") and not prj_task.get("parent_task"): - parent_task_subject = frappe.db.get_value("Task", tmp_task.get("parent_task"), "subject") - corresponding_prj_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks)) - if len(corresponding_prj_task): - prj_task.parent_task = corresponding_prj_task[0].name - prj_task.save() + def check_for_parent_tasks(self, template_task, project_task, project_tasks): + parent_task_subject = frappe.db.get_value("Task", template_task.get("parent_task"), "subject") + corresponding_project_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks)) + if len(corresponding_project_task): + project_task.parent_task = corresponding_project_task[0].name + project_task.save() def is_row_updated(self, row, existing_task_data, fields): if self.get("__islocal") or not existing_task_data: return True diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index ce56a50b4e2..1d2980ce461 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -17,78 +17,79 @@ class TestProject(unittest.TestCase): """ Test Action: Basic Test of a Project created from template. The template has a single task. """ - frappe.db.sql('delete from tabTask where project = "Test Project with Templ - no parent and dependend tasks"') - frappe.delete_doc('Project', 'Test Project with Templ - no parent and dependend tasks') + project_name = "Test Project with Template - No Parent and Dependend Tasks" + frappe.db.sql(""" delete from tabTask where project = %s """, project_name) + frappe.delete_doc('Project', project_name) - task1 = task_exists("Test Temp Task with no parent and dependency") + task1 = task_exists("Test Template Task with No Parent and Dependency") if not task1: - task1 = create_task(subject="Test Temp Task with no parent and dependency", is_template=1, begin=5, duration=3) + task1 = create_task(subject="Test Template Task with No Parent and Dependency", is_template=1, begin=5, duration=3) - template = make_project_template("Test Project Template - no parent and dependend tasks", [task1]) - project = get_project("Test Project with Templ - no parent and dependend tasks", template) - tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') + template = make_project_template("Test Project Template - No Parent and Dependend Tasks", [task1]) + project = get_project(project_name, template) + tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks'], dict(project=project.name), order_by='creation asc') - self.assertEqual(tasks[0].subject, 'Test Temp Task with no parent and dependency') - self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, tasks[0])) + self.assertEqual(tasks[0].subject, 'Test Template Task with No Parent and Dependency') + self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 5, 3)) self.assertEqual(len(tasks), 1) def test_project_template_having_parent_child_tasks(self): + project_name = "Test Project with Template - Tasks with Parent-Child Relation" + frappe.db.sql(""" delete from tabTask where project = %s """, project_name) + frappe.delete_doc('Project', project_name) - frappe.db.sql('delete from tabTask where project = "Test Project with Templ - tasks with parent-child"') - frappe.delete_doc('Project', 'Test Project with Templ - tasks with parent-child') - - task1 = task_exists("Test Temp Task parent") + task1 = task_exists("Test Template Task Parent") if not task1: - task1 = create_task(subject="Test Temp Task parent", is_group=1, is_template=1, begin=1, duration=1) + task1 = create_task(subject="Test Template Task Parent", is_group=1, is_template=1, begin=1, duration=1) - task2 = task_exists("Test Temp Task child 1") + task2 = task_exists("Test Template Task Child 1") if not task2: - task2 = create_task(subject="Test Temp Task child 1", parent_task=task1.name, is_template=1, begin=1, duration=3) + task2 = create_task(subject="Test Template Task Child 1", parent_task=task1.name, is_template=1, begin=1, duration=3) - task3 = task_exists("Test Temp Task child 2") + task3 = task_exists("Test Template Task Child 2") if not task3: - task3 = create_task(subject="Test Temp Task child 2", parent_task=task1.name, is_template=1, begin=2, duration=3) + task3 = create_task(subject="Test Template Task Child 2", parent_task=task1.name, is_template=1, begin=2, duration=3) - template = make_project_template("Test Project Template - tasks with parent-child", [task1, task2, task3]) - project = get_project("Test Project with Templ - tasks with parent-child", template) - tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') + template = make_project_template("Test Project Template - Tasks with Parent-Child Relation", [task1, task2, task3]) + 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') - self.assertEqual(tasks[0].subject, 'Test Temp Task parent') - self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, tasks[0])) + self.assertEqual(tasks[0].subject, 'Test Template Task Parent') + self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 1, 1)) - self.assertEqual(tasks[1].subject, 'Test Temp Task child 1') - self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, tasks[1])) + self.assertEqual(tasks[1].subject, 'Test Template Task Child 1') + self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 1, 3)) self.assertEqual(tasks[1].parent_task, tasks[0].name) - self.assertEqual(tasks[2].subject, 'Test Temp Task child 2') - self.assertEqual(getdate(tasks[2].exp_end_date), calculate_end_date(project, tasks[2])) + self.assertEqual(tasks[2].subject, 'Test Template Task Child 2') + self.assertEqual(getdate(tasks[2].exp_end_date), calculate_end_date(project, 2, 3)) self.assertEqual(tasks[2].parent_task, tasks[0].name) self.assertEqual(len(tasks), 3) def test_project_template_having_dependent_tasks(self): + project_name = "Test Project with Template - Dependent Tasks" + frappe.db.sql(""" delete from tabTask where project = %s """, project_name) + frappe.delete_doc('Project', project_name) - frappe.db.sql('delete from tabTask where project = "Test Project with Templ - dependent tasks"') - frappe.delete_doc('Project', 'Test Project with Templ - dependent tasks') - - task1 = task_exists("Test Temp Task for dependency") + task1 = task_exists("Test Template Task for Dependency") if not task1: - task1 = create_task(subject="Test Temp Task for dependency", is_template=1, begin=3, duration=1) + task1 = create_task(subject="Test Template Task for Dependency", is_template=1, begin=3, duration=1) - task2 = task_exists("Test Temp Task with dependency") + task2 = task_exists("Test Template Task with Dependency") if not task2: - task2 = create_task(subject="Test Temp Task with dependency", depends_on=task1.name, is_template=1, begin=2, duration=2) + 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 Templ - dependent tasks", [task1, task2]) - project = get_project("Test Project with Templ - dependent tasks", template) - tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') + 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') - self.assertEqual(tasks[1].subject, 'Test Temp Task with dependency') - self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, tasks[1])) + self.assertEqual(tasks[1].subject, 'Test Template Task with Dependency') + self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 2, 2)) self.assertTrue(tasks[1].depends_on_tasks.find(tasks[0].name) >= 0 ) - self.assertEqual(tasks[0].subject, 'Test Temp Task for dependency') - self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, tasks[0]) ) + self.assertEqual(tasks[0].subject, 'Test Template Task for Dependency') + self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 3, 1) ) self.assertEqual(len(tasks), 2) @@ -129,5 +130,5 @@ def task_exists(subject): return False return frappe.get_doc("Task", result[0].name) -def calculate_end_date(project, task): - return getdate(add_days(project.expected_start_date, task.start + task.duration)) \ No newline at end of file +def calculate_end_date(project, start, duration): + return getdate(add_days(project.expected_start_date, start + duration)) \ No newline at end of file diff --git a/erpnext/projects/doctype/project_template/project_template.py b/erpnext/projects/doctype/project_template/project_template.py index 1beebf7a258..aace40240c4 100644 --- a/erpnext/projects/doctype/project_template/project_template.py +++ b/erpnext/projects/doctype/project_template/project_template.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document from frappe import _ +from frappe.utils import get_link_to_form class ProjectTemplate(Document): @@ -18,8 +19,8 @@ class ProjectTemplate(Document): if task_details.depends_on: for dependency_task in task_details.depends_on: if not self.check_dependent_task_presence(dependency_task.task): - task_details_format = """{0}""".format(task_details.name) - dependency_task_format = """{0}""".format(dependency_task.task) + task_details_format = get_link_to_form("Task",task_details.name) + dependency_task_format = get_link_to_form("Task", dependency_task.task) frappe.throw(_("Task {0} depends on Task {1}. Please add Task {1} to the Tasks list.").format(frappe.bold(task_details_format), frappe.bold(dependency_task_format))) def check_dependent_task_presence(self, task): diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json index a9e3d9bc0fe..bb55256f7d9 100644 --- a/erpnext/projects/doctype/task/task.json +++ b/erpnext/projects/doctype/task/task.json @@ -371,11 +371,13 @@ "label": "Is Template" }, { + "depends_on": "is_template", "fieldname": "start", "fieldtype": "Int", "label": "Begin On (Days)" }, { + "depends_on": "is_template", "fieldname": "duration", "fieldtype": "Int", "label": "Duration (Days)" @@ -386,7 +388,7 @@ "is_tree": 1, "links": [], "max_attachments": 5, - "modified": "2020-12-07 13:26:53.614689", + "modified": "2020-12-21 11:59:24.196834", "modified_by": "Administrator", "module": "Projects", "name": "Task", diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index aded78b8574..25714f8cde3 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -104,7 +104,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 "_Test Project" + task.project = project or None if is_template else "_Test Project" task.is_template = is_template task.start = begin task.duration = duration From c36cab81f229376cbdde96cf7cfe4ccbd33b6f36 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Mon, 21 Dec 2020 23:46:02 +0530 Subject: [PATCH 123/295] fix: Update year_to_date and month_to_date field labels to show company currency --- .../doctype/salary_slip/salary_slip.js | 6 +++--- .../doctype/salary_slip/salary_slip.json | 20 +++++++++++++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js index f7e22c63879..56948717628 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.js +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js @@ -125,15 +125,15 @@ frappe.ui.form.on("Salary Slip", { change_form_labels: function(frm, company_currency) { frm.set_currency_labels(["base_hour_rate", "base_gross_pay", "base_total_deduction", - "base_net_pay", "base_rounded_total", "base_total_in_words"], + "base_net_pay", "base_rounded_total", "base_total_in_words", "base_year_to_date", "base_month_to_date"], company_currency); - frm.set_currency_labels(["hour_rate", "gross_pay", "total_deduction", "net_pay", "rounded_total", "total_in_words"], + frm.set_currency_labels(["hour_rate", "gross_pay", "total_deduction", "net_pay", "rounded_total", "total_in_words", "year_to_date", "month_to_date"], frm.doc.currency); // toggle fields frm.toggle_display(["exchange_rate", "base_hour_rate", "base_gross_pay", "base_total_deduction", - "base_net_pay", "base_rounded_total", "base_total_in_words"], + "base_net_pay", "base_rounded_total", "base_total_in_words", "base_year_to_date", "base_month_to_date"], frm.doc.currency != company_currency); }, diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json index d981a39953d..43deee43aac 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.json +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json @@ -70,10 +70,12 @@ "net_pay", "base_net_pay", "year_to_date", + "base_year_to_date", "column_break_53", "rounded_total", "base_rounded_total", "month_to_date", + "base_month_to_date", "section_break_55", "total_in_words", "column_break_69", @@ -584,12 +586,26 @@ { "fieldname": "year_to_date", "fieldtype": "Currency", + "label": "Year To Date", + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "month_to_date", + "fieldtype": "Currency", + "label": "Month To Date", + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "base_year_to_date", + "fieldtype": "Currency", "label": "Year To Date(Company Currency)", "options": "Company:company:default_currency", "read_only": 1 }, { - "fieldname": "month_to_date", + "fieldname": "base_month_to_date", "fieldtype": "Currency", "label": "Month To Date(Company Currency)", "options": "Company:company:default_currency", @@ -600,7 +616,7 @@ "idx": 9, "is_submittable": 1, "links": [], - "modified": "2020-12-18 23:57:41.042954", + "modified": "2020-12-21 23:43:44.959840", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Slip", From 090783804bdc59f22b8a7afee43cb3ddabcd37b2 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Mon, 21 Dec 2020 23:52:05 +0530 Subject: [PATCH 124/295] fix: Improve month_to_date computation --- erpnext/payroll/doctype/salary_slip/salary_slip.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index e86a7fc3158..02e5f2d1d12 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe, erpnext import datetime, math -from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate +from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate, get_first_day from frappe.model.naming import make_autoname from frappe import msgprint, _ @@ -1150,8 +1150,7 @@ class SalarySlip(TransactionBase): def compute_month_to_date(self): month_to_date = 0 - date = datetime.datetime.strptime(self.start_date,"%Y-%m-%d") - first_day_of_the_month = "1-" + str(date.month) + "-" + str(date.year) + first_day_of_the_month = get_first_day(self.start_date) salary_slips_from_this_month = frappe.get_list('Salary Slip', fields = ['employee_name', 'start_date', 'net_pay'], filters = {'employee_name' : self.employee_name, From 3a26f26671e19df3acd7ef690300810e1d5026d3 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 22 Dec 2020 11:56:59 +0530 Subject: [PATCH 125/295] fix: get_doc to avoid modified error --- erpnext/projects/doctype/project/project.py | 35 ++++++++++--------- .../projects/doctype/project/test_project.py | 2 +- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 2cdfb7af444..97134602f86 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -81,27 +81,28 @@ class Project(Document): def dependency_mapping(self, template_tasks, project_tasks): for template_task in template_tasks: project_task = list(filter(lambda x: x.subject == template_task.subject, project_tasks))[0] - if template_task.get("depends_on") and not project_task.get("depends_on"): - self.check_depends_on_value(template_task, project_task, project_tasks) - if template_task.get("parent_task") and not project_task.get("parent_task"): - self.check_for_parent_tasks(template_task, project_task, project_tasks) + project_task = frappe.get_doc("Task", project_task.name) + self.check_depends_on_value(template_task, project_task, project_tasks) + self.check_for_parent_tasks(template_task, project_task, project_tasks) def check_depends_on_value(self, template_task, project_task, project_tasks): - for child_task in template_task.get("depends_on"): - child_task_subject = frappe.db.get_value("Task", child_task.task, "subject") - corresponding_project_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks)) - if len(corresponding_project_task): - project_task.append("depends_on",{ - "task": corresponding_project_task[0].name - }) - project_task.save() + if template_task.get("depends_on") and not project_task.get("depends_on"): + for child_task in template_task.get("depends_on"): + child_task_subject = frappe.db.get_value("Task", child_task.task, "subject") + corresponding_project_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks)) + if len(corresponding_project_task): + project_task.append("depends_on",{ + "task": corresponding_project_task[0].name + }) + project_task.save() def check_for_parent_tasks(self, template_task, project_task, project_tasks): - parent_task_subject = frappe.db.get_value("Task", template_task.get("parent_task"), "subject") - corresponding_project_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks)) - if len(corresponding_project_task): - project_task.parent_task = corresponding_project_task[0].name - project_task.save() + if template_task.get("parent_task") and not project_task.get("parent_task"): + parent_task_subject = frappe.db.get_value("Task", template_task.get("parent_task"), "subject") + corresponding_project_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks)) + if len(corresponding_project_task): + project_task.parent_task = corresponding_project_task[0].name + project_task.save() def is_row_updated(self, row, existing_task_data, fields): if self.get("__islocal") or not existing_task_data: return True diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index 1d2980ce461..d77b14ce33c 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -52,7 +52,7 @@ class TestProject(unittest.TestCase): template = make_project_template("Test Project Template - Tasks with Parent-Child Relation", [task1, task2, task3]) 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') + tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name', 'parent_task'], dict(project=project.name), order_by='creation asc') self.assertEqual(tasks[0].subject, 'Test Template Task Parent') self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 1, 1)) From 468f67a4de5e267d7518105541a4dddbdfdcf610 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 22 Dec 2020 12:44:09 +0530 Subject: [PATCH 126/295] fix: Add parent for all-products page --- erpnext/www/all-products/index.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/www/all-products/index.py b/erpnext/www/all-products/index.py index 0394e4b2cc5..7d7793ac49b 100644 --- a/erpnext/www/all-products/index.py +++ b/erpnext/www/all-products/index.py @@ -15,6 +15,9 @@ def get_context(context): context.items = get_products_for_website(field_filters, attribute_filters, search) + # Add homepage as parent + context.parents = [{"name": frappe._("Home"), "route":"/"}] + product_settings = get_product_settings() context.field_filters = get_field_filter_data() \ if product_settings.enable_field_filters else [] From 6900a79421b141e9d86d7e111ba9eac06e7cf75d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 22 Dec 2020 11:37:13 +0100 Subject: [PATCH 127/295] fix: fail silently --- erpnext/regional/germany/accounts_controller.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/germany/accounts_controller.py b/erpnext/regional/germany/accounts_controller.py index 5b2b31f2043..0ab027b4d6e 100644 --- a/erpnext/regional/germany/accounts_controller.py +++ b/erpnext/regional/germany/accounts_controller.py @@ -37,7 +37,14 @@ def validate_regional(doc): for field in required_fields: condition = field.get("condition") - if condition and not frappe.safe_eval(condition, doc.as_dict()): + condition_true = True + try: + condition_true = frappe.safe_eval(condition, doc.as_dict()) + except: + # invalid condition should not result in an error + pass + + if condition and not condition_true: continue field_name = field.get("field_name") From 5adbe49ca65b9230531341e0d2d906670e39002e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 22 Dec 2020 11:37:43 +0100 Subject: [PATCH 128/295] refactor: translation syntax --- erpnext/regional/germany/accounts_controller.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/erpnext/regional/germany/accounts_controller.py b/erpnext/regional/germany/accounts_controller.py index 0ab027b4d6e..63da96bddab 100644 --- a/erpnext/regional/germany/accounts_controller.py +++ b/erpnext/regional/germany/accounts_controller.py @@ -55,9 +55,6 @@ def validate_regional(doc): def missing(field_label, regulation): """Notify the user that a required field is missing.""" - context = 'Specific for Germany. Example: Remember to set Company Tax ID. It is required by § 14 Abs. 4 Nr. 2 UStG.' - msgprint(_('Remember to set {field_label}. It is required by {regulation}.', context=context).format( - field_label=frappe.bold(_(field_label)), - regulation=regulation - ) - ) + 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.') + formatted_msg = translated_msg.format(field_label=frappe.bold(_(field_label)), regulation=regulation) + msgprint(formatted_msg) From a69021018aea2b2e51f4cccb999dad97bcdc5752 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 22 Dec 2020 11:38:09 +0100 Subject: [PATCH 129/295] test: add test for accounts controller --- erpnext/regional/germany/test_accounts_controller.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 erpnext/regional/germany/test_accounts_controller.py diff --git a/erpnext/regional/germany/test_accounts_controller.py b/erpnext/regional/germany/test_accounts_controller.py new file mode 100644 index 00000000000..63bb843d307 --- /dev/null +++ b/erpnext/regional/germany/test_accounts_controller.py @@ -0,0 +1,12 @@ +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) From 511be6466df429acde392aa458c9215cbde48238 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 22 Dec 2020 11:43:33 +0100 Subject: [PATCH 130/295] Revert "fix: fail silently" This reverts commit 6900a79421b141e9d86d7e111ba9eac06e7cf75d. --- erpnext/regional/germany/accounts_controller.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/erpnext/regional/germany/accounts_controller.py b/erpnext/regional/germany/accounts_controller.py index 63da96bddab..b789960ca01 100644 --- a/erpnext/regional/germany/accounts_controller.py +++ b/erpnext/regional/germany/accounts_controller.py @@ -37,14 +37,7 @@ def validate_regional(doc): for field in required_fields: condition = field.get("condition") - condition_true = True - try: - condition_true = frappe.safe_eval(condition, doc.as_dict()) - except: - # invalid condition should not result in an error - pass - - if condition and not condition_true: + if condition and not frappe.safe_eval(condition, doc.as_dict()): continue field_name = field.get("field_name") From 4ebee5014eebcf49669ccabda45c971f3822c814 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 22 Dec 2020 18:14:46 +0530 Subject: [PATCH 131/295] feat: aholiday check before setting start and end date in task --- erpnext/projects/doctype/project/project.py | 20 +++++++++++++++++-- .../projects/doctype/project/test_project.py | 3 --- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 97134602f86..f6bb6e9e745 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -13,6 +13,7 @@ from frappe.desk.reportview import get_match_cond from erpnext.hr.doctype.daily_work_summary.daily_work_summary import get_users_email from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday from frappe.model.document import Document +from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list class Project(Document): def get_feed(self): @@ -69,8 +70,8 @@ class Project(Document): subject = task_details.subject, project = self.name, status = 'Open', - exp_start_date = add_days(self.expected_start_date, task_details.start), - exp_end_date = add_days(self.expected_start_date, task_details.start + task_details.duration), + exp_start_date = self.calculate_start_date(task_details), + exp_end_date = self.calculate_end_date(task_details), description = task_details.description, task_weight = task_details.task_weight, type = task_details.type, @@ -78,6 +79,21 @@ class Project(Document): is_group = task_details.is_group )).insert() + def calculate_start_date(self, task_details): + self.start_date = add_days(self.expected_start_date, task_details.start) + self.start_date = self.update_if_holiday(self.start_date) + return self.start_date + + def calculate_end_date(self, task_details): + self.end_date = add_days(self.start_date, task_details.duration) + return self.update_if_holiday(self.end_date) + + def update_if_holiday(self, date): + holiday_list = self.holiday_list or get_holiday_list() + while is_holiday(holiday_list, date): + date = add_days(date, 1) + return date + def dependency_mapping(self, template_tasks, project_tasks): for template_task in template_tasks: project_task = list(filter(lambda x: x.subject == template_task.subject, project_tasks))[0] diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index d77b14ce33c..0faf97670d8 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -14,9 +14,6 @@ from frappe.utils import getdate, nowdate, add_days class TestProject(unittest.TestCase): def test_project_with_template_having_no_parent_and_depend_tasks(self): - """ - Test Action: Basic Test of a Project created from template. The template has a single task. - """ project_name = "Test Project with Template - No Parent and Dependend Tasks" frappe.db.sql(""" delete from tabTask where project = %s """, project_name) frappe.delete_doc('Project', project_name) From 6cf018c762ee4d67bfc83b9f6fc3814b51462734 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 22 Dec 2020 19:40:41 +0530 Subject: [PATCH 132/295] fix: holiday update in tests --- erpnext/projects/doctype/project/project.py | 16 ++++++++-------- erpnext/projects/doctype/project/test_project.py | 8 ++++++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index f6bb6e9e745..60f85b0e7a6 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -81,18 +81,12 @@ class Project(Document): def calculate_start_date(self, task_details): self.start_date = add_days(self.expected_start_date, task_details.start) - self.start_date = self.update_if_holiday(self.start_date) + self.start_date = update_if_holiday(self.holiday_list, self.start_date) return self.start_date def calculate_end_date(self, task_details): self.end_date = add_days(self.start_date, task_details.duration) - return self.update_if_holiday(self.end_date) - - def update_if_holiday(self, date): - holiday_list = self.holiday_list or get_holiday_list() - while is_holiday(holiday_list, date): - date = add_days(date, 1) - return date + return update_if_holiday(self.holiday_list, self.end_date) def dependency_mapping(self, template_tasks, project_tasks): for template_task in template_tasks: @@ -547,3 +541,9 @@ def set_project_status(project, status): project.status = status project.save() + +def update_if_holiday(holiday_list, date): + holiday_list = holiday_list or get_holiday_list() + while is_holiday(holiday_list, date): + date = add_days(date, 1) + return date diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index 0faf97670d8..af978e85fd5 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -8,7 +8,7 @@ test_records = frappe.get_test_records('Project') test_ignore = ["Sales Order"] from erpnext.projects.doctype.project_template.test_project_template import make_project_template -from erpnext.projects.doctype.project.project import set_project_status +from erpnext.projects.doctype.project.project import set_project_status, update_if_holiday from erpnext.projects.doctype.task.test_task import create_task from frappe.utils import getdate, nowdate, add_days @@ -128,4 +128,8 @@ def task_exists(subject): return frappe.get_doc("Task", result[0].name) def calculate_end_date(project, start, duration): - return getdate(add_days(project.expected_start_date, start + duration)) \ No newline at end of file + start = add_days(project.expected_start_date, start) + start = update_if_holiday(project.holiday_list, start) + end = add_days(start, duration) + end = update_if_holiday(project.holiday_list, end) + return getdate(end) \ No newline at end of file From 8dec1c142f96bb171c0e23c92ef9d3b1100cf6b6 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 22 Dec 2020 19:55:31 +0530 Subject: [PATCH 133/295] fix: removed unused imports --- erpnext/projects/doctype/project/test_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index af978e85fd5..97b67b38eb3 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -8,7 +8,7 @@ test_records = frappe.get_test_records('Project') test_ignore = ["Sales Order"] from erpnext.projects.doctype.project_template.test_project_template import make_project_template -from erpnext.projects.doctype.project.project import set_project_status, update_if_holiday +from erpnext.projects.doctype.project.project import update_if_holiday from erpnext.projects.doctype.task.test_task import create_task from frappe.utils import getdate, nowdate, add_days From 2acd8cbc02aca5904e35ece8dfb4b1608e23891e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 22 Dec 2020 17:34:22 +0100 Subject: [PATCH 134/295] fix: sider --- erpnext/regional/germany/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/germany/accounts_controller.py b/erpnext/regional/germany/accounts_controller.py index b789960ca01..7f76493608e 100644 --- a/erpnext/regional/germany/accounts_controller.py +++ b/erpnext/regional/germany/accounts_controller.py @@ -48,6 +48,6 @@ def validate_regional(doc): 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.') + 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) From df8ea194064d5d7abcdf1a696324af55d679baa3 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 22 Dec 2020 17:34:31 +0100 Subject: [PATCH 135/295] fix: whitespace --- erpnext/regional/germany/test_accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/germany/test_accounts_controller.py b/erpnext/regional/germany/test_accounts_controller.py index 63bb843d307..8bd378c971f 100644 --- a/erpnext/regional/germany/test_accounts_controller.py +++ b/erpnext/regional/germany/test_accounts_controller.py @@ -7,6 +7,6 @@ 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) From 1fb412e3f6b0099082601b6539b0ce62f0345438 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Wed, 23 Dec 2020 11:39:37 +1100 Subject: [PATCH 136/295] docs: fix simple typo, udpate -> update There is a small typo in erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py. Should read `update` rather than `udpate`. --- erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py b/erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py index ad043dd99d3..97e217aa054 100644 --- a/erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py +++ b/erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py @@ -5,11 +5,11 @@ from __future__ import unicode_literals import frappe def execute(): - # udpate sales cycle + # update sales cycle for d in ['Sales Invoice', 'Sales Order', 'Quotation', 'Delivery Note']: frappe.db.sql("""update `tab%s` set taxes_and_charges=charge""" % d) - # udpate purchase cycle + # update purchase cycle for d in ['Purchase Invoice', 'Purchase Order', 'Supplier Quotation', 'Purchase Receipt']: frappe.db.sql("""update `tab%s` set taxes_and_charges=purchase_other_charges""" % d) From 09c6842199f90a11702b40a72d6bf7ccec22c08b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 23 Dec 2020 11:29:26 +0530 Subject: [PATCH 137/295] fix: accounting entries of asset when submitting purchase receipt --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 97e0fa738cd..878dd588779 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -323,7 +323,7 @@ class PurchaseReceipt(BuyingController): elif d.warehouse not in warehouse_with_no_account or \ d.rejected_warehouse not in warehouse_with_no_account: warehouse_with_no_account.append(d.warehouse) - elif d.item_code not in stock_items and flt(d.qty) and auto_accounting_for_non_stock_items: + elif d.item_code not in stock_items and not d.is_fixed_asset and flt(d.qty) and auto_accounting_for_non_stock_items: service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed") credit_currency = get_account_currency(service_received_but_not_billed_account) From 0411a43c6e4c8b5940e7068be0893bea8f0b872b Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 23 Dec 2020 12:14:41 +0530 Subject: [PATCH 138/295] fix: set finished good item rate based on qty --- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index afdb54ceaa2..579b8c5fe1d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -511,7 +511,7 @@ class StockEntry(StockController): bom_items = self.get_bom_raw_materials(finished_item_qty) outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()]) - return flt(outgoing_items_cost - scrap_items_cost) + return flt((outgoing_items_cost - scrap_items_cost) / finished_item_qty) def distribute_additional_costs(self): # If no incoming items, set additional costs blank From d556847fca7ac82e7f11fc5d30de30ebb45ad63e Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 17 Dec 2020 15:17:25 +0530 Subject: [PATCH 139/295] fix: allow addition and removal of employee in payroll Entry --- .../payroll_employee_detail.json | 5 +-- .../doctype/payroll_entry/payroll_entry.js | 38 ++++++++++--------- .../doctype/payroll_entry/payroll_entry.json | 5 +-- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json b/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json index 8a55224dca7..09c7eb9a456 100644 --- a/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json +++ b/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json @@ -17,8 +17,7 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Employee", - "options": "Employee", - "read_only": 1 + "options": "Employee" }, { "fetch_from": "employee.employee_name", @@ -52,7 +51,7 @@ ], "istable": 1, "links": [], - "modified": "2020-09-30 12:40:07.999878", + "modified": "2020-12-17 15:43:29.542977", "modified_by": "Administrator", "module": "Payroll", "name": "Payroll Employee Detail", diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index cb48abbc363..28236c04994 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -31,7 +31,7 @@ frappe.ui.form.on('Payroll Entry', { refresh: function(frm) { if (frm.doc.docstatus == 0) { - if(!frm.is_new()) { + if (!frm.is_new()) { frm.page.clear_primary_action(); frm.add_custom_button(__("Get Employees"), function() { @@ -61,33 +61,33 @@ frappe.ui.form.on('Payroll Entry', { doc: frm.doc, method: 'fill_employee_details', }).then(r => { - if (r.docs && r.docs[0].employees){ + if (r.docs && r.docs[0].employees) { frm.employees = r.docs[0].employees; frm.dirty(); frm.save(); frm.refresh(); - if(r.docs[0].validate_attendance){ + if (r.docs[0].validate_attendance) { render_employee_attendance(frm, r.message); } } - }) + }); }, create_salary_slips: function(frm) { frm.call({ doc: frm.doc, method: "create_salary_slips", - callback: function(r) { + callback: function() { frm.refresh(); frm.toolbar.refresh(); } - }) + }); }, add_context_buttons: function(frm) { - if(frm.doc.salary_slips_submitted || (frm.doc.__onload && frm.doc.__onload.submitted_ss)) { + if (frm.doc.salary_slips_submitted || (frm.doc.__onload && frm.doc.__onload.submitted_ss)) { frm.events.add_bank_entry_button(frm); - } else if(frm.doc.salary_slips_created) { + } else if (frm.doc.salary_slips_created) { frm.add_custom_button(__("Submit Salary Slip"), function() { submit_salary_slip(frm); }).addClass("btn-primary"); @@ -192,9 +192,9 @@ frappe.ui.form.on('Payroll Entry', { }, start_date: function (frm) { - if(!in_progress && frm.doc.start_date){ + if (!in_progress && frm.doc.start_date) { frm.trigger("set_end_date"); - }else{ + } else { // reset flag in_progress = false; } @@ -228,7 +228,7 @@ frappe.ui.form.on('Payroll Entry', { } }, - set_end_date: function(frm){ + set_end_date: function(frm) { frappe.call({ method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.get_end_date', args: { @@ -243,9 +243,9 @@ frappe.ui.form.on('Payroll Entry', { }); }, - validate_attendance: function(frm){ - if(frm.doc.validate_attendance && frm.doc.employees){ - frappe.call({ + validate_attendance: function(frm) { + if (frm.doc.validate_attendance && frm.doc.employees) { + frappe.call ({ method: 'validate_employee_attendance', args: {}, callback: function(r) { @@ -255,7 +255,7 @@ frappe.ui.form.on('Payroll Entry', { freeze: true, freeze_message: __('Validating Employee Attendance...') }); - }else{ + } else { frm.fields_dict.attendance_detail_html.html(""); } }, @@ -274,14 +274,16 @@ const submit_salary_slip = function (frm) { frappe.call({ method: 'submit_salary_slips', args: {}, - callback: function() {frm.events.refresh(frm);}, + callback: function() { + frm.events.refresh(frm); + }, doc: frm.doc, freeze: true, freeze_message: __('Submitting Salary Slips and creating Journal Entry...') }); }, function() { - if(frappe.dom.freeze_count) { + if (frappe.dom.freeze_count) { frappe.dom.unfreeze(); frm.events.refresh(frm); } @@ -316,4 +318,4 @@ let render_employee_attendance = function(frm, data) { data: data }) ); -} +}; diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.json b/erpnext/payroll/doctype/payroll_entry/payroll_entry.json index 7a48dd14758..0444134aa4d 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.json +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.json @@ -129,8 +129,7 @@ "fieldname": "employees", "fieldtype": "Table", "label": "Employee Details", - "options": "Payroll Employee Detail", - "read_only": 1 + "options": "Payroll Employee Detail" }, { "fieldname": "section_break_13", @@ -290,7 +289,7 @@ "icon": "fa fa-cog", "is_submittable": 1, "links": [], - "modified": "2020-10-23 13:00:33.753228", + "modified": "2020-12-17 15:13:17.766210", "modified_by": "Administrator", "module": "Payroll", "name": "Payroll Entry", From 1ab4f09ee9b597c7775d5d2a0fb2ad6732660686 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 23 Dec 2020 15:18:41 +0530 Subject: [PATCH 140/295] fix: use file_url to save file and not file name --- erpnext/hr/doctype/employee/employee.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index dfc600ca3c5..0fde3a12ac8 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -135,7 +135,7 @@ class Employee(NestedSet): try: frappe.get_doc({ "doctype": "File", - "file_name": self.image, + "file_url": self.image, "attached_to_doctype": "User", "attached_to_name": self.user_id }).insert() From d2f91e8c1189185c49113e2db72ab0352bcac5d0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 24 Dec 2020 11:03:36 +0530 Subject: [PATCH 141/295] fix: Do not cancel reference document on Quality Inspection cancellation (#24197) --- .../stock/doctype/quality_inspection/quality_inspection.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js index 376848afaa4..03e3de115b7 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js @@ -4,6 +4,11 @@ cur_frm.cscript.refresh = cur_frm.cscript.inspection_type; frappe.ui.form.on("Quality Inspection", { + refresh: function(frm) { + // Ignore cancellation of reference doctype on cancel all. + frm.ignore_doctypes_on_cancel_all = [frm.doc.reference_type]; + }, + item_code: function(frm) { if (frm.doc.item_code) { return frm.call({ From 527a156512f773c092b13465f567e06258e708cf Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Wed, 23 Dec 2020 17:22:51 +0530 Subject: [PATCH 142/295] feat: validated employees whose salary has been already precessed --- .../doctype/payroll_entry/payroll_entry.js | 104 +++++++++++++----- .../doctype/payroll_entry/payroll_entry.py | 25 ++++- 2 files changed, 97 insertions(+), 32 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index 28236c04994..2288a277917 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -10,15 +10,22 @@ frappe.ui.form.on('Payroll Entry', { } frm.toggle_reqd(['payroll_frequency'], !frm.doc.salary_slip_based_on_timesheet); - frm.set_query("department", function() { + frm.events.department_filters(frm); + frm.events.payroll_payable_account_filters(frm); + }, + + department_filters: function (frm) { + frm.set_query("department", function () { return { "filters": { "company": frm.doc.company, } }; }); + }, - frm.set_query("payroll_payable_account", function() { + payroll_payable_account_filters: function (frm) { + frm.set_query("payroll_payable_account", function () { return { filters: { "company": frm.doc.company, @@ -29,12 +36,12 @@ frappe.ui.form.on('Payroll Entry', { }); }, - refresh: function(frm) { + refresh: function (frm) { if (frm.doc.docstatus == 0) { if (!frm.is_new()) { frm.page.clear_primary_action(); frm.add_custom_button(__("Get Employees"), - function() { + function () { frm.events.get_employee_details(frm); } ).toggleClass('btn-primary', !(frm.doc.employees || []).length); @@ -42,7 +49,7 @@ frappe.ui.form.on('Payroll Entry', { if ((frm.doc.employees || []).length) { frm.page.clear_primary_action(); frm.page.set_primary_action(__('Create Salary Slips'), () => { - frm.save('Submit').then(()=>{ + frm.save('Submit').then(() => { frm.page.clear_primary_action(); frm.refresh(); frm.events.refresh(frm); @@ -73,36 +80,36 @@ frappe.ui.form.on('Payroll Entry', { }); }, - create_salary_slips: function(frm) { + create_salary_slips: function (frm) { frm.call({ doc: frm.doc, method: "create_salary_slips", - callback: function() { + callback: function () { frm.refresh(); frm.toolbar.refresh(); } }); }, - add_context_buttons: function(frm) { + add_context_buttons: function (frm) { if (frm.doc.salary_slips_submitted || (frm.doc.__onload && frm.doc.__onload.submitted_ss)) { frm.events.add_bank_entry_button(frm); } else if (frm.doc.salary_slips_created) { - frm.add_custom_button(__("Submit Salary Slip"), function() { + frm.add_custom_button(__("Submit Salary Slip"), function () { submit_salary_slip(frm); }).addClass("btn-primary"); } }, - add_bank_entry_button: function(frm) { + add_bank_entry_button: function (frm) { frappe.call({ method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.payroll_entry_has_bank_entries', args: { 'name': frm.doc.name }, - callback: function(r) { + callback: function (r) { if (r.message && !r.message.submitted) { - frm.add_custom_button("Make Bank Entry", function() { + frm.add_custom_button("Make Bank Entry", function () { make_bank_entry(frm); }).addClass("btn-primary"); } @@ -141,8 +148,37 @@ frappe.ui.form.on('Payroll Entry', { }, payroll_frequency: function (frm) { - frm.trigger("set_start_end_dates"); - frm.events.clear_employee_table(frm); + frm.trigger("set_start_end_dates").then( ()=> { + frm.events.clear_employee_table(frm); + frm.events.get_employee_with_salary_slip_and_set_query(frm); + }); + }, + + employee_filters: function (frm, emp_list) { + frm.set_query('employee', 'employees', () => { + return { + filters: { + name: ["not in", emp_list] + } + }; + }); + }, + + get_employee_with_salary_slip_and_set_query: function (frm) { + frappe.db.get_list('Salary Slip', { + filters: { + start_date: frm.doc.start_date, + end_date: frm.doc.end_date, + docstatus: 1, + }, + fields: ['employee'] + }).then((emp) => { + var emp_list = []; + emp.forEach((employee_data) => { + emp_list.push(Object.values(employee_data)[0]); + }); + frm.events.employee_filters(frm, emp_list); + }); }, company: function (frm) { @@ -164,17 +200,17 @@ frappe.ui.form.on('Payroll Entry', { from_currency: frm.doc.currency, to_currency: company_currency, }, - callback: function(r) { + callback: function (r) { frm.set_value("exchange_rate", flt(r.message)); frm.set_df_property('exchange_rate', 'hidden', 0); - frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency - + " = [?] " + company_currency); + frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency + + " = [?] " + company_currency); } }); } else { frm.set_value("exchange_rate", 1.0); frm.set_df_property('exchange_rate', 'hidden', 1); - frm.set_df_property("exchange_rate", "description", "" ); + frm.set_df_property("exchange_rate", "description", ""); } } }, @@ -228,7 +264,7 @@ frappe.ui.form.on('Payroll Entry', { } }, - set_end_date: function(frm) { + set_end_date: function (frm) { frappe.call({ method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.get_end_date', args: { @@ -243,12 +279,12 @@ frappe.ui.form.on('Payroll Entry', { }); }, - validate_attendance: function(frm) { + validate_attendance: function (frm) { if (frm.doc.validate_attendance && frm.doc.employees) { - frappe.call ({ + frappe.call({ method: 'validate_employee_attendance', args: {}, - callback: function(r) { + callback: function (r) { render_employee_attendance(frm, r.message); }, doc: frm.doc, @@ -270,11 +306,11 @@ frappe.ui.form.on('Payroll Entry', { const submit_salary_slip = function (frm) { frappe.confirm(__('This will submit Salary Slips and create accrual Journal Entry. Do you want to proceed?'), - function() { + function () { frappe.call({ method: 'submit_salary_slips', args: {}, - callback: function() { + callback: function () { frm.events.refresh(frm); }, doc: frm.doc, @@ -282,7 +318,7 @@ const submit_salary_slip = function (frm) { freeze_message: __('Submitting Salary Slips and creating Journal Entry...') }); }, - function() { + function () { if (frappe.dom.freeze_count) { frappe.dom.unfreeze(); frm.events.refresh(frm); @@ -297,9 +333,11 @@ let make_bank_entry = function (frm) { return frappe.call({ doc: cur_frm.doc, method: "make_payment_entry", - callback: function() { + callback: function () { frappe.set_route( - 'List', 'Journal Entry', {"Journal Entry Account.reference_name": frm.doc.name} + 'List', 'Journal Entry', { + "Journal Entry Account.reference_name": frm.doc.name + } ); }, freeze: true, @@ -311,11 +349,19 @@ let make_bank_entry = function (frm) { } }; - -let render_employee_attendance = function(frm, data) { +let render_employee_attendance = function (frm, data) { frm.fields_dict.attendance_detail_html.html( frappe.render_template('employees_to_mark_attendance', { data: data }) ); }; + +frappe.ui.form.on('Payroll Employee Detail', { + employee: function(frm) { + frm.events.clear_employee_table(frm); + if (!frm.doc.payroll_frequency) { + frappe.throw(__("Please set a Payroll Frequency")); + } + } +}); diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 8c2d9740ece..a25a6e7a32c 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe, erpnext from frappe.model.document import Document from dateutil.relativedelta import relativedelta -from frappe.utils import cint, flt, nowdate, add_days, getdate, fmt_money, add_to_date, DATE_FORMAT, date_diff +from frappe.utils import cint, flt, add_days, getdate, add_to_date, DATE_FORMAT, date_diff, comma_and from frappe import _ from erpnext.accounts.utils import get_fiscal_year from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee @@ -19,16 +19,26 @@ class PayrollEntry(Document): # check if salary slips were manually submitted entries = frappe.db.count("Salary Slip", {'payroll_entry': self.name, 'docstatus': 1}, ['name']) if cint(entries) == len(self.employees): - self.set_onload("submitted_ss", True) + self.set_onload("submitted_ss", True) def on_submit(self): self.create_salary_slips() def before_submit(self): + self.validate_employee_details() if self.validate_attendance: if self.validate_employee_attendance(): frappe.throw(_("Cannot Submit, Employees left to mark attendance")) + def validate_employee_details(self): + emp_with_sal_slip = [] + for employee_details in self.employees: + if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": self.start_date, "end_date": self.end_date, "docstatus": 1}): + emp_with_sal_slip.append(employee_details.employee) + + if len(emp_with_sal_slip): + frappe.throw(_("Salary Slip already exists for {0} ").format(comma_and(emp_with_sal_slip))) + def on_cancel(self): frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip` where payroll_entry=%s """, (self.name))) @@ -71,8 +81,17 @@ class PayrollEntry(Document): and t2.docstatus = 1 %s order by t2.from_date desc """ % cond, {"sal_struct": tuple(sal_struct), "from_date": self.end_date, "payroll_payable_account": self.payroll_payable_account}, as_dict=True) + + emp_list = self.remove_payrolled_employees(emp_list) return emp_list + def remove_payrolled_employees(self, emp_list): + for employee_details in emp_list: + if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": self.start_date, "end_date": self.end_date, "docstatus": 1}): + emp_list.remove(employee_details) + + return emp_list + def fill_employee_details(self): self.set('employees', []) employees = self.get_emp_list() @@ -542,7 +561,7 @@ def create_salary_slips_for_employees(employees, args, publish_progress=True): title = _("Creating Salary Slips...")) else: salary_slip_name = frappe.db.sql( - '''SELECT + '''SELECT name FROM `tabSalary Slip` WHERE company=%s From ed208194323a85b49bead2f593076c44dc0117f4 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 24 Dec 2020 16:24:10 +0530 Subject: [PATCH 143/295] fix: travis --- .../stock/doctype/purchase_receipt/test_purchase_receipt.py | 2 +- erpnext/stock/doctype/serial_no/serial_no.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 9b8eeed1a12..5921651feca 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -527,7 +527,7 @@ class TestPurchaseReceipt(unittest.TestCase): se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1, serial_no=serial_no, basic_rate=100, do_not_submit=True) - self.assertRaises(SerialNoDuplicateError, se.submit) + se.submit() def test_auto_asset_creation(self): asset_item = "Test Asset Item" diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 25ce2d59695..86f3c1f5616 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -365,8 +365,8 @@ def has_serial_no_exists(sn, sle): status = True # If status is receipt then system will allow to in-ward the delivered serial no - if (status and sle.voucher_type == 'Stock Entry' and - frappe.db.get_value('Stock Entry', sle.voucher_no, 'purpose') == 'Material Receipt'): + if (status and sle.voucher_type == "Stock Entry" and frappe.db.get_value("Stock Entry", + sle.voucher_no, "purpose") in ("Material Receipt", "Material Transfer")): status = False return status From 304100db3b01992a8c87c6e32838456dae383f54 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 30 Nov 2020 13:05:28 +0530 Subject: [PATCH 144/295] fix: don't cancel job card if manufacturing entry has made --- .../doctype/job_card/job_card.py | 55 ++++++++++++------- .../doctype/work_order/test_work_order.py | 50 +++++++++++++---- 2 files changed, 75 insertions(+), 30 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index d15d81ed93d..ec28eb7795c 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -17,6 +17,7 @@ class OverlapError(frappe.ValidationError): pass class OperationMismatchError(frappe.ValidationError): pass class OperationSequenceError(frappe.ValidationError): pass +class JobCardCancelError(frappe.ValidationError): pass class JobCard(Document): def validate(self): @@ -217,33 +218,49 @@ class JobCard(Document): field = "operation_id" data = self.get_current_operation_data() if data and len(data) > 0: - for_quantity = data[0].completed_qty - time_in_mins = data[0].time_in_mins + for_quantity = flt(data[0].completed_qty) + time_in_mins = flt(data[0].time_in_mins) - if self.get(field): - time_data = frappe.db.sql(""" + wo = frappe.get_doc('Work Order', self.work_order) + if self.operation_id: + self.validate_produced_quantity(for_quantity, wo) + self.update_work_order_data(for_quantity, time_in_mins, wo) + + def validate_produced_quantity(self, for_quantity, wo): + if self.docstatus < 2: return + + if wo.produced_qty > for_quantity: + first_part_msg = (_("The {0} {1} is used to calculate the valuation cost for the finished good {2}.") + .format(frappe.bold(_("Job Card")), frappe.bold(self.name), frappe.bold(self.production_item))) + + second_part_msg = (_("Kindly cancel the Manufacturing Entries first against the work order {0}.") + .format(frappe.bold(get_link_to_form("Work Order", self.work_order)))) + + frappe.throw(_("{0} {1}").format(first_part_msg, second_part_msg), + JobCardCancelError, title = _("Error")) + + def update_work_order_data(self, for_quantity, time_in_mins, wo): + time_data = frappe.db.sql(""" SELECT min(from_time) as start_time, max(to_time) as end_time FROM `tabJob Card` jc, `tabJob Card Time Log` jctl WHERE jctl.parent = jc.name and jc.work_order = %s - and jc.{0} = %s and jc.docstatus = 1 - """.format(field), (self.work_order, self.get(field)), as_dict=1) + and jc.operation_id = %s and jc.docstatus = 1 + """, (self.work_order, self.operation_id), as_dict=1) - wo = frappe.get_doc('Work Order', self.work_order) + for data in wo.operations: + if data.get("name") == self.operation_id: + data.completed_qty = for_quantity + data.actual_operation_time = time_in_mins + data.actual_start_time = time_data[0].start_time if time_data else None + data.actual_end_time = time_data[0].end_time if time_data else None - for data in wo.operations: - if data.get("name") == self.get(field): - data.completed_qty = for_quantity - data.actual_operation_time = time_in_mins - data.actual_start_time = time_data[0].start_time if time_data else None - data.actual_end_time = time_data[0].end_time if time_data else None - - wo.flags.ignore_validate_update_after_submit = True - wo.update_operation_status() - wo.calculate_operating_cost() - wo.set_actual_dates() - wo.save() + wo.flags.ignore_validate_update_after_submit = True + wo.update_operation_status() + wo.calculate_operating_cost() + wo.set_actual_dates() + wo.save() def get_current_operation_data(self): return frappe.get_all('Job Card', diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index ce9699e1b3c..a77bd159afe 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import unittest import frappe -from frappe.utils import flt, time_diff_in_hours, now, add_months, cint, today +from frappe.utils import flt, now, add_months, cint, today, add_to_date from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry, ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError, CapacityError) from erpnext.stock.doctype.stock_entry import test_stock_entry @@ -14,6 +14,7 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde from erpnext.stock.doctype.item.test_item import make_item from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse +from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError class TestWorkOrder(unittest.TestCase): def setUp(self): @@ -369,21 +370,49 @@ class TestWorkOrder(unittest.TestCase): self.assertEqual(ste.total_additional_costs, 1000) def test_job_card(self): + stock_entries = [] data = frappe.get_cached_value('BOM', {'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item']) - if data: - frappe.db.set_value("Manufacturing Settings", - None, "disable_capacity_planning", 0) + bom, bom_item = data - bom, bom_item = data + bom_doc = frappe.get_doc('BOM', bom) + work_order = make_wo_order_test_record(item=bom_item, qty=1, + bom_no=bom, source_warehouse="_Test Warehouse - _TC") - bom_doc = frappe.get_doc('BOM', bom) - work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom) - self.assertTrue(work_order.planned_end_date) + for row in work_order.required_items: + stock_entry_doc = test_stock_entry.make_stock_entry(item_code=row.item_code, + target="_Test Warehouse - _TC", qty=row.required_qty, basic_rate=100) + stock_entries.append(stock_entry_doc) - job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}) - self.assertEqual(len(job_cards), len(bom_doc.operations)) + ste = frappe.get_doc(make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1)) + ste.submit() + stock_entries.append(ste) + + job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}) + self.assertEqual(len(job_cards), len(bom_doc.operations)) + + for i, job_card in enumerate(job_cards): + doc = frappe.get_doc("Job Card", job_card) + doc.append("time_logs", { + "from_time": now(), + "hours": i, + "to_time": add_to_date(now(), i), + "completed_qty": doc.for_quantity + }) + doc.submit() + + ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) + ste1.submit() + stock_entries.append(ste1) + + for job_card in job_cards: + doc = frappe.get_doc("Job Card", job_card) + self.assertRaises(JobCardCancelError, doc.cancel) + + stock_entries.reverse() + for stock_entry in stock_entries: + stock_entry.cancel() def test_capcity_planning(self): frappe.db.set_value("Manufacturing Settings", None, { @@ -509,7 +538,6 @@ class TestWorkOrder(unittest.TestCase): ste1.submit() ste_cancel_list.append(ste1) - print(wo_order.name) ste3 = frappe.get_doc(make_stock_entry(wo_order.name, "Material Consumption for Manufacture", 2)) self.assertEquals(ste3.fg_completed_qty, 2) From aea62da544b21025023c22317be39ec38e77b772 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 24 Dec 2020 22:44:31 +0530 Subject: [PATCH 145/295] fix: multiple pricing rule with margin type not working --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 6 +++++- erpnext/accounts/doctype/pricing_rule/utils.py | 10 +++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 55a5b0e5139..05652642eb0 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -345,9 +345,13 @@ def apply_price_discount_rule(pricing_rule, item_details, args): if ((pricing_rule.margin_type in ['Amount', 'Percentage'] and pricing_rule.currency == args.currency) or (pricing_rule.margin_type == 'Percentage')): item_details.margin_type = pricing_rule.margin_type - item_details.margin_rate_or_amount = pricing_rule.margin_rate_or_amount item_details.has_margin = True + if pricing_rule.apply_multiple_pricing_rules and item_details.margin_rate_or_amount is not None: + item_details.margin_rate_or_amount += pricing_rule.margin_rate_or_amount + else: + item_details.margin_rate_or_amount = pricing_rule.margin_rate_or_amount + if pricing_rule.rate_or_discount == 'Rate': pricing_rule_rate = 0.0 if pricing_rule.currency == args.currency: diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 2c7cd14451d..fb1fbe484ed 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -164,7 +164,15 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True): frappe.throw(_("Invalid {0}").format(args.get(field))) parent_groups = frappe.db.sql_list("""select name from `tab%s` - where lft<=%s and rgt>=%s""" % (parenttype, '%s', '%s'), (lft, rgt)) + where lft>=%s and rgt<=%s""" % (parenttype, '%s', '%s'), (lft, rgt)) + + if parenttype in ["Customer Group", "Item Group", "Territory"]: + parent_field = "parent_{0}".format(frappe.scrub(parenttype)) + root_name = frappe.db.get_list(parenttype, + {"is_group": 1, parent_field: ("is", "not set")}, "name", as_list=1) + + if root_name and root_name[0][0]: + parent_groups.append(root_name[0][0]) if parent_groups: if allow_blank: parent_groups.append('') From 6d74f5b59bd08ccaea79e714b3db657ef78352ed Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 25 Dec 2020 10:26:43 +0530 Subject: [PATCH 146/295] feat: GST E Invoicing (#23455) * feat: init e-invoice settings * feat: read public key file * feat: rsa encryption with public key * feat: save token and sek from auth request * chore: handle error response * feat: AES decryption of SEK with appkey * feat: decrypt json data with SEK * feat: make e invoice from erpnext sales invoice * feat: generate IRN * feat: decode signed json and QR code * chore: validations * feat: cancel IRN * feat: complete e-invoice schema * chore: move e-invoice settings to regional * chore: split einvoice settings and operations * chore: rename schema to template & js cleanup * feat: make IRN field on regional setup * feat: Generate & Cancel IRN from Sales Invoice * chore: minor fixes * fix: item discount * chore: show irn cancelled check after cancellation * fix: hide cancel irn dialog on error * fix: public key is required on validate * fix: cannot find attached key file * fix: validation if e invoicing is disabled * fix: do not show generate irn for invalid supply type * fix: update irn_cancelled after cancelling irn * chore: show irn field for proper gst_category * feat: e-way bill details in e-invoice * fix: save e-way bill no on irn generation * chore: no copy on e invoice custom fields * feat: cancel e-way bill before cancelling IRN * feat: manual download / upload json * chore: group e-invoicing actions * fix: fn name * chore: save signed invoice and qrcode after uplaoding irn * fix: fetch token if not valid * chore: move einvoicing stuff to seperate folder * feat: QRCode Image and E-Invoice Print Format * fix: bug * fix: invalid syntax * chore: code cleanup * chore: clean up e invoice actions * fix: download & upload e-invoice * fix: print format * fix: validations * fix: add permissions on regional setup * feat: add patch * fix: validate document name * fix: return date * fix: credit note einvoice * fix: validations * fix: error logging * fix: e_invoice module not found * fix: add missing package * fix: rename e_invoice_utils.py * fix: einvoice field validation * fix: patch * fix: invoice totals calculation * fix: other charges calculation * chore: improve document name validation message * fix: qr code image string * feat: initialize GSP connector * chore: remove unwanted fields * fix: qr code generation * feat: fetch and cache GSTIN details * feat: generate & cancel IRN * feat: cancel eway bill * chore: remove unwanted fuctions * chore: clean up einvoice actions * fix: attach qrcode on irn generation * fix: generate & cancel IRN * fix: show/hide eway bill fields * fix: valiations * feat: generate eway bill from IRN * chore: remove unwanted imports * chore: error logging * feat: header & footer in GST E Invoice * chore: remove test pincode * fix: invalid syntax * feat: cess non advolem on einvoice item * chore: remove fetch token from e invocie settings * fix: imports * fix: error handling * feat: update timeline on einvoice actions * fix: qrcode image size * fix: exclude intra company transactions * fix: eway bill test * fix: ewaybill mandatory conditions * chore: add tests * fix: returning condition * feat: log e-invocing requests * chore: add ack date and ack no field for print formats * fix: sider issues * feat: show e-invoice preview before IRN generation * fix: use as_list for error message * fix: minor ux issues * fix: dialog is undefined * fix: error handling * feat: add docs link to e invoice settings * feat: multiple gstins for e invoicing * fix: uncomment test condition * fix: remove test pincode * fix: cannot cancel irn without submitting sales invoice * chore: code cleanup * fix: sider issues * fix: e invoice request log permissions Co-authored-by: Nabin Hait --- .../doctype/sales_invoice/regional/india.js | 2 + .../doctype/sales_invoice/sales_invoice.py | 2 +- .../sales_invoice/test_sales_invoice.py | 269 +++-- .../print_format/gst_e_invoice/__init__.py | 0 .../gst_e_invoice/gst_e_invoice.html | 162 +++ .../gst_e_invoice/gst_e_invoice.json | 24 + erpnext/controllers/accounts_controller.py | 10 + erpnext/hooks.py | 3 +- erpnext/patches.txt | 1 + .../patches/v12_0/setup_einvoice_fields.py | 55 + .../doctype/e_invoice_request_log/__init__.py | 0 .../e_invoice_request_log.js | 8 + .../e_invoice_request_log.json | 103 ++ .../e_invoice_request_log.py | 10 + .../test_e_invoice_request_log.py | 10 + .../doctype/e_invoice_settings/__init__.py | 0 .../e_invoice_settings/e_invoice_settings.js | 11 + .../e_invoice_settings.json | 58 ++ .../e_invoice_settings/e_invoice_settings.py | 14 + .../test_e_invoice_settings.py | 10 + .../doctype/e_invoice_user/__init__.py | 0 .../e_invoice_user/e_invoice_user.json | 48 + .../doctype/e_invoice_user/e_invoice_user.py | 10 + erpnext/regional/india/e_invoice/__init__.py | 0 .../india/e_invoice/einv_item_template.json | 31 + .../india/e_invoice/einv_template.json | 110 ++ .../india/e_invoice/einv_validation.json | 956 ++++++++++++++++++ erpnext/regional/india/e_invoice/einvoice.js | 305 ++++++ erpnext/regional/india/e_invoice/utils.py | 772 ++++++++++++++ erpnext/regional/india/setup.py | 31 +- requirements.txt | 1 + 31 files changed, 2922 insertions(+), 94 deletions(-) create mode 100644 erpnext/accounts/print_format/gst_e_invoice/__init__.py create mode 100644 erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html create mode 100644 erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.json create mode 100644 erpnext/patches/v12_0/setup_einvoice_fields.py create mode 100644 erpnext/regional/doctype/e_invoice_request_log/__init__.py create mode 100644 erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.js create mode 100644 erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json create mode 100644 erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.py create mode 100644 erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.py create mode 100644 erpnext/regional/doctype/e_invoice_settings/__init__.py create mode 100644 erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js create mode 100644 erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json create mode 100644 erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py create mode 100644 erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.py create mode 100644 erpnext/regional/doctype/e_invoice_user/__init__.py create mode 100644 erpnext/regional/doctype/e_invoice_user/e_invoice_user.json create mode 100644 erpnext/regional/doctype/e_invoice_user/e_invoice_user.py create mode 100644 erpnext/regional/india/e_invoice/__init__.py create mode 100644 erpnext/regional/india/e_invoice/einv_item_template.json create mode 100644 erpnext/regional/india/e_invoice/einv_template.json create mode 100644 erpnext/regional/india/e_invoice/einv_validation.json create mode 100644 erpnext/regional/india/e_invoice/einvoice.js create mode 100644 erpnext/regional/india/e_invoice/utils.py diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india.js b/erpnext/accounts/doctype/sales_invoice/regional/india.js index 6336db16ebc..f54bce8aac7 100644 --- a/erpnext/accounts/doctype/sales_invoice/regional/india.js +++ b/erpnext/accounts/doctype/sales_invoice/regional/india.js @@ -1,6 +1,8 @@ {% include "erpnext/regional/india/taxes.js" %} +{% include "erpnext/regional/india/e_invoice/einvoice.js" %} erpnext.setup_auto_gst_taxation('Sales Invoice'); +erpnext.setup_einvoice_actions('Sales Invoice') frappe.ui.form.on("Sales Invoice", { setup: function(frm) { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 50734c865cd..40009ac69d0 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -232,9 +232,9 @@ class SalesInvoice(SellingController): frappe.throw(_("At least one mode of payment is required for POS invoice.")) def before_cancel(self): + super(SalesInvoice, self).before_cancel() self.update_time_sheet(None) - def on_cancel(self): super(SalesInvoice, self).on_cancel() diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index ceb79079893..3c681eeecf2 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1825,93 +1825,7 @@ class TestSalesInvoice(unittest.TestCase): # check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1)) def test_eway_bill_json(self): - if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'): - address = frappe.get_doc({ - "address_line1": "_Test Address Line 1", - "address_title": "_Test Address for Eway bill", - "address_type": "Billing", - "city": "_Test City", - "state": "Test State", - "country": "India", - "doctype": "Address", - "is_primary_address": 1, - "phone": "+91 0000000000", - "gstin": "27AAECE4835E1ZR", - "gst_state": "Maharashtra", - "gst_state_number": "27", - "pincode": "401108" - }).insert() - - address.append("links", { - "link_doctype": "Company", - "link_name": "_Test Company" - }) - - address.save() - - if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'): - address = frappe.get_doc({ - "address_line1": "_Test Address Line 1", - "address_title": "_Test Customer-Address for Eway bill", - "address_type": "Shipping", - "city": "_Test City", - "state": "Test State", - "country": "India", - "doctype": "Address", - "is_primary_address": 1, - "phone": "+91 0000000000", - "gst_state": "Maharashtra", - "gst_state_number": "27", - "pincode": "410038" - }).insert() - - address.append("links", { - "link_doctype": "Customer", - "link_name": "_Test Customer" - }) - - address.save() - - gst_settings = frappe.get_doc("GST Settings") - - gst_account = frappe.get_all( - "GST Account", - fields=["cgst_account", "sgst_account", "igst_account"], - filters = {"company": "_Test Company"}) - - if not gst_account: - gst_settings.append("gst_accounts", { - "company": "_Test Company", - "cgst_account": "CGST - _TC", - "sgst_account": "SGST - _TC", - "igst_account": "IGST - _TC", - }) - - gst_settings.save() - - si = create_sales_invoice(do_not_save =1, rate = '60000') - - si.distance = 2000 - si.company_address = "_Test Address for Eway bill-Billing" - si.customer_address = "_Test Customer-Address for Eway bill-Shipping" - si.vehicle_no = "KA12KA1234" - si.gst_category = "Registered Regular" - - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "CGST - _TC", - "cost_center": "Main - _TC", - "description": "CGST @ 9.0", - "rate": 9 - }) - - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "SGST - _TC", - "cost_center": "Main - _TC", - "description": "SGST @ 9.0", - "rate": 9 - }) + si = make_sales_invoice_for_ewaybill() si.submit() @@ -1927,6 +1841,187 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(data['billLists'][0]['sgstValue'], 5400) self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234') self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000) + + def test_einvoice_submission_without_irn(self): + # init + frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 1) + country = frappe.flags.country + frappe.flags.country = 'India' + + si = make_sales_invoice_for_ewaybill() + self.assertRaises(frappe.ValidationError, si.submit) + + si.irn = 'test_irn' + si.submit() + + # reset + frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 0) + frappe.flags.country = country + + 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 = [] + si.append("items", { + "item_code": "_Test Item", + "uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + "qty": 2, + "rate": 100, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }) + si.append("items", { + "item_code": "_Test Item 2", + "uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + "qty": 4, + "rate": 150, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }) + si.save() + + einvoice = make_einvoice(si) + + total_item_ass_value = sum([d['AssAmt'] for d in einvoice['ItemList']]) + total_item_cgst_value = sum([d['CgstAmt'] for d in einvoice['ItemList']]) + total_item_sgst_value = sum([d['SgstAmt'] for d in einvoice['ItemList']]) + total_item_igst_value = sum([d['IgstAmt'] for d in einvoice['ItemList']]) + total_item_value = sum([d['TotItemVal'] for d in einvoice['ItemList']]) + + self.assertEqual(einvoice['Version'], '1.1') + self.assertEqual(einvoice['ValDtls']['AssVal'], total_item_ass_value) + self.assertEqual(einvoice['ValDtls']['CgstVal'], total_item_cgst_value) + self.assertEqual(einvoice['ValDtls']['SgstVal'], total_item_sgst_value) + self.assertEqual(einvoice['ValDtls']['IgstVal'], total_item_igst_value) + self.assertEqual(einvoice['ValDtls']['TotInvVal'], total_item_value) + self.assertTrue(einvoice['EwbDtls']) + +def make_sales_invoice_for_ewaybill(): + if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'): + address = frappe.get_doc({ + "address_line1": "_Test Address Line 1", + "address_title": "_Test Address for Eway bill", + "address_type": "Billing", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 1, + "phone": "+910000000000", + "gstin": "27AAECE4835E1ZR", + "gst_state": "Maharashtra", + "gst_state_number": "27", + "pincode": "401108" + }).insert() + + address.append("links", { + "link_doctype": "Company", + "link_name": "_Test Company" + }) + + address.save() + + if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'): + address = frappe.get_doc({ + "address_line1": "_Test Address Line 1", + "address_title": "_Test Customer-Address for Eway bill", + "address_type": "Shipping", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 1, + "phone": "+910000000000", + "gstin": "27AACCM7806M1Z3", + "gst_state": "Maharashtra", + "gst_state_number": "27", + "pincode": "410038" + }).insert() + + address.append("links", { + "link_doctype": "Customer", + "link_name": "_Test Customer" + }) + + address.save() + + if not frappe.db.exists('Supplier', '_Test Transporter'): + frappe.get_doc({ + "doctype": "Supplier", + "supplier_name": "_Test Transporter", + "country": "India", + "supplier_group": "_Test Supplier Group", + "supplier_type": "Company", + "is_transporter": 1 + }).insert() + + gst_settings = frappe.get_doc("GST Settings") + + gst_account = frappe.get_all( + "GST Account", + fields=["cgst_account", "sgst_account", "igst_account"], + filters = {"company": "_Test Company"}) + + if not gst_account: + gst_settings.append("gst_accounts", { + "company": "_Test Company", + "cgst_account": "CGST - _TC", + "sgst_account": "SGST - _TC", + "igst_account": "IGST - _TC", + }) + + gst_settings.save() + + si = create_sales_invoice(do_not_save =1, rate = '60000') + + si.distance = 2000 + si.company_address = "_Test Address for Eway bill-Billing" + si.customer_address = "_Test Customer-Address for Eway bill-Shipping" + si.vehicle_no = "KA12KA1234" + si.gst_category = "Registered Regular" + si.mode_of_transport = 'Road' + si.transporter = '_Test Transporter' + + si.append("taxes", { + "charge_type": "On Net Total", + "account_head": "CGST - _TC", + "cost_center": "Main - _TC", + "description": "CGST @ 9.0", + "rate": 9 + }) + + si.append("taxes", { + "charge_type": "On Net Total", + "account_head": "SGST - _TC", + "cost_center": "Main - _TC", + "description": "SGST @ 9.0", + "rate": 9 + }) + + return si def check_gl_entries(doc, voucher_no, expected_gle, posting_date): gl_entries = frappe.db.sql("""select account, debit, credit, posting_date diff --git a/erpnext/accounts/print_format/gst_e_invoice/__init__.py b/erpnext/accounts/print_format/gst_e_invoice/__init__.py new file mode 100644 index 00000000000..e69de29bb2d 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 new file mode 100644 index 00000000000..9827e00b71b --- /dev/null +++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html @@ -0,0 +1,162 @@ +{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%} +{%- set einvoice = json.loads(doc.signed_einvoice) -%} + +
    +
    + {% if letter_head and not no_letterhead %} +
    {{ letter_head }}
    + {% endif %} + +
    + {% if print_settings.repeat_header_footer %} + + {% endif %} +
    +
    1. Transaction Details
    +
    +
    +
    +
    {{ einvoice.Irn }}
    +
    +
    +
    +
    {{ einvoice.AckNo }}
    +
    +
    +
    +
    {{ frappe.utils.format_datetime(einvoice.AckDt, "dd/MM/yyyy hh:mm:ss") }}
    +
    +
    +
    +
    {{ einvoice.TranDtls.SupTyp }}
    +
    +
    +
    +
    {{ einvoice.DocDtls.Typ }}
    +
    +
    +
    +
    {{ einvoice.DocDtls.No }}
    +
    +
    +
    + +
    +
    +
    +
    2. Party Details
    + {%- set seller = einvoice.SellerDtls -%} +
    +
    Seller
    +

    {{ seller.Gstin }}

    +

    {{ seller.LglNm }}

    +

    {{ seller.Addr1 }}

    + {%- if seller.Addr2 -%}

    {{ seller.Addr2 }}

    {% endif %} +

    {{ seller.Loc }}

    +

    {{ frappe.db.get_value("Address", doc.company_address, "gst_state") }} - {{ seller.Pin }}

    + + {%- if einvoice.ShipDtls -%} + {%- set shipping = einvoice.ShipDtls -%} +
    Shipping
    +

    {{ shipping.Gstin }}

    +

    {{ shipping.LglNm }}

    +

    {{ shipping.Addr1 }}

    + {%- if shipping.Addr2 -%}

    {{ shipping.Addr2 }}

    {% endif %} +

    {{ shipping.Loc }}

    +

    {{ frappe.db.get_value("Address", doc.shipping_address_name, "gst_state") }} - {{ shipping.Pin }}

    + {% endif %} +
    + {%- set buyer = einvoice.BuyerDtls -%} +
    +
    Buyer
    +

    {{ buyer.Gstin }}

    +

    {{ buyer.LglNm }}

    +

    {{ buyer.Addr1 }}

    + {%- if buyer.Addr2 -%}

    {{ buyer.Addr2 }}

    {% endif %} +

    {{ buyer.Loc }}

    +

    {{ frappe.db.get_value("Address", doc.customer_address, "gst_state") }} - {{ buyer.Pin }}

    +
    +
    +
    +
    3. Item Details
    + + + + + + + + + + + + + + + + + + {% for item in einvoice.ItemList %} + + + + + + + + + + + + + + {% endfor %} + +
    Sr. No.ItemHSN CodeQtyUOMRateDiscountTaxable AmountTax RateOther ChargesTotal
    {{ item.SlNo }}{{ item.PrdDesc }}{{ item.HsnCd }}{{ item.Qty }}{{ item.Unit }}{{ frappe.utils.fmt_money(item.UnitPrice, None, "INR") }}{{ frappe.utils.fmt_money(item.Discount, None, "INR") }}{{ frappe.utils.fmt_money(item.AssAmt, None, "INR") }}{{ item.GstRt + item.CesRt }} %{{ frappe.utils.fmt_money(0, None, "INR") }}{{ frappe.utils.fmt_money(item.TotItemVal, None, "INR") }}
    +
    +
    +
    4. Value Details
    + + + + + + + + + + + + + + + + + {%- set value_details = einvoice.ValDtls -%} + + + + + + + + + + + + + +
    Taxable AmountCGSTSGSTIGSTCESSState CESSDiscountOther ChargesRound OffTotal Value
    {{ frappe.utils.fmt_money(value_details.AssVal, None, "INR") }}{{ frappe.utils.fmt_money(value_details.CgstVal, None, "INR") }}{{ frappe.utils.fmt_money(value_details.SgstVal, None, "INR") }}{{ frappe.utils.fmt_money(value_details.IgstVal, None, "INR") }}{{ 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.RndOffAmt, None, "INR") }}{{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }}
    +
    +
    \ No newline at end of file diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.json b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.json new file mode 100644 index 00000000000..1001199a092 --- /dev/null +++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.json @@ -0,0 +1,24 @@ +{ + "align_labels_right": 1, + "creation": "2020-10-10 18:01:21.032914", + "custom_format": 0, + "default_print_language": "en-US", + "disabled": 1, + "doc_type": "Sales Invoice", + "docstatus": 0, + "doctype": "Print Format", + "font": "Default", + "html": "", + "idx": 0, + "line_breaks": 1, + "modified": "2020-10-23 19:54:40.634936", + "modified_by": "Administrator", + "module": "Accounts", + "name": "GST E-Invoice", + "owner": "Administrator", + "print_format_builder": 0, + "print_format_type": "Jinja", + "raw_printing": 0, + "show_section_headings": 1, + "standard": "Yes" +} \ No newline at end of file diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 32c5d3a3b14..0f1aa23064c 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -110,8 +110,14 @@ 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 validate_deferred_start_and_end_date(self): for d in self.items: @@ -1518,3 +1524,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil @erpnext.allow_regional def validate_regional(doc): pass + +@erpnext.allow_regional +def validate_einvoice_fields(doc): + pass diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 1e3bb6a5cfb..a2d9d861bb8 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -397,7 +397,8 @@ regional_overrides = { 'erpnext.accounts.party.get_regional_address_details': 'erpnext.regional.india.utils.get_regional_address_details', 'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption', 'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period', - 'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries' + 'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries', + 'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields' }, 'United Arab Emirates': { 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data', diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9e33014c38e..d69dabf15cd 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -732,6 +732,7 @@ erpnext.patches.v13_0.set_youtube_video_id erpnext.patches.v13_0.print_uom_after_quantity_patch erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account 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 diff --git a/erpnext/patches/v12_0/setup_einvoice_fields.py b/erpnext/patches/v12_0/setup_einvoice_fields.py new file mode 100644 index 00000000000..d0782765dee --- /dev/null +++ b/erpnext/patches/v12_0/setup_einvoice_fields.py @@ -0,0 +1,55 @@ +from __future__ import unicode_literals +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields +from erpnext.regional.india.setup import add_permissions, add_print_formats + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + frappe.reload_doc("regional", "doctype", "e_invoice_settings") + custom_fields = { + 'Sales Invoice': [ + dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1, + depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), + + dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1), + + dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), + + dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1, + depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), + + dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1, + depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), + + dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1), + + dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1), + + dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1) + ] + } + create_custom_fields(custom_fields, update=True) + add_permissions() + add_print_formats() + + einvoice_cond = 'in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category)' + t = { + 'mode_of_transport': [{'default': None}], + 'distance': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.transporter'}], + 'gst_vehicle_type': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'}], + 'lr_date': [{'mandatory_depends_on': f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)'}], + 'lr_no': [{'mandatory_depends_on': f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)'}], + 'vehicle_no': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'}], + 'ewaybill': [ + {'read_only_depends_on': 'eval:doc.irn && doc.ewaybill'}, + {'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)'} + ] + } + + for field, conditions in t.items(): + for c in conditions: + [(prop, value)] = c.items() + frappe.db.set_value('Custom Field', { 'fieldname': field }, prop, value) diff --git a/erpnext/regional/doctype/e_invoice_request_log/__init__.py b/erpnext/regional/doctype/e_invoice_request_log/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.js b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.js new file mode 100644 index 00000000000..7b7ba964e5e --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('E Invoice Request Log', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json new file mode 100644 index 00000000000..5c1c79dc047 --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json @@ -0,0 +1,103 @@ +{ + "actions": [], + "autoname": "EINV-REQ-.#####", + "creation": "2020-12-08 12:54:08.175992", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user", + "url", + "headers", + "response", + "column_break_7", + "timestamp", + "reference_invoice", + "data" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User" + }, + { + "fieldname": "reference_invoice", + "fieldtype": "Link", + "label": "Reference Invoice", + "options": "Sales Invoice" + }, + { + "fieldname": "headers", + "fieldtype": "Code", + "label": "Headers", + "options": "JSON" + }, + { + "fieldname": "data", + "fieldtype": "Code", + "label": "Data", + "options": "JSON" + }, + { + "default": "Now", + "fieldname": "timestamp", + "fieldtype": "Datetime", + "label": "Timestamp" + }, + { + "fieldname": "response", + "fieldtype": "Code", + "label": "Response", + "options": "JSON" + }, + { + "fieldname": "url", + "fieldtype": "Data", + "label": "URL" + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-12-24 21:09:38.882866", + "modified_by": "Administrator", + "module": "Regional", + "name": "E Invoice Request Log", + "owner": "Administrator", + "permissions": [ + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.py b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.py new file mode 100644 index 00000000000..9150bdd9260 --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.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 EInvoiceRequestLog(Document): + pass diff --git a/erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.py b/erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.py new file mode 100644 index 00000000000..c84e9a249bd --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestEInvoiceRequestLog(unittest.TestCase): + pass diff --git a/erpnext/regional/doctype/e_invoice_settings/__init__.py b/erpnext/regional/doctype/e_invoice_settings/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js new file mode 100644 index 00000000000..cc2d9f06d2d --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js @@ -0,0 +1,11 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('E Invoice Settings', { + refresh(frm) { + const docs_link = 'https://docs.erpnext.com/docs/user/manual/en/regional/india/setup-e-invoicing'; + frm.dashboard.set_headline( + __("Read {0} for more information on E Invoicing features.", [`documentation`]) + ); + } +}); diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json new file mode 100644 index 00000000000..4dcb22a54c7 --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json @@ -0,0 +1,58 @@ +{ + "actions": [], + "creation": "2020-09-24 16:23:16.235722", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "enable", + "section_break_2", + "credentials", + "auth_token", + "token_expiry" + ], + "fields": [ + { + "default": "0", + "fieldname": "enable", + "fieldtype": "Check", + "label": "Enable" + }, + { + "depends_on": "enable", + "fieldname": "section_break_2", + "fieldtype": "Section Break" + }, + { + "fieldname": "auth_token", + "fieldtype": "Data", + "hidden": 1, + "read_only": 1 + }, + { + "fieldname": "token_expiry", + "fieldtype": "Datetime", + "hidden": 1, + "read_only": 1 + }, + { + "fieldname": "credentials", + "fieldtype": "Table", + "label": "Credentials", + "mandatory_depends_on": "enable", + "options": "E Invoice User" + } + ], + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2020-12-22 15:34:57.280044", + "modified_by": "Administrator", + "module": "Regional", + "name": "E Invoice Settings", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py new file mode 100644 index 00000000000..c24ad886ea1 --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py @@ -0,0 +1,14 @@ +# -*- 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 _ +from frappe.model.document import Document + +class EInvoiceSettings(Document): + def validate(self): + if self.enable and not self.credentials: + frappe.throw(_('You must add atleast one credentials to be able to use E Invoicing.')) + diff --git a/erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.py new file mode 100644 index 00000000000..a11ce63ee6c --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestEInvoiceSettings(unittest.TestCase): + pass diff --git a/erpnext/regional/doctype/e_invoice_user/__init__.py b/erpnext/regional/doctype/e_invoice_user/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json new file mode 100644 index 00000000000..dd9d99773a3 --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json @@ -0,0 +1,48 @@ +{ + "actions": [], + "creation": "2020-12-22 15:02:46.229474", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "gstin", + "username", + "password" + ], + "fields": [ + { + "fieldname": "gstin", + "fieldtype": "Data", + "in_list_view": 1, + "label": "GSTIN", + "reqd": 1 + }, + { + "fieldname": "username", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Username", + "reqd": 1 + }, + { + "fieldname": "password", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Password", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-12-22 15:10:53.466205", + "modified_by": "Administrator", + "module": "Regional", + "name": "E Invoice User", + "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/regional/doctype/e_invoice_user/e_invoice_user.py b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.py new file mode 100644 index 00000000000..056c54f069d --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.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 EInvoiceUser(Document): + pass diff --git a/erpnext/regional/india/e_invoice/__init__.py b/erpnext/regional/india/e_invoice/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/india/e_invoice/einv_item_template.json b/erpnext/regional/india/e_invoice/einv_item_template.json new file mode 100644 index 00000000000..78e56518dff --- /dev/null +++ b/erpnext/regional/india/e_invoice/einv_item_template.json @@ -0,0 +1,31 @@ +{{ + "SlNo": "{item.sr_no}", + "PrdDesc": "{item.description}", + "IsServc": "{item.is_service_item}", + "HsnCd": "{item.gst_hsn_code}", + "Barcde": "{item.barcode}", + "Unit": "{item.uom}", + "Qty": "{item.qty}", + "FreeQty": "{item.free_qty}", + "UnitPrice": "{item.unit_rate}", + "TotAmt": "{item.gross_amount}", + "Discount": "{item.discount_amount}", + "AssAmt": "{item.taxable_value}", + "PrdSlNo": "{item.serial_no}", + "GstRt": "{item.tax_rate}", + "IgstAmt": "{item.igst_amount}", + "CgstAmt": "{item.cgst_amount}", + "SgstAmt": "{item.sgst_amount}", + "CesRt": "{item.cess_rate}", + "CesAmt": "{item.cess_amount}", + "CesNonAdvlAmt": "{item.cess_nadv_amount}", + "StateCesRt": "{item.state_cess_rate}", + "StateCesAmt": "{item.state_cess_amount}", + "StateCesNonAdvlAmt": "{item.state_cess_nadv_amount}", + "OthChrg": "{item.other_charges}", + "TotItemVal": "{item.total_value}", + "BchDtls": {{ + "Nm": "{item.batch_no}", + "ExpDt": "{item.batch_expiry_date}" + }} +}} \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/einv_template.json b/erpnext/regional/india/e_invoice/einv_template.json new file mode 100644 index 00000000000..e5751da5612 --- /dev/null +++ b/erpnext/regional/india/e_invoice/einv_template.json @@ -0,0 +1,110 @@ +{{ + "Version": "1.1", + "TranDtls": {{ + "TaxSch": "{transaction_details.tax_scheme}", + "SupTyp": "{transaction_details.supply_type}", + "RegRev": "{transaction_details.reverse_charge}", + "EcmGstin": "{transaction_details.ecom_gstin}", + "IgstOnIntra": "{transaction_details.igst_on_intra}" + }}, + "DocDtls": {{ + "Typ": "{doc_details.invoice_type}", + "No": "{doc_details.invoice_name}", + "Dt": "{doc_details.invoice_date}" + }}, + "SellerDtls": {{ + "Gstin": "{seller_details.gstin}", + "LglNm": "{seller_details.legal_name}", + "TrdNm": "{seller_details.trade_name}", + "Loc": "{seller_details.location}", + "Pin": "{seller_details.pincode}", + "Stcd": "{seller_details.state_code}", + "Addr1": "{seller_details.address_line1}", + "Addr2": "{seller_details.address_line2}", + "Ph": "{seller_details.phone}", + "Em": "{seller_details.email}" + }}, + "BuyerDtls": {{ + "Gstin": "{buyer_details.gstin}", + "LglNm": "{buyer_details.legal_name}", + "TrdNm": "{buyer_details.trade_name}", + "Addr1": "{buyer_details.address_line1}", + "Addr2": "{buyer_details.address_line2}", + "Loc": "{buyer_details.location}", + "Pin": "{buyer_details.pincode}", + "Stcd": "{buyer_details.state_code}", + "Ph": "{buyer_details.phone}", + "Em": "{buyer_details.email}", + "Pos": "{buyer_details.place_of_supply}" + }}, + "DispDtls": {{ + "Nm": "{dispatch_details.company_name}", + "Addr1": "{dispatch_details.address_line1}", + "Addr2": "{dispatch_details.address_line2}", + "Loc": "{dispatch_details.location}", + "Pin": "{dispatch_details.pincode}", + "Stcd": "{dispatch_details.state_code}" + }}, + "ShipDtls": {{ + "Gstin": "{shipping_details.gstin}", + "LglNm": "{shipping_details.legal_name}", + "TrdNm": "{shipping_details.trader_name}", + "Addr1": "{shipping_details.address_line1}", + "Addr2": "{shipping_details.address_line2}", + "Loc": "{shipping_details.location}", + "Pin": "{shipping_details.pincode}", + "Stcd": "{shipping_details.state_code}" + }}, + "ItemList": [ + {item_list} + ], + "ValDtls": {{ + "AssVal": "{invoice_value_details.base_net_total}", + "CgstVal": "{invoice_value_details.total_cgst_amt}", + "SgstVal": "{invoice_value_details.total_sgst_amt}", + "IgstVal": "{invoice_value_details.total_igst_amt}", + "CesVal": "{invoice_value_details.total_cess_amt}", + "Discount": "{invoice_value_details.invoice_discount_amt}", + "RndOffAmt": "{invoice_value_details.round_off}", + "OthChrg": "{invoice_value_details.total_other_charges}", + "TotInvVal": "{invoice_value_details.base_grand_total}", + "TotInvValFc": "{invoice_value_details.grand_total}" + }}, + "PayDtls": {{ + "Nm": "{payment_details.payee_name}", + "AccDet": "{payment_details.account_no}", + "Mode": "{payment_details.mode_of_payment}", + "FinInsBr": "{payment_details.ifsc_code}", + "PayTerm": "{payment_details.terms}", + "PaidAmt": "{payment_details.paid_amount}", + "PaymtDue": "{payment_details.outstanding_amount}" + }}, + "RefDtls": {{ + "DocPerdDtls": {{ + "InvStDt": "{period_details.start_date}", + "InvEndDt": "{period_details.end_date}" + }}, + "PrecDocDtls": [{{ + "InvNo": "{prev_doc_details.invoice_name}", + "InvDt": "{prev_doc_details.invoice_date}" + }}] + }}, + "ExpDtls": {{ + "ShipBNo": "{export_details.bill_no}", + "ShipBDt": "{export_details.bill_date}", + "Port": "{export_details.port}", + "ForCur": "{export_details.foreign_curr_code}", + "CntCode": "{export_details.country_code}", + "ExpDuty": "{export_details.export_duty}" + }}, + "EwbDtls": {{ + "TransId": "{eway_bill_details.gstin}", + "TransName": "{eway_bill_details.name}", + "TransMode": "{eway_bill_details.mode_of_transport}", + "Distance": "{eway_bill_details.distance}", + "TransDocNo": "{eway_bill_details.document_name}", + "TransDocDt": "{eway_bill_details.document_date}", + "VehNo": "{eway_bill_details.vehicle_no}", + "VehType": "{eway_bill_details.vehicle_type}" + }} +}} \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/einv_validation.json b/erpnext/regional/india/e_invoice/einv_validation.json new file mode 100644 index 00000000000..86290cfe524 --- /dev/null +++ b/erpnext/regional/india/e_invoice/einv_validation.json @@ -0,0 +1,956 @@ +{ + "Version": { + "type": "string", + "minLength": 1, + "maxLength": 6, + "description": "Version of the schema" + }, + "Irn": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "Invoice Reference Number" + }, + "TranDtls": { + "type": "object", + "properties": { + "TaxSch": { + "type": "string", + "minLength": 3, + "maxLength": 10, + "enum": ["GST"], + "description": "GST- Goods and Services Tax Scheme" + }, + "SupTyp": { + "type": "string", + "minLength": 3, + "maxLength": 10, + "enum": ["B2B", "SEZWP", "SEZWOP", "EXPWP", "EXPWOP", "DEXP"], + "description": "Type of Supply: B2B-Business to Business, SEZWP - SEZ with payment, SEZWOP - SEZ without payment, EXPWP - Export with Payment, EXPWOP - Export without payment,DEXP - Deemed Export" + }, + "RegRev": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "enum": ["Y", "N"], + "description": "Y- whether the tax liability is payable under reverse charge" + }, + "EcmGstin": { + "type": "string", + "minLength": 15, + "maxLength": 15, + "pattern": "([0-9]{2}[0-9A-Z]{13})", + "description": "E-Commerce GSTIN", + "validationMsg": "E-Commerce GSTIN is invalid" + }, + "IgstOnIntra": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "enum": ["Y", "N"], + "description": "Y- indicates the supply is intra state but chargeable to IGST" + } + }, + "required": ["TaxSch", "SupTyp"] + }, + "DocDtls": { + "type": "object", + "properties": { + "Typ": { + "type": "string", + "minLength": 3, + "maxLength": 3, + "enum": ["INV", "CRN", "DBN"], + "description": "Document Type" + }, + "No": { + "type": "string", + "minLength": 1, + "maxLength": 16, + "pattern": "^([A-Z1-9]{1}[A-Z0-9/-]{0,15})$", + "description": "Document Number", + "validationMsg": "Document Number should not be starting with 0, / and -" + }, + "Dt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Document Date" + } + }, + "required": ["Typ", "No", "Dt"] + }, + "SellerDtls": { + "type": "object", + "properties": { + "Gstin": { + "type": "string", + "minLength": 15, + "maxLength": 15, + "pattern": "([0-9]{2}[0-9A-Z]{13})", + "description": "Supplier GSTIN", + "validationMsg": "Company GSTIN is invalid" + }, + "LglNm": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Legal Name" + }, + "TrdNm": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Tradename" + }, + "Addr1": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Address Line 1" + }, + "Addr2": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Address Line 2" + }, + "Loc": { + "type": "string", + "minLength": 3, + "maxLength": 50, + "description": "Location" + }, + "Pin": { + "type": "number", + "minimum": 100000, + "maximum": 999999, + "description": "Pincode" + }, + "Stcd": { + "type": "string", + "minLength": 1, + "maxLength": 2, + "description": "Supplier State Code" + }, + "Ph": { + "type": "string", + "minLength": 6, + "maxLength": 12, + "description": "Phone" + }, + "Em": { + "type": "string", + "minLength": 6, + "maxLength": 100, + "description": "Email-Id" + } + }, + "required": ["Gstin", "LglNm", "Addr1", "Loc", "Pin", "Stcd"] + }, + "BuyerDtls": { + "type": "object", + "properties": { + "Gstin": { + "type": "string", + "minLength": 3, + "maxLength": 15, + "pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$", + "description": "Buyer GSTIN", + "validationMsg": "Customer GSTIN is invalid" + }, + "LglNm": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Legal Name" + }, + "TrdNm": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Trade Name" + }, + "Pos": { + "type": "string", + "minLength": 1, + "maxLength": 2, + "description": "Place of Supply State code" + }, + "Addr1": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Address Line 1" + }, + "Addr2": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Address Line 2" + }, + "Loc": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Location" + }, + "Pin": { + "type": "number", + "minimum": 100000, + "maximum": 999999, + "description": "Pincode" + }, + "Stcd": { + "type": "string", + "minLength": 1, + "maxLength": 2, + "description": "Buyer State Code" + }, + "Ph": { + "type": "string", + "minLength": 6, + "maxLength": 12, + "description": "Phone" + }, + "Em": { + "type": "string", + "minLength": 6, + "maxLength": 100, + "description": "Email-Id" + } + }, + "required": ["Gstin", "LglNm", "Pos", "Addr1", "Loc", "Stcd"] + }, + "DispDtls": { + "type": "object", + "properties": { + "Nm": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Dispatch Address Name" + }, + "Addr1": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Address Line 1" + }, + "Addr2": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Address Line 2" + }, + "Loc": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Location" + }, + "Pin": { + "type": "number", + "minimum": 100000, + "maximum": 999999, + "description": "Pincode" + }, + "Stcd": { + "type": "string", + "minLength": 1, + "maxLength": 2, + "description": "State Code" + } + }, + "required": ["Nm", "Addr1", "Loc", "Pin", "Stcd"] + }, + "ShipDtls": { + "type": "object", + "properties": { + "Gstin": { + "type": "string", + "maxLength": 15, + "minLength": 3, + "pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$", + "description": "Shipping Address GSTIN", + "validationMsg": "Shipping Address GSTIN is invalid" + }, + "LglNm": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Legal Name" + }, + "TrdNm": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Trade Name" + }, + "Addr1": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Address Line 1" + }, + "Addr2": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Address Line 2" + }, + "Loc": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Location" + }, + "Pin": { + "type": "number", + "minimum": 100000, + "maximum": 999999, + "description": "Pincode" + }, + "Stcd": { + "type": "string", + "minLength": 1, + "maxLength": 2, + "description": "State Code" + } + }, + "required": ["LglNm", "Addr1", "Loc", "Pin", "Stcd"] + }, + "ItemList": { + "type": "Array", + "properties": { + "SlNo": { + "type": "string", + "minLength": 1, + "maxLength": 6, + "description": "Serial No. of Item" + }, + "PrdDesc": { + "type": "string", + "minLength": 3, + "maxLength": 300, + "description": "Item Name" + }, + "IsServc": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "enum": ["Y", "N"], + "description": "Is Service Item" + }, + "HsnCd": { + "type": "string", + "minLength": 4, + "maxLength": 8, + "description": "HSN Code" + }, + "Barcde": { + "type": "string", + "minLength": 3, + "maxLength": 30, + "description": "Barcode" + }, + "Qty": { + "type": "number", + "minimum": 0, + "maximum": 9999999999.999, + "description": "Quantity" + }, + "FreeQty": { + "type": "number", + "minimum": 0, + "maximum": 9999999999.999, + "description": "Free Quantity" + }, + "Unit": { + "type": "string", + "minLength": 3, + "maxLength": 8, + "description": "UOM" + }, + "UnitPrice": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.999, + "description": "Rate" + }, + "TotAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Gross Amount" + }, + "Discount": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Discount" + }, + "PreTaxVal": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Pre tax value" + }, + "AssAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Taxable Value" + }, + "GstRt": { + "type": "number", + "minimum": 0, + "maximum": 999.999, + "description": "GST Rate" + }, + "IgstAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "IGST Amount" + }, + "CgstAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "CGST Amount" + }, + "SgstAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "SGST Amount" + }, + "CesRt": { + "type": "number", + "minimum": 0, + "maximum": 999.999, + "description": "Cess Rate" + }, + "CesAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Cess Amount (Advalorem)" + }, + "CesNonAdvlAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Cess Amount (Non-Advalorem)" + }, + "StateCesRt": { + "type": "number", + "minimum": 0, + "maximum": 999.999, + "description": "State CESS Rate" + }, + "StateCesAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "State CESS Amount" + }, + "StateCesNonAdvlAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "State CESS Amount (Non Advalorem)" + }, + "OthChrg": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Other Charges" + }, + "TotItemVal": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Total Item Value" + }, + "OrdLineRef": { + "type": "string", + "minLength": 1, + "maxLength": 50, + "description": "Order line reference" + }, + "OrgCntry": { + "type": "string", + "minLength": 2, + "maxLength": 2, + "description": "Origin Country" + }, + "PrdSlNo": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "description": "Serial number" + }, + "BchDtls": { + "type": "object", + "properties": { + "Nm": { + "type": "string", + "minLength": 3, + "maxLength": 20, + "description": "Batch number" + }, + "ExpDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Batch Expiry Date" + }, + "WrDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Warranty Date" + } + }, + "required": ["Nm"] + }, + "AttribDtls": { + "type": "Array", + "Attribute": { + "type": "object", + "properties": { + "Nm": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Attribute name of the item" + }, + "Val": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Attribute value of the item" + } + } + } + } + }, + "required": [ + "SlNo", + "IsServc", + "HsnCd", + "UnitPrice", + "TotAmt", + "AssAmt", + "GstRt", + "TotItemVal" + ] + }, + "ValDtls": { + "type": "object", + "properties": { + "AssVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Total Assessable value of all items" + }, + "CgstVal": { + "type": "number", + "maximum": 99999999999999.99, + "minimum": 0, + "description": "Total CGST value of all items" + }, + "SgstVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Total SGST value of all items" + }, + "IgstVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Total IGST value of all items" + }, + "CesVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Total CESS value of all items" + }, + "StCesVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Total State CESS value of all items" + }, + "Discount": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Invoice Discount" + }, + "OthChrg": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Other Charges" + }, + "RndOffAmt": { + "type": "number", + "minimum": -99.99, + "maximum": 99.99, + "description": "Rounded off Amount" + }, + "TotInvVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Final Invoice Value " + }, + "TotInvValFc": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Final Invoice value in Foreign Currency" + } + }, + "required": ["AssVal", "TotInvVal"] + }, + "PayDtls": { + "type": "object", + "properties": { + "Nm": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Payee Name" + }, + "AccDet": { + "type": "string", + "minLength": 1, + "maxLength": 18, + "description": "Bank Account Number of Payee" + }, + "Mode": { + "type": "string", + "minLength": 1, + "maxLength": 18, + "description": "Mode of Payment" + }, + "FinInsBr": { + "type": "string", + "minLength": 1, + "maxLength": 11, + "description": "Branch or IFSC code" + }, + "PayTerm": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Terms of Payment" + }, + "PayInstr": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Payment Instruction" + }, + "CrTrn": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Credit Transfer" + }, + "DirDr": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Direct Debit" + }, + "CrDay": { + "type": "number", + "minimum": 0, + "maximum": 9999, + "description": "Credit Days" + }, + "PaidAmt": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Advance Amount" + }, + "PaymtDue": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Outstanding Amount" + } + } + }, + "RefDtls": { + "type": "object", + "properties": { + "InvRm": { + "type": "string", + "maxLength": 100, + "minLength": 3, + "pattern": "^[0-9A-Za-z/-]{3,100}$", + "description": "Remarks/Note" + }, + "DocPerdDtls": { + "type": "object", + "properties": { + "InvStDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Invoice Period Start Date" + }, + "InvEndDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Invoice Period End Date" + } + }, + "required": ["InvStDt ", "InvEndDt "] + }, + "PrecDocDtls": { + "type": "object", + "properties": { + "InvNo": { + "type": "string", + "minLength": 1, + "maxLength": 16, + "pattern": "^[1-9A-Z]{1}[0-9A-Z/-]{1,15}$", + "description": "Reference of Original Invoice" + }, + "InvDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Date of Orginal Invoice" + }, + "OthRefNo": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "description": "Other Reference" + } + } + }, + "required": ["InvNo", "InvDt"], + "ContrDtls": { + "type": "object", + "properties": { + "RecAdvRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$", + "description": "Receipt Advice No." + }, + "RecAdvDt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Date of receipt advice" + }, + "TendRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$", + "description": "Lot/Batch Reference No." + }, + "ContrRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$", + "description": "Contract Reference Number" + }, + "ExtRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$", + "description": "Any other reference" + }, + "ProjRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$", + "description": "Project Reference Number" + }, + "PORefr": { + "type": "string", + "minLength": 1, + "maxLength": 16, + "pattern": "^([0-9A-Za-z/-]){1,16}$", + "description": "PO Reference Number" + }, + "PORefDt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "PO Reference date" + } + } + } + } + }, + "AddlDocDtls": { + "type": "Array", + "properties": { + "Url": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Supporting document URL" + }, + "Docs": { + "type": "string", + "minLength": 3, + "maxLength": 1000, + "description": "Supporting document in Base64 Format" + }, + "Info": { + "type": "string", + "minLength": 3, + "maxLength": 1000, + "description": "Any additional information" + } + } + }, + + "ExpDtls": { + "type": "object", + "properties": { + "ShipBNo": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "description": "Shipping Bill No." + }, + "ShipBDt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Shipping Bill Date" + }, + "Port": { + "type": "string", + "minLength": 2, + "maxLength": 10, + "pattern": "^[0-9A-Za-z]{2,10}$", + "description": "Port Code. Refer the master" + }, + "RefClm": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "description": "Claiming Refund. Y/N" + }, + "ForCur": { + "type": "string", + "minLength": 3, + "maxLength": 16, + "description": "Additional Currency Code. Refer the master" + }, + "CntCode": { + "type": "string", + "minLength": 2, + "maxLength": 2, + "description": "Country Code. Refer the master" + }, + "ExpDuty": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Export Duty" + } + } + }, + "EwbDtls": { + "type": "object", + "properties": { + "TransId": { + "type": "string", + "minLength": 15, + "maxLength": 15, + "description": "Transporter GSTIN" + }, + "TransName": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Transporter Name" + }, + "TransMode": { + "type": "string", + "maxLength": 1, + "minLength": 1, + "enum": ["1", "2", "3", "4"], + "description": "Mode of Transport" + }, + "Distance": { + "type": "number", + "minimum": 1, + "maximum": 9999, + "description": "Distance" + }, + "TransDocNo": { + "type": "string", + "minLength": 1, + "maxLength": 15, + "pattern": "^([0-9A-Z/-]){1,15}$", + "description": "Tranport Document Number" + }, + "TransDocDt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Transport Document Date" + }, + "VehNo": { + "type": "string", + "minLength": 4, + "maxLength": 20, + "description": "Vehicle Number" + }, + "VehType": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "enum": ["O", "R"], + "description": "Vehicle Type" + } + }, + "required": ["Distance"] + }, + "required": [ + "Version", + "TranDtls", + "DocDtls", + "SellerDtls", + "BuyerDtls", + "ItemList", + "ValDtls" + ] +} diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js new file mode 100644 index 00000000000..9c86cc89f55 --- /dev/null +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -0,0 +1,305 @@ +erpnext.setup_einvoice_actions = (doctype) => { + frappe.ui.form.on(doctype, { + refresh(frm) { + const einvoicing_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable"); + const supply_type = frm.doc.gst_category; + const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type); + const company_transaction = frm.doc.billing_address_gstin == frm.doc.company_gstin; + + if (!einvoicing_enabled || !valid_supply_type || company_transaction) return; + + const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc; + + const add_custom_button = (label, action) => { + if (!frm.custom_buttons[label]) { + frm.add_custom_button(label, action, __('E Invoicing')); + } + }; + + if (!irn && !__unsaved) { + const action = () => { + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.get_einvoice', + args: { doctype, docname: name }, + freeze: true, + callback: (res) => { + const einvoice = res.message; + show_einvoice_preview(frm, einvoice); + } + }); + }; + + add_custom_button(__("Generate IRN"), action); + } + + if (irn && !irn_cancelled && !ewaybill) { + const fields = [ + { + "label": "Reason", + "fieldname": "reason", + "fieldtype": "Select", + "reqd": 1, + "default": "1-Duplicate", + "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] + }, + { + "label": "Remark", + "fieldname": "remark", + "fieldtype": "Data", + "reqd": 1 + } + ]; + const action = () => { + const d = new frappe.ui.Dialog({ + title: __("Cancel IRN"), + fields: fields, + primary_action: function() { + const data = d.get_values(); + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.cancel_irn', + args: { + doctype, + docname: name, + irn: irn, + reason: data.reason.split('-')[0], + remark: data.remark + }, + freeze: true, + callback: () => frm.reload_doc() || d.hide(), + error: () => d.hide() + }); + }, + primary_action_label: __('Submit') + }); + d.show(); + }; + add_custom_button(__("Cancel IRN"), action); + } + + if (irn && !irn_cancelled && !ewaybill) { + const action = () => { + const d = new frappe.ui.Dialog({ + title: __('Generate E-Way Bill'), + wide: 1, + fields: get_ewaybill_fields(frm), + primary_action: function() { + const data = d.get_values(); + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.generate_eway_bill', + args: { + doctype, + docname: name, + irn, + ...data + }, + freeze: true, + callback: () => frm.reload_doc() || d.hide(), + error: () => d.hide() + }); + }, + primary_action_label: __('Submit') + }); + d.show(); + }; + + add_custom_button(__("Generate E-Way Bill"), action); + } + + if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) { + const fields = [ + { + "label": "Reason", + "fieldname": "reason", + "fieldtype": "Select", + "reqd": 1, + "default": "1-Duplicate", + "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] + }, + { + "label": "Remark", + "fieldname": "remark", + "fieldtype": "Data", + "reqd": 1 + } + ]; + const action = () => { + const d = new frappe.ui.Dialog({ + title: __('Cancel E-Way Bill'), + fields: fields, + primary_action: function() { + const data = d.get_values(); + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', + args: { + doctype, + docname: name, + eway_bill: ewaybill, + reason: data.reason.split('-')[0], + remark: data.remark + }, + freeze: true, + callback: () => frm.reload_doc() || d.hide(), + error: () => d.hide() + }); + }, + primary_action_label: __('Submit') + }); + d.show(); + }; + add_custom_button(__("Cancel E-Way Bill"), action); + } + } + }); +}; + +const get_ewaybill_fields = (frm) => { + return [ + { + 'fieldname': 'transporter', + 'label': 'Transporter', + 'fieldtype': 'Link', + 'options': 'Supplier', + 'default': frm.doc.transporter + }, + { + 'fieldname': 'gst_transporter_id', + 'label': 'GST Transporter ID', + 'fieldtype': 'Data', + 'fetch_from': 'transporter.gst_transporter_id', + 'default': frm.doc.gst_transporter_id + }, + { + 'fieldname': 'driver', + 'label': 'Driver', + 'fieldtype': 'Link', + 'options': 'Driver', + 'default': frm.doc.driver + }, + { + 'fieldname': 'lr_no', + 'label': 'Transport Receipt No', + 'fieldtype': 'Data', + 'default': frm.doc.lr_no + }, + { + 'fieldname': 'vehicle_no', + 'label': 'Vehicle No', + 'fieldtype': 'Data', + 'depends_on': 'eval:(doc.mode_of_transport === "Road")', + 'default': frm.doc.vehicle_no + }, + { + 'fieldname': 'distance', + 'label': 'Distance (in km)', + 'fieldtype': 'Float', + 'default': frm.doc.distance + }, + { + 'fieldname': 'transporter_col_break', + 'fieldtype': 'Column Break', + }, + { + 'fieldname': 'transporter_name', + 'label': 'Transporter Name', + 'fieldtype': 'Data', + 'fetch_from': 'transporter.name', + 'read_only': 1, + 'default': frm.doc.transporter_name + }, + { + 'fieldname': 'mode_of_transport', + 'label': 'Mode of Transport', + 'fieldtype': 'Select', + 'options': `\nRoad\nAir\nRail\nShip`, + 'default': frm.doc.mode_of_transport + }, + { + 'fieldname': 'driver_name', + 'label': 'Driver Name', + 'fieldtype': 'Data', + 'fetch_from': 'driver.full_name', + 'read_only': 1, + 'default': frm.doc.driver_name + }, + { + 'fieldname': 'lr_date', + 'label': 'Transport Receipt Date', + 'fieldtype': 'Date', + 'default': frm.doc.lr_date + }, + { + 'fieldname': 'gst_vehicle_type', + 'label': 'GST Vehicle Type', + 'fieldtype': 'Select', + 'options': `Regular\nOver Dimensional Cargo (ODC)`, + 'depends_on': 'eval:(doc.mode_of_transport === "Road")', + 'default': frm.doc.gst_vehicle_type + } + ]; +}; + +const request_irn_generation = (frm) => { + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.generate_irn', + args: { doctype: frm.doc.doctype, docname: frm.doc.name }, + freeze: true, + callback: () => frm.reload_doc() + }); +}; + +const get_preview_dialog = (frm, action) => { + const dialog = new frappe.ui.Dialog({ + title: __("Preview"), + wide: 1, + fields: [ + { + "label": "Preview", + "fieldname": "preview_html", + "fieldtype": "HTML" + } + ], + primary_action: () => action(frm) || dialog.hide(), + primary_action_label: __('Generate IRN') + }); + return dialog; +}; + +const show_einvoice_preview = (frm, einvoice) => { + const preview_dialog = get_preview_dialog(frm, request_irn_generation); + + // initialize e-invoice fields + einvoice["Irn"] = einvoice["AckNo"] = ''; einvoice["AckDt"] = frappe.datetime.nowdate(); + frm.doc.signed_einvoice = JSON.stringify(einvoice); + + // initialize preview wrapper + const $preview_wrapper = preview_dialog.get_field("preview_html").$wrapper; + $preview_wrapper.html( + `
    + +
    +
    ` + ); + + frappe.call({ + method: "frappe.www.printview.get_html_and_style", + args: { + doc: frm.doc, + print_format: "GST E-Invoice", + no_letterhead: 1 + }, + callback: function (r) { + if (!r.exc) { + $preview_wrapper.find(".print-format").html(r.message.html); + const style = ` + .print-format { box-shadow: 0px 0px 5px rgba(0,0,0,0.2); padding: 0.30in; min-height: 80vh; } + .print-preview { min-height: 0px; } + .modal-dialog { width: 720px; }`; + + frappe.dom.set_style(style, "custom-print-style"); + preview_dialog.show(); + } + } + }); +}; \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py new file mode 100644 index 00000000000..cb92c42464e --- /dev/null +++ b/erpnext/regional/india/e_invoice/utils.py @@ -0,0 +1,772 @@ +# -*- 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 os +import re +import jwt +import sys +import json +import base64 +import frappe +import traceback +from frappe import _, bold +from pyqrcode import create as qrcreate +from frappe.integrations.utils import make_post_request, make_get_request +from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply +from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date + +def validate_einvoice_fields(doc): + einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable')) + invalid_doctype = doc.doctype not in ['Sales Invoice'] + invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] + company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin') + + if not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction: return + + if doc.docstatus == 0 and doc._action == 'save': + if doc.irn: + frappe.throw(_('You cannot edit the invoice after generating IRN'), title=_('Edit Not Allowed')) + if len(doc.name) > 16: + raise_document_name_too_long_error() + + elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn: + frappe.throw(_('You must generate IRN before submitting the document.'), title=_('Missing IRN')) + + elif doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled: + frappe.throw(_('You must cancel IRN before cancelling the document.'), title=_('Cancel Not Allowed')) + +def raise_document_name_too_long_error(): + title = _('Document ID Too Long') + msg = _('As you have E-Invoicing enabled, to be able to generate IRN for this invoice, ') + msg += _('document id {} exceed 16 letters. ').format(bold(_('should not'))) + msg += '

    ' + msg += _('You must {} your {} in order to have document id of {} length 16. ').format( + bold(_('modify')), bold(_('naming series')), bold(_('maximum')) + ) + msg += _('Please account for ammended documents too. ') + frappe.throw(msg, title=title) + +def read_json(name): + file_path = os.path.join(os.path.dirname(__file__), '{name}.json'.format(name=name)) + with open(file_path, 'r') as f: + return cstr(f.read()) + +def get_transaction_details(invoice): + supply_type = '' + if invoice.gst_category == 'Registered Regular': supply_type = 'B2B' + elif invoice.gst_category == 'SEZ': supply_type = 'SEZWOP' + elif invoice.gst_category == 'Overseas': supply_type = 'EXPWOP' + elif invoice.gst_category == 'Deemed Export': supply_type = 'DEXP' + + if not supply_type: + rr, sez, overseas, export = bold('Registered Regular'), bold('SEZ'), bold('Overseas'), bold('Deemed Export') + frappe.throw(_('GST category should be one of {}, {}, {}, {}').format(rr, sez, overseas, export), + title=_('Invalid Supply Type')) + + return frappe._dict(dict( + tax_scheme='GST', + supply_type=supply_type, + reverse_charge=invoice.reverse_charge + )) + +def get_doc_details(invoice): + invoice_type = 'CRN' if invoice.is_return else 'INV' + + invoice_name = invoice.name + invoice_date = format_date(invoice.posting_date, 'dd/mm/yyyy') + + return frappe._dict(dict( + invoice_type=invoice_type, + invoice_name=invoice_name, + invoice_date=invoice_date + )) + +def get_party_details(address_name): + address = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0] + gstin = address.get('gstin') + + gstin_details = get_gstin_details(gstin) + legal_name = gstin_details.get('LegalName') + location = gstin_details.get('AddrLoc') or address.get('city') + state_code = gstin_details.get('StateCode') + pincode = gstin_details.get('AddrPncd') + address_line1 = '{} {}'.format(gstin_details.get('AddrBno'), gstin_details.get('AddrFlno')) + address_line2 = '{} {}'.format(gstin_details.get('AddrBnm'), gstin_details.get('AddrSt')) + email_id = address.get('email_id') + phone = address.get('phone') + # get last 10 digit + phone = phone.replace(" ", "")[-10:] if phone else '' + + if state_code == 97: + # according to einvoice standard + pincode = 999999 + + return frappe._dict(dict( + gstin=gstin, legal_name=legal_name, location=location, + pincode=pincode, state_code=state_code, address_line1=address_line1, + address_line2=address_line2, email=email_id, phone=phone + )) + +def get_gstin_details(gstin): + if not hasattr(frappe.local, 'gstin_cache'): + frappe.local.gstin_cache = {} + + key = gstin + details = frappe.local.gstin_cache.get(key) + if details: + return details + + details = frappe.cache().hget('gstin_cache', key) + if details: + frappe.local.gstin_cache[key] = details + return details + + if not details: + return GSPConnector.get_gstin_details(gstin) + +def get_overseas_address_details(address_name): + address_title, address_line1, address_line2, city, phone, email_id = frappe.db.get_value( + 'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city', 'phone', 'email_id'] + ) + + return frappe._dict(dict( + gstin='URP', legal_name=address_title, address_line1=address_line1, + address_line2=address_line2, email=email_id, phone=phone, + pincode=999999, state_code=96, place_of_supply=96, location=city + )) + +def get_item_list(invoice): + item_list = [] + + for d in invoice.items: + einvoice_item_schema = read_json('einv_item_template') + item = frappe._dict({}) + item.update(d.as_dict()) + + item.sr_no = d.idx + item.qty = abs(item.qty) + item.description = d.item_name + item.taxable_value = abs(item.base_net_amount) + item.discount_amount = abs(item.discount_amount * item.qty) + item.unit_rate = abs(item.base_price_list_rate) if item.discount_amount else abs(item.base_net_rate) + item.gross_amount = abs(item.unit_rate * item.qty) + + 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 + item.is_service_item = 'N' if frappe.db.get_value('Item', d.item_code, 'is_stock_item') else 'Y' + + item = update_item_taxes(invoice, item) + + item.total_value = abs( + item.taxable_value + item.igst_amount + item.sgst_amount + + item.cgst_amount + item.cess_amount + item.cess_nadv_amount + item.other_charges + ) + einv_item = einvoice_item_schema.format(item=item) + item_list.append(einv_item) + + return ', '.join(item_list) + +def update_item_taxes(invoice, item): + gst_accounts = get_gst_accounts(invoice.company) + gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] + + for attr in [ + 'tax_rate', 'cess_rate', 'cess_nadv_amount', + 'cgst_amount', 'sgst_amount', 'igst_amount', + 'cess_amount', 'cess_nadv_amount', 'other_charges' + ]: + item[attr] = 0 + + for t in invoice.taxes: + item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code) + if t.account_head in gst_accounts_list: + if t.account_head in gst_accounts.cess_account: + if t.charge_type == 'On Item Quantity': + item.cess_nadv_amount += abs(item_tax_detail[1]) + else: + item.cess_rate += item_tax_detail[0] + item.cess_amount += abs(item_tax_detail[1]) + elif t.account_head in gst_accounts.igst_account: + item.tax_rate += item_tax_detail[0] + item.igst_amount += abs(item_tax_detail[1]) + elif t.account_head in gst_accounts.sgst_account: + item.tax_rate += item_tax_detail[0] + item.sgst_amount += abs(item_tax_detail[1]) + elif t.account_head in gst_accounts.cgst_account: + item.tax_rate += item_tax_detail[0] + item.cgst_amount += abs(item_tax_detail[1]) + + return item + +def get_invoice_value_details(invoice): + invoice_value_details = frappe._dict(dict()) + invoice_value_details.base_net_total = abs(invoice.base_net_total) + invoice_value_details.invoice_discount_amt = invoice.discount_amount if invoice.discount_amount and invoice.discount_amount > 0 else 0 + # discount amount cannnot be -ve in an e-invoice, so if -ve include discount in round_off + invoice_value_details.round_off = invoice.rounding_adjustment - (invoice.discount_amount if invoice.discount_amount and invoice.discount_amount < 0 else 0) + disable_rounded = frappe.db.get_single_value('Global Defaults', 'disable_rounded_total') + invoice_value_details.base_grand_total = abs(invoice.base_grand_total) if disable_rounded else abs(invoice.base_rounded_total) + invoice_value_details.grand_total = abs(invoice.grand_total) if disable_rounded else abs(invoice.rounded_total) + + invoice_value_details = update_invoice_taxes(invoice, invoice_value_details) + + return invoice_value_details + +def update_invoice_taxes(invoice, invoice_value_details): + gst_accounts = get_gst_accounts(invoice.company) + gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] + + invoice_value_details.total_cgst_amt = 0 + invoice_value_details.total_sgst_amt = 0 + invoice_value_details.total_igst_amt = 0 + invoice_value_details.total_cess_amt = 0 + invoice_value_details.total_other_charges = 0 + for t in invoice.taxes: + if t.account_head in gst_accounts_list: + if t.account_head in gst_accounts.cess_account: + invoice_value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount) + elif t.account_head in gst_accounts.igst_account: + invoice_value_details.total_igst_amt += abs(t.base_tax_amount_after_discount_amount) + elif t.account_head in gst_accounts.sgst_account: + invoice_value_details.total_sgst_amt += abs(t.base_tax_amount_after_discount_amount) + elif t.account_head in gst_accounts.cgst_account: + invoice_value_details.total_cgst_amt += abs(t.base_tax_amount_after_discount_amount) + else: + invoice_value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount) + + return invoice_value_details + +def get_payment_details(invoice): + payee_name = invoice.company + mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments]) + paid_amount = invoice.base_paid_amount + outstanding_amount = invoice.outstanding_amount + + return frappe._dict(dict( + payee_name=payee_name, mode_of_payment=mode_of_payment, + paid_amount=paid_amount, outstanding_amount=outstanding_amount + )) + +def get_return_doc_reference(invoice): + invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date') + return frappe._dict(dict( + invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy') + )) + +def get_eway_bill_details(invoice): + if invoice.is_return: + frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes'), title=_('E Invoice Validation Failed')) + + mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' } + vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' } + + return frappe._dict(dict( + gstin=invoice.gst_transporter_id, + name=invoice.transporter_name, + mode_of_transport=mode_of_transport[invoice.mode_of_transport], + distance=invoice.distance or 0, + document_name=invoice.lr_no, + document_date=format_date(invoice.lr_date, 'dd/mm/yyyy'), + vehicle_no=invoice.vehicle_no, + vehicle_type=vehicle_type[invoice.gst_vehicle_type] + )) + +def make_einvoice(invoice): + schema = read_json('einv_template') + + transaction_details = get_transaction_details(invoice) + item_list = get_item_list(invoice) + doc_details = get_doc_details(invoice) + invoice_value_details = get_invoice_value_details(invoice) + seller_details = get_party_details(invoice.company_address) + + if invoice.gst_category == 'Overseas': + buyer_details = get_overseas_address_details(invoice.customer_address) + else: + buyer_details = get_party_details(invoice.customer_address) + place_of_supply = get_place_of_supply(invoice, invoice.doctype) or invoice.billing_address_gstin + place_of_supply = place_of_supply[:2] + buyer_details.update(dict(place_of_supply=place_of_supply)) + + shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({}) + if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name: + shipping_details = get_party_details(invoice.shipping_address_name) + + if invoice.is_pos and invoice.base_paid_amount: + payment_details = get_payment_details(invoice) + + if invoice.is_return and invoice.return_against: + prev_doc_details = get_return_doc_reference(invoice) + + if invoice.transporter: + eway_bill_details = get_eway_bill_details(invoice) + + # not yet implemented + dispatch_details = period_details = export_details = frappe._dict({}) + + einvoice = schema.format( + transaction_details=transaction_details, doc_details=doc_details, dispatch_details=dispatch_details, + seller_details=seller_details, buyer_details=buyer_details, shipping_details=shipping_details, + item_list=item_list, invoice_value_details=invoice_value_details, payment_details=payment_details, + period_details=period_details, prev_doc_details=prev_doc_details, + export_details=export_details, eway_bill_details=eway_bill_details + ) + einvoice = json.loads(einvoice) + + validations = json.loads(read_json('einv_validation')) + errors = validate_einvoice(validations, einvoice) + if errors: + message = "\n".join([ + "E Invoice: ", json.dumps(einvoice, indent=4), + "-" * 50, + "Errors: ", json.dumps(errors, indent=4) + ]) + frappe.log_error(title="E Invoice Validation Failed", message=message) + frappe.throw(errors, title=_('E Invoice Validation Failed'), as_list=1) + + return einvoice + +def validate_einvoice(validations, einvoice, errors=[]): + for fieldname, field_validation in validations.items(): + value = einvoice.get(fieldname, None) + if not value or value == "None": + # remove keys with empty values + einvoice.pop(fieldname, None) + continue + + value_type = field_validation.get("type").lower() + if value_type in ['object', 'array']: + child_validations = field_validation.get('properties') + + if isinstance(value, list): + for d in value: + validate_einvoice(child_validations, d, errors) + if not d: + # remove empty dicts + einvoice.pop(fieldname, None) + else: + validate_einvoice(child_validations, value, errors) + if not value: + # remove empty dicts + einvoice.pop(fieldname, None) + continue + + # convert to int or str + if value_type == 'string': + einvoice[fieldname] = str(value) + elif value_type == 'number': + is_integer = '.' not in str(field_validation.get('maximum')) + einvoice[fieldname] = flt(value, 2) if not is_integer else cint(value) + value = einvoice[fieldname] + + max_length = field_validation.get('maxLength') + minimum = flt(field_validation.get('minimum')) + maximum = flt(field_validation.get('maximum')) + pattern_str = field_validation.get('pattern') + pattern = re.compile(pattern_str or '') + + label = field_validation.get('description') or fieldname + + if value_type == 'string' and len(value) > max_length: + errors.append(_('{} should not exceed {} characters').format(label, max_length)) + if value_type == 'number' and (value > maximum or value < minimum): + errors.append(_('{} {} should be between {} and {}').format(label, value, minimum, maximum)) + if pattern_str and not pattern.match(value): + errors.append(field_validation.get('validationMsg')) + + return errors + +class RequestFailed(Exception): pass + +class GSPConnector(): + def __init__(self, doctype=None, docname=None): + self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings') + self.invoice = frappe.get_cached_doc(doctype, docname) if doctype and docname else None + self.credentials = self.get_credentials() + + self.base_url = 'https://gsp.adaequare.com/' + self.authenticate_url = self.base_url + 'gsp/authenticate?grant_type=token' + self.gstin_details_url = self.base_url + 'test/enriched/ei/api/master/gstin' + self.generate_irn_url = self.base_url + 'test/enriched/ei/api/invoice' + self.irn_details_url = self.base_url + 'test/enriched/ei/api/invoice/irn' + self.cancel_irn_url = self.base_url + 'test/enriched/ei/api/invoice/cancel' + self.cancel_ewaybill_url = self.base_url + '/test/enriched/ei/api/ewayapi' + self.generate_ewaybill_url = self.base_url + 'test/enriched/ei/api/ewaybill' + + def get_credentials(self): + if self.invoice: + gstin = self.get_seller_gstin() + credentials = next(d for d in self.e_invoice_settings.credentials if d.gstin == gstin) + else: + credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None + return credentials + + def get_seller_gstin(self): + gstin = self.invoice.company_gstin or frappe.db.get_value('Address', self.invoice.company_address, 'gstin') + if not gstin: + frappe.throw(_('Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.')) + return gstin + + def get_auth_token(self): + if time_diff_in_seconds(self.e_invoice_settings.token_expiry, now_datetime()) < 150.0: + self.fetch_auth_token() + + return self.e_invoice_settings.auth_token + + def make_request(self, request_type, url, headers=None, data=None): + if request_type == 'post': + res = make_post_request(url, headers=headers, data=data) + else: + res = make_get_request(url, headers=headers, data=data) + + self.log_request(url, headers, data, res) + return res + + def log_request(self, url, headers, data, res): + headers.update({ 'password': self.credentials.password }) + request_log = frappe.get_doc({ + "doctype": "E Invoice Request Log", + "user": frappe.session.user, + "reference_invoice": self.invoice.name if self.invoice else None, + "url": url, + "headers": json.dumps(headers, indent=4) if headers else None, + "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) + frappe.db.commit() + + def fetch_auth_token(self): + headers = { + 'gspappid': frappe.conf.einvoice_client_id, + 'gspappsecret': frappe.conf.einvoice_client_secret + } + res = {} + try: + 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() + + except Exception: + self.log_error(res) + self.raise_error(True) + + def get_headers(self): + return { + 'content-type': 'application/json', + 'user_name': self.credentials.username, + 'password': self.credentials.get_password(), + 'gstin': self.credentials.gstin, + 'authorization': self.get_auth_token(), + 'requestid': str(base64.b64encode(os.urandom(18))), + } + + def fetch_gstin_details(self, gstin): + headers = self.get_headers() + + try: + params = '?gstin={gstin}'.format(gstin=gstin) + res = self.make_request('get', self.gstin_details_url + params, headers) + if res.get('success'): + return res.get('result') + else: + self.log_error(res) + raise RequestFailed + + except RequestFailed: + self.raise_error() + + except Exception: + self.log_error() + self.raise_error(True) + + @staticmethod + def get_gstin_details(gstin): + '''fetch and cache GSTIN details''' + if not hasattr(frappe.local, 'gstin_cache'): + frappe.local.gstin_cache = {} + + key = gstin + gsp_connector = GSPConnector() + details = gsp_connector.fetch_gstin_details(gstin) + + frappe.local.gstin_cache[key] = details + frappe.cache().hset('gstin_cache', key, details) + return details + + def generate_irn(self): + headers = self.get_headers() + einvoice = make_einvoice(self.invoice) + data = json.dumps(einvoice, indent=4) + + try: + res = self.make_request('post', self.generate_irn_url, headers, data) + if res.get('success'): + self.set_einvoice_data(res.get('result')) + + elif '2150' in res.get('message'): + # IRN already generated but not updated in invoice + # Extract the IRN from the response description and fetch irn details + irn = res.get('result')[0].get('Desc').get('Irn') + irn_details = self.get_irn_details(irn) + if irn_details: + self.set_einvoice_data(irn_details) + else: + raise RequestFailed('IRN has already been generated for the invoice but cannot fetch details for the it. \ + Contact ERPNext support to resolve the issue.') + + else: + raise RequestFailed + + except RequestFailed: + errors = self.sanitize_error_message(res.get('message')) + self.raise_error(errors=errors) + + except Exception: + self.log_error(data) + self.raise_error(True) + + def get_irn_details(self, irn): + headers = self.get_headers() + + try: + params = '?irn={irn}'.format(irn=irn) + res = self.make_request('get', self.irn_details_url + params, headers) + if res.get('success'): + return res.get('result') + else: + raise RequestFailed + + except RequestFailed: + errors = self.sanitize_error_message(res.get('message')) + self.raise_error(errors=errors) + + except Exception: + self.log_error() + self.raise_error(True) + + def cancel_irn(self, irn, reason, remark): + headers = self.get_headers() + data = json.dumps({ + 'Irn': irn, + 'Cnlrsn': reason, + 'Cnlrem': remark + }, indent=4) + + try: + res = self.make_request('post', self.cancel_irn_url, headers, data) + if res.get('success'): + self.invoice.irn_cancelled = 1 + self.invoice.flags.updater_reference = { + 'doctype': self.invoice.doctype, + 'docname': self.invoice.name, + 'label': _('IRN Cancelled - {}').format(remark) + } + self.update_invoice() + + else: + raise RequestFailed + + except RequestFailed: + errors = self.sanitize_error_message(res.get('message')) + self.raise_error(errors=errors) + + except Exception: + self.log_error(data) + self.raise_error(True) + + def generate_eway_bill(self, **kwargs): + args = frappe._dict(kwargs) + + headers = self.get_headers() + eway_bill_details = get_eway_bill_details(args) + data = json.dumps({ + 'Irn': args.irn, + 'Distance': cint(eway_bill_details.distance), + 'TransMode': eway_bill_details.mode_of_transport, + 'TransId': eway_bill_details.gstin, + 'TransName': eway_bill_details.transporter, + 'TrnDocDt': eway_bill_details.document_date, + 'TrnDocNo': eway_bill_details.document_name, + 'VehNo': eway_bill_details.vehicle_no, + 'VehType': eway_bill_details.vehicle_type + }, indent=4) + + try: + res = self.make_request('post', self.generate_ewaybill_url, headers, data) + if res.get('success'): + self.invoice.ewaybill = res.get('result').get('EwbNo') + self.invoice.eway_bill_cancelled = 0 + self.invoice.update(args) + self.invoice.flags.updater_reference = { + 'doctype': self.invoice.doctype, + 'docname': self.invoice.name, + 'label': _('E-Way Bill Generated') + } + self.update_invoice() + + else: + raise RequestFailed + + except RequestFailed: + errors = self.sanitize_error_message(res.get('message')) + self.raise_error(errors=errors) + + except Exception: + self.log_error(data) + self.raise_error(True) + + def cancel_eway_bill(self, eway_bill, reason, remark): + headers = self.get_headers() + data = json.dumps({ + 'ewbNo': eway_bill, + 'cancelRsnCode': reason, + 'cancelRmrk': remark + }, indent=4) + + try: + res = self.make_request('post', self.cancel_ewaybill_url, headers, data) + if res.get('success'): + self.invoice.ewaybill = '' + self.invoice.eway_bill_cancelled = 1 + self.invoice.flags.updater_reference = { + 'doctype': self.invoice.doctype, + 'docname': self.invoice.name, + 'label': _('E-Way Bill Cancelled - {}').format(remark) + } + self.update_invoice() + + else: + raise RequestFailed + + except RequestFailed: + errors = self.sanitize_error_message(res.get('message')) + self.raise_error(errors=errors) + + except Exception: + self.log_error(data) + self.raise_error(True) + + def sanitize_error_message(self, message): + ''' + On validation errors, response message looks something like this: + message = '2174 : For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, + 3095 : Supplier GSTIN is inactive' + we search for string between ':' to extract the error messages + errors = [ + ': For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, 3095 ', + ': Test' + ] + then we trim down the message by looping over errors + ''' + errors = re.findall(': [^:]+', message) + for idx, e in enumerate(errors): + # remove colons + errors[idx] = errors[idx].replace(':', '').strip() + # if not last + if idx != len(errors) - 1: + # remove last 7 chars eg: ', 3095 ' + errors[idx] = errors[idx][:-6] + + return errors + + def log_error(self, data={}): + if not isinstance(data, dict): + data = json.loads(data) + + seperator = "--" * 50 + err_tb = traceback.format_exc() + err_msg = str(sys.exc_info()[1]) + data = json.dumps(data, indent=4) + + message = "\n".join([ + "Error", err_msg, seperator, + "Data:", data, seperator, + "Exception:", err_tb + ]) + frappe.log_error(title=_('E Invoice Request Failed'), message=message) + + def raise_error(self, raise_exception=False, errors=[]): + title = _('E Invoice Request Failed') + if errors: + frappe.throw(errors, title=title, as_list=1) + else: + link_to_error_list = 'Error Log' + frappe.msgprint( + _('An error occurred while making e-invoicing request. Please check {} for more information.').format(link_to_error_list), + title=title, + raise_exception=raise_exception, + indicator='red' + ) + + def set_einvoice_data(self, res): + enc_signed_invoice = res.get('SignedInvoice') + dec_signed_invoice = jwt.decode(enc_signed_invoice, verify=False)['data'] + + self.invoice.irn = res.get('Irn') + self.invoice.ewaybill = res.get('EwbNo') + self.invoice.signed_einvoice = dec_signed_invoice + self.invoice.signed_qr_code = res.get('SignedQRCode') + + self.attach_qrcode_image() + + self.invoice.flags.updater_reference = { + 'doctype': self.invoice.doctype, + 'docname': self.invoice.name, + '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 + + _file = frappe.new_doc('File') + _file.update({ + 'file_name': f'QRCode_{docname}.png', + 'attached_to_doctype': doctype, + 'attached_to_name': docname, + 'content': 'qrcode', + 'is_private': 1 + }) + _file.insert() + frappe.db.commit() + 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) + + self.invoice.qrcode_image = _file.file_url + + def update_invoice(self): + self.invoice.flags.ignore_validate_update_after_submit = True + self.invoice.flags.ignore_validate = True + self.invoice.save() + +@frappe.whitelist() +def get_einvoice(doctype, docname): + invoice = frappe.get_doc(doctype, docname) + return make_einvoice(invoice) + +@frappe.whitelist() +def generate_irn(doctype, docname): + gsp_connector = GSPConnector(doctype, docname) + gsp_connector.generate_irn() + +@frappe.whitelist() +def cancel_irn(doctype, docname, irn, reason, remark): + gsp_connector = GSPConnector(doctype, docname) + gsp_connector.cancel_irn(irn, reason, remark) + +@frappe.whitelist() +def generate_eway_bill(doctype, docname, **kwargs): + gsp_connector = GSPConnector(doctype, docname) + gsp_connector.generate_eway_bill(**kwargs) + +@frappe.whitelist() +def cancel_eway_bill(doctype, docname, eway_bill, reason, remark): + gsp_connector = GSPConnector(doctype, docname) + gsp_connector.cancel_eway_bill(eway_bill, reason, remark) \ No newline at end of file diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index cbcd6e3203a..5321a9a3b5a 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -87,7 +87,7 @@ def add_custom_roles_for_reports(): )).insert() def add_permissions(): - for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate'): + for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate', 'E Invoice Settings'): add_permission(doctype, 'All', 0) for role in ('Accounts Manager', 'Accounts User', 'System Manager'): add_permission(doctype, role, 0) @@ -103,9 +103,10 @@ def add_permissions(): def add_print_formats(): frappe.reload_doc("regional", "print_format", "gst_tax_invoice") frappe.reload_doc("accounts", "print_format", "gst_pos_invoice") + frappe.reload_doc("accounts", "print_format", "GST E-Invoice") frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where - name in('GST POS Invoice', 'GST Tax Invoice') """) + name in('GST POS Invoice', 'GST Tax Invoice', 'GST E-Invoice') """) def make_custom_fields(update=True): hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC', @@ -351,7 +352,6 @@ def make_custom_fields(update=True): 'label': 'Mode of Transport', 'fieldtype': 'Select', 'options': '\nRoad\nAir\nRail\nShip', - 'default': 'Road', 'insert_after': 'transporter_name', 'print_hide': 1, 'translatable': 0 @@ -388,13 +388,34 @@ def make_custom_fields(update=True): 'fieldname': 'ewaybill', 'label': 'E-Way Bill No.', 'fieldtype': 'Data', - 'depends_on': 'eval:(doc.docstatus === 1)', + 'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)', 'allow_on_submit': 1, 'insert_after': 'tax_id', 'translatable': 0 } ] + si_einvoice_fields = [ + dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1, + depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), + + dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1), + + dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), + + dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1, + depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), + + dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1, + depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), + + dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1), + + dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1), + + dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1) + ] + custom_fields = { 'Address': [ dict(fieldname='gstin', label='Party GSTIN', fieldtype='Data', @@ -407,7 +428,7 @@ def make_custom_fields(update=True): 'Purchase Invoice': purchase_invoice_gst_category + invoice_gst_fields + purchase_invoice_itc_fields + purchase_invoice_gst_fields, 'Purchase Order': purchase_invoice_gst_fields, 'Purchase Receipt': purchase_invoice_gst_fields, - 'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields, + 'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields, 'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields, 'Sales Order': sales_invoice_gst_fields, 'Tax Category': inter_state_gst_field, diff --git a/requirements.txt b/requirements.txt index 678cf74fef0..4511aa54d8d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,4 @@ taxjar==1.9.0 tweepy==3.8.0 Unidecode==1.1.1 WooCommerce==2.1.1 +pycryptodome==3.9.8 \ No newline at end of file From 5bcc6c6b15a6dc722361edbb4dae31f462e4196f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 25 Dec 2020 14:27:38 +0530 Subject: [PATCH 147/295] fix: added shipment link in delivery note dashboard --- .../stock/doctype/delivery_note/delivery_note.js | 1 + .../stock/doctype/delivery_note/delivery_note.py | 14 +++++++++++--- .../delivery_note/delivery_note_dashboard.py | 2 +- erpnext/stock/doctype/shipment/shipment.json | 5 +++-- erpnext/stock/doctype/shipment/shipment.py | 7 ++++++- 5 files changed, 22 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 03921c554e3..5f2658c1028 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -15,6 +15,7 @@ frappe.ui.form.on("Delivery Note", { 'Installation Note': 'Installation Note', 'Sales Invoice': 'Invoice', 'Stock Entry': 'Return', + 'Shipment': 'Shipment' }, frm.set_indicator_formatter('item_code', function(doc) { diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 1a6a5550927..a30cadf0a04 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -598,6 +598,9 @@ def make_shipment(source_name, target_doc=None): pickup_contact_display += '
    ' + user.mobile_no target.pickup_contact = pickup_contact_display + # As we are using session user details in the pickup_contact then pickup_contact_person will be session user + target.pickup_contact_person = frappe.session.user + contact = frappe.db.get_value("Contact", source.contact_person, ['email_id', 'phone', 'mobile_no'], as_dict=1) delivery_contact_display = '{}'.format(source.contact_display) if contact: @@ -609,6 +612,13 @@ def make_shipment(source_name, target_doc=None): delivery_contact_display += '
    ' + contact.mobile_no target.delivery_contact = delivery_contact_display + if source.shipping_address_name: + target.delivery_address_name = source.shipping_address_name + target.delivery_address = source.shipping_address + elif source.customer_address: + target.delivery_address_name = source.customer_address + target.delivery_address = source.address_display + doclist = get_mapped_doc("Delivery Note", source_name, { "Delivery Note": { "doctype": "Shipment", @@ -617,9 +627,7 @@ def make_shipment(source_name, target_doc=None): "company": "pickup_company", "company_address": "pickup_address_name", "company_address_display": "pickup_address", - "address_display": "delivery_address", "customer": "delivery_customer", - "shipping_address_name": "delivery_address_name", "contact_person": "delivery_contact_name", "contact_email": "delivery_contact_email" }, @@ -637,7 +645,7 @@ def make_shipment(source_name, target_doc=None): } } }, target_doc, postprocess) - + return doclist @frappe.whitelist() diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py index beeb9ebb05d..47684d5c6ec 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py @@ -19,7 +19,7 @@ def get_data(): }, { 'label': _('Reference'), - 'items': ['Sales Order', 'Quality Inspection'] + 'items': ['Sales Order', 'Shipment', 'Quality Inspection'] }, { 'label': _('Returns'), diff --git a/erpnext/stock/doctype/shipment/shipment.json b/erpnext/stock/doctype/shipment/shipment.json index 37a9cc6c02c..76c331c5c25 100644 --- a/erpnext/stock/doctype/shipment/shipment.json +++ b/erpnext/stock/doctype/shipment/shipment.json @@ -345,7 +345,8 @@ "label": "Status", "no_copy": 1, "options": "Draft\nSubmitted\nBooked\nCancelled\nCompleted", - "print_hide": 1 + "print_hide": 1, + "read_only": 1 }, { "fieldname": "tracking_url", @@ -430,7 +431,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-12-02 15:43:44.607039", + "modified": "2020-12-25 15:02:34.891976", "modified_by": "Administrator", "module": "Stock", "name": "Shipment", diff --git a/erpnext/stock/doctype/shipment/shipment.py b/erpnext/stock/doctype/shipment/shipment.py index de0c243b057..9167bfcd2f5 100644 --- a/erpnext/stock/doctype/shipment/shipment.py +++ b/erpnext/stock/doctype/shipment/shipment.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import flt +from frappe.utils import flt, get_time, to_timedelta from frappe.model.document import Document from erpnext.accounts.party import get_party_shipping_address from frappe.contacts.doctype.contact.contact import get_default_contact @@ -13,6 +13,7 @@ from frappe.contacts.doctype.contact.contact import get_default_contact class Shipment(Document): def validate(self): self.validate_weight() + self.validate_pickup_time() self.set_value_of_goods() if self.docstatus == 0: self.status = 'Draft' @@ -32,6 +33,10 @@ class Shipment(Document): if flt(parcel.weight) <= 0: frappe.throw(_('Parcel weight cannot be 0')) + def validate_pickup_time(self): + if self.pickup_from and self.pickup_to and get_time(self.pickup_to) < get_time(self.pickup_from): + frappe.throw(_("Pickup To time should be greater than Pickup From time")) + def set_value_of_goods(self): value_of_goods = 0 for entry in self.get("shipment_delivery_note"): From de10d7dcf2079c37bc5a7f6afa7be74299825ba6 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 25 Dec 2020 15:15:55 +0530 Subject: [PATCH 148/295] Update shipment.py --- erpnext/stock/doctype/shipment/shipment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/shipment/shipment.py b/erpnext/stock/doctype/shipment/shipment.py index 9167bfcd2f5..4697a7b3235 100644 --- a/erpnext/stock/doctype/shipment/shipment.py +++ b/erpnext/stock/doctype/shipment/shipment.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import flt, get_time, to_timedelta +from frappe.utils import flt, get_time from frappe.model.document import Document from erpnext.accounts.party import get_party_shipping_address from frappe.contacts.doctype.contact.contact import get_default_contact From dab1ab990d158cde0d12192018abadf069311272 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 25 Dec 2020 13:23:12 +0530 Subject: [PATCH 149/295] fix: partial serial no return issue --- .../controllers/sales_and_purchase_return.py | 53 +++++++++++++++---- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 8f65c31f3d1..79792262c0a 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -204,21 +204,25 @@ def get_already_returned_items(doc): return items def get_returned_qty_map_for_row(row_name, doctype): + if doctype == "POS Invoice": return {} + child_doctype = doctype + " Item" - reference_field = frappe.scrub(child_doctype) if doctype == "Purchase Receipt" else "dn_detail" + reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype) fields = [ "sum(abs(`tab{0}`.qty)) as qty".format(child_doctype), "sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype) ] - if doctype == "Purchase Receipt": + if doctype in ("Purchase Receipt", "Purchase Invoice"): fields += [ "sum(abs(`tab{0}`.rejected_qty)) as rejected_qty".format(child_doctype), - "sum(abs(`tab{0}`.received_qty)) as received_qty".format(child_doctype), - "sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype) + "sum(abs(`tab{0}`.received_qty)) as received_qty".format(child_doctype) ] + if doctype == "Purchase Receipt": + fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)] + data = frappe.db.get_list(doctype, fields = fields, filters = [ @@ -231,6 +235,7 @@ def get_returned_qty_map_for_row(row_name, doctype): def make_return_doc(doctype, source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos company = frappe.db.get_value("Delivery Note", source_name, "company") default_warehouse_for_sales_return = frappe.db.get_value("Company", company, "default_warehouse_for_sales_return") @@ -290,6 +295,12 @@ def make_return_doc(doctype, source_name, target_doc=None): def update_item(source_doc, target_doc, source_parent): target_doc.qty = -1 * source_doc.qty + if source_doc.serial_no: + returned_serial_nos = get_returned_serial_nos(source_doc, source_parent) + serial_nos = list(set(get_serial_nos(source_doc.serial_no)) - set(returned_serial_nos)) + if serial_nos: + target_doc.serial_no = '\n'.join(serial_nos) + if doctype == "Purchase Receipt": returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0)) @@ -305,10 +316,12 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.purchase_receipt_item = source_doc.name elif doctype == "Purchase Invoice": - target_doc.received_qty = -1 * source_doc.received_qty - target_doc.rejected_qty = -1 * source_doc.rejected_qty - target_doc.qty = -1* source_doc.qty - target_doc.stock_qty = -1 * source_doc.stock_qty + returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) + target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0)) + target_doc.rejected_qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get('rejected_qty') or 0)) + target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0)) + + target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0)) target_doc.purchase_order = source_doc.purchase_order target_doc.purchase_receipt = source_doc.purchase_receipt target_doc.rejected_warehouse = source_doc.rejected_warehouse @@ -330,6 +343,10 @@ def make_return_doc(doctype, source_name, target_doc=None): if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return elif doctype == "Sales Invoice" or doctype == "POS Invoice": + returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) + target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0)) + target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0)) + target_doc.sales_order = source_doc.sales_order target_doc.delivery_note = source_doc.delivery_note target_doc.so_detail = source_doc.so_detail @@ -406,4 +423,22 @@ def get_filters(voucher_type, voucher_no, voucher_detail_no, return_against, ite if reference_voucher_detail_no: filters["voucher_detail_no"] = reference_voucher_detail_no - return filters \ No newline at end of file + return filters + +def get_returned_serial_nos(child_doc, parent_doc): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + return_ref_field = frappe.scrub(child_doc.doctype) + if child_doc.doctype == "Delivery Note Item": + return_ref_field = "dn_detail" + + serial_nos = [] + + fields = ["`{0}`.`serial_no`".format("tab" + child_doc.doctype)] + + filters = [[parent_doc.doctype, "return_against", "=", parent_doc.name], [parent_doc.doctype, "is_return", "=", 1], + [child_doc.doctype, return_ref_field, "=", child_doc.name], [parent_doc.doctype, "docstatus", "=", 1]] + + for row in frappe.get_all(parent_doc.doctype, fields = fields, filters=filters): + serial_nos.extend(get_serial_nos(row.serial_no)) + + return serial_nos \ No newline at end of file From 46d5f4c7f14f9cdbf046f2afc06ce93ff751852d Mon Sep 17 00:00:00 2001 From: "hasnain2808@gmail.com" Date: Fri, 25 Dec 2020 16:34:43 +0530 Subject: [PATCH 150/295] refactor(analytics report): linting --- .../purchase_analytics/purchase_analytics.js | 24 +++++++++++-------- .../report/sales_analytics/sales_analytics.js | 17 ++++++------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/erpnext/buying/report/purchase_analytics/purchase_analytics.js b/erpnext/buying/report/purchase_analytics/purchase_analytics.js index 7ee9f2c372a..ba8535a3ae4 100644 --- a/erpnext/buying/report/purchase_analytics/purchase_analytics.js +++ b/erpnext/buying/report/purchase_analytics/purchase_analytics.js @@ -87,12 +87,18 @@ frappe.query_reports["Purchase Analytics"] = { row_name = data[2].content; length = data.length; - if (tree_type == "Supplier" || tree_type == "Item") { + if (tree_type == "Supplier") { row_values = data .slice(4, length - 1) .map(function (column) { return column.content; }); + } else if (tree_type == "Item") { + row_values = data + .slice(5, length - 1) + .map(function (column) { + return column.content; + }); } else { row_values = data .slice(3, length - 1) @@ -109,17 +115,15 @@ frappe.query_reports["Purchase Analytics"] = { let raw_data = frappe.query_report.chart.data; let new_datasets = raw_data.datasets; - let found = false; - - for (let i = 0; i < new_datasets.length; i++) { - if (new_datasets[i].name == row_name) { - found = true; - new_datasets.splice(i, 1); - break; + let element_found = new_datasets.some((element, index, array)=>{ + if(element.name == row_name){ + array.splice(index, 1) + return true } - } + return false + }) - if (!found) { + if (!element_found) { new_datasets.push(entry); } let new_data = { diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.js b/erpnext/selling/report/sales_analytics/sales_analytics.js index aad6bfd5ef1..9089b53fb04 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.js +++ b/erpnext/selling/report/sales_analytics/sales_analytics.js @@ -76,7 +76,6 @@ frappe.query_reports["Sales Analytics"] = { events: { onCheckRow: function (data) { if (!data) return; - const data_doctype = $( data[2].html )[0].attributes.getNamedItem("data-doctype").value; @@ -114,17 +113,15 @@ frappe.query_reports["Sales Analytics"] = { let raw_data = frappe.query_report.chart.data; let new_datasets = raw_data.datasets; - let found = false; - - for (let i = 0; i < new_datasets.length; i++) { - if (new_datasets[i].name == row_name) { - found = true; - new_datasets.splice(i, 1); - break; + let element_found = new_datasets.some((element, index, array)=>{ + if(element.name == row_name){ + array.splice(index, 1) + return true } - } + return false + }) - if (!found) { + if (!element_found) { new_datasets.push(entry); } From b99c77b753827f46ccfcc85388a00679276ad7c5 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 25 Dec 2020 18:12:35 +0530 Subject: [PATCH 151/295] Reposting fixes (#24202) * fix: finished item validation and rate * fix: Check if stock and account balance in sync after reposting * fix: validate stock accounts in journal entry * fix: validate expense against budget --- .../doctype/journal_entry/journal_entry.py | 21 +++- .../journal_entry/test_journal_entry.py | 18 ++- erpnext/accounts/general_ledger.py | 62 ---------- erpnext/accounts/utils.py | 115 ++++++++++++++---- erpnext/controllers/buying_controller.py | 2 +- erpnext/controllers/stock_controller.py | 10 +- .../purchase_receipt/purchase_receipt.py | 2 +- .../repost_item_valuation.py | 35 +++++- .../stock/doctype/stock_entry/stock_entry.py | 9 +- .../doctype/stock_entry/test_stock_entry.py | 78 ++++++------ .../stock_entry_detail.json | 2 +- .../stock_and_account_value_comparison.py | 3 +- 12 files changed, 205 insertions(+), 152 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index cd712738aa6..cb90f8036e2 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -6,14 +6,18 @@ import frappe, erpnext, json from frappe.utils import cstr, flt, fmt_money, formatdate, getdate, nowdate, cint, get_link_to_form from frappe import msgprint, _, scrub from erpnext.controllers.accounts_controller import AccountsController -from erpnext.accounts.utils import get_balance_on, get_account_currency +from erpnext.accounts.utils import get_balance_on, get_stock_accounts, get_stock_and_account_balance, \ + get_account_currency, check_if_stock_and_account_balance_synced from erpnext.accounts.party import get_party_account from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount -from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import get_party_account_based_on_invoice_discounting +from erpnext.accounts.doctype.invoice_discounting.invoice_discounting \ + import get_party_account_based_on_invoice_discounting from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts from six import string_types, iteritems +class StockAccountInvalidTransaction(frappe.ValidationError): pass + class JournalEntry(AccountsController): def __init__(self, *args, **kwargs): super(JournalEntry, self).__init__(*args, **kwargs) @@ -46,6 +50,7 @@ class JournalEntry(AccountsController): self.validate_empty_accounts_table() self.set_account_and_party_balance() self.validate_inter_company_accounts() + self.validate_stock_accounts() if not self.title: self.title = self.get_title() @@ -57,6 +62,8 @@ class JournalEntry(AccountsController): self.update_expense_claim() self.update_inter_company_jv() self.update_invoice_discounting() + check_if_stock_and_account_balance_synced(self.posting_date, + self.company, self.doctype, self.name) def on_cancel(self): from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries @@ -95,6 +102,16 @@ class JournalEntry(AccountsController): if account_currency == previous_account_currency: if self.total_credit != doc.total_debit or self.total_debit != doc.total_credit: frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry")) + + def validate_stock_accounts(self): + stock_accounts = get_stock_accounts(self.company, self.doctype, self.name) + for account in stock_accounts: + account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account, + self.posting_date, self.company) + + if account_bal == stock_bal: + frappe.throw(_("Account: {0} can only be updated via Stock Transactions") + .format(account), StockAccountInvalidTransaction) def update_inter_company_jv(self): if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference: diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py index 1d2eacdb80c..b56f8e5fe2f 100644 --- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py @@ -6,7 +6,7 @@ import unittest, frappe from frappe.utils import flt, nowdate from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.exceptions import InvalidAccountCurrency -from erpnext.accounts.general_ledger import StockAccountInvalidTransaction +from erpnext.accounts.doctype.journal_entry.journal_entry import StockAccountInvalidTransaction class TestJournalEntry(unittest.TestCase): def test_journal_entry_with_against_jv(self): @@ -84,25 +84,31 @@ class TestJournalEntry(unittest.TestCase): company = "_Test Company with perpetual inventory" stock_account = get_inventory_account(company) + from erpnext.accounts.utils import get_stock_and_account_balance + account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(stock_account, nowdate(), company) + diff = flt(account_bal) - flt(stock_bal) + + if not diff: + diff = 100 + jv = frappe.new_doc("Journal Entry") jv.company = company jv.posting_date = nowdate() jv.append("accounts", { "account": stock_account, "cost_center": "Main - TCP1", - "debit_in_account_currency": 100 + "debit_in_account_currency": 0 if diff > 0 else abs(diff), + "credit_in_account_currency": diff if diff > 0 else 0 }) jv.append("accounts", { "account": "Stock Adjustment - TCP1", - "credit_in_account_currency": 100, "cost_center": "Main - TCP1", + "debit_in_account_currency": diff if diff > 0 else 0, + "credit_in_account_currency": 0 if diff > 0 else abs(diff) }) jv.insert() - from erpnext.accounts.utils import get_stock_and_account_balance - account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(stock_account, nowdate(), company) - if account_bal == stock_bal: self.assertRaises(StockAccountInvalidTransaction, jv.submit) frappe.db.rollback() diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index c7f0c8781c0..287c79f13fe 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -5,15 +5,11 @@ from __future__ import unicode_literals import frappe, erpnext from frappe.utils import flt, cstr, cint, comma_and, today, getdate, formatdate, now from frappe import _ -from erpnext.accounts.utils import get_stock_and_account_balance from frappe.model.meta import get_field_precision from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions - class ClosedAccountingPeriod(frappe.ValidationError): pass -class StockAccountInvalidTransaction(frappe.ValidationError): pass -class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError): pass def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, update_outstanding='Yes', from_repost=False): if gl_map: @@ -131,10 +127,6 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False): for entry in gl_map: make_entry(entry, adv_adj, update_outstanding, from_repost) - if not from_repost: - validate_account_for_perpetual_inventory(gl_map) - - def make_entry(args, adv_adj, update_outstanding, from_repost=False): gle = frappe.new_doc("GL Entry") gle.update(args) @@ -144,63 +136,9 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False): gle.run_method("on_update_with_args", adv_adj, update_outstanding, from_repost) gle.submit() - # check against budget if not from_repost: validate_expense_against_budget(args) -def validate_account_for_perpetual_inventory(gl_map): - if cint(erpnext.is_perpetual_inventory_enabled(gl_map[0].company)): - account_list = [gl_entries.account for gl_entries in gl_map] - - aii_accounts = [d.name for d in frappe.get_all("Account", - filters={'account_type': 'Stock', 'is_group': 0, 'company': gl_map[0].company})] - - for account in account_list: - if account not in aii_accounts: - continue - - # Always use current date to get stock and account balance as there can future entries for - # other items - account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account, - gl_map[0].posting_date, gl_map[0].company) - - if gl_map[0].voucher_type=="Journal Entry": - # In case of Journal Entry, there are no corresponding SL entries, - # hence deducting currency amount - account_bal -= flt(gl_map[0].debit) - flt(gl_map[0].credit) - if account_bal == stock_bal: - frappe.throw(_("Account: {0} can only be updated via Stock Transactions") - .format(account), StockAccountInvalidTransaction) - - elif abs(account_bal - stock_bal) > 0.1: - precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), - currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency")) - - diff = flt(stock_bal - account_bal, precision) - error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses on {3}.").format( - stock_bal, account_bal, frappe.bold(account), gl_map[0].posting_date) - error_resolution = _("Please create adjustment Journal Entry for amount {0} ").format(frappe.bold(diff)) - stock_adjustment_account = frappe.db.get_value("Company",gl_map[0].company,"stock_adjustment_account") - - db_or_cr_warehouse_account =('credit_in_account_currency' if diff < 0 else 'debit_in_account_currency') - db_or_cr_stock_adjustment_account = ('debit_in_account_currency' if diff < 0 else 'credit_in_account_currency') - - journal_entry_args = { - 'accounts':[ - {'account': account, db_or_cr_warehouse_account : abs(diff)}, - {'account': stock_adjustment_account, db_or_cr_stock_adjustment_account : abs(diff)} - ] - } - - frappe.msgprint(msg="""{0}

    {1}

    """.format(error_reason, error_resolution), - raise_exception=StockValueAndAccountBalanceOutOfSync, - title=_('Values Out Of Sync'), - primary_action={ - 'label': _('Make Journal Entry'), - 'client_action': 'erpnext.route_to_adjustment_jv', - 'args': journal_entry_args - }) - def validate_cwip_accounts(gl_map): cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")]) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 540ac841823..67c7fd2d22a 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -12,11 +12,12 @@ from frappe.utils import formatdate, get_number_format_info from six import iteritems # imported to enable erpnext.accounts.utils.get_account_currency from erpnext.accounts.doctype.account.account import get_account_currency +from frappe.model.meta import get_field_precision from erpnext.stock.utils import get_stock_value_on from erpnext.stock import get_warehouse_account_map - +class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError): pass class FiscalYearError(frappe.ValidationError): pass @frappe.whitelist() @@ -585,24 +586,6 @@ def fix_total_debit_credit(): (dr_or_cr, dr_or_cr, '%s', '%s', '%s', dr_or_cr), (d.diff, d.voucher_type, d.voucher_no)) -def get_stock_and_account_balance(account=None, posting_date=None, company=None): - if not posting_date: posting_date = nowdate() - - warehouse_account = get_warehouse_account_map(company) - - account_balance = get_balance_on(account, posting_date, in_account_currency=False, ignore_account_permission=True) - - related_warehouses = [wh for wh, wh_details in warehouse_account.items() - if wh_details.account == account and not wh_details.is_group] - - total_stock_value = 0.0 - for warehouse in related_warehouses: - value = get_stock_value_on(warehouse, posting_date) - total_stock_value += value - - precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency") - return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses - def get_currency_precision(): precision = cint(frappe.db.get_default("currency_precision")) if not precision: @@ -903,12 +886,6 @@ def get_coa(doctype, parent, is_root, chart=None): return accounts -def get_stock_accounts(company): - return frappe.get_all("Account", filters = { - "account_type": "Stock", - "company": company - }) - def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None, warehouse_account=None, company=None): def _delete_gl_entries(voucher_type, voucher_no): @@ -983,4 +960,90 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle): if not account_existed: matched = False break - return matched \ No newline at end of file + return matched + +def check_if_stock_and_account_balance_synced(posting_date, company, voucher_type=None, voucher_no=None): + if not cint(erpnext.is_perpetual_inventory_enabled(company)): + return + + accounts = get_stock_accounts(company, voucher_type, voucher_no) + stock_adjustment_account = frappe.db.get_value("Company", company, "stock_adjustment_account") + + for account in accounts: + account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account, + posting_date, company) + + if abs(account_bal - stock_bal) > 0.1: + precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), + currency=frappe.get_cached_value('Company', company, "default_currency")) + + diff = flt(stock_bal - account_bal, precision) + + error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses as on {3}.").format( + stock_bal, account_bal, frappe.bold(account), posting_date) + error_resolution = _("Please create an adjustment Journal Entry for amount {0} on {1}")\ + .format(frappe.bold(diff), frappe.bold(posting_date)) + + frappe.msgprint( + msg="""{0}

    {1}

    """.format(error_reason, error_resolution), + raise_exception=StockValueAndAccountBalanceOutOfSync, + title=_('Values Out Of Sync'), + primary_action={ + 'label': _('Make Journal Entry'), + 'client_action': 'erpnext.route_to_adjustment_jv', + 'args': get_journal_entry(account, stock_adjustment_account, diff) + }) + +def get_stock_accounts(company, voucher_type=None, voucher_no=None): + stock_accounts = [d.name for d in frappe.db.get_all("Account", { + "account_type": "Stock", + "company": company, + "is_group": 0 + })] + if voucher_type and voucher_no: + if voucher_type == "Journal Entry": + stock_accounts = [d.account for d in frappe.db.get_all("Journal Entry Account", { + "parent": voucher_no, + "account": ["in", stock_accounts] + }, "account")] + + else: + stock_accounts = [d.account for d in frappe.db.get_all("GL Entry", { + "voucher_type": voucher_type, + "voucher_no": voucher_no, + "account": ["in", stock_accounts] + }, "account")] + + return stock_accounts + +def get_stock_and_account_balance(account=None, posting_date=None, company=None): + if not posting_date: posting_date = nowdate() + + warehouse_account = get_warehouse_account_map(company) + + account_balance = get_balance_on(account, posting_date, in_account_currency=False, ignore_account_permission=True) + + related_warehouses = [wh for wh, wh_details in warehouse_account.items() + if wh_details.account == account and not wh_details.is_group] + + total_stock_value = 0.0 + for warehouse in related_warehouses: + value = get_stock_value_on(warehouse, posting_date) + total_stock_value += value + + precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency") + return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses + +def get_journal_entry(account, stock_adjustment_account, amount): + db_or_cr_warehouse_account =('credit_in_account_currency' if amount < 0 else 'debit_in_account_currency') + db_or_cr_stock_adjustment_account = ('debit_in_account_currency' if amount < 0 else 'credit_in_account_currency') + + return { + 'accounts':[{ + 'account': account, + db_or_cr_warehouse_account: abs(amount) + }, { + 'account': stock_adjustment_account, + db_or_cr_stock_adjustment_account : abs(amount) + }] + } diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index dc61870df30..6edc020701d 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -241,7 +241,7 @@ class BuyingController(StockController): if rate > 0: d.rate = rate - d.amount = flt(d.consumed_qty) * flt(d.rate) + d.amount = flt(flt(d.consumed_qty) * flt(d.rate), d.precision("amount")) supplied_items_cost += flt(d.amount) return supplied_items_cost diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 51c063c2c0b..439997616c7 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -6,7 +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 erpnext.accounts.utils import get_fiscal_year +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 from erpnext.stock.stock_ledger import get_valuation_rate @@ -402,6 +402,14 @@ class StockController(AccountsController): if check_if_future_sle_exists(args): create_repost_item_valuation_entry(args) + elif not is_reposting_pending(): + check_if_stock_and_account_balance_synced(self.posting_date, + self.company, self.doctype, self.name) + +def is_reposting_pending(): + return frappe.db.exists("Repost Item Valuation", + {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) + def check_if_future_sle_exists(args): sl_entries = frappe.db.get_all("Stock Ledger Entry", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 226064bae78..159a6085ff3 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -408,7 +408,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): diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index a942f2edda7..ba2c2c6f446 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -5,11 +5,11 @@ from __future__ import unicode_literals import frappe, erpnext from frappe.model.document import Document -from frappe.utils import cint +from frappe.utils import cint, get_link_to_form from erpnext.stock.stock_ledger import repost_future_sle -from erpnext.accounts.utils import update_gl_entries_after - - +from erpnext.accounts.utils import update_gl_entries_after, check_if_stock_and_account_balance_synced +from frappe.utils.user import get_users_with_role +from frappe import _ class RepostItemValuation(Document): def validate(self): self.set_status() @@ -51,12 +51,20 @@ def repost(doc): repost_sl_entries(doc) repost_gl_entries(doc) + check_if_stock_and_account_balance_synced(doc.posting_date, doc.company) + doc.set_status('Completed') except Exception: frappe.db.rollback() traceback = frappe.get_traceback() frappe.log_error(traceback) - frappe.db.set_value(doc.doctype, doc.name, 'error_log', traceback) + + message = frappe.message_log.pop() + if traceback: + message += "
    " + "Traceback:
    " + traceback + frappe.db.set_value(doc.doctype, doc.name, 'error_log', message) + + notify_error_to_stock_managers(doc) doc.set_status('Failed') raise finally: @@ -86,4 +94,19 @@ def repost_gl_entries(doc): warehouses = [doc.warehouse] update_gl_entries_after(doc.posting_date, doc.posting_time, - warehouses, items, company=doc.company) \ No newline at end of file + warehouses, items, company=doc.company) + +def notify_error_to_stock_managers(doc, traceback): + recipients = get_users_with_role("Stock Manager") + if not recipients: + get_users_with_role("System Manager") + + subject = _("Error while reposting item valuation") + message = (_("Hi,") + "
    " + + _("An error has been appeared while reposting item valuation via {0}") + .format(get_link_to_form(doc.doctype, doc.name)) + "
    " + + _("Please check the error message and take necessary actions to fix the error and then restart the reposting again.") + ) + frappe.sendmail(recipients=recipients, subject=subject, message=message) + + diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 579b8c5fe1d..92d268f0993 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -442,6 +442,7 @@ class StockEntry(StockController): """ # Set rate for outgoing items outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate) + finished_item_qty = sum([d.transfer_qty for d in self.items if d.is_finished_item]) # Set basic rate for incoming items for d in self.get('items'): @@ -451,7 +452,7 @@ class StockEntry(StockController): d.basic_rate = 0.0 elif d.is_finished_item: if self.purpose == "Manufacture": - d.basic_rate = self.get_basic_rate_for_manufactured_item(d.transfer_qty, outgoing_items_cost) + d.basic_rate = self.get_basic_rate_for_manufactured_item(finished_item_qty, outgoing_items_cost) elif self.purpose == "Repack": d.basic_rate = self.get_basic_rate_for_repacked_items(d.transfer_qty, outgoing_items_cost) @@ -666,7 +667,7 @@ class StockEntry(StockController): production_item, wo_qty = frappe.db.get_value("Work Order", self.work_order, ["production_item", "qty"]) - number_of_finished_items = 0 + finished_items = [] for d in self.get('items'): if d.is_finished_item: if d.item_code != production_item: @@ -675,9 +676,9 @@ class StockEntry(StockController): elif flt(d.transfer_qty) > flt(self.fg_completed_qty): frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}"). \ format(d.idx, d.transfer_qty, self.fg_completed_qty)) - number_of_finished_items += 1 + finished_items.append(d.item_code) - if number_of_finished_items > 1: + if len(set(finished_items)) > 1: frappe.throw(_("Multiple items cannot be marked as finished item")) if self.purpose == "Manufacture": diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 1a641855aa2..123f0c86471 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -179,22 +179,20 @@ class TestStockEntry(unittest.TestCase): def test_material_transfer_gl_entry(self): company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') - create_stock_reconciliation(qty=100, rate=100) - mtn = make_stock_entry(item_code="_Test Item", source="Stores - TCP1", - target="Finished Goods - TCP1", qty=45) + target="Finished Goods - TCP1", qty=45, company=company) self.check_stock_ledger_entries("Stock Entry", mtn.name, [["_Test Item", "Stores - TCP1", -45.0], ["_Test Item", "Finished Goods - TCP1", 45.0]]) - stock_in_hand_account = get_inventory_account(mtn.company, mtn.get("items")[0].s_warehouse) + source_warehouse_account = get_inventory_account(mtn.company, mtn.get("items")[0].s_warehouse) - fixed_asset_account = get_inventory_account(mtn.company, mtn.get("items")[0].t_warehouse) + target_warehouse_account = get_inventory_account(mtn.company, mtn.get("items")[0].t_warehouse) - if stock_in_hand_account == fixed_asset_account: + if source_warehouse_account == target_warehouse_account: # no gl entry as both source and target warehouse has linked to same account. self.assertFalse(frappe.db.sql("""select * from `tabGL Entry` - where voucher_type='Stock Entry' and voucher_no=%s""", mtn.name)) + where voucher_type='Stock Entry' and voucher_no=%s""", mtn.name, as_dict=1)) else: stock_value_diff = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Stock Entry", @@ -202,8 +200,8 @@ class TestStockEntry(unittest.TestCase): self.check_gl_entries("Stock Entry", mtn.name, sorted([ - [stock_in_hand_account, 0.0, stock_value_diff], - [fixed_asset_account, stock_value_diff, 0.0], + [source_warehouse_account, 0.0, stock_value_diff], + [target_warehouse_account, stock_value_diff, 0.0], ]) ) @@ -754,37 +752,37 @@ class TestStockEntry(unittest.TestCase): def test_total_basic_amount_zero(self): se = frappe.get_doc({"doctype":"Stock Entry", - "purpose":"Material Receipt", - "stock_entry_type":"Material Receipt", - "posting_date": nowdate(), - "company":"_Test Company with perpetual inventory", - "items":[ - { - "item_code":"_Test Item", - "description":"_Test Item", - "qty": 1, - "basic_rate": 0, - "uom":"Nos", - "t_warehouse": "Stores - TCP1", - "allow_zero_valuation_rate": 1, - "cost_center": "Main - TCP1" - }, - { - "item_code":"_Test Item", - "description":"_Test Item", - "qty": 2, - "basic_rate": 0, - "uom":"Nos", - "t_warehouse": "Stores - TCP1", - "allow_zero_valuation_rate": 1, - "cost_center": "Main - TCP1" - }, - ], - "additional_costs":[ - {"expense_account":"Miscellaneous Expenses - TCP1", - "amount":100, - "description": "miscellanous"} - ] + "purpose":"Material Receipt", + "stock_entry_type":"Material Receipt", + "posting_date": nowdate(), + "company":"_Test Company with perpetual inventory", + "items":[ + { + "item_code":"_Test Item", + "description":"_Test Item", + "qty": 1, + "basic_rate": 0, + "uom":"Nos", + "t_warehouse": "Stores - TCP1", + "allow_zero_valuation_rate": 1, + "cost_center": "Main - TCP1" + }, + { + "item_code":"_Test Item", + "description":"_Test Item", + "qty": 2, + "basic_rate": 0, + "uom":"Nos", + "t_warehouse": "Stores - TCP1", + "allow_zero_valuation_rate": 1, + "cost_center": "Main - TCP1" + }, + ], + "additional_costs":[ + {"expense_account":"Miscellaneous Expenses - TCP1", + "amount":100, + "description": "miscellanous" + }] }) se.insert() se.submit() 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 6fe60298eeb..b78ae6d79b3 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -526,7 +526,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-09-23 17:55:03.384138", + "modified": "2020-12-23 17:55:03.384138", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py index 1af68dd7f22..14d543b1740 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py +++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py @@ -57,8 +57,7 @@ def get_gl_data(report_filters, filters): if report_filters.account: stock_accounts = [report_filters.account] else: - stock_accounts = [k.name - for k in get_stock_accounts(report_filters.company)] + stock_accounts = get_stock_accounts(report_filters.company) filters.update({ "account": ("in", stock_accounts) From 0788df412ce9fc8f8803170ec1cede05ba6e91ff Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 25 Dec 2020 20:52:59 +0530 Subject: [PATCH 152/295] fix: Removed permissions from UAE VAT Settings --- .../uae_vat_settings/uae_vat_settings.json | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.json b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.json index ce2c1d4e142..1ff5680bfe9 100644 --- a/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.json +++ b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.json @@ -29,25 +29,12 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-09-30 20:08:18.764798", + "modified": "2020-12-25 20:20:22.342426", "modified_by": "Administrator", "module": "Regional", "name": "UAE VAT Settings", "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], + "permissions": [], "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", From 60a1d251969bbad3eaa053bc712b0bcb33c73f46 Mon Sep 17 00:00:00 2001 From: Saqib Date: Sat, 26 Dec 2020 13:01:49 +0530 Subject: [PATCH 153/295] fix: cancelling of asset value adjustement (#24193) --- .../doctype/asset_value_adjustment/asset_value_adjustment.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 74ca62ffdad..14308277c14 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -21,9 +21,6 @@ class AssetValueAdjustment(Document): self.reschedule_depreciations(self.new_asset_value) def on_cancel(self): - if self.journal_entry: - frappe.throw(_("Cancel the journal entry {0} first").format(self.journal_entry)) - self.reschedule_depreciations(self.current_asset_value) def validate_date(self): From 71f203dbc565d170c993668f6f0f92de2303194a Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 28 Dec 2020 12:35:19 +0530 Subject: [PATCH 154/295] fix: template task status, subject in project template task --- .../patches/v13_0/update_project_template_tasks.py | 2 ++ .../doctype/project_template/project_template.js | 10 ++++++++++ .../project_template_task/project_template_task.json | 11 +++++++++-- erpnext/projects/doctype/task/task.json | 4 ++-- erpnext/projects/doctype/task/task.py | 2 ++ 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py index 26c42592816..f24a2c62f1f 100644 --- a/erpnext/patches/v13_0/update_project_template_tasks.py +++ b/erpnext/patches/v13_0/update_project_template_tasks.py @@ -5,6 +5,8 @@ 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") for template_name in frappe.db.sql(""" select diff --git a/erpnext/projects/doctype/project_template/project_template.js b/erpnext/projects/doctype/project_template/project_template.js index 7668df3e139..04153dc5704 100644 --- a/erpnext/projects/doctype/project_template/project_template.js +++ b/erpnext/projects/doctype/project_template/project_template.js @@ -15,3 +15,13 @@ frappe.ui.form.on('Project Template', { }); } }); + +frappe.ui.form.on('Project Template Task', { + task: function (frm, cdt, cdn) { + var row = locals[cdt][cdn]; + frappe.db.get_value("Task", row.task, "subject", (value) => { + row.subject = value.subject; + refresh_field("tasks"); + }); + } +}) diff --git a/erpnext/projects/doctype/project_template_task/project_template_task.json b/erpnext/projects/doctype/project_template_task/project_template_task.json index 80c510db1b0..7a552945bd5 100644 --- a/erpnext/projects/doctype/project_template_task/project_template_task.json +++ b/erpnext/projects/doctype/project_template_task/project_template_task.json @@ -5,7 +5,8 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "task" + "task", + "subject" ], "fields": [ { @@ -15,11 +16,17 @@ "label": "Task", "options": "Task", "reqd": 1 + }, + { + "fieldname": "subject", + "fieldtype": "Read Only", + "in_list_view": 1, + "label": "Subject" } ], "istable": 1, "links": [], - "modified": "2020-12-07 13:28:40.961810", + "modified": "2020-12-28 12:10:26.321913", "modified_by": "Administrator", "module": "Projects", "name": "Project Template Task", diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json index bb55256f7d9..160cc5812f7 100644 --- a/erpnext/projects/doctype/task/task.json +++ b/erpnext/projects/doctype/task/task.json @@ -115,7 +115,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "Open\nWorking\nPending Review\nOverdue\nCompleted\nCancelled" + "options": "Open\nWorking\nPending Review\nOverdue\nTemplate\nCompleted\nCancelled" }, { "fieldname": "priority", @@ -388,7 +388,7 @@ "is_tree": 1, "links": [], "max_attachments": 5, - "modified": "2020-12-21 11:59:24.196834", + "modified": "2020-12-28 11:32:58.714991", "modified_by": "Administrator", "module": "Projects", "name": "Task", diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 80b764ba4f0..a2095c95d51 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -56,6 +56,8 @@ class Task(NestedSet): validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual") def validate_status(self): + if self.is_template and self.status != "Template": + self.status = "Template" if self.status!=self.get_db_value("status") and self.status == "Completed": for d in self.depends_on: if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"): From 40d2c6a0cccea8b08bf3480bcb563099f1429780 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 28 Dec 2020 14:08:45 +0530 Subject: [PATCH 155/295] fix: finished good produced qty validation --- .../stock/doctype/stock_entry/stock_entry.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 92d268f0993..2fc7da83896 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -259,11 +259,16 @@ class StockEntry(StockController): item_code.append(item.item_code) def validate_fg_completed_qty(self): + item_wise_qty = {} if self.purpose == "Manufacture" and self.work_order: for d in self.items: - if d.is_finished_item and d.qty != self.fg_completed_qty: - frappe.throw(_("Finished product quantity {0} and For Quantity {1} cannot be different") - .format(d.qty, self.fg_completed_qty)) + if d.is_finished_item: + item_wise_qty.setdefault(d.item_code, []).append(d.qty) + + for item_code, qty_list in iteritems(item_wise_qty): + if self.fg_completed_qty != sum(qty_list): + frappe.throw(_("The finished product {0} quantity {1} and For Quantity {2} cannot be different") + .format(frappe.bold(item_code), frappe.bold(sum(qty_list)), frappe.bold(self.fg_completed_qty))) def validate_difference_account(self): if not cint(erpnext.is_perpetual_inventory_enabled(self.company)): @@ -319,7 +324,7 @@ class StockEntry(StockController): if self.purpose == "Manufacture": if validate_for_manufacture: - if d.bom_no: + if d.is_finished_item or d.is_scrap_item: d.s_warehouse = None if not d.t_warehouse: frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx)) @@ -699,7 +704,7 @@ class StockEntry(StockController): # SLE for target warehouse self.get_sle_for_target_warehouse(sl_entries, finished_item_row) - + # reverse sl entries if cancel if self.docstatus == 2: sl_entries.reverse() @@ -727,9 +732,9 @@ class StockEntry(StockController): sle.dependant_sle_voucher_detail_no = d.name elif finished_item_row and (finished_item_row.item_code != d.item_code or finished_item_row.t_warehouse != d.s_warehouse): sle.dependant_sle_voucher_detail_no = finished_item_row.name - + sl_entries.append(sle) - + def get_sle_for_target_warehouse(self, sl_entries, finished_item_row): for d in self.get('items'): if cstr(d.t_warehouse): From 527b7e16e5449e13cd2c68469265dfa10637faf5 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 28 Dec 2020 15:29:30 +0530 Subject: [PATCH 156/295] Revert "feat: Batch wise item selling pricing" --- erpnext/public/js/controllers/buying.js | 4 - erpnext/public/js/controllers/transaction.js | 6 - erpnext/selling/sales_common.js | 4 - erpnext/stock/doctype/batch/test_batch.py | 70 +---------- .../stock/doctype/item_price/item_price.json | 116 ++++-------------- .../stock/doctype/item_price/item_price.py | 5 +- erpnext/stock/get_item_details.py | 5 +- 7 files changed, 31 insertions(+), 179 deletions(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index fcd7c15a7ca..db85a3ec99e 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -195,10 +195,6 @@ 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) }, diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 7bd72c6b6bd..3bc20f87336 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1104,11 +1104,6 @@ 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) { @@ -1413,7 +1408,6 @@ 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 }); diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index ce084646e15..7f00fca8f05 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -399,10 +399,6 @@ 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/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index 97f85bafd95..e41f1a8aaaf 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -8,8 +8,6 @@ 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): @@ -184,7 +182,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''' @@ -254,72 +252,6 @@ 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/item_price/item_price.json b/erpnext/stock/doctype/item_price/item_price.json index 83177b372ad..5f62381f8b3 100644 --- a/erpnext/stock/doctype/item_price/item_price.json +++ b/erpnext/stock/doctype/item_price/item_price.json @@ -18,7 +18,6 @@ "price_list", "customer", "supplier", - "batch_no", "column_break_3", "buying", "selling", @@ -48,41 +47,31 @@ "oldfieldtype": "Select", "options": "Item", "reqd": 1, - "search_index": 1, - "show_days": 1, - "show_seconds": 1 + "search_index": 1 }, { "fieldname": "uom", "fieldtype": "Link", "label": "UOM", - "options": "UOM", - "show_days": 1, - "show_seconds": 1 + "options": "UOM" }, { "default": "0", "description": "Quantity that must be bought or sold per UOM", "fieldname": "packing_unit", "fieldtype": "Int", - "label": "Packing Unit", - "show_days": 1, - "show_seconds": 1 + "label": "Packing Unit" }, { "fieldname": "column_break_17", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "item_name", "fieldtype": "Data", "in_list_view": 1, "label": "Item Name", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fetch_from": "item_code.brand", @@ -90,25 +79,19 @@ "fieldtype": "Read Only", "in_list_view": 1, "label": "Brand", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "item_description", "fieldtype": "Text", "label": "Item Description", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "price_list_details", "fieldtype": "Section Break", "label": "Price List", - "options": "fa fa-tags", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-tags" }, { "fieldname": "price_list", @@ -117,9 +100,7 @@ "in_standard_filter": 1, "label": "Price List", "options": "Price List", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "bold": 1, @@ -127,49 +108,37 @@ "fieldname": "customer", "fieldtype": "Link", "label": "Customer", - "options": "Customer", - "show_days": 1, - "show_seconds": 1 + "options": "Customer" }, { "depends_on": "eval:doc.buying == 1", "fieldname": "supplier", "fieldtype": "Link", "label": "Supplier", - "options": "Supplier", - "show_days": 1, - "show_seconds": 1 + "options": "Supplier" }, { "fieldname": "column_break_3", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "default": "0", "fieldname": "buying", "fieldtype": "Check", "label": "Buying", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "default": "0", "fieldname": "selling", "fieldtype": "Check", "label": "Selling", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "item_details", "fieldtype": "Section Break", - "options": "fa fa-tag", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-tag" }, { "bold": 1, @@ -177,15 +146,11 @@ "fieldtype": "Link", "label": "Currency", "options": "Currency", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "col_br_1", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "price_list_rate", @@ -197,80 +162,53 @@ "oldfieldname": "ref_rate", "oldfieldtype": "Currency", "options": "currency", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "fieldname": "section_break_15", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "default": "Today", "fieldname": "valid_from", "fieldtype": "Date", - "label": "Valid From", - "show_days": 1, - "show_seconds": 1 + "label": "Valid From" }, { "default": "0", "fieldname": "lead_time_days", "fieldtype": "Int", - "label": "Lead Time in days", - "show_days": 1, - "show_seconds": 1 + "label": "Lead Time in days" }, { "fieldname": "column_break_18", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "valid_upto", "fieldtype": "Date", - "label": "Valid Upto", - "show_days": 1, - "show_seconds": 1 + "label": "Valid Upto" }, { "fieldname": "section_break_24", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "fieldname": "note", "fieldtype": "Text", - "label": "Note", - "show_days": 1, - "show_seconds": 1 + "label": "Note" }, { "fieldname": "reference", "fieldtype": "Data", "in_list_view": 1, - "label": "Reference", - "show_days": 1, - "show_seconds": 1 - }, - { - "fieldname": "batch_no", - "fieldtype": "Link", - "label": "Batch No", - "options": "Batch", - "show_days": 1, - "show_seconds": 1 + "label": "Reference" } ], "icon": "fa fa-flag", "idx": 1, - "index_web_pages_for_search": 1, "links": [], - "modified": "2020-12-08 18:12:15.395772", + "modified": "2020-07-06 22:31:32.943475", "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 e82a19b0dc0..bed5ea9ab66 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -54,8 +54,7 @@ class ItemPrice(Document): "valid_upto", "packing_unit", "customer", - "supplier", - "batch_no"]: + "supplier",]: if self.get(field): conditions += " and {0} = %({0})s ".format(field) else: @@ -69,7 +68,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, Batch, UOM, Qty, and Dates."), ItemPriceDuplicateItem,) + frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty, and Dates."), ItemPriceDuplicateItem,) def before_save(self): if self.selling: diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 2d2abd71aa1..08f7a83b893 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -672,8 +672,6 @@ 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 +690,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, batch_no desc, uom desc """.format(conditions=conditions), args) + order by valid_from desc, uom desc """.format(conditions=conditions), args) def get_price_list_rate_for(args, item_code): """ @@ -711,7 +709,6 @@ 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 From 88471854d53745a2756e947ac92c411b782d8a27 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 28 Dec 2020 15:40:23 +0530 Subject: [PATCH 157/295] fix: sider --- .../projects/doctype/project_template/project_template.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/projects/doctype/project_template/project_template.js b/erpnext/projects/doctype/project_template/project_template.js index 04153dc5704..3d3c15c6e05 100644 --- a/erpnext/projects/doctype/project_template/project_template.js +++ b/erpnext/projects/doctype/project_template/project_template.js @@ -20,8 +20,8 @@ frappe.ui.form.on('Project Template Task', { task: function (frm, cdt, cdn) { var row = locals[cdt][cdn]; frappe.db.get_value("Task", row.task, "subject", (value) => { - row.subject = value.subject; - refresh_field("tasks"); - }); + row.subject = value.subject; + refresh_field("tasks"); + }); } -}) +}); From 29a03bd5a1d250479369f8539450414bfbef080c Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 28 Dec 2020 16:59:13 +0530 Subject: [PATCH 158/295] feat: Add 'Manual Inspection' checkbox - fix merge conflict in js file - Dont auto set status if manual inspection is checked - Added 'Manual Inspection' checkbox in QI readings table --- .../doctype/quality_inspection/quality_inspection.js | 1 + .../doctype/quality_inspection/quality_inspection.py | 11 ++++++----- .../quality_inspection_reading.json | 10 +++++++++- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js index 544bc2c307a..f7565fd505c 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js @@ -51,6 +51,7 @@ frappe.ui.form.on("Quality Inspection", { }; } }); + }, refresh: function(frm) { // Ignore cancellation of reference doctype on cancel all. diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index f582658d871..9672b623944 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -76,11 +76,12 @@ class QualityInspection(Document): def inspect_and_set_status(self): for reading in self.readings: - if reading.formula_based_criteria: - self.set_status_based_on_acceptance_formula(reading) - else: - # if not formula based check acceptance values set - self.set_status_based_on_acceptance_values(reading) + if not reading.manual_inspection: # dont auto set status if manual + if reading.formula_based_criteria: + self.set_status_based_on_acceptance_formula(reading) + else: + # if not formula based check acceptance values set + self.set_status_based_on_acceptance_values(reading) def set_status_based_on_acceptance_values(self, reading): if cint(reading.non_numeric): diff --git a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json index 0792f26d2ab..264a6ea634b 100644 --- a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json +++ b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json @@ -10,6 +10,7 @@ "status", "value", "non_numeric", + "manual_inspection", "column_break_4", "min_value", "max_value", @@ -201,12 +202,19 @@ "fieldname": "non_numeric", "fieldtype": "Check", "label": "Non-Numeric" + }, + { + "default": "0", + "description": "Set the status manually.", + "fieldname": "manual_inspection", + "fieldtype": "Check", + "label": "Manual Inspection" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-12-21 11:36:24.885019", + "modified": "2020-12-28 16:40:47.586382", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection Reading", From a69e81a1510d3dc4d3ece2744224023ed3f14a23 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 28 Dec 2020 18:07:22 +0530 Subject: [PATCH 159/295] chore: Made 'Parameter' a link field in QI and QI Template - Added doctype Quality Inspection Parameter - Made 'Parameter' a link field in QI and QI Template - Added patch to create Quality Inspection Parameter records for every parameter in the system --- erpnext/patches.txt | 1 + .../convert_qi_parameter_to_link_field.py | 23 +++++ .../item_quality_inspection_parameter.json | 5 +- .../quality_inspection_parameter/__init__.py | 0 .../quality_inspection_parameter.js | 8 ++ .../quality_inspection_parameter.json | 86 +++++++++++++++++++ .../quality_inspection_parameter.py | 10 +++ .../test_quality_inspection_parameter.py | 10 +++ .../quality_inspection_reading.json | 5 +- 9 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py create mode 100644 erpnext/stock/doctype/quality_inspection_parameter/__init__.py create mode 100644 erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.js create mode 100644 erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json create mode 100644 erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.py create mode 100644 erpnext/stock/doctype/quality_inspection_parameter/test_quality_inspection_parameter.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d69dabf15cd..621f4173ad5 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -741,3 +741,4 @@ 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 erpnext.patches.v13_0.update_returned_qty_in_pr_dn +erpnext.patches.v13_0.convert_qi_parameter_to_link_field #2345 diff --git a/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py new file mode 100644 index 00000000000..289b6a761e3 --- /dev/null +++ b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py @@ -0,0 +1,23 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc('stock', 'doctype', 'quality_inspection_parameter') + + # get all distinct parameters from QI readigs table + reading_params = frappe.db.get_all("Quality Inspection Reading", fields=["distinct specification"]) + reading_params = [d.specification for d in reading_params] + + # get all distinct parameters from QI Template as some may be unused in QI + template_params = frappe.db.get_all("Item Quality Inspection Parameter", fields=["distinct specification"]) + template_params = [d.specification for d in template_params] + + params = list(set(reading_params + template_params)) + + for parameter in params: + if not frappe.db.exists("Quality Inspection Parameter", parameter): + frappe.get_doc({ + "doctype": "Quality Inspection Parameter", + "parameter": parameter, + "description": parameter + }).insert(ignore_permissions=True) \ No newline at end of file diff --git a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json index 9b980a1e013..fc06e89f2fb 100644 --- a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json +++ b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json @@ -18,11 +18,12 @@ "fields": [ { "fieldname": "specification", - "fieldtype": "Data", + "fieldtype": "Link", "in_list_view": 1, "label": "Parameter", "oldfieldname": "specification", "oldfieldtype": "Data", + "options": "Quality Inspection Parameter", "print_width": "200px", "reqd": 1, "width": "100px" @@ -79,7 +80,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-12-21 11:37:55.387677", + "modified": "2020-12-28 17:41:04.350225", "modified_by": "Administrator", "module": "Stock", "name": "Item Quality Inspection Parameter", diff --git a/erpnext/stock/doctype/quality_inspection_parameter/__init__.py b/erpnext/stock/doctype/quality_inspection_parameter/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.js b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.js new file mode 100644 index 00000000000..47c7e11d237 --- /dev/null +++ b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Quality Inspection Parameter', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json new file mode 100644 index 00000000000..0b5a9b5b3ce --- /dev/null +++ b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json @@ -0,0 +1,86 @@ +{ + "actions": [], + "autoname": "field:parameter", + "creation": "2020-12-28 17:06:00.254129", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "parameter", + "description" + ], + "fields": [ + { + "fieldname": "parameter", + "fieldtype": "Data", + "label": "Parameter", + "unique": 1 + }, + { + "fieldname": "description", + "fieldtype": "Text Editor", + "label": "Description" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-12-28 18:06:54.897317", + "modified_by": "Administrator", + "module": "Stock", + "name": "Quality Inspection Parameter", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock User", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Quality Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing User", + "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/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.py b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.py new file mode 100644 index 00000000000..86784221a0c --- /dev/null +++ b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.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 QualityInspectionParameter(Document): + pass diff --git a/erpnext/stock/doctype/quality_inspection_parameter/test_quality_inspection_parameter.py b/erpnext/stock/doctype/quality_inspection_parameter/test_quality_inspection_parameter.py new file mode 100644 index 00000000000..cefdc0867b1 --- /dev/null +++ b/erpnext/stock/doctype/quality_inspection_parameter/test_quality_inspection_parameter.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestQualityInspectionParameter(unittest.TestCase): + pass diff --git a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json index 264a6ea634b..739845bcdac 100644 --- a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json +++ b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json @@ -36,11 +36,12 @@ { "columns": 3, "fieldname": "specification", - "fieldtype": "Data", + "fieldtype": "Link", "in_list_view": 1, "label": "Parameter", "oldfieldname": "specification", "oldfieldtype": "Data", + "options": "Quality Inspection Parameter", "reqd": 1 }, { @@ -214,7 +215,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-12-28 16:40:47.586382", + "modified": "2020-12-28 17:40:47.407210", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection Reading", From 91e3c07d774fae54d7e0d67e9dd21cd8bc65c182 Mon Sep 17 00:00:00 2001 From: Afshan Date: Mon, 28 Dec 2020 19:42:46 +0530 Subject: [PATCH 160/295] fix: assest depreciation ledger --- .../asset_depreciation_ledger.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py index 16bef565252..2162a02eff9 100644 --- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py +++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py @@ -47,21 +47,22 @@ def get_data(filters): for d in gl_entries: asset_data = assets_details.get(d.against_voucher) - if not asset_data.get("accumulated_depreciation_amount"): - asset_data.accumulated_depreciation_amount = d.debit - else: - asset_data.accumulated_depreciation_amount += d.debit + if asset_data: + if not asset_data.get("accumulated_depreciation_amount"): + asset_data.accumulated_depreciation_amount = d.debit + else: + asset_data.accumulated_depreciation_amount += d.debit - row = frappe._dict(asset_data) - row.update({ - "depreciation_amount": d.debit, - "depreciation_date": d.posting_date, - "amount_after_depreciation": (flt(row.gross_purchase_amount) - - flt(row.accumulated_depreciation_amount)), - "depreciation_entry": d.voucher_no - }) + row = frappe._dict(asset_data) + row.update({ + "depreciation_amount": d.debit, + "depreciation_date": d.posting_date, + "amount_after_depreciation": (flt(row.gross_purchase_amount) - + flt(row.accumulated_depreciation_amount)), + "depreciation_entry": d.voucher_no + }) - data.append(row) + data.append(row) return data From 81285204dc5a038a4eafc0525b4956caf821337b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 29 Dec 2020 16:25:45 +0530 Subject: [PATCH 161/295] fix: option name for the field 'Role Allowed to Create/Edit Back-dated Transactions' --- erpnext/stock/doctype/stock_settings/stock_settings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 859aea2eb60..3ff396ba77e 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -217,7 +217,7 @@ "fieldname": "role_allowed_to_create_edit_back_dated_transactions", "fieldtype": "Link", "label": "Role Allowed to Create/Edit Back-dated Transactions", - "options": "User" + "options": "Role" }, { "fieldname": "column_break_26", @@ -234,7 +234,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-11-23 22:26:54.225608", + "modified": "2020-12-29 12:53:31.162247", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", From e061004956bbe585fc0874ba2532c5ac83a4ac11 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 29 Dec 2020 17:00:39 +0530 Subject: [PATCH 162/295] fix: Improve validation message --- .../test_accounting_dimension_filter.py | 6 +++--- erpnext/accounts/doctype/gl_entry/gl_entry.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) 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 index fa700e115ce..e822c0c0171 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -17,14 +17,14 @@ class TestAccountingDimensionFilter(unittest.TestCase): def test_allowed_dimension_validation(self): si = create_sales_invoice(do_not_save=1) si.items[0].cost_center = 'Main - _TC' - si.location = 'Block 1' + si.department = 'Accounts - _TC' si.save() self.assertRaises(InvalidAccountDimensionError, si.submit) def test_mandatory_dimension_validation(self): si = create_sales_invoice(do_not_save=1) - si.location = 'Block 1' + si.department = '' # Test with no department for Sales Account si.items[0].department = '' @@ -71,7 +71,7 @@ def create_accounting_dimension_filter(): }], 'dimensions': [{ 'accounting_dimension': 'Department', - 'dimension_value': '_Test Department - _TC' + 'dimension_value': 'Accounts - _TC' }] }).insert() else: diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 329d6e5aa75..e27bf5e2b35 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -107,12 +107,12 @@ class GLEntry(Document): 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 account {1}").format( - frappe.bold(self.get(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError) + 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 account {1}").format( - frappe.bold(self.get(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError) + 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 \ From 353f8f4d857381baf0bfa8e4b50e698b49f59da9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 29 Dec 2020 22:13:40 +0530 Subject: [PATCH 163/295] fix: Add test case for YTD --- ...test_employee_tax_exemption_declaration.py | 16 ++-- .../doctype/salary_slip/salary_slip.py | 49 ++++++------ .../doctype/salary_slip/test_salary_slip.py | 76 ++++++++++++++----- .../salary_structure/test_salary_structure.py | 18 +++-- 4 files changed, 106 insertions(+), 53 deletions(-) diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py index 0609d191497..311f3527f6e 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py @@ -86,19 +86,21 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): self.assertEqual(declaration.total_exemption_amount, 100000) -def create_payroll_period(): - if not frappe.db.exists("Payroll Period", "_Test Payroll Period"): +def create_payroll_period(**args): + args = frappe._dict(args) + name = args.name or "_Test Payroll Period" + if not frappe.db.exists("Payroll Period", name): from datetime import date payroll_period = frappe.get_doc(dict( doctype = 'Payroll Period', - name = "_Test Payroll Period", - company = erpnext.get_default_company(), - start_date = date(date.today().year, 1, 1), - end_date = date(date.today().year, 12, 31) + name = name, + company = args.company or erpnext.get_default_company(), + start_date = args.start_date or date(date.today().year, 1, 1), + end_date = args.end_date or date(date.today().year, 12, 31) )).insert() return payroll_period else: - return frappe.get_doc("Payroll Period", "_Test Payroll Period") + return frappe.get_doc("Payroll Period", name) def create_exemption_category(): if not frappe.db.exists("Employee Tax Exemption Category", "_Test Category"): diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 02e5f2d1d12..9f46d50e588 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -18,6 +18,7 @@ from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_fac from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount from erpnext.payroll.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry +from erpnext.accounts.utils import get_fiscal_year class SalarySlip(TransactionBase): def __init__(self, *args, **kwargs): @@ -1129,21 +1130,25 @@ class SalarySlip(TransactionBase): def compute_year_to_date(self): year_to_date = 0 - payroll_period = frappe.get_list('Payroll Period', - fields = ['start_date','end_date','company'], - filters= {'start_date' : ['<=', self.start_date], - 'end_date' : ['>=', self.end_date], - 'company' : self.company - })[0] - salary_slips_from_current_payroll_period = frappe.get_list('Salary Slip', - fields = ['employee_name', 'start_date', 'end_date', 'net_pay'], - filters = {'employee_name' : self.employee_name, - 'start_date' : ['>=', payroll_period.start_date], - 'end_date' : ['<', self.start_date] - }) + payroll_period = get_payroll_period(self.start_date, self.end_date, self.company) - for salary_slip in salary_slips_from_current_payroll_period: - year_to_date += salary_slip.net_pay + 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 + + salary_slip_sum = frappe.get_list('Salary Slip', + fields = ['sum(net_pay) as sum'], + filters = {'employee_name' : self.employee_name, + 'start_date' : ['>=', period_start_date], + 'end_date' : ['<', period_end_date]}) + + + year_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0 year_to_date += self.net_pay self.year_to_date = year_to_date @@ -1151,14 +1156,14 @@ class SalarySlip(TransactionBase): def compute_month_to_date(self): month_to_date = 0 first_day_of_the_month = get_first_day(self.start_date) - salary_slips_from_this_month = frappe.get_list('Salary Slip', - fields = ['employee_name', 'start_date', 'net_pay'], - filters = {'employee_name' : self.employee_name, - 'start_date' : ['>=', first_day_of_the_month], - 'end_date' : ['<', self.start_date] - }) - for salary_slip in salary_slips_from_this_month: - month_to_date += salary_slip.net_pay + salary_slip_sum = frappe.get_list('Salary Slip', + fields = ['sum(net_pay) as sum'], + filters = {'employee_name' : self.employee_name, + 'start_date' : ['>=', first_day_of_the_month], + 'end_date' : ['<', self.start_date] + }) + + year_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0 month_to_date += self.net_pay self.month_to_date = month_to_date diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 5daf1d439d1..687d3602a97 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -290,6 +290,35 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(salary_slip.gross_pay, 78000) self.assertEqual(salary_slip.base_gross_pay, 78000*70) + def test_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", + start_date=getdate("2019-04-01"), end_date=getdate("2020-03-31")) + + create_tax_slab(payroll_period, allow_tax_exemption=True, currency="INR", effective_date=getdate("2019-04-01")) + + 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) + + salary_slips = frappe.get_all('Salary Slip', fields=['year_to_date'], filters={'employee_name': + 'test_ytd@salary.com'}, order_by = 'posting_date') + + net_pay = 70026.00 + month = 1 + for slip in salary_slips: + year_to_date = month * net_pay + self.assertEqual(slip.year_to_date, year_to_date) + month += 1 + def test_tax_for_payroll_period(self): data = {} # test the impact of tax exemption declaration, tax exemption proof submission @@ -631,8 +660,13 @@ def create_benefit_claim(employee, payroll_period, amount, component): }).submit() return claim_date -def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=erpnext.get_default_currency()): - frappe.db.sql("""delete from `tabIncome Tax Slab`""") +def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=None): + # frappe.db.sql("""delete from `tabIncome Tax Slab`""") + + print(payroll_period.name, effective_date) + + if not currency: + currency = erpnext.get_default_currency() slabs = [ { @@ -652,26 +686,32 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = } ] - income_tax_slab = frappe.new_doc("Income Tax Slab") - income_tax_slab.name = "Tax Slab: " + payroll_period.name - income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2) - income_tax_slab.currency = currency + income_tax_slab_name = frappe.db.get_value("Income Tax Slab", "Tax Slab: " + payroll_period.name) + if not income_tax_slab_name: + income_tax_slab = frappe.new_doc("Income Tax Slab") + income_tax_slab.name = "Tax Slab: " + payroll_period.name + income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2) + income_tax_slab.currency = currency - if allow_tax_exemption: - income_tax_slab.allow_tax_exemption = 1 - income_tax_slab.standard_tax_exemption_amount = 50000 + if allow_tax_exemption: + income_tax_slab.allow_tax_exemption = 1 + income_tax_slab.standard_tax_exemption_amount = 50000 - for item in slabs: - income_tax_slab.append("slabs", item) + for item in slabs: + income_tax_slab.append("slabs", item) - income_tax_slab.append("other_taxes_and_charges", { - "description": "cess", - "percent": 4 - }) + income_tax_slab.append("other_taxes_and_charges", { + "description": "cess", + "percent": 4 + }) - income_tax_slab.save() - if not dont_submit: - income_tax_slab.submit() + income_tax_slab.save() + if not dont_submit: + income_tax_slab.submit() + + return income_tax_slab.name + else: + return income_tax_slab_name def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True): deducted_dates = [] diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index abb669740b6..e1c6a008aa0 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -114,7 +114,7 @@ class TestSalaryStructure(unittest.TestCase): self.assertEqual(sal_struct.currency, 'USD') def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None, - test_tax=False, company=None, currency=erpnext.get_default_currency()): + test_tax=False, company=None, currency=erpnext.get_default_currency(), payroll_period=None): if test_tax: frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure)) @@ -141,16 +141,22 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do if employee and not frappe.db.get_value("Salary Structure Assignment", {'employee':employee, 'docstatus': 1}) and salary_structure_doc.docstatus==1: - create_salary_structure_assignment(employee, salary_structure, company=company, currency=currency) + create_salary_structure_assignment(employee, salary_structure, company=company, currency=currency, + payroll_period=payroll_period) return salary_structure_doc -def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None, currency=erpnext.get_default_currency()): +def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None, currency=erpnext.get_default_currency(), + payroll_period=None): + if frappe.db.exists("Salary Structure Assignment", {"employee": employee}): frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""",(employee)) - payroll_period = create_payroll_period() - create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency) + if not payroll_period: + payroll_period = create_payroll_period() + income_tax_slab = create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency) + else: + income_tax_slab = frappe.db.get_value('Income Tax Slab', "Tax Slab: " + payroll_period.name) salary_structure_assignment = frappe.new_doc("Salary Structure Assignment") salary_structure_assignment.employee = employee @@ -162,7 +168,7 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non salary_structure_assignment.payroll_payable_account = get_payable_account(company) salary_structure_assignment.company = company or erpnext.get_default_company() salary_structure_assignment.save(ignore_permissions=True) - salary_structure_assignment.income_tax_slab = "Tax Slab: _Test Payroll Period" + salary_structure_assignment.income_tax_slab = income_tax_slab salary_structure_assignment.submit() return salary_structure_assignment From 20133bd1dfc7fa983e78af18ea827bfee7c2e86d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 29 Dec 2020 22:19:12 +0530 Subject: [PATCH 164/295] fix: Remove comments --- erpnext/payroll/doctype/salary_slip/test_salary_slip.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 687d3602a97..d1eaae74900 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -661,10 +661,6 @@ def create_benefit_claim(employee, payroll_period, amount, component): return claim_date def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=None): - # frappe.db.sql("""delete from `tabIncome Tax Slab`""") - - print(payroll_period.name, effective_date) - if not currency: currency = erpnext.get_default_currency() From 8b7ebe5044b823ccad309f9cd241bd335af93cba Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 30 Dec 2020 12:45:46 +0530 Subject: [PATCH 165/295] fix: Test Case cleanup and fixes --- .../loan_management/doctype/loan/test_loan.py | 2 +- .../income_tax_slab/income_tax_slab.py | 7 +++-- .../doctype/salary_slip/test_salary_slip.py | 30 +++++++++++-------- .../salary_structure/test_salary_structure.py | 5 ++-- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index a63d06590f8..1d831aeb644 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -45,7 +45,7 @@ class TestLoan(unittest.TestCase): create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) self.applicant1 = make_employee("robert_loan@loan.com") - make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant1, currency='INR') + make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant1, currency='INR', company="_Test Company") if not frappe.db.exists("Customer", "_Test Loan Customer"): frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) diff --git a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py index 253f023f68b..e2b22a6ce96 100644 --- a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py +++ b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py @@ -3,8 +3,11 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe +import erpnext from frappe.model.document import Document class IncomeTaxSlab(Document): - pass + def validate(self): + if self.company: + self.currency = erpnext.get_company_currency(self.company) diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index d1eaae74900..9e3e707f39d 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -240,7 +240,12 @@ class TestSalarySlip(unittest.TestCase): interest_income_account='Interest Income Account - _TC', penalty_income_account='Penalty Income Account - _TC') - make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR') + payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company", + start_date=getdate("2019-04-01"), end_date=getdate("2020-03-31")) + + make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR', + payroll_period=payroll_period) + frappe.db.sql("""delete from `tabLoan""") loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1)) loan.repay_from_salary = 1 @@ -298,7 +303,8 @@ class TestSalarySlip(unittest.TestCase): payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company", start_date=getdate("2019-04-01"), end_date=getdate("2020-03-31")) - create_tax_slab(payroll_period, allow_tax_exemption=True, currency="INR", effective_date=getdate("2019-04-01")) + 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) @@ -309,15 +315,13 @@ class TestSalarySlip(unittest.TestCase): create_salary_slips_for_payroll_period(applicant, salary_structure.name, payroll_period, deduct_random=False) - salary_slips = frappe.get_all('Salary Slip', fields=['year_to_date'], filters={'employee_name': + salary_slips = frappe.get_all('Salary Slip', fields=['year_to_date', 'net_pay'], filters={'employee_name': 'test_ytd@salary.com'}, order_by = 'posting_date') - net_pay = 70026.00 - month = 1 + year_to_date = 0 for slip in salary_slips: - year_to_date = month * net_pay + year_to_date += slip.net_pay self.assertEqual(slip.year_to_date, year_to_date) - month += 1 def test_tax_for_payroll_period(self): data = {} @@ -439,10 +443,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip" employee = frappe.db.get_value("Employee", {"user_id": user}) - if not frappe.db.exists('Salary Structure', salary_structure): - salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee) - else: - salary_structure_doc = frappe.get_doc('Salary Structure', salary_structure) + salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee) salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) if not salary_slip_name: @@ -660,7 +661,8 @@ def create_benefit_claim(employee, payroll_period, amount, component): }).submit() return claim_date -def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=None): +def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=None, + company=None): if not currency: currency = erpnext.get_default_currency() @@ -687,6 +689,10 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = income_tax_slab = frappe.new_doc("Income Tax Slab") income_tax_slab.name = "Tax Slab: " + payroll_period.name income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2) + + if company: + income_tax_slab.company = company + income_tax_slab.currency = currency if allow_tax_exemption: diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index e1c6a008aa0..2b249c7a197 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -154,9 +154,8 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non if not payroll_period: payroll_period = create_payroll_period() - income_tax_slab = create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency) - else: - income_tax_slab = frappe.db.get_value('Income Tax Slab', "Tax Slab: " + payroll_period.name) + + income_tax_slab = create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency) salary_structure_assignment = frappe.new_doc("Salary Structure Assignment") salary_structure_assignment.employee = employee From 97d055dfc34ba987f538a1f794bcc9e6eb97e87e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 30 Dec 2020 14:06:13 +0530 Subject: [PATCH 166/295] fix: Test Case --- erpnext/payroll/doctype/salary_slip/salary_slip.py | 2 +- .../doctype/salary_structure/test_salary_structure.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 9f46d50e588..99d8a8317cd 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -1163,7 +1163,7 @@ class SalarySlip(TransactionBase): 'end_date' : ['<', self.start_date] }) - year_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0 + month_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0 month_to_date += self.net_pay self.month_to_date = month_to_date diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index 2b249c7a197..f2fb558a14b 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -155,7 +155,10 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non if not payroll_period: payroll_period = create_payroll_period() - income_tax_slab = create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency) + income_tax_slab = frappe.db.get_value("Income Tax Slab", {"currency": currency}) + + if not income_tax_slab: + income_tax_slab = create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency) salary_structure_assignment = frappe.new_doc("Salary Structure Assignment") salary_structure_assignment.employee = employee From f946bbd03294d170d8b1093994fe2e174f381c04 Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 30 Dec 2020 14:43:42 +0530 Subject: [PATCH 167/295] fix: minor e-invoicing issues (#24239) * fix: invoice value set to zero if rounded total disabled * fix: unit rate & gross amount calculation * fix: e-invoice-setup patch * chore: no need to re-run the patch * fix: item value & invoice value calculations --- .../sales_invoice/test_sales_invoice.py | 63 +++++++++++++----- erpnext/patches.txt | 1 + .../patches/v12_0/setup_einvoice_fields.py | 1 + .../india/e_invoice/einv_template.json | 2 +- erpnext/regional/india/e_invoice/utils.py | 66 +++++++++---------- 5 files changed, 81 insertions(+), 52 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 3c681eeecf2..eb223ee42ca 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1885,8 +1885,8 @@ class TestSalesInvoice(unittest.TestCase): "item_code": "_Test Item", "uom": "Nos", "warehouse": "_Test Warehouse - _TC", - "qty": 2, - "rate": 100, + "qty": 2000, + "rate": 12, "income_account": "Sales - _TC", "expense_account": "Cost of Goods Sold - _TC", "cost_center": "_Test Cost Center - _TC", @@ -1895,31 +1895,52 @@ class TestSalesInvoice(unittest.TestCase): "item_code": "_Test Item 2", "uom": "Nos", "warehouse": "_Test Warehouse - _TC", - "qty": 4, - "rate": 150, + "qty": 420, + "rate": 15, "income_account": "Sales - _TC", "expense_account": "Cost of Goods Sold - _TC", "cost_center": "_Test Cost Center - _TC", }) + si.discount_amount = 100 si.save() einvoice = make_einvoice(si) - total_item_ass_value = sum([d['AssAmt'] for d in einvoice['ItemList']]) - total_item_cgst_value = sum([d['CgstAmt'] for d in einvoice['ItemList']]) - total_item_sgst_value = sum([d['SgstAmt'] for d in einvoice['ItemList']]) - total_item_igst_value = sum([d['IgstAmt'] for d in einvoice['ItemList']]) - total_item_value = sum([d['TotItemVal'] for d in einvoice['ItemList']]) + total_item_ass_value = 0 + total_item_cgst_value = 0 + total_item_sgst_value = 0 + total_item_igst_value = 0 + total_item_value = 0 + + for item in einvoice['ItemList']: + total_item_ass_value += item['AssAmt'] + total_item_cgst_value += item['CgstAmt'] + total_item_sgst_value += item['SgstAmt'] + total_item_igst_value += item['IgstAmt'] + total_item_value += item['TotItemVal'] + + self.assertTrue(item['AssAmt'], item['TotAmt'] - item['Discount']) + self.assertTrue(item['TotItemVal'], item['AssAmt'] + item['CgstAmt'] + item['SgstAmt'] + item['IgstAmt']) + + value_details = einvoice['ValDtls'] self.assertEqual(einvoice['Version'], '1.1') - self.assertEqual(einvoice['ValDtls']['AssVal'], total_item_ass_value) - self.assertEqual(einvoice['ValDtls']['CgstVal'], total_item_cgst_value) - self.assertEqual(einvoice['ValDtls']['SgstVal'], total_item_sgst_value) - self.assertEqual(einvoice['ValDtls']['IgstVal'], total_item_igst_value) - self.assertEqual(einvoice['ValDtls']['TotInvVal'], total_item_value) + self.assertEqual(value_details['AssVal'], total_item_ass_value) + self.assertEqual(value_details['CgstVal'], total_item_cgst_value) + 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'] + + value_details['OthChrg'] - value_details['Discount'] + ) + + self.assertEqual(value_details['TotInvVal'], si.base_grand_total) self.assertTrue(einvoice['EwbDtls']) -def make_sales_invoice_for_ewaybill(): +def make_test_address_for_ewaybill(): if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'): address = frappe.get_doc({ "address_line1": "_Test Address Line 1", @@ -1967,7 +1988,8 @@ def make_sales_invoice_for_ewaybill(): }) address.save() - + +def make_test_transporter_for_ewaybill(): if not frappe.db.exists('Supplier', '_Test Transporter'): frappe.get_doc({ "doctype": "Supplier", @@ -1978,12 +2000,17 @@ def make_sales_invoice_for_ewaybill(): "is_transporter": 1 }).insert() +def make_sales_invoice_for_ewaybill(): + make_test_address_for_ewaybill() + make_test_transporter_for_ewaybill() + gst_settings = frappe.get_doc("GST Settings") gst_account = frappe.get_all( "GST Account", fields=["cgst_account", "sgst_account", "igst_account"], - filters = {"company": "_Test Company"}) + filters = {"company": "_Test Company"} + ) if not gst_account: gst_settings.append("gst_accounts", { @@ -1995,7 +2022,7 @@ def make_sales_invoice_for_ewaybill(): gst_settings.save() - si = create_sales_invoice(do_not_save =1, rate = '60000') + si = create_sales_invoice(do_not_save=1, rate='60000') si.distance = 2000 si.company_address = "_Test Address for Eway bill-Billing" diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d69dabf15cd..f2e4f72d673 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -711,6 +711,7 @@ erpnext.patches.v13_0.delete_old_sales_reports execute:frappe.delete_doc_if_exists("DocType", "Bank Reconciliation") erpnext.patches.v13_0.move_doctype_reports_and_notification_from_hr_to_payroll #22-06-2020 erpnext.patches.v13_0.move_payroll_setting_separately_from_hr_settings #22-06-2020 +execute:frappe.reload_doc("regional", "doctype", "e_invoice_settings") erpnext.patches.v13_0.check_is_income_tax_component #22-06-2020 erpnext.patches.v13_0.loyalty_points_entry_for_pos_invoice #22-07-2020 erpnext.patches.v12_0.add_taxjar_integration_field diff --git a/erpnext/patches/v12_0/setup_einvoice_fields.py b/erpnext/patches/v12_0/setup_einvoice_fields.py index d0782765dee..2474bc3b82c 100644 --- a/erpnext/patches/v12_0/setup_einvoice_fields.py +++ b/erpnext/patches/v12_0/setup_einvoice_fields.py @@ -8,6 +8,7 @@ def execute(): if not company: return + frappe.reload_doc("custom", "doctype", "custom_field") frappe.reload_doc("regional", "doctype", "e_invoice_settings") custom_fields = { 'Sales Invoice': [ diff --git a/erpnext/regional/india/e_invoice/einv_template.json b/erpnext/regional/india/e_invoice/einv_template.json index e5751da5612..60f490d6166 100644 --- a/erpnext/regional/india/e_invoice/einv_template.json +++ b/erpnext/regional/india/e_invoice/einv_template.json @@ -59,7 +59,7 @@ {item_list} ], "ValDtls": {{ - "AssVal": "{invoice_value_details.base_net_total}", + "AssVal": "{invoice_value_details.base_total}", "CgstVal": "{invoice_value_details.total_cgst_amt}", "SgstVal": "{invoice_value_details.total_sgst_amt}", "IgstVal": "{invoice_value_details.total_igst_amt}", diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index cb92c42464e..db966100430 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -146,12 +146,12 @@ def get_item_list(invoice): item.update(d.as_dict()) item.sr_no = d.idx - item.qty = abs(item.qty) - item.description = d.item_name - item.taxable_value = abs(item.base_net_amount) item.discount_amount = abs(item.discount_amount * item.qty) - item.unit_rate = abs(item.base_price_list_rate) if item.discount_amount else abs(item.base_net_rate) - item.gross_amount = abs(item.unit_rate * item.qty) + item.description = d.item_name + item.qty = abs(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.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 @@ -180,35 +180,35 @@ def update_item_taxes(invoice, item): item[attr] = 0 for t in invoice.taxes: + # this contains item wise tax rate & tax amount (incl. discount) item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code) 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 + if t.account_head in gst_accounts.cess_account: + item_tax_amount_after_discount = item_tax_detail[1] if t.charge_type == 'On Item Quantity': - item.cess_nadv_amount += abs(item_tax_detail[1]) + item.cess_nadv_amount += abs(item_tax_amount_after_discount) else: - item.cess_rate += item_tax_detail[0] - item.cess_amount += abs(item_tax_detail[1]) - elif t.account_head in gst_accounts.igst_account: - item.tax_rate += item_tax_detail[0] - item.igst_amount += abs(item_tax_detail[1]) - elif t.account_head in gst_accounts.sgst_account: - item.tax_rate += item_tax_detail[0] - item.sgst_amount += abs(item_tax_detail[1]) - elif t.account_head in gst_accounts.cgst_account: - item.tax_rate += item_tax_detail[0] - item.cgst_amount += abs(item_tax_detail[1]) - + item.cess_rate += item_tax_rate + item.cess_amount += abs(item_tax_amount_after_discount) + + for tax_type in ['igst', 'cgst', 'sgst']: + if t.account_head in gst_accounts[f'{tax_type}_account']: + item.tax_rate += item_tax_rate + item[f'{tax_type}_amount'] += abs(item_tax_amount) + return item def get_invoice_value_details(invoice): invoice_value_details = frappe._dict(dict()) - invoice_value_details.base_net_total = abs(invoice.base_net_total) - invoice_value_details.invoice_discount_amt = invoice.discount_amount if invoice.discount_amount and invoice.discount_amount > 0 else 0 - # discount amount cannnot be -ve in an e-invoice, so if -ve include discount in round_off - invoice_value_details.round_off = invoice.rounding_adjustment - (invoice.discount_amount if invoice.discount_amount and invoice.discount_amount < 0 else 0) - disable_rounded = frappe.db.get_single_value('Global Defaults', 'disable_rounded_total') - invoice_value_details.base_grand_total = abs(invoice.base_grand_total) if disable_rounded else abs(invoice.base_rounded_total) - invoice_value_details.grand_total = abs(invoice.grand_total) if disable_rounded else abs(invoice.rounded_total) + invoice_value_details.base_total = abs(invoice.base_total) + invoice_value_details.invoice_discount_amt = invoice.discount_amount + invoice_value_details.round_off = invoice.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) invoice_value_details = update_invoice_taxes(invoice, invoice_value_details) @@ -226,15 +226,14 @@ def update_invoice_taxes(invoice, invoice_value_details): for t in invoice.taxes: if t.account_head in gst_accounts_list: if t.account_head in gst_accounts.cess_account: + # using after discount amt since item also uses after discount amt for cess calc invoice_value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount) - elif t.account_head in gst_accounts.igst_account: - invoice_value_details.total_igst_amt += abs(t.base_tax_amount_after_discount_amount) - elif t.account_head in gst_accounts.sgst_account: - invoice_value_details.total_sgst_amt += abs(t.base_tax_amount_after_discount_amount) - elif t.account_head in gst_accounts.cgst_account: - invoice_value_details.total_cgst_amt += abs(t.base_tax_amount_after_discount_amount) + + 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) else: - invoice_value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount) + invoice_value_details.total_other_charges += abs(t.base_tax_amount) return invoice_value_details @@ -358,7 +357,8 @@ def validate_einvoice(validations, einvoice, errors=[]): einvoice[fieldname] = str(value) elif value_type == 'number': is_integer = '.' not in str(field_validation.get('maximum')) - einvoice[fieldname] = flt(value, 2) if not is_integer else cint(value) + precision = 3 if '.999' in str(field_validation.get('maximum')) else 2 + einvoice[fieldname] = flt(value, precision) if not is_integer else cint(value) value = einvoice[fieldname] max_length = field_validation.get('maxLength') From 66dcaf3ab2cda4663317827e20ca87dc1fa3181b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 30 Dec 2020 15:47:22 +0530 Subject: [PATCH 168/295] fix: Salary structure assignment in tests --- .../income_tax_slab/income_tax_slab.py | 2 +- .../doctype/salary_slip/test_salary_slip.py | 20 +++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py index e2b22a6ce96..81e364778ca 100644 --- a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py +++ b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py @@ -3,7 +3,7 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe +#import frappe import erpnext from frappe.model.document import Document diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 9e3e707f39d..bb310c4d873 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -9,7 +9,7 @@ import calendar import random from erpnext.accounts.utils import get_fiscal_year from frappe.utils.make_random import get_random -from frappe.utils import getdate, nowdate, add_days, add_months, flt, get_first_day, get_last_day +from frappe.utils import getdate, nowdate, add_days, add_months, flt, get_first_day, get_last_day, cstr from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_month_details from erpnext.hr.doctype.employee.test_employee import make_employee @@ -240,8 +240,7 @@ class TestSalarySlip(unittest.TestCase): interest_income_account='Interest Income Account - _TC', penalty_income_account='Penalty Income Account - _TC') - payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company", - start_date=getdate("2019-04-01"), end_date=getdate("2020-03-31")) + payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR', payroll_period=payroll_period) @@ -300,8 +299,7 @@ class TestSalarySlip(unittest.TestCase): applicant = make_employee("test_ytd@salary.com", company="_Test Company") - payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company", - start_date=getdate("2019-04-01"), end_date=getdate("2020-03-31")) + 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") @@ -666,6 +664,9 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = if not currency: currency = erpnext.get_default_currency() + if company: + currency = erpnext.get_company_currency(company) + slabs = [ { "from_amount": 250000, @@ -684,15 +685,12 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = } ] - income_tax_slab_name = frappe.db.get_value("Income Tax Slab", "Tax Slab: " + payroll_period.name) + income_tax_slab_name = frappe.db.get_value("Income Tax Slab", {"currency": currency}) if not income_tax_slab_name: income_tax_slab = frappe.new_doc("Income Tax Slab") - income_tax_slab.name = "Tax Slab: " + payroll_period.name + income_tax_slab.name = "Tax Slab: " + payroll_period.name + " " + cstr(currency) income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2) - - if company: - income_tax_slab.company = company - + income_tax_slab.company = company or '' income_tax_slab.currency = currency if allow_tax_exemption: From aa44c754de3f740b79e1fb3c686f7ce99fd6ea85 Mon Sep 17 00:00:00 2001 From: Karthikeyan S Date: Wed, 30 Dec 2020 19:22:37 +0530 Subject: [PATCH 169/295] fix(GST E Invoice): update live URLs for adaequare GSP (#24248) --- erpnext/regional/india/e_invoice/utils.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index db966100430..102a2f0f568 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -386,15 +386,15 @@ class GSPConnector(): self.invoice = frappe.get_cached_doc(doctype, docname) if doctype and docname else None self.credentials = self.get_credentials() - self.base_url = 'https://gsp.adaequare.com/' - self.authenticate_url = self.base_url + 'gsp/authenticate?grant_type=token' - self.gstin_details_url = self.base_url + 'test/enriched/ei/api/master/gstin' - self.generate_irn_url = self.base_url + 'test/enriched/ei/api/invoice' - self.irn_details_url = self.base_url + 'test/enriched/ei/api/invoice/irn' - self.cancel_irn_url = self.base_url + 'test/enriched/ei/api/invoice/cancel' - self.cancel_ewaybill_url = self.base_url + '/test/enriched/ei/api/ewayapi' - self.generate_ewaybill_url = self.base_url + 'test/enriched/ei/api/ewaybill' - + self.base_url = 'https://gsp.adaequare.com' + self.authenticate_url = self.base_url + '/gsp/authenticate?grant_type=token' + self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin' + self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice' + self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn' + self.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel' + self.cancel_ewaybill_url = self.base_url + '/enriched/ei/api/ewayapi' + self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill' + def get_credentials(self): if self.invoice: gstin = self.get_seller_gstin() From a60707873c8c976a99f83cca001d9820a1777418 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 30 Dec 2020 20:24:15 +0530 Subject: [PATCH 170/295] fix: Partial loan security unpledging --- erpnext/loan_management/doctype/loan/loan.py | 6 +-- .../loan_management/doctype/loan/test_loan.py | 37 +++++++++++++++++++ .../loan_security_shortfall.py | 1 - .../loan_security_unpledge.py | 16 +++++--- 4 files changed, 51 insertions(+), 9 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index cd40a665d43..578c7f116ac 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -280,10 +280,10 @@ def make_loan_write_off(loan, company=None, posting_date=None, amount=0, as_dict return write_off @frappe.whitelist() -def unpledge_security(loan=None, loan_security_pledge=None, as_dict=0, save=0, submit=0, approve=0): - # if loan is passed it will be considered as full unpledge +def unpledge_security(loan=None, loan_security_pledge=None, security_map=None, as_dict=0, save=0, submit=0, approve=0): + # if no security_map is passed it will be considered as full unpledge if loan: - pledge_qty_map = get_pledged_security_qty(loan) + pledge_qty_map = security_map or get_pledged_security_qty(loan) loan_doc = frappe.get_doc('Loan', loan) unpledge_request = create_loan_security_unpledge(pledge_qty_map, loan_doc.name, loan_doc.company, loan_doc.applicant_type, loan_doc.applicant) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index a63d06590f8..2b9c7c9d0a8 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -325,6 +325,43 @@ class TestLoan(unittest.TestCase): self.assertEquals(amounts['payable_principal_amount'], 0.0) self.assertEqual(amounts['interest_amount'], 0) + def test_partial_loan_security_unpledge(self): + pledge = [{ + "loan_security": "Test Security 1", + "qty": 2000.00 + }, + { + "loan_security": "Test Security 2", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + create_pledge(loan_application) + + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), 600000) + repayment_entry.submit() + + unpledge_map = {'Test Security 2': 2000} + + unpledge_request = unpledge_security(loan=loan.name, security_map = unpledge_map, save=1) + unpledge_request.submit() + unpledge_request.status = 'Approved' + unpledge_request.save() + unpledge_request.submit() + unpledge_request.load_from_db() + self.assertEqual(unpledge_request.docstatus, 1) + def test_disbursal_check_with_shortfall(self): pledges = [{ "loan_security": "Test Security 2", diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py index 8ec0bfb62c0..64698068842 100644 --- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py @@ -81,7 +81,6 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): process_loan_security_shortfall) def create_loan_security_shortfall(loan, loan_amount, security_value, shortfall_amount, process_loan_security_shortfall): - existing_shortfall = frappe.db.get_value("Loan Security Shortfall", {"loan": loan, "status": "Pending"}, "name") if existing_shortfall: diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index c29f325bfc9..61c418d3d31 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -30,6 +30,8 @@ class LoanSecurityUnpledge(Document): d.idx, frappe.bold(d.loan_security))) def validate_unpledge_qty(self): + from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import get_ltv_ratio + pledge_qty_map = get_pledged_security_qty(self.loan) ltv_ratio_map = frappe._dict(frappe.get_all("Loan Security Type", @@ -47,6 +49,8 @@ class LoanSecurityUnpledge(Document): pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount) security_value = 0 + unpledge_qty_map = {} + ltv_ratio = 0 for security in self.securities: pledged_qty = pledge_qty_map.get(security.loan_security, 0) @@ -57,13 +61,15 @@ class LoanSecurityUnpledge(Document): msg += _("You are trying to unpledge more.") frappe.throw(msg, title=_("Loan Security Unpledge Error")) - qty_after_unpledge = pledged_qty - security.qty - ltv_ratio = ltv_ratio_map.get(security.loan_security_type) + unpledge_qty_map.setdefault(security.loan_security, 0) + unpledge_qty_map[security.loan_security] += security.qty - current_price = loan_security_price_map.get(security.loan_security) - if not current_price: - frappe.throw(_("No valid Loan Security Price found for {0}").format(frappe.bold(security.loan_security))) + for security in pledge_qty_map: + if not ltv_ratio: + ltv_ratio = get_ltv_ratio(security) + qty_after_unpledge = pledge_qty_map.get(security, 0) - unpledge_qty_map.get(security, 0) + current_price = loan_security_price_map.get(security) security_value += qty_after_unpledge * current_price if not security_value and flt(pending_principal_amount, 2) > 0: From 97162166325c7cf004b7d4dd5e6d1a0c8ae628e0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 30 Dec 2020 20:33:35 +0530 Subject: [PATCH 171/295] fix: check for string types --- erpnext/loan_management/doctype/loan/loan.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index 578c7f116ac..2e0a4d13ab2 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import frappe, math, json import erpnext from frappe import _ +from six import string_types from frappe.utils import flt, rounded, add_months, nowdate, getdate, now_datetime from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.controllers.accounts_controller import AccountsController @@ -282,6 +283,9 @@ def make_loan_write_off(loan, company=None, posting_date=None, amount=0, as_dict @frappe.whitelist() def unpledge_security(loan=None, loan_security_pledge=None, security_map=None, as_dict=0, save=0, submit=0, approve=0): # if no security_map is passed it will be considered as full unpledge + if security_map and isinstance(security_map, string_types): + security_map = json.loads(security_map) + if loan: pledge_qty_map = security_map or get_pledged_security_qty(loan) loan_doc = frappe.get_doc('Loan', loan) From 23ab5c5cc0bfac56260acc2be982e594eaaeaffe Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 31 Dec 2020 11:29:06 +0530 Subject: [PATCH 172/295] fix: Multiplle sider issues --- erpnext/accounts/doctype/budget/budget.js | 4 ++-- erpnext/education/doctype/fees/fees.js | 4 ++-- erpnext/hr/doctype/expense_claim/expense_claim.js | 2 +- erpnext/public/js/queries.js | 2 +- erpnext/public/js/utils.js | 2 +- erpnext/public/js/utils/dimension_tree_filter.js | 8 ++++---- .../doctype/stock_reconciliation/stock_reconciliation.js | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/budget/budget.js b/erpnext/accounts/doctype/budget/budget.js index e60bc60475e..e162e3222d3 100644 --- a/erpnext/accounts/doctype/budget/budget.js +++ b/erpnext/accounts/doctype/budget/budget.js @@ -11,7 +11,7 @@ frappe.ui.form.on('Budget', { report_type: "Profit and Loss", is_group: 0 } - } + }; }); frm.set_query("monthly_distribution", function() { @@ -19,7 +19,7 @@ frappe.ui.form.on('Budget', { filters: { fiscal_year: frm.doc.fiscal_year } - } + }; }); erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); diff --git a/erpnext/education/doctype/fees/fees.js b/erpnext/education/doctype/fees/fees.js index 433bd64d2fb..40f50999adf 100644 --- a/erpnext/education/doctype/fees/fees.js +++ b/erpnext/education/doctype/fees/fees.js @@ -15,9 +15,9 @@ frappe.ui.form.on("Fees", { }, onload: function(frm){ - frm.set_query("academic_term",function(){ + frm.set_query("academic_term",function() { return{ - "filters":{ + "filters": { "academic_year": (frm.doc.academic_year) } }; diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index e399b22f90f..629341ff2a5 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -16,7 +16,7 @@ frappe.ui.form.on('Expense Claim', { frappe.ui.form.on('Expense Claim Detail', { expense_type: function(frm, cdt, cdn) { var d = locals[cdt][cdn]; - if(!frm.doc.company) { + if (!frm.doc.company) { d.expense_type = ""; frappe.msgprint(__("Please set the Company")); this.frm.refresh_fields(); diff --git a/erpnext/public/js/queries.js b/erpnext/public/js/queries.js index 98f1b504ccc..b635adcd443 100644 --- a/erpnext/public/js/queries.js +++ b/erpnext/public/js/queries.js @@ -115,7 +115,7 @@ $.extend(erpnext.queries, { ["Warehouse", "is_group", "=",0] ] - } + }; }, get_filtered_dimensions: function(doc, child_fields, dimension, company) { diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 2635d47f886..c39609bd389 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -202,7 +202,7 @@ $.extend(erpnext.utils, { let found = filters.some(el => el.fieldname === dimension['fieldname']); if (!found) { - filters.splice(index, 0 ,{ + filters.splice(index, 0, { "fieldname": dimension["fieldname"], "label": __(dimension["label"]), "fieldtype": "Link", diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js index c79736d0e19..319cbd2b5d5 100644 --- a/erpnext/public/js/utils/dimension_tree_filter.js +++ b/erpnext/public/js/utils/dimension_tree_filter.js @@ -69,13 +69,13 @@ erpnext.accounts.dimensions = { 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 + 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 (default_dimension) { if (frappe.meta.has_field(doctype, dimension['fieldname'])) { frm.set_value(dimension['fieldname'], default_dimension); } @@ -98,4 +98,4 @@ erpnext.accounts.dimensions = { }); } } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index be9404d9c8c..ac4ed5e75d9 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -2,7 +2,7 @@ // License: GNU General Public License v3. See license.txt frappe.provide("erpnext.stock"); -frappe.provide("erpnext.accounts.dimensions") +frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on("Stock Reconciliation", { onload: function(frm) { From d5d571ab9dca8b2853aaa551b5f82faa42f0f54c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 31 Dec 2020 12:55:35 +0530 Subject: [PATCH 173/295] fix: update old loan patch --- erpnext/patches/v13_0/update_old_loans.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py index 561e967d6df..8cf09aa6925 100644 --- a/erpnext/patches/v13_0/update_old_loans.py +++ b/erpnext/patches/v13_0/update_old_loans.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import nowdate +from frappe.utils import nowdate, flt from erpnext.accounts.doctype.account.test_account import create_account from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans from erpnext.loan_management.doctype.loan.loan import make_repayment_entry @@ -113,15 +113,15 @@ def execute(): interest_paid = 0 principal_paid = 0 - if total_interest > entry.interest_amount: - interest_paid = entry.interest_amount + if flt(total_interest) > flt(entry.interest_amount): + interest_paid = flt(entry.interest_amount) else: - interest_paid = total_interest + interest_paid = flt(total_interest) - if total_principal > entry.payable_principal_amount: - principal_paid = entry.payable_principal_amount + if flt(total_principal) > flt(entry.payable_principal_amount): + principal_paid = flt(entry.payable_principal_amount) else: - principal_paid = total_principal + principal_paid = flt(total_principal) frappe.db.sql(""" UPDATE `tabLoan Interest Accrual` SET paid_principal_amount = `paid_principal_amount` + %s, @@ -129,8 +129,8 @@ def execute(): WHERE name = %s""", (principal_paid, interest_paid, entry.name)) - total_principal -= principal_paid - total_interest -= interest_paid + total_principal = flt(total_principal) - principal_paid + total_interest = flt(total_interest) - interest_paid def create_loan_type(loan, loan_type_name, penalty_account): loan_type_doc = frappe.new_doc('Loan Type') From 2b67d57480038a9248dfd76cda736974fc14e64f Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 31 Dec 2020 13:26:05 +0530 Subject: [PATCH 174/295] fix: cannot submit e-invoice if legal name not found --- erpnext/regional/india/e_invoice/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 102a2f0f568..02ce6c14c90 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -88,7 +88,7 @@ def get_party_details(address_name): gstin = address.get('gstin') gstin_details = get_gstin_details(gstin) - legal_name = gstin_details.get('LegalName') + legal_name = gstin_details.get('LegalName') or gstin_details.get('TradeName') location = gstin_details.get('AddrLoc') or address.get('city') state_code = gstin_details.get('StateCode') pincode = gstin_details.get('AddrPncd') From 8b60fe6125265fa64cf9f3c29cb63fa61d1ef346 Mon Sep 17 00:00:00 2001 From: Afshan Date: Thu, 31 Dec 2020 16:10:39 +0530 Subject: [PATCH 175/295] fix: setting correct account for sal struct assignment if not specified. --- .../salary_structure_assignment/salary_structure_assignment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py index dccb5df1a11..a0c3013061d 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py @@ -43,7 +43,7 @@ class SalaryStructureAssignment(Document): def set_payroll_payable_account(self): if not self.payroll_payable_account: - payroll_payable_account = frappe.db.get_value('Company', self.company, 'default_payable_account') + payroll_payable_account = frappe.db.get_value('Company', self.company, 'default_payroll_payable_account') if not payroll_payable_account: payroll_payable_account = frappe.db.get_value( "Account", { From e69148c266ead416c03626ee6442af6aba556723 Mon Sep 17 00:00:00 2001 From: Afshan Date: Thu, 31 Dec 2020 16:45:00 +0530 Subject: [PATCH 176/295] fix: allow leave policy assignment to be cancelled. --- .../leave_policy_assignment/leave_policy_assignment.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json index bbb42227154..a0327bdaa0b 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json @@ -111,13 +111,14 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-12-17 16:27:20.311060", + "modified": "2020-12-31 16:43:30.695206", "modified_by": "Administrator", "module": "HR", "name": "Leave Policy Assignment", "owner": "Administrator", "permissions": [ { + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -131,6 +132,7 @@ "write": 1 }, { + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -144,6 +146,7 @@ "write": 1 }, { + "cancel": 1, "create": 1, "delete": 1, "email": 1, From 8c39ab68df1e8871be0f92e96ad5d42119e3047c Mon Sep 17 00:00:00 2001 From: vorasmit Date: Fri, 1 Jan 2021 10:54:57 +0530 Subject: [PATCH 177/295] Delete update_sales_invoice_remarks.py --- .../v12_0/update_sales_invoice_remarks.py | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 erpnext/patches/v12_0/update_sales_invoice_remarks.py diff --git a/erpnext/patches/v12_0/update_sales_invoice_remarks.py b/erpnext/patches/v12_0/update_sales_invoice_remarks.py deleted file mode 100644 index 7e8feaaca6c..00000000000 --- a/erpnext/patches/v12_0/update_sales_invoice_remarks.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import unicode_literals -import frappe - -from frappe import _ -from frappe.utils import formatdate - -def execute(): - si_list = frappe.db.get_all('Sales Invoice', filters = { - 'docstatus': 1, - 'remarks': 'No Remarks', - 'po_no' : ['!=', ''], - 'po_date' : ['!=', ''] - }, - fields = ['name', 'po_no', 'po_date'] - ) - - for doc in si_list: - remarks = _("Against Customer Order {0} dated {1}").format(doc.po_no, - formatdate(doc.po_date)) - - frappe.db.set_value('Sales Invoice', doc.name, 'remarks', remarks) - - gl_entry_list = frappe.db.get_all('GL Entry', filters = { - 'voucher_type': 'Sales Invoice', - 'remarks': 'No Remarks', - 'voucher_no' : doc.name - }, - fields = ['name'] - ) - - for entry in gl_entry_list: - frappe.db.set_value('GL Entry', entry.name, 'remarks', remarks) \ No newline at end of file From 3ef965f2531b6cb89e749a0edb18998085294eb2 Mon Sep 17 00:00:00 2001 From: vorasmit Date: Fri, 1 Jan 2021 10:55:20 +0530 Subject: [PATCH 178/295] Update patches.txt --- erpnext/patches.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 4a38cb3ab80..25be8841174 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -735,4 +735,3 @@ erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail 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.v12_0.update_sales_invoice_remarks \ No newline at end of file From a245f667d068257b15d95648d8698048ce0be661 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Sat, 2 Jan 2021 10:30:22 +0530 Subject: [PATCH 179/295] fix: pos error pop up (#24237) --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index d486ff60285..ac98dccdb5e 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -267,6 +267,8 @@ class POSInvoice(SalesInvoice): from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile if not self.pos_profile: pos_profile = get_pos_profile(self.company) or {} + if not pos_profile: + frappe.throw(_("No POS Profile found. Please create a New POS Profile first")) self.pos_profile = pos_profile.get('name') profile = {} From 7877d5a7c243b0e98be9e1f1362b8dfbd18acd2e Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 4 Jan 2021 11:10:04 +0530 Subject: [PATCH 180/295] fix: Create QI Parameters (links) in test cases --- erpnext/controllers/tests/test_item_variant.py | 3 +++ .../test_quality_inspection.py | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/tests/test_item_variant.py b/erpnext/controllers/tests/test_item_variant.py index c257215e718..813f0a00758 100644 --- a/erpnext/controllers/tests/test_item_variant.py +++ b/erpnext/controllers/tests/test_item_variant.py @@ -6,6 +6,7 @@ import unittest from erpnext.stock.doctype.item.test_item import set_item_variant_settings from erpnext.controllers.item_variant import copy_attributes_to_variant, make_variant_item_code +from erpnext.stock.doctype.quality_inspection.test_quality_inspection import create_quality_inspection_parameter from six import string_types @@ -56,6 +57,8 @@ def make_quality_inspection_template(): qc = frappe.new_doc("Quality Inspection Template") qc.quality_inspection_template_name = qc_template + + create_quality_inspection_parameter("Moisture") qc.append('item_quality_inspection_parameter', { "specification": "Moisture", "value": "< 5%", diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index d0bfb466e05..8c5a04b3f06 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -126,12 +126,18 @@ def create_quality_inspection(**args): qa.inspected_by = frappe.session.user qa.status = args.status or "Accepted" - readings = args.readings or {"specification": "Size", "min_value": 0, "max_value": 10} + if not args.readings: + create_quality_inspection_parameter("Size") + readings = {"specification": "Size", "min_value": 0, "max_value": 10} + else: + readings = args.readings + if args.status == "Rejected": readings["reading_1"] = "12" # status is auto set in child on save if isinstance(readings, list): for entry in readings: + create_quality_inspection_parameter(entry["specification"]) qa.append("readings", entry) else: qa.append("readings", readings) @@ -142,3 +148,11 @@ def create_quality_inspection(**args): qa.submit() return qa + +def create_quality_inspection_parameter(parameter): + if not frappe.db.exists("Quality Inspection Parameter", parameter): + frappe.get_doc({ + "doctype": "Quality Inspection Parameter", + "parameter": parameter, + "description": parameter + }).insert() \ No newline at end of file From 03b25be9e9cd6080ddece0b330ea2e1442049da7 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 4 Jan 2021 11:16:59 +0530 Subject: [PATCH 181/295] feat: Allow Discharge despite Unbilled Healthcare Services --- .../healthcare_settings.json | 15 +++- .../healthcare_settings.py | 2 +- .../inpatient_record/inpatient_record.py | 69 +++++++++++++++---- 3 files changed, 70 insertions(+), 16 deletions(-) diff --git a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json index 01043867141..b33c326313d 100644 --- a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json +++ b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json @@ -17,6 +17,8 @@ "enable_free_follow_ups", "max_visits", "valid_days", + "inpatient_settings_section", + "allow_discharge_despite_unbilled_services", "healthcare_service_items", "inpatient_visit_charge_item", "op_consulting_charge_item", @@ -302,11 +304,22 @@ "fieldname": "enable_free_follow_ups", "fieldtype": "Check", "label": "Enable Free Follow-ups" + }, + { + "fieldname": "inpatient_settings_section", + "fieldtype": "Section Break", + "label": "Inpatient Settings" + }, + { + "default": "0", + "fieldname": "allow_discharge_despite_unbilled_services", + "fieldtype": "Check", + "label": "Allow Discharge Despite Unbilled Healthcare Services" } ], "issingle": 1, "links": [], - "modified": "2020-07-08 15:17:21.543218", + "modified": "2021-01-04 10:19:22.329272", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare Settings", diff --git a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.py b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.py index a16fceb74dd..e2ccc34a74b 100644 --- a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.py +++ b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.py @@ -11,7 +11,7 @@ import json class HealthcareSettings(Document): def validate(self): - for key in ['collect_registration_fee', 'link_customer_to_patient', 'patient_name_by', + for key in ['collect_registration_fee', 'link_customer_to_patient', 'patient_name_by', 'allow_discharge_despite_unbilled_services', 'lab_test_approval_required', 'create_sample_collection_for_lab_test', 'default_medical_code_standard']: frappe.db.set_default(key, self.get(key, "")) diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py index bc769706018..6a32aca9d0c 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe, json from frappe import _ -from frappe.utils import today, now_datetime, getdate, get_datetime +from frappe.utils import today, now_datetime, getdate, get_datetime, get_link_to_form from frappe.model.document import Document from frappe.desk.reportview import get_match_cond @@ -113,6 +113,7 @@ def schedule_inpatient(args): inpatient_record.status = 'Admission Scheduled' inpatient_record.save(ignore_permissions = True) + @frappe.whitelist() def schedule_discharge(args): discharge_order = json.loads(args) @@ -126,16 +127,19 @@ def schedule_discharge(args): frappe.db.set_value('Patient', discharge_order['patient'], 'inpatient_status', inpatient_record.status) frappe.db.set_value('Patient Encounter', inpatient_record.discharge_encounter, 'inpatient_status', inpatient_record.status) + def set_details_from_ip_order(inpatient_record, ip_order): for key in ip_order: inpatient_record.set(key, ip_order[key]) + def set_ip_child_records(inpatient_record, inpatient_record_child, encounter_child): for item in encounter_child: table = inpatient_record.append(inpatient_record_child) for df in table.meta.get('fields'): table.set(df.fieldname, item.get(df.fieldname)) + def check_out_inpatient(inpatient_record): if inpatient_record.inpatient_occupancies: for inpatient_occupancy in inpatient_record.inpatient_occupancies: @@ -144,54 +148,88 @@ def check_out_inpatient(inpatient_record): inpatient_occupancy.check_out = now_datetime() frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant") + def discharge_patient(inpatient_record): - validate_invoiced_inpatient(inpatient_record) + validate_inpatient_invoicing(inpatient_record) inpatient_record.discharge_date = today() inpatient_record.status = "Discharged" inpatient_record.save(ignore_permissions = True) -def validate_invoiced_inpatient(inpatient_record): - pending_invoices = [] + +def validate_inpatient_invoicing(inpatient_record): + if frappe.db.get_default("allow_discharge_despite_unbilled_services"): + return + + pending_invoices = get_pending_invoices(inpatient_record) + + if pending_invoices: + message = _("Cannot mark Inpatient Record as Discharged since there are unbilled services. ") + + formatted_doc_rows = '' + + for doctype, docnames in pending_invoices.items(): + formatted_doc_rows += """ + {0} + {1} + """.format(doctype, docnames) + + message += """ + + + + + + {2} +
    {0}{1}
    + """.format(_("Healthcare Service"), _("Documents"), formatted_doc_rows) + + frappe.throw(message, title=_("Unbilled Services"), is_minimizable=True, wide=True) + + +def get_pending_invoices(inpatient_record): + pending_invoices = {} if inpatient_record.inpatient_occupancies: service_unit_names = False for inpatient_occupancy in inpatient_record.inpatient_occupancies: - if inpatient_occupancy.invoiced != 1: + if not inpatient_occupancy.invoiced: if service_unit_names: service_unit_names += ", " + inpatient_occupancy.service_unit else: service_unit_names = inpatient_occupancy.service_unit if service_unit_names: - pending_invoices.append("Inpatient Occupancy (" + service_unit_names + ")") + pending_invoices["Inpatient Occupancy"] = service_unit_names docs = ["Patient Appointment", "Patient Encounter", "Lab Test", "Clinical Procedure"] for doc in docs: - doc_name_list = get_inpatient_docs_not_invoiced(doc, inpatient_record) + doc_name_list = get_unbilled_inpatient_docs(doc, inpatient_record) if doc_name_list: pending_invoices = get_pending_doc(doc, doc_name_list, pending_invoices) - if pending_invoices: - frappe.throw(_("Can not mark Inpatient Record Discharged, there are Unbilled Invoices {0}").format(", " - .join(pending_invoices)), title=_('Unbilled Invoices')) + return pending_invoices + def get_pending_doc(doc, doc_name_list, pending_invoices): if doc_name_list: doc_ids = False for doc_name in doc_name_list: + doc_link = get_link_to_form(doc, doc_name.name) if doc_ids: - doc_ids += ", "+doc_name.name + doc_ids += ", " + doc_link else: - doc_ids = doc_name.name + doc_ids = doc_link if doc_ids: - pending_invoices.append(doc + " (" + doc_ids + ")") + pending_invoices[doc] = doc_ids return pending_invoices -def get_inpatient_docs_not_invoiced(doc, inpatient_record): + +def get_unbilled_inpatient_docs(doc, inpatient_record): return frappe.db.get_list(doc, filters = {'patient': inpatient_record.patient, 'inpatient_record': inpatient_record.name, 'docstatus': 1, 'invoiced': 0}) + def admit_patient(inpatient_record, service_unit, check_in, expected_discharge=None): inpatient_record.admitted_datetime = check_in inpatient_record.status = 'Admitted' @@ -203,6 +241,7 @@ def admit_patient(inpatient_record, service_unit, check_in, expected_discharge=N frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_status', 'Admitted') frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_record', inpatient_record.name) + def transfer_patient(inpatient_record, service_unit, check_in): item_line = inpatient_record.append('inpatient_occupancies', {}) item_line.service_unit = service_unit @@ -212,6 +251,7 @@ def transfer_patient(inpatient_record, service_unit, check_in): frappe.db.set_value("Healthcare Service Unit", service_unit, "occupancy_status", "Occupied") + def patient_leave_service_unit(inpatient_record, check_out, leave_from): if inpatient_record.inpatient_occupancies: for inpatient_occupancy in inpatient_record.inpatient_occupancies: @@ -221,6 +261,7 @@ def patient_leave_service_unit(inpatient_record, check_out, leave_from): frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant") inpatient_record.save(ignore_permissions = True) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_leave_from(doctype, txt, searchfield, start, page_len, filters): From 7206e12c2f2d9ed46d11748d9ebb098e034bba28 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 4 Jan 2021 12:11:00 +0530 Subject: [PATCH 182/295] test: Allow Discharge despite Unbilled Services --- .../healthcare_settings.py | 2 +- .../inpatient_record/inpatient_record.py | 2 +- .../inpatient_record/test_inpatient_record.py | 35 +++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.py b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.py index e2ccc34a74b..a16fceb74dd 100644 --- a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.py +++ b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.py @@ -11,7 +11,7 @@ import json class HealthcareSettings(Document): def validate(self): - for key in ['collect_registration_fee', 'link_customer_to_patient', 'patient_name_by', 'allow_discharge_despite_unbilled_services', + for key in ['collect_registration_fee', 'link_customer_to_patient', 'patient_name_by', 'lab_test_approval_required', 'create_sample_collection_for_lab_test', 'default_medical_code_standard']: frappe.db.set_default(key, self.get(key, "")) diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py index 6a32aca9d0c..dc549a65db6 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py @@ -158,7 +158,7 @@ def discharge_patient(inpatient_record): def validate_inpatient_invoicing(inpatient_record): - if frappe.db.get_default("allow_discharge_despite_unbilled_services"): + if frappe.db.get_single_value("Healthcare Settings", "allow_discharge_despite_unbilled_services"): return pending_invoices = get_pending_invoices(inpatient_record) diff --git a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py index 70706adb2e4..e8a9444fecd 100644 --- a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py @@ -40,6 +40,31 @@ 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")) + def test_allow_discharge_despite_unbilled_services(self): + frappe.db.sql("""delete from `tabInpatient Record`""") + setup_inpatient_settings() + 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()) + + # 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) + # Should not validate Pending Invoices + ip_record.discharge() + + self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_record")) + self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_status")) + + def test_validate_overlap_admission(self): frappe.db.sql("""delete from `tabInpatient Record`""") patient = create_patient() @@ -63,6 +88,13 @@ def mark_invoiced_inpatient_occupancy(ip_record): inpatient_occupancy.invoiced = 1 ip_record.save(ignore_permissions = True) + +def setup_inpatient_settings(): + settings = frappe.get_single("Healthcare Settings") + settings.allow_discharge_despite_unbilled_services = 1 + settings.save() + + def create_inpatient(patient): patient_obj = frappe.get_doc('Patient', patient) inpatient_record = frappe.new_doc('Inpatient Record') @@ -78,6 +110,7 @@ def create_inpatient(patient): inpatient_record.scheduled_date = today() return inpatient_record + def get_healthcare_service_unit(): service_unit = get_random("Healthcare Service Unit", filters={"inpatient_occupancy": 1}) if not service_unit: @@ -105,6 +138,7 @@ def get_healthcare_service_unit(): return service_unit.name return service_unit + def get_service_unit_type(): service_unit_type = get_random("Healthcare Service Unit Type", filters={"inpatient_occupancy": 1}) @@ -116,6 +150,7 @@ def get_service_unit_type(): return service_unit_type.name return service_unit_type + def create_patient(): patient = frappe.db.exists('Patient', '_Test IPD Patient') if not patient: From a873ae0d9f0157ce69c48a26277097eff710af9a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 4 Jan 2021 14:23:31 +0530 Subject: [PATCH 183/295] fix: Check for custom dimensions --- .../public/js/utils/dimension_tree_filter.js | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js index 319cbd2b5d5..96e181788e3 100644 --- a/erpnext/public/js/utils/dimension_tree_filter.js +++ b/erpnext/public/js/utils/dimension_tree_filter.js @@ -23,22 +23,24 @@ erpnext.accounts.dimensions = { }, setup_filters(frm, doctype) { - this.accounting_dimensions.forEach((dimension) => { - frappe.model.with_doctype(dimension['document_type'], () => { - 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 (this.accounting_dimensions) { + this.accounting_dimensions.forEach((dimension) => { + frappe.model.with_doctype(dimension['document_type'], () => { + 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); - } + if (frappe.meta.has_field(doctype, dimension['fieldname'])) { + this.setup_account_filters(frm, dimension['fieldname'], parent_fields); + } + }); }); }); - }); + } }, setup_child_filters(frm, doctype, parentfield, dimension) { @@ -91,7 +93,7 @@ erpnext.accounts.dimensions = { }, copy_dimension_from_first_row(frm, cdt, cdn, fieldname) { - if (frappe.meta.has_field(frm.doctype, 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']]); From 27fd9e4d7def5cf817773b1f4ac6168d0537767d Mon Sep 17 00:00:00 2001 From: Anupam Date: Mon, 4 Jan 2021 18:18:00 +0530 Subject: [PATCH 184/295] fix: added empty value in Quality Inspection Reading status --- .../quality_inspection_reading.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json index c1976dd1fb5..9baa702754f 100644 --- a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json +++ b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json @@ -130,7 +130,7 @@ "label": "Status", "oldfieldname": "status", "oldfieldtype": "Select", - "options": "Accepted\nRejected" + "options": "\nAccepted\nRejected" }, { "fieldname": "section_break_3", @@ -158,7 +158,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-11-16 16:34:29.947856", + "modified": "2021-01-04 18:16:53.978410", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection Reading", From 0c883853b3acc8c22b2c44cff212b6ba13ac5b25 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 4 Jan 2021 18:48:50 +0530 Subject: [PATCH 185/295] fix: Dont validate warehouse values between MR to Stock Entry - Remove validation thta checks if warehouse in Stock Entry is the same as MR that it was pulled from --- erpnext/stock/doctype/stock_entry/stock_entry.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 2fc7da83896..4782a9df0f1 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1333,9 +1333,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"]: From 0da201c6a5d6467d3e7a392ae1bb4c616681d364 Mon Sep 17 00:00:00 2001 From: Afshan Date: Mon, 4 Jan 2021 19:04:05 +0530 Subject: [PATCH 186/295] fix: set company in leave allocation and leave ledger entry --- .../doctype/leave_allocation/leave_allocation.json | 12 +++++++++++- .../leave_ledger_entry/leave_ledger_entry.json | 14 +++++++++++++- erpnext/patches.txt | 1 + .../v13_0/set_company_in_leave_ledger_entry.py | 7 +++++++ 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json index 4b315014dae..3a300c0d632 100644 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json @@ -11,6 +11,7 @@ "employee", "employee_name", "department", + "company", "column_break1", "leave_type", "from_date", @@ -219,6 +220,15 @@ "label": "Leave Policy Assignment", "options": "Leave Policy Assignment", "read_only": 1 + }, + { + "fetch_from": "employee.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "read_only": 1, + "reqd": 1 } ], "icon": "fa fa-ok", @@ -226,7 +236,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-08-20 14:25:10.314323", + "modified": "2021-01-04 18:46:13.184104", "modified_by": "Administrator", "module": "HR", "name": "Leave Allocation", diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json index 4abba5f2d4a..d74760a5cf8 100644 --- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json +++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2019-05-09 15:47:39.760406", "doctype": "DocType", "engine": "InnoDB", @@ -8,6 +9,7 @@ "leave_type", "transaction_type", "transaction_name", + "company", "leaves", "column_break_7", "from_date", @@ -106,12 +108,22 @@ "fieldtype": "Link", "label": "Holiday List", "options": "Holiday List" + }, + { + "fetch_from": "employee.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "read_only": 1, + "reqd": 1 } ], "in_create": 1, "index_web_pages_for_search": 1, "is_submittable": 1, - "modified": "2020-09-04 12:16:36.569066", + "links": [], + "modified": "2021-01-04 18:47:45.146652", "modified_by": "Administrator", "module": "HR", "name": "Leave Ledger Entry", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index f2e4f72d673..923ed2f3390 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -742,3 +742,4 @@ 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 erpnext.patches.v13_0.update_returned_qty_in_pr_dn +erpnext.patches.v13_0.set_company_in_leave_ledger_entry \ No newline at end of file diff --git a/erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py b/erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py new file mode 100644 index 00000000000..66857c4e659 --- /dev/null +++ b/erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py @@ -0,0 +1,7 @@ +import frappe + +def execute(): + frappe.reload_doc('HR', 'doctype', 'Leave Allocation') + frappe.reload_doc('HR', 'doctype', 'Leave Ledger Entry') + frappe.db.sql("""update `tabLeave Ledger Entry` as lle set company = (select company from `tabEmployee` where employee = lle.employee)""") + frappe.db.sql("""update `tabLeave Allocation` as la set company = (select company from `tabEmployee` where employee = la.employee)""") \ No newline at end of file From 517fd8b9e6a59a1b0945d07950d60904041cc9d9 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 5 Jan 2021 09:23:39 +0530 Subject: [PATCH 187/295] fix: Ignore customer and supplier while deleting company transactions (#24279) * fix: Ignore customer and supplier while deleting company transactions * fix: Test cases fixed based on Travis --- erpnext/accounts/doctype/budget/test_budget.py | 12 ++++++------ .../doctype/payroll_entry/test_payroll_entry.py | 8 ++++---- .../doctype/salary_slip/test_salary_slip.py | 17 +++++++++-------- .../company/delete_company_transactions.py | 2 +- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/erpnext/accounts/doctype/budget/test_budget.py b/erpnext/accounts/doctype/budget/test_budget.py index 0f115f9cc20..cd88b117614 100644 --- a/erpnext/accounts/doctype/budget/test_budget.py +++ b/erpnext/accounts/doctype/budget/test_budget.py @@ -159,10 +159,10 @@ class TestBudget(unittest.TestCase): budget = make_budget(budget_against="Cost Center") month = now_datetime().month - if month > 10: - month = 10 + if month > 9: + month = 9 - for i in range(month): + 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) @@ -181,10 +181,10 @@ class TestBudget(unittest.TestCase): budget = make_budget(budget_against="Project") month = now_datetime().month - if month > 10: - month = 10 + if month > 9: + month = 9 - for i in range(month): + 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") diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index 54106c8d166..e098ec79b0f 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -22,7 +22,7 @@ class TestPayrollEntry(unittest.TestCase): frappe.db.sql("delete from `tab%s`" % dt) make_earning_salary_component(setup=True, company_list=["_Test Company"]) - make_deduction_salary_component(setup=True, company_list=["_Test Company"]) + make_deduction_salary_component(setup=True, test_tax=False, company_list=["_Test Company"]) frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 0) @@ -107,9 +107,9 @@ class TestPayrollEntry(unittest.TestCase): frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC": frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account", "_Test Payroll Payable - _TC") - - make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=frappe.db.get_value("Company", "_Test Company", "default_currency")) - make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=frappe.db.get_value("Company", "_Test Company", "default_currency")) + currency=frappe.db.get_value("Company", "_Test Company", "default_currency") + make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=currency, test_tax=False) + make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=currency, test_tax=False) dates = get_start_end_dates('Monthly', nowdate()) if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}): diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index bb310c4d873..d6fb4195988 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -585,14 +585,6 @@ def make_deduction_salary_component(setup=False, test_tax=False, company_list=No "amount": 200, "exempted_from_income_tax": 1 - }, - { - "salary_component": 'TDS', - "abbr":'T', - "type": "Deduction", - "depends_on_payment_days": 0, - "variable_based_on_taxable_salary": 1, - "round_to_the_nearest_integer": 1 } ] if not test_tax: @@ -603,6 +595,15 @@ def make_deduction_salary_component(setup=False, test_tax=False, company_list=No "type": "Deduction", "round_to_the_nearest_integer": 1 }) + else: + data.append({ + "salary_component": 'TDS', + "abbr":'T', + "type": "Deduction", + "depends_on_payment_days": 0, + "variable_based_on_taxable_salary": 1, + "round_to_the_nearest_integer": 1 + }) if setup or test_tax: make_salary_component(data, test_tax, company_list) diff --git a/erpnext/setup/doctype/company/delete_company_transactions.py b/erpnext/setup/doctype/company/delete_company_transactions.py index 566f20cfa12..7a72fe31023 100644 --- a/erpnext/setup/doctype/company/delete_company_transactions.py +++ b/erpnext/setup/doctype/company/delete_company_transactions.py @@ -28,7 +28,7 @@ def delete_company_transactions(company_name): "Party Account", "Employee", "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template", "POS Profile", "BOM", "Company", "Bank Account", "Item Tax Template", "Mode Of Payment", - "Item Default"): + "Item Default", "Customer", "Supplier"): delete_for_doctype(doctype, company_name) # reset company values From 06a401ffbfa812452c9aafc4fca5b2b5d3702554 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Tue, 5 Jan 2021 11:54:34 +0530 Subject: [PATCH 188/295] fix: incoming rate attribute error (#24287) Co-authored-by: pateljannat --- erpnext/controllers/selling_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 85cfb951fcc..812021f5c86 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -233,7 +233,7 @@ class SellingController(StockController): 'allow_zero_valuation': d.allow_zero_valuation_rate, 'sales_invoice_item': d.get("sales_invoice_item"), 'dn_detail': d.get("dn_detail"), - 'incoming_rate': p.incoming_rate + 'incoming_rate': p.get("incoming_rate") })) else: il.append(frappe._dict({ @@ -252,7 +252,7 @@ class SellingController(StockController): 'allow_zero_valuation': d.allow_zero_valuation_rate, 'sales_invoice_item': d.get("sales_invoice_item"), 'dn_detail': d.get("dn_detail"), - 'incoming_rate': d.incoming_rate + 'incoming_rate': d.get("incoming_rate") })) return il From 16a809483b69b20403aefc679f39a2018d187ee6 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Tue, 5 Jan 2021 12:28:45 +0530 Subject: [PATCH 189/295] fix: indentation --- erpnext/selling/doctype/sales_order/sales_order.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index ee87afd673f..9a3c260a2aa 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -830,7 +830,7 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order.")) for supplier in suppliers: - doc = get_mapped_doc("Sales Order", source_name, { + doc = get_mapped_doc("Sales Order", source_name, { "Sales Order": { "doctype": "Purchase Order", "field_no_map": [ @@ -1087,4 +1087,4 @@ def update_produced_qty_in_so_item(sales_order, sales_order_item): if not total_produced_qty and frappe.flags.in_patch: return - frappe.db.set_value('Sales Order Item', sales_order_item, 'produced_qty', total_produced_qty) \ No newline at end of file + frappe.db.set_value('Sales Order Item', sales_order_item, 'produced_qty', total_produced_qty) From 1f591ab02e40ab884899059ae95f7d86315da83b Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 5 Jan 2021 13:53:51 +0530 Subject: [PATCH 190/295] fix(e-invoicing): minor calculation fixes (#24285) --- .../controllers/sales_and_purchase_return.py | 2 + erpnext/public/js/controllers/transaction.js | 1 + erpnext/regional/india/e_invoice/utils.py | 41 +++++++++++++------ erpnext/stock/get_item_details.py | 4 +- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 79792262c0a..a048d6e2dfb 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -328,6 +328,7 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.po_detail = source_doc.po_detail target_doc.pr_detail = source_doc.pr_detail target_doc.purchase_invoice_item = source_doc.name + target_doc.price_list_rate = 0 elif doctype == "Delivery Note": returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) @@ -353,6 +354,7 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.dn_detail = source_doc.dn_detail target_doc.expense_account = source_doc.expense_account target_doc.sales_invoice_item = source_doc.name + target_doc.price_list_rate = 0 if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 3bc20f87336..bed9c14141e 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -543,6 +543,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ company: me.frm.doc.company, order_type: me.frm.doc.order_type, is_pos: cint(me.frm.doc.is_pos), + is_return: cint(me.frm.doc.is_return), is_subcontracted: me.frm.doc.is_subcontracted, transaction_date: me.frm.doc.transaction_date || me.frm.doc.posting_date, ignore_pricing_rule: me.frm.doc.ignore_pricing_rule, diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 02ce6c14c90..e5f7d2d78c8 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -92,21 +92,18 @@ def get_party_details(address_name): location = gstin_details.get('AddrLoc') or address.get('city') state_code = gstin_details.get('StateCode') pincode = gstin_details.get('AddrPncd') - address_line1 = '{} {}'.format(gstin_details.get('AddrBno'), gstin_details.get('AddrFlno')) - address_line2 = '{} {}'.format(gstin_details.get('AddrBnm'), gstin_details.get('AddrSt')) - email_id = address.get('email_id') - phone = address.get('phone') - # get last 10 digit - phone = phone.replace(" ", "")[-10:] if phone else '' + address_line1 = '{} {}'.format(gstin_details.get('AddrBno') or "", gstin_details.get('AddrFlno') or "") + address_line2 = '{} {}'.format(gstin_details.get('AddrBnm') or "", gstin_details.get('AddrSt') or "") if state_code == 97: # according to einvoice standard pincode = 999999 return frappe._dict(dict( - gstin=gstin, legal_name=legal_name, location=location, - pincode=pincode, state_code=state_code, address_line1=address_line1, - address_line2=address_line2, email=email_id, phone=phone + gstin=gstin, legal_name=legal_name, + location=location, pincode=pincode, + state_code=state_code, address_line1=address_line1, + address_line2=address_line2 )) def get_gstin_details(gstin): @@ -146,9 +143,10 @@ def get_item_list(invoice): item.update(d.as_dict()) item.sr_no = d.idx - item.discount_amount = abs(item.discount_amount * item.qty) - item.description = d.item_name + item.description = d.item_name.replace('"', '\\"') + 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) @@ -156,6 +154,7 @@ def get_item_list(invoice): 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 item.is_service_item = 'N' if frappe.db.get_value('Item', d.item_code, 'is_stock_item') else 'Y' + item.serial_no = "" item = update_item_taxes(invoice, item) @@ -272,7 +271,25 @@ def get_eway_bill_details(invoice): vehicle_type=vehicle_type[invoice.gst_vehicle_type] )) +def validate_mandatory_fields(invoice): + if not invoice.company_address: + frappe.throw(_('Company Address is mandatory to fetch company GSTIN details.'), title=_('Missing Fields')) + if not invoice.customer_address: + frappe.throw(_('Customer Address is mandatory to fetch customer GSTIN details.'), title=_('Missing Fields')) + if not frappe.db.get_value('Address', invoice.company_address, 'gstin'): + frappe.throw( + _('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'), + title=_('Missing Fields') + ) + if not frappe.db.get_value('Address', invoice.customer_address, 'gstin'): + frappe.throw( + _('GSTIN is mandatory to fetch customer GSTIN details. Please enter GSTIN in selected customer address.'), + title=_('Missing Fields') + ) + def make_einvoice(invoice): + validate_mandatory_fields(invoice) + schema = read_json('einv_template') transaction_details = get_transaction_details(invoice) @@ -351,7 +368,7 @@ def validate_einvoice(validations, einvoice, errors=[]): # remove empty dicts einvoice.pop(fieldname, None) continue - + # convert to int or str if value_type == 'string': einvoice[fieldname] = str(value) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 08f7a83b893..bf45251c9d8 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -74,7 +74,9 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru update_party_blanket_order(args, out) - get_price_list_rate(args, item, out) + if not doc or cint(doc.get('is_return')) == 0: + # get price list rate only if the invoice is not a credit or debit note + get_price_list_rate(args, item, out) if args.customer and cint(args.is_pos): out.update(get_pos_profile_item_details(args.company, args)) From a56a5ccefa48e4764d058e3e0b46b99f22ea7282 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 5 Jan 2021 15:59:17 +0530 Subject: [PATCH 191/295] refactor: fetch & validate address from erpnext rather than gst portal (#24297) --- erpnext/regional/india/e_invoice/utils.py | 54 ++++++++++++++--------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index e5f7d2d78c8..abe15043af8 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -15,7 +15,7 @@ from frappe import _, bold from pyqrcode import create as qrcreate from frappe.integrations.utils import make_post_request, make_get_request from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply -from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date +from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date, get_link_to_form def validate_einvoice_fields(doc): einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable')) @@ -84,26 +84,32 @@ def get_doc_details(invoice): )) def get_party_details(address_name): - address = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0] - gstin = address.get('gstin') + d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0] - gstin_details = get_gstin_details(gstin) - legal_name = gstin_details.get('LegalName') or gstin_details.get('TradeName') - location = gstin_details.get('AddrLoc') or address.get('city') - state_code = gstin_details.get('StateCode') - pincode = gstin_details.get('AddrPncd') - address_line1 = '{} {}'.format(gstin_details.get('AddrBno') or "", gstin_details.get('AddrFlno') or "") - address_line2 = '{} {}'.format(gstin_details.get('AddrBnm') or "", gstin_details.get('AddrSt') or "") + if (not d.gstin + or not d.city + or not d.pincode + or not d.address_title + or not d.address_line1 + or not d.gst_state_number): - if state_code == 97: + frappe.throw( + msg=_('Address lines, city, pincode, gstin is mandatory for address {}. Please set them and try again.').format( + get_link_to_form('Address', address_name) + ), + title=_('Missing Address Fields') + ) + + if d.gst_state_number == 97: # according to einvoice standard pincode = 999999 return frappe._dict(dict( - gstin=gstin, legal_name=legal_name, - location=location, pincode=pincode, - state_code=state_code, address_line1=address_line1, - address_line2=address_line2 + gstin=d.gstin, legal_name=d.address_title, + location=d.city, pincode=d.pincode, + state_code=d.gst_state_number, + address_line1=d.address_line1, + address_line2=d.address_line2 )) def get_gstin_details(gstin): @@ -124,14 +130,22 @@ def get_gstin_details(gstin): return GSPConnector.get_gstin_details(gstin) def get_overseas_address_details(address_name): - address_title, address_line1, address_line2, city, phone, email_id = frappe.db.get_value( - 'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city', 'phone', 'email_id'] + address_title, address_line1, address_line2, city = frappe.db.get_value( + 'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city'] ) + if not address_title or not address_line1 or not city: + frappe.throw( + msg=_('Address lines and city is mandatory for address {}. Please set them and try again.').format( + get_link_to_form('Address', address_name) + ), + title=_('Missing Address Fields') + ) + return frappe._dict(dict( - gstin='URP', legal_name=address_title, address_line1=address_line1, - address_line2=address_line2, email=email_id, phone=phone, - pincode=999999, state_code=96, place_of_supply=96, location=city + gstin='URP', legal_name=address_title, location=city, + address_line1=address_line1, address_line2=address_line2, + pincode=999999, state_code=96, place_of_supply=96 )) def get_item_list(invoice): From b01b108dfa7baf53562f361a69657fe2ec1fc981 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 5 Jan 2021 17:34:16 +0530 Subject: [PATCH 192/295] fix: do not consider current salary slip in sum --- erpnext/payroll/doctype/salary_slip/salary_slip.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 99d8a8317cd..3bb1f62b08e 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -1145,7 +1145,9 @@ class SalarySlip(TransactionBase): fields = ['sum(net_pay) as sum'], filters = {'employee_name' : self.employee_name, 'start_date' : ['>=', period_start_date], - 'end_date' : ['<', period_end_date]}) + 'end_date' : ['<', period_end_date], + 'name': ['!=', self.name] + }) year_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0 @@ -1160,7 +1162,8 @@ class SalarySlip(TransactionBase): fields = ['sum(net_pay) as sum'], filters = {'employee_name' : self.employee_name, 'start_date' : ['>=', first_day_of_the_month], - 'end_date' : ['<', self.start_date] + 'end_date' : ['<', self.start_date], + 'name': ['!=', self.name] }) month_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0 From f7b9b0687ead28f180839c19fe04a02f6829be35 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 5 Jan 2021 20:43:11 +0530 Subject: [PATCH 193/295] fix: tax calculation on salary slip for the first month (#24272) * fix: tax calculation on salary slip for the first month * fix: net pay precision issue * fix: net pay precision issue Co-authored-by: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> --- erpnext/hr/doctype/employee/employee.json | 3 +- .../doctype/salary_slip/salary_slip.js | 1 - .../doctype/salary_slip/salary_slip.py | 46 +++++++++++-------- .../doctype/salary_slip/test_salary_slip.py | 2 +- 4 files changed, 29 insertions(+), 23 deletions(-) diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index 4f1c04ff5d0..dc2aaa4a067 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -813,7 +813,7 @@ "idx": 24, "image_field": "image", "links": [], - "modified": "2020-10-16 15:02:04.283657", + "modified": "2021-01-01 16:54:33.477439", "modified_by": "Administrator", "module": "HR", "name": "Employee", @@ -855,7 +855,6 @@ "write": 1 } ], - "quick_entry": 1, "search_fields": "employee_name", "show_name_in_global_search": 1, "sort_field": "modified", diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js index 8e05bb2057e..51fb3596e9b 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.js +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js @@ -151,7 +151,6 @@ frappe.ui.form.on("Salary Slip", { var salary_detail_fields = ["formula", "abbr", "statistical_component", "variable_based_on_taxable_salary"]; frm.fields_dict['earnings'].grid.set_column_disp(salary_detail_fields, false); frm.fields_dict['deductions'].grid.set_column_disp(salary_detail_fields, false); - calculate_totals(frm); frm.trigger("set_dynamic_labels"); }, diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 3bb1f62b08e..d725f68a6bd 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -143,8 +143,8 @@ class SalarySlip(TransactionBase): self.salary_slip_based_on_timesheet = self._salary_structure_doc.salary_slip_based_on_timesheet or 0 self.set_time_sheet() self.pull_sal_struct() - payroll_based_on, consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, ["payroll_based_on","consider_unmarked_attendance_as"]) - return [payroll_based_on, consider_unmarked_attendance_as] + ps = frappe.db.get_value("Payroll Settings", None, ["payroll_based_on","consider_unmarked_attendance_as"], as_dict=1) + return [ps.payroll_based_on, ps.consider_unmarked_attendance_as] def set_time_sheet(self): if self.salary_slip_based_on_timesheet: @@ -424,16 +424,19 @@ class SalarySlip(TransactionBase): def calculate_net_pay(self): if self.salary_structure: self.calculate_component_amounts("earnings") - self.gross_pay = self.get_component_totals("earnings") + self.gross_pay = self.get_component_totals("earnings", depends_on_payment_days=1) self.base_gross_pay = flt(flt(self.gross_pay) * flt(self.exchange_rate), self.precision('base_gross_pay')) if self.salary_structure: self.calculate_component_amounts("deductions") + + self.set_loan_repayment() + self.set_component_amounts_based_on_payment_days() + self.set_net_pay() + + def set_net_pay(self): self.total_deduction = self.get_component_totals("deductions") self.base_total_deduction = flt(flt(self.total_deduction) * flt(self.exchange_rate), self.precision('base_total_deduction')) - - self.set_loan_repayment() - self.net_pay = flt(self.gross_pay) - (flt(self.total_deduction) + flt(self.total_loan_repayment)) self.rounded_total = rounded(self.net_pay) self.base_net_pay = flt(flt(self.net_pay) * flt(self.exchange_rate), self.precision('base_net_pay')) @@ -455,8 +458,6 @@ class SalarySlip(TransactionBase): else: self.add_tax_components(payroll_period) - self.set_component_amounts_based_on_payment_days(component_type) - def add_structure_components(self, component_type): data = self.get_data_for_eval() for struct_row in self._salary_structure_doc.get(component_type): @@ -813,7 +814,7 @@ class SalarySlip(TransactionBase): cint(row.depends_on_payment_days) and cint(self.total_working_days) and (not self.salary_slip_based_on_timesheet or getdate(self.start_date) < joining_date or - getdate(self.end_date) > relieving_date + (relieving_date and getdate(self.end_date) > relieving_date) )): additional_amount = flt((flt(row.additional_amount) * flt(self.payment_days) / cint(self.total_working_days)), row.precision("additional_amount")) @@ -946,15 +947,21 @@ class SalarySlip(TransactionBase): struct_row['variable_based_on_taxable_salary'] = component.variable_based_on_taxable_salary return struct_row - def get_component_totals(self, component_type): + def get_component_totals(self, component_type, depends_on_payment_days=0): + joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, + ["date_of_joining", "relieving_date"]) + total = 0.0 for d in self.get(component_type): if not d.do_not_include_in_total: - d.amount = flt(d.amount, d.precision("amount")) - total += d.amount + if depends_on_payment_days: + amount = self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0] + else: + amount = flt(d.amount, d.precision("amount")) + total += amount return total - def set_component_amounts_based_on_payment_days(self, component_type): + def set_component_amounts_based_on_payment_days(self): joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, ["date_of_joining", "relieving_date"]) @@ -964,8 +971,9 @@ class SalarySlip(TransactionBase): if not joining_date: frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name))) - for d in self.get(component_type): - d.amount = self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0] + for component_type in ("earnings", "deductions"): + for d in self.get(component_type): + d.amount = flt(self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0], d.precision("amount")) def set_loan_repayment(self): self.total_loan_repayment = 0 @@ -1089,17 +1097,17 @@ class SalarySlip(TransactionBase): self.calculate_net_pay() def set_totals(self): - self.gross_pay = 0 + self.gross_pay = 0.0 if self.salary_slip_based_on_timesheet == 1: self.calculate_total_for_salary_slip_based_on_timesheet() else: - self.total_deduction = 0 + self.total_deduction = 0.0 if self.earnings: for earning in self.earnings: - self.gross_pay += flt(earning.amount) + self.gross_pay += flt(earning.amount, earning.precision("amount")) if self.deductions: for deduction in self.deductions: - self.total_deduction += flt(deduction.amount) + self.total_deduction += flt(deduction.amount, deduction.precision("amount")) self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) - flt(self.total_loan_repayment) self.set_base_totals() diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index d6fb4195988..4368c03c2ae 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -318,7 +318,7 @@ class TestSalarySlip(unittest.TestCase): year_to_date = 0 for slip in salary_slips: - year_to_date += slip.net_pay + year_to_date += flt(slip.net_pay) self.assertEqual(slip.year_to_date, year_to_date) def test_tax_for_payroll_period(self): From 5eef19723d9bc139f2ef1cad03c17f6c42b17b2f Mon Sep 17 00:00:00 2001 From: Wolfram Schmidt Date: Tue, 5 Jan 2021 18:47:11 +0100 Subject: [PATCH 194/295] Update item_tax_template_dashboard.py added missing backlink. In Item Groups I can set Item Tax Temple in Table taxes. --- .../doctype/item_tax_template/item_tax_template_dashboard.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py b/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py index acc308e0e68..3d80a9785f0 100644 --- a/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py +++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py @@ -20,7 +20,8 @@ def get_data(): 'items': ['Purchase Invoice', 'Purchase Order', 'Purchase Receipt'] }, { - 'items': ['Item'] + 'label': _('Stock'), + 'items': ['Item Groups', 'Item'] } ] } From dd768a07c5ff7d4efa243351f2a1fb1be23b044e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 5 Jan 2021 23:55:00 +0530 Subject: [PATCH 195/295] fix: Sanctioned loan security unpledge --- .../loan_management/doctype/loan/test_loan.py | 21 +++++++++++++++++++ .../loan_security_unpledge.py | 12 ++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 8b1f9a2266f..2abd7d84d97 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -362,6 +362,27 @@ class TestLoan(unittest.TestCase): unpledge_request.load_from_db() self.assertEqual(unpledge_request.docstatus, 1) + def test_santined_loan_security_unpledge(self): + pledge = [{ + "loan_security": "Test Security 1", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + create_pledge(loan_application) + + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + unpledge_map = {'Test Security 1': 4000} + unpledge_request = unpledge_security(loan=loan.name, security_map = unpledge_map, save=1) + unpledge_request.submit() + unpledge_request.status = 'Approved' + unpledge_request.save() + unpledge_request.submit() + def test_disbursal_check_with_shortfall(self): pledges = [{ "loan_security": "Test Security 2", diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index 61c418d3d31..ae88a07e251 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -44,10 +44,16 @@ class LoanSecurityUnpledge(Document): "valid_upto": (">=", get_datetime()) }, as_list=1)) - total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid', - 'total_interest_payable', 'written_off_amount']) + loan_details = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid', + 'total_interest_payable', 'written_off_amount', 'disbursed_amount', 'status'], as_dict=1) + + if loan_details.status == 'Disbursed': + pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.interest_payable) \ + - flt(loan_details.principal_paid) - flt(loan_details.written_off_amount) + else: + pending_principal_amount = flt(loan_details.disbursed_amount) - flt(loan_details.total_interest_payable) \ + - flt(loan_details.total_principal_paid) - flt(loan_details.written_off_amount) - pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount) security_value = 0 unpledge_qty_map = {} ltv_ratio = 0 From 05fe7ac29cde423616f15b05643705d4bab026f0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 6 Jan 2021 09:10:28 +0530 Subject: [PATCH 196/295] fix: fieldname --- .../doctype/loan_security_unpledge/loan_security_unpledge.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index ae88a07e251..c4c2d683780 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -48,8 +48,8 @@ class LoanSecurityUnpledge(Document): 'total_interest_payable', 'written_off_amount', 'disbursed_amount', 'status'], as_dict=1) if loan_details.status == 'Disbursed': - pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.interest_payable) \ - - flt(loan_details.principal_paid) - flt(loan_details.written_off_amount) + pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \ + - flt(loan_details.total_principal_paid) - flt(loan_details.written_off_amount) else: pending_principal_amount = flt(loan_details.disbursed_amount) - flt(loan_details.total_interest_payable) \ - flt(loan_details.total_principal_paid) - flt(loan_details.written_off_amount) From ad8be7c1fedd6bc41afaef23f86b44bffa3a9a1f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 6 Jan 2021 09:29:03 +0530 Subject: [PATCH 197/295] fix: Consider only submitted salary slips --- erpnext/payroll/doctype/salary_slip/salary_slip.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index d725f68a6bd..47c9d31bf4b 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -429,11 +429,11 @@ class SalarySlip(TransactionBase): if self.salary_structure: self.calculate_component_amounts("deductions") - + self.set_loan_repayment() self.set_component_amounts_based_on_payment_days() self.set_net_pay() - + def set_net_pay(self): self.total_deduction = self.get_component_totals("deductions") self.base_total_deduction = flt(flt(self.total_deduction) * flt(self.exchange_rate), self.precision('base_total_deduction')) @@ -1154,10 +1154,10 @@ class SalarySlip(TransactionBase): filters = {'employee_name' : self.employee_name, 'start_date' : ['>=', period_start_date], 'end_date' : ['<', period_end_date], - 'name': ['!=', self.name] + 'name': ['!=', self.name], + 'docstatus': 1 }) - year_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0 year_to_date += self.net_pay @@ -1171,7 +1171,8 @@ class SalarySlip(TransactionBase): filters = {'employee_name' : self.employee_name, 'start_date' : ['>=', first_day_of_the_month], 'end_date' : ['<', self.start_date], - 'name': ['!=', self.name] + 'name': ['!=', self.name], + 'docstatus': 1 }) month_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0 From 5a579089c2ba5fa6ffb00538bcee66246e4f07d2 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Wed, 6 Jan 2021 11:21:13 +0530 Subject: [PATCH 198/295] fix: indentation --- erpnext/selling/doctype/sales_order/sales_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 9a3c260a2aa..e5a8a7196cc 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -830,7 +830,7 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order.")) for supplier in suppliers: - doc = get_mapped_doc("Sales Order", source_name, { + doc = get_mapped_doc("Sales Order", source_name, { "Sales Order": { "doctype": "Purchase Order", "field_no_map": [ From e7fa6f6a1cb571544708f30d6456891132c27115 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Wed, 6 Jan 2021 13:15:30 +0530 Subject: [PATCH 199/295] fix: edditable employee grid --- .../doctype/payroll_entry/payroll_entry.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index a25a6e7a32c..6bcd4e0c006 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -21,6 +21,9 @@ class PayrollEntry(Document): if cint(entries) == len(self.employees): self.set_onload("submitted_ss", True) + def validate(self): + self.number_of_employees = len(self.employees) + def on_submit(self): self.create_salary_slips() @@ -113,7 +116,7 @@ class PayrollEntry(Document): for d in employees: self.append('employees', d) - self.number_of_employees = len(employees) + self.number_of_employees = len(self.employees) if self.validate_attendance: return self.validate_employee_attendance() @@ -145,8 +148,8 @@ class PayrollEntry(Document): """ self.check_permission('write') self.created = 1 - emp_list = [d.employee for d in self.get_emp_list()] - if emp_list: + employees = [emp.employee for emp in self.employees] + if employees: args = frappe._dict({ "salary_slip_based_on_timesheet": self.salary_slip_based_on_timesheet, "payroll_frequency": self.payroll_frequency, @@ -160,10 +163,10 @@ class PayrollEntry(Document): "exchange_rate": self.exchange_rate, "currency": self.currency }) - if len(emp_list) > 30: - frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=emp_list, args=args) + if len(employees) > 30: + frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=employees, args=args) else: - create_salary_slips_for_employees(emp_list, args, publish_progress=False) + create_salary_slips_for_employees(employees, args, publish_progress=False) # since this method is called via frm.call this doc needs to be updated manually self.reload() From fd5ebe9d4a757c5ded79b2cc2e698695e21f2ce8 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 7 Jan 2021 15:16:15 +0530 Subject: [PATCH 200/295] fix: patch and columns --- erpnext/patches/v13_0/update_project_template_tasks.py | 9 +++++++-- .../project_template_task/project_template_task.json | 4 +++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py index f24a2c62f1f..886616e0caf 100644 --- a/erpnext/patches/v13_0/update_project_template_tasks.py +++ b/erpnext/patches/v13_0/update_project_template_tasks.py @@ -8,6 +8,8 @@ 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") + for template_name in frappe.db.sql(""" select name @@ -30,11 +32,14 @@ def execute(): description = task.description, is_template = 1 )).insert() - new_tasks.append(new_task.name) + print(new_task) + new_tasks.append(new_task) if replace_tasks: template.tasks = [] for tsk in new_tasks: + print(tsk.name, tsk.subject) template.append("tasks", { - "task": tsk + "task": tsk.name, + "subject": tsk.subject }) template.save() \ No newline at end of file diff --git a/erpnext/projects/doctype/project_template_task/project_template_task.json b/erpnext/projects/doctype/project_template_task/project_template_task.json index 7a552945bd5..69530b15b40 100644 --- a/erpnext/projects/doctype/project_template_task/project_template_task.json +++ b/erpnext/projects/doctype/project_template_task/project_template_task.json @@ -10,6 +10,7 @@ ], "fields": [ { + "columns": 2, "fieldname": "task", "fieldtype": "Link", "in_list_view": 1, @@ -18,6 +19,7 @@ "reqd": 1 }, { + "columns": 6, "fieldname": "subject", "fieldtype": "Read Only", "in_list_view": 1, @@ -26,7 +28,7 @@ ], "istable": 1, "links": [], - "modified": "2020-12-28 12:10:26.321913", + "modified": "2021-01-07 15:13:40.995071", "modified_by": "Administrator", "module": "Projects", "name": "Project Template Task", From fab080c875f7542fb4f989a77a8bbbb2e791db8b Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 7 Jan 2021 15:25:39 +0530 Subject: [PATCH 201/295] fix: Company Wise Valuation Rate for RM in BOM --- erpnext/manufacturing/doctype/bom/bom.js | 17 ++++++++----- erpnext/manufacturing/doctype/bom/bom.py | 24 +++++++++++++++---- .../doctype/work_order/work_order.py | 1 + 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 1c4b7a1e1ce..15affd84e10 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -411,7 +411,7 @@ cur_frm.cscript.hour_rate = function(doc) { cur_frm.cscript.time_in_mins = cur_frm.cscript.hour_rate; -cur_frm.cscript.bom_no = function(doc, cdt, cdn) { +cur_frm.cscript.bom_no = function(doc, cdt, cdn) { get_bom_material_detail(doc, cdt, cdn, false); }; @@ -419,17 +419,22 @@ cur_frm.cscript.is_default = function(doc) { if (doc.is_default) cur_frm.set_value("is_active", 1); }; -var get_bom_material_detail= function(doc, cdt, cdn, scrap_items) { +var get_bom_material_detail = function(doc, cdt, cdn, scrap_items) { + if (!doc.company) { + frappe.throw({message: __("Please select a Company first."), title: __("Mandatory")}); + } + var d = locals[cdt][cdn]; if (d.item_code) { return frappe.call({ doc: doc, method: "get_bom_material_detail", args: { - 'item_code': d.item_code, - 'bom_no': d.bom_no != null ? d.bom_no: '', + "company": doc.company, + "item_code": d.item_code, + "bom_no": d.bom_no != null ? d.bom_no: '', "scrap_items": scrap_items, - 'qty': d.qty, + "qty": d.qty, "stock_qty": d.stock_qty, "include_item_in_manufacturing": d.include_item_in_manufacturing, "uom": d.uom, @@ -468,7 +473,7 @@ cur_frm.cscript.rate = function(doc, cdt, cdn) { } if (d.bom_no) { - frappe.msgprint(__("You can not change rate if BOM mentioned agianst any item")); + frappe.msgprint(__("You cannot change the rate if BOM is mentioned against any Item.")); get_bom_material_detail(doc, cdt, cdn, scrap_items); } else { erpnext.bom.calculate_rm_cost(doc); diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 6363242b0a6..03beedb6635 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -65,6 +65,10 @@ class BOM(WebsiteGenerator): def validate(self): self.route = frappe.scrub(self.name).replace('_', '-') + + if not self.company: + frappe.throw(_("Please select a Company first."), title=_("Mandatory")) + self.clear_operations() self.validate_main_item() self.validate_currency() @@ -125,6 +129,7 @@ class BOM(WebsiteGenerator): self.validate_bom_currecny(item) ret = self.get_bom_material_detail({ + "company": self.company, "item_code": item.item_code, "item_name": item.item_name, "bom_no": item.bom_no, @@ -213,6 +218,7 @@ class BOM(WebsiteGenerator): for d in self.get("items"): rate = self.get_rm_rate({ + "company": self.company, "item_code": d.item_code, "bom_no": d.bom_no, "qty": d.qty, @@ -611,10 +617,20 @@ def get_valuation_rate(args): """ Get weighted average of valuation rate from all warehouses """ total_qty, total_value, valuation_rate = 0.0, 0.0, 0.0 - for d in frappe.db.sql("""select actual_qty, stock_value from `tabBin` - where item_code=%s""", args['item_code'], as_dict=1): - total_qty += flt(d.actual_qty) - total_value += flt(d.stock_value) + item_bins = frappe.db.sql(""" + select + bin.actual_qty, bin.stock_value + from + `tabBin` bin, `tabWarehouse` warehouse + where + bin.item_code=%(item)s + and bin.warehouse = warehouse.name + and warehouse.company=%(company)s""", + {"item": args['item_code'], "company": args['company']}, as_dict=1) + + for d in item_bins: + total_qty += flt(d.actual_qty) + total_value += flt(d.stock_value) if total_qty: valuation_rate = total_value / total_qty diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index cc93bf9fd63..8e7fac8ce88 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -725,6 +725,7 @@ def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"): args.update(item_data) args["rate"] = get_bom_item_rate({ + "company": wo_doc.company, "item_code": args.get("item_code"), "qty": args.get("required_qty"), "uom": args.get("stock_uom"), From ff6ee9d4e712ac1d573e010c5a36193abacbcf50 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 8 Jan 2021 09:14:43 +0530 Subject: [PATCH 202/295] fix: Formula field description and Rearrange grid view - Missing closing quote in Formula field description - In grid view of child table in QI, show only input fields --- .../item_quality_inspection_parameter.json | 4 ++-- .../quality_inspection_reading.json | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json index fc06e89f2fb..3e81619cfd1 100644 --- a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json +++ b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json @@ -43,7 +43,7 @@ }, { "depends_on": "formula_based_criteria", - "description": "Simple Python formula applied on Reading fields.
    Numeric eg. 1: reading_1 > 0.2 and reading_1 < 0.5
    \nNumeric eg. 2: mean > 3.5 (mean of populated fields)
    \nValue based eg.: reading_value in (\"A\", \"B\", \"C)", + "description": "Simple Python formula applied on Reading fields.
    Numeric eg. 1: reading_1 > 0.2 and reading_1 < 0.5
    \nNumeric eg. 2: mean > 3.5 (mean of populated fields)
    \nValue based eg.: reading_value in (\"A\", \"B\", \"C\")", "fieldname": "acceptance_formula", "fieldtype": "Code", "label": "Acceptance Criteria Formula" @@ -80,7 +80,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-12-28 17:41:04.350225", + "modified": "2021-01-07 21:32:49.866439", "modified_by": "Administrator", "module": "Stock", "name": "Item Quality Inspection Parameter", diff --git a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json index 739845bcdac..dddb3d517dd 100644 --- a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json +++ b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json @@ -49,7 +49,6 @@ "depends_on": "eval:(!doc.formula_based_criteria && doc.non_numeric)", "fieldname": "value", "fieldtype": "Data", - "in_list_view": 1, "label": "Acceptance Criteria Value", "oldfieldname": "value", "oldfieldtype": "Data" @@ -76,7 +75,6 @@ "columns": 1, "fieldname": "reading_3", "fieldtype": "Data", - "in_list_view": 1, "label": "Reading 3", "oldfieldname": "reading_3", "oldfieldtype": "Data" @@ -153,7 +151,7 @@ }, { "depends_on": "formula_based_criteria", - "description": "Simple Python formula applied on Reading fields.
    Numeric eg. 1: reading_1 > 0.2 and reading_1 < 0.5
    \nNumeric eg. 2: mean > 3.5 (mean of populated fields)
    \nValue based eg.: reading_value in (\"A\", \"B\", \"C)", + "description": "Simple Python formula applied on Reading fields.
    Numeric eg. 1: reading_1 > 0.2 and reading_1 < 0.5
    \nNumeric eg. 2: mean > 3.5 (mean of populated fields)
    \nValue based eg.: reading_value in (\"A\", \"B\", \"C\")", "fieldname": "acceptance_formula", "fieldtype": "Code", "label": "Acceptance Criteria Formula" @@ -190,6 +188,7 @@ "depends_on": "non_numeric", "fieldname": "reading_value", "fieldtype": "Data", + "in_list_view": 1, "label": "Reading Value" }, { @@ -202,6 +201,7 @@ "default": "0", "fieldname": "non_numeric", "fieldtype": "Check", + "in_list_view": 1, "label": "Non-Numeric" }, { @@ -215,7 +215,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-12-28 17:40:47.407210", + "modified": "2021-01-07 21:56:40.235579", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection Reading", From c4963bfdb24819e2edd4a28c11551190965d9cd1 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 8 Jan 2021 09:56:04 +0530 Subject: [PATCH 203/295] fix: Back Update from QC based on Batch No --- .../quality_inspection/quality_inspection.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index ae4eb9b9956..b30d48d5475 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -69,11 +69,21 @@ class QualityInspection(Document): doctype = 'Stock Entry Detail' if self.reference_type and self.reference_name: + conditions = "" + if self.batch_no: + conditions += " and t1.batch_no = '%s'"%(self.batch_no) + frappe.db.sql(""" - UPDATE `tab{child_doc}` t1, `tab{parent_doc}` t2 - SET t1.quality_inspection = %s, t2.modified = %s - WHERE t1.parent = %s and t1.item_code = %s and t1.parent = t2.name - """.format(parent_doc=self.reference_type, child_doc=doctype), + UPDATE + `tab{child_doc}` t1, `tab{parent_doc}` t2 + SET + t1.quality_inspection = %s, t2.modified = %s + WHERE + t1.parent = %s + and t1.item_code = %s + and t1.parent = t2.name + {conditions} + """.format(parent_doc=self.reference_type, child_doc=doctype, conditions=conditions), (quality_inspection, self.modified, self.reference_name, self.item_code)) def set_status_based_on_acceptance_formula(self): From a93151502c5f27b8492449d83d95eba25ff2887e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 8 Jan 2021 12:10:26 +0530 Subject: [PATCH 204/295] fix: Components formulated from additional salary not being fetched in Payroll Entry --- erpnext/payroll/doctype/salary_slip/salary_slip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 47c9d31bf4b..183ad13411a 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -577,7 +577,7 @@ class SalarySlip(TransactionBase): 'default_amount': amount if not struct_row.get("is_additional_component") else 0, 'depends_on_payment_days' : struct_row.depends_on_payment_days, 'salary_component' : struct_row.salary_component, - 'abbr' : struct_row.abbr, + 'abbr' : struct_row.abbr or struct_row.get("salary_component_abbr"), 'additional_salary': additional_salary, 'do_not_include_in_total' : struct_row.do_not_include_in_total, 'is_tax_applicable': struct_row.is_tax_applicable, From 3777c6aa381f35af5359039db211875c784dab50 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 8 Jan 2021 12:36:51 +0530 Subject: [PATCH 205/295] fix: payment entry multi-currency issue --- .../accounts/doctype/payment_entry/payment_entry.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index e1174717382..9bdd26b805b 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -401,6 +401,8 @@ frappe.ui.form.on('Payment Entry', { set_account_currency_and_balance: function(frm, account, currency_field, balance_field, callback_function) { + + var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; if (frm.doc.posting_date && account) { frappe.call({ method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_account_details", @@ -427,6 +429,14 @@ frappe.ui.form.on('Payment Entry', { if(!frm.doc.paid_amount && frm.doc.received_amount) frm.events.received_amount(frm); + + if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency + && frm.doc.paid_amount != frm.doc.received_amount) { + if (company_currency != frm.doc.paid_from_account_currency && + frm.doc.payment_type == "Pay") { + frm.doc.paid_amount = frm.doc.received_amount; + } + } } }, () => { From b7637f49cd543e8a4fdbfc0d840323583d5c3651 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 8 Jan 2021 18:35:49 +0530 Subject: [PATCH 206/295] fix: Remove QI link on cancel wherever same QI name exists --- .../stock/doctype/quality_inspection/quality_inspection.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index b30d48d5475..2084e3fa545 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -70,9 +70,12 @@ class QualityInspection(Document): if self.reference_type and self.reference_name: conditions = "" - if self.batch_no: + if self.batch_no and self.docstatus == 1: conditions += " and t1.batch_no = '%s'"%(self.batch_no) + if self.docstatus == 2: # if cancel, then remove qi link wherever same name + conditions += " and t1.quality_inspection = '%s'"%(self.name) + frappe.db.sql(""" UPDATE `tab{child_doc}` t1, `tab{parent_doc}` t2 From 2ffa4b9ce4614273f06bc74aa62e3c85ca304712 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Fri, 8 Jan 2021 20:58:00 +0530 Subject: [PATCH 207/295] fix: Link timesheets with corresponding projects --- erpnext/projects/doctype/timesheet/timesheet.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.json b/erpnext/projects/doctype/timesheet/timesheet.json index 4c2edf4f03a..b28682184ef 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", From 1b208e0695983124bb2ff31a576c5ed5d26a2967 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 10 Jan 2021 12:29:55 +0530 Subject: [PATCH 208/295] fix: Add total loan interest amount field in loans --- .../loan_interest_accrual.json | 9 +++- .../loan_interest_accrual.py | 6 ++- .../test_loan_interest_accrual.py | 43 ++++++++++++++++--- .../doctype/loan_repayment/loan_repayment.py | 2 +- 4 files changed, 52 insertions(+), 8 deletions(-) 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 f157f0df8f0..185bf7a6663 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 d17f5af4907..7d7992d40ae 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 46a64405539..85e008ac293 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 415ba993c7b..ac30c91b670 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 From d3634f6dac7f5611a5336fedb9e13fb68c9974b6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 10 Jan 2021 19:26:45 +0530 Subject: [PATCH 209/295] feat: Loan Interest Report --- .../report/loan_interest_report/__init__.py | 0 .../loan_interest_report.js | 9 ++ .../loan_interest_report.json | 29 +++++++ .../loan_interest_report.py | 87 +++++++++++++++++++ 4 files changed, 125 insertions(+) create mode 100644 erpnext/loan_management/report/loan_interest_report/__init__.py create mode 100644 erpnext/loan_management/report/loan_interest_report/loan_interest_report.js create mode 100644 erpnext/loan_management/report/loan_interest_report/loan_interest_report.json create mode 100644 erpnext/loan_management/report/loan_interest_report/loan_interest_report.py 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 00000000000..e69de29bb2d 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 00000000000..852e3ca366f --- /dev/null +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js @@ -0,0 +1,9 @@ +// 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": [ + + ] +}; 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 00000000000..321d6064e33 --- /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 00000000000..e039fe82d38 --- /dev/null +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py @@ -0,0 +1,87 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import flt, get_first_day, getdate + + +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": _("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": _("Interest For The Month"), "fieldname": "month_interest", "fieldtype": "Currency", "options": "Currency", "width": 100}, + {"label": _("Penalty For The Month"), "fieldname": "month_penalty", "fieldtype": "Currency", "options": "Currency", "width": 100}, + {"label": _("Accrued Interest"), "fieldname": "accrued_interest", "fieldtype": "Currency", "options": "Currency", "width": 100}, + {"label": _("Total Repayment"), "fieldname": "total_repayment", "fieldtype": "Currency", "options": "Currency", "width": 100}, + {"label": _("Principal Outstanding"), "fieldname": "principal_outstanding", "fieldtype": "Currency", "options": "Currency", "width": 100}, + {"label": _("Interest Outstanding"), "fieldname": "interest_outstanding", "fieldtype": "Currency", "options": "Currency", "width": 100}, + {"label": _("Total Outstanding"), "fieldname": "total_payment", "fieldtype": "Currency", "options": "Currency", "width": 100}, + {"label": _("Interest %"), "fieldname": "rate_of_interest", "fieldtype": "Percent", "width": 100}, + {"label": _("Penalty Interest %"), "fieldname": "precentage_percentage", "fieldtype": "Percent", "width": 100}, + ] + + return columns + +def get_active_loan_details(filters): + 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"], + filters={"status": ("!=", "Closed")}) + + loan_list = [d.loan for d in loan_details] + + sanctioned_amount_map = get_sanctioned_amount_map() + payments = get_payments(loan_list) + accrual_map = get_interest_accruals(loan_list) + + 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)), + "month_interest": flt(accrual_map.get(loan.loan, {}).get("month_interest")), + "accrued_interest": flt(accrual_map.get(loan.loan, {}).get("accrued_interest")) + }) + 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 = {} + current_month_start = get_first_day(getdate()) + + interest_accruals = frappe.get_all("Loan Interest Accrual", + fields=["loan", "interest_amount", "posting_date", "penalty"], + filters={"loan": ("in", loans)}) + + for entry in interest_accruals: + accrual_map.setdefault(entry.loan, { + 'month_interest': 0.0, + 'accrued_interest': 0.0 + }) + + if getdate(entry.posting_date) < getdate(current_month_start): + accrual_map[entry.loan]['accrued_interest'] += entry.interest_amount + else: + accrual_map[entry.loan]['month_interest'] += entry.interest_amount + + return accrual_map \ No newline at end of file From 2460e101a70f7eb1ef91a30984b2f32f0b78ceff Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 11 Jan 2021 10:05:54 +0530 Subject: [PATCH 210/295] fix: indicator ofor template task --- erpnext/patches/v13_0/update_project_template_tasks.py | 3 +-- erpnext/projects/doctype/task/task_list.js | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py index 886616e0caf..5fa062306cf 100644 --- a/erpnext/patches/v13_0/update_project_template_tasks.py +++ b/erpnext/patches/v13_0/update_project_template_tasks.py @@ -32,12 +32,11 @@ def execute(): description = task.description, is_template = 1 )).insert() - print(new_task) new_tasks.append(new_task) + if replace_tasks: template.tasks = [] for tsk in new_tasks: - print(tsk.name, tsk.subject) template.append("tasks", { "task": tsk.name, "subject": tsk.subject diff --git a/erpnext/projects/doctype/task/task_list.js b/erpnext/projects/doctype/task/task_list.js index 941fe975468..39734ee8b1c 100644 --- a/erpnext/projects/doctype/task/task_list.js +++ b/erpnext/projects/doctype/task/task_list.js @@ -20,7 +20,8 @@ frappe.listview_settings['Task'] = { "Pending Review": "orange", "Working": "orange", "Completed": "green", - "Cancelled": "dark grey" + "Cancelled": "dark grey", + "Template": "blue" } return [__(doc.status), colors[doc.status], "status,=," + doc.status]; }, From aff3f611d35dd5c6538a16e4bcebf32c2f06d138 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 11 Jan 2021 12:48:11 +0530 Subject: [PATCH 211/295] fix: allow medication entries to be deleted from the table --- .../inpatient_medication_entry.js | 1 + .../inpatient_medication_entry.json | 3 +-- .../inpatient_medication_entry.py | 18 ------------------ 3 files changed, 2 insertions(+), 20 deletions(-) diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js index ca97489b8d8..a7b06b1718b 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js @@ -5,6 +5,7 @@ frappe.ui.form.on('Inpatient Medication Entry', { refresh: function(frm) { // Ignore cancellation of doctype on cancel all frm.ignore_doctypes_on_cancel_all = ['Stock Entry']; + frm.fields_dict['medication_orders'].grid.wrapper.find('.grid-add-row').hide(); frm.set_query('item_code', () => { return { diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.json b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.json index dd4c423a9e0..b1a6ee4ed14 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.json +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.json @@ -139,7 +139,6 @@ "fieldtype": "Table", "label": "Inpatient Medication Orders", "options": "Inpatient Medication Entry Detail", - "read_only": 1, "reqd": 1 }, { @@ -180,7 +179,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-11-03 13:22:37.820707", + "modified": "2021-01-11 12:37:46.749659", "modified_by": "Administrator", "module": "Healthcare", "name": "Inpatient Medication Entry", 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 70ae7138662..bba521313df 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py @@ -15,8 +15,6 @@ class InpatientMedicationEntry(Document): self.validate_medication_orders() def get_medication_orders(self): - self.validate_datetime_filters() - # pull inpatient medication orders based on selected filters orders = get_pending_medication_orders(self) @@ -27,22 +25,6 @@ class InpatientMedicationEntry(Document): self.set('medication_orders', []) frappe.msgprint(_('No pending medication orders found for selected criteria')) - def validate_datetime_filters(self): - if self.from_date and self.to_date: - self.validate_from_to_dates('from_date', 'to_date') - - if self.from_date and getdate(self.from_date) > getdate(): - frappe.throw(_('From Date cannot be after the current date.')) - - if self.to_date and getdate(self.to_date) > getdate(): - frappe.throw(_('To Date cannot be after the current date.')) - - if self.from_time and self.from_time > nowtime(): - frappe.throw(_('From Time cannot be after the current time.')) - - if self.to_time and self.to_time > nowtime(): - frappe.throw(_('To Time cannot be after the current time.')) - def add_mo_to_table(self, orders): # Add medication orders in the child table self.set('medication_orders', []) From dcda8b9e8cfaac31056792ff3ab9a70975c2727a Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Mon, 11 Jan 2021 12:50:39 +0530 Subject: [PATCH 212/295] feat: Patient appointment status changes (#24201) * feat: patient appointment status changes * fix: sider * fix: sider * fix: test status on cancel of docs and test refactor Co-authored-by: pateljannat Co-authored-by: Rucha Mahabal --- .../patient_appointment/test_patient_appointment.py | 4 +++- .../doctype/therapy_plan/test_therapy_plan.py | 13 +++++++++++-- .../healthcare/doctype/therapy_plan/therapy_plan.py | 3 ++- .../doctype/therapy_session/therapy_session.js | 9 +++++++++ .../doctype/therapy_session/therapy_session.py | 7 +++++++ 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index 3df7ba15314..b681ed1a226 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -23,8 +23,10 @@ class TestPatientAppointment(unittest.TestCase): self.assertEquals(appointment.status, 'Open') appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2)) self.assertEquals(appointment.status, 'Scheduled') - create_encounter(appointment) + encounter = create_encounter(appointment) self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed') + encounter.cancel() + self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Open') def test_start_encounter(self): patient, medical_department, practitioner = create_healthcare_docs() diff --git a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py index a061c66a54d..7fb159d6b50 100644 --- a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py +++ b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py @@ -5,10 +5,10 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.utils import getdate, flt +from frappe.utils import getdate, flt, nowdate from erpnext.healthcare.doctype.therapy_type.test_therapy_type import create_therapy_type from erpnext.healthcare.doctype.therapy_plan.therapy_plan import make_therapy_session, make_sales_invoice -from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_patient +from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_patient, create_appointment class TestTherapyPlan(unittest.TestCase): def test_creation_on_encounter_submission(self): @@ -28,6 +28,15 @@ class TestTherapyPlan(unittest.TestCase): frappe.get_doc(session).submit() self.assertEquals(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed') + patient, medical_department, practitioner = create_healthcare_docs() + appointment = create_appointment(patient, practitioner, nowdate()) + session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name) + session = frappe.get_doc(session) + session.submit() + self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed') + session.cancel() + self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Open') + def test_therapy_plan_from_template(self): patient = create_patient() template = create_therapy_plan_template() diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py index bc0ff1a5057..ac01c604dda 100644 --- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py +++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py @@ -47,7 +47,7 @@ class TherapyPlan(Document): @frappe.whitelist() -def make_therapy_session(therapy_plan, patient, therapy_type, company): +def make_therapy_session(therapy_plan, patient, therapy_type, company, appointment=None): therapy_type = frappe.get_doc('Therapy Type', therapy_type) therapy_session = frappe.new_doc('Therapy Session') @@ -58,6 +58,7 @@ def make_therapy_session(therapy_plan, patient, therapy_type, company): therapy_session.duration = therapy_type.default_duration therapy_session.rate = therapy_type.rate therapy_session.exercises = therapy_type.exercises + therapy_session.appointment = appointment if frappe.flags.in_test: therapy_session.start_date = today() diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.js b/erpnext/healthcare/doctype/therapy_session/therapy_session.js index a2b01c9c181..fd200036935 100644 --- a/erpnext/healthcare/doctype/therapy_session/therapy_session.js +++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.js @@ -19,6 +19,15 @@ frappe.ui.form.on('Therapy Session', { } }; }); + + frm.set_query('appointment', function() { + + return { + filters: { + 'status': ['in', ['Open', 'Scheduled']] + } + }; + }); }, refresh: function(frm) { diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.py b/erpnext/healthcare/doctype/therapy_session/therapy_session.py index 85d09701774..c00054421dc 100644 --- a/erpnext/healthcare/doctype/therapy_session/therapy_session.py +++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.py @@ -43,7 +43,14 @@ class TherapySession(Document): self.update_sessions_count_in_therapy_plan() insert_session_medical_record(self) + def on_update(self): + if self.appointment: + frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Closed') + def on_cancel(self): + if self.appointment: + frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Open') + self.update_sessions_count_in_therapy_plan(on_cancel=True) def update_sessions_count_in_therapy_plan(self, on_cancel=False): From 7646d7b741ecd24ea42909cc7e0c2e128a69d393 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 11 Jan 2021 10:27:05 +0530 Subject: [PATCH 213/295] fix: Batch/Serial Selector for Batched Item --- erpnext/stock/doctype/stock_entry/stock_entry.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 98116ec1832..f75e8b727db 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -524,7 +524,7 @@ frappe.ui.form.on('Stock Entry', { }) ); } - + for (let i in frm.doc.items) { let item = frm.doc.items[i]; @@ -675,7 +675,13 @@ frappe.ui.form.on('Stock Entry Detail', { }); refresh_field("items"); - if (!d.serial_no) { + let no_batch_serial_number_value = !d.serial_no; + if (d.has_batch_no && !d.has_serial_no) { + // check only batch_no for batched item + no_batch_serial_number_value = !d.batch_no; + } + + if (no_batch_serial_number_value) { erpnext.stock.select_batch_and_serial_no(frm, d); } } From 36fa0512d227693cdc0a904f92d41a93554349c5 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Tue, 12 Jan 2021 17:02:19 +0530 Subject: [PATCH 214/295] fix: not able to create dunning from sales invoice --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 50eb400775e..566734e7d14 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -179,7 +179,7 @@ class SalesInvoice(SellingController): # this sequence because outstanding may get -ve self.make_gl_entries() - + if self.update_stock == 1: self.repost_future_sle_and_gle() @@ -261,10 +261,10 @@ class SalesInvoice(SellingController): self.update_stock_ledger() self.make_gl_entries_on_cancel() - + if self.update_stock == 1: self.repost_future_sle_and_gle() - + frappe.db.set(self, 'status', 'Cancelled') if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction": @@ -551,7 +551,7 @@ class SalesInvoice(SellingController): def add_remarks(self): if not self.remarks: if self.po_no and self.po_date: - self.remarks = _("Against Customer Order {0} dated {1}").format(self.po_no, + self.remarks = _("Against Customer Order {0} dated {1}").format(self.po_no, formatdate(self.po_date)) else: self.remarks = _("No Remarks") @@ -1699,6 +1699,7 @@ def get_mode_of_payment_info(mode_of_payment, company): where mpa.parent = mp.name and mpa.company = %s and mp.enabled = 1 and mp.name = %s""", (company, mode_of_payment), as_dict=1) +@frappe.whitelist() def create_dunning(source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text, calculate_interest_and_amount From 7cd7bf7f968ddb24ef2e71a96948f9e1bc3e373b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 13 Jan 2021 00:05:29 +0530 Subject: [PATCH 215/295] fix: incorrect key --- erpnext/controllers/buying_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 6edc020701d..4dee375e5a2 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -336,7 +336,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 From 1354197c72f7e402a38fb0dd8812a155f9b8ac45 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 13 Jan 2021 09:12:50 +0530 Subject: [PATCH 216/295] feat(Healthcare Settings): Do Not Bill Patient Encounters for Inpatients --- .../doctype/healthcare_settings/healthcare_settings.json | 9 ++++++++- erpnext/healthcare/utils.py | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json index b33c326313d..ddf1bce4927 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/utils.py b/erpnext/healthcare/utils.py index 96282f50a92..50f6f476220 100644 --- a/erpnext/healthcare/utils.py +++ b/erpnext/healthcare/utils.py @@ -90,6 +90,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) From 0f05925ff481792a2558f97c4ad1495e76064d38 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 13 Jan 2021 09:46:33 +0530 Subject: [PATCH 217/295] test: Do Not Bill Patient Encounters for Inpatients --- .../inpatient_record/test_inpatient_record.py | 37 +++++++++++++++++-- erpnext/healthcare/utils.py | 4 +- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py index e8a9444fecd..10990d412d8 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() diff --git a/erpnext/healthcare/utils.py b/erpnext/healthcare/utils.py index 50f6f476220..6a499aab7fa 100644 --- a/erpnext/healthcare/utils.py +++ b/erpnext/healthcare/utils.py @@ -77,11 +77,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: From 8aeadc743eaa717bec021eb206182f7b6fc2a361 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Wed, 13 Jan 2021 12:56:04 +0530 Subject: [PATCH 218/295] fix: assessment plan error handling for course field (#23961) * fix: assessment plan error handling for course field * fix: message rectification * fix(travis): clean-up tests * fix: travis * fix: tests Co-authored-by: pateljannat Co-authored-by: Rucha Mahabal --- .../program_enrollment/program_enrollment.py | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/erpnext/education/doctype/program_enrollment/program_enrollment.py b/erpnext/education/doctype/program_enrollment/program_enrollment.py index 6fbcd8aa97f..886a7d85d8b 100644 --- a/erpnext/education/doctype/program_enrollment/program_enrollment.py +++ b/erpnext/education/doctype/program_enrollment/program_enrollment.py @@ -124,21 +124,24 @@ class ProgramEnrollment(Document): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_program_courses(doctype, txt, searchfield, start, page_len, filters): - if filters.get('program'): - return frappe.db.sql("""select course, course_name from `tabProgram Course` - where parent = %(program)s and course like %(txt)s {match_cond} - order by - if(locate(%(_txt)s, course), locate(%(_txt)s, course), 99999), - idx desc, - `tabProgram Course`.course asc - limit {start}, {page_len}""".format( - match_cond=get_match_cond(doctype), - start=start, - page_len=page_len), { - "txt": "%{0}%".format(txt), - "_txt": txt.replace('%', ''), - "program": filters['program'] - }) + if not filters.get('program'): + frappe.msgprint(_("Please select a Program first.")) + return [] + + return frappe.db.sql("""select course, course_name from `tabProgram Course` + where parent = %(program)s and course like %(txt)s {match_cond} + order by + if(locate(%(_txt)s, course), locate(%(_txt)s, course), 99999), + idx desc, + `tabProgram Course`.course asc + limit {start}, {page_len}""".format( + match_cond=get_match_cond(doctype), + start=start, + page_len=page_len), { + "txt": "%{0}%".format(txt), + "_txt": txt.replace('%', ''), + "program": filters['program'] + }) @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs From 1e396dcb2a58fa904f718c6223ad8ee6278a7dc8 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Wed, 13 Jan 2021 14:01:57 +0530 Subject: [PATCH 219/295] fix: validated GST state --- erpnext/regional/india/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index f256a66266d..0d8263835d2 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -48,6 +48,9 @@ def validate_gstin_for_india(doc, method): validate_gstin_check_digit(doc.gstin) set_gst_state_and_state_number(doc) + if not doc.gst_state: + frappe.throw(_("Please Enter GST state")) + if doc.gst_state_number != doc.gstin[:2]: frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.") .format(doc.gst_state_number)) From ef5f0c0461953315905d965aeb48ffb82979f0ae Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 13 Jan 2021 19:59:16 +0530 Subject: [PATCH 220/295] feat: Issue Summary Script Report (#23603) * feat: Issue Summary Report * feat: Add Issue Metrics to Issue Summary Report * fix: code clean-up * feat: Added Report Summary * feat: Add SLA status fields * fix: add report link to desk page * fix: sider issues Co-authored-by: Marica Co-authored-by: Nabin Hait --- .../support/desk_page/support/support.json | 4 +- .../support/report/issue_summary/__init__.py | 0 .../report/issue_summary/issue_summary.js | 73 ++++ .../report/issue_summary/issue_summary.json | 26 ++ .../report/issue_summary/issue_summary.py | 353 ++++++++++++++++++ 5 files changed, 454 insertions(+), 2 deletions(-) create mode 100644 erpnext/support/report/issue_summary/__init__.py create mode 100644 erpnext/support/report/issue_summary/issue_summary.js create mode 100644 erpnext/support/report/issue_summary/issue_summary.json create mode 100644 erpnext/support/report/issue_summary/issue_summary.py diff --git a/erpnext/support/desk_page/support/support.json b/erpnext/support/desk_page/support/support.json index 28410f3a71a..18cf87ab0ba 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]" + "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]" } ], "category": "Modules", @@ -43,7 +43,7 @@ "idx": 0, "is_standard": 1, "label": "Support", - "modified": "2020-08-11 15:49:34.307341", + "modified": "2020-10-12 18:40:22.252915", "modified_by": "Administrator", "module": "Support", "name": "Support", diff --git a/erpnext/support/report/issue_summary/__init__.py b/erpnext/support/report/issue_summary/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/support/report/issue_summary/issue_summary.js b/erpnext/support/report/issue_summary/issue_summary.js new file mode 100644 index 00000000000..684482ac8d2 --- /dev/null +++ b/erpnext/support/report/issue_summary/issue_summary.js @@ -0,0 +1,73 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Issue Summary"] = { + "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: "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" + } + ] +}; \ No newline at end of file diff --git a/erpnext/support/report/issue_summary/issue_summary.json b/erpnext/support/report/issue_summary/issue_summary.json new file mode 100644 index 00000000000..b8a580ccef1 --- /dev/null +++ b/erpnext/support/report/issue_summary/issue_summary.json @@ -0,0 +1,26 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2020-10-12 01:01:55.181777", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2020-10-12 14:54:55.655920", + "modified_by": "Administrator", + "module": "Support", + "name": "Issue Summary", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Issue", + "report_name": "Issue Summary", + "report_type": "Script Report", + "roles": [ + { + "role": "Support Team" + } + ] +} \ No newline at end of file diff --git a/erpnext/support/report/issue_summary/issue_summary.py b/erpnext/support/report/issue_summary/issue_summary.py new file mode 100644 index 00000000000..3d735314f4e --- /dev/null +++ b/erpnext/support/report/issue_summary/issue_summary.py @@ -0,0 +1,353 @@ +# 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 flt + +def execute(filters=None): + return IssueSummary(filters).run() + +class IssueSummary(object): + def __init__(self, filters=None): + self.filters = frappe._dict(filters or {}) + + def run(self): + self.get_columns() + self.get_data() + self.get_chart_data() + self.get_report_summary() + + return self.columns, self.data, None, self.chart, self.report_summary + + 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 + }) + + self.statuses = ['Open', 'Replied', 'Resolved', 'Closed'] + for status in self.statuses: + self.columns.append({ + 'label': _(status), + 'fieldname': scrub(status), + 'fieldtype': 'Int', + 'width': 80 + }) + + self.columns.append({ + 'label': _('Total Issues'), + 'fieldname': 'total_issues', + 'fieldtype': 'Int', + 'width': 100 + }) + + self.sla_status_map = { + 'SLA Failed': 'failed', + 'SLA Fulfilled': 'fulfilled', + 'SLA Ongoing': 'ongoing' + } + + for label, fieldname in self.sla_status_map.items(): + self.columns.append({ + 'label': _(label), + 'fieldname': fieldname, + 'fieldtype': 'Int', + 'width': 100 + }) + + self.metrics = ['Avg First Response Time', 'Avg Response Time', 'Avg Hold Time', + 'Avg Resolution Time', 'Avg User Resolution Time'] + + for metric in self.metrics: + self.columns.append({ + 'label': _(metric), + 'fieldname': scrub(metric), + 'fieldtype': 'Duration', + 'width': 170 + }) + + def get_data(self): + self.get_issues() + self.get_rows() + + 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', 'status', 'avg_response_time', + 'first_response_time', 'total_hold_time', 'user_resolution_time', 'resolution_time', 'agreement_status'], + filters=filters + ) + + 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_summary_data() + + for entity, data in iteritems(self.issue_summary_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} + + for status in self.statuses: + count = flt(data.get(status, 0.0)) + row[scrub(status)] = count + + row['total_issues'] = data.get('total_issues', 0.0) + + for sla_status in self.sla_status_map.values(): + value = flt(data.get(sla_status), 0.0) + row[sla_status] = value + + for metric in self.metrics: + value = flt(data.get(scrub(metric)), 0.0) + row[scrub(metric)] = value + + self.data.append(row) + + def get_summary_data(self): + self.issue_summary_data = frappe._dict() + + for d in self.entries: + status = d.status + agreement_status = scrub(d.agreement_status) + + if self.filters.based_on == 'Assigned To': + if d._assign: + for entry in json.loads(d._assign): + self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(status, 0.0) + self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(agreement_status, 0.0) + self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault('total_issues', 0.0) + self.issue_summary_data[entry][status] += 1 + self.issue_summary_data[entry][agreement_status] += 1 + self.issue_summary_data[entry]['total_issues'] += 1 + + else: + field = self.field_map.get(self.filters.based_on) + value = d.get(field) + if not value: + value = _('Not Specified') + + self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(status, 0.0) + self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(agreement_status, 0.0) + self.issue_summary_data.setdefault(value, frappe._dict()).setdefault('total_issues', 0.0) + self.issue_summary_data[value][status] += 1 + self.issue_summary_data[value][agreement_status] += 1 + self.issue_summary_data[value]['total_issues'] += 1 + + self.get_metrics_data() + + def get_metrics_data(self): + issues = [] + + metrics_list = ['avg_response_time', 'avg_first_response_time', 'avg_hold_time', + 'avg_resolution_time', 'avg_user_resolution_time'] + + for entry in self.entries: + issues.append(entry.name) + + field = self.field_map.get(self.filters.based_on) + + if issues: + if self.filters.based_on == 'Assigned To': + assignment_map = frappe._dict() + for d in self.entries: + if d._assign: + for entry in json.loads(d._assign): + for metric in metrics_list: + self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(metric, 0.0) + + self.issue_summary_data[entry]['avg_response_time'] += d.get('avg_response_time') or 0.0 + self.issue_summary_data[entry]['avg_first_response_time'] += d.get('first_response_time') or 0.0 + self.issue_summary_data[entry]['avg_hold_time'] += d.get('total_hold_time') or 0.0 + self.issue_summary_data[entry]['avg_resolution_time'] += d.get('resolution_time') or 0.0 + self.issue_summary_data[entry]['avg_user_resolution_time'] += d.get('user_resolution_time') or 0.0 + + if not assignment_map.get(entry): + assignment_map[entry] = 0 + assignment_map[entry] += 1 + + for entry in assignment_map: + for metric in metrics_list: + self.issue_summary_data[entry][metric] /= flt(assignment_map.get(entry)) + + else: + data = frappe.db.sql(""" + SELECT + {0}, AVG(first_response_time) as avg_frt, + AVG(avg_response_time) as avg_resp_time, + AVG(total_hold_time) as avg_hold_time, + AVG(resolution_time) as avg_resolution_time, + AVG(user_resolution_time) as avg_user_resolution_time + FROM `tabIssue` + WHERE + name IN %(issues)s + GROUP BY {0} + """.format(field), {'issues': issues}, as_dict=1) + + for entry in data: + value = entry.get(field) + if not value: + value = _('Not Specified') + + for metric in metrics_list: + self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(metric, 0.0) + + self.issue_summary_data[value]['avg_response_time'] = entry.get('avg_resp_time') or 0.0 + self.issue_summary_data[value]['avg_first_response_time'] = entry.get('avg_frt') or 0.0 + self.issue_summary_data[value]['avg_hold_time'] = entry.get('avg_hold_time') or 0.0 + self.issue_summary_data[value]['avg_resolution_time'] = entry.get('avg_resolution_time') or 0.0 + self.issue_summary_data[value]['avg_user_resolution_time'] = entry.get('avg_user_resolution_time') or 0.0 + + def get_chart_data(self): + if not self.data: + return None + + labels = [] + open_issues = [] + replied_issues = [] + resolved_issues = [] + closed_issues = [] + + entity = self.filters.based_on + entity_field = self.field_map.get(entity) + if entity == 'Assigned To': + entity_field = 'user' + + for entry in self.data: + labels.append(entry.get(entity_field)) + open_issues.append(entry.get('open')) + replied_issues.append(entry.get('replied')) + resolved_issues.append(entry.get('resolved')) + closed_issues.append(entry.get('closed')) + + self.chart = { + 'data': { + 'labels': labels[:30], + 'datasets': [ + { + 'name': 'Open', + 'values': open_issues[:30] + }, + { + 'name': 'Replied', + 'values': replied_issues[:30] + }, + { + 'name': 'Resolved', + 'values': resolved_issues[:30] + }, + { + 'name': 'Closed', + 'values': closed_issues[:30] + } + ] + }, + 'type': 'bar', + 'barOptions': { + 'stacked': True + } + } + + def get_report_summary(self): + if not self.data: + return None + + open_issues = 0 + replied = 0 + resolved = 0 + closed = 0 + + for entry in self.data: + open_issues += entry.get('open') + replied += entry.get('replied') + resolved += entry.get('resolved') + closed += entry.get('closed') + + self.report_summary = [ + { + 'value': open_issues, + 'indicator': 'Red', + 'label': _('Open'), + 'datatype': 'Int', + }, + { + 'value': replied, + 'indicator': 'Grey', + 'label': _('Replied'), + 'datatype': 'Int', + }, + { + 'value': resolved, + 'indicator': 'Green', + 'label': _('Resolved'), + 'datatype': 'Int', + }, + { + 'value': closed, + 'indicator': 'Green', + 'label': _('Closed'), + 'datatype': 'Int', + } + ] + From 511434190dca98a987b47ba4278d72da793baa3b Mon Sep 17 00:00:00 2001 From: Kanchan Chauhan Date: Wed, 13 Jan 2021 20:59:43 +0530 Subject: [PATCH 221/295] fix(work order): Actual start and end dates update (#24360) Currently, even when the Work Order (without Operations) is completed and Stock Entries are there, the Actual Start Date and Actual End Date is not updated. For some reason need to db_set, then it updates the Actual Start Date and Actual End Date --- erpnext/manufacturing/doctype/work_order/work_order.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 8e7fac8ce88..ca530bbaddc 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -456,10 +456,10 @@ class WorkOrder(Document): if data and len(data): dates = [d.posting_datetime for d in data] - self.actual_start_date = min(dates) + self.db_set('actual_start_date', min(dates)) if self.status == "Completed": - self.actual_end_date = max(dates) + self.db_set('actual_end_date', max(dates)) self.set_lead_time() From 53cb9f9f47fba12046db8a694f169577a34a74d4 Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 13 Jan 2021 21:00:44 +0530 Subject: [PATCH 222/295] fix(e-invoicing): ux issues (#24358) * fix: overseas invoice rounding adjustment * fix: overseas shipping address * fix: qrcode for document name having forward slash * feat: sandbox mode toggle * fix: cannot delete sales invoice if linked to e invoice req log --- .../e_invoice_request_log.json | 7 +++--- .../e_invoice_settings.json | 9 ++++++- erpnext/regional/india/e_invoice/utils.py | 25 ++++++++++++------- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json index 5c1c79dc047..3034370feac 100644 --- a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json +++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json @@ -24,9 +24,8 @@ }, { "fieldname": "reference_invoice", - "fieldtype": "Link", - "label": "Reference Invoice", - "options": "Sales Invoice" + "fieldtype": "Data", + "label": "Reference Invoice" }, { "fieldname": "headers", @@ -64,7 +63,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-12-24 21:09:38.882866", + "modified": "2021-01-13 12:06:57.253111", "modified_by": "Administrator", "module": "Regional", "name": "E Invoice Request Log", diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json index 4dcb22a54c7..db8bda75bfd 100644 --- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json @@ -7,6 +7,7 @@ "field_order": [ "enable", "section_break_2", + "sandbox_mode", "credentials", "auth_token", "token_expiry" @@ -41,12 +42,18 @@ "label": "Credentials", "mandatory_depends_on": "enable", "options": "E Invoice User" + }, + { + "default": "0", + "fieldname": "sandbox_mode", + "fieldtype": "Check", + "label": "Sandbox Mode" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-12-22 15:34:57.280044", + "modified": "2021-01-13 12:04:49.449199", "modified_by": "Administrator", "module": "Regional", "name": "E Invoice Settings", diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index abe15043af8..d0cac90e4df 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -218,8 +218,8 @@ 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.discount_amount - invoice_value_details.round_off = invoice.rounding_adjustment + invoice_value_details.invoice_discount_amt = 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) @@ -322,7 +322,10 @@ def make_einvoice(invoice): shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({}) if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name: - shipping_details = get_party_details(invoice.shipping_address_name) + if invoice.gst_category == 'Overseas': + shipping_details = get_overseas_address_details(invoice.shipping_address_name) + else: + shipping_details = get_party_details(invoice.shipping_address_name) if invoice.is_pos and invoice.base_paid_amount: payment_details = get_payment_details(invoice) @@ -414,15 +417,19 @@ class RequestFailed(Exception): pass class GSPConnector(): def __init__(self, doctype=None, docname=None): self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings') + sandbox_mode = self.e_invoice_settings.sandbox_mode + self.invoice = frappe.get_cached_doc(doctype, docname) if doctype and docname else None self.credentials = self.get_credentials() - self.base_url = 'https://gsp.adaequare.com' - self.authenticate_url = self.base_url + '/gsp/authenticate?grant_type=token' - self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin' - self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice' - self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn' + # authenticate url is same for sandbox & live + self.authenticate_url = 'https://gsp.adaequare.com/gsp/authenticate?grant_type=token' + self.base_url = 'https://gsp.adaequare.com' if not sandbox_mode else 'https://gsp.adaequare.com/test' + self.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel' + 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.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill' @@ -758,7 +765,7 @@ class GSPConnector(): _file = frappe.new_doc('File') _file.update({ - 'file_name': f'QRCode_{docname}.png', + 'file_name': 'QRCode_{}.png'.format(docname.replace('/', '-')), 'attached_to_doctype': doctype, 'attached_to_name': docname, 'content': 'qrcode', From 00ccec7314283617ea7c89da6600531e6e206bac Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Wed, 13 Jan 2021 21:02:15 +0530 Subject: [PATCH 223/295] fix: subscription prepaid date validation (#24356) --- erpnext/accounts/doctype/subscription/subscription.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 552a5d476b0..e023b47caca 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -446,7 +446,7 @@ class Subscription(Document): if not self.generate_invoice_at_period_start: return False - if self.is_new_subscription(): + if self.is_new_subscription() and getdate() >= getdate(self.current_invoice_start): return True # Check invoice dates and make sure it doesn't have outstanding invoices From 0b04e23f6d5e962c2736a308e0c4053426266bfb Mon Sep 17 00:00:00 2001 From: Anuja Pawar <60467153+Anuja-pawar@users.noreply.github.com> Date: Wed, 13 Jan 2021 21:04:03 +0530 Subject: [PATCH 224/295] fix: BOM Stock Report UoM correction (#24339) --- .../report/bom_stock_report/bom_stock_report.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py index 75ebcbc971b..1c6758e6f36 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py @@ -20,6 +20,7 @@ def get_columns(): _("Item") + ":Link/Item:150", _("Description") + "::300", _("BOM Qty") + ":Float:160", + _("BOM UoM") + "::160", _("Required Qty") + ":Float:120", _("In Stock Qty") + ":Float:120", _("Enough Parts to Build") + ":Float:200", @@ -32,7 +33,7 @@ def get_bom_stock(filters): bom = filters.get("bom") table = "`tabBOM Item`" - qty_field = "qty" + qty_field = "stock_qty" qty_to_produce = filters.get("qty_to_produce", 1) if int(qty_to_produce) <= 0: @@ -40,7 +41,6 @@ def get_bom_stock(filters): if filters.get("show_exploded_view"): table = "`tabBOM Explosion Item`" - qty_field = "stock_qty" if filters.get("warehouse"): warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1) @@ -59,6 +59,7 @@ def get_bom_stock(filters): bom_item.item_code, bom_item.description , bom_item.{qty_field}, + bom_item.stock_uom, bom_item.{qty_field} * {qty_to_produce} / bom.quantity, sum(ledger.actual_qty) as actual_qty, sum(FLOOR(ledger.actual_qty / (bom_item.{qty_field} * {qty_to_produce} / bom.quantity))) From 33fac19bcef75d224728b9e2338c2952187883c1 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Wed, 13 Jan 2021 21:06:04 +0530 Subject: [PATCH 225/295] fix: calculation of remaining_sub_periods if relieving date before month start date (#24319) --- erpnext/payroll/doctype/payroll_period/payroll_period.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/payroll_period/payroll_period.py b/erpnext/payroll/doctype/payroll_period/payroll_period.py index d7893d06572..46f6cd842c3 100644 --- a/erpnext/payroll/doctype/payroll_period/payroll_period.py +++ b/erpnext/payroll/doctype/payroll_period/payroll_period.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import date_diff, getdate, formatdate, cint, month_diff, flt +from frappe.utils import date_diff, getdate, formatdate, cint, month_diff, flt, add_months from frappe.model.document import Document from erpnext.hr.utils import get_holidays_for_employee @@ -88,6 +88,8 @@ def get_period_factor(employee, start_date, end_date, payroll_frequency, payroll period_start = joining_date if relieving_date and getdate(relieving_date) < getdate(period_end): period_end = relieving_date + if month_diff(period_end, start_date) > 1: + start_date = add_months(start_date, - (month_diff(period_end, start_date)+1)) total_sub_periods, remaining_sub_periods = 0.0, 0.0 From 4a649a4fce762ad3c1c0a046da545b287f44e53a Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Wed, 13 Jan 2021 21:10:49 +0530 Subject: [PATCH 226/295] fix: removing payment_field from loan repayment closuer (#24291) --- .../loan_repayment_and_closure/loan_repayment_and_closure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py index b63cc8ed5ac..c6f6b990cc5 100644 --- a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py +++ b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py @@ -103,7 +103,7 @@ def get_data(filters): loan_repayments = frappe.get_all("Loan Repayment", filters = query_filters, - fields=["posting_date", "applicant", "name", "against_loan", "payment_type", "payable_amount", + fields=["posting_date", "applicant", "name", "against_loan", "payable_amount", "pending_principal_amount", "interest_payable", "penalty_amount", "amount_paid"] ) From e62ce4b1729d64ae30081fdc096bb4da11febd50 Mon Sep 17 00:00:00 2001 From: Anuja Pawar <60467153+Anuja-pawar@users.noreply.github.com> Date: Wed, 13 Jan 2021 21:13:12 +0530 Subject: [PATCH 227/295] fix: Add button PO, PI, SI, DN and, Quotation Dashboard (#24187) --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js | 2 +- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 2 +- erpnext/buying/doctype/purchase_order/purchase_order.js | 4 ++-- erpnext/selling/doctype/quotation/quotation.js | 2 +- erpnext/stock/doctype/delivery_note/delivery_note.js | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 7830cfd3702..3863768a8b2 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -498,7 +498,7 @@ cur_frm.cscript.select_print_heading = function(doc,cdt,cdn){ frappe.ui.form.on("Purchase Invoice", { setup: function(frm) { frm.custom_make_buttons = { - 'Purchase Invoice': 'Debit Note', + 'Purchase Invoice': 'Return / Debit Note', 'Payment Entry': 'Payment' } diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 5efc32e11d9..89b716c180d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -592,7 +592,7 @@ frappe.ui.form.on('Sales Invoice', { frm.custom_make_buttons = { 'Delivery Note': 'Delivery', - 'Sales Invoice': 'Sales Return', + 'Sales Invoice': 'Return / Credit Note', 'Payment Request': 'Payment Request', 'Payment Entry': 'Payment' }, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 47483c9d1c3..38532d18f36 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -58,8 +58,8 @@ frappe.ui.form.on("Purchase Order Item", { erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend({ setup: function() { this.frm.custom_make_buttons = { - 'Purchase Receipt': 'Receipt', - 'Purchase Invoice': 'Invoice', + 'Purchase Receipt': 'Purchase Receipt', + 'Purchase Invoice': 'Purchase Invoice', 'Stock Entry': 'Material to Supplier', 'Payment Entry': 'Payment', } diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 661e107e1e9..5a0d9c90655 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -7,7 +7,7 @@ frappe.ui.form.on('Quotation', { setup: function(frm) { frm.custom_make_buttons = { - 'Sales Order': 'Make Sales Order' + 'Sales Order': 'Sales Order' }, frm.set_query("quotation_to", function() { diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 5f2658c1028..cb1e31b15b6 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -13,7 +13,7 @@ frappe.ui.form.on("Delivery Note", { frm.custom_make_buttons = { 'Packing Slip': 'Packing Slip', 'Installation Note': 'Installation Note', - 'Sales Invoice': 'Invoice', + 'Sales Invoice': 'Sales Invoice', 'Stock Entry': 'Return', 'Shipment': 'Shipment' }, From fdad94f98314ee2e4c603f044e4682e3c43dfd69 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 14 Jan 2021 00:40:58 +0530 Subject: [PATCH 228/295] feat(Payroll): compute Year to Date for Salary Slip components --- .../doctype/salary_detail/salary_detail.json | 11 +++- .../doctype/salary_slip/salary_slip.js | 8 +-- .../doctype/salary_slip/salary_slip.py | 53 +++++++++++++++---- 3 files changed, 57 insertions(+), 15 deletions(-) diff --git a/erpnext/payroll/doctype/salary_detail/salary_detail.json b/erpnext/payroll/doctype/salary_detail/salary_detail.json index 5c1eb61281c..9bc25a67107 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 amount spent on this salary component from the beginning of the year (payroll or fiscal) to the current payroll 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-13 17:33:19.184195", "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 51fb3596e9b..945bd452751 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.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 183ad13411a..2d3bc57900d 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)) From 2c315a738eb99e1a04735f257573b21f9de0fc18 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 14 Jan 2021 10:06:24 +0530 Subject: [PATCH 229/295] feat: Salary Slip with Year to Date Print Format --- erpnext/payroll/print_format/__init__.py | 0 .../salary_slip_with_year_to_date/__init__.py | 0 .../salary_slip_with_year_to_date.json | 25 +++++++++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 erpnext/payroll/print_format/__init__.py create mode 100644 erpnext/payroll/print_format/salary_slip_with_year_to_date/__init__.py create mode 100644 erpnext/payroll/print_format/salary_slip_with_year_to_date/salary_slip_with_year_to_date.json diff --git a/erpnext/payroll/print_format/__init__.py b/erpnext/payroll/print_format/__init__.py new file mode 100644 index 00000000000..e69de29bb2d 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 00000000000..e69de29bb2d 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 00000000000..71ba37f6ed2 --- /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 From 49702c1487f7b597b78ff7396e652860cee9ad28 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 14 Jan 2021 11:57:24 +0530 Subject: [PATCH 230/295] test: year to date computation for salary slip components --- .../doctype/salary_slip/salary_slip.js | 2 +- .../doctype/salary_slip/test_salary_slip.py | 36 +++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js index 945bd452751..b50c774fbe4 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.js +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js @@ -139,7 +139,7 @@ frappe.ui.form.on("Salary Slip", { change_grid_labels: function(frm) { let fields = ["amount", "year_to_date", "default_amount", "additional_amount", "tax_on_flexible_benefit", - "tax_on_additional_salary"]; + "tax_on_additional_salary"]; frm.set_currency_labels(fields, frm.doc.currency, "earnings"); frm.set_currency_labels(fields, frm.doc.currency, "deductions"); diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 4368c03c2ae..f58a8e58c20 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: From 814858061f36487641eac763cb53e9982ddcaaf7 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 14 Jan 2021 12:13:08 +0530 Subject: [PATCH 231/295] fix: test for raising MR-SE mismatch error - Make test add mismatched item code instead of warehouse, since warehouse can be different. --- .../stock/doctype/material_request/test_material_request.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 0a29fa05e1a..72a3a5e67c7 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) From 6c90e269829d35c0e3b0e11b84507d08c76a8f5e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 14 Jan 2021 13:42:40 +0530 Subject: [PATCH 232/295] feat: add descriptions for YTD and MTD fields --- erpnext/payroll/doctype/salary_detail/salary_detail.json | 4 ++-- erpnext/payroll/doctype/salary_slip/salary_slip.json | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/payroll/doctype/salary_detail/salary_detail.json b/erpnext/payroll/doctype/salary_detail/salary_detail.json index 9bc25a67107..393f647cc88 100644 --- a/erpnext/payroll/doctype/salary_detail/salary_detail.json +++ b/erpnext/payroll/doctype/salary_detail/salary_detail.json @@ -229,7 +229,7 @@ "fieldtype": "Column Break" }, { - "description": "Total amount spent on this salary component from the beginning of the year (payroll or fiscal) to the current payroll date.", + "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", @@ -239,7 +239,7 @@ ], "istable": 1, "links": [], - "modified": "2021-01-13 17:33:19.184195", + "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.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json index 43deee43aac..9f9691b59d1 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", From bc9eaac3428435f336c7e722c3268c8716ba605a Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 14 Jan 2021 13:47:40 +0530 Subject: [PATCH 233/295] fix: Clear merge conflicts - 'get_project_template' was removed a month back - in 'make_project' insert the project directly as a check for it is there in the beginning. --- erpnext/projects/doctype/project/test_project.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index 715cdd92858..d85c82612a2 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') @@ -108,11 +108,6 @@ def make_project(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}) - if args.project_template_name: - template = make_project_template(args.project_template_name) - else: - template = get_project_template() - project = frappe.get_doc(dict( doctype = 'Project', project_name = args.project_name, @@ -124,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 From 3f015f71428c4d455ddf1859625a0585b8e1406c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 14 Jan 2021 18:44:14 +0530 Subject: [PATCH 234/295] fix: Update fieldnames --- erpnext/projects/doctype/timesheet/timesheet.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index b068245a8ba..b123af5d188 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 From 00981206adc346badc92be97920b86f550a3167e Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Thu, 14 Jan 2021 19:23:18 +0530 Subject: [PATCH 235/295] fix: last purchase rate not updating when voucher cancelled if only one voucher is present (#24322) * fix: last purchase rate not updating * chore: use orm for updating Co-authored-by: Afshan <33727827+AfshanKhan@users.noreply.github.com> --- erpnext/buying/utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/buying/utils.py b/erpnext/buying/utils.py index 47b48665b60..a73cb0d62ec 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 = [] From 4569e52b45cfa3d346b9445cb3840060528fc667 Mon Sep 17 00:00:00 2001 From: Anuja Pawar <60467153+Anuja-pawar@users.noreply.github.com> Date: Thu, 14 Jan 2021 19:24:30 +0530 Subject: [PATCH 236/295] fix: fix for not having fiscal year while creating new company (#24130) * fix: Fiscal year fix while creating new company * fix: for failing Travis * fix: suggested changes Co-authored-by: Afshan <33727827+AfshanKhan@users.noreply.github.com> --- erpnext/regional/india/setup.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 5321a9a3b5a..526198424f3 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): From 847c706d2f624745e6a2c2dcc37f044d4eea5795 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 14 Jan 2021 19:37:13 +0530 Subject: [PATCH 237/295] fix: Ignore group cost center validation for period closing voucher --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index c4412749080..288111b1b69 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -137,9 +137,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) From 5ead7ee6cfa5f564c8e3f1ddd60a13bb82d5c409 Mon Sep 17 00:00:00 2001 From: Anuja P Date: Thu, 14 Jan 2021 20:58:34 +0530 Subject: [PATCH 238/295] fix:Payment Period based on invoice date report fix/refactor --- .../payment_period_based_on_invoice_date.py | 122 +++++++++++++++--- 1 file changed, 105 insertions(+), 17 deletions(-) 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 57a1231f5a9..98731d32ae9 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): From 47b42e64415b5551c0918cc6f43dd049b3c88bfb Mon Sep 17 00:00:00 2001 From: Anuja P Date: Thu, 14 Jan 2021 21:20:00 +0530 Subject: [PATCH 239/295] fix: Translation fixes --- .../payment_period_based_on_invoice_date.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 98731d32ae9..7195c7e0b8b 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 @@ -136,19 +136,19 @@ def get_columns(filters): }, { "fieldname": "range1", - "label": _("0-30"), + "label": "0-30", "fieldtype": "Currency", "width": 140 }, { "fieldname": "range2", - "label": _("30-60"), + "label": "30-60", "fieldtype": "Currency", "width": 140 }, { "fieldname": "range3", - "label": _("60-90"), + "label": "60-90", "fieldtype": "Currency", "width": 140 }, From 060d6472488f314665e17ca0c736a5238c0907d5 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 15 Jan 2021 00:19:14 +0530 Subject: [PATCH 240/295] fix: travis --- .../patient_history_settings/patient_history_settings.py | 4 ++-- .../patient_medical_record/test_patient_medical_record.py | 1 + erpnext/healthcare/page/patient_history/patient_history.js | 2 +- erpnext/hooks.py | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py index 9f18c6bbf52..9374870ac8c 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py @@ -156,8 +156,8 @@ def get_patient_history_config_dt(doctype): def validate_medical_record_required(doc): - if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_setup_wizard or \ - frappe.db.get_value('Doctype', doc.doctype, 'module') != 'Healthcare': + if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_setup_wizard \ + or doc.meta.module != 'Healthcare': return False if doc.doctype not in get_patient_history_doctypes(): 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 419d956425e..c1d9872a019 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/page/patient_history/patient_history.js b/erpnext/healthcare/page/patient_history/patient_history.js index 9c44d63b965..05c5190f807 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.js +++ b/erpnext/healthcare/page/patient_history/patient_history.js @@ -19,7 +19,7 @@ frappe.pages['patient_history'].on_page_load = function(wrapper) { fieldname: 'patient', placeholder: __('Select Patient'), only_select: true, - change: function(){ + change: function() { let patient_id = patient.get_value(); if (pid != patient_id && patient_id) { me.start = 0; diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 9a0e06db7ab..57b0b075047 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -223,7 +223,7 @@ standard_queries = { doc_events = { "*": { "on_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.create_medical_record", - "on_update": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.update_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": { From b15a19c6e038cd05febde2c9390ce864b386a36b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 15 Jan 2021 01:27:30 +0530 Subject: [PATCH 241/295] test: Medical Record creation for custom doctypes --- .../patient_history_settings.py | 6 +- .../test_patient_history_settings.py | 98 ++++++++++++++++++- 2 files changed, 98 insertions(+), 6 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py index 9374870ac8c..7c7b6c8f4be 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py @@ -97,14 +97,12 @@ def set_subject_field(doc): 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) + 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) - - subject += '
    ' + subject += frappe.bold(_(entry.get('label')) + ': ') + cstr(formatted_value) + '
    ' return subject 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 index 548c423670f..3190d844775 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py @@ -3,8 +3,102 @@ # See license.txt from __future__ import unicode_literals -# import frappe +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): - pass + 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( + getdate().strftime("%d-%m-%Y")) + 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 From 1873c389e5eaadc11365b1adcdab664c75478662 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 15 Jan 2021 02:12:57 +0530 Subject: [PATCH 242/295] feat: Only allow submittable doctypes for Patient Medical Record --- .../patient_history_settings.js | 3 ++- .../patient_history_settings.py | 22 ++++++++++++++++--- .../test_patient_history_settings.py | 8 +++---- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js index bf3c5b954e5..92922b2888b 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js @@ -7,7 +7,8 @@ frappe.ui.form.on('Patient History Settings', { return { filters: { custom: 1, - module: 'Healthcare' + is_submittable: 1, + module: 'Healthcare', } }; }); diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py index 7c7b6c8f4be..9ef97214c58 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py @@ -6,14 +6,23 @@ from __future__ import unicode_literals import frappe import json from frappe import _ -from frappe.utils import cstr +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) @@ -155,10 +164,17 @@ def get_patient_history_config_dt(doctype): def validate_medical_record_required(doc): if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_setup_wizard \ - or doc.meta.module != 'Healthcare': + or get_module(doc) != 'Healthcare': return False if doc.doctype not in get_patient_history_doctypes(): return False - return True \ No newline at end of file + 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 index 3190d844775..c93b788aed7 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py @@ -12,7 +12,7 @@ from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment imp class TestPatientHistorySettings(unittest.TestCase): def setUp(self): dt = create_custom_doctype() - settings = frappe.get_single('Patient History Settings') + settings = frappe.get_single("Patient History Settings") settings.append("custom_doctypes", { "document_type": dt.name, "date_fieldname": "date", @@ -40,12 +40,12 @@ class TestPatientHistorySettings(unittest.TestCase): doc = create_doc(patient) # check for medical record - medical_rec = frappe.db.exists('Patient Medical Record', {'status': 'Open', 'reference_name': doc.name}) + 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) + medical_rec = frappe.get_doc("Patient Medical Record", medical_rec) expected_subject = "Date: {0}
    Rating: 3
    Feedback: Test Patient History Settings
    ".format( - getdate().strftime("%d-%m-%Y")) + 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()) From b1d08126b0db8d967d137d64079a8764cc3ca429 Mon Sep 17 00:00:00 2001 From: Rohan Date: Fri, 15 Jan 2021 12:56:30 +0530 Subject: [PATCH 243/295] feat: add "Sync Now" to Plaid Settings (#23602) --- .../doctype/plaid_settings/plaid_settings.js | 16 +++++++++++++ .../doctype/plaid_settings/plaid_settings.py | 23 +++++++++++-------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js index 22a4004955f..f26b130805c 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js @@ -15,6 +15,22 @@ frappe.ui.form.on('Plaid Settings', { 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"); } } }); diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index e535e81bdef..e12d9ee46cc 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,19 @@ 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 + ) From d2c71350cd4e358f7c7579e28a50aa139bd12fa2 Mon Sep 17 00:00:00 2001 From: Kanchan Chauhan Date: Fri, 15 Jan 2021 13:56:03 +0530 Subject: [PATCH 244/295] refactor(Job Application): New fields in Job Applicant and webform (#23326) * refactor(Job Application): New fields in Job Applicant and webform * fix: translation syntax Co-authored-by: Nabin Hait Co-authored-by: Rucha Mahabal --- .../doctype/job_applicant/job_applicant.json | 66 +- .../hr/doctype/job_opening/job_opening.json | 600 +++++------------- erpnext/hr/doctype/job_opening/job_opening.py | 8 +- .../templates/job_opening_row.html | 13 +- .../job_application/job_application.json | 266 +++++--- erpnext/templates/generators/job_opening.html | 13 +- 6 files changed, 447 insertions(+), 519 deletions(-) diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.json b/erpnext/hr/doctype/job_applicant/job_applicant.json index c13548ab82e..1360fd1890a 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 4437e02fc84..b8f6df6f7a6 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 00883d75f19..1e897671770 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 5da8cc82a2e..c015101600a 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 %}
    diff --git a/erpnext/hr/web_form/job_application/job_application.json b/erpnext/hr/web_form/job_application/job_application.json index f630570c4c2..512ba5c5555 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/templates/generators/job_opening.html b/erpnext/templates/generators/job_opening.html index f92e72eaa7e..c562db3c25a 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 %} From e30b33a3b85c14e6a8c7be53d288c625c9d54eef Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 15 Jan 2021 15:47:15 +0530 Subject: [PATCH 245/295] fix: Linting issues --- erpnext/education/doctype/fees/fees.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/education/doctype/fees/fees.js b/erpnext/education/doctype/fees/fees.js index 40f50999adf..ac66acd00f5 100644 --- a/erpnext/education/doctype/fees/fees.js +++ b/erpnext/education/doctype/fees/fees.js @@ -14,15 +14,15 @@ frappe.ui.form.on("Fees", { erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, - onload: function(frm){ - frm.set_query("academic_term",function() { + onload: function(frm) { + frm.set_query("academic_term", function() { return{ "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) From 0c4d61269a60498fb26c9be5e493fc32103acf33 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 15 Jan 2021 15:57:18 +0530 Subject: [PATCH 246/295] fix: test case --- .../test_accounting_dimension_filter.py | 2 ++ 1 file changed, 2 insertions(+) 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 index e822c0c0171..5bb4b6f6b59 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -18,6 +18,7 @@ class TestAccountingDimensionFilter(unittest.TestCase): 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) @@ -25,6 +26,7 @@ class TestAccountingDimensionFilter(unittest.TestCase): 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 = '' From 1564d6ee1fc8a02dd2c6fbfab492f235cc7dbf47 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 15 Jan 2021 19:51:15 +0530 Subject: [PATCH 247/295] fix: Test Case --- .../test_accounting_dimension_filter.py | 8 ++++++++ 1 file changed, 8 insertions(+) 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 index 5bb4b6f6b59..7877abd0263 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -13,6 +13,7 @@ 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) @@ -22,6 +23,7 @@ class TestAccountingDimensionFilter(unittest.TestCase): 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) @@ -34,11 +36,17 @@ class TestAccountingDimensionFilter(unittest.TestCase): 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'}): From 7976d12ed0248c821bc299b94278f914fa6e5d99 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 16 Jan 2021 18:10:01 +0530 Subject: [PATCH 248/295] fix: Applicant-Wise Loan Security Exposure report --- .../__init__.py | 0 .../applicant_wise_loan_security_exposure.js | 9 ++ ...applicant_wise_loan_security_exposure.json | 29 +++++ .../applicant_wise_loan_security_exposure.py | 112 ++++++++++++++++++ .../loan_interest_report.py | 5 +- 5 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 erpnext/loan_management/report/applicant_wise_loan_security_exposure/__init__.py create mode 100644 erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.js create mode 100644 erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.json create mode 100644 erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py 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 00000000000..e69de29bb2d 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 00000000000..e954a3942cf --- /dev/null +++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.js @@ -0,0 +1,9 @@ +// 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": [ + + ] +}; 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 00000000000..a778cd7055d --- /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 00000000000..6b7f3ad3284 --- /dev/null +++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py @@ -0,0 +1,112 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.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": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "Currency", "width": 100}, + {"label": _("% Of Applicant Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "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(loan_security_details) + + for key, qty in iteritems(pledge_values): + row = {} + current_value = flt(qty * loan_security_details.get(key[1])['latest_price']) + 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, + 'portfolio_percent': current_value * 100 / total_value_map.get(key[0]) + }) + + data.append(row) + + return data + +def get_loan_security_details(filters): + update_time = get_datetime() + security_detail_map = {} + + loan_security_price_map = frappe._dict(frappe.db.sql(""" + SELECT loan_security, loan_security_price + 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_list=1)) + + 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))}) + security_detail_map.setdefault(security.loan_security, security) + + return security_detail_map + +def get_applicant_wise_total_loan_security_qty(loan_security_details): + current_pledges = {} + total_value_map = {} + applicant_type_map = {} + applicant_wise_unpledges = {} + + 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' + GROUP BY up.applicant, u.loan_security + """, 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' + GROUP BY lp.applicant, p.loan_security + """, 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)['latest_price'] + + 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/loan_interest_report.py b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py index e039fe82d38..e38770b4ed2 100644 --- a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py @@ -15,6 +15,7 @@ def execute(filters=None): 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}, @@ -37,7 +38,7 @@ def get_active_loan_details(filters): 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"], + "total_interest_payable", "written_off_amount", "status"], filters={"status": ("!=", "Closed")}) loan_list = [d.loan for d in loan_details] @@ -70,7 +71,7 @@ def get_interest_accruals(loans): current_month_start = get_first_day(getdate()) interest_accruals = frappe.get_all("Loan Interest Accrual", - fields=["loan", "interest_amount", "posting_date", "penalty"], + fields=["loan", "interest_amount", "posting_date", "penalty_amount"], filters={"loan": ("in", loans)}) for entry in interest_accruals: From addea9697da2fcb3f63e790ecd8172c417dde19f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 17 Jan 2021 11:36:51 +0530 Subject: [PATCH 249/295] feat: Loan Exposure report --- .../report/loan_security_exposure/__init__.py | 0 .../loan_security_exposure.js | 16 ++++ .../loan_security_exposure.json | 29 +++++++ .../loan_security_exposure.py | 77 +++++++++++++++++++ 4 files changed, 122 insertions(+) create mode 100644 erpnext/loan_management/report/loan_security_exposure/__init__.py create mode 100644 erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.js create mode 100644 erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.json create mode 100644 erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py 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 00000000000..e69de29bb2d 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 00000000000..777f29624a7 --- /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 00000000000..d4dca08212d --- /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 00000000000..dc880f72666 --- /dev/null +++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py @@ -0,0 +1,77 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import get_datetime, 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": _("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}, + ] + + 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) + + for security, value in iteritems(current_pledges): + row = {} + current_value = flt(value['qty'] * loan_security_details.get(security)['latest_price']) + row.update(loan_security_details.get(security)) + row.update({ + 'total_qty': value['qty'], + 'current_value': current_value, + 'portfolio_percent': current_value * 100 / total_portfolio_value, + 'pledged_applicant_count': value['applicant_count'] + }) + + 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 + + + From 914ab7e4b037efd1a662c304add0e06326b3f1bc Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 17 Jan 2021 14:40:32 +0530 Subject: [PATCH 250/295] fix: Add filters and currency in reports --- .../process_loan_security_shortfall.json | 6 +- .../applicant_wise_loan_security_exposure.js | 9 ++- .../applicant_wise_loan_security_exposure.py | 30 ++++++--- .../loan_interest_report.js | 9 ++- .../loan_interest_report.py | 62 +++++++++++++------ .../loan_security_exposure.py | 12 ++-- 6 files changed, 93 insertions(+), 35 deletions(-) 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 ffc36711324..3feb3055a6a 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/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 index e954a3942cf..73d60c40458 100644 --- 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 @@ -4,6 +4,13 @@ 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.py b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py index 6b7f3ad3284..0aff2e3623a 100644 --- 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 @@ -3,6 +3,7 @@ from __future__ import unicode_literals import frappe +import erpnext from frappe import _ from frappe.utils import get_datetime, flt from six import iteritems @@ -24,9 +25,10 @@ def get_columns(filters): {"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": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "Currency", "width": 100}, - {"label": _("% Of Applicant Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100} + {"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "currency", "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 @@ -34,7 +36,10 @@ def get_columns(filters): 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(loan_security_details) + 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): row = {} @@ -43,9 +48,10 @@ def get_data(filters): row.update({ 'applicant_type': applicant_type_map.get(key[0]), 'applicant_name': key[0], - 'total_qty': qty, + 'total_qty': qty, 'current_value': current_value, - 'portfolio_percent': current_value * 100 / total_value_map.get(key[0]) + 'portfolio_percent': flt(current_value * 100 / total_value_map.get(key[0]), 2), + 'currency': currency }) data.append(row) @@ -73,19 +79,24 @@ def get_loan_security_details(filters): return security_detail_map -def get_applicant_wise_total_loan_security_qty(loan_security_details): +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 - """, as_dict=1) + """.format(conditions=conditions), filters, as_dict=1) for unpledge in unpledges: applicant_wise_unpledges.setdefault((unpledge.applicant, unpledge.loan_security), unpledge.qty) @@ -95,8 +106,9 @@ def get_applicant_wise_total_loan_security_qty(loan_security_details): FROM `tabLoan Security Pledge` lp, `tabPledge`p WHERE p.parent = lp.name AND lp.status = 'Pledged' + {conditions} GROUP BY lp.applicant, p.loan_security - """, as_dict=1) + """.format(conditions=conditions), filters, as_dict=1) for security in pledges: current_pledges.setdefault((security.applicant, security.loan_security), security.qty) 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 index 852e3ca366f..a227b6d7973 100644 --- a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js @@ -4,6 +4,13 @@ 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.py b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py index e38770b4ed2..730176f4a96 100644 --- a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import frappe +import erpnext from frappe import _ from frappe.utils import flt, get_first_day, getdate @@ -19,33 +20,41 @@ def get_columns(filters): {"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": _("Interest For The Month"), "fieldname": "month_interest", "fieldtype": "Currency", "options": "Currency", "width": 100}, - {"label": _("Penalty For The Month"), "fieldname": "month_penalty", "fieldtype": "Currency", "options": "Currency", "width": 100}, - {"label": _("Accrued Interest"), "fieldname": "accrued_interest", "fieldtype": "Currency", "options": "Currency", "width": 100}, - {"label": _("Total Repayment"), "fieldname": "total_repayment", "fieldtype": "Currency", "options": "Currency", "width": 100}, - {"label": _("Principal Outstanding"), "fieldname": "principal_outstanding", "fieldtype": "Currency", "options": "Currency", "width": 100}, - {"label": _("Interest Outstanding"), "fieldname": "interest_outstanding", "fieldtype": "Currency", "options": "Currency", "width": 100}, - {"label": _("Total Outstanding"), "fieldname": "total_payment", "fieldtype": "Currency", "options": "Currency", "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": _("Interest For The Month"), "fieldname": "month_interest", "fieldtype": "Currency", "options": "currency", "width": 120}, + {"label": _("Penalty For The Month"), "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": _("Interest %"), "fieldname": "rate_of_interest", "fieldtype": "Percent", "width": 100}, - {"label": _("Penalty Interest %"), "fieldname": "precentage_percentage", "fieldtype": "Percent", "width": 100}, + {"label": _("Penalty Interest %"), "fieldname": "penalty_interest", "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={"status": ("!=", "Closed")}) + filters=filter_obj) loan_list = [d.loan for d in loan_details] 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({ @@ -54,8 +63,16 @@ def get_active_loan_details(filters): - flt(loan.total_interest_payable) - flt(loan.written_off_amount), "total_repayment": flt(payments.get(loan.loan)), "month_interest": flt(accrual_map.get(loan.loan, {}).get("month_interest")), - "accrued_interest": flt(accrual_map.get(loan.loan, {}).get("accrued_interest")) + "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), + "currency": currency }) + + loan['total_outstanding'] = loan['principal_outstanding'] + loan['interest_outstanding'] \ + + loan['penalty'] + return loan_details def get_sanctioned_amount_map(): @@ -71,18 +88,25 @@ def get_interest_accruals(loans): current_month_start = get_first_day(getdate()) interest_accruals = frappe.get_all("Loan Interest Accrual", - fields=["loan", "interest_amount", "posting_date", "penalty_amount"], - filters={"loan": ("in", loans)}) + fields=["loan", "interest_amount", "posting_date", "penalty_amount", + "paid_interest_amount"], filters={"loan": ("in", loans)}, order_by="posting_date") for entry in interest_accruals: accrual_map.setdefault(entry.loan, { - 'month_interest': 0.0, - 'accrued_interest': 0.0 + "month_interest": 0.0, + "accrued_interest": 0.0, + "interest_outstanding": 0.0 }) if getdate(entry.posting_date) < getdate(current_month_start): - accrual_map[entry.loan]['accrued_interest'] += entry.interest_amount + accrual_map[entry.loan]["accrued_interest"] += entry.interest_amount else: - accrual_map[entry.loan]['month_interest'] += entry.interest_amount + accrual_map[entry.loan]["month_interest"] += entry.interest_amount - return accrual_map \ No newline at end of file + accrual_map[entry.loan]["interest_outstanding"] += entry.interest_amount - entry.paid_interest_amount + 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)) \ 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 index dc880f72666..2f4d98066b8 100644 --- a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py +++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import frappe +import erpnext from frappe import _ from frappe.utils import get_datetime, flt from six import iteritems @@ -23,10 +24,11 @@ def get_columns(filters): {"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": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "Currency", "width": 100}, + {"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "currency", "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 @@ -35,6 +37,7 @@ 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): row = {} @@ -43,8 +46,9 @@ def get_data(filters): row.update({ 'total_qty': value['qty'], 'current_value': current_value, - 'portfolio_percent': current_value * 100 / total_portfolio_value, - 'pledged_applicant_count': value['applicant_count'] + 'portfolio_percent': flt(current_value * 100 / total_portfolio_value, 2), + 'pledged_applicant_count': value['applicant_count'], + 'currency': currency }) data.append(row) From ec600631559cfde903ab9af874156e6a46984597 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 17 Jan 2021 17:39:51 +0530 Subject: [PATCH 251/295] fix: Auto Loan Write Off --- erpnext/loan_management/doctype/loan/loan.py | 24 ++++++++++--------- .../doctype/loan_type/loan_type.json | 6 ++--- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index 2e0a4d13ab2..e607d4f3cbf 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_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json index 18a97315f0a..3ef53044c20 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", From dd25ecb70deb94bc2257df8e2d843da31ff7006a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 17 Jan 2021 17:52:02 +0530 Subject: [PATCH 252/295] fix: Add reports in Desk Page --- erpnext/loan_management/desk_page/loan/loan.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/desk_page/loan/loan.json b/erpnext/loan_management/desk_page/loan/loan.json index fc59c193251..75036bd0972 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", From 2cb12e6f70f0510c0fa99c6558b4420247403942 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 17 Jan 2021 18:38:21 +0530 Subject: [PATCH 253/295] fix: Loan Security name fetch --- erpnext/loan_management/doctype/loan/test_loan.py | 4 ++-- .../loan_security_price/loan_security_price.json | 11 ++++++++++- erpnext/loan_management/doctype/pledge/pledge.json | 10 +++++++++- .../doctype/proposed_pledge/proposed_pledge.json | 10 +++++++++- .../loan_management/doctype/unpledge/unpledge.json | 10 +++++++++- 5 files changed, 39 insertions(+), 6 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 2abd7d84d97..f3c9db62338 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_security_price/loan_security_price.json b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json index a55b482bd66..b6e87637567 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/pledge/pledge.json b/erpnext/loan_management/doctype/pledge/pledge.json index 801e3a31173..c23479c8251 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/proposed_pledge/proposed_pledge.json b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json index 3e7e778a259..a0b3a79b56c 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 00356685eb9..0091e6c43d4 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", From 52172252b4ab9533866d323a74ca6a2fc3e1140f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 18 Jan 2021 12:07:54 +0530 Subject: [PATCH 254/295] fix: Interest calculations in Loan Interest Report --- .../applicant_wise_loan_security_exposure.py | 1 - .../loan_interest_report.py | 40 ++++++++++++------- .../loan_security_exposure.py | 4 +- 3 files changed, 27 insertions(+), 18 deletions(-) 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 index 0aff2e3623a..6d7c3b730db 100644 --- 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 @@ -59,7 +59,6 @@ def get_data(filters): return data def get_loan_security_details(filters): - update_time = get_datetime() security_detail_map = {} loan_security_price_map = frappe._dict(frappe.db.sql(""" 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 index 730176f4a96..aa0325ef35c 100644 --- a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import erpnext from frappe import _ -from frappe.utils import flt, get_first_day, getdate +from frappe.utils import flt, getdate, add_days def execute(filters=None): @@ -22,13 +22,13 @@ def get_columns(filters): {"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": _("Interest For The Month"), "fieldname": "month_interest", "fieldtype": "Currency", "options": "currency", "width": 120}, - {"label": _("Penalty For The Month"), "fieldname": "penalty", "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": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100}, @@ -62,11 +62,11 @@ def get_active_loan_details(filters): "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)), - "month_interest": flt(accrual_map.get(loan.loan, {}).get("month_interest")), "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")), "currency": currency }) @@ -85,26 +85,38 @@ def get_payments(loans): def get_interest_accruals(loans): accrual_map = {} - current_month_start = get_first_day(getdate()) interest_accruals = frappe.get_all("Loan Interest Accrual", fields=["loan", "interest_amount", "posting_date", "penalty_amount", - "paid_interest_amount"], filters={"loan": ("in", loans)}, order_by="posting_date") + "paid_interest_amount", "accrual_type"], filters={"loan": ("in", loans)}, order_by="posting_date desc") for entry in interest_accruals: accrual_map.setdefault(entry.loan, { - "month_interest": 0.0, "accrued_interest": 0.0, - "interest_outstanding": 0.0 + "undue_interest": 0.0, + "interest_outstanding": 0.0, + "last_accrual_date": '', + "due_date": '' }) - if getdate(entry.posting_date) < getdate(current_month_start): - accrual_map[entry.loan]["accrued_interest"] += entry.interest_amount - else: - accrual_map[entry.loan]["month_interest"] += entry.interest_amount + 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 - accrual_map[entry.loan]["interest_outstanding"] += entry.interest_amount - entry.paid_interest_amount - accrual_map[entry.loan]["penalty"] = entry.penalty_amount + 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 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 index 2f4d98066b8..3ef10c0f419 100644 --- a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py +++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py @@ -2,10 +2,9 @@ # 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 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 @@ -56,7 +55,6 @@ def get_data(filters): 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) From ea19434af4b2b511086d1ea80b952b2ce264e1fc Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 18 Jan 2021 13:53:52 +0530 Subject: [PATCH 255/295] feat: Issue Analytics Script Report (#23604) * feat: Issue Analytics Report * feat: add more filters, code clean-up * fix: add report link to desk page * test: Issue Analytics Report * fix: sider issues * fix: test * debug: travis * debug: travis * fix: travis * fix: travis Co-authored-by: Marica Co-authored-by: Nabin Hait --- .../support/desk_page/support/support.json | 4 +- erpnext/support/doctype/issue/issue.py | 2 +- erpnext/support/doctype/issue/test_issue.py | 8 +- .../report/issue_analytics/__init__.py | 0 .../report/issue_analytics/issue_analytics.js | 141 +++++++++++ .../issue_analytics/issue_analytics.json | 26 ++ .../report/issue_analytics/issue_analytics.py | 222 ++++++++++++++++++ .../issue_analytics/test_issue_analytics.py | 211 +++++++++++++++++ 8 files changed, 609 insertions(+), 5 deletions(-) create mode 100644 erpnext/support/report/issue_analytics/__init__.py create mode 100644 erpnext/support/report/issue_analytics/issue_analytics.js create mode 100644 erpnext/support/report/issue_analytics/issue_analytics.json create mode 100644 erpnext/support/report/issue_analytics/issue_analytics.py create mode 100644 erpnext/support/report/issue_analytics/test_issue_analytics.py diff --git a/erpnext/support/desk_page/support/support.json b/erpnext/support/desk_page/support/support.json index 18cf87ab0ba..dba2b1463f5 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 62b39cced53..02d10a4ddad 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -214,7 +214,7 @@ 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() + self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) 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 c962dc6b317..483bb155dbf 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 00000000000..e69de29bb2d 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 00000000000..f87b2c2dddc --- /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 00000000000..dd18498d1da --- /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 00000000000..0b629151a6b --- /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 00000000000..432906db9b9 --- /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 From 20e5315480fc48c6c04ad14131ae6a7862743a1c Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 18 Jan 2021 14:56:55 +0530 Subject: [PATCH 256/295] feat: Allow selecting admission service unit in Patient Appointment for inpatients --- erpnext/controllers/queries.py | 28 +++++++++++++++++++ .../inpatient_medication_entry.py | 2 +- .../patient_appointment.js | 10 +++---- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index e3aac9aba85..81f0ad3fed1 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -655,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/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py index bba521313df..e7319085e46 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/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js index 79e1775b9db..3d9f8788de8 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js @@ -31,14 +31,14 @@ 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 } - }; + } }); frm.set_query('therapy_plan', function() { From fa2b0d43bd2b1035709285e0207d7c7806066a1b Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 18 Jan 2021 15:27:40 +0530 Subject: [PATCH 257/295] fix: handled invoices with no item_code --- .../item_wise_purchase_register.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 a36e7f8581f..8a7ea72c161 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 @@ -53,8 +53,8 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum 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 "", 'description': d.description, 'invoice': d.parent, 'posting_date': d.posting_date, @@ -315,7 +315,7 @@ 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 Item`.`item_code`, `tabPurchase Invoice Item`.description, + `tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description, `tabPurchase Invoice Item`.`item_name`, `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`, From 6bec696396307d496fff7e70b38caf378d85c4f7 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 18 Jan 2021 16:21:56 +0530 Subject: [PATCH 258/295] fix: item_group fallback --- .../item_wise_purchase_register.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 8a7ea72c161..eeb5140bbe2 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 @@ -54,7 +54,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum row = { 'item_code': d.item_code, 'item_name': item_record.item_name if item_record else d.item_name, - 'item_group': item_record.item_group if item_record else "", + '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 +315,8 @@ 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 Item`.`item_code`, `tabPurchase Invoice Item`.description, `tabPurchase Invoice Item`.`item_name`, + `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`, From 7b8eac958ec0a60229620425c3dbaa35a8add7ff Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 18 Jan 2021 16:44:41 +0530 Subject: [PATCH 259/295] feat: re-linking bank accounts with plaid (#24392) --- erpnext/accounts/doctype/bank/bank.js | 85 ++++++++++++++++++- .../doctype/plaid_settings/plaid_connector.py | 23 +++-- .../doctype/plaid_settings/plaid_settings.js | 18 ++-- .../doctype/plaid_settings/plaid_settings.py | 6 +- 4 files changed, 118 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/bank/bank.js b/erpnext/accounts/doctype/bank/bank.js index de9498e0752..49b2b186c4b 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/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py index 8d4b5104905..66d0e5f77db 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 f26b130805c..bbc2ca8846c 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js @@ -12,7 +12,7 @@ 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); }); @@ -46,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) @@ -94,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) { @@ -123,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 e12d9ee46cc..70c7f3fe5d7 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -230,7 +230,6 @@ def automatic_synchronization(): if settings.enabled == 1 and settings.automatic_sync == 1: enqueue_synchronization() - @frappe.whitelist() def enqueue_synchronization(): plaid_accounts = frappe.get_all("Bank Account", @@ -243,3 +242,8 @@ def enqueue_synchronization(): 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) From f39cbd3a1d170a087cd7f35db4474684f6169798 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 19 Jan 2021 13:59:03 +0530 Subject: [PATCH 260/295] test: appointment booking for admission service unit --- .../inpatient_record/test_inpatient_record.py | 10 ++- .../patient_appointment.js | 2 +- .../patient_appointment.py | 14 ++++ .../test_patient_appointment.py | 64 ++++++++++++++++++- 4 files changed, 83 insertions(+), 7 deletions(-) diff --git a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py index 10990d412d8..8a918b02751 100644 --- a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py @@ -142,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/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js index 3d9f8788de8..3d5073b13e7 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js @@ -38,7 +38,7 @@ frappe.ui.form.on('Patient Appointment', { company: frm.doc.company, inpatient_record: frm.doc.inpatient_record } - } + }; }); frm.set_query('therapy_plan', function() { diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index dc820cb464e..b05c673d84c 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 b681ed1a226..78708139806 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,61 @@ 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.lab_test.test_lab_test import create_patient_encounter + 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.lab_test.test_lab_test import create_patient_encounter + 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 +180,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 +191,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(): From a28579a1302e1c301c41f623c35b21902d002458 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 19 Jan 2021 14:16:44 +0530 Subject: [PATCH 261/295] fix: sider issues --- .../doctype/patient_appointment/test_patient_appointment.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index 78708139806..6886f318431 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -80,7 +80,6 @@ class TestPatientAppointment(unittest.TestCase): 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.lab_test.test_lab_test import create_patient_encounter from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import \ create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy @@ -107,7 +106,6 @@ class TestPatientAppointment(unittest.TestCase): 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.lab_test.test_lab_test import create_patient_encounter from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import \ create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy From 97b9995f8f3396fcae2f9b6cb3d12b4109262dbc Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Tue, 19 Jan 2021 19:01:13 +0530 Subject: [PATCH 262/295] fix: use supplied year for IRS 1099 forms --- .../irs_1099_form/irs_1099_form.json | 49 ++++---- erpnext/regional/report/irs_1099/irs_1099.js | 12 +- erpnext/regional/report/irs_1099/irs_1099.py | 109 +++++++++++------- 3 files changed, 99 insertions(+), 71 deletions(-) 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 ce8c44a9a19..e59700f5a5e 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/irs_1099/irs_1099.js b/erpnext/regional/report/irs_1099/irs_1099.js index 2d74652cfe2..070ff43f78c 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 d3509e500f8..c1c8aedc9f3 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 From c69ab6d184f2927bb8a148d2e3d20fc32ee0eba5 Mon Sep 17 00:00:00 2001 From: Afshan Date: Tue, 19 Jan 2021 19:29:31 +0530 Subject: [PATCH 263/295] fix: select sal comp while making sal struct --- erpnext/payroll/doctype/salary_structure/salary_structure.js | 4 ++++ erpnext/payroll/doctype/salary_structure/salary_structure.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.js b/erpnext/payroll/doctype/salary_structure/salary_structure.js index ba824c5d6fa..6c7b382264f 100755 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.js +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.js @@ -70,6 +70,9 @@ frappe.ui.form.on('Salary Structure', { }); }, + company: function(frm) { + frm.trigger('set_earning_deduction_component'); + }, currency: function(frm) { calculate_totals(frm.doc); @@ -117,6 +120,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 77914bb5319..340f4e85696 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py @@ -216,7 +216,7 @@ def get_earning_deduction_components(doctype, txt, searchfield, start, page_len, return frappe.db.sql(""" select t1.salary_component from `tabSalary Component` t1, `tabSalary Component Account` t2 - where t1.salary_component = t2.parent + where t1.name = t2.parent and t1.type = %s and t2.company = %s order by salary_component From e2ed2324c3eb50f6bb5a67e95f218db6153f8a5d Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 20 Jan 2021 13:17:42 +0530 Subject: [PATCH 264/295] fix: (e-invoicing) qrcode image generation (#24395) * fix: invalid taxable value of item for e-invoice * fix: qrcode image duplicate error * fix: net total discount calculation * fix: auto fetch auth token --- erpnext/regional/india/e_invoice/utils.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index d0cac90e4df..83635a199ef 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -161,9 +161,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 +198,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,7 +217,10 @@ 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) + 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) invoice_value_details.invoice_discount_amt = 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) @@ -473,7 +476,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 +489,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) @@ -757,7 +761,7 @@ class GSPConnector(): 'label': _('IRN Generated') } self.update_invoice() - + def attach_qrcode_image(self): qrcode = self.invoice.signed_qr_code doctype = self.invoice.doctype @@ -768,7 +772,7 @@ class GSPConnector(): 'file_name': 'QRCode_{}.png'.format(docname.replace('/', '-')), 'attached_to_doctype': doctype, 'attached_to_name': docname, - 'content': 'qrcode', + 'content': str(base64.b64encode(os.urandom(64))), 'is_private': 1 }) _file.insert() From 912647f4f23f2b6a6a7c10290682e757fdf840f4 Mon Sep 17 00:00:00 2001 From: Afshan Date: Wed, 20 Jan 2021 13:58:32 +0530 Subject: [PATCH 265/295] fix: allow statistical component in salary structure. --- .../payroll/doctype/salary_structure/salary_structure.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py index 340f4e85696..bf1c7469645 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py @@ -216,8 +216,9 @@ def get_earning_deduction_components(doctype, txt, searchfield, start, page_len, return frappe.db.sql(""" select t1.salary_component from `tabSalary Component` t1, `tabSalary Component Account` t2 - where t1.name = t2.parent + where (t1.name = t2.parent and t1.type = %s - and t2.company = %s + and t2.company = %s) + or (t1.statistical_component = 1) order by salary_component - """, (filters['type'], filters['company']) ) + """, (filters['type'], filters['company'])) From 36e3e05a0edcbe2736df77a9c57c7eae63f25f30 Mon Sep 17 00:00:00 2001 From: Afshan Date: Wed, 20 Jan 2021 16:48:42 +0530 Subject: [PATCH 266/295] fix: query for salary componet in salary structure --- .../doctype/salary_structure/salary_structure.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py index bf1c7469645..e71803172c5 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py @@ -217,8 +217,12 @@ def get_earning_deduction_components(doctype, txt, searchfield, start, page_len, select t1.salary_component from `tabSalary Component` t1, `tabSalary Component Account` t2 where (t1.name = t2.parent - and t1.type = %s - and t2.company = %s) - or (t1.statistical_component = 1) + and t1.type = %(type)s + and t2.company = %(company)s) + or (t1.type = %(type)s + and t1.statistical_component = 1) order by salary_component - """, (filters['type'], filters['company'])) + """,{ + "type": filters['type'], + "company": filters['company'] + }) From 3575939386154a78bc58334383703def9e00bd88 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 20 Jan 2021 20:48:51 +0530 Subject: [PATCH 267/295] fix: sider issues --- erpnext/non_profit/doctype/membership/membership.py | 4 ++-- .../non_profit/doctype/membership/test_membership.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 853d7f51f8f..5c32c81242e 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -63,7 +63,7 @@ class Membership(Document): 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")) @@ -140,7 +140,7 @@ class Membership(Document): frappe.sendmail(**email_args) def generate_and_send_invoice(self): - invoice = self.generate_invoice(False) + invoice = self.generate_invoice(save=False) self.send_acknowlement() def make_invoice(membership, member, plan, settings): diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py index ce31b919562..a7fad9debe9 100644 --- a/erpnext/non_profit/doctype/membership/test_membership.py +++ b/erpnext/non_profit/doctype/membership/test_membership.py @@ -6,14 +6,14 @@ import unittest import frappe import erpnext from erpnext.non_profit.doctype.member.member import create_member -from frappe.utils import nowdate, getdate, add_months +from frappe.utils import nowdate, add_months from erpnext.stock.doctype.item.test_item import create_item class TestMembership(unittest.TestCase): 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 @@ -58,11 +58,11 @@ class TestMembership(unittest.TestCase): # 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), @@ -70,7 +70,7 @@ class TestMembership(unittest.TestCase): frappe.set_user("Administrator") # create the same membership but as administrator - new_entry = make_membership(self.member, { + make_membership(self.member, { "from_date": add_months(nowdate(), 2), "to_date": add_months(nowdate(), 3), }) From fa4b3ba505b45c5f99de77ebf0ffe36d40360887 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 20 Jan 2021 21:59:27 +0530 Subject: [PATCH 268/295] fix: basic validations for Membership Type Linked Item --- .../doctype/membership_type/membership_type.js | 12 ++++++++++-- .../doctype/membership_type/membership_type.py | 6 +++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.js b/erpnext/non_profit/doctype/membership_type/membership_type.js index 94ccdd83345..5bd8a6cf6ec 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.js +++ b/erpnext/non_profit/doctype/membership_type/membership_type.js @@ -3,12 +3,20 @@ frappe.ui.form.on('Membership Type', { refresh: function (frm) { - frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => { + 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_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 b95b04316f2..38e6f655566 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.py +++ b/erpnext/non_profit/doctype/membership_type/membership_type.py @@ -7,7 +7,11 @@ from frappe.model.document import Document import frappe 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 From bc49815d546719f13b1513822d5aee5b6c1e7022 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 20 Jan 2021 23:14:27 +0530 Subject: [PATCH 269/295] refactor: missing validations, code clean-up --- erpnext/non_profit/doctype/member/member.py | 8 +- .../doctype/membership/membership.js | 18 +- .../doctype/membership/membership.py | 167 ++++++++++-------- .../membership_settings.js | 23 ++- 4 files changed, 131 insertions(+), 85 deletions(-) diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index f40f278fedd..04b99f93f21 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -55,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 ee8a8c0a7ba..99aabf39268 100644 --- a/erpnext/non_profit/doctype/membership/membership.js +++ b/erpnext/non_profit/doctype/membership/membership.js @@ -4,16 +4,22 @@ 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) { !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 +33,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.py b/erpnext/non_profit/doctype/membership/membership.py index 5c32c81242e..5f9a98cd1c1 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -14,25 +14,31 @@ 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 + self.create_member_from_website_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 + 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(self.member) @@ -40,7 +46,7 @@ class Membership(Document): 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": @@ -57,7 +63,7 @@ class Membership(Document): if status_changed_to not in ("Completed", "Authorized"): return self.load_from_db() - self.db_set('paid', 1) + 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) @@ -71,47 +77,59 @@ 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: - if not settings.payment_account: - frappe.throw(_("You need to set Payment Account in Membership Settings")) - - 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() + 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))) @@ -135,50 +153,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(save=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): if frappe.flags.in_test: return True - signature = frappe.request.headers.get('X-Razorpay-Signature') + signature = frappe.request.headers.get("X-Razorpay-Signature") settings = frappe.get_doc("Membership Settings") key = settings.get_webhook_secret() @@ -187,6 +211,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) @@ -195,16 +220,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: @@ -214,15 +239,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) @@ -252,28 +277,30 @@ 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 diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js index 1d894027b01..c95aab2a7a1 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 } }; }); From 53d0eebbe850bc98ed3b84824239af9bd96d1b57 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 20 Jan 2021 23:22:08 +0530 Subject: [PATCH 270/295] fix: patch for renaming field in Membership Settings --- erpnext/non_profit/doctype/membership/membership.py | 4 ++-- erpnext/patches/v13_0/update_member_email_address.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 5f9a98cd1c1..db4481e40db 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -284,13 +284,13 @@ def trigger_razorpay_subscription(*args, **kwargs): def notify_failure(log): try: - content = _(""" + 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)) + """.format(get_link_to_form("Error Log", log.name)) sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content) except: diff --git a/erpnext/patches/v13_0/update_member_email_address.py b/erpnext/patches/v13_0/update_member_email_address.py index da7828adbcb..38843e31bfd 100644 --- a/erpnext/patches/v13_0/update_member_email_address.py +++ b/erpnext/patches/v13_0/update_member_email_address.py @@ -3,10 +3,11 @@ 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"): @@ -17,3 +18,5 @@ def execute(): # Set the value for it frappe.db.set_value("Member", member, "email_id", email) + + rename_field("Membership Settings", "enable_auto_invoicing", "enable_invoicing") From 4284ad880bd46fb2c061085a5b8a73229a87e513 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 21 Jan 2021 11:54:14 +0530 Subject: [PATCH 271/295] fix: create member from membership for website users only --- erpnext/non_profit/doctype/membership/membership.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index db4481e40db..c58e35a73eb 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -18,7 +18,11 @@ class Membership(Document): def validate(self): if not self.member or not frappe.db.exists("Member", self.member): # for web forms - self.create_member_from_website_user() + 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")) self.validate_membership_period() From 3af46cc6cccc100906d975a1babcfb46e89577cf Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 21 Jan 2021 11:59:34 +0530 Subject: [PATCH 272/295] fix: show custom buttons after save --- erpnext/non_profit/doctype/membership/membership.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/non_profit/doctype/membership/membership.js b/erpnext/non_profit/doctype/membership/membership.js index 99aabf39268..573ac3319a4 100644 --- a/erpnext/non_profit/doctype/membership/membership.js +++ b/erpnext/non_profit/doctype/membership/membership.js @@ -9,6 +9,9 @@ frappe.ui.form.on('Membership', { }, refresh: function(frm) { + if (frm.doc.__islocal) + return; + !frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => { frm.call({ doc: frm.doc, From b48eab972ed42e7b4594d0110673d08a8ce56bae Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 21 Jan 2021 13:00:10 +0530 Subject: [PATCH 273/295] fix: sider issues --- erpnext/non_profit/doctype/membership_type/membership_type.js | 4 ++-- erpnext/non_profit/doctype/membership_type/membership_type.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.js b/erpnext/non_profit/doctype/membership_type/membership_type.js index 5bd8a6cf6ec..91a5cb74ba1 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.js +++ b/erpnext/non_profit/doctype/membership_type/membership_type.js @@ -16,7 +16,7 @@ frappe.ui.form.on('Membership Type', { 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 38e6f655566..022829bd3a6 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.py +++ b/erpnext/non_profit/doctype/membership_type/membership_type.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals from frappe.model.document import Document import frappe +from frappe import _ class MembershipType(Document): def validate(self): From d98b5064784b6d3696d2685839d5fe8ef72106db Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 21 Jan 2021 13:23:59 +0530 Subject: [PATCH 274/295] fix: patch --- erpnext/patches/v13_0/update_member_email_address.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v13_0/update_member_email_address.py b/erpnext/patches/v13_0/update_member_email_address.py index 38843e31bfd..4056f84069c 100644 --- a/erpnext/patches/v13_0/update_member_email_address.py +++ b/erpnext/patches/v13_0/update_member_email_address.py @@ -19,4 +19,5 @@ def execute(): # Set the value for it frappe.db.set_value("Member", member, "email_id", email) - rename_field("Membership Settings", "enable_auto_invoicing", "enable_invoicing") + if frappe.db.exists("DocType", "Membership Settings"): + rename_field("Membership Settings", "enable_auto_invoicing", "enable_invoicing") From b9781a46759dea0cf80bc1b2395f9c951fb88c70 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 21 Jan 2021 15:13:29 +0530 Subject: [PATCH 275/295] fix: membership test cases --- erpnext/non_profit/doctype/membership/test_membership.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py index a7fad9debe9..db565270241 100644 --- a/erpnext/non_profit/doctype/membership/test_membership.py +++ b/erpnext/non_profit/doctype/membership/test_membership.py @@ -33,7 +33,7 @@ class TestMembership(unittest.TestCase): 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.linked_item = create_item("_Test Item for Non Profit Membership", is_stock_item=0).name plan.insert() # make test member From f1cca59d80ff447cb360780a18b34f2af1f8c90e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 21 Jan 2021 16:36:15 +0530 Subject: [PATCH 276/295] feat: update expiry for memberships --- erpnext/hooks.py | 3 ++- .../doctype/membership/membership.json | 16 ++++++++++++++-- .../non_profit/doctype/membership/membership.py | 9 +++++++++ .../doctype/membership/membership_list.js | 15 +++++++++++++++ 4 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 erpnext/non_profit/doctype/membership/membership_list.js diff --git a/erpnext/hooks.py b/erpnext/hooks.py index a2d9d861bb8..f7ec1c1b6e4 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -341,7 +341,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", diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json index 7f218966a02..6da053f9fc4 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 c58e35a73eb..c113b80d56f 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -308,3 +308,12 @@ def get_plan_from_razorpay_id(plan_id): 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 00000000000..a959159899d --- /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']; + } + } +}; From eee71f37d80d45a172effdada150d91c0d227f5f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 21 Jan 2021 17:47:20 +0530 Subject: [PATCH 277/295] fix: test --- .../doctype/membership/test_membership.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py index db565270241..2d9b336f8c8 100644 --- a/erpnext/non_profit/doctype/membership/test_membership.py +++ b/erpnext/non_profit/doctype/membership/test_membership.py @@ -7,7 +7,6 @@ import frappe import erpnext from erpnext.non_profit.doctype.member.member import create_member from frappe.utils import nowdate, add_months -from erpnext.stock.doctype.item.test_item import create_item class TestMembership(unittest.TestCase): def setUp(self): @@ -33,7 +32,7 @@ class TestMembership(unittest.TestCase): 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", is_stock_item=0).name + plan.linked_item = create_item("_Test Item for Non Profit Membership").name plan.insert() # make test member @@ -92,4 +91,18 @@ def make_membership(member, payload={}): data.update(payload) membership = frappe.get_doc(data) membership.insert(ignore_permissions=True, ignore_if_duplicate=True) - return membership \ No newline at end of file + 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 From 96edfd93c925465ff1d51890365804c13a722230 Mon Sep 17 00:00:00 2001 From: Afshan Date: Thu, 21 Jan 2021 18:22:35 +0530 Subject: [PATCH 278/295] fix: stock ageing should not take cancelled stock entries. --- erpnext/stock/report/stock_ageing/stock_ageing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 8aaf7abcbe4..ff603fcfb3a 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), From 577d2bed6ec0d919b89ff55bb442b629d5e1c5af Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 21 Jan 2021 18:43:55 +0530 Subject: [PATCH 279/295] fix: failing einvoice test (#24434) --- .../sales_invoice/test_sales_invoice.py | 27 ++++--------------- erpnext/regional/india/e_invoice/utils.py | 9 ++++--- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 5435d3b21e6..3a6dbeb51c2 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1861,23 +1861,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 +1913,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/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 83635a199ef..eb210be16a5 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -217,11 +217,14 @@ def update_item_taxes(invoice, item): def get_invoice_value_details(invoice): invoice_value_details = frappe._dict(dict()) + 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) - invoice_value_details.invoice_discount_amt = invoice.base_discount_amount + + # 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) @@ -247,9 +250,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 From bcc0674d37655206cc3cb00900978db10fd40131 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 21 Jan 2021 19:52:13 +0530 Subject: [PATCH 280/295] fix: test --- .../doctype/membership/test_membership.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py index 2d9b336f8c8..ff7e6c473c5 100644 --- a/erpnext/non_profit/doctype/membership/test_membership.py +++ b/erpnext/non_profit/doctype/membership/test_membership.py @@ -28,12 +28,15 @@ class TestMembership(unittest.TestCase): settings.save() # make test plan - 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() + 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({ @@ -42,7 +45,7 @@ class TestMembership(unittest.TestCase): 'plan_id': plan.name })) self.member_doc.make_customer_and_link() - self.member = "self.member_doc.name" + self.member = self.member_doc.name def test_auto_generate_invoice_and_payment_entry(self): entry = make_membership(self.member) From 02e424fae27f20a5d6375b436adce257d01fb328 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 21 Jan 2021 20:16:17 +0530 Subject: [PATCH 281/295] chore: Add description for settings --- .../doctype/membership_settings/membership_settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 961a9b9b3b1..3887b0a2bea 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -126,6 +126,7 @@ { "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" @@ -150,7 +151,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-11-09 12:28:49.972434", + "modified": "2021-01-21 19:57:53.213286", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", From ded08245cc7cb1b0f0ec6d7801de800e51aff3ab Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 22 Jan 2021 08:52:17 +0530 Subject: [PATCH 282/295] fix: duplicate filters added on patient change --- erpnext/healthcare/page/patient_history/patient_history.html | 2 +- erpnext/healthcare/page/patient_history/patient_history.js | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/healthcare/page/patient_history/patient_history.html b/erpnext/healthcare/page/patient_history/patient_history.html index deaaa97868c..be486c62d1e 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.html +++ b/erpnext/healthcare/page/patient_history/patient_history.html @@ -10,7 +10,7 @@
    -
    +
    diff --git a/erpnext/healthcare/page/patient_history/patient_history.js b/erpnext/healthcare/page/patient_history/patient_history.js index 05c5190f807..54343aae449 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.js +++ b/erpnext/healthcare/page/patient_history/patient_history.js @@ -10,6 +10,7 @@ frappe.pages['patient_history'].on_page_load = function(wrapper) { frappe.breadcrumbs.add('Healthcare'); let pid = ''; 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'), @@ -96,6 +97,7 @@ frappe.pages['patient_history'].on_page_load = function(wrapper) { }; 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 => { @@ -123,6 +125,7 @@ let setup_filters = function(patient, me) { }); doctype_filter.refresh(); + $('.date-filter').empty(); let date_range_field = frappe.ui.form.make_control({ df: { fieldtype: 'DateRange', @@ -389,9 +392,11 @@ let show_patient_vital_charts = function(patient, me, btn_show_id, pts, title) { formatTooltipY: d => d + ' ' + pts, } }); + 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(); } } }); From b9a21c4824b0743415d4cf3049b807dec183b9b1 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 22 Jan 2021 09:00:40 +0530 Subject: [PATCH 283/295] feat: added search to the Select fields dialog in Patient History Settings --- .../patient_history_settings/patient_history_settings.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js index 92922b2888b..5ea17af4598 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js @@ -53,6 +53,14 @@ frappe.ui.form.on('Patient History Settings', { ] }); + d.$body.prepend(` + ` + ); + + frappe.utils.setup_search(d.$body, '.unit-checkbox', '.label-area'); + d.set_primary_action(__('Save'), () => { let values = d.get_values().fields; From 02a7af1e6cb818c512c295f67a3ab2a6efc8b17f Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Fri, 22 Jan 2021 10:03:29 +0530 Subject: [PATCH 284/295] fix: full form for opportunity in crm dashboard (#24436) Co-authored-by: Rucha Mahabal --- .../create_opportunity/create_opportunity.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/onboarding_step/create_opportunity/create_opportunity.json b/erpnext/crm/onboarding_step/create_opportunity/create_opportunity.json index 9f996d9e2be..0ee9317c852 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 From cf7209f3d4879179dc7c912f00ff82ff6d6dd87e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 22 Jan 2021 11:47:13 +0530 Subject: [PATCH 285/295] fix: Project Template patch --- .../v13_0/update_project_template_tasks.py | 71 ++++++++++--------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py index 5fa062306cf..1b0441fe685 100644 --- a/erpnext/patches/v13_0/update_project_template_tasks.py +++ b/erpnext/patches/v13_0/update_project_template_tasks.py @@ -5,40 +5,41 @@ 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_doc = frappe.get_doc('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_doc: + 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 From 6058ea88e88504fd0f2677d32406a0284b3254ca Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 22 Jan 2021 13:35:08 +0530 Subject: [PATCH 286/295] fix: SLA not applied automatically when priority is missing (#24447) --- erpnext/support/doctype/issue/issue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 02d10a4ddad..62b39cced53 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -214,7 +214,7 @@ 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(priority=self.priority, service_level_agreement=self.service_level_agreement) + 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, From 02d495f1fbb18871feb0e22b22e55b947b180382 Mon Sep 17 00:00:00 2001 From: Afshan Date: Fri, 22 Jan 2021 14:24:16 +0530 Subject: [PATCH 287/295] fix: added query for fetching salary component --- .../additional_salary/additional_salary.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.js b/erpnext/payroll/doctype/additional_salary/additional_salary.js index 7737e6c8869..f818abaf87d 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,20 @@ 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 { + query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", + 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", From 217440952f2cb6266423b34246f486eac41ea875 Mon Sep 17 00:00:00 2001 From: Afshan Date: Fri, 22 Jan 2021 14:53:54 +0530 Subject: [PATCH 288/295] fix: slider --- .../payroll/doctype/additional_salary/additional_salary.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.js b/erpnext/payroll/doctype/additional_salary/additional_salary.js index f818abaf87d..d20c98c0984 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.js +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.js @@ -50,10 +50,10 @@ frappe.ui.form.on('Additional Salary', { }, set_earning_component: function(frm) { - if(!frm.doc.company) return; + if (!frm.doc.company) return; frm.set_query("salary_component", function() { return { - query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", + query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", filters: {type: "earning", company: frm.doc.company} }; }); From 75a93d90573ea97391b6ca03d8a404d681588a69 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 22 Jan 2021 15:51:25 +0530 Subject: [PATCH 289/295] feat: fetch date field for custom doctypes --- .../patient_history_settings.js | 23 +++++++++++++++++++ .../patient_history_settings.py | 8 +++++++ 2 files changed, 31 insertions(+) diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js index 5ea17af4598..17324495e60 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js @@ -85,10 +85,33 @@ frappe.ui.form.on('Patient History Settings', { }); 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) { diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py index 9ef97214c58..2e8c994c3d9 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py @@ -49,6 +49,14 @@ class PatientHistorySettings(Document): 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) From 5b4ece50547bb4acc7984c266798d2dde3428aa9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 22 Jan 2021 17:12:35 +0530 Subject: [PATCH 290/295] fix: Update patch --- erpnext/patches/v13_0/update_project_template_tasks.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py index 1b0441fe685..8cc27d217fe 100644 --- a/erpnext/patches/v13_0/update_project_template_tasks.py +++ b/erpnext/patches/v13_0/update_project_template_tasks.py @@ -10,10 +10,12 @@ def execute(): frappe.reload_doc("projects", "doctype", "task") # Update property setter status if any - property_setter_doc = frappe.get_doc('Property Setter', {'doc_type': 'Task', + property_setter = frappe.db.get_value('Property Setter', {'doc_type': 'Task', 'field_name': 'status', 'property': 'options'}) - if property_setter_doc: + 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() From cb1da4d07cb7128e38dfa8047b0a5294a7e77ab0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 22 Jan 2021 19:26:56 +0530 Subject: [PATCH 291/295] fix: doctype meta not loading while setting up fields --- .../patient_history_settings.js | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js index 17324495e60..cf01fcc35b6 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js @@ -66,21 +66,24 @@ frappe.ui.form.on('Patient History Settings', { let selected_fields = []; - for (let idx in values) { - let value = values[idx]; + frappe.model.with_doctype(doc.document_type, function() { + for (let idx in values) { + let value = values[idx]; - let field = frappe.meta.get_docfield(doc.document_type, value); - if (field) { - selected_fields.push({ - label: field.label, - fieldname: field.fieldname, - fieldtype: field.fieldtype - }); + 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.refresh(); + frappe.model.set_value(doctype, doc.name, 'selected_fields', JSON.stringify(selected_fields)); + }) + d.hide(); }); From 6f4ad3b73d74c11426743024a04b786ab3666644 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 23 Jan 2021 12:52:41 +0530 Subject: [PATCH 292/295] fix: sider --- .../patient_history_settings/patient_history_settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js index cf01fcc35b6..453da6a12bf 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js @@ -82,7 +82,7 @@ frappe.ui.form.on('Patient History Settings', { d.refresh(); frappe.model.set_value(doctype, doc.name, 'selected_fields', JSON.stringify(selected_fields)); - }) + }); d.hide(); }); From ee283280067b42ba673eebd8e74043b46c300305 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 23 Jan 2021 13:26:24 +0530 Subject: [PATCH 293/295] fix(travis): Issue Analytics Report Test (#24453) --- erpnext/support/doctype/issue/issue.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 62b39cced53..1ac295919b5 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, From c680547be37c98d1a3638547dedb64cf8f83c6c3 Mon Sep 17 00:00:00 2001 From: Kaviya Periyasamy <36359901+KaviyaPeriyasamy@users.noreply.github.com> Date: Sun, 24 Jan 2021 10:59:37 +0530 Subject: [PATCH 294/295] fix(einvoice): QRCode generation (#24412) --- erpnext/regional/india/e_invoice/einvoice.js | 3 ++ erpnext/regional/india/e_invoice/utils.py | 36 +++++++++++--------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 9c86cc89f55..9fa94c401f0 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 eb210be16a5..2366fcb9eda 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 @@ -436,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): @@ -527,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''' @@ -622,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) @@ -671,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'): @@ -769,21 +771,21 @@ class GSPConnector(): 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': str(base64.b64encode(os.urandom(64))), - '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): From 91eb9bb5c22510b3203471b2cd79484073a9449b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 24 Jan 2021 19:15:57 +0530 Subject: [PATCH 295/295] fix: Add loan to value field in Loan Interest Report --- .../applicant_wise_loan_security_exposure.py | 26 ++++++-- .../loan_interest_report.py | 61 ++++++++++++++++++- .../loan_security_exposure.py | 10 ++- 3 files changed, 87 insertions(+), 10 deletions(-) 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 index 6d7c3b730db..f280402ce9b 100644 --- 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 @@ -26,6 +26,7 @@ def get_columns(filters): {"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}, @@ -43,13 +44,16 @@ def get_data(filters): for key, qty in iteritems(pledge_values): row = {} - current_value = flt(qty * loan_security_details.get(key[1])['latest_price']) + 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), 'currency': currency }) @@ -60,20 +64,30 @@ def get_data(filters): def get_loan_security_details(filters): security_detail_map = {} + loan_security_price_map = {} + lsp_validity_map = {} - loan_security_price_map = frappe._dict(frappe.db.sql(""" - SELECT loan_security, loan_security_price + 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_list=1)) + """, 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))}) + 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 @@ -118,6 +132,6 @@ def get_applicant_wise_total_loan_security_qty(filters, loan_security_details): 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)['latest_price'] + * 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/loan_interest_report.py b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py index aa0325ef35c..2bfe6d3c33e 100644 --- a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py @@ -6,6 +6,8 @@ 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): @@ -31,6 +33,7 @@ def get_columns(filters): {"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}, ] @@ -50,6 +53,9 @@ def get_active_loan_details(filters): 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) @@ -67,12 +73,16 @@ def get_active_loan_details(filters): "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(): @@ -121,4 +131,53 @@ def get_interest_accruals(loans): 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)) \ No newline at end of file + 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/loan_security_exposure.py b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py index 3ef10c0f419..ff88052df5b 100644 --- a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py +++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py @@ -24,6 +24,7 @@ def get_columns(filters): {"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}, @@ -40,13 +41,16 @@ def get_data(filters): for security, value in iteritems(current_pledges): row = {} - current_value = flt(value['qty'] * loan_security_details.get(security)['latest_price']) + 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['qty'], + '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['applicant_count'], + 'pledged_applicant_count': value.get('applicant_count'), 'currency': currency })