From 1bac7930834d6f688950e836c45305a62e7ecb3f Mon Sep 17 00:00:00 2001 From: ruthra Date: Tue, 4 Jan 2022 15:53:41 +0530 Subject: [PATCH 001/447] feat: Payment Terms Status report - calculate status at runtime for payment terms based on invoices - invoices are used in FIFO method --- .../__init__.py | 0 .../payment_terms_status_for_sales_order.js | 84 +++++++ .../payment_terms_status_for_sales_order.json | 38 ++++ .../payment_terms_status_for_sales_order.py | 211 ++++++++++++++++++ ...st_payment_terms_status_for_sales_order.py | 119 ++++++++++ 5 files changed, 452 insertions(+) create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/__init__.py create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/__init__.py b/erpnext/selling/report/payment_terms_status_for_sales_order/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js new file mode 100644 index 0000000000..0450631a3b --- /dev/null +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js @@ -0,0 +1,84 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +function get_filters() { + let filters = [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + }, + { + "fieldname":"period_start_date", + "label": __("Start Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1) + }, + { + "fieldname":"period_end_date", + "label": __("End Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.get_today() + }, + { + "fieldname":"sales_order", + "label": __("Sales Order"), + "fieldtype": "MultiSelectList", + "width": 100, + "options": "Sales Order", + "get_data": function(txt) { + return frappe.db.get_link_options("Sales Order", txt, this.filters()); + }, + "filters": () => { + return { + docstatus: 1, + payment_terms_template: ['not in', ['']], + company: frappe.query_report.get_filter_value("company"), + transaction_date: ['between', [frappe.query_report.get_filter_value("period_start_date"), frappe.query_report.get_filter_value("period_end_date")]] + } + }, + on_change: function(){ + frappe.query_report.refresh(); + } + } + ] + + return filters; +} + +frappe.query_reports["Payment Terms Status for Sales Order"] = { + "filters": get_filters(), + "formatter": function(value, row, column, data, default_formatter){ + if(column.fieldname == 'invoices' && value) { + invoices = value.split(','); + const invoice_formatter = (prev_value, curr_value) => { + if(prev_value != "") { + return prev_value + ", " + default_formatter(curr_value, row, column, data); + } + else { + return default_formatter(curr_value, row, column, data); + } + } + return invoices.reduce(invoice_formatter, "") + } + else if (column.fieldname == 'paid_amount' && value){ + formatted_value = default_formatter(value, row, column, data); + if(value > 0) { + formatted_value = "" + formatted_value + "" + } + return formatted_value; + } + else if (column.fieldname == 'status' && value == 'Completed'){ + return "" + default_formatter(value, row, column, data) + ""; + } + + return default_formatter(value, row, column, data); + }, + +}; diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json new file mode 100644 index 0000000000..850fa4dc47 --- /dev/null +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json @@ -0,0 +1,38 @@ +{ + "add_total_row": 1, + "columns": [], + "creation": "2021-12-28 10:39:34.533964", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-12-30 10:42:06.058457", + "modified_by": "Administrator", + "module": "Selling", + "name": "Payment Terms Status for Sales Order", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Sales Order", + "report_name": "Payment Terms Status for Sales Order", + "report_type": "Script Report", + "roles": [ + { + "role": "Sales User" + }, + { + "role": "Sales Manager" + }, + { + "role": "Maintenance User" + }, + { + "role": "Accounts User" + }, + { + "role": "Stock User" + } + ] +} \ No newline at end of file diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py new file mode 100644 index 0000000000..aa2f757218 --- /dev/null +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -0,0 +1,211 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# License: MIT. See LICENSE + +import frappe +from frappe import _, qb, query_builder +from frappe.query_builder import functions + + +def get_columns(): + columns = [ + { + "label": _("Sales Order"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Sales Order", + "read_only": 1, + }, + { + "label": _("Submitted"), + "fieldname": "submitted", + "fieldtype": "Date", + "read_only": 1 + }, + { + "label": _("Payment Term"), + "fieldname": "payment_term", + "fieldtype": "Data", + "read_only": 1 + }, + { + "label": _("Description"), + "fieldname": "description", + "fieldtype": "Data", + "read_only": 1 + }, + { + "label": _("Due Date"), + "fieldname": "due_date", + "fieldtype": "Date", + "read_only": 1 + }, + { + "label": _("Invoice Portion"), + "fieldname": "invoice_portion", + "fieldtype": "Percent", + "read_only": 1, + }, + { + "label": _("Payment Amount"), + "fieldname": "payment_amount", + "fieldtype": "Currency", + "read_only": 1, + }, + { + "label": _("Paid Amount"), + "fieldname": "paid_amount", + "fieldtype": "Currency", + "read_only": 1 + }, + { + "label": _("Invoices"), + "fieldname": "invoices", + "fieldtype": "Link", + "options": "Sales Invoice", + "read_only": 1, + }, + { + "label": _("Status"), + "fieldname": "status", + "fieldtype": "Data", + "read_only": 1 + } + ] + return columns + + +def get_conditions(filters): + """ + Convert filter options to conditions used in query + """ + filters = frappe._dict(filters) if filters else frappe._dict({}) + conditions = frappe._dict({}) + + conditions.company = filters.company or frappe.defaults.get_user_default("company") + conditions.end_date = filters.period_end_date or frappe.utils.today() + conditions.start_date = filters.period_start_date or frappe.utils.add_months( + conditions.end_date, -1 + ) + conditions.sales_order = filters.sales_order or [] + + return conditions + + +def get_so_with_invoices(filters): + """ + Get Sales Order with payment terms template with their associated Invoices + """ + sorders = [] + + so = qb.DocType("Sales Order") + ps = qb.DocType("Payment Schedule") + datediff = query_builder.CustomFunction("DATEDIFF", ["cur_date", "due_date"]) + ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"]) + + conditions = get_conditions(filters) + query_so = ( + qb.from_(so) + .join(ps) + .on(ps.parent == so.name) + .select( + so.name, + so.transaction_date.as_("submitted"), + ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"), + ps.payment_term, + ps.description, + ps.due_date, + ps.invoice_portion, + ps.payment_amount, + ps.paid_amount, + ) + .where( + (so.docstatus == 1) + & (so.payment_terms_template != "NULL") + & (so.company == conditions.company) + & (so.transaction_date[conditions.start_date : conditions.end_date]) + ) + .orderby(so.name, so.transaction_date, ps.due_date) + ) + + if conditions.sales_order != []: + query_so = query_so.where(so.name.isin(conditions.sales_order)) + + sorders = query_so.run(as_dict=True) + + invoices = [] + if sorders != []: + soi = qb.DocType("Sales Order Item") + si = qb.DocType("Sales Invoice") + sii = qb.DocType("Sales Invoice Item") + query_inv = ( + qb.from_(sii) + .right_join(si) + .on(si.name == sii.parent) + .inner_join(soi) + .on(soi.name == sii.so_detail) + .select(sii.sales_order, sii.parent.as_("invoice"), si.base_net_total.as_("invoice_amount")) + .where((sii.sales_order.isin([x.name for x in sorders])) & (si.docstatus == 1)) + .groupby(sii.parent) + ) + invoices = query_inv.run(as_dict=True) + + return sorders, invoices + + +def set_payment_terms_statuses(sales_orders, invoices): + """ + compute status for payment terms with associated sales invoice using FIFO + """ + + for so in sales_orders: + for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]: + if so.payment_amount - so.paid_amount > 0: + amount = so.payment_amount - so.paid_amount + if inv.invoice_amount >= amount: + inv.invoice_amount -= amount + so.paid_amount += amount + if so.invoices: + so.invoices = so.invoices + "," + inv.invoice + else: + so.invoices = inv.invoice + so.status = "Completed" + break + else: + so.paid_amount += inv.invoice_amount + inv.invoice_amount = 0 + if so.invoices: + so.invoices = so.invoices + "," + inv.invoice + else: + so.invoices = inv.invoice + so.status = "Partly Paid" + + return sales_orders, invoices + + +def prepare_chart(s_orders): + if len(set([x.name for x in s_orders])) == 1: + chart = { + "data": { + "labels": [term.payment_term for term in s_orders], + "datasets": [ + {"name": "Payment Amount", "values": [x.payment_amount for x in s_orders],}, + {"name": "Paid Amount", "values": [x.paid_amount for x in s_orders],}, + ], + }, + "type": "bar", + } + return chart + + +def execute(filters=None): + columns = get_columns() + sales_orders, so_invoices = get_so_with_invoices(filters) + sales_orders, so_invoices = set_payment_terms_statuses(sales_orders, so_invoices) + + prepare_chart(sales_orders) + + data = sales_orders + message = [] + chart = prepare_chart(sales_orders) + + return columns, data, message, chart diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py new file mode 100644 index 0000000000..e9dba84f3a --- /dev/null +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -0,0 +1,119 @@ +import datetime +import unittest + +import frappe +from frappe import qb +from frappe.utils import add_days + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order import ( + execute, +) +from erpnext.stock.doctype.item.test_item import create_item +from erpnext.tests.utils import ERPNextTestCase + +test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template"] + + +class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): + def test_payment_terms_status(self): + # disable Must be a whole number + nos = frappe.get_doc("UOM", "Nos") + nos.must_be_whole_number = 0 + nos.save() + + template = None + if frappe.db.exists("Payment Terms Template", "_Test 50-50"): + template = frappe.get_doc("Payment Terms Template", "_Test 50-50") + else: + template = frappe.get_doc( + { + "doctype": "Payment Terms Template", + "template_name": "_Test 50-50", + "terms": [ + { + "doctype": "Payment Terms Template Detail", + "due_date_based_on": "Day(s) after invoice date", + "payment_term_name": "_Test 50% on 15 Days", + "description": "_Test 50-50", + "invoice_portion": 50, + "credit_days": 15, + }, + { + "doctype": "Payment Terms Template Detail", + "due_date_based_on": "Day(s) after invoice date", + "payment_term_name": "_Test 50% on 30 Days", + "description": "_Test 50-50", + "invoice_portion": 50, + "credit_days": 30, + }, + ], + } + ) + template.insert() + + # item = create_item(item_code="_Test Excavator", is_stock_item=0, valuation_rate=1000000) + item = create_item(item_code="_Test Excavator", is_stock_item=0) + so = make_sales_order( + transaction_date="2021-06-15", + delivery_date=add_days("2021-06-15", -30), + item=item.item_code, + qty=1, + rate=1000000, + po_no=54321, + do_not_save=True, + ) + so.payment_terms_template = template.name + so.save() + so.submit() + + # make invoice with 60% of the total sales order value + sinv = make_sales_invoice(so.name) + # sinv.posting_date = "2021-06-29" + sinv.items[0].qty *= 0.60 + sinv.insert() + sinv.submit() + + columns, data, message, chart = execute( + { + "company": "_Test Company", + "period_start_date": "2021-06-01", + "period_end_date": "2021-06-30", + "sales_order": [so.name], + } + ) + + # revert changes to Nos + nos.must_be_whole_number = 1 + nos.save() + + expected_value = [ + { + "name": so.name, + "submitted": datetime.date(2021, 6, 15), + "status": "Completed", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 6, 30), + "invoice_portion": 50.0, + "payment_amount": 500000.0, + "paid_amount": 500000.0, + "invoices": sinv.name, + }, + { + "name": so.name, + "submitted": datetime.date(2021, 6, 15), + "status": "Partly Paid", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 7, 15), + "invoice_portion": 50.0, + "payment_amount": 500000.0, + "paid_amount": 100000.0, + "invoices": sinv.name, + }, + ] + + self.assertEqual(data, expected_value) From 9f1e68801d527628551984402fd0c06e401084d8 Mon Sep 17 00:00:00 2001 From: ruthra Date: Wed, 5 Jan 2022 10:11:19 +0530 Subject: [PATCH 002/447] test: fix failing test case payment terms status --- .../test_payment_terms_status_for_sales_order.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index e9dba84f3a..19c01f2d43 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -21,8 +21,7 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): def test_payment_terms_status(self): # disable Must be a whole number nos = frappe.get_doc("UOM", "Nos") - nos.must_be_whole_number = 0 - nos.save() + nos.db_set("must_be_whole_number", 0, commit=True) template = None if frappe.db.exists("Payment Terms Template", "_Test 50-50"): @@ -62,9 +61,9 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): item=item.item_code, qty=1, rate=1000000, - po_no=54321, do_not_save=True, ) + so.po_no = "" so.payment_terms_template = template.name so.save() so.submit() @@ -86,8 +85,7 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): ) # revert changes to Nos - nos.must_be_whole_number = 1 - nos.save() + nos.db_set("must_be_whole_number", 1, commit=True) expected_value = [ { From edd980acdc9e51f74eb6b70a793ae17b2e827710 Mon Sep 17 00:00:00 2001 From: ruthra Date: Wed, 5 Jan 2022 10:43:20 +0530 Subject: [PATCH 003/447] refactor: remove unused imports --- .../test_payment_terms_status_for_sales_order.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index 19c01f2d43..4f27a5683d 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -1,11 +1,8 @@ import datetime -import unittest import frappe -from frappe import qb from frappe.utils import add_days -from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order import ( From 4535a7a301f76fa3b867902f19e806dcb01bdb75 Mon Sep 17 00:00:00 2001 From: ruthra Date: Wed, 5 Jan 2022 10:46:32 +0530 Subject: [PATCH 004/447] test: qty and rate changed to remove need for fractional Nos --- .../test_payment_terms_status_for_sales_order.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index 4f27a5683d..5d6e91e8a5 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -16,9 +16,6 @@ test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Temp class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): def test_payment_terms_status(self): - # disable Must be a whole number - nos = frappe.get_doc("UOM", "Nos") - nos.db_set("must_be_whole_number", 0, commit=True) template = None if frappe.db.exists("Payment Terms Template", "_Test 50-50"): @@ -56,8 +53,8 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): transaction_date="2021-06-15", delivery_date=add_days("2021-06-15", -30), item=item.item_code, - qty=1, - rate=1000000, + qty=10, + rate=100000, do_not_save=True, ) so.po_no = "" @@ -67,8 +64,7 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): # make invoice with 60% of the total sales order value sinv = make_sales_invoice(so.name) - # sinv.posting_date = "2021-06-29" - sinv.items[0].qty *= 0.60 + sinv.items[0].qty = 6 sinv.insert() sinv.submit() @@ -81,9 +77,6 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): } ) - # revert changes to Nos - nos.db_set("must_be_whole_number", 1, commit=True) - expected_value = [ { "name": so.name, From 0c0a9ed96d3b11741cf506c95f9e5e16fcdcfe68 Mon Sep 17 00:00:00 2001 From: ChillarAnand Date: Wed, 5 Jan 2022 09:17:46 +0530 Subject: [PATCH 005/447] refactor: Remove non-profit domain from ERPNext --- erpnext/domains/non_profit.py | 22 - erpnext/hooks.py | 16 - erpnext/modules.txt | 1 - erpnext/non_profit/__init__.py | 0 erpnext/non_profit/doctype/__init__.py | 0 .../certification_application/__init__.py | 0 .../certification_application.js | 8 - .../certification_application.json | 323 ------- .../certification_application.py | 9 - .../test_certification_application.py | 8 - .../doctype/certified_consultant/__init__.py | 0 .../certified_consultant.js | 8 - .../certified_consultant.json | 724 --------------- .../certified_consultant.py | 9 - .../test_certified_consultant.py | 8 - .../non_profit/doctype/chapter/__init__.py | 0 erpnext/non_profit/doctype/chapter/chapter.js | 8 - .../non_profit/doctype/chapter/chapter.json | 397 -------- erpnext/non_profit/doctype/chapter/chapter.py | 49 - .../doctype/chapter/templates/chapter.html | 79 -- .../chapter/templates/chapter_row.html | 25 - .../doctype/chapter/test_chapter.py | 8 - .../doctype/chapter_member/__init__.py | 0 .../chapter_member/chapter_member.json | 199 ---- .../doctype/chapter_member/chapter_member.py | 9 - .../non_profit/doctype/donation/__init__.py | 0 .../non_profit/doctype/donation/donation.js | 26 - .../non_profit/doctype/donation/donation.json | 156 ---- .../non_profit/doctype/donation/donation.py | 220 ----- .../doctype/donation/donation_dashboard.py | 16 - .../doctype/donation/test_donation.py | 77 -- erpnext/non_profit/doctype/donor/__init__.py | 0 erpnext/non_profit/doctype/donor/donor.js | 17 - erpnext/non_profit/doctype/donor/donor.json | 110 --- erpnext/non_profit/doctype/donor/donor.py | 17 - .../non_profit/doctype/donor/donor_list.js | 3 - .../non_profit/doctype/donor/test_donor.py | 8 - .../non_profit/doctype/donor_type/__init__.py | 0 .../doctype/donor_type/donor_type.js | 8 - .../doctype/donor_type/donor_type.json | 94 -- .../doctype/donor_type/donor_type.py | 9 - .../doctype/donor_type/test_donor_type.py | 8 - .../doctype/grant_application/__init__.py | 0 .../grant_application/grant_application.js | 27 - .../grant_application/grant_application.json | 851 ------------------ .../grant_application/grant_application.py | 58 -- .../templates/grant_application.html | 68 -- .../templates/grant_application_row.html | 11 - .../test_grant_application.py | 8 - erpnext/non_profit/doctype/member/__init__.py | 0 erpnext/non_profit/doctype/member/member.js | 64 -- erpnext/non_profit/doctype/member/member.json | 210 ----- erpnext/non_profit/doctype/member/member.py | 185 ---- .../doctype/member/member_dashboard.py | 22 - .../non_profit/doctype/member/member_list.js | 3 - .../non_profit/doctype/member/test_member.py | 8 - .../non_profit/doctype/membership/__init__.py | 0 .../doctype/membership/membership.js | 41 - .../doctype/membership/membership.json | 184 ---- .../doctype/membership/membership_list.js | 15 - .../doctype/membership/test_membership.py | 164 ---- .../doctype/membership_type/__init__.py | 0 .../membership_type/membership_type.js | 22 - .../membership_type/membership_type.json | 71 -- .../membership_type/membership_type.py | 18 - .../membership_type/test_membership_type.py | 8 - .../doctype/non_profit_settings/__init__.py | 0 .../non_profit_settings.js | 133 --- .../non_profit_settings.json | 273 ------ .../non_profit_settings.py | 38 - .../test_non_profit_settings.py | 9 - .../non_profit/doctype/volunteer/__init__.py | 0 .../doctype/volunteer/test_volunteer.py | 8 - .../non_profit/doctype/volunteer/volunteer.js | 17 - .../doctype/volunteer/volunteer.json | 148 --- .../non_profit/doctype/volunteer/volunteer.py | 12 - .../doctype/volunteer_skill/__init__.py | 0 .../volunteer_skill/volunteer_skill.json | 73 -- .../volunteer_skill/volunteer_skill.py | 9 - .../doctype/volunteer_type/__init__.py | 0 .../volunteer_type/test_volunteer_type.py | 8 - .../doctype/volunteer_type/volunteer_type.js | 8 - .../volunteer_type/volunteer_type.json | 94 -- .../doctype/volunteer_type/volunteer_type.py | 9 - erpnext/non_profit/report/__init__.py | 0 .../report/expiring_memberships/__init__.py | 0 .../expiring_memberships.js | 24 - .../expiring_memberships.json | 27 - .../expiring_memberships.py | 34 - erpnext/non_profit/utils.py | 12 - erpnext/non_profit/web_form/__init__.py | 0 .../certification_application/__init__.py | 0 .../certification_application.js | 16 - .../certification_application.json | 79 -- .../certification_application.py | 3 - .../certification_application_usd/__init__.py | 0 .../certification_application_usd.js | 16 - .../certification_application_usd.json | 80 -- .../certification_application_usd.py | 3 - .../web_form/grant_application/__init__.py | 0 .../grant_application/grant_application.js | 3 - .../grant_application/grant_application.json | 108 --- .../grant_application/grant_application.py | 4 - .../workspace/non_profit/non_profit.json | 272 ------ erpnext/patches.txt | 2 + .../v13_0/non_profit_deprecation_warning.py | 10 + .../v14_0/delete_non_profit_doctypes.py | 19 + erpnext/regional/india/setup.py | 8 - .../operations/install_fixtures.py | 1 - 109 files changed, 31 insertions(+), 6246 deletions(-) delete mode 100644 erpnext/domains/non_profit.py delete mode 100644 erpnext/non_profit/__init__.py delete mode 100644 erpnext/non_profit/doctype/__init__.py delete mode 100644 erpnext/non_profit/doctype/certification_application/__init__.py delete mode 100644 erpnext/non_profit/doctype/certification_application/certification_application.js delete mode 100644 erpnext/non_profit/doctype/certification_application/certification_application.json delete mode 100644 erpnext/non_profit/doctype/certification_application/certification_application.py delete mode 100644 erpnext/non_profit/doctype/certification_application/test_certification_application.py delete mode 100644 erpnext/non_profit/doctype/certified_consultant/__init__.py delete mode 100644 erpnext/non_profit/doctype/certified_consultant/certified_consultant.js delete mode 100644 erpnext/non_profit/doctype/certified_consultant/certified_consultant.json delete mode 100644 erpnext/non_profit/doctype/certified_consultant/certified_consultant.py delete mode 100644 erpnext/non_profit/doctype/certified_consultant/test_certified_consultant.py delete mode 100644 erpnext/non_profit/doctype/chapter/__init__.py delete mode 100644 erpnext/non_profit/doctype/chapter/chapter.js delete mode 100644 erpnext/non_profit/doctype/chapter/chapter.json delete mode 100644 erpnext/non_profit/doctype/chapter/chapter.py delete mode 100644 erpnext/non_profit/doctype/chapter/templates/chapter.html delete mode 100644 erpnext/non_profit/doctype/chapter/templates/chapter_row.html delete mode 100644 erpnext/non_profit/doctype/chapter/test_chapter.py delete mode 100644 erpnext/non_profit/doctype/chapter_member/__init__.py delete mode 100644 erpnext/non_profit/doctype/chapter_member/chapter_member.json delete mode 100644 erpnext/non_profit/doctype/chapter_member/chapter_member.py delete mode 100644 erpnext/non_profit/doctype/donation/__init__.py delete mode 100644 erpnext/non_profit/doctype/donation/donation.js delete mode 100644 erpnext/non_profit/doctype/donation/donation.json delete mode 100644 erpnext/non_profit/doctype/donation/donation.py delete mode 100644 erpnext/non_profit/doctype/donation/donation_dashboard.py delete mode 100644 erpnext/non_profit/doctype/donation/test_donation.py delete mode 100644 erpnext/non_profit/doctype/donor/__init__.py delete mode 100644 erpnext/non_profit/doctype/donor/donor.js delete mode 100644 erpnext/non_profit/doctype/donor/donor.json delete mode 100644 erpnext/non_profit/doctype/donor/donor.py delete mode 100644 erpnext/non_profit/doctype/donor/donor_list.js delete mode 100644 erpnext/non_profit/doctype/donor/test_donor.py delete mode 100644 erpnext/non_profit/doctype/donor_type/__init__.py delete mode 100644 erpnext/non_profit/doctype/donor_type/donor_type.js delete mode 100644 erpnext/non_profit/doctype/donor_type/donor_type.json delete mode 100644 erpnext/non_profit/doctype/donor_type/donor_type.py delete mode 100644 erpnext/non_profit/doctype/donor_type/test_donor_type.py delete mode 100644 erpnext/non_profit/doctype/grant_application/__init__.py delete mode 100644 erpnext/non_profit/doctype/grant_application/grant_application.js delete mode 100644 erpnext/non_profit/doctype/grant_application/grant_application.json delete mode 100644 erpnext/non_profit/doctype/grant_application/grant_application.py delete mode 100644 erpnext/non_profit/doctype/grant_application/templates/grant_application.html delete mode 100644 erpnext/non_profit/doctype/grant_application/templates/grant_application_row.html delete mode 100644 erpnext/non_profit/doctype/grant_application/test_grant_application.py delete mode 100644 erpnext/non_profit/doctype/member/__init__.py delete mode 100644 erpnext/non_profit/doctype/member/member.js delete mode 100644 erpnext/non_profit/doctype/member/member.json delete mode 100644 erpnext/non_profit/doctype/member/member.py delete mode 100644 erpnext/non_profit/doctype/member/member_dashboard.py delete mode 100644 erpnext/non_profit/doctype/member/member_list.js delete mode 100644 erpnext/non_profit/doctype/member/test_member.py delete mode 100644 erpnext/non_profit/doctype/membership/__init__.py delete mode 100644 erpnext/non_profit/doctype/membership/membership.js delete mode 100644 erpnext/non_profit/doctype/membership/membership.json delete mode 100644 erpnext/non_profit/doctype/membership/membership_list.js delete mode 100644 erpnext/non_profit/doctype/membership/test_membership.py delete mode 100644 erpnext/non_profit/doctype/membership_type/__init__.py delete mode 100644 erpnext/non_profit/doctype/membership_type/membership_type.js delete mode 100644 erpnext/non_profit/doctype/membership_type/membership_type.json delete mode 100644 erpnext/non_profit/doctype/membership_type/membership_type.py delete mode 100644 erpnext/non_profit/doctype/membership_type/test_membership_type.py delete mode 100644 erpnext/non_profit/doctype/non_profit_settings/__init__.py delete mode 100644 erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js delete mode 100644 erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.json delete mode 100644 erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py delete mode 100644 erpnext/non_profit/doctype/non_profit_settings/test_non_profit_settings.py delete mode 100644 erpnext/non_profit/doctype/volunteer/__init__.py delete mode 100644 erpnext/non_profit/doctype/volunteer/test_volunteer.py delete mode 100644 erpnext/non_profit/doctype/volunteer/volunteer.js delete mode 100644 erpnext/non_profit/doctype/volunteer/volunteer.json delete mode 100644 erpnext/non_profit/doctype/volunteer/volunteer.py delete mode 100644 erpnext/non_profit/doctype/volunteer_skill/__init__.py delete mode 100644 erpnext/non_profit/doctype/volunteer_skill/volunteer_skill.json delete mode 100644 erpnext/non_profit/doctype/volunteer_skill/volunteer_skill.py delete mode 100644 erpnext/non_profit/doctype/volunteer_type/__init__.py delete mode 100644 erpnext/non_profit/doctype/volunteer_type/test_volunteer_type.py delete mode 100644 erpnext/non_profit/doctype/volunteer_type/volunteer_type.js delete mode 100644 erpnext/non_profit/doctype/volunteer_type/volunteer_type.json delete mode 100644 erpnext/non_profit/doctype/volunteer_type/volunteer_type.py delete mode 100644 erpnext/non_profit/report/__init__.py delete mode 100644 erpnext/non_profit/report/expiring_memberships/__init__.py delete mode 100644 erpnext/non_profit/report/expiring_memberships/expiring_memberships.js delete mode 100644 erpnext/non_profit/report/expiring_memberships/expiring_memberships.json delete mode 100644 erpnext/non_profit/report/expiring_memberships/expiring_memberships.py delete mode 100644 erpnext/non_profit/utils.py delete mode 100644 erpnext/non_profit/web_form/__init__.py delete mode 100644 erpnext/non_profit/web_form/certification_application/__init__.py delete mode 100644 erpnext/non_profit/web_form/certification_application/certification_application.js delete mode 100644 erpnext/non_profit/web_form/certification_application/certification_application.json delete mode 100644 erpnext/non_profit/web_form/certification_application/certification_application.py delete mode 100644 erpnext/non_profit/web_form/certification_application_usd/__init__.py delete mode 100644 erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.js delete mode 100644 erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.json delete mode 100644 erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.py delete mode 100644 erpnext/non_profit/web_form/grant_application/__init__.py delete mode 100644 erpnext/non_profit/web_form/grant_application/grant_application.js delete mode 100644 erpnext/non_profit/web_form/grant_application/grant_application.json delete mode 100644 erpnext/non_profit/web_form/grant_application/grant_application.py delete mode 100644 erpnext/non_profit/workspace/non_profit/non_profit.json create mode 100644 erpnext/patches/v13_0/non_profit_deprecation_warning.py create mode 100644 erpnext/patches/v14_0/delete_non_profit_doctypes.py diff --git a/erpnext/domains/non_profit.py b/erpnext/domains/non_profit.py deleted file mode 100644 index d9fc5e5df0..0000000000 --- a/erpnext/domains/non_profit.py +++ /dev/null @@ -1,22 +0,0 @@ -data = { - 'desktop_icons': [ - 'Non Profit', - 'Member', - 'Donor', - 'Volunteer', - 'Grant Application', - 'Accounts', - 'Buying', - 'HR', - 'ToDo' - ], - 'restricted_roles': [ - 'Non Profit Manager', - 'Non Profit Member', - 'Non Profit Portal User' - ], - 'modules': [ - 'Non Profit' - ], - 'default_portal_role': 'Non Profit Manager' -} diff --git a/erpnext/hooks.py b/erpnext/hooks.py index f014b0e1e9..4502f2b97b 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -69,7 +69,6 @@ domains = { 'Education': 'erpnext.domains.education', 'Hospitality': 'erpnext.domains.hospitality', 'Manufacturing': 'erpnext.domains.manufacturing', - 'Non Profit': 'erpnext.domains.non_profit', 'Retail': 'erpnext.domains.retail', 'Services': 'erpnext.domains.services', } @@ -176,7 +175,6 @@ standard_portal_menu_items = [ {"title": _("Fees"), "route": "/fees", "reference_doctype": "Fees", "role":"Student"}, {"title": _("Newsletter"), "route": "/newsletters", "reference_doctype": "Newsletter"}, {"title": _("Admission"), "route": "/admissions", "reference_doctype": "Student Admission", "role": "Student"}, - {"title": _("Certification"), "route": "/certification", "reference_doctype": "Certification Application", "role": "Non Profit Portal User"}, {"title": _("Material Request"), "route": "/material-requests", "reference_doctype": "Material Request", "role": "Customer"}, {"title": _("Appointment Booking"), "route": "/book_appointment"}, ] @@ -373,7 +371,6 @@ scheduler_events = { "erpnext.selling.doctype.quotation.quotation.set_expired_status", "erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status", "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email", - "erpnext.non_profit.doctype.membership.membership.set_expired_status", "erpnext.hr.doctype.interview.interview.send_daily_feedback_reminder" ], "daily_long": [ @@ -566,19 +563,6 @@ global_search_doctypes = { {'doctype': 'Assessment Code', 'index': 39}, {'doctype': 'Discussion', 'index': 40}, ], - "Non Profit": [ - {'doctype': 'Certified Consultant', 'index': 1}, - {'doctype': 'Certification Application', 'index': 2}, - {'doctype': 'Volunteer', 'index': 3}, - {'doctype': 'Membership', 'index': 4}, - {'doctype': 'Member', 'index': 5}, - {'doctype': 'Donor', 'index': 6}, - {'doctype': 'Chapter', 'index': 7}, - {'doctype': 'Grant Application', 'index': 8}, - {'doctype': 'Volunteer Type', 'index': 9}, - {'doctype': 'Donor Type', 'index': 10}, - {'doctype': 'Membership Type', 'index': 11} - ], "Hospitality": [ {'doctype': 'Hotel Room', 'index': 0}, {'doctype': 'Hotel Room Reservation', 'index': 1}, diff --git a/erpnext/modules.txt b/erpnext/modules.txt index ae0bb2d5c9..cd1586c290 100644 --- a/erpnext/modules.txt +++ b/erpnext/modules.txt @@ -17,7 +17,6 @@ Education Regional Restaurant ERPNext Integrations -Non Profit Hotels Quality Management Communication diff --git a/erpnext/non_profit/__init__.py b/erpnext/non_profit/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/doctype/__init__.py b/erpnext/non_profit/doctype/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/doctype/certification_application/__init__.py b/erpnext/non_profit/doctype/certification_application/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/doctype/certification_application/certification_application.js b/erpnext/non_profit/doctype/certification_application/certification_application.js deleted file mode 100644 index 1e6a9a4b01..0000000000 --- a/erpnext/non_profit/doctype/certification_application/certification_application.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Certification Application', { - refresh: function(frm) { - - } -}); diff --git a/erpnext/non_profit/doctype/certification_application/certification_application.json b/erpnext/non_profit/doctype/certification_application/certification_application.json deleted file mode 100644 index f562fa6734..0000000000 --- a/erpnext/non_profit/doctype/certification_application/certification_application.json +++ /dev/null @@ -1,323 +0,0 @@ -{ - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "NPO-CAPP-.YYYY.-.#####", - "beta": 0, - "creation": "2018-06-08 16:12:42.091729", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "name_of_applicant", - "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": "Name of Applicant", - "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": "email", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Email", - "length": 0, - "no_copy": 0, - "options": "User", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_1", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "certification_status", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Certification Status", - "length": 0, - "no_copy": 0, - "options": "Yet to appear\nCertified\nNot Certified", - "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": "payment_details", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Payment Details", - "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": "paid", - "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": "Paid", - "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": "currency", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Currency", - "length": 0, - "no_copy": 0, - "options": "USD\nINR", - "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": "amount", - "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": "Amount", - "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 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-11-04 03:36:35.337403", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Certification Application", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Non Profit", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 -} \ No newline at end of file diff --git a/erpnext/non_profit/doctype/certification_application/certification_application.py b/erpnext/non_profit/doctype/certification_application/certification_application.py deleted file mode 100644 index cbbe191fba..0000000000 --- a/erpnext/non_profit/doctype/certification_application/certification_application.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -from frappe.model.document import Document - - -class CertificationApplication(Document): - pass diff --git a/erpnext/non_profit/doctype/certification_application/test_certification_application.py b/erpnext/non_profit/doctype/certification_application/test_certification_application.py deleted file mode 100644 index 8687b4daf4..0000000000 --- a/erpnext/non_profit/doctype/certification_application/test_certification_application.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - - -class TestCertificationApplication(unittest.TestCase): - pass diff --git a/erpnext/non_profit/doctype/certified_consultant/__init__.py b/erpnext/non_profit/doctype/certified_consultant/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/doctype/certified_consultant/certified_consultant.js b/erpnext/non_profit/doctype/certified_consultant/certified_consultant.js deleted file mode 100644 index cd004c3489..0000000000 --- a/erpnext/non_profit/doctype/certified_consultant/certified_consultant.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Certified Consultant', { - refresh: function(frm) { - - } -}); diff --git a/erpnext/non_profit/doctype/certified_consultant/certified_consultant.json b/erpnext/non_profit/doctype/certified_consultant/certified_consultant.json deleted file mode 100644 index d77f1b2569..0000000000 --- a/erpnext/non_profit/doctype/certified_consultant/certified_consultant.json +++ /dev/null @@ -1,724 +0,0 @@ -{ - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "NPO-CONS-.YYYY.-.#####", - "beta": 0, - "creation": "2018-06-13 17:27:19.838334", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "name_of_consultant", - "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": "Name of Consultant", - "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": "country", - "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": "Country", - "length": 0, - "no_copy": 0, - "options": "Country", - "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": "email", - "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": "Email", - "length": 0, - "no_copy": 0, - "options": "User", - "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": "phone", - "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": "Phone", - "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": "website_url", - "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": "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address", - "fieldtype": "Small Text", - "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": "Address", - "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": "column_break1", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "image", - "fieldtype": "Attach Image", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Image", - "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": "certification_application", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Certification Application", - "length": 0, - "no_copy": 0, - "options": "Certification Application", - "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": "section_break1", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Certification Validity", - "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": "from_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "From", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_beak2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "to_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "To", - "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": "section_break2", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "introduction", - "fieldtype": "Small Text", - "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": "Introduction", - "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": "details", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Details", - "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": "section_break3", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "discuss_id", - "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": "Discuss ID", - "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": "github_id", - "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": "GitHub ID", - "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": "show_in_website", - "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": "Show in 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 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-11-04 03:36:47.386618", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Certified Consultant", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Non Profit Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Non Profit", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 -} \ No newline at end of file diff --git a/erpnext/non_profit/doctype/certified_consultant/certified_consultant.py b/erpnext/non_profit/doctype/certified_consultant/certified_consultant.py deleted file mode 100644 index 47361cc39e..0000000000 --- a/erpnext/non_profit/doctype/certified_consultant/certified_consultant.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -from frappe.model.document import Document - - -class CertifiedConsultant(Document): - pass diff --git a/erpnext/non_profit/doctype/certified_consultant/test_certified_consultant.py b/erpnext/non_profit/doctype/certified_consultant/test_certified_consultant.py deleted file mode 100644 index d10353c1e4..0000000000 --- a/erpnext/non_profit/doctype/certified_consultant/test_certified_consultant.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - - -class TestCertifiedConsultant(unittest.TestCase): - pass diff --git a/erpnext/non_profit/doctype/chapter/__init__.py b/erpnext/non_profit/doctype/chapter/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/doctype/chapter/chapter.js b/erpnext/non_profit/doctype/chapter/chapter.js deleted file mode 100644 index c8b6d4a644..0000000000 --- a/erpnext/non_profit/doctype/chapter/chapter.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Chapter', { - refresh: function() { - - } -}); diff --git a/erpnext/non_profit/doctype/chapter/chapter.json b/erpnext/non_profit/doctype/chapter/chapter.json deleted file mode 100644 index 86cba9a178..0000000000 --- a/erpnext/non_profit/doctype/chapter/chapter.json +++ /dev/null @@ -1,397 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 1, - "allow_import": 0, - "allow_rename": 1, - "autoname": "prompt", - "beta": 0, - "creation": "2017-09-14 13:36:03.904702", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "chapter_head", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Chapter Head", - "length": 0, - "no_copy": 0, - "options": "Member", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "region", - "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": "Region", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_5", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "introduction", - "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": "Introduction", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "meetup_embed_html", - "fieldtype": "Code", - "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": "Meetup Embed HTML", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address", - "fieldtype": "Text", - "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": "Address", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "chapters/chapter_name\nleave blank automatically set after saving chapter.", - "fieldname": "route", - "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": "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "published", - "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": "Published", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "chapter_members", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Chapter Members", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "members", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Members", - "length": 0, - "no_copy": 0, - "options": "Chapter Member", - "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, - "unique": 0 - } - ], - "has_web_view": 1, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_published_field": "published", - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2017-12-14 12:59:31.424240", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Chapter", - "name_case": "Title Case", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Non Profit Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Non Profit", - "route": "chapters", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/non_profit/doctype/chapter/chapter.py b/erpnext/non_profit/doctype/chapter/chapter.py deleted file mode 100644 index c01b1ef3e4..0000000000 --- a/erpnext/non_profit/doctype/chapter/chapter.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -from frappe.website.website_generator import WebsiteGenerator - - -class Chapter(WebsiteGenerator): - _website = frappe._dict( - condition_field = "published", - ) - - def get_context(self, context): - context.no_cache = True - context.show_sidebar = True - context.parents = [dict(label='View All Chapters', - route='chapters', title='View Chapters')] - - def validate(self): - if not self.route: #pylint: disable=E0203 - self.route = 'chapters/' + self.scrub(self.name) - - def enable(self): - chapter = frappe.get_doc('Chapter', frappe.form_dict.name) - chapter.append('members', dict(enable=self.value)) - chapter.save(ignore_permissions=1) - frappe.db.commit() - - -def get_list_context(context): - context.allow_guest = True - context.no_cache = True - context.show_sidebar = True - context.title = 'All Chapters' - context.no_breadcrumbs = True - context.order_by = 'creation desc' - - -@frappe.whitelist() -def leave(title, user_id, leave_reason): - chapter = frappe.get_doc("Chapter", title) - for member in chapter.members: - if member.user == user_id: - member.enabled = 0 - member.leave_reason = leave_reason - chapter.save(ignore_permissions=1) - frappe.db.commit() - return "Thank you for Feedback" diff --git a/erpnext/non_profit/doctype/chapter/templates/chapter.html b/erpnext/non_profit/doctype/chapter/templates/chapter.html deleted file mode 100644 index 321828f73f..0000000000 --- a/erpnext/non_profit/doctype/chapter/templates/chapter.html +++ /dev/null @@ -1,79 +0,0 @@ -{% extends "templates/web.html" %} - -{% block page_content %} -

{{ title }}

-

{{ introduction }}

-{% if meetup_embed_html %} - {{ meetup_embed_html }} -{% endif %} -

Member Details

- -{% if members %} - - {% set index = [1] %} - {% for user in members %} - {% if user.enabled == 1 %} - - - - {% set __ = index.append(1) %} - {% endif %} - {% endfor %} -
-
-
-
-
- {{ index|length }}. {{ frappe.db.get_value('User', user.user, 'full_name') }}
-
-
- {% if user.website_url %} - {{ user.website_url | truncate (50) or '' }} - {% endif %} -
-
-
-

-
- {% if user.introduction %} - {{ user.introduction }} - {% endif %} -
-
- -
-{% else %} -

No member yet.

-{% endif %} - -

Chapter Head

-
- - - {% set doc = frappe.get_doc('Member',chapter_head) %} - - - - - - - - - - - - -
Name{{ doc.member_name }}
Email{{ frappe.db.get_value('User', doc.email, 'email') or '' }}
Phone{{ frappe.db.get_value('User', doc.email, 'phone') or '' }}
-
- -{% if address %} -

Address

-
-

{{ address or ''}}

-
-{% endif %} - -

Join this Chapter

-

Leave this Chapter

- -{% endblock %} diff --git a/erpnext/non_profit/doctype/chapter/templates/chapter_row.html b/erpnext/non_profit/doctype/chapter/templates/chapter_row.html deleted file mode 100644 index cad34fa5be..0000000000 --- a/erpnext/non_profit/doctype/chapter/templates/chapter_row.html +++ /dev/null @@ -1,25 +0,0 @@ -{% if doc.published %} - -{% endif %} diff --git a/erpnext/non_profit/doctype/chapter/test_chapter.py b/erpnext/non_profit/doctype/chapter/test_chapter.py deleted file mode 100644 index 98601efcf2..0000000000 --- a/erpnext/non_profit/doctype/chapter/test_chapter.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - - -class TestChapter(unittest.TestCase): - pass diff --git a/erpnext/non_profit/doctype/chapter_member/__init__.py b/erpnext/non_profit/doctype/chapter_member/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/doctype/chapter_member/chapter_member.json b/erpnext/non_profit/doctype/chapter_member/chapter_member.json deleted file mode 100644 index 478bfd9331..0000000000 --- a/erpnext/non_profit/doctype/chapter_member/chapter_member.json +++ /dev/null @@ -1,199 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-09-14 13:38:04.296375", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "user", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "User", - "length": 0, - "no_copy": 0, - "options": "User", - "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_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "introduction", - "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": "Introduction", - "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_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "website_url", - "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": "Website URL", - "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_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "default": "1", - "fieldname": "enabled", - "fieldtype": "Check", - "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": "Enabled", - "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_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "leave_reason", - "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": "Leave Reason", - "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 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-03-07 05:36:51.664816", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Chapter Member", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Non Profit", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/non_profit/doctype/chapter_member/chapter_member.py b/erpnext/non_profit/doctype/chapter_member/chapter_member.py deleted file mode 100644 index 80c0446ee5..0000000000 --- a/erpnext/non_profit/doctype/chapter_member/chapter_member.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -from frappe.model.document import Document - - -class ChapterMember(Document): - pass diff --git a/erpnext/non_profit/doctype/donation/__init__.py b/erpnext/non_profit/doctype/donation/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/doctype/donation/donation.js b/erpnext/non_profit/doctype/donation/donation.js deleted file mode 100644 index 10e8220144..0000000000 --- a/erpnext/non_profit/doctype/donation/donation.js +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Donation', { - refresh: function(frm) { - if (frm.doc.docstatus === 1 && !frm.doc.paid) { - frm.add_custom_button(__('Create Payment Entry'), function() { - frm.events.make_payment_entry(frm); - }); - } - }, - - make_payment_entry: function(frm) { - return frappe.call({ - method: 'erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry', - args: { - 'dt': frm.doc.doctype, - 'dn': frm.doc.name - }, - callback: function(r) { - var doc = frappe.model.sync(r.message); - frappe.set_route('Form', doc[0].doctype, doc[0].name); - } - }); - }, -}); diff --git a/erpnext/non_profit/doctype/donation/donation.json b/erpnext/non_profit/doctype/donation/donation.json deleted file mode 100644 index 6759569d54..0000000000 --- a/erpnext/non_profit/doctype/donation/donation.json +++ /dev/null @@ -1,156 +0,0 @@ -{ - "actions": [], - "autoname": "naming_series:", - "creation": "2021-02-17 10:28:52.645731", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "naming_series", - "donor", - "donor_name", - "email", - "column_break_4", - "company", - "date", - "payment_details_section", - "paid", - "amount", - "mode_of_payment", - "razorpay_payment_id", - "amended_from" - ], - "fields": [ - { - "fieldname": "donor", - "fieldtype": "Link", - "label": "Donor", - "options": "Donor", - "reqd": 1 - }, - { - "fetch_from": "donor.donor_name", - "fieldname": "donor_name", - "fieldtype": "Data", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Donor Name", - "read_only": 1 - }, - { - "fetch_from": "donor.email", - "fieldname": "email", - "fieldtype": "Data", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Email", - "read_only": 1 - }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" - }, - { - "fieldname": "date", - "fieldtype": "Date", - "label": "Date", - "reqd": 1 - }, - { - "fieldname": "payment_details_section", - "fieldtype": "Section Break", - "label": "Payment Details" - }, - { - "fieldname": "amount", - "fieldtype": "Currency", - "label": "Amount", - "reqd": 1 - }, - { - "fieldname": "mode_of_payment", - "fieldtype": "Link", - "label": "Mode of Payment", - "options": "Mode of Payment" - }, - { - "fieldname": "razorpay_payment_id", - "fieldtype": "Data", - "label": "Razorpay Payment ID", - "read_only": 1 - }, - { - "fieldname": "naming_series", - "fieldtype": "Select", - "label": "Naming Series", - "options": "NPO-DTN-.YYYY.-" - }, - { - "default": "0", - "fieldname": "paid", - "fieldtype": "Check", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Paid" - }, - { - "fieldname": "company", - "fieldtype": "Link", - "label": "Company", - "options": "Company", - "reqd": 1 - }, - { - "fieldname": "amended_from", - "fieldtype": "Link", - "label": "Amended From", - "no_copy": 1, - "options": "Donation", - "print_hide": 1, - "read_only": 1 - } - ], - "index_web_pages_for_search": 1, - "is_submittable": 1, - "links": [], - "modified": "2021-03-11 10:53:11.269005", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Donation", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "select": 1, - "share": 1, - "submit": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Non Profit Manager", - "select": 1, - "share": 1, - "submit": 1, - "write": 1 - } - ], - "search_fields": "donor_name, email", - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "donor_name", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/non_profit/doctype/donation/donation.py b/erpnext/non_profit/doctype/donation/donation.py deleted file mode 100644 index 54bc94b755..0000000000 --- a/erpnext/non_profit/doctype/donation/donation.py +++ /dev/null @@ -1,220 +0,0 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import json - -import frappe -from frappe import _ -from frappe.email import sendmail_to_system_managers -from frappe.model.document import Document -from frappe.utils import flt, get_link_to_form, getdate - -from erpnext.non_profit.doctype.membership.membership import verify_signature - - -class Donation(Document): - def validate(self): - if not self.donor or not frappe.db.exists('Donor', self.donor): - # for web forms - user_type = frappe.db.get_value('User', frappe.session.user, 'user_type') - if user_type == 'Website User': - self.create_donor_for_website_user() - else: - frappe.throw(_('Please select a Member')) - - def create_donor_for_website_user(self): - donor_name = frappe.get_value('Donor', dict(email=frappe.session.user)) - - if not donor_name: - user = frappe.get_doc('User', frappe.session.user) - donor = frappe.get_doc(dict( - doctype='Donor', - donor_type=self.get('donor_type'), - email=frappe.session.user, - member_name=user.get_fullname() - )).insert(ignore_permissions=True) - donor_name = donor.name - - if self.get('__islocal'): - self.donor = donor_name - - def on_payment_authorized(self, *args, **kwargs): - self.load_from_db() - self.create_payment_entry() - - def create_payment_entry(self, date=None): - settings = frappe.get_doc('Non Profit Settings') - if not settings.automate_donation_payment_entries: - return - - if not settings.donation_payment_account: - frappe.throw(_('You need to set Payment Account for Donation in {0}').format( - get_link_to_form('Non Profit Settings', 'Non Profit Settings'))) - - from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry - - frappe.flags.ignore_account_permission = True - pe = get_payment_entry(dt=self.doctype, dn=self.name) - frappe.flags.ignore_account_permission = False - pe.paid_from = settings.donation_debit_account - pe.paid_to = settings.donation_payment_account - pe.posting_date = date or getdate() - pe.reference_no = self.name - pe.reference_date = date or getdate() - pe.flags.ignore_mandatory = True - pe.insert() - pe.submit() - - -@frappe.whitelist(allow_guest=True) -def capture_razorpay_donations(*args, **kwargs): - """ - Creates Donation from Razorpay Webhook Request Data on payment.captured event - Creates Donor from email if not found - """ - data = frappe.request.get_data(as_text=True) - - try: - verify_signature(data, endpoint='Donation') - except Exception as e: - log = frappe.log_error(e, 'Donation Webhook Verification Error') - notify_failure(log) - return { 'status': 'Failed', 'reason': e } - - if isinstance(data, str): - data = json.loads(data) - data = frappe._dict(data) - - payment = data.payload.get('payment', {}).get('entity', {}) - payment = frappe._dict(payment) - - try: - if not data.event == 'payment.captured': - return - - # to avoid capturing subscription payments as donations - if payment.description and 'subscription' in str(payment.description).lower(): - return - - donor = get_donor(payment.email) - if not donor: - donor = create_donor(payment) - - donation = create_donation(donor, payment) - donation.run_method('create_payment_entry') - - except Exception as e: - 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 donation entry for {0}').format(donor.name)) - notify_failure(log) - return { 'status': 'Failed', 'reason': e } - - return { 'status': 'Success' } - - -def create_donation(donor, payment): - if not frappe.db.exists('Mode of Payment', payment.method): - create_mode_of_payment(payment.method) - - company = get_company_for_donations() - donation = frappe.get_doc({ - 'doctype': 'Donation', - 'company': company, - 'donor': donor.name, - 'donor_name': donor.donor_name, - 'email': donor.email, - 'date': getdate(), - 'amount': flt(payment.amount) / 100, # Convert to rupees from paise - 'mode_of_payment': payment.method, - 'razorpay_payment_id': payment.id - }).insert(ignore_mandatory=True) - - donation.submit() - return donation - - -def get_donor(email): - donors = frappe.get_all('Donor', - filters={'email': email}, - order_by='creation desc') - - try: - return frappe.get_doc('Donor', donors[0]['name']) - except Exception: - return None - - -@frappe.whitelist() -def create_donor(payment): - donor_details = frappe._dict(payment) - donor_type = frappe.db.get_single_value('Non Profit Settings', 'default_donor_type') - - donor = frappe.new_doc('Donor') - donor.update({ - 'donor_name': donor_details.email, - 'donor_type': donor_type, - 'email': donor_details.email, - 'contact': donor_details.contact - }) - - if donor_details.get('notes'): - donor = get_additional_notes(donor, donor_details) - - donor.insert(ignore_mandatory=True) - return donor - - -def get_company_for_donations(): - company = frappe.db.get_single_value('Non Profit Settings', 'donation_company') - if not company: - from erpnext.non_profit.utils import get_company - company = get_company() - return company - - -def get_additional_notes(donor, donor_details): - if type(donor_details.notes) == dict: - for k, v in donor_details.notes.items(): - notes = '\n'.join('{}: {}'.format(k, v)) - - # extract donor name from notes - if 'name' in k.lower(): - donor.update({ - 'donor_name': donor_details.notes.get(k) - }) - - # extract pan from notes - if 'pan' in k.lower(): - donor.update({ - 'pan_number': donor_details.notes.get(k) - }) - - donor.add_comment('Comment', notes) - - elif type(donor_details.notes) == str: - donor.add_comment('Comment', donor_details.notes) - - return donor - - -def create_mode_of_payment(method): - frappe.get_doc({ - 'doctype': 'Mode of Payment', - 'mode_of_payment': method - }).insert(ignore_mandatory=True) - - -def notify_failure(log): - try: - content = ''' - Dear System Manager, - Razorpay webhook for creating donation failed due to some reason. - Please check the error log linked below - Error Log: {0} - Regards, Administrator - '''.format(get_link_to_form('Error Log', log.name)) - - sendmail_to_system_managers(_('[Important] [ERPNext] Razorpay donation webhook failed, please check.'), content) - except Exception: - pass diff --git a/erpnext/non_profit/doctype/donation/donation_dashboard.py b/erpnext/non_profit/doctype/donation/donation_dashboard.py deleted file mode 100644 index 492ad62171..0000000000 --- a/erpnext/non_profit/doctype/donation/donation_dashboard.py +++ /dev/null @@ -1,16 +0,0 @@ -from frappe import _ - - -def get_data(): - return { - 'fieldname': 'donation', - 'non_standard_fieldnames': { - 'Payment Entry': 'reference_name' - }, - 'transactions': [ - { - 'label': _('Payment'), - 'items': ['Payment Entry'] - } - ] - } diff --git a/erpnext/non_profit/doctype/donation/test_donation.py b/erpnext/non_profit/doctype/donation/test_donation.py deleted file mode 100644 index 5fa731a6aa..0000000000 --- a/erpnext/non_profit/doctype/donation/test_donation.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - -import frappe - -from erpnext.non_profit.doctype.donation.donation import create_donation - - -class TestDonation(unittest.TestCase): - def setUp(self): - create_donor_type() - settings = frappe.get_doc('Non Profit Settings') - settings.company = '_Test Company' - settings.donation_company = '_Test Company' - settings.default_donor_type = '_Test Donor' - settings.automate_donation_payment_entries = 1 - settings.donation_debit_account = 'Debtors - _TC' - settings.donation_payment_account = 'Cash - _TC' - settings.creation_user = 'Administrator' - settings.flags.ignore_permissions = True - settings.save() - - def test_payment_entry_for_donations(self): - donor = create_donor() - create_mode_of_payment() - payment = frappe._dict({ - 'amount': 100, - 'method': 'Debit Card', - 'id': 'pay_MeXAmsgeKOhq7O' - }) - donation = create_donation(donor, payment) - - self.assertTrue(donation.name) - - # Naive test to check if at all payment entry is generated - # This method is actually triggered from Payment Gateway - # In any case if details were missing, this would throw an error - donation.on_payment_authorized() - donation.reload() - - self.assertEqual(donation.paid, 1) - self.assertTrue(frappe.db.exists('Payment Entry', {'reference_no': donation.name})) - - -def create_donor_type(): - if not frappe.db.exists('Donor Type', '_Test Donor'): - frappe.get_doc({ - 'doctype': 'Donor Type', - 'donor_type': '_Test Donor' - }).insert() - - -def create_donor(): - donor = frappe.db.exists('Donor', 'donor@test.com') - if donor: - return frappe.get_doc('Donor', 'donor@test.com') - else: - return frappe.get_doc({ - 'doctype': 'Donor', - 'donor_name': '_Test Donor', - 'donor_type': '_Test Donor', - 'email': 'donor@test.com' - }).insert() - - -def create_mode_of_payment(): - if not frappe.db.exists('Mode of Payment', 'Debit Card'): - frappe.get_doc({ - 'doctype': 'Mode of Payment', - 'mode_of_payment': 'Debit Card', - 'accounts': [{ - 'company': '_Test Company', - 'default_account': 'Cash - _TC' - }] - }).insert() diff --git a/erpnext/non_profit/doctype/donor/__init__.py b/erpnext/non_profit/doctype/donor/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/doctype/donor/donor.js b/erpnext/non_profit/doctype/donor/donor.js deleted file mode 100644 index 090d5af32e..0000000000 --- a/erpnext/non_profit/doctype/donor/donor.js +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Donor', { - refresh: function(frm) { - frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Donor'}; - - frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal); - - if(!frm.doc.__islocal) { - frappe.contacts.render_address_and_contact(frm); - } else { - frappe.contacts.clear_address_and_contact(frm); - } - - } -}); diff --git a/erpnext/non_profit/doctype/donor/donor.json b/erpnext/non_profit/doctype/donor/donor.json deleted file mode 100644 index 72f24ef922..0000000000 --- a/erpnext/non_profit/doctype/donor/donor.json +++ /dev/null @@ -1,110 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "autoname": "field:email", - "creation": "2017-09-19 16:20:27.510196", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "donor_name", - "column_break_5", - "donor_type", - "email", - "image", - "address_contacts", - "address_html", - "column_break_9", - "contact_html" - ], - "fields": [ - { - "fieldname": "donor_name", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Donor Name", - "reqd": 1 - }, - { - "fieldname": "column_break_5", - "fieldtype": "Column Break" - }, - { - "fieldname": "donor_type", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Donor Type", - "options": "Donor Type", - "reqd": 1 - }, - { - "fieldname": "email", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Email", - "reqd": 1, - "unique": 1 - }, - { - "fieldname": "image", - "fieldtype": "Attach Image", - "hidden": 1, - "label": "Image", - "no_copy": 1, - "print_hide": 1 - }, - { - "depends_on": "eval:!doc.__islocal;", - "fieldname": "address_contacts", - "fieldtype": "Section Break", - "label": "Address and Contact", - "options": "fa fa-map-marker" - }, - { - "fieldname": "address_html", - "fieldtype": "HTML", - "label": "Address HTML" - }, - { - "fieldname": "column_break_9", - "fieldtype": "Column Break" - }, - { - "fieldname": "contact_html", - "fieldtype": "HTML", - "label": "Contact HTML" - } - ], - "image_field": "image", - "links": [ - { - "link_doctype": "Donation", - "link_fieldname": "donor" - } - ], - "modified": "2021-02-17 16:36:33.470731", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Donor", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Non Profit Manager", - "share": 1, - "write": 1 - } - ], - "quick_entry": 1, - "restrict_to_domain": "Non Profit", - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "donor_name", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/non_profit/doctype/donor/donor.py b/erpnext/non_profit/doctype/donor/donor.py deleted file mode 100644 index 058321b159..0000000000 --- a/erpnext/non_profit/doctype/donor/donor.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -from frappe.contacts.address_and_contact import load_address_and_contact -from frappe.model.document import Document - - -class Donor(Document): - def onload(self): - """Load address and contacts in `__onload`""" - load_address_and_contact(self) - - def validate(self): - from frappe.utils import validate_email_address - if self.email: - validate_email_address(self.email.strip(), True) diff --git a/erpnext/non_profit/doctype/donor/donor_list.js b/erpnext/non_profit/doctype/donor/donor_list.js deleted file mode 100644 index 31d4d292e7..0000000000 --- a/erpnext/non_profit/doctype/donor/donor_list.js +++ /dev/null @@ -1,3 +0,0 @@ -frappe.listview_settings['Donor'] = { - add_fields: ["donor_name", "donor_type", "image"], -}; diff --git a/erpnext/non_profit/doctype/donor/test_donor.py b/erpnext/non_profit/doctype/donor/test_donor.py deleted file mode 100644 index fe591c8e72..0000000000 --- a/erpnext/non_profit/doctype/donor/test_donor.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - - -class TestDonor(unittest.TestCase): - pass diff --git a/erpnext/non_profit/doctype/donor_type/__init__.py b/erpnext/non_profit/doctype/donor_type/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/doctype/donor_type/donor_type.js b/erpnext/non_profit/doctype/donor_type/donor_type.js deleted file mode 100644 index 7b1fd4fe89..0000000000 --- a/erpnext/non_profit/doctype/donor_type/donor_type.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Donor Type', { - refresh: function() { - - } -}); diff --git a/erpnext/non_profit/doctype/donor_type/donor_type.json b/erpnext/non_profit/doctype/donor_type/donor_type.json deleted file mode 100644 index 07118fdc82..0000000000 --- a/erpnext/non_profit/doctype/donor_type/donor_type.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:donor_type", - "beta": 0, - "creation": "2017-09-19 16:19:16.639635", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "donor_type", - "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": 1, - "label": "Donor Type", - "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, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2017-12-05 07:04:36.757595", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Donor Type", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Non Profit Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Non Profit", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/non_profit/doctype/donor_type/donor_type.py b/erpnext/non_profit/doctype/donor_type/donor_type.py deleted file mode 100644 index 17dca899d5..0000000000 --- a/erpnext/non_profit/doctype/donor_type/donor_type.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -from frappe.model.document import Document - - -class DonorType(Document): - pass diff --git a/erpnext/non_profit/doctype/donor_type/test_donor_type.py b/erpnext/non_profit/doctype/donor_type/test_donor_type.py deleted file mode 100644 index d433733ee2..0000000000 --- a/erpnext/non_profit/doctype/donor_type/test_donor_type.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - - -class TestDonorType(unittest.TestCase): - pass diff --git a/erpnext/non_profit/doctype/grant_application/__init__.py b/erpnext/non_profit/doctype/grant_application/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/doctype/grant_application/grant_application.js b/erpnext/non_profit/doctype/grant_application/grant_application.js deleted file mode 100644 index 70f319b828..0000000000 --- a/erpnext/non_profit/doctype/grant_application/grant_application.js +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Grant Application', { - refresh: function(frm) { - frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Grant Application'}; - - frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal); - - if(!frm.doc.__islocal) { - frappe.contacts.render_address_and_contact(frm); - } else { - frappe.contacts.clear_address_and_contact(frm); - } - - if(frm.doc.status == 'Received' && !frm.doc.email_notification_sent){ - frm.add_custom_button(__("Send Grant Review Email"), function() { - frappe.call({ - method: "erpnext.non_profit.doctype.grant_application.grant_application.send_grant_review_emails", - args: { - grant_application: frm.doc.name - } - }); - }); - } - } -}); diff --git a/erpnext/non_profit/doctype/grant_application/grant_application.json b/erpnext/non_profit/doctype/grant_application/grant_application.json deleted file mode 100644 index 2eb2087925..0000000000 --- a/erpnext/non_profit/doctype/grant_application/grant_application.json +++ /dev/null @@ -1,851 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 1, - "allow_import": 0, - "allow_rename": 0, - "autoname": "", - "beta": 0, - "creation": "2017-09-21 12:02:01.206913", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "applicant_type", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Applicant Type", - "length": 0, - "no_copy": 0, - "options": "Individual\nOrganization", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "applicant_name", - "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": "Name", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.applicant_type=='Organization'", - "fieldname": "contact_person", - "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": "Contact Person", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "email", - "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": "Email", - "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, - "unique": 0 - }, - { - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Open", - "fieldname": "status", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Status", - "length": 0, - "no_copy": 0, - "options": "Open\nReceived\nIn Progress\nApproved\nRejected\nExpired\nWithdrawn", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "website_url", - "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": "Website URL", - "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, - "unique": 0 - }, - { - "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": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address_contacts", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Address and Contact", - "length": 0, - "no_copy": 0, - "options": "fa fa-map-marker", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address_html", - "fieldtype": "HTML", - "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": "Address HTML", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_9", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "contact_html", - "fieldtype": "HTML", - "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": "Contact HTML", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "grant_application_details", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Grant Application Details ", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "grant_description", - "fieldtype": "Long Text", - "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": "Grant 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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_15", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amount", - "fieldtype": "Currency", - "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": "Requested Amount", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "has_any_past_grant_record", - "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": "Has any past Grant Record", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_17", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "published", - "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": "Show 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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "assessment_result", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Assessment Result", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "assessment_mark", - "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": "Assessment Mark (Out of 10)", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "note", - "fieldtype": "Small Text", - "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": "Note", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_24", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "assessment_manager", - "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": "Assessment Manager", - "length": 0, - "no_copy": 0, - "options": "User", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "email_notification_sent", - "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": "Email Notification Sent", - "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, - "unique": 0 - } - ], - "has_web_view": 1, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_field": "", - "image_view": 0, - "in_create": 0, - "is_published_field": "published", - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2017-12-06 12:39:57.677899", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Grant Application", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Non Profit Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Non Profit", - "route": "grant-application", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "applicant_name", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/non_profit/doctype/grant_application/grant_application.py b/erpnext/non_profit/doctype/grant_application/grant_application.py deleted file mode 100644 index cc5e1b1442..0000000000 --- a/erpnext/non_profit/doctype/grant_application/grant_application.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -from frappe import _ -from frappe.contacts.address_and_contact import load_address_and_contact -from frappe.utils import get_url -from frappe.website.website_generator import WebsiteGenerator - - -class GrantApplication(WebsiteGenerator): - _website = frappe._dict( - condition_field = "published", - ) - - def validate(self): - if not self.route: #pylint: disable=E0203 - self.route = 'grant-application/' + self.scrub(self.name) - - def onload(self): - """Load address and contacts in `__onload`""" - load_address_and_contact(self) - - def get_context(self, context): - context.no_cache = True - context.show_sidebar = True - context.parents = [dict(label='View All Grant Applications', - route='grant-application', title='View Grants')] - -def get_list_context(context): - context.allow_guest = True - context.no_cache = True - context.no_breadcrumbs = True - context.show_sidebar = True - context.order_by = 'creation desc' - context.introduction =''' - Apply for new Grant Application''' - -@frappe.whitelist() -def send_grant_review_emails(grant_application): - grant = frappe.get_doc("Grant Application", grant_application) - url = get_url('grant-application/{0}'.format(grant_application)) - frappe.sendmail( - recipients= grant.assessment_manager, - sender=frappe.session.user, - subject='Grant Application for {0}'.format(grant.applicant_name), - message='

Please Review this grant application


' + url, - reference_doctype=grant.doctype, - reference_name=grant.name - ) - - grant.status = 'In Progress' - grant.email_notification_sent = 1 - grant.save() - frappe.db.commit() - - frappe.msgprint(_("Review Invitation Sent")) diff --git a/erpnext/non_profit/doctype/grant_application/templates/grant_application.html b/erpnext/non_profit/doctype/grant_application/templates/grant_application.html deleted file mode 100644 index 52e8469284..0000000000 --- a/erpnext/non_profit/doctype/grant_application/templates/grant_application.html +++ /dev/null @@ -1,68 +0,0 @@ -{% extends "templates/web.html" %} - -{% block page_content %} -

{{ applicant_name }}

- {% if frappe.user == owner %} -

Edit Grant

- {% endif %} -
- - - - - - - - - - - - - - - - - - - - - -
Organization/Indvidual{{ applicant_type }}
Grant Applicant Name{{ applicant_name}}
Date{{ frappe.format_date(creation) }}
Status{{ status }}
Email{{ email }}
-

Q. Please outline your current situation and why you are applying for a grant?

-

{{ grant_description }}

-

Q. Requested grant amount

-

{{ amount }}

-

Q. Have you recevied grant from us before?

-

{{ has_any_past_grant_record }}

-

Contact

- {% if frappe.user != 'Guest' %} - - {% if contact_person %} - - - - - {% endif %} - - - - -
Contact Person{{ contact_person }}
Email{{ email }}
- {% else %} -

You must register and login to view contact details

- {% endif %} -
- {% if frappe.session.user == assessment_manager %} - {% if assessment_scale %} -

Assessment Review done

- {% endif %} - {% else %} -


Post a New Grant

- {% endif %} -{% endblock %} -{% block style %} - - -{% endblock %} diff --git a/erpnext/non_profit/doctype/grant_application/templates/grant_application_row.html b/erpnext/non_profit/doctype/grant_application/templates/grant_application_row.html deleted file mode 100644 index e375b16154..0000000000 --- a/erpnext/non_profit/doctype/grant_application/templates/grant_application_row.html +++ /dev/null @@ -1,11 +0,0 @@ -{% if doc.published %} - -{% endif %} diff --git a/erpnext/non_profit/doctype/grant_application/test_grant_application.py b/erpnext/non_profit/doctype/grant_application/test_grant_application.py deleted file mode 100644 index ef267d7af8..0000000000 --- a/erpnext/non_profit/doctype/grant_application/test_grant_application.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - - -class TestGrantApplication(unittest.TestCase): - pass diff --git a/erpnext/non_profit/doctype/member/__init__.py b/erpnext/non_profit/doctype/member/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/doctype/member/member.js b/erpnext/non_profit/doctype/member/member.js deleted file mode 100644 index e58ec0f5ee..0000000000 --- a/erpnext/non_profit/doctype/member/member.js +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Member', { - setup: function(frm) { - frappe.db.get_single_value('Non Profit Settings', 'enable_razorpay_for_memberships').then(val => { - if (val && (frm.doc.subscription_id || frm.doc.customer_id)) { - frm.set_df_property('razorpay_details_section', 'hidden', false); - } - }) - }, - - refresh: function(frm) { - - frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Member'}; - - frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal); - - if(!frm.doc.__islocal) { - frappe.contacts.render_address_and_contact(frm); - - // custom buttons - frm.add_custom_button(__('Accounting Ledger'), function() { - frappe.set_route('query-report', 'General Ledger', - {party_type:'Member', party:frm.doc.name}); - }); - - frm.add_custom_button(__('Accounts Receivable'), function() { - frappe.set_route('query-report', 'Accounts Receivable', {member:frm.doc.name}); - }); - - if (!frm.doc.customer) { - frm.add_custom_button(__('Create Customer'), () => { - frm.call('make_customer_and_link').then(() => { - frm.reload_doc(); - }); - }); - } - - // indicator - erpnext.utils.set_party_dashboard_indicators(frm); - - } else { - frappe.contacts.clear_address_and_contact(frm); - } - - frappe.call({ - method:"frappe.client.get_value", - args:{ - 'doctype':"Membership", - 'filters':{'member': frm.doc.name}, - 'fieldname':[ - 'to_date' - ] - }, - callback: function (data) { - if(data.message) { - frappe.model.set_value(frm.doctype,frm.docname, - "membership_expiry_date", data.message.to_date); - } - } - }); - } -}); diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json deleted file mode 100644 index 7c1baf1a8d..0000000000 --- a/erpnext/non_profit/doctype/member/member.json +++ /dev/null @@ -1,210 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "autoname": "naming_series:", - "creation": "2017-09-11 09:24:52.898356", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "naming_series", - "member_name", - "membership_expiry_date", - "column_break_5", - "membership_type", - "email_id", - "image", - "customer_section", - "customer", - "customer_name", - "supplier_section", - "supplier", - "address_contacts", - "address_html", - "column_break_9", - "contact_html", - "razorpay_details_section", - "subscription_id", - "customer_id", - "subscription_status", - "column_break_21", - "subscription_start", - "subscription_end" - ], - "fields": [ - { - "fieldname": "naming_series", - "fieldtype": "Select", - "label": "Series", - "options": "NPO-MEM-.YYYY.-", - "reqd": 1 - }, - { - "fieldname": "member_name", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Member Name", - "reqd": 1 - }, - { - "fieldname": "membership_expiry_date", - "fieldtype": "Date", - "label": "Membership Expiry Date" - }, - { - "fieldname": "column_break_5", - "fieldtype": "Column Break" - }, - { - "fieldname": "membership_type", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Membership Type", - "options": "Membership Type", - "reqd": 1 - }, - { - "fieldname": "image", - "fieldtype": "Attach Image", - "hidden": 1, - "label": "Image", - "no_copy": 1, - "print_hide": 1 - }, - { - "collapsible": 1, - "fieldname": "customer_section", - "fieldtype": "Section Break", - "label": "Customer" - }, - { - "fieldname": "customer", - "fieldtype": "Link", - "label": "Customer", - "options": "Customer" - }, - { - "fetch_from": "customer.customer_name", - "fieldname": "customer_name", - "fieldtype": "Data", - "label": "Customer Name", - "read_only": 1 - }, - { - "collapsible": 1, - "fieldname": "supplier_section", - "fieldtype": "Section Break", - "label": "Supplier" - }, - { - "fieldname": "supplier", - "fieldtype": "Link", - "label": "Supplier", - "options": "Supplier" - }, - { - "depends_on": "eval:!doc.__islocal;", - "fieldname": "address_contacts", - "fieldtype": "Section Break", - "label": "Address and Contact", - "options": "fa fa-map-marker" - }, - { - "fieldname": "address_html", - "fieldtype": "HTML", - "label": "Address HTML" - }, - { - "fieldname": "column_break_9", - "fieldtype": "Column Break" - }, - { - "fieldname": "contact_html", - "fieldtype": "HTML", - "label": "Contact HTML" - }, - { - "fieldname": "email_id", - "fieldtype": "Data", - "label": "Email Address", - "options": "Email" - }, - { - "fieldname": "subscription_id", - "fieldtype": "Data", - "label": "Subscription ID", - "read_only": 1 - }, - { - "fieldname": "customer_id", - "fieldtype": "Data", - "label": "Customer ID", - "read_only": 1 - }, - { - "fieldname": "razorpay_details_section", - "fieldtype": "Section Break", - "hidden": 1, - "label": "Razorpay Details" - }, - { - "fieldname": "column_break_21", - "fieldtype": "Column Break" - }, - { - "fieldname": "subscription_start", - "fieldtype": "Date", - "label": "Subscription Start " - }, - { - "fieldname": "subscription_end", - "fieldtype": "Date", - "label": "Subscription End" - }, - { - "fieldname": "subscription_status", - "fieldtype": "Select", - "label": "Subscription Status", - "options": "\nActive\nHalted" - } - ], - "image_field": "image", - "links": [], - "modified": "2021-07-11 14:27:26.368039", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Member", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Non Profit Manager", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Non Profit Member", - "share": 1, - "write": 1 - } - ], - "quick_entry": 1, - "restrict_to_domain": "Non Profit", - "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/member/member.py b/erpnext/non_profit/doctype/member/member.py deleted file mode 100644 index 4d80e57ecc..0000000000 --- a/erpnext/non_profit/doctype/member/member.py +++ /dev/null @@ -1,185 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -from frappe import _ -from frappe.contacts.address_and_contact import load_address_and_contact -from frappe.integrations.utils import get_payment_gateway_controller -from frappe.model.document import Document -from frappe.utils import cint, get_link_to_form - -from erpnext.non_profit.doctype.membership_type.membership_type import get_membership_type - - -class Member(Document): - def onload(self): - """Load address and contacts in `__onload`""" - load_address_and_contact(self) - - - def validate(self): - if self.email_id: - self.validate_email_type(self.email_id) - - def validate_email_type(self, email): - from frappe.utils import validate_email_address - validate_email_address(email.strip(), True) - - def setup_subscription(self): - non_profit_settings = frappe.get_doc('Non Profit Settings') - if not non_profit_settings.enable_razorpay_for_memberships: - frappe.throw(_('Please check Enable Razorpay for Memberships in {0} to setup subscription')).format( - get_link_to_form('Non Profit Settings', 'Non Profit Settings')) - - controller = get_payment_gateway_controller("Razorpay") - settings = controller.get_settings({}) - - plan_id = frappe.get_value("Membership Type", self.membership_type, "razorpay_plan_id") - - if not plan_id: - frappe.throw(_("Please setup Razorpay Plan ID")) - - subscription_details = { - "plan_id": plan_id, - "billing_frequency": cint(non_profit_settings.billing_frequency), - "customer_notify": 1 - } - - args = { - 'subscription_details': subscription_details - } - - subscription = controller.setup_subscription(settings, **args) - - return subscription - - @frappe.whitelist() - def make_customer_and_link(self): - if self.customer: - frappe.msgprint(_("A customer is already linked to this Member")) - - customer = create_customer(frappe._dict({ - 'fullname': self.member_name, - 'email': self.email_id, - 'phone': None - })) - - self.customer = customer - self.save() - frappe.msgprint(_("Customer {0} has been created succesfully.").format(self.customer)) - - -def get_or_create_member(user_details): - member_list = frappe.get_all("Member", filters={'email': user_details.email, 'membership_type': user_details.plan_id}) - if member_list and member_list[0]: - return member_list[0]['name'] - else: - return create_member(user_details) - -def create_member(user_details): - user_details = frappe._dict(user_details) - member = frappe.new_doc("Member") - member.update({ - "member_name": user_details.fullname, - "email_id": user_details.email, - "pan_number": user_details.pan or None, - "membership_type": user_details.plan_id, - "customer_id": user_details.customer_id or None, - "subscription_id": user_details.subscription_id or None, - "subscription_status": user_details.subscription_status or "" - }) - - member.insert(ignore_permissions=True) - member.customer = create_customer(user_details, member.name) - member.save(ignore_permissions=True) - - return member - -def create_customer(user_details, member=None): - customer = frappe.new_doc("Customer") - customer.customer_name = user_details.fullname - customer.customer_type = "Individual" - customer.flags.ignore_mandatory = True - customer.insert(ignore_permissions=True) - - try: - contact = frappe.new_doc("Contact") - contact.first_name = user_details.fullname - if user_details.mobile: - contact.add_phone(user_details.mobile, is_primary_phone=1, is_primary_mobile_no=1) - if user_details.email: - contact.add_email(user_details.email, is_primary=1) - contact.insert(ignore_permissions=True) - - contact.append("links", { - "link_doctype": "Customer", - "link_name": customer.name - }) - - if member: - contact.append("links", { - "link_doctype": "Member", - "link_name": member - }) - - contact.save(ignore_permissions=True) - - except frappe.DuplicateEntryError: - return customer.name - - except Exception as e: - frappe.log_error(frappe.get_traceback(), _("Contact Creation Failed")) - pass - - return customer.name - -@frappe.whitelist(allow_guest=True) -def create_member_subscription_order(user_details): - """Create Member subscription and order for payment - - Args: - user_details (TYPE): Description - - Returns: - Dictionary: Dictionary with subscription details - { - 'subscription_details': { - 'plan_id': 'plan_EXwyxDYDCj3X4v', - 'billing_frequency': 24, - 'customer_notify': 1 - }, - 'subscription_id': 'sub_EZycCvXFvqnC6p' - } - """ - - user_details = frappe._dict(user_details) - member = get_or_create_member(user_details) - - subscription = member.setup_subscription() - - member.subscription_id = subscription.get('subscription_id') - member.save(ignore_permissions=True) - - return subscription - -@frappe.whitelist() -def register_member(fullname, email, rzpay_plan_id, subscription_id, pan=None, mobile=None): - plan = get_membership_type(rzpay_plan_id) - if not plan: - raise frappe.DoesNotExistError - - member = frappe.db.exists("Member", {'email': email, 'subscription_id': subscription_id }) - if member: - return member - else: - member = create_member(dict( - fullname=fullname, - email=email, - plan_id=plan, - subscription_id=subscription_id, - pan=pan, - mobile=mobile - )) - - return member.name diff --git a/erpnext/non_profit/doctype/member/member_dashboard.py b/erpnext/non_profit/doctype/member/member_dashboard.py deleted file mode 100644 index 0e31e3ceb8..0000000000 --- a/erpnext/non_profit/doctype/member/member_dashboard.py +++ /dev/null @@ -1,22 +0,0 @@ -from frappe import _ - - -def get_data(): - return { - 'heatmap': True, - 'heatmap_message': _('Member Activity'), - 'fieldname': 'member', - 'non_standard_fieldnames': { - 'Bank Account': 'party' - }, - 'transactions': [ - { - 'label': _('Membership Details'), - 'items': ['Membership'] - }, - { - 'label': _('Fee'), - 'items': ['Bank Account'] - } - ] - } diff --git a/erpnext/non_profit/doctype/member/member_list.js b/erpnext/non_profit/doctype/member/member_list.js deleted file mode 100644 index 8e41e7fdde..0000000000 --- a/erpnext/non_profit/doctype/member/member_list.js +++ /dev/null @@ -1,3 +0,0 @@ -frappe.listview_settings['Member'] = { - add_fields: ["member_name", "membership_type", "image"], -}; diff --git a/erpnext/non_profit/doctype/member/test_member.py b/erpnext/non_profit/doctype/member/test_member.py deleted file mode 100644 index 46f14ed131..0000000000 --- a/erpnext/non_profit/doctype/member/test_member.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - - -class TestMember(unittest.TestCase): - pass diff --git a/erpnext/non_profit/doctype/membership/__init__.py b/erpnext/non_profit/doctype/membership/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/doctype/membership/membership.js b/erpnext/non_profit/doctype/membership/membership.js deleted file mode 100644 index 31872048a0..0000000000 --- a/erpnext/non_profit/doctype/membership/membership.js +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Membership', { - setup: function(frm) { - frappe.db.get_single_value("Non Profit Settings", "enable_razorpay_for_memberships").then(val => { - if (val) frm.set_df_property("razorpay_details_section", "hidden", false); - }) - }, - - refresh: function(frm) { - if (frm.doc.__islocal) - return; - - !frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => { - frm.call({ - 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(); - } - }); - }); - - frappe.db.get_single_value("Non Profit Settings", "send_email").then(val => { - if (val) frm.add_custom_button("Send Acknowledgement", () => { - frm.call("send_acknowlement").then(() => { - frm.reload_doc(); - }); - }); - }) - }, - - onload: function(frm) { - frm.add_fetch("membership_type", "amount", "amount"); - } -}); diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json deleted file mode 100644 index 11d32f9c2b..0000000000 --- a/erpnext/non_profit/doctype/membership/membership.json +++ /dev/null @@ -1,184 +0,0 @@ -{ - "actions": [], - "autoname": "NPO-MSH-.YYYY.-.#####", - "creation": "2017-09-11 11:39:18.492184", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "member", - "member_name", - "membership_type", - "column_break_3", - "company", - "membership_status", - "membership_validity_section", - "from_date", - "to_date", - "column_break_8", - "member_since_date", - "payment_details", - "paid", - "currency", - "amount", - "invoice", - "razorpay_details_section", - "subscription_id", - "payment_id" - ], - "fields": [ - { - "fieldname": "member", - "fieldtype": "Link", - "label": "Member", - "options": "Member" - }, - { - "fieldname": "membership_type", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Membership Type", - "options": "Membership Type", - "reqd": 1 - }, - { - "fieldname": "column_break_3", - "fieldtype": "Column Break" - }, - { - "fieldname": "membership_status", - "fieldtype": "Select", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Membership Status", - "options": "New\nCurrent\nExpired\nPending\nCancelled" - }, - { - "fieldname": "membership_validity_section", - "fieldtype": "Section Break", - "label": "Validity" - }, - { - "fieldname": "from_date", - "fieldtype": "Date", - "in_list_view": 1, - "label": "From", - "reqd": 1 - }, - { - "fieldname": "to_date", - "fieldtype": "Date", - "in_list_view": 1, - "label": "To", - "reqd": 1 - }, - { - "fieldname": "column_break_8", - "fieldtype": "Column Break" - }, - { - "fieldname": "member_since_date", - "fieldtype": "Date", - "label": "Member Since" - }, - { - "fieldname": "payment_details", - "fieldtype": "Section Break", - "label": "Payment Details" - }, - { - "default": "0", - "fieldname": "paid", - "fieldtype": "Check", - "label": "Paid" - }, - { - "fieldname": "currency", - "fieldtype": "Link", - "label": "Currency", - "options": "Currency" - }, - { - "fieldname": "amount", - "fieldtype": "Float", - "label": "Amount" - }, - { - "fieldname": "razorpay_details_section", - "fieldtype": "Section Break", - "hidden": 1, - "label": "Razorpay Details" - }, - { - "fieldname": "subscription_id", - "fieldtype": "Data", - "label": "Subscription ID", - "read_only": 1 - }, - { - "fieldname": "payment_id", - "fieldtype": "Data", - "label": "Payment ID", - "read_only": 1 - }, - { - "fieldname": "invoice", - "fieldtype": "Link", - "label": "Invoice", - "options": "Sales Invoice" - }, - { - "fetch_from": "member.member_name", - "fieldname": "member_name", - "fieldtype": "Data", - "label": "Member Name", - "read_only": 1 - }, - { - "fieldname": "company", - "fieldtype": "Link", - "label": "Company", - "options": "Company", - "reqd": 1 - } - ], - "index_web_pages_for_search": 1, - "links": [], - "modified": "2021-02-19 14:33:44.925122", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Membership", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Non Profit Manager", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Non Profit Member", - "share": 1, - "write": 1 - } - ], - "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_list.js b/erpnext/non_profit/doctype/membership/membership_list.js deleted file mode 100644 index a959159899..0000000000 --- a/erpnext/non_profit/doctype/membership/membership_list.js +++ /dev/null @@ -1,15 +0,0 @@ -frappe.listview_settings['Membership'] = { - get_indicator: function(doc) { - if (doc.membership_status == 'New') { - return [__('New'), 'blue', 'membership_status,=,New']; - } else if (doc.membership_status === 'Current') { - return [__('Current'), 'green', 'membership_status,=,Current']; - } else if (doc.membership_status === 'Pending') { - return [__('Pending'), 'yellow', 'membership_status,=,Pending']; - } else if (doc.membership_status === 'Expired') { - return [__('Expired'), 'grey', 'membership_status,=,Expired']; - } else { - return [__('Cancelled'), 'red', 'membership_status,=,Cancelled']; - } - } -}; diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py deleted file mode 100644 index fbe344c6a1..0000000000 --- a/erpnext/non_profit/doctype/membership/test_membership.py +++ /dev/null @@ -1,164 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - -import frappe -from frappe.utils import add_months, nowdate - -import erpnext -from erpnext.non_profit.doctype.member.member import create_member -from erpnext.non_profit.doctype.membership.membership import update_halted_razorpay_subscription - - -class TestMembership(unittest.TestCase): - def setUp(self): - plan = setup_membership() - - # make test member - self.member_doc = create_member( - frappe._dict({ - "fullname": "_Test_Member", - "email": "_test_member_erpnext@example.com", - "plan_id": plan.name, - "subscription_id": "sub_DEX6xcJ1HSW4CR", - "customer_id": "cust_C0WlbKhp3aLA7W", - "subscription_status": "Active" - }) - ) - self.member_doc.make_customer_and_link() - self.member = self.member_doc.name - - def test_auto_generate_invoice_and_payment_entry(self): - entry = make_membership(self.member) - - # Naive test to see if at all invoice was generated and attached to member - # In any case if details were missing, the invoicing would throw an error - invoice = entry.generate_invoice(save=True) - self.assertEqual(invoice.name, entry.invoice) - - def test_renew_within_30_days(self): - # create a membership for two months - # Should work fine - make_membership(self.member, { "from_date": nowdate() }) - make_membership(self.member, { "from_date": add_months(nowdate(), 1) }) - - from frappe.utils.user import add_role - add_role("test@example.com", "Non Profit Manager") - frappe.set_user("test@example.com") - - # create next membership with expiry not within 30 days - self.assertRaises(frappe.ValidationError, make_membership, self.member, { - "from_date": add_months(nowdate(), 2), - }) - - frappe.set_user("Administrator") - # create the same membership but as administrator - make_membership(self.member, { - "from_date": add_months(nowdate(), 2), - "to_date": add_months(nowdate(), 3), - }) - - def test_halted_memberships(self): - make_membership(self.member, { - "from_date": add_months(nowdate(), 2), - "to_date": add_months(nowdate(), 3) - }) - - self.assertEqual(frappe.db.get_value("Member", self.member, "subscription_status"), "Active") - payload = get_subscription_payload() - update_halted_razorpay_subscription(data=payload) - self.assertEqual(frappe.db.get_value("Member", self.member, "subscription_status"), "Halted") - - def tearDown(self): - frappe.db.rollback() - -def set_config(key, value): - frappe.db.set_value("Non Profit Settings", None, key, value) - -def make_membership(member, payload={}): - data = { - "doctype": "Membership", - "member": member, - "membership_status": "Current", - "membership_type": "_rzpy_test_milythm", - "currency": "INR", - "paid": 1, - "from_date": nowdate(), - "amount": 100 - } - data.update(payload) - membership = frappe.get_doc(data) - membership.insert(ignore_permissions=True, ignore_if_duplicate=True) - return membership - -def create_item(item_code): - if not frappe.db.exists("Item", item_code): - item = frappe.new_doc("Item") - item.item_code = item_code - item.item_name = item_code - item.stock_uom = "Nos" - item.description = item_code - item.item_group = "All Item Groups" - item.is_stock_item = 0 - item.save() - else: - item = frappe.get_doc("Item", item_code) - return item - -def setup_membership(): - # Get default company - company = frappe.get_doc("Company", erpnext.get_default_company()) - - # update non profit settings - settings = frappe.get_doc("Non Profit Settings") - # Enable razorpay - settings.enable_razorpay_for_memberships = 1 - settings.billing_cycle = "Monthly" - settings.billing_frequency = 24 - # Enable invoicing - settings.allow_invoicing = 1 - settings.automate_membership_payment_entries = 1 - settings.company = company.name - settings.donation_company = company.name - settings.membership_payment_account = company.default_cash_account - settings.membership_debit_account = company.default_receivable_account - settings.flags.ignore_mandatory = True - settings.save() - - # make test plan - if not frappe.db.exists("Membership Type", "_rzpy_test_milythm"): - plan = frappe.new_doc("Membership Type") - plan.membership_type = "_rzpy_test_milythm" - plan.amount = 100 - plan.razorpay_plan_id = "_rzpy_test_milythm" - plan.linked_item = create_item("_Test Item for Non Profit Membership").name - plan.insert() - else: - plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm") - - return plan - -def get_subscription_payload(): - return { - "entity": "event", - "account_id": "acc_BFQ7uQEaa7j2z7", - "event": "subscription.halted", - "contains": [ - "subscription" - ], - "payload": { - "subscription": { - "entity": { - "id": "sub_DEX6xcJ1HSW4CR", - "entity": "subscription", - "plan_id": "_rzpy_test_milythm", - "customer_id": "cust_C0WlbKhp3aLA7W", - "status": "halted", - "notes": { - "Important": "Notes for Internal Reference" - }, - } - } - } - } diff --git a/erpnext/non_profit/doctype/membership_type/__init__.py b/erpnext/non_profit/doctype/membership_type/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.js b/erpnext/non_profit/doctype/membership_type/membership_type.js deleted file mode 100644 index 2f2427629c..0000000000 --- a/erpnext/non_profit/doctype/membership_type/membership_type.js +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Membership Type', { - refresh: function (frm) { - frappe.db.get_single_value('Non Profit Settings', 'enable_razorpay_for_memberships').then(val => { - if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false); - }); - - frappe.db.get_single_value('Non Profit Settings', 'allow_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.json b/erpnext/non_profit/doctype/membership_type/membership_type.json deleted file mode 100644 index 6ce1ecde12..0000000000 --- a/erpnext/non_profit/doctype/membership_type/membership_type.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "actions": [], - "autoname": "field:membership_type", - "creation": "2017-09-18 12:56:56.343999", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "membership_type", - "amount", - "razorpay_plan_id", - "linked_item" - ], - "fields": [ - { - "fieldname": "membership_type", - "fieldtype": "Data", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Membership Type", - "reqd": 1, - "unique": 1 - }, - { - "fieldname": "amount", - "fieldtype": "Float", - "in_list_view": 1, - "label": "Amount", - "reqd": 1 - }, - { - "fieldname": "razorpay_plan_id", - "fieldtype": "Data", - "hidden": 1, - "label": "Razorpay Plan ID", - "unique": 1 - }, - { - "fieldname": "linked_item", - "fieldtype": "Link", - "label": "Linked Item", - "options": "Item", - "unique": 1 - } - ], - "links": [], - "modified": "2020-08-05 15:21:43.595745", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Membership Type", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Non Profit Manager", - "share": 1, - "write": 1 - } - ], - "quick_entry": 1, - "restrict_to_domain": "Non Profit", - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.py b/erpnext/non_profit/doctype/membership_type/membership_type.py deleted file mode 100644 index b446421571..0000000000 --- a/erpnext/non_profit/doctype/membership_type/membership_type.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -from frappe import _ -from frappe.model.document import Document - - -class MembershipType(Document): - 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}) diff --git a/erpnext/non_profit/doctype/membership_type/test_membership_type.py b/erpnext/non_profit/doctype/membership_type/test_membership_type.py deleted file mode 100644 index 98bc087acd..0000000000 --- a/erpnext/non_profit/doctype/membership_type/test_membership_type.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - - -class TestMembershipType(unittest.TestCase): - pass diff --git a/erpnext/non_profit/doctype/non_profit_settings/__init__.py b/erpnext/non_profit/doctype/non_profit_settings/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js deleted file mode 100644 index 4c4ca9834b..0000000000 --- a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on("Non Profit Settings", { - refresh: function(frm) { - frm.set_query("inv_print_format", function() { - return { - filters: { - "doc_type": "Sales Invoice" - } - }; - }); - - frm.set_query("membership_print_format", function() { - return { - filters: { - "doc_type": "Membership" - } - }; - }); - - frm.set_query("membership_debit_account", function() { - return { - filters: { - "account_type": "Receivable", - "is_group": 0, - "company": frm.doc.company - } - }; - }); - - frm.set_query("donation_debit_account", function() { - return { - filters: { - "account_type": "Receivable", - "is_group": 0, - "company": frm.doc.donation_company - } - }; - }); - - frm.set_query("membership_payment_account", function () { - var account_types = ["Bank", "Cash"]; - return { - filters: { - "account_type": ["in", account_types], - "is_group": 0, - "company": frm.doc.company - } - }; - }); - - frm.set_query("donation_payment_account", function () { - var account_types = ["Bank", "Cash"]; - return { - filters: { - "account_type": ["in", account_types], - "is_group": 0, - "company": frm.doc.donation_company - } - }; - }); - - let docs_url = "https://docs.erpnext.com/docs/user/manual/en/non_profit/membership"; - - frm.set_intro(__("You can learn more about memberships in the manual. ") + `${__('ERPNext Docs')}`, true); - frm.trigger("setup_buttons_for_membership"); - frm.trigger("setup_buttons_for_donation"); - }, - - setup_buttons_for_membership: function(frm) { - let label; - - if (frm.doc.membership_webhook_secret) { - - frm.add_custom_button(__("Copy Webhook URL"), () => { - frappe.utils.copy_to_clipboard(`https://${frappe.boot.sitename}/api/method/erpnext.non_profit.doctype.membership.membership.trigger_razorpay_subscription`); - }, __("Memberships")); - - frm.add_custom_button(__("Revoke Key"), () => { - frm.call("revoke_key", { - key: "membership_webhook_secret" - }).then(() => { - frm.refresh(); - }); - }, __("Memberships")); - - label = __("Regenerate Webhook Secret"); - - } else { - label = __("Generate Webhook Secret"); - } - - frm.add_custom_button(label, () => { - frm.call("generate_webhook_secret", { - field: "membership_webhook_secret" - }).then(() => { - frm.refresh(); - }); - }, __("Memberships")); - }, - - setup_buttons_for_donation: function(frm) { - let label; - - if (frm.doc.donation_webhook_secret) { - label = __("Regenerate Webhook Secret"); - - frm.add_custom_button(__("Copy Webhook URL"), () => { - frappe.utils.copy_to_clipboard(`https://${frappe.boot.sitename}/api/method/erpnext.non_profit.doctype.donation.donation.capture_razorpay_donations`); - }, __("Donations")); - - frm.add_custom_button(__("Revoke Key"), () => { - frm.call("revoke_key", { - key: "donation_webhook_secret" - }).then(() => { - frm.refresh(); - }); - }, __("Donations")); - - } else { - label = __("Generate Webhook Secret"); - } - - frm.add_custom_button(label, () => { - frm.call("generate_webhook_secret", { - field: "donation_webhook_secret" - }).then(() => { - frm.refresh(); - }); - }, __("Donations")); - } -}); diff --git a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.json b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.json deleted file mode 100644 index 25ff0c1bb0..0000000000 --- a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.json +++ /dev/null @@ -1,273 +0,0 @@ -{ - "actions": [], - "creation": "2020-03-29 12:57:03.005120", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "enable_razorpay_for_memberships", - "razorpay_settings_section", - "billing_cycle", - "billing_frequency", - "membership_webhook_secret", - "column_break_6", - "allow_invoicing", - "automate_membership_invoicing", - "automate_membership_payment_entries", - "company", - "membership_debit_account", - "membership_payment_account", - "column_break_9", - "send_email", - "send_invoice", - "membership_print_format", - "inv_print_format", - "email_template", - "donation_settings_section", - "donation_company", - "default_donor_type", - "donation_webhook_secret", - "column_break_22", - "automate_donation_payment_entries", - "donation_debit_account", - "donation_payment_account", - "section_break_27", - "creation_user" - ], - "fields": [ - { - "fieldname": "billing_cycle", - "fieldtype": "Select", - "label": "Billing Cycle", - "options": "Monthly\nYearly" - }, - { - "depends_on": "eval:doc.enable_razorpay_for_memberships", - "fieldname": "razorpay_settings_section", - "fieldtype": "Section Break", - "label": "RazorPay Settings for Memberships" - }, - { - "description": "The number of billing cycles for which the customer should be charged. For example, if a customer is buying a 1-year membership that should be billed on a monthly basis, this value should be 12.", - "fieldname": "billing_frequency", - "fieldtype": "Int", - "label": "Billing Frequency" - }, - { - "fieldname": "column_break_6", - "fieldtype": "Section Break", - "label": "Membership Invoicing" - }, - { - "fieldname": "column_break_9", - "fieldtype": "Column Break" - }, - { - "description": "This company will be set for the Memberships created via webhook.", - "fieldname": "company", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Company", - "options": "Company", - "reqd": 1 - }, - { - "default": "0", - "depends_on": "eval:doc.allow_invoicing && doc.send_email", - "fieldname": "send_invoice", - "fieldtype": "Check", - "label": "Send Invoice with Email" - }, - { - "default": "0", - "fieldname": "send_email", - "fieldtype": "Check", - "label": "Send Membership Acknowledgement" - }, - { - "depends_on": "eval: doc.send_invoice", - "fieldname": "inv_print_format", - "fieldtype": "Link", - "label": "Invoice Print Format", - "mandatory_depends_on": "eval: doc.send_invoice", - "options": "Print Format" - }, - { - "depends_on": "eval:doc.send_email", - "fieldname": "membership_print_format", - "fieldtype": "Link", - "label": "Membership Print Format", - "options": "Print Format" - }, - { - "depends_on": "eval:doc.send_email", - "fieldname": "email_template", - "fieldtype": "Link", - "label": "Email Template", - "mandatory_depends_on": "eval:doc.send_email", - "options": "Email Template" - }, - { - "default": "0", - "fieldname": "allow_invoicing", - "fieldtype": "Check", - "label": "Allow Invoicing for Memberships", - "mandatory_depends_on": "eval:doc.send_invoice || doc.make_payment_entry" - }, - { - "default": "0", - "depends_on": "eval:doc.allow_invoicing", - "description": "Automatically create an invoice when payment is authorized from a web form entry", - "fieldname": "automate_membership_invoicing", - "fieldtype": "Check", - "label": "Automate Invoicing for Web Forms" - }, - { - "default": "0", - "depends_on": "eval:doc.allow_invoicing", - "description": "Auto creates Payment Entry for Sales Invoices created for Membership from web forms.", - "fieldname": "automate_membership_payment_entries", - "fieldtype": "Check", - "label": "Automate Payment Entry Creation" - }, - { - "default": "0", - "fieldname": "enable_razorpay_for_memberships", - "fieldtype": "Check", - "label": "Enable RazorPay For Memberships" - }, - { - "depends_on": "eval:doc.automate_membership_payment_entries", - "description": "Account for accepting membership payments", - "fieldname": "membership_payment_account", - "fieldtype": "Link", - "label": "Membership Payment To", - "mandatory_depends_on": "eval:doc.automate_membership_payment_entries", - "options": "Account" - }, - { - "fieldname": "membership_webhook_secret", - "fieldtype": "Password", - "label": "Membership Webhook Secret", - "read_only": 1 - }, - { - "fieldname": "donation_webhook_secret", - "fieldtype": "Password", - "label": "Donation Webhook Secret", - "read_only": 1 - }, - { - "depends_on": "automate_donation_payment_entries", - "description": "Account for accepting donation payments", - "fieldname": "donation_payment_account", - "fieldtype": "Link", - "label": "Donation Payment To", - "mandatory_depends_on": "automate_donation_payment_entries", - "options": "Account" - }, - { - "default": "0", - "description": "Auto creates Payment Entry for Donations created from web forms.", - "fieldname": "automate_donation_payment_entries", - "fieldtype": "Check", - "label": "Automate Donation Payment Entries" - }, - { - "depends_on": "eval:doc.allow_invoicing", - "fieldname": "membership_debit_account", - "fieldtype": "Link", - "label": "Debit Account", - "mandatory_depends_on": "eval:doc.allow_invoicing", - "options": "Account" - }, - { - "depends_on": "automate_donation_payment_entries", - "fieldname": "donation_debit_account", - "fieldtype": "Link", - "label": "Debit Account", - "mandatory_depends_on": "automate_donation_payment_entries", - "options": "Account" - }, - { - "description": "This company will be set for the Donations created via webhook.", - "fieldname": "donation_company", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Company", - "options": "Company", - "reqd": 1 - }, - { - "fieldname": "donation_settings_section", - "fieldtype": "Section Break", - "label": "Donation Settings" - }, - { - "fieldname": "column_break_22", - "fieldtype": "Column Break" - }, - { - "description": "This Donor Type will be set for the Donor created via Donation web form entry.", - "fieldname": "default_donor_type", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Default Donor Type", - "options": "Donor Type", - "reqd": 1 - }, - { - "fieldname": "section_break_27", - "fieldtype": "Section Break" - }, - { - "description": "The user that will be used to create Donations, Memberships, Invoices, and Payment Entries. This user should have the relevant permissions.", - "fieldname": "creation_user", - "fieldtype": "Link", - "label": "Creation User", - "options": "User", - "reqd": 1 - } - ], - "index_web_pages_for_search": 1, - "issingle": 1, - "links": [], - "modified": "2021-03-11 10:43:38.124240", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Non Profit Settings", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "Non Profit Manager", - "share": 1, - "write": 1 - }, - { - "email": 1, - "print": 1, - "read": 1, - "role": "Non Profit Member", - "share": 1 - } - ], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py deleted file mode 100644 index ace6605542..0000000000 --- a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -from frappe import _ -from frappe.integrations.utils import get_payment_gateway_controller -from frappe.model.document import Document - - -class NonProfitSettings(Document): - @frappe.whitelist() - def generate_webhook_secret(self, field="membership_webhook_secret"): - key = frappe.generate_hash(length=20) - self.set(field, key) - self.save() - - secret_for = "Membership" if field == "membership_webhook_secret" else "Donation" - - frappe.msgprint( - _("Here is your webhook secret for {0} API, this will be shown to you only once.").format(secret_for) + "

" + key, - _("Webhook Secret") - ) - - @frappe.whitelist() - def revoke_key(self, key): - self.set(key, None) - self.save() - - def get_webhook_secret(self, endpoint="Membership"): - fieldname = "membership_webhook_secret" if endpoint == "Membership" else "donation_webhook_secret" - return self.get_password(fieldname=fieldname, raise_exception=False) - -@frappe.whitelist() -def get_plans_for_membership(*args, **kwargs): - controller = get_payment_gateway_controller("Razorpay") - plans = controller.get_plans() - return [plan.get("item") for plan in plans.get("items")] diff --git a/erpnext/non_profit/doctype/non_profit_settings/test_non_profit_settings.py b/erpnext/non_profit/doctype/non_profit_settings/test_non_profit_settings.py deleted file mode 100644 index 51d1ba02eb..0000000000 --- a/erpnext/non_profit/doctype/non_profit_settings/test_non_profit_settings.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -# import frappe -import unittest - - -class TestNonProfitSettings(unittest.TestCase): - pass diff --git a/erpnext/non_profit/doctype/volunteer/__init__.py b/erpnext/non_profit/doctype/volunteer/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/doctype/volunteer/test_volunteer.py b/erpnext/non_profit/doctype/volunteer/test_volunteer.py deleted file mode 100644 index 0a0ab2cf34..0000000000 --- a/erpnext/non_profit/doctype/volunteer/test_volunteer.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - - -class TestVolunteer(unittest.TestCase): - pass diff --git a/erpnext/non_profit/doctype/volunteer/volunteer.js b/erpnext/non_profit/doctype/volunteer/volunteer.js deleted file mode 100644 index ac93d8c801..0000000000 --- a/erpnext/non_profit/doctype/volunteer/volunteer.js +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Volunteer', { - refresh: function(frm) { - - frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Volunteer'}; - - frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal); - - if(!frm.doc.__islocal) { - frappe.contacts.render_address_and_contact(frm); - } else { - frappe.contacts.clear_address_and_contact(frm); - } - } -}); diff --git a/erpnext/non_profit/doctype/volunteer/volunteer.json b/erpnext/non_profit/doctype/volunteer/volunteer.json deleted file mode 100644 index 08b7f87b2a..0000000000 --- a/erpnext/non_profit/doctype/volunteer/volunteer.json +++ /dev/null @@ -1,148 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "autoname": "field:email", - "creation": "2017-09-19 16:16:45.676019", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "volunteer_name", - "column_break_5", - "volunteer_type", - "email", - "image", - "address_contacts", - "address_html", - "column_break_9", - "contact_html", - "volunteer_availability_and_skills_details", - "availability", - "availability_timeslot", - "column_break_12", - "volunteer_skills", - "section_break_15", - "note" - ], - "fields": [ - { - "fieldname": "volunteer_name", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Volunteer Name", - "reqd": 1 - }, - { - "fieldname": "column_break_5", - "fieldtype": "Column Break" - }, - { - "fieldname": "volunteer_type", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Volunteer Type", - "options": "Volunteer Type", - "reqd": 1 - }, - { - "fieldname": "email", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Email", - "reqd": 1, - "unique": 1 - }, - { - "fieldname": "image", - "fieldtype": "Attach Image", - "hidden": 1, - "label": "Image", - "no_copy": 1, - "print_hide": 1 - }, - { - "depends_on": "eval:!doc.__islocal;", - "fieldname": "address_contacts", - "fieldtype": "Section Break", - "label": "Address and Contact", - "options": "fa fa-map-marker" - }, - { - "fieldname": "address_html", - "fieldtype": "HTML", - "label": "Address HTML" - }, - { - "fieldname": "column_break_9", - "fieldtype": "Column Break" - }, - { - "fieldname": "contact_html", - "fieldtype": "HTML", - "label": "Contact HTML" - }, - { - "fieldname": "volunteer_availability_and_skills_details", - "fieldtype": "Section Break", - "label": "Availability and Skills" - }, - { - "fieldname": "availability", - "fieldtype": "Select", - "label": "Availability", - "options": "\nWeekly\nWeekdays\nWeekends" - }, - { - "fieldname": "availability_timeslot", - "fieldtype": "Select", - "label": "Availability Timeslot", - "options": "\nMorning\nAfternoon\nEvening\nAnytime" - }, - { - "fieldname": "column_break_12", - "fieldtype": "Column Break" - }, - { - "fieldname": "volunteer_skills", - "fieldtype": "Table", - "label": "Volunteer Skills", - "options": "Volunteer Skill" - }, - { - "fieldname": "section_break_15", - "fieldtype": "Section Break" - }, - { - "fieldname": "note", - "fieldtype": "Long Text", - "label": "Note" - } - ], - "image_field": "image", - "links": [], - "modified": "2020-09-16 23:45:15.595952", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Volunteer", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Non Profit Manager", - "share": 1, - "write": 1 - } - ], - "quick_entry": 1, - "restrict_to_domain": "Non Profit", - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "volunteer_name", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/non_profit/doctype/volunteer/volunteer.py b/erpnext/non_profit/doctype/volunteer/volunteer.py deleted file mode 100644 index b44d67dae3..0000000000 --- a/erpnext/non_profit/doctype/volunteer/volunteer.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -from frappe.contacts.address_and_contact import load_address_and_contact -from frappe.model.document import Document - - -class Volunteer(Document): - def onload(self): - """Load address and contacts in `__onload`""" - load_address_and_contact(self) diff --git a/erpnext/non_profit/doctype/volunteer_skill/__init__.py b/erpnext/non_profit/doctype/volunteer_skill/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/doctype/volunteer_skill/volunteer_skill.json b/erpnext/non_profit/doctype/volunteer_skill/volunteer_skill.json deleted file mode 100644 index 7d210aa7bd..0000000000 --- a/erpnext/non_profit/doctype/volunteer_skill/volunteer_skill.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-09-20 15:26:26.453435", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "volunteer_skill", - "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": "Volunteer Skill", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "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": "2017-12-06 11:54:14.396354", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Volunteer Skill", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Non Profit", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/non_profit/doctype/volunteer_skill/volunteer_skill.py b/erpnext/non_profit/doctype/volunteer_skill/volunteer_skill.py deleted file mode 100644 index fe7251876d..0000000000 --- a/erpnext/non_profit/doctype/volunteer_skill/volunteer_skill.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -from frappe.model.document import Document - - -class VolunteerSkill(Document): - pass diff --git a/erpnext/non_profit/doctype/volunteer_type/__init__.py b/erpnext/non_profit/doctype/volunteer_type/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/doctype/volunteer_type/test_volunteer_type.py b/erpnext/non_profit/doctype/volunteer_type/test_volunteer_type.py deleted file mode 100644 index cef27c83a5..0000000000 --- a/erpnext/non_profit/doctype/volunteer_type/test_volunteer_type.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - - -class TestVolunteerType(unittest.TestCase): - pass diff --git a/erpnext/non_profit/doctype/volunteer_type/volunteer_type.js b/erpnext/non_profit/doctype/volunteer_type/volunteer_type.js deleted file mode 100644 index 5c17505be9..0000000000 --- a/erpnext/non_profit/doctype/volunteer_type/volunteer_type.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Volunteer Type', { - refresh: function() { - - } -}); diff --git a/erpnext/non_profit/doctype/volunteer_type/volunteer_type.json b/erpnext/non_profit/doctype/volunteer_type/volunteer_type.json deleted file mode 100644 index 256b25fe91..0000000000 --- a/erpnext/non_profit/doctype/volunteer_type/volunteer_type.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "prompt", - "beta": 0, - "creation": "2017-09-19 16:13:07.763273", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amount", - "fieldtype": "Float", - "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": "Amount", - "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, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2017-12-06 11:52:08.800425", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Volunteer Type", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Non Profit Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Non Profit", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/non_profit/doctype/volunteer_type/volunteer_type.py b/erpnext/non_profit/doctype/volunteer_type/volunteer_type.py deleted file mode 100644 index 3b1ae1a88e..0000000000 --- a/erpnext/non_profit/doctype/volunteer_type/volunteer_type.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -from frappe.model.document import Document - - -class VolunteerType(Document): - pass diff --git a/erpnext/non_profit/report/__init__.py b/erpnext/non_profit/report/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/report/expiring_memberships/__init__.py b/erpnext/non_profit/report/expiring_memberships/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/report/expiring_memberships/expiring_memberships.js b/erpnext/non_profit/report/expiring_memberships/expiring_memberships.js deleted file mode 100644 index be3a2438fc..0000000000 --- a/erpnext/non_profit/report/expiring_memberships/expiring_memberships.js +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt -/* eslint-disable */ - -frappe.query_reports["Expiring Memberships"] = { - "filters": [ - { - "fieldname": "fiscal_year", - "label": __("Fiscal Year"), - "fieldtype": "Link", - "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), - "reqd": 1 - }, - { - "fieldname":"month", - "label": __("Month"), - "fieldtype": "Select", - "options": "Jan\nFeb\nMar\nApr\nMay\nJun\nJul\nAug\nSep\nOct\nNov\nDec", - "default": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", - "Dec"][frappe.datetime.str_to_obj(frappe.datetime.get_today()).getMonth()], - } - ] -} diff --git a/erpnext/non_profit/report/expiring_memberships/expiring_memberships.json b/erpnext/non_profit/report/expiring_memberships/expiring_memberships.json deleted file mode 100644 index c311057201..0000000000 --- a/erpnext/non_profit/report/expiring_memberships/expiring_memberships.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2018-05-24 11:44:08.942809", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "letter_head": "ERPNext Foundation", - "modified": "2018-05-24 11:44:08.942809", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Expiring Memberships", - "owner": "Administrator", - "ref_doctype": "Membership", - "report_name": "Expiring Memberships", - "report_type": "Script Report", - "roles": [ - { - "role": "Non Profit Manager" - }, - { - "role": "Non Profit Member" - } - ] -} \ No newline at end of file diff --git a/erpnext/non_profit/report/expiring_memberships/expiring_memberships.py b/erpnext/non_profit/report/expiring_memberships/expiring_memberships.py deleted file mode 100644 index 3ddbfdc3b0..0000000000 --- a/erpnext/non_profit/report/expiring_memberships/expiring_memberships.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -from frappe import _ - - -def execute(filters=None): - columns = get_columns(filters) - data = get_data(filters) - return columns, data - -def get_columns(filters): - return [ - _("Membership Type") + ":Link/Membership Type:100", _("Membership ID") + ":Link/Membership:140", - _("Member ID") + ":Link/Member:140", _("Member Name") + ":Data:140", _("Email") + ":Data:140", - _("Expiring On") + ":Date:120" - ] - -def get_data(filters): - - filters["month"] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"].index(filters.month) + 1 - - return frappe.db.sql(""" - select ms.membership_type,ms.name,m.name,m.member_name,m.email,ms.max_membership_date - from `tabMember` m - inner join (select name,membership_type,max(to_date) as max_membership_date,member - from `tabMembership` - where paid = 1 - group by member - order by max_membership_date asc) ms - on m.name = ms.member - where month(max_membership_date) = %(month)s and year(max_membership_date) = %(year)s """,{'month': filters.get('month'),'year':filters.get('fiscal_year')}) diff --git a/erpnext/non_profit/utils.py b/erpnext/non_profit/utils.py deleted file mode 100644 index 47ea5f5783..0000000000 --- a/erpnext/non_profit/utils.py +++ /dev/null @@ -1,12 +0,0 @@ -import frappe - - -def get_company(): - company = frappe.defaults.get_defaults().company - if company: - return company - else: - company = frappe.get_list("Company", limit=1) - if company: - return company[0].name - return None diff --git a/erpnext/non_profit/web_form/__init__.py b/erpnext/non_profit/web_form/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/web_form/certification_application/__init__.py b/erpnext/non_profit/web_form/certification_application/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/web_form/certification_application/certification_application.js b/erpnext/non_profit/web_form/certification_application/certification_application.js deleted file mode 100644 index 8b455edafa..0000000000 --- a/erpnext/non_profit/web_form/certification_application/certification_application.js +++ /dev/null @@ -1,16 +0,0 @@ -frappe.ready(function() { - // bind events here - $(".page-header-actions-block .btn-primary, .page-header-actions-block .btn-default").addClass('hidden'); - $(".text-right .btn-primary").addClass('hidden'); - - if (frappe.utils.get_url_arg('name')) { - $('.page-content .btn-form-submit').addClass('hidden'); - } else { - user_name = frappe.full_name - user_email_id = frappe.session.user - $('[data-fieldname="currency"]').val("INR"); - $('[data-fieldname="name_of_applicant"]').val(user_name); - $('[data-fieldname="email"]').val(user_email_id); - $('[data-fieldname="amount"]').val(20000); - } -}) diff --git a/erpnext/non_profit/web_form/certification_application/certification_application.json b/erpnext/non_profit/web_form/certification_application/certification_application.json deleted file mode 100644 index 5fda978fba..0000000000 --- a/erpnext/non_profit/web_form/certification_application/certification_application.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "accept_payment": 1, - "allow_comments": 0, - "allow_delete": 0, - "allow_edit": 0, - "allow_incomplete": 0, - "allow_multiple": 1, - "allow_print": 0, - "amount": 0.0, - "amount_based_on_field": 1, - "amount_field": "amount", - "creation": "2018-06-08 16:24:05.805225", - "doc_type": "Certification Application", - "docstatus": 0, - "doctype": "Web Form", - "idx": 0, - "introduction_text": "", - "is_standard": 1, - "login_required": 1, - "max_attachment_size": 0, - "modified": "2018-06-11 16:11:14.544987", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "certification-application", - "owner": "Administrator", - "payment_button_help": "Pay for your certification using RazorPay", - "payment_button_label": "Pay Now", - "payment_gateway": "Razorpay", - "published": 1, - "route": "certification-application", - "show_sidebar": 1, - "sidebar_items": [], - "success_url": "/certification-application", - "title": "Certification Application", - "web_form_fields": [ - { - "fieldname": "name_of_applicant", - "fieldtype": "Data", - "hidden": 0, - "label": "Name of Applicant", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 - }, - { - "fieldname": "email", - "fieldtype": "Link", - "hidden": 0, - "label": "Email", - "max_length": 0, - "max_value": 0, - "options": "User", - "read_only": 1, - "reqd": 1 - }, - { - "fieldname": "currency", - "fieldtype": "Select", - "hidden": 0, - "label": "Currency", - "max_length": 0, - "max_value": 0, - "options": "USD\nINR", - "read_only": 1, - "reqd": 0 - }, - { - "fieldname": "amount", - "fieldtype": "Float", - "hidden": 0, - "label": "Amount", - "max_length": 0, - "max_value": 0, - "read_only": 1, - "reqd": 0 - } - ] -} \ No newline at end of file diff --git a/erpnext/non_profit/web_form/certification_application/certification_application.py b/erpnext/non_profit/web_form/certification_application/certification_application.py deleted file mode 100644 index 02e3e93333..0000000000 --- a/erpnext/non_profit/web_form/certification_application/certification_application.py +++ /dev/null @@ -1,3 +0,0 @@ -def get_context(context): - # do your magic here - pass diff --git a/erpnext/non_profit/web_form/certification_application_usd/__init__.py b/erpnext/non_profit/web_form/certification_application_usd/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.js b/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.js deleted file mode 100644 index 005d1dd6c1..0000000000 --- a/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.js +++ /dev/null @@ -1,16 +0,0 @@ -frappe.ready(function() { - // bind events here - $(".page-header-actions-block .btn-primary, .page-header-actions-block .btn-default").addClass('hidden'); - $(".text-right .btn-primary").addClass('hidden'); - - if (frappe.utils.get_url_arg('name')) { - $('.page-content .btn-form-submit').addClass('hidden'); - } else { - user_name = frappe.full_name - user_email_id = frappe.session.user - $('[data-fieldname="currency"]').val("USD"); - $('[data-fieldname="name_of_applicant"]').val(user_name); - $('[data-fieldname="email"]').val(user_email_id); - $('[data-fieldname="amount"]').val(300); - } -}) diff --git a/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.json b/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.json deleted file mode 100644 index 266109f580..0000000000 --- a/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "accept_payment": 1, - "allow_comments": 0, - "allow_delete": 0, - "allow_edit": 0, - "allow_incomplete": 0, - "allow_multiple": 1, - "allow_print": 0, - "amount": 0.0, - "amount_based_on_field": 1, - "amount_field": "amount", - "creation": "2018-06-13 09:22:48.262441", - "currency": "USD", - "doc_type": "Certification Application", - "docstatus": 0, - "doctype": "Web Form", - "idx": 0, - "introduction_text": "", - "is_standard": 1, - "login_required": 1, - "max_attachment_size": 0, - "modified": "2018-06-13 09:26:35.502064", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "certification-application-usd", - "owner": "Administrator", - "payment_button_help": "Pay for your certification using PayPal", - "payment_button_label": "Pay Now", - "payment_gateway": "PayPal", - "published": 1, - "route": "certification-application-usd", - "show_sidebar": 1, - "sidebar_items": [], - "success_url": "/certification-application-usd", - "title": "Certification Application USD", - "web_form_fields": [ - { - "fieldname": "name_of_applicant", - "fieldtype": "Data", - "hidden": 0, - "label": "Name of Applicant", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 - }, - { - "fieldname": "email", - "fieldtype": "Link", - "hidden": 0, - "label": "Email", - "max_length": 0, - "max_value": 0, - "options": "User", - "read_only": 1, - "reqd": 1 - }, - { - "fieldname": "currency", - "fieldtype": "Select", - "hidden": 0, - "label": "Currency", - "max_length": 0, - "max_value": 0, - "options": "USD\nINR", - "read_only": 1, - "reqd": 0 - }, - { - "fieldname": "amount", - "fieldtype": "Float", - "hidden": 0, - "label": "Amount", - "max_length": 0, - "max_value": 0, - "read_only": 1, - "reqd": 0 - } - ] -} \ No newline at end of file diff --git a/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.py b/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.py deleted file mode 100644 index 02e3e93333..0000000000 --- a/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.py +++ /dev/null @@ -1,3 +0,0 @@ -def get_context(context): - # do your magic here - pass diff --git a/erpnext/non_profit/web_form/grant_application/__init__.py b/erpnext/non_profit/web_form/grant_application/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/web_form/grant_application/grant_application.js b/erpnext/non_profit/web_form/grant_application/grant_application.js deleted file mode 100644 index f09e540919..0000000000 --- a/erpnext/non_profit/web_form/grant_application/grant_application.js +++ /dev/null @@ -1,3 +0,0 @@ -frappe.ready(function() { - // bind events here -}); diff --git a/erpnext/non_profit/web_form/grant_application/grant_application.json b/erpnext/non_profit/web_form/grant_application/grant_application.json deleted file mode 100644 index 73c9445500..0000000000 --- a/erpnext/non_profit/web_form/grant_application/grant_application.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "accept_payment": 0, - "allow_comments": 0, - "allow_delete": 1, - "allow_edit": 1, - "allow_incomplete": 0, - "allow_multiple": 1, - "allow_print": 0, - "amount": 0.0, - "amount_based_on_field": 0, - "creation": "2017-10-30 15:57:10.825188", - "currency": "INR", - "doc_type": "Grant Application", - "docstatus": 0, - "doctype": "Web Form", - "idx": 0, - "introduction_text": "Share as many details as you can to get quick response from organization", - "is_standard": 1, - "login_required": 1, - "max_attachment_size": 0, - "modified": "2017-12-06 12:32:16.893289", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "grant-application", - "owner": "Administrator", - "payment_button_label": "Buy Now", - "published": 1, - "route": "my-grant", - "show_sidebar": 1, - "sidebar_items": [], - "success_url": "/grant-application", - "title": "Grant Application", - "web_form_fields": [ - { - "fieldname": "applicant_type", - "fieldtype": "Select", - "hidden": 0, - "label": "Applicant Type", - "max_length": 0, - "max_value": 0, - "options": "Individual\nOrganization", - "read_only": 0, - "reqd": 1 - }, - { - "fieldname": "applicant_name", - "fieldtype": "Data", - "hidden": 0, - "label": "Name", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 1 - }, - { - "fieldname": "email", - "fieldtype": "Data", - "hidden": 0, - "label": "Email Address", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 1 - }, - { - "description": "", - "fieldname": "grant_description", - "fieldtype": "Text", - "hidden": 0, - "label": "Please outline your current situation and why you are applying for a grant?", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 1 - }, - { - "fieldname": "amount", - "fieldtype": "Float", - "hidden": 0, - "label": "Requested Amount", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 - }, - { - "fieldname": "has_any_past_grant_record", - "fieldtype": "Check", - "hidden": 0, - "label": "Have you received any grant from us before?", - "max_length": 0, - "max_value": 0, - "options": "", - "read_only": 0, - "reqd": 0 - }, - { - "fieldname": "published", - "fieldtype": "Check", - "hidden": 0, - "label": "Show on Website", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 - } - ] -} \ No newline at end of file diff --git a/erpnext/non_profit/web_form/grant_application/grant_application.py b/erpnext/non_profit/web_form/grant_application/grant_application.py deleted file mode 100644 index 3dfb381f65..0000000000 --- a/erpnext/non_profit/web_form/grant_application/grant_application.py +++ /dev/null @@ -1,4 +0,0 @@ -def get_context(context): - context.no_cache = True - context.parents = [dict(label='View All ', - route='grant-application', title='View All')] diff --git a/erpnext/non_profit/workspace/non_profit/non_profit.json b/erpnext/non_profit/workspace/non_profit/non_profit.json deleted file mode 100644 index ba2f919d01..0000000000 --- a/erpnext/non_profit/workspace/non_profit/non_profit.json +++ /dev/null @@ -1,272 +0,0 @@ -{ - "charts": [], - "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Member\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Non Profit Settings\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Membership\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Chapter\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Chapter Member\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loan Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Grant Application\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Membership\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Volunteer\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Chapter\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Donation\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tax Exemption Certification (India)\", \"col\": 4}}]", - "creation": "2020-03-02 17:23:47.811421", - "docstatus": 0, - "doctype": "Workspace", - "for_user": "", - "hide_custom": 0, - "icon": "non-profit", - "idx": 0, - "label": "Non Profit", - "links": [ - { - "hidden": 0, - "is_query_report": 0, - "label": "Loan Management", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Loan Type", - "link_count": 0, - "link_to": "Loan Type", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Loan Application", - "link_count": 0, - "link_to": "Loan Application", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Loan", - "link_count": 0, - "link_to": "Loan", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Grant Application", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Grant Application", - "link_count": 0, - "link_to": "Grant Application", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Membership", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Member", - "link_count": 0, - "link_to": "Member", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Membership", - "link_count": 0, - "link_to": "Membership", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Membership Type", - "link_count": 0, - "link_to": "Membership Type", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Membership Settings", - "link_count": 0, - "link_to": "Non Profit Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Volunteer", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Volunteer", - "link_count": 0, - "link_to": "Volunteer", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Volunteer Type", - "link_count": 0, - "link_to": "Volunteer Type", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Chapter", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Chapter", - "link_count": 0, - "link_to": "Chapter", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Donation", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Donor", - "link_count": 0, - "link_to": "Donor", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Donor Type", - "link_count": 0, - "link_to": "Donor Type", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Donation", - "link_count": 0, - "link_to": "Donation", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Tax Exemption Certification (India)", - "link_count": 0, - "link_type": "DocType", - "onboard": 0, - "type": "Card Break" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Tax Exemption 80G Certificate", - "link_count": 0, - "link_to": "Tax Exemption 80G Certificate", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - } - ], - "modified": "2021-08-05 12:16:01.146207", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Non Profit", - "owner": "Administrator", - "parent_page": "", - "public": 1, - "restrict_to_domain": "Non Profit", - "roles": [], - "sequence_id": 18, - "shortcuts": [ - { - "label": "Member", - "link_to": "Member", - "type": "DocType" - }, - { - "label": "Non Profit Settings", - "link_to": "Non Profit Settings", - "type": "DocType" - }, - { - "label": "Membership", - "link_to": "Membership", - "type": "DocType" - }, - { - "label": "Chapter", - "link_to": "Chapter", - "type": "DocType" - }, - { - "label": "Chapter Member", - "link_to": "Chapter Member", - "type": "DocType" - } - ], - "title": "Non Profit" -} \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 268db40a8e..970a8f9b5d 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -323,3 +323,5 @@ execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings' erpnext.patches.v14_0.set_payroll_cost_centers erpnext.patches.v13_0.agriculture_deprecation_warning erpnext.patches.v14_0.delete_agriculture_doctypes +erpnext.patches.v13_0.non_profit_deprecation_warning +erpnext.patches.v14_0.delete_non_profit_doctypes diff --git a/erpnext/patches/v13_0/non_profit_deprecation_warning.py b/erpnext/patches/v13_0/non_profit_deprecation_warning.py new file mode 100644 index 0000000000..c170de5c1c --- /dev/null +++ b/erpnext/patches/v13_0/non_profit_deprecation_warning.py @@ -0,0 +1,10 @@ +import click + + +def execute(): + + click.secho( + "Non Profit Domain is moved to a separate app and will be removed from ERPNext in version-14.\n" + "When upgrading to ERPNext version-14, please install the app to continue using the Agriculture domain: https://github.com/frappe/non_profit", + fg="yellow", + ) diff --git a/erpnext/patches/v14_0/delete_non_profit_doctypes.py b/erpnext/patches/v14_0/delete_non_profit_doctypes.py new file mode 100644 index 0000000000..3b3dbe4576 --- /dev/null +++ b/erpnext/patches/v14_0/delete_non_profit_doctypes.py @@ -0,0 +1,19 @@ +import frappe + + +def execute(): + frappe.delete_doc("Module Def", "Non Profit", ignore_missing=True, force=True) + + frappe.delete_doc("Workspace", "Non Profit", ignore_missing=True, force=True) + + reports = frappe.get_all("Report", {"module": "Non Profit", "is_standard": "Yes"}, pluck='name') + for report in reports: + frappe.delete_doc("Report", report, ignore_missing=True, force=True) + + dashboards = frappe.get_all("Dashboard", {"module": "Non Profit", "is_standard": 1}, pluck='name') + for dashboard in dashboards: + frappe.delete_doc("Dashboard", dashboard, ignore_missing=True, force=True) + + doctypes = frappe.get_all("DocType", {"module": "Non Profit", "custom": 0}, pluck='name') + for doctype in doctypes: + frappe.delete_doc("DocType", doctype, ignore_missing=True) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index c0dcb70b92..fa4e454f33 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -679,14 +679,6 @@ def get_custom_fields(): 'insert_after': 'email_id' } ], - 'Donor': [ - { - 'fieldname': 'pan_number', - 'label': 'PAN Details', - 'fieldtype': 'Data', - 'insert_after': 'email' - } - ], 'Finance Book': [ { 'fieldname': 'for_income_tax', diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 336b51c0ab..906db561db 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -198,7 +198,6 @@ def install(country=None): {'doctype': "Party Type", "party_type": "Member", "account_type": "Receivable"}, {'doctype': "Party Type", "party_type": "Shareholder", "account_type": "Payable"}, {'doctype': "Party Type", "party_type": "Student", "account_type": "Receivable"}, - {'doctype': "Party Type", "party_type": "Donor", "account_type": "Receivable"}, {'doctype': "Opportunity Type", "name": _("Sales")}, {'doctype': "Opportunity Type", "name": _("Support")}, From e04b3aaf7a2cb0792436a7e5f868572cb35c39f4 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 18 Jan 2022 14:36:22 +0530 Subject: [PATCH 006/447] feat(Employee Advance): add 'Returned' and 'Partly Claimed and Returned' status --- .../employee_advance/employee_advance.json | 42 +++++++++++++++++-- .../employee_advance/employee_advance.py | 39 ++++++++++------- .../hr/doctype/expense_claim/expense_claim.js | 2 +- .../hr/doctype/expense_claim/expense_claim.py | 31 +++++++++----- 4 files changed, 85 insertions(+), 29 deletions(-) diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json index 04754530c3..b0501830cc 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.json +++ b/erpnext/hr/doctype/employee_advance/employee_advance.json @@ -2,7 +2,7 @@ "actions": [], "allow_import": 1, "autoname": "naming_series:", - "creation": "2017-10-09 14:26:29.612365", + "creation": "2022-01-17 18:36:51.450395", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -121,7 +121,7 @@ "fieldtype": "Select", "label": "Status", "no_copy": 1, - "options": "Draft\nPaid\nUnpaid\nClaimed\nCancelled", + "options": "Draft\nPaid\nUnpaid\nClaimed\nReturned\nPartly Claimed and Returned\nCancelled", "read_only": 1 }, { @@ -200,7 +200,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-09-11 18:38:38.617478", + "modified": "2022-01-17 19:33:52.345823", "modified_by": "Administrator", "module": "HR", "name": "Employee Advance", @@ -237,5 +237,41 @@ "search_fields": "employee,employee_name", "sort_field": "modified", "sort_order": "DESC", + "states": [ + { + "color": "Red", + "custom": 1, + "title": "Draft" + }, + { + "color": "Green", + "custom": 1, + "title": "Paid" + }, + { + "color": "Orange", + "custom": 1, + "title": "Unpaid" + }, + { + "color": "Blue", + "custom": 1, + "title": "Claimed" + }, + { + "color": "Gray", + "title": "Returned" + }, + { + "color": "Yellow", + "title": "Partly Claimed and Returned" + }, + { + "color": "Red", + "custom": 1, + "title": "Cancelled" + } + ], + "title_field": "employee_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index 7aac2b63ed..e17eb214a1 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -28,18 +28,31 @@ class EmployeeAdvance(Document): def on_cancel(self): self.ignore_linked_doctypes = ('GL Entry') - def set_status(self): + def set_status(self, update=False): + precision = self.precision("paid_amount") + total_amount = flt(flt(self.claimed_amount) + flt(self.return_amount), precision) + status = None + if self.docstatus == 0: - self.status = "Draft" - if self.docstatus == 1: - if self.claimed_amount and flt(self.claimed_amount) == flt(self.paid_amount): - self.status = "Claimed" - elif self.paid_amount and self.advance_amount == flt(self.paid_amount): - self.status = "Paid" + status = "Draft" + elif self.docstatus == 1: + if flt(self.claimed_amount) > 0 and flt(self.claimed_amount, precision) == flt(self.paid_amount, precision): + status = "Claimed" + elif flt(self.return_amount) > 0 and flt(self.return_amount, precision) == flt(self.paid_amount, precision): + status = "Returned" + elif flt(self.claimed_amount) > 0 and (flt(self.return_amount) > 0) and total_amount == flt(self.paid_amount, precision): + status = "Partly Claimed and Returned" + elif flt(self.paid_amount) > 0 and flt(self.advance_amount, precision) == flt(self.paid_amount, precision): + status = "Paid" else: - self.status = "Unpaid" + status = "Unpaid" elif self.docstatus == 2: - self.status = "Cancelled" + status = "Cancelled" + + if update: + self.db_set("status", status) + else: + self.status = status def set_total_advance_paid(self): gle = frappe.qb.DocType("GL Entry") @@ -85,9 +98,7 @@ class EmployeeAdvance(Document): self.db_set("paid_amount", paid_amount) self.db_set("return_amount", return_amount) - self.set_status() - frappe.db.set_value("Employee Advance", self.name , "status", self.status) - + self.set_status(update=True) def update_claimed_amount(self): claimed_amount = frappe.db.sql(""" @@ -103,8 +114,8 @@ class EmployeeAdvance(Document): frappe.db.set_value("Employee Advance", self.name, "claimed_amount", flt(claimed_amount)) self.reload() - self.set_status() - frappe.db.set_value("Employee Advance", self.name, "status", self.status) + self.set_status(update=True) + @frappe.whitelist() def get_pending_amount(employee, posting_date): diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index 047945787d..af80b63845 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -171,7 +171,7 @@ frappe.ui.form.on("Expense Claim", { ['docstatus', '=', 1], ['employee', '=', frm.doc.employee], ['paid_amount', '>', 0], - ['status', '!=', 'Claimed'] + ['status', 'not in', ['Claimed', 'Returned', 'Partly Claimed and Returned']] ] }; }); diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index 7e3898b7d5..2d2bb093ce 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -341,18 +341,27 @@ def get_expense_claim_account(expense_claim_type, company): @frappe.whitelist() def get_advances(employee, advance_id=None): - if not advance_id: - condition = 'docstatus=1 and employee={0} and paid_amount > 0 and paid_amount > claimed_amount + return_amount'.format(frappe.db.escape(employee)) - else: - condition = 'name={0}'.format(frappe.db.escape(advance_id)) + advance = frappe.qb.DocType("Employee Advance") - return frappe.db.sql(""" - select - name, posting_date, paid_amount, claimed_amount, advance_account - from - `tabEmployee Advance` - where {0} - """.format(condition), as_dict=1) + query = ( + frappe.qb.from_(advance) + .select( + advance.name, advance.posting_date, advance.paid_amount, + advance.claimed_amount, advance.advance_account + ) + ) + + if not advance_id: + query = query.where( + (advance.docstatus == 1) + & (advance.employee == employee) + & (advance.paid_amount > 0) + & (advance.status.notin(["Claimed", "Returned", "Partly Claimed and Returned"])) + ) + else: + query = query.where(advance.name == advance_id) + + return query.run(as_dict=True) @frappe.whitelist() From bf30932de0524c42ab3d9718e3d19a73e1cb2d39 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 18 Jan 2022 14:36:38 +0530 Subject: [PATCH 007/447] patch: Employee Advance return statuses --- erpnext/patches.txt | 3 ++- .../v14_0/update_employee_advance_status.py | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v14_0/update_employee_advance_status.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index fa62b7fc27..ed39c204f6 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -326,4 +326,5 @@ erpnext.patches.v13_0.agriculture_deprecation_warning erpnext.patches.v14_0.delete_agriculture_doctypes erpnext.patches.v13_0.update_exchange_rate_settings erpnext.patches.v14_0.rearrange_company_fields -erpnext.patches.v14_0.update_leave_notification_template \ No newline at end of file +erpnext.patches.v14_0.update_leave_notification_template +erpnext.patches.v14_0.update_employee_advance_status \ No newline at end of file diff --git a/erpnext/patches/v14_0/update_employee_advance_status.py b/erpnext/patches/v14_0/update_employee_advance_status.py new file mode 100644 index 0000000000..a20e35a9f6 --- /dev/null +++ b/erpnext/patches/v14_0/update_employee_advance_status.py @@ -0,0 +1,26 @@ +import frappe + + +def execute(): + frappe.reload_doc('hr', 'doctype', 'employee_advance') + + advance = frappe.qb.DocType('Employee Advance') + (frappe.qb + .update(advance) + .set(advance.status, 'Returned') + .where( + (advance.docstatus == 1) + & ((advance.return_amount) & (advance.paid_amount == advance.return_amount)) + & (advance.status == 'Paid') + ) + ).run() + + (frappe.qb + .update(advance) + .set(advance.status, 'Partly Claimed and Returned') + .where( + (advance.docstatus == 1) + & ((advance.claimed_amount & advance.return_amount) & (advance.paid_amount == (advance.return_amount + advance.claimed_amount))) + & (advance.status == 'Paid') + ) + ).run() \ No newline at end of file From 0843d4388569616803a562a6928d6f1204f0e733 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 18 Jan 2022 14:37:45 +0530 Subject: [PATCH 008/447] fix(Expense Claim): validate advances after setting totals --- erpnext/hr/doctype/expense_claim/expense_claim.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index 2d2bb093ce..5146a5be90 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -23,10 +23,10 @@ class ExpenseClaim(AccountsController): def validate(self): validate_active_employee(self.employee) - self.validate_advances() + set_employee_name(self) self.validate_sanctioned_amount() self.calculate_total_amount() - set_employee_name(self) + self.validate_advances() self.set_expense_account(validate=True) self.set_payable_account() self.set_cost_center() From 85be0d22d4300a40f46b7268f7c56d8e7facbd40 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 18 Jan 2022 14:38:16 +0530 Subject: [PATCH 009/447] fix: employee advance status update on return via additional salary --- erpnext/payroll/doctype/additional_salary/additional_salary.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py index bf8bd05fcc..d618568416 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -105,6 +105,8 @@ class AdditionalSalary(Document): return_amount += self.amount frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", return_amount) + advance = frappe.get_doc("Employee Advance", self.ref_docname) + advance.set_status(update=True) def update_employee_referral(self, cancel=False): if self.ref_doctype == "Employee Referral": From 17b1f5f256ff63d34b3c20b4792cd77ae75402e0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 18 Jan 2022 18:35:25 +0530 Subject: [PATCH 010/447] test: employee advance status --- .../employee_advance/employee_advance.py | 6 +- .../employee_advance/test_employee_advance.py | 97 ++++++++++++++++++- 2 files changed, 100 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index e17eb214a1..f63bb86129 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -233,7 +233,8 @@ def make_return_entry(employee, company, employee_advance_name, return_amount, 'reference_name': employee_advance_name, 'party_type': 'Employee', 'party': employee, - 'is_advance': 'Yes' + 'is_advance': 'Yes', + 'cost_center': erpnext.get_default_cost_center(company) }) bank_amount = flt(return_amount) if bank_cash_account.account_currency==currency \ @@ -244,7 +245,8 @@ def make_return_entry(employee, company, employee_advance_name, return_amount, "debit_in_account_currency": bank_amount, "account_currency": bank_cash_account.account_currency, "account_type": bank_cash_account.account_type, - "exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1 + "exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1, + "cost_center": erpnext.get_default_cost_center(company) }) return je.as_dict() diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py index 5f2e720eb4..5f3a66a04f 100644 --- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py @@ -4,7 +4,7 @@ import unittest import frappe -from frappe.utils import nowdate +from frappe.utils import flt, nowdate import erpnext from erpnext.hr.doctype.employee.test_employee import make_employee @@ -12,6 +12,12 @@ from erpnext.hr.doctype.employee_advance.employee_advance import ( EmployeeAdvanceOverPayment, create_return_through_additional_salary, make_bank_entry, + make_return_entry, +) +from erpnext.hr.doctype.expense_claim.expense_claim import get_advances +from erpnext.hr.doctype.expense_claim.test_expense_claim import ( + get_payable_account, + make_expense_claim, ) from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure @@ -52,9 +58,75 @@ class TestEmployeeAdvance(unittest.TestCase): self.assertEqual(advance.paid_amount, 0) self.assertEqual(advance.status, "Unpaid") + def test_claimed_and_returned_status(self): + # Claimed Status check, full amount claimed + payable_account = get_payable_account("_Test Company") + claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True) + + advance = make_employee_advance(claim.employee) + pe = make_payment_entry(advance) + pe.submit() + + claim = get_advances_for_claim(claim, advance.name) + claim.save() + claim.submit() + + advance.reload() + self.assertEqual(advance.claimed_amount, 1000) + self.assertEqual(advance.status, "Claimed") + + # cancel claim; status should be Paid + claim.cancel() + advance.reload() + self.assertEqual(advance.claimed_amount, 0) + self.assertEqual(advance.status, "Paid") + + # Partly Claimed and Returned status check + # 500 Claimed, 500 Returned + claim = make_expense_claim(payable_account, 500, 500, "_Test Company", "Travel Expenses - _TC", do_not_submit=True) + + advance = make_employee_advance(claim.employee) + pe = make_payment_entry(advance) + pe.submit() + + claim = get_advances_for_claim(claim, advance.name, amount=500) + claim.save() + claim.submit() + + advance.reload() + self.assertEqual(advance.claimed_amount, 500) + self.assertEqual(advance.status, "Paid") + + entry = make_return_entry( + employee=advance.employee, + company=advance.company, + employee_advance_name=advance.name, + return_amount=flt(advance.paid_amount - advance.claimed_amount), + advance_account=advance.advance_account, + mode_of_payment=advance.mode_of_payment, + currency=advance.currency, + exchange_rate=advance.exchange_rate + ) + + entry = frappe.get_doc(entry) + entry.insert() + entry.submit() + + advance.reload() + self.assertEqual(advance.return_amount, 500) + self.assertEqual(advance.status, "Partly Claimed and Returned") + + # Cancel return entry; status should change to Paid + entry.cancel() + advance.reload() + self.assertEqual(advance.return_amount, 0) + self.assertEqual(advance.status, "Paid") + def test_repay_unclaimed_amount_from_salary(self): employee_name = make_employee("_T@employe.advance") advance = make_employee_advance(employee_name, {"repay_unclaimed_amount_from_salary": 1}) + pe = make_payment_entry(advance) + pe.submit() args = {"type": "Deduction"} create_salary_component("Advance Salary - Deduction", **args) @@ -82,11 +154,13 @@ class TestEmployeeAdvance(unittest.TestCase): advance.reload() self.assertEqual(advance.return_amount, 1000) + self.assertEqual(advance.status, "Returned") # update advance return amount on additional salary cancellation additional_salary.cancel() advance.reload() self.assertEqual(advance.return_amount, 700) + self.assertEqual(advance.status, "Paid") def tearDown(self): frappe.db.rollback() @@ -118,3 +192,24 @@ def make_employee_advance(employee_name, args=None): doc.submit() return doc + + +def get_advances_for_claim(claim, advance_name, amount=None): + advances = get_advances(claim.employee, advance_name) + + for entry in advances: + if amount: + allocated_amount = amount + else: + allocated_amount = flt(entry.paid_amount) - flt(entry.claimed_amount) + + claim.append("advances", { + "employee_advance": entry.name, + "posting_date": entry.posting_date, + "advance_account": entry.advance_account, + "advance_paid": entry.paid_amount, + "unclaimed_amount": allocated_amount, + "allocated_amount": allocated_amount + }) + + return claim \ No newline at end of file From f423de530a4140bfb84e93dbfa0f1140dcaa8e9b Mon Sep 17 00:00:00 2001 From: ChillarAnand Date: Thu, 20 Jan 2022 11:02:30 +0530 Subject: [PATCH 011/447] refactor: Code cleanup --- .../doctype/membership/membership.py | 415 ------------------ erpnext/regional/india/setup.py | 8 - .../operations/install_fixtures.py | 1 - 3 files changed, 424 deletions(-) delete mode 100644 erpnext/non_profit/doctype/membership/membership.py diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py deleted file mode 100644 index f9b295a223..0000000000 --- a/erpnext/non_profit/doctype/membership/membership.py +++ /dev/null @@ -1,415 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import json -from datetime import datetime - -import frappe -from frappe import _ -from frappe.email import sendmail_to_system_managers -from frappe.model.document import Document -from frappe.utils import add_days, add_months, add_years, get_link_to_form, getdate, nowdate - -import erpnext -from erpnext.non_profit.doctype.member.member import create_member - - -class Membership(Document): - def validate(self): - if not self.member or not frappe.db.exists("Member", self.member): - # for web forms - user_type = frappe.db.get_value("User", frappe.session.user, "user_type") - if user_type == "Website User": - self.create_member_from_website_user() - else: - frappe.throw(_("Please select a Member")) - - self.validate_membership_period() - - 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) - - # if person applied for offline membership - if last_membership and last_membership.name != self.name 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")) - - self.from_date = add_days(last_membership.to_date, 1) - elif frappe.session.user == "Administrator": - self.from_date = self.from_date - else: - self.from_date = nowdate() - - if frappe.db.get_single_value("Non Profit Settings", "billing_cycle") == "Yearly": - self.to_date = add_years(self.from_date, 1) - else: - self.to_date = add_months(self.from_date, 1) - - def on_payment_authorized(self, status_changed_to=None): - if status_changed_to not in ("Completed", "Authorized"): - return - self.load_from_db() - self.db_set("paid", 1) - settings = frappe.get_doc("Non Profit Settings") - if settings.allow_invoicing and settings.automate_membership_invoicing: - self.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True) - - - @frappe.whitelist() - 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")) - - if self.invoice: - frappe.throw(_("An invoice is already linked to this document")) - - member = frappe.get_doc("Member", self.member) - if not member.customer: - frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member))) - - plan = frappe.get_doc("Membership Type", self.membership_type) - settings = frappe.get_doc("Non Profit Settings") - self.validate_membership_type_and_settings(plan, settings) - - invoice = make_invoice(self, member, plan, settings) - self.reload() - self.invoice = invoice.name - - if with_payment_entry: - self.make_payment_entry(settings, invoice) - - if save: - self.save() - - return invoice - - def validate_membership_type_and_settings(self, plan, settings): - settings_link = get_link_to_form("Membership Type", self.membership_type) - - if not settings.membership_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.membership_payment_account: - frappe.throw(_("You need to set Payment Account for Membership in {0}").format( - get_link_to_form("Non Profit Settings", "Non Profit 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.membership_payment_account - pe.reference_no = self.name - pe.reference_date = getdate() - pe.flags.ignore_mandatory = True - pe.save() - pe.submit() - - @frappe.whitelist() - def send_acknowlement(self): - settings = frappe.get_doc("Non Profit Settings") - if not settings.send_email: - frappe.throw(_("You need to enable Send Acknowledge Email in {0}").format( - get_link_to_form("Non Profit Settings", "Non Profit 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 - attachments = [frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format)] - - if self.invoice and settings.send_invoice: - attachments.append(frappe.attach_print("Sales Invoice", self.invoice, print_format=settings.inv_print_format)) - - email_template = frappe.get_doc("Email Template", settings.email_template) - context = { "doc": self, "member": member} - - email_args = { - "recipients": [email], - "message": frappe.render_template(email_template.get("response"), context), - "subject": frappe.render_template(email_template.get("subject"), context), - "attachments": attachments, - "reference_doctype": self.doctype, - "reference_name": self.name - } - - if not frappe.flags.in_test: - 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): - 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.membership_debit_account, - "currency": membership.currency, - "company": settings.company, - "is_pos": 0, - "items": [ - { - "item_code": plan.linked_item, - "rate": membership.amount, - "qty": 1 - } - ] - }) - invoice.set_missing_values() - invoice.insert() - invoice.submit() - - frappe.msgprint(_("Sales Invoice created successfully")) - - return invoice - - -def get_member_based_on_subscription(subscription_id, email=None, customer_id=None): - filters = {"subscription_id": subscription_id} - if email: - filters.update({"email_id": email}) - if customer_id: - filters.update({"customer_id": customer_id}) - - members = frappe.get_all("Member", filters=filters, order_by="creation desc") - - try: - return frappe.get_doc("Member", members[0]["name"]) - except Exception: - return None - - -def verify_signature(data, endpoint="Membership"): - signature = frappe.request.headers.get("X-Razorpay-Signature") - - settings = frappe.get_doc("Non Profit Settings") - key = settings.get_webhook_secret(endpoint) - - controller = frappe.get_doc("Razorpay Settings") - - controller.verify_signature(data, signature, key) - frappe.set_user(settings.creation_user) - - -@frappe.whitelist(allow_guest=True) -def trigger_razorpay_subscription(*args, **kwargs): - data = frappe.request.get_data(as_text=True) - data = process_request_data(data) - - subscription = data.payload.get("subscription", {}).get("entity", {}) - subscription = frappe._dict(subscription) - - payment = data.payload.get("payment", {}).get("entity", {}) - payment = frappe._dict(payment) - - try: - if not data.event == "subscription.charged": - return - - 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) - })) - - member.subscription_id = subscription.id - member.customer_id = payment.customer_id - - if subscription.get("notes"): - member = get_additional_notes(member, subscription) - - company = get_company_for_memberships() - # Update Membership - membership = frappe.new_doc("Membership") - membership.update({ - "company": company, - "member": member.name, - "membership_status": "Current", - "membership_type": member.membership_type, - "currency": "INR", - "paid": 1, - "payment_id": payment.id, - "from_date": datetime.fromtimestamp(subscription.current_start), - "to_date": datetime.fromtimestamp(subscription.current_end), - "amount": payment.amount / 100 # Convert to rupees from paise - }) - membership.flags.ignore_mandatory = True - membership.insert() - - # Update membership values - member.subscription_start = datetime.fromtimestamp(subscription.start_at) - member.subscription_end = datetime.fromtimestamp(subscription.end_at) - member.subscription_status = "Active" - member.flags.ignore_mandatory = True - member.save() - - settings = frappe.get_doc("Non Profit Settings") - if settings.allow_invoicing and settings.automate_membership_invoicing: - membership.reload() - membership.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True) - - except Exception as e: - 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": "Success"} - - -@frappe.whitelist(allow_guest=True) -def update_halted_razorpay_subscription(*args, **kwargs): - """ - When all retries have been exhausted, Razorpay moves the subscription to the halted state. - The customer has to manually retry the charge or change the card linked to the subscription, - for the subscription to move back to the active state. - """ - if frappe.request: - data = frappe.request.get_data(as_text=True) - data = process_request_data(data) - elif frappe.flags.in_test: - data = kwargs.get("data") - data = frappe._dict(data) - else: - return - - if not data.event == "subscription.halted": - return - - subscription = data.payload.get("subscription", {}).get("entity", {}) - subscription = frappe._dict(subscription) - - try: - member = get_member_based_on_subscription(subscription.id, customer_id=subscription.customer_id) - if not member: - frappe.throw(_("Member with Razorpay Subscription ID {0} not found").format(subscription.id)) - - member.subscription_status = "Halted" - member.flags.ignore_mandatory = True - member.save() - - if subscription.get("notes"): - member = get_additional_notes(member, subscription) - - except Exception as e: - message = "{0}\n\n{1}".format(e, frappe.get_traceback()) - log = frappe.log_error(message, _("Error updating halted status for member {0}").format(member.name)) - notify_failure(log) - return {"status": "Failed", "reason": e} - - return {"status": "Success"} - - -def process_request_data(data): - try: - verify_signature(data) - except Exception as e: - log = frappe.log_error(e, "Membership Webhook Verification Error") - notify_failure(log) - return {"status": "Failed", "reason": e} - - if isinstance(data, str): - data = json.loads(data) - data = frappe._dict(data) - - return data - - -def get_company_for_memberships(): - company = frappe.db.get_single_value("Non Profit Settings", "company") - if not company: - from erpnext.non_profit.utils import get_company - company = get_company() - return company - - -def get_additional_notes(member, subscription): - if type(subscription.notes) == dict: - for k, v in subscription.notes.items(): - notes = "\n".join("{}: {}".format(k, v)) - - # extract member name from notes - if "name" in k.lower(): - member.update({ - "member_name": subscription.notes.get(k) - }) - - # extract pan number from notes - if "pan" in k.lower(): - member.update({ - "pan_number": subscription.notes.get(k) - }) - - member.add_comment("Comment", notes) - - elif type(subscription.notes) == str: - member.add_comment("Comment", subscription.notes) - - return member - - -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 - 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 Exception: - 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") - - try: - return plan[0]["name"] - except Exception: - return None - - -def set_expired_status(): - frappe.db.sql(""" - UPDATE - `tabMembership` SET `membership_status` = 'Expired' - WHERE - `membership_status` not in ('Cancelled') AND `to_date` < %s - """, (nowdate())) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index fa4e454f33..ecd9294342 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -671,14 +671,6 @@ def get_custom_fields(): 'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)' } ], - 'Member': [ - { - 'fieldname': 'pan_number', - 'label': 'PAN Details', - 'fieldtype': 'Data', - 'insert_after': 'email_id' - } - ], 'Finance Book': [ { 'fieldname': 'for_income_tax', diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 906db561db..bf3525998c 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -195,7 +195,6 @@ def install(country=None): {'doctype': "Party Type", "party_type": "Customer", "account_type": "Receivable"}, {'doctype': "Party Type", "party_type": "Supplier", "account_type": "Payable"}, {'doctype': "Party Type", "party_type": "Employee", "account_type": "Payable"}, - {'doctype': "Party Type", "party_type": "Member", "account_type": "Receivable"}, {'doctype': "Party Type", "party_type": "Shareholder", "account_type": "Payable"}, {'doctype': "Party Type", "party_type": "Student", "account_type": "Receivable"}, From 4bdf6248aae8140bd3c46846044d9dfe97fcacc6 Mon Sep 17 00:00:00 2001 From: ChillarAnand Date: Mon, 24 Jan 2022 11:06:11 +0530 Subject: [PATCH 012/447] refactor: Remove regional setup for non-profit --- .../tax_exemption_80g_certificate/__init__.py | 0 .../tax_exemption_80g_certificate.js | 67 ---- .../tax_exemption_80g_certificate.json | 297 ------------------ .../tax_exemption_80g_certificate.py | 104 ------ .../test_tax_exemption_80g_certificate.py | 106 ------- .../__init__.py | 0 .../tax_exemption_80g_certificate_detail.json | 66 ---- .../tax_exemption_80g_certificate_detail.py | 10 - 8 files changed, 650 deletions(-) delete mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate/__init__.py delete mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js delete mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json delete mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py delete mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py delete mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate_detail/__init__.py delete mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json delete mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.py diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/__init__.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js deleted file mode 100644 index 54cde9c0cf..0000000000 --- a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Tax Exemption 80G Certificate', { - refresh: function(frm) { - if (frm.doc.donor) { - frm.set_query('donation', function() { - return { - filters: { - docstatus: 1, - donor: frm.doc.donor - } - }; - }); - } - }, - - recipient: function(frm) { - if (frm.doc.recipient === 'Donor') { - frm.set_value({ - 'member': '', - 'member_name': '', - 'member_email': '', - 'member_pan_number': '', - 'fiscal_year': '', - 'total': 0, - 'payments': [] - }); - } else { - frm.set_value({ - 'donor': '', - 'donor_name': '', - 'donor_email': '', - 'donor_pan_number': '', - 'donation': '', - 'date_of_donation': '', - 'amount': 0, - 'mode_of_payment': '', - 'razorpay_payment_id': '' - }); - } - }, - - get_payments: function(frm) { - frm.call({ - doc: frm.doc, - method: 'get_payments', - freeze: true - }); - }, - - company: function(frm) { - if ((frm.doc.member || frm.doc.donor) && frm.doc.company) { - frm.call({ - doc: frm.doc, - method: 'set_company_address', - freeze: true - }); - } - }, - - donation: function(frm) { - if (frm.doc.recipient === 'Donor' && !frm.doc.donor) { - frappe.msgprint(__('Please select donor first')); - } - } -}); diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json deleted file mode 100644 index 9eee722f42..0000000000 --- a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json +++ /dev/null @@ -1,297 +0,0 @@ -{ - "actions": [], - "autoname": "naming_series:", - "creation": "2021-02-15 12:37:21.577042", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "naming_series", - "recipient", - "member", - "member_name", - "member_email", - "member_pan_number", - "donor", - "donor_name", - "donor_email", - "donor_pan_number", - "column_break_4", - "date", - "fiscal_year", - "section_break_11", - "company", - "company_address", - "company_address_display", - "column_break_14", - "company_pan_number", - "company_80g_number", - "company_80g_wef", - "title", - "section_break_6", - "get_payments", - "payments", - "total", - "donation_details_section", - "donation", - "date_of_donation", - "amount", - "column_break_27", - "mode_of_payment", - "razorpay_payment_id" - ], - "fields": [ - { - "fieldname": "recipient", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Certificate Recipient", - "options": "Member\nDonor", - "reqd": 1 - }, - { - "depends_on": "eval:doc.recipient === \"Member\";", - "fieldname": "member", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Member", - "mandatory_depends_on": "eval:doc.recipient === \"Member\";", - "options": "Member" - }, - { - "depends_on": "eval:doc.recipient === \"Member\";", - "fetch_from": "member.member_name", - "fieldname": "member_name", - "fieldtype": "Data", - "label": "Member Name", - "read_only": 1 - }, - { - "depends_on": "eval:doc.recipient === \"Donor\";", - "fieldname": "donor", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Donor", - "mandatory_depends_on": "eval:doc.recipient === \"Donor\";", - "options": "Donor" - }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" - }, - { - "fieldname": "date", - "fieldtype": "Date", - "label": "Date", - "reqd": 1 - }, - { - "depends_on": "eval:doc.recipient === \"Member\";", - "fieldname": "section_break_6", - "fieldtype": "Section Break" - }, - { - "fieldname": "payments", - "fieldtype": "Table", - "label": "Payments", - "options": "Tax Exemption 80G Certificate Detail" - }, - { - "fieldname": "total", - "fieldtype": "Currency", - "in_list_view": 1, - "label": "Total", - "read_only": 1 - }, - { - "depends_on": "eval:doc.recipient === \"Member\";", - "fieldname": "fiscal_year", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Fiscal Year", - "options": "Fiscal Year" - }, - { - "fieldname": "company", - "fieldtype": "Link", - "label": "Company", - "options": "Company", - "reqd": 1 - }, - { - "fieldname": "get_payments", - "fieldtype": "Button", - "label": "Get Memberships" - }, - { - "fieldname": "naming_series", - "fieldtype": "Select", - "label": "Naming Series", - "options": "NPO-80G-.YYYY.-" - }, - { - "fieldname": "section_break_11", - "fieldtype": "Section Break", - "label": "Company Details" - }, - { - "fieldname": "company_address", - "fieldtype": "Link", - "label": "Company Address", - "options": "Address" - }, - { - "fieldname": "column_break_14", - "fieldtype": "Column Break" - }, - { - "fetch_from": "company.pan_details", - "fieldname": "company_pan_number", - "fieldtype": "Data", - "label": "PAN Number", - "read_only": 1 - }, - { - "fieldname": "company_address_display", - "fieldtype": "Small Text", - "hidden": 1, - "label": "Company Address Display", - "print_hide": 1, - "read_only": 1 - }, - { - "fetch_from": "company.company_80g_number", - "fieldname": "company_80g_number", - "fieldtype": "Data", - "label": "80G Number", - "read_only": 1 - }, - { - "fetch_from": "company.with_effect_from", - "fieldname": "company_80g_wef", - "fieldtype": "Date", - "label": "80G With Effect From", - "read_only": 1 - }, - { - "depends_on": "eval:doc.recipient === \"Donor\";", - "fieldname": "donation_details_section", - "fieldtype": "Section Break", - "label": "Donation Details" - }, - { - "fieldname": "donation", - "fieldtype": "Link", - "label": "Donation", - "mandatory_depends_on": "eval:doc.recipient === \"Donor\";", - "options": "Donation" - }, - { - "fetch_from": "donation.amount", - "fieldname": "amount", - "fieldtype": "Currency", - "label": "Amount", - "read_only": 1 - }, - { - "fetch_from": "donation.mode_of_payment", - "fieldname": "mode_of_payment", - "fieldtype": "Link", - "label": "Mode of Payment", - "options": "Mode of Payment", - "read_only": 1 - }, - { - "fetch_from": "donation.razorpay_payment_id", - "fieldname": "razorpay_payment_id", - "fieldtype": "Data", - "label": "RazorPay Payment ID", - "read_only": 1 - }, - { - "fetch_from": "donation.date", - "fieldname": "date_of_donation", - "fieldtype": "Date", - "label": "Date of Donation", - "read_only": 1 - }, - { - "fieldname": "column_break_27", - "fieldtype": "Column Break" - }, - { - "depends_on": "eval:doc.recipient === \"Donor\";", - "fetch_from": "donor.donor_name", - "fieldname": "donor_name", - "fieldtype": "Data", - "label": "Donor Name", - "read_only": 1 - }, - { - "depends_on": "eval:doc.recipient === \"Donor\";", - "fetch_from": "donor.email", - "fieldname": "donor_email", - "fieldtype": "Data", - "label": "Email", - "read_only": 1 - }, - { - "depends_on": "eval:doc.recipient === \"Member\";", - "fetch_from": "member.email_id", - "fieldname": "member_email", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Email", - "read_only": 1 - }, - { - "depends_on": "eval:doc.recipient === \"Member\";", - "fetch_from": "member.pan_number", - "fieldname": "member_pan_number", - "fieldtype": "Data", - "label": "PAN Details", - "read_only": 1 - }, - { - "depends_on": "eval:doc.recipient === \"Donor\";", - "fetch_from": "donor.pan_number", - "fieldname": "donor_pan_number", - "fieldtype": "Data", - "label": "PAN Details", - "read_only": 1 - }, - { - "fieldname": "title", - "fieldtype": "Data", - "hidden": 1, - "label": "Title", - "print_hide": 1 - } - ], - "index_web_pages_for_search": 1, - "links": [], - "modified": "2021-02-22 00:03:34.215633", - "modified_by": "Administrator", - "module": "Regional", - "name": "Tax Exemption 80G Certificate", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "search_fields": "member, member_name", - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "title", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py deleted file mode 100644 index 0f0897841b..0000000000 --- a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -from frappe import _ -from frappe.contacts.doctype.address.address import get_company_address -from frappe.model.document import Document -from frappe.utils import flt, get_link_to_form, getdate - -from erpnext.accounts.utils import get_fiscal_year - - -class TaxExemption80GCertificate(Document): - def validate(self): - self.validate_date() - self.validate_duplicates() - self.validate_company_details() - self.set_company_address() - self.calculate_total() - self.set_title() - - def validate_date(self): - if self.recipient == 'Member': - if getdate(self.date): - fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True) - - if not (fiscal_year.year_start_date <= getdate(self.date) \ - <= fiscal_year.year_end_date): - frappe.throw(_('The Certificate Date is not in the Fiscal Year {0}').format(frappe.bold(self.fiscal_year))) - - def validate_duplicates(self): - if self.recipient == 'Donor': - certificate = frappe.db.exists(self.doctype, { - 'donation': self.donation, - 'name': ('!=', self.name) - }) - if certificate: - frappe.throw(_('An 80G Certificate {0} already exists for the donation {1}').format( - get_link_to_form(self.doctype, certificate), frappe.bold(self.donation) - ), title=_('Duplicate Certificate')) - - def validate_company_details(self): - fields = ['company_80g_number', 'with_effect_from', 'pan_details'] - company_details = frappe.db.get_value('Company', self.company, fields, as_dict=True) - if not company_details.company_80g_number: - frappe.throw(_('Please set the {0} for company {1}').format(frappe.bold('80G Number'), - get_link_to_form('Company', self.company))) - - if not company_details.pan_details: - frappe.throw(_('Please set the {0} for company {1}').format(frappe.bold('PAN Number'), - get_link_to_form('Company', self.company))) - - @frappe.whitelist() - def set_company_address(self): - address = get_company_address(self.company) - self.company_address = address.company_address - self.company_address_display = address.company_address_display - - def calculate_total(self): - if self.recipient == 'Donor': - return - - total = 0 - for entry in self.payments: - total += flt(entry.amount) - self.total = total - - def set_title(self): - if self.recipient == 'Member': - self.title = self.member_name - else: - self.title = self.donor_name - - @frappe.whitelist() - def get_payments(self): - if not self.member: - frappe.throw(_('Please select a Member first.')) - - fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True) - - memberships = frappe.db.get_all('Membership', { - 'member': self.member, - 'from_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)], - 'membership_status': ('!=', 'Cancelled') - }, ['from_date', 'amount', 'name', 'invoice', 'payment_id'], order_by='from_date') - - if not memberships: - frappe.msgprint(_('No Membership Payments found against the Member {0}').format(self.member)) - - total = 0 - self.payments = [] - - for doc in memberships: - self.append('payments', { - 'date': doc.from_date, - 'amount': doc.amount, - 'invoice_id': doc.invoice, - 'razorpay_payment_id': doc.payment_id, - 'membership': doc.name - }) - total += flt(doc.amount) - - self.total = total diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py deleted file mode 100644 index 6fa3b85d06..0000000000 --- a/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - -import frappe -from frappe.utils import getdate - -from erpnext.accounts.utils import get_fiscal_year -from erpnext.non_profit.doctype.donation.donation import create_donation -from erpnext.non_profit.doctype.donation.test_donation import ( - create_donor, - create_donor_type, - create_mode_of_payment, -) -from erpnext.non_profit.doctype.member.member import create_member -from erpnext.non_profit.doctype.membership.test_membership import make_membership, setup_membership - - -class TestTaxExemption80GCertificate(unittest.TestCase): - def setUp(self): - frappe.db.sql('delete from `tabTax Exemption 80G Certificate`') - frappe.db.sql('delete from `tabMembership`') - create_donor_type() - settings = frappe.get_doc('Non Profit Settings') - settings.company = '_Test Company' - settings.donation_company = '_Test Company' - settings.default_donor_type = '_Test Donor' - settings.creation_user = 'Administrator' - settings.save() - - company = frappe.get_doc('Company', '_Test Company') - company.pan_details = 'BBBTI3374C' - company.company_80g_number = 'NQ.CIT(E)I2018-19/DEL-IE28615-27062018/10087' - company.with_effect_from = getdate() - company.save() - - def test_duplicate_donation_certificate(self): - donor = create_donor() - create_mode_of_payment() - payment = frappe._dict({ - 'amount': 100, - 'method': 'Debit Card', - 'id': 'pay_MeXAmsgeKOhq7O' - }) - donation = create_donation(donor, payment) - - args = frappe._dict({ - 'recipient': 'Donor', - 'donor': donor.name, - 'donation': donation.name - }) - certificate = create_80g_certificate(args) - certificate.insert() - - # check company details - self.assertEqual(certificate.company_pan_number, 'BBBTI3374C') - self.assertEqual(certificate.company_80g_number, 'NQ.CIT(E)I2018-19/DEL-IE28615-27062018/10087') - - # check donation details - self.assertEqual(certificate.amount, donation.amount) - - duplicate_certificate = create_80g_certificate(args) - # duplicate validation - self.assertRaises(frappe.ValidationError, duplicate_certificate.insert) - - def test_membership_80g_certificate(self): - plan = setup_membership() - - # make test member - member_doc = create_member(frappe._dict({ - 'fullname': "_Test_Member", - 'email': "_test_member_erpnext@example.com", - 'plan_id': plan.name - })) - member_doc.make_customer_and_link() - member = member_doc.name - - membership = make_membership(member, { "from_date": getdate() }) - invoice = membership.generate_invoice(save=True) - - args = frappe._dict({ - 'recipient': 'Member', - 'member': member, - 'fiscal_year': get_fiscal_year(getdate(), as_dict=True).get('name') - }) - certificate = create_80g_certificate(args) - certificate.get_payments() - certificate.insert() - - self.assertEqual(len(certificate.payments), 1) - self.assertEqual(certificate.payments[0].amount, membership.amount) - self.assertEqual(certificate.payments[0].invoice_id, invoice.name) - - -def create_80g_certificate(args): - certificate = frappe.get_doc({ - 'doctype': 'Tax Exemption 80G Certificate', - 'recipient': args.recipient, - 'date': getdate(), - 'company': '_Test Company' - }) - - certificate.update(args) - - return certificate diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/__init__.py b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json deleted file mode 100644 index dfa817dd27..0000000000 --- a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "actions": [], - "creation": "2021-02-15 12:43:52.754124", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "date", - "amount", - "invoice_id", - "column_break_4", - "razorpay_payment_id", - "membership" - ], - "fields": [ - { - "fieldname": "date", - "fieldtype": "Date", - "in_list_view": 1, - "label": "Date", - "reqd": 1 - }, - { - "fieldname": "amount", - "fieldtype": "Currency", - "in_list_view": 1, - "label": "Amount", - "reqd": 1 - }, - { - "fieldname": "invoice_id", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Invoice ID", - "options": "Sales Invoice", - "reqd": 1 - }, - { - "fieldname": "razorpay_payment_id", - "fieldtype": "Data", - "label": "Razorpay Payment ID" - }, - { - "fieldname": "membership", - "fieldtype": "Link", - "label": "Membership", - "options": "Membership" - }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" - } - ], - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2021-02-15 16:35:10.777587", - "modified_by": "Administrator", - "module": "Regional", - "name": "Tax Exemption 80G Certificate Detail", - "owner": "Administrator", - "permissions": [], - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.py b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.py deleted file mode 100644 index bb7f07f688..0000000000 --- a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -# import frappe -from frappe.model.document import Document - - -class TaxExemption80GCertificateDetail(Document): - pass From 16ca81d6a61be4bf7ef2df9ae488c9a49076244c Mon Sep 17 00:00:00 2001 From: ChillarAnand Date: Thu, 27 Jan 2022 10:05:40 +0530 Subject: [PATCH 013/447] refactor: Remove non-profit code in ERPNext --- .../doctype/payment_entry/payment_entry.js | 16 +++----------- .../doctype/payment_entry/payment_entry.py | 22 +------------------ 2 files changed, 4 insertions(+), 34 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 3be3925b5a..a9bc0280bc 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -114,8 +114,6 @@ frappe.ui.form.on('Payment Entry', { var doctypes = ["Expense Claim", "Journal Entry"]; } else if (frm.doc.party_type == "Student") { var doctypes = ["Fees"]; - } else if (frm.doc.party_type == "Donor") { - var doctypes = ["Donation"]; } else { var doctypes = ["Journal Entry"]; } @@ -144,7 +142,7 @@ frappe.ui.form.on('Payment Entry', { const child = locals[cdt][cdn]; const filters = {"docstatus": 1, "company": doc.company}; const party_type_doctypes = ['Sales Invoice', 'Sales Order', 'Purchase Invoice', - 'Purchase Order', 'Expense Claim', 'Fees', 'Dunning', 'Donation']; + 'Purchase Order', 'Expense Claim', 'Fees', 'Dunning']; if (in_list(party_type_doctypes, child.reference_doctype)) { filters[doc.party_type.toLowerCase()] = doc.party; @@ -747,8 +745,7 @@ frappe.ui.form.on('Payment Entry', { (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") || - (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") || - (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Donor") + (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") ) { if(total_positive_outstanding > total_negative_outstanding) if (!frm.doc.paid_amount) @@ -791,8 +788,7 @@ frappe.ui.form.on('Payment Entry', { (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") || - (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") || - (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Donor") + (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") ) { if(total_positive_outstanding_including_order > paid_amount) { var remaining_outstanding = total_positive_outstanding_including_order - paid_amount; @@ -951,12 +947,6 @@ frappe.ui.form.on('Payment Entry', { frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Expense Claim or Journal Entry", [row.idx])); return false; } - - if (frm.doc.party_type == "Donor" && row.reference_doctype != "Donation") { - frappe.model.set_value(row.doctype, row.name, "reference_doctype", null); - frappe.msgprint(__("Row #{0}: Reference Document Type must be Donation", [row.idx])); - return false; - } } if (row) { diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 02a144d3e7..286052cf3a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -91,7 +91,6 @@ class PaymentEntry(AccountsController): self.update_expense_claim() self.update_outstanding_amounts() self.update_advance_paid() - self.update_donation() self.update_payment_schedule() self.set_status() @@ -101,7 +100,6 @@ class PaymentEntry(AccountsController): self.update_expense_claim() self.update_outstanding_amounts() self.update_advance_paid() - self.update_donation(cancel=1) self.delink_advance_entry_references() self.update_payment_schedule(cancel=1) self.set_payment_req_status() @@ -284,8 +282,6 @@ class PaymentEntry(AccountsController): valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance", "Gratuity") elif self.party_type == "Shareholder": valid_reference_doctypes = ("Journal Entry") - elif self.party_type == "Donor": - valid_reference_doctypes = ("Donation") for d in self.get("references"): if not d.allocated_amount: @@ -843,13 +839,6 @@ class PaymentEntry(AccountsController): else: update_reimbursed_amount(doc, d.allocated_amount) - def update_donation(self, cancel=0): - if self.payment_type == "Receive" and self.party_type == "Donor" and self.party: - for d in self.get("references"): - if d.reference_doctype=="Donation" and d.reference_name: - is_paid = 0 if cancel else 1 - frappe.db.set_value("Donation", d.reference_name, "paid", is_paid) - def on_recurring(self, reference_doc, auto_repeat_doc): self.reference_no = reference_doc.name self.reference_date = nowdate() @@ -1337,10 +1326,6 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre total_amount = ref_doc.get("grand_total") exchange_rate = 1 outstanding_amount = ref_doc.get("outstanding_amount") - elif reference_doctype == "Donation": - total_amount = ref_doc.get("amount") - outstanding_amount = total_amount - exchange_rate = 1 elif reference_doctype == "Dunning": total_amount = ref_doc.get("dunning_amount") exchange_rate = 1 @@ -1611,8 +1596,6 @@ def set_party_type(dt): party_type = "Employee" elif dt == "Fees": party_type = "Student" - elif dt == "Donation": - party_type = "Donor" return party_type def set_party_account(dt, dn, doc, party_type): @@ -1640,7 +1623,7 @@ def set_party_account_currency(dt, party_account, doc): return party_account_currency def set_payment_type(dt, doc): - if (dt in ("Sales Order", "Donation") or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \ + if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \ or (dt=="Purchase Invoice" and doc.outstanding_amount < 0): payment_type = "Receive" else: @@ -1673,9 +1656,6 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre elif dt == "Dunning": grand_total = doc.grand_total outstanding_amount = doc.grand_total - elif dt == "Donation": - grand_total = doc.amount - outstanding_amount = doc.amount elif dt == "Gratuity": grand_total = doc.amount outstanding_amount = flt(doc.amount) - flt(doc.paid_amount) From 538b64b1fa460327236ef8b260aec6c52adf21ff Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 31 Jan 2022 21:50:42 +0530 Subject: [PATCH 014/447] refactor: Employee Leave Balance Report - incorrect opening balance on boundary allocation dates - carry forwarded leaves accounted in leaves allocated column, should be part of opening balance - leaves allocated column also adds expired leave allocations causing negative balances, should only consider non-expiry records --- .../employee_leave_balance.py | 139 +++++++++++++----- 1 file changed, 103 insertions(+), 36 deletions(-) diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index b375b18b07..6280ef323b 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -6,8 +6,9 @@ from itertools import groupby import frappe from frappe import _ -from frappe.utils import add_days +from frappe.utils import add_days, date_diff, getdate +from erpnext.hr.doctype.leave_allocation.leave_allocation import get_previous_allocation from erpnext.hr.doctype.leave_application.leave_application import ( get_leave_balance_on, get_leaves_for_period, @@ -46,27 +47,27 @@ def get_columns(): 'label': _('Opening Balance'), 'fieldtype': 'float', 'fieldname': 'opening_balance', - 'width': 130, + 'width': 150, }, { - 'label': _('Leave Allocated'), + 'label': _('New Leave(s) Allocated'), 'fieldtype': 'float', 'fieldname': 'leaves_allocated', - 'width': 130, + 'width': 200, }, { - 'label': _('Leave Taken'), + 'label': _('Leave(s) Taken'), 'fieldtype': 'float', 'fieldname': 'leaves_taken', - 'width': 130, + 'width': 150, }, { - 'label': _('Leave Expired'), + 'label': _('Leave(s) Expired'), 'fieldtype': 'float', 'fieldname': 'leaves_expired', - 'width': 130, + 'width': 150, }, { 'label': _('Closing Balance'), 'fieldtype': 'float', 'fieldname': 'closing_balance', - 'width': 130, + 'width': 150, }] return columns @@ -108,10 +109,9 @@ def get_data(filters): leaves_taken = get_leaves_for_period(employee.name, leave_type, filters.from_date, filters.to_date) * -1 - new_allocation, expired_leaves = get_allocated_and_expired_leaves(filters.from_date, filters.to_date, employee.name, leave_type) - - - opening = get_leave_balance_on(employee.name, leave_type, add_days(filters.from_date, -1)) #allocation boundary condition + new_allocation, expired_leaves, carry_forwarded_leaves = get_allocated_and_expired_leaves( + filters.from_date, filters.to_date, employee.name, leave_type) + opening = get_opening_balance(employee.name, leave_type, filters, carry_forwarded_leaves) row.leaves_allocated = new_allocation row.leaves_expired = expired_leaves - leaves_taken if expired_leaves - leaves_taken > 0 else 0 @@ -125,6 +125,55 @@ def get_data(filters): return data + +def get_opening_balance(employee, leave_type, filters, carry_forwarded_leaves): + # allocation boundary condition + # opening balance is the closing leave balance 1 day before the filter start date + opening_balance_date = add_days(filters.from_date, -1) + allocation = get_previous_allocation(filters.from_date, leave_type, employee) + + if allocation and allocation.get("to_date") and opening_balance_date and \ + getdate(allocation.get("to_date")) == getdate(opening_balance_date): + # if opening balance date is same as the previous allocation's expiry + # then opening balance should only consider carry forwarded leaves + opening_balance = carry_forwarded_leaves + else: + # else directly get closing leave balance on the previous day + opening_balance = get_closing_balance_on(opening_balance_date, employee, leave_type, filters) + + return opening_balance + + +def get_closing_balance_on(date, employee, leave_type, filters): + closing_balance = get_leave_balance_on(employee, leave_type, date) + leave_allocation = get_leave_allocation_for_date(employee, leave_type, date) + if leave_allocation: + # if balance is greater than the days remaining for leave allocation's end date + # then balance should be = remaining days + remaining_days = date_diff(leave_allocation[0].to_date, filters.from_date) + 1 + if remaining_days < closing_balance: + closing_balance = remaining_days + + return closing_balance + + +def get_leave_allocation_for_date(employee, leave_type, date): + allocation = frappe.qb.DocType('Leave Allocation') + records = ( + frappe.qb.from_(allocation) + .select( + allocation.name, allocation.to_date + ).where( + (allocation.docstatus == 1) + & (allocation.employee == employee) + & (allocation.leave_type == leave_type) + & ((allocation.from_date <= date) & (allocation.to_date >= date)) + ) + ).run(as_dict=True) + + return records + + def get_conditions(filters): conditions={ 'status': 'Active', @@ -140,6 +189,7 @@ def get_conditions(filters): return conditions + def get_department_leave_approver_map(department=None): # get current department and all its child @@ -171,39 +221,55 @@ def get_department_leave_approver_map(department=None): return approvers + def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type): - - from frappe.utils import getdate - new_allocation = 0 expired_leaves = 0 + carry_forwarded_leaves = 0 - records= frappe.db.sql(""" - SELECT - employee, leave_type, from_date, to_date, leaves, transaction_name, - transaction_type, is_carry_forward, is_expired - FROM `tabLeave Ledger Entry` - WHERE employee=%(employee)s AND leave_type=%(leave_type)s - AND docstatus=1 - AND transaction_type = 'Leave Allocation' - AND (from_date between %(from_date)s AND %(to_date)s - OR to_date between %(from_date)s AND %(to_date)s - OR (from_date < %(from_date)s AND to_date > %(to_date)s)) - """, { - "from_date": from_date, - "to_date": to_date, - "employee": employee, - "leave_type": leave_type - }, as_dict=1) + records = get_leave_ledger_entries(from_date, to_date, employee, leave_type) for record in records: if record.to_date < getdate(to_date): expired_leaves += record.leaves - if record.from_date >= getdate(from_date): - new_allocation += record.leaves + # new allocation records with `is_expired=1` are created when leave expires + # these new records should not be considered, else it leads to negative leave balance + if record.is_expired: + continue + + if record.from_date >= getdate(from_date): + if record.is_carry_forward: + carry_forwarded_leaves += record.leaves + else: + new_allocation += record.leaves + + return new_allocation, expired_leaves, carry_forwarded_leaves + + +def get_leave_ledger_entries(from_date, to_date, employee, leave_type): + ledger = frappe.qb.DocType('Leave Ledger Entry') + records = ( + frappe.qb.from_(ledger) + .select( + ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date, + ledger.leaves, ledger.transaction_name, ledger.transaction_type, + ledger.is_carry_forward, ledger.is_expired + ).where( + (ledger.docstatus == 1) + & (ledger.transaction_type == 'Leave Allocation') + & (ledger.employee == employee) + & (ledger.leave_type == leave_type) + & ( + (ledger.from_date[from_date: to_date]) + | (ledger.to_date[from_date: to_date]) + | ((ledger.from_date < from_date) & (ledger.to_date > to_date)) + ) + ) + ).run(as_dict=True) + + return records - return new_allocation, expired_leaves def get_chart_data(data): labels = [] @@ -224,6 +290,7 @@ def get_chart_data(data): return chart + def get_dataset_for_chart(employee_data, datasets, labels): leaves = [] employee_data = sorted(employee_data, key=lambda k: k['employee_name']) From 899ca8b3849995ac9164e70c4d5c73d6ebdb106a Mon Sep 17 00:00:00 2001 From: ChillarAnand Date: Mon, 31 Jan 2022 17:43:01 +0530 Subject: [PATCH 015/447] refactor: Code cleanup --- .../workspace/non_profit/non_profit.json | 272 ------------------ .../v14_0/delete_non_profit_doctypes.py | 20 ++ .../80g_certificate_for_donation.json | 26 -- .../80g_certificate_for_donation/__init__.py | 0 .../80g_certificate_for_membership.json | 26 -- .../__init__.py | 0 6 files changed, 20 insertions(+), 324 deletions(-) delete mode 100644 erpnext/non_profit/workspace/non_profit/non_profit.json delete mode 100644 erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json delete mode 100644 erpnext/regional/print_format/80g_certificate_for_donation/__init__.py delete mode 100644 erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json delete mode 100644 erpnext/regional/print_format/80g_certificate_for_membership/__init__.py diff --git a/erpnext/non_profit/workspace/non_profit/non_profit.json b/erpnext/non_profit/workspace/non_profit/non_profit.json deleted file mode 100644 index fc90475fb3..0000000000 --- a/erpnext/non_profit/workspace/non_profit/non_profit.json +++ /dev/null @@ -1,272 +0,0 @@ -{ - "charts": [], - "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Member\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Non Profit Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Membership\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Chapter\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Chapter Member\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loan Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Grant Application\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Membership\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Volunteer\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Chapter\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Donation\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tax Exemption Certification (India)\",\"col\":4}}]", - "creation": "2020-03-02 17:23:47.811421", - "docstatus": 0, - "doctype": "Workspace", - "for_user": "", - "hide_custom": 0, - "icon": "non-profit", - "idx": 0, - "label": "Non Profit", - "links": [ - { - "hidden": 0, - "is_query_report": 0, - "label": "Loan Management", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Loan Type", - "link_count": 0, - "link_to": "Loan Type", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Loan Application", - "link_count": 0, - "link_to": "Loan Application", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Loan", - "link_count": 0, - "link_to": "Loan", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Grant Application", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Grant Application", - "link_count": 0, - "link_to": "Grant Application", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Membership", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Member", - "link_count": 0, - "link_to": "Member", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Membership", - "link_count": 0, - "link_to": "Membership", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Membership Type", - "link_count": 0, - "link_to": "Membership Type", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Membership Settings", - "link_count": 0, - "link_to": "Non Profit Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Volunteer", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Volunteer", - "link_count": 0, - "link_to": "Volunteer", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Volunteer Type", - "link_count": 0, - "link_to": "Volunteer Type", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Chapter", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Chapter", - "link_count": 0, - "link_to": "Chapter", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Donation", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Donor", - "link_count": 0, - "link_to": "Donor", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Donor Type", - "link_count": 0, - "link_to": "Donor Type", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Donation", - "link_count": 0, - "link_to": "Donation", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Tax Exemption Certification (India)", - "link_count": 0, - "link_type": "DocType", - "onboard": 0, - "type": "Card Break" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Tax Exemption 80G Certificate", - "link_count": 0, - "link_to": "Tax Exemption 80G Certificate", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - } - ], - "modified": "2022-01-13 17:40:50.220877", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Non Profit", - "owner": "Administrator", - "parent_page": "", - "public": 1, - "restrict_to_domain": "Non Profit", - "roles": [], - "sequence_id": 18.0, - "shortcuts": [ - { - "label": "Member", - "link_to": "Member", - "type": "DocType" - }, - { - "label": "Non Profit Settings", - "link_to": "Non Profit Settings", - "type": "DocType" - }, - { - "label": "Membership", - "link_to": "Membership", - "type": "DocType" - }, - { - "label": "Chapter", - "link_to": "Chapter", - "type": "DocType" - }, - { - "label": "Chapter Member", - "link_to": "Chapter Member", - "type": "DocType" - } - ], - "title": "Non Profit" -} \ No newline at end of file diff --git a/erpnext/patches/v14_0/delete_non_profit_doctypes.py b/erpnext/patches/v14_0/delete_non_profit_doctypes.py index 3b3dbe4576..86355a6426 100644 --- a/erpnext/patches/v14_0/delete_non_profit_doctypes.py +++ b/erpnext/patches/v14_0/delete_non_profit_doctypes.py @@ -6,6 +6,14 @@ def execute(): frappe.delete_doc("Workspace", "Non Profit", ignore_missing=True, force=True) + print_formats = frappe.get_all("Print Format", {"module": "Non Profit", "standard": "Yes"}, pluck='name') + for print_format in print_formats: + frappe.delete_doc("Print Format", print_format, ignore_missing=True, force=True) + + print_formats = ['80G Certificate for Membership', '80G Certificate for Donation'] + for print_format in print_formats: + frappe.delete_doc("Print Format", print_format, ignore_missing=True, force=True) + reports = frappe.get_all("Report", {"module": "Non Profit", "is_standard": "Yes"}, pluck='name') for report in reports: frappe.delete_doc("Report", report, ignore_missing=True, force=True) @@ -17,3 +25,15 @@ def execute(): doctypes = frappe.get_all("DocType", {"module": "Non Profit", "custom": 0}, pluck='name') for doctype in doctypes: frappe.delete_doc("DocType", doctype, ignore_missing=True) + + doctypes = ['Tax Exemption 80G Certificate', 'Tax Exemption 80G Certificate Detail'] + for doctype in doctypes: + frappe.delete_doc("DocType", doctype, ignore_missing=True) + + custom_fields = [ + {"dt": "Member", "fieldname": "pan_number"}, + {"dt": "Donor", "fieldname": "pan_number"}, + ] + for field in custom_fields: + custom_field = frappe.db.get_value("Custom Field", field) + frappe.delete_doc("Custom Field", custom_field, ignore_missing=True) diff --git a/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json b/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json deleted file mode 100644 index a8da0bd209..0000000000 --- a/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "absolute_value": 0, - "align_labels_right": 0, - "creation": "2021-02-22 00:17:33.878581", - "css": ".details {\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n line-height: 150%;\n}\n\n.certificate-footer {\n font-size: 15px;\n font-family: Tahoma, sans-serif;\n line-height: 140%;\n margin-top: 120px;\n}\n\n.company-address {\n color: #666666;\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n}", - "custom_format": 1, - "default_print_language": "en", - "disabled": 0, - "doc_type": "Tax Exemption 80G Certificate", - "docstatus": 0, - "doctype": "Print Format", - "font": "Default", - "html": "{% if letter_head and not no_letterhead -%}\n
{{ letter_head }}
\n{%- endif %}\n\n
\n

{{ doc.company }} 80G Donor Certificate

\n
\n

\n\n
\n

{{ _(\"Certificate No. : \") }} {{ doc.name }}

\n

\n \t{{ _(\"Date\") }} : {{ doc.get_formatted(\"date\") }}
\n

\n

\n \n
\n\n This is to confirm that the {{ doc.company }} received an amount of {{doc.get_formatted(\"amount\")}}\n from {{ doc.donor_name }}\n {% if doc.pan_number -%}\n bearing PAN Number {{ doc.member_pan_number }}\n {%- endif %}\n\n via the Mode of Payment {{doc.mode_of_payment}}\n\n {% if doc.razorpay_payment_id -%}\n bearing RazorPay Payment ID {{ doc.razorpay_payment_id }}\n {%- endif %}\n\n on {{ doc.get_formatted(\"date_of_donation\") }}\n

\n \n

\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n

\n\n
\n
\n\n

\n

{{doc.company_address_display }}

\n\n", - "idx": 0, - "line_breaks": 0, - "modified": "2021-02-22 00:20:08.516600", - "modified_by": "Administrator", - "module": "Regional", - "name": "80G Certificate for Donation", - "owner": "Administrator", - "print_format_builder": 0, - "print_format_type": "Jinja", - "raw_printing": 0, - "show_section_headings": 0, - "standard": "Yes" -} \ No newline at end of file diff --git a/erpnext/regional/print_format/80g_certificate_for_donation/__init__.py b/erpnext/regional/print_format/80g_certificate_for_donation/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json b/erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json deleted file mode 100644 index f1b15aab29..0000000000 --- a/erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "absolute_value": 0, - "align_labels_right": 0, - "creation": "2021-02-15 16:53:55.026611", - "css": ".details {\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n line-height: 150%;\n}\n\n.certificate-footer {\n font-size: 15px;\n font-family: Tahoma, sans-serif;\n line-height: 140%;\n margin-top: 120px;\n}\n\n.company-address {\n color: #666666;\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n}", - "custom_format": 1, - "default_print_language": "en", - "disabled": 0, - "doc_type": "Tax Exemption 80G Certificate", - "docstatus": 0, - "doctype": "Print Format", - "font": "Default", - "html": "{% if letter_head and not no_letterhead -%}\n
{{ letter_head }}
\n{%- endif %}\n\n
\n

{{ doc.company }} Members 80G Donor Certificate

\n

Financial Cycle {{ doc.fiscal_year }}

\n
\n

\n\n
\n

{{ _(\"Certificate No. : \") }} {{ doc.name }}

\n

\n \t{{ _(\"Date\") }} : {{ doc.get_formatted(\"date\") }}
\n

\n

\n \n
\n This is to confirm that the {{ doc.company }} received a total amount of {{doc.get_formatted(\"total\")}}\n from {{ doc.member_name }}\n {% if doc.pan_number -%}\n bearing PAN Number {{ doc.member_pan_number }}\n {%- endif %}\n as per the payment details given below:\n \n

\n \n \t\n \t\t\n \t\t\t\n \t\t\t\n \t\t\t\n \t\t\n \t\n \t\n \t\t{%- for payment in doc.payments -%}\n \t\t\n \t\t\t\n \t\t\t\n \t\t\t\n \t\t\n \t\t{%- endfor -%}\n \t\n
{{ _(\"Date\") }}{{ _(\"Amount\") }}{{ _(\"Invoice ID\") }}
{{ payment.date }} {{ payment.get_formatted(\"amount\") }}{{ payment.invoice_id }}
\n \n
\n \n

\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n

\n\n
\n
\n\n

\n

{{doc.company_address_display }}

\n\n", - "idx": 0, - "line_breaks": 0, - "modified": "2021-02-21 23:29:00.778973", - "modified_by": "Administrator", - "module": "Regional", - "name": "80G Certificate for Membership", - "owner": "Administrator", - "print_format_builder": 0, - "print_format_type": "Jinja", - "raw_printing": 0, - "show_section_headings": 0, - "standard": "Yes" -} \ No newline at end of file diff --git a/erpnext/regional/print_format/80g_certificate_for_membership/__init__.py b/erpnext/regional/print_format/80g_certificate_for_membership/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 From dddeef71799bdc9c0c787fc1fee9dd05e3c998e0 Mon Sep 17 00:00:00 2001 From: ChillarAnand Date: Thu, 3 Feb 2022 16:41:36 +0530 Subject: [PATCH 016/447] chore: Clean up whitespace --- erpnext/patches.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 09cfdcee82..a8134f20fc 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -336,7 +336,6 @@ erpnext.patches.v13_0.enable_provisional_accounting erpnext.patches.v13_0.non_profit_deprecation_warning erpnext.patches.v14_0.delete_non_profit_doctypes - [post_model_sync] erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template From 1ea749cf1a8f4f25cb5465ac607f242779e0aaac Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 3 Feb 2022 23:34:46 +0530 Subject: [PATCH 017/447] fix: expired leaves not calculated properly because of newly created expiry ledger entries --- .../report/employee_leave_balance/employee_leave_balance.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index 6280ef323b..5172fb8fc2 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -230,14 +230,14 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type): records = get_leave_ledger_entries(from_date, to_date, employee, leave_type) for record in records: - if record.to_date < getdate(to_date): - expired_leaves += record.leaves - # new allocation records with `is_expired=1` are created when leave expires # these new records should not be considered, else it leads to negative leave balance if record.is_expired: continue + if record.to_date < getdate(to_date): + expired_leaves += record.leaves + if record.from_date >= getdate(from_date): if record.is_carry_forward: carry_forwarded_leaves += record.leaves From 26b40e7cfd6649a08e430ce5ce9000f61cd09d89 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 4 Feb 2022 00:01:05 +0530 Subject: [PATCH 018/447] refactor: Leaves Taken calculation - fix issue where Leaves Taken also adds leaves expiring on boundary date as leaves taken, causing ambiguity - remove unnecessary `skip_expiry_leaves` function - `get_allocation_expiry` considering cancelled entries too --- .../doctype/leave_application/leave_application.py | 14 +++++--------- .../leave_ledger_entry/leave_ledger_entry.py | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 70250f5bcf..0cda5e267c 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -480,7 +480,8 @@ def get_allocation_expiry(employee, leave_type, to_date, from_date): 'leave_type': leave_type, 'is_carry_forward': 1, 'transaction_type': 'Leave Allocation', - 'to_date': ['between', (from_date, to_date)] + 'to_date': ['between', (from_date, to_date)], + 'docstatus': 1 },fields=['to_date']) return expiry[0]['to_date'] if expiry else None @@ -636,18 +637,18 @@ def get_remaining_leaves(allocation, leaves_taken, date, expiry): return _get_remaining_leaves(total_leaves, allocation.to_date) -def get_leaves_for_period(employee, leave_type, from_date, to_date, do_not_skip_expired_leaves=False): +def get_leaves_for_period(employee, leave_type, from_date, to_date, skip_expired_leaves=True): leave_entries = get_leave_entries(employee, leave_type, from_date, to_date) leave_days = 0 for leave_entry in leave_entries: inclusive_period = leave_entry.from_date >= getdate(from_date) and leave_entry.to_date <= getdate(to_date) - if inclusive_period and leave_entry.transaction_type == 'Leave Encashment': + if inclusive_period and leave_entry.transaction_type == 'Leave Encashment': leave_days += leave_entry.leaves elif inclusive_period and leave_entry.transaction_type == 'Leave Allocation' and leave_entry.is_expired \ - and (do_not_skip_expired_leaves or not skip_expiry_leaves(leave_entry, to_date)): + and not skip_expired_leaves: leave_days += leave_entry.leaves elif leave_entry.transaction_type == 'Leave Application': @@ -669,11 +670,6 @@ def get_leaves_for_period(employee, leave_type, from_date, to_date, do_not_skip_ return leave_days -def skip_expiry_leaves(leave_entry, date): - ''' Checks whether the expired leaves coincide with the to_date of leave balance check. - This allows backdated leave entry creation for non carry forwarded allocation ''' - end_date = frappe.db.get_value("Leave Allocation", {'name': leave_entry.transaction_name}, ['to_date']) - return True if end_date == date and not leave_entry.is_carry_forward else False def get_leave_entries(employee, leave_type, from_date, to_date): ''' Returns leave entries between from_date and to_date. ''' diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py index 5c5299ea7e..a5923e0021 100644 --- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py +++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py @@ -171,7 +171,7 @@ def expire_carried_forward_allocation(allocation): ''' Expires remaining leaves in the on carried forward allocation ''' from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period leaves_taken = get_leaves_for_period(allocation.employee, allocation.leave_type, - allocation.from_date, allocation.to_date, do_not_skip_expired_leaves=True) + allocation.from_date, allocation.to_date, skip_expired_leaves=False) leaves = flt(allocation.leaves) + flt(leaves_taken) # allow expired leaves entry to be created From 55ac8519bfecdb42f45ad255835070097544dfcb Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 4 Feb 2022 12:29:00 +0530 Subject: [PATCH 019/447] refactor: balance in Balance Summary report near allocation expiry date - Leave Balance shows minimum leaves remaining after comparing with remaining days for allocation expiry causing ambiguity - refactor remaining leaves calculation to return both, actual leave balance and balance for consumption - show actual balance in leave application, use balance for consumption only in validations --- .../leave_application/leave_application.py | 68 +++++++++++++------ 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 0cda5e267c..ca376dca61 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -29,11 +29,13 @@ from erpnext.hr.utils import ( validate_active_employee, ) +from typing import Dict class LeaveDayBlockedError(frappe.ValidationError): pass class OverlapError(frappe.ValidationError): pass class AttendanceAlreadyMarkedError(frappe.ValidationError): pass class NotAnOptionalHoliday(frappe.ValidationError): pass +class InsufficientLeaveBalanceError(frappe.ValidationError): pass from frappe.model.document import Document @@ -260,15 +262,18 @@ class LeaveApplication(Document): frappe.throw(_("The day(s) on which you are applying for leave are holidays. You need not apply for leave.")) if not is_lwp(self.leave_type): - self.leave_balance = get_leave_balance_on(self.employee, self.leave_type, self.from_date, self.to_date, - consider_all_leaves_in_the_allocation_period=True) - if self.status != "Rejected" and (self.leave_balance < self.total_leave_days or not self.leave_balance): + leave_balance = get_leave_balance_on(self.employee, self.leave_type, self.from_date, self.to_date, + consider_all_leaves_in_the_allocation_period=True, for_consumption=True) + self.leave_balance = leave_balance.get("leave_balance") + leave_balance_for_consumption = leave_balance.get("leave_balance_for_consumption") + + if self.status != "Rejected" and (leave_balance_for_consumption < self.total_leave_days or not leave_balance_for_consumption): if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): - frappe.msgprint(_("Note: There is not enough leave balance for Leave Type {0}") - .format(self.leave_type)) + frappe.msgprint(_("Insufficient leave balance for Leave Type {0}") + .format(frappe.bold(self.leave_type)), title=_("Warning"), indicator="orange") else: - frappe.throw(_("There is not enough leave balance for Leave Type {0}") - .format(self.leave_type)) + frappe.throw(_("Insufficient leave balance for Leave Type {0}") + .format(self.leave_type), InsufficientLeaveBalanceError, title=_("Insufficient Balance")) def validate_leave_overlap(self): if not self.name: @@ -425,7 +430,7 @@ class LeaveApplication(Document): if self.status != 'Approved' and submit: return - expiry_date = get_allocation_expiry(self.employee, self.leave_type, + expiry_date = get_allocation_expiry_for_cf_leaves(self.employee, self.leave_type, self.to_date, self.from_date) lwp = frappe.db.get_value("Leave Type", self.leave_type, "is_lwp") @@ -472,7 +477,7 @@ class LeaveApplication(Document): create_leave_ledger_entry(self, args, submit) -def get_allocation_expiry(employee, leave_type, to_date, from_date): +def get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, from_date): ''' Returns expiry of carry forward allocation in leave ledger entry ''' expiry = frappe.get_all("Leave Ledger Entry", filters={ @@ -544,7 +549,8 @@ def get_leave_details(employee, date): return ret @frappe.whitelist() -def get_leave_balance_on(employee, leave_type, date, to_date=None, consider_all_leaves_in_the_allocation_period=False): +def get_leave_balance_on(employee, leave_type, date, to_date=None, + consider_all_leaves_in_the_allocation_period=False, for_consumption=False): ''' Returns leave balance till date :param employee: employee name @@ -552,6 +558,11 @@ def get_leave_balance_on(employee, leave_type, date, to_date=None, consider_all_ :param date: date to check balance on :param to_date: future date to check for allocation expiry :param consider_all_leaves_in_the_allocation_period: consider all leaves taken till the allocation end date + :param for_consumption: flag to check if leave balance is required for consumption or display + eg: employee has leave balance = 10 but allocation is expiring in 1 day so employee can only consume 1 leave + in this case leave_balance = 10 but leave_balance_for_consumption = 1 + if True, returns a dict eg: {'leave_balance': 10, 'leave_balance_for_consumption': 1} + else, returns leave_balance (in this case 10) ''' if not to_date: @@ -561,11 +572,17 @@ def get_leave_balance_on(employee, leave_type, date, to_date=None, consider_all_ allocation = allocation_records.get(leave_type, frappe._dict()) end_date = allocation.to_date if consider_all_leaves_in_the_allocation_period else date - expiry = get_allocation_expiry(employee, leave_type, to_date, date) + cf_expiry = get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, date) leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date) - return get_remaining_leaves(allocation, leaves_taken, date, expiry) + remaining_leaves = get_remaining_leaves(allocation, leaves_taken, date, cf_expiry) + + if for_consumption: + return remaining_leaves + else: + return remaining_leaves.get('leave_balance') + def get_leave_allocation_records(employee, date, leave_type=None): ''' returns the total allocated leaves and carry forwarded leaves based on ledger entries ''' @@ -617,25 +634,34 @@ def get_pending_leaves_for_period(employee, leave_type, from_date, to_date): }, fields=['SUM(total_leave_days) as leaves'])[0] return leaves['leaves'] if leaves['leaves'] else 0.0 -def get_remaining_leaves(allocation, leaves_taken, date, expiry): - ''' Returns minimum leaves remaining after comparing with remaining days for allocation expiry ''' +def get_remaining_leaves(allocation, leaves_taken, date, cf_expiry) -> Dict[str, float]: + '''Returns a dict of leave_balance and leave_balance_for_consumption + leave_balance returns the available leave balance + leave_balance_for_consumption returns the minimum leaves remaining after comparing with remaining days for allocation expiry + ''' def _get_remaining_leaves(remaining_leaves, end_date): - + ''' Returns minimum leaves remaining after comparing with remaining days for allocation expiry ''' if remaining_leaves > 0: remaining_days = date_diff(end_date, date) + 1 remaining_leaves = min(remaining_days, remaining_leaves) return remaining_leaves - total_leaves = flt(allocation.total_leaves_allocated) + flt(leaves_taken) + leave_balance = leave_balance_for_consumption = flt(allocation.total_leaves_allocated) + flt(leaves_taken) - if expiry and allocation.unused_leaves: - remaining_leaves = flt(allocation.unused_leaves) + flt(leaves_taken) - remaining_leaves = _get_remaining_leaves(remaining_leaves, expiry) + # balance for carry forwarded leaves + if cf_expiry and allocation.unused_leaves: + cf_leaves = flt(allocation.unused_leaves) + flt(leaves_taken) + remaining_cf_leaves = _get_remaining_leaves(cf_leaves, cf_expiry) - total_leaves = flt(allocation.new_leaves_allocated) + flt(remaining_leaves) + leave_balance = flt(allocation.new_leaves_allocated) + flt(cf_leaves) + leave_balance_for_consumption = flt(allocation.new_leaves_allocated) + flt(remaining_cf_leaves) - return _get_remaining_leaves(total_leaves, allocation.to_date) + remaining_leaves = _get_remaining_leaves(leave_balance_for_consumption, allocation.to_date) + return { + 'leave_balance': leave_balance, + 'leave_balance_for_consumption': remaining_leaves + } def get_leaves_for_period(employee, leave_type, from_date, to_date, skip_expired_leaves=True): leave_entries = get_leave_entries(employee, leave_type, from_date, to_date) From b5c686ac4035491f5e2bf3a4709f54f94c04dd06 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 4 Feb 2022 12:39:58 +0530 Subject: [PATCH 020/447] fix: sort imports, sider issues --- erpnext/hr/doctype/leave_application/leave_application.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index ca376dca61..dbb3db36f4 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -1,6 +1,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +from typing import Dict import frappe from frappe import _ @@ -29,13 +30,13 @@ from erpnext.hr.utils import ( validate_active_employee, ) -from typing import Dict class LeaveDayBlockedError(frappe.ValidationError): pass class OverlapError(frappe.ValidationError): pass class AttendanceAlreadyMarkedError(frappe.ValidationError): pass class NotAnOptionalHoliday(frappe.ValidationError): pass -class InsufficientLeaveBalanceError(frappe.ValidationError): pass +class InsufficientLeaveBalanceError(frappe.ValidationError): + pass from frappe.model.document import Document From dbfa463738a7f91f9d5c21a700f604f3325cd923 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 4 Feb 2022 13:04:25 +0530 Subject: [PATCH 021/447] fix: show actual balance instead of consumption balance in opening balance - not changing opening balance based on remaining days --- .../employee_leave_balance.py | 66 +++++-------------- 1 file changed, 16 insertions(+), 50 deletions(-) diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index 5172fb8fc2..3f0337e508 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -138,42 +138,12 @@ def get_opening_balance(employee, leave_type, filters, carry_forwarded_leaves): # then opening balance should only consider carry forwarded leaves opening_balance = carry_forwarded_leaves else: - # else directly get closing leave balance on the previous day - opening_balance = get_closing_balance_on(opening_balance_date, employee, leave_type, filters) + # else directly get leave balance on the previous day + opening_balance = get_leave_balance_on(employee, leave_type, opening_balance_date) return opening_balance -def get_closing_balance_on(date, employee, leave_type, filters): - closing_balance = get_leave_balance_on(employee, leave_type, date) - leave_allocation = get_leave_allocation_for_date(employee, leave_type, date) - if leave_allocation: - # if balance is greater than the days remaining for leave allocation's end date - # then balance should be = remaining days - remaining_days = date_diff(leave_allocation[0].to_date, filters.from_date) + 1 - if remaining_days < closing_balance: - closing_balance = remaining_days - - return closing_balance - - -def get_leave_allocation_for_date(employee, leave_type, date): - allocation = frappe.qb.DocType('Leave Allocation') - records = ( - frappe.qb.from_(allocation) - .select( - allocation.name, allocation.to_date - ).where( - (allocation.docstatus == 1) - & (allocation.employee == employee) - & (allocation.leave_type == leave_type) - & ((allocation.from_date <= date) & (allocation.to_date >= date)) - ) - ).run(as_dict=True) - - return records - - def get_conditions(filters): conditions={ 'status': 'Active', @@ -191,28 +161,24 @@ def get_conditions(filters): def get_department_leave_approver_map(department=None): - # get current department and all its child department_list = frappe.get_list('Department', - filters={ - 'disabled': 0 - }, - or_filters={ - 'name': department, - 'parent_department': department - }, - fields=['name'], - pluck='name' - ) + filters={'disabled': 0}, + or_filters={ + 'name': department, + 'parent_department': department + }, + pluck='name' + ) # retrieve approvers list from current department and from its subsequent child departments approver_list = frappe.get_all('Department Approver', - filters={ - 'parentfield': 'leave_approvers', - 'parent': ('in', department_list) - }, - fields=['parent', 'approver'], - as_list=1 - ) + filters={ + 'parentfield': 'leave_approvers', + 'parent': ('in', department_list) + }, + fields=['parent', 'approver'], + as_list=True + ) approvers = {} From 26bd3053d190df07e8b75e0e86203050047b25cf Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 4 Feb 2022 17:34:56 +0530 Subject: [PATCH 022/447] perf: Weed out disabled variants via sql query instead of pythonic looping separately - If the number of variants are large (almost 2lakhs), the query to get variants and attribute data takes time - If the no.of disabled attributes is large as well, the list comprehension weeding out disabled variants takes forever - We dont need to loop over the variants data so many times - Avoid any `if a in list(b)` is best when the iterables have tremendous data --- .../variant_selector/item_variants_cache.py | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/erpnext/e_commerce/variant_selector/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py index bb6b3ef37f..9b22255d9a 100644 --- a/erpnext/e_commerce/variant_selector/item_variants_cache.py +++ b/erpnext/e_commerce/variant_selector/item_variants_cache.py @@ -66,25 +66,39 @@ class ItemVariantsCacheManager: ) ] - # join with Website Item - item_variants_data = frappe.get_all( - 'Item Variant Attribute', - {'variant_of': parent_item_code}, - ['parent', 'attribute', 'attribute_value'], - order_by='name', - as_list=1 + # Get Variants and tehir Attributes that are not disabled + iva = frappe.qb.DocType("Item Variant Attribute") + item = frappe.qb.DocType("Item") + query = ( + frappe.qb.from_(iva) + .join(item).on(item.name == iva.parent) + .select( + iva.parent, iva.attribute, iva.attribute_value + ).where( + (iva.variant_of == parent_item_code) + & (item.disabled == 0) + ).orderby(iva.name) ) + item_variants_data = query.run() - disabled_items = set( - [i.name for i in frappe.db.get_all('Item', {'disabled': 1})] - ) + # item_variants_data = frappe.get_all( + # 'Item Variant Attribute', + # {'variant_of': parent_item_code}, + # ['parent', 'attribute', 'attribute_value'], + # order_by='name', + # as_list=1 + # ) + + # disabled_items = set( + # [i.name for i in frappe.db.get_all('Item', {'disabled': 1})] + # ) attribute_value_item_map = frappe._dict() item_attribute_value_map = frappe._dict() # dont consider variants that are disabled # pull all other variants - item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items] + # item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items] for row in item_variants_data: item_code, attribute, attribute_value = row From 363ed9ccba3f848908113e6d728735a1c894aec8 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sat, 5 Feb 2022 14:06:18 +0100 Subject: [PATCH 023/447] =?UTF-8?q?revert:=20BU=20Schl=C3=BCssel=20(a21f76?= =?UTF-8?q?f)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- erpnext/regional/report/datev/datev.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index beac7ed65c..92a10c288f 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -343,8 +343,7 @@ def run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=1): /* against number or, if empty, party against number */ %(temporary_against_account_number)s as 'Gegenkonto (ohne BU-Schlüssel)', - /* disable automatic VAT deduction */ - '40' as 'BU-Schlüssel', + '' as 'BU-Schlüssel', gl.posting_date as 'Belegdatum', gl.voucher_no as 'Belegfeld 1', From 4284017e9de3b033156ca6947665bc99f0daefc3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 7 Feb 2022 12:26:01 +0530 Subject: [PATCH 024/447] fix: Copyright info --- .../payment_terms_status_for_sales_order.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js index 0450631a3b..0e36b3fe3d 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js @@ -1,4 +1,4 @@ -// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt /* eslint-disable */ From a64228741d065f7ac33b3208d3a704616250f925 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 8 Feb 2022 11:15:19 +0530 Subject: [PATCH 025/447] fix: Trim spaces from attributes (multi-variant creation) & explicit method for building cache - Multiple Item Variants creation fails due to extra spaces in attributes from popup. Clean them before passing to server side - Mention explicit method to build variants cache to avoid ambiguity between old method path (pre-refactor) --- erpnext/e_commerce/variant_selector/item_variants_cache.py | 5 ++++- erpnext/stock/doctype/item/item.js | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/e_commerce/variant_selector/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py index 9b22255d9a..3aefc446c2 100644 --- a/erpnext/e_commerce/variant_selector/item_variants_cache.py +++ b/erpnext/e_commerce/variant_selector/item_variants_cache.py @@ -138,4 +138,7 @@ def build_cache(item_code): def enqueue_build_cache(item_code): if frappe.cache().hget('item_cache_build_in_progress', item_code): return - frappe.enqueue(build_cache, item_code=item_code, queue='long') + frappe.enqueue( + "erpnext.e_commerce.variant_selector.item_variants_cache.build_cache", + item_code=item_code, queue='long' + ) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 2a30ca11fb..dfc09181ca 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -545,7 +545,7 @@ $.extend(erpnext.item, { let selected_attributes = {}; me.multiple_variant_dialog.$wrapper.find('.form-column').each((i, col) => { if(i===0) return; - let attribute_name = $(col).find('label').html(); + let attribute_name = $(col).find('label').html().trim(); selected_attributes[attribute_name] = []; let checked_opts = $(col).find('.checkbox input'); checked_opts.each((i, opt) => { From 4f5a0b8941101f759f2d1af33d952a1bfdfd3cf4 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 8 Feb 2022 12:02:02 +0530 Subject: [PATCH 026/447] chore: Fix flaky test `test_exact_match_with_price` - Clear cart settings in cache to avoid stale values --- erpnext/e_commerce/variant_selector/test_variant_selector.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/e_commerce/variant_selector/test_variant_selector.py b/erpnext/e_commerce/variant_selector/test_variant_selector.py index b83961e6e1..4d907c6221 100644 --- a/erpnext/e_commerce/variant_selector/test_variant_selector.py +++ b/erpnext/e_commerce/variant_selector/test_variant_selector.py @@ -104,6 +104,8 @@ class TestVariantSelector(ERPNextTestCase): }) make_web_item_price(item_code="Test-Tshirt-Temp-S-R", price_list_rate=100) + + frappe.local.shopping_cart_settings = None # clear cached settings values next_values = get_next_attribute_and_values( "Test-Tshirt-Temp", selected_attributes={"Test Size": "Small", "Test Colour": "Red"} From d636c3fb04ae5b1b95c67052bfb5f894e4cbf4f4 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Wed, 9 Feb 2022 10:52:38 -0500 Subject: [PATCH 027/447] test: many users linked to customer shopping cart --- .../shopping_cart/test_shopping_cart.py | 25 +++++++++++-------- erpnext/tests/utils.py | 14 +++++++++++ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py index 8519e68d09..05c874af98 100644 --- a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py +++ b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py @@ -57,13 +57,19 @@ class TestShoppingCart(unittest.TestCase): return quotation def test_get_cart_customer(self): - self.login_as_customer() + def validate_quotation(): + # test if quotation with customer is fetched + quotation = _get_cart_quotation() + self.assertEqual(quotation.quotation_to, "Customer") + self.assertEqual(quotation.party_name, "_Test Customer") + self.assertEqual(quotation.contact_email, frappe.session.user) + return quotation - # test if quotation with customer is fetched - quotation = _get_cart_quotation() - self.assertEqual(quotation.quotation_to, "Customer") - self.assertEqual(quotation.party_name, "_Test Customer") - self.assertEqual(quotation.contact_email, frappe.session.user) + self.login_as_customer("test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer") + validate_quotation() + + self.login_as_customer() + quotation = validate_quotation() return quotation @@ -254,10 +260,9 @@ class TestShoppingCart(unittest.TestCase): self.create_user_if_not_exists("test_cart_user@example.com") frappe.set_user("test_cart_user@example.com") - def login_as_customer(self): - self.create_user_if_not_exists("test_contact_customer@example.com", - "_Test Contact For _Test Customer") - frappe.set_user("test_contact_customer@example.com") + def login_as_customer(self, email="test_contact_customer@example.com", name="_Test Contact For _Test Customer"): + self.create_user_if_not_exists(email, name) + frappe.set_user(email) def clear_existing_quotations(self): quotations = frappe.get_all("Quotation", filters={ diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index 2bd7e9e71d..40c95eb7a3 100644 --- a/erpnext/tests/utils.py +++ b/erpnext/tests/utils.py @@ -66,6 +66,20 @@ def create_test_contact_and_address(): contact.add_phone("+91 0000000000", is_primary_phone=True) contact.insert() + contact_two = frappe.get_doc({ + "doctype": 'Contact', + "first_name": "_Test Contact 2 for _Test Customer", + "links": [ + { + "link_doctype": "Customer", + "link_name": "_Test Customer" + } + ] + }) + contact_two.add_email("test_contact_two_customer@example.com", is_primary=True) + contact_two.add_phone("+92 0000000000", is_primary_phone=True) + contact_two.insert() + @contextmanager def change_settings(doctype, settings_dict): From 34ad5b1abf534f55876fcfac2852df2ea42ecb41 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Wed, 9 Feb 2022 10:53:24 -0500 Subject: [PATCH 028/447] fix: get cart for logged in user. --- erpnext/e_commerce/shopping_cart/cart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py index 458cf69af7..372aed0b95 100644 --- a/erpnext/e_commerce/shopping_cart/cart.py +++ b/erpnext/e_commerce/shopping_cart/cart.py @@ -310,7 +310,7 @@ def _get_cart_quotation(party=None): party = get_party() quotation = frappe.get_all("Quotation", fields=["name"], filters= - {"party_name": party.name, "order_type": "Shopping Cart", "docstatus": 0}, + {"party_name": party.name, "contact_email": frappe.session.user, "order_type": "Shopping Cart", "docstatus": 0}, order_by="modified desc", limit_page_length=1) if quotation: From 49bee568a1d5bcfc315f539af734bc55a84a35ef Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Wed, 9 Feb 2022 12:03:05 -0500 Subject: [PATCH 029/447] fix: get cart items for logged in user. --- erpnext/e_commerce/product_data_engine/query.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/e_commerce/product_data_engine/query.py b/erpnext/e_commerce/product_data_engine/query.py index 007bf8b348..cfc3c7b357 100644 --- a/erpnext/e_commerce/product_data_engine/query.py +++ b/erpnext/e_commerce/product_data_engine/query.py @@ -264,7 +264,7 @@ class ProductQuery: customer = get_customer(silent=True) if customer: quotation = frappe.get_all("Quotation", fields=["name"], filters= - {"party_name": customer, "order_type": "Shopping Cart", "docstatus": 0}, + {"party_name": customer, "contact_email": frappe.session.user, "order_type": "Shopping Cart", "docstatus": 0}, order_by="modified desc", limit_page_length=1) if quotation: items = frappe.get_all( @@ -298,4 +298,4 @@ class ProductQuery: # slice results manually result[:self.page_length] - return result \ No newline at end of file + return result From da73685f7172290151a279f8cf796628dbf6617e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 10 Feb 2022 13:07:51 +0530 Subject: [PATCH 030/447] fix: Multiple fixes in Gross Profit report --- .../report/gross_profit/gross_profit.js | 10 +++-- .../report/gross_profit/gross_profit.py | 43 +++++++++++++------ 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js index 685f2d6176..c8a9a228c6 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.js +++ b/erpnext/accounts/report/gross_profit/gross_profit.js @@ -8,20 +8,22 @@ frappe.query_reports["Gross Profit"] = { "label": __("Company"), "fieldtype": "Link", "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_user_default("Company") + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 }, { "fieldname":"from_date", "label": __("From Date"), "fieldtype": "Date", - "default": frappe.defaults.get_user_default("year_start_date") + "default": frappe.defaults.get_user_default("year_start_date"), + "reqd": 1 }, { "fieldname":"to_date", "label": __("To Date"), "fieldtype": "Date", - "default": frappe.defaults.get_user_default("year_end_date") + "default": frappe.defaults.get_user_default("year_end_date"), + "reqd": 1 }, { "fieldname":"sales_invoice", diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 84effc0f46..225b7c6426 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -369,20 +369,37 @@ class GrossProfitGenerator(object): return self.average_buying_rate[item_code] def get_last_purchase_rate(self, item_code, row): - condition = '' - if row.project: - condition += " AND a.project=%s" % (frappe.db.escape(row.project)) - elif row.cost_center: - condition += " AND a.cost_center=%s" % (frappe.db.escape(row.cost_center)) - if self.filters.to_date: - condition += " AND modified='%s'" % (self.filters.to_date) + purchase_invoice = frappe.qb.DocType("Purchase Invoice") + purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item") - last_purchase_rate = frappe.db.sql(""" - select (a.base_rate / a.conversion_factor) - from `tabPurchase Invoice Item` a - where a.item_code = %s and a.docstatus=1 - {0} - order by a.modified desc limit 1""".format(condition), item_code) + query = (frappe.qb.from_(purchase_invoice_item) + .inner_join( + purchase_invoice + ).on( + purchase_invoice.name == purchase_invoice_item.parent + ).select( + purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor + ).where( + purchase_invoice.docstatus == 1 + ).where( + purchase_invoice.posting_date <= self.filters.to_date + ).where( + purchase_invoice_item.item_code == item_code + )) + + if row.project: + query.where( + purchase_invoice_item.item_code == row.project + ) + + if row.cost_center: + query.where( + purchase_invoice_item.cost_center == row.cost_center + ) + + query.orderby(purchase_invoice.posting_date, order=frappe.qb.desc) + query.limit(1) + last_purchase_rate = query.run() return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0 From 2172ab2d37d8be0c43d1f885a40657d352d255b4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 11 Feb 2022 14:48:39 +0530 Subject: [PATCH 031/447] fix: Update columns in new format --- .../report/gross_profit/gross_profit.json | 4 +- .../report/gross_profit/gross_profit.py | 80 ++++++------------- 2 files changed, 28 insertions(+), 56 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.json b/erpnext/accounts/report/gross_profit/gross_profit.json index 76c560ad24..0730ffd77e 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.json +++ b/erpnext/accounts/report/gross_profit/gross_profit.json @@ -1,5 +1,5 @@ { - "add_total_row": 0, + "add_total_row": 1, "columns": [], "creation": "2013-02-25 17:03:34", "disable_prepared_report": 0, @@ -9,7 +9,7 @@ "filters": [], "idx": 3, "is_standard": "Yes", - "modified": "2021-11-13 19:14:23.730198", + "modified": "2022-02-11 10:18:36.956558", "modified_by": "Administrator", "module": "Accounts", "name": "Gross Profit", diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 225b7c6426..c403b76f87 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -70,43 +70,42 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_ data.append(row) def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data): - for idx, src in enumerate(gross_profit_data.grouped_data): + for src in gross_profit_data.grouped_data: row = [] for col in group_wise_columns.get(scrub(filters.group_by)): row.append(src.get(col)) row.append(filters.currency) - if idx == len(gross_profit_data.grouped_data)-1: - row[0] = "Total" data.append(row) def get_columns(group_wise_columns, filters): columns = [] column_map = frappe._dict({ - "parent": _("Sales Invoice") + ":Link/Sales Invoice:120", - "invoice_or_item": _("Sales Invoice") + ":Link/Sales Invoice:120", - "posting_date": _("Posting Date") + ":Date:100", - "posting_time": _("Posting Time") + ":Data:100", - "item_code": _("Item Code") + ":Link/Item:100", - "item_name": _("Item Name") + ":Data:100", - "item_group": _("Item Group") + ":Link/Item Group:100", - "brand": _("Brand") + ":Link/Brand:100", - "description": _("Description") +":Data:100", - "warehouse": _("Warehouse") + ":Link/Warehouse:100", - "qty": _("Qty") + ":Float:80", - "base_rate": _("Avg. Selling Rate") + ":Currency/currency:100", - "buying_rate": _("Valuation Rate") + ":Currency/currency:100", - "base_amount": _("Selling Amount") + ":Currency/currency:100", - "buying_amount": _("Buying Amount") + ":Currency/currency:100", - "gross_profit": _("Gross Profit") + ":Currency/currency:100", - "gross_profit_percent": _("Gross Profit %") + ":Percent:100", - "project": _("Project") + ":Link/Project:100", - "sales_person": _("Sales person"), - "allocated_amount": _("Allocated Amount") + ":Currency/currency:100", - "customer": _("Customer") + ":Link/Customer:100", - "customer_group": _("Customer Group") + ":Link/Customer Group:100", - "territory": _("Territory") + ":Link/Territory:100" + "parent": {"label": _('Sales Invoice'), "fieldname": "parent_invoice", "fieldtype": "Link", "options": "Sales Invoice", "width": 120}, + "invoice_or_item": {"label": _('Sales Invoice'), "fieldtype": "Link", "options": "Sales Invoice", "width": 120}, + "posting_date": {"label": _('Posting Date'), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, + "posting_time": {"label": _('Posting Time'), "fieldname": "posting_time", "fieldtype": "Data", "width": 100}, + "item_code": {"label": _('Item Code'), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 100}, + "item_name": {"label": _('Item Name'), "fieldname": "item_name", "fieldtype": "Data", "width": 100}, + "item_group": {"label": _('Item Group'), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", "width": 100}, + "brand": {"label": _('Brand'), "fieldtype": "Link", "options": "Brand", "width": 100}, + "description": {"label": _('Description'), "fieldname": "description", "fieldtype": "Data", "width": 100}, + "warehouse": {"label": _('Warehouse'), "fieldname": "warehouse", "fieldtype": "Link", "options": "warehouse", "width": 100}, + "qty": {"label": _('Qty'), "fieldname": "qty", "fieldtype": "Float", "width": 80}, + "base_rate": {"label": _('Avg. Selling Rate'), "fieldname": "avg._selling_rate", "fieldtype": "Currency", "options": "currency", "width": 100}, + "buying_rate": {"label": _('Valuation Rate'), "fieldname": "valuation_rate", "fieldtype": "Currency", "options": "currency", "width": 100}, + "base_amount": {"label": _('Selling Amount'), "fieldname": "selling_amount", "fieldtype": "Currency", "options": "currency", "width": 100}, + "buying_amount": {"label": _('Buying Amount'), "fieldname": "buying_amount", "fieldtype": "Currency", "options": "currency", "width": 100}, + "gross_profit": {"label": _('Gross Profit'), "fieldname": "gross_profit", "fieldtype": "Currency", "options": "currency", "width": 100}, + "gross_profit_percent": {"label": _('Gross Profit Percent'), "fieldname": "gross_profit_%", + "fieldtype": "Percent", "width": 100}, + "project": {"label": _('Project'), "fieldname": "project", "fieldtype": "Link", "options": "Project", "width": 100}, + "sales_person": {"label": _('Sales Person'), "fieldname": "sales_person", "fieldtype": "Data","width": 100}, + "allocated_amount": {"label": _('Allocated Amount'), "fieldname": "allocated_amount", "fieldtype": "Currency", "options": "currency", "width": 100}, + "customer": {"label": _('Customer'), "fieldname": "customer", "fieldtype": "Link", "options": "Customer", "width": 100}, + "customer_group": {"label": _('Customer Group'), "fieldname": "customer_group", "fieldtype": "Link", "options": "customer", "width": 100}, + "territory": {"label": _('Territory'), "fieldname": "territory", "fieldtype": "Link", "options": "territory", "width": 100}, }) for col in group_wise_columns.get(scrub(filters.group_by)): @@ -223,16 +222,6 @@ class GrossProfitGenerator(object): self.get_average_rate_based_on_group_by() def get_average_rate_based_on_group_by(self): - # sum buying / selling totals for group - self.totals = frappe._dict( - qty=0, - base_amount=0, - buying_amount=0, - gross_profit=0, - gross_profit_percent=0, - base_rate=0, - buying_rate=0 - ) for key in list(self.grouped): if self.filters.get("group_by") != "Invoice": for i, row in enumerate(self.grouped[key]): @@ -244,7 +233,6 @@ class GrossProfitGenerator(object): new_row.base_amount += flt(row.base_amount, self.currency_precision) new_row = self.set_average_rate(new_row) self.grouped_data.append(new_row) - self.add_to_totals(new_row) else: for i, row in enumerate(self.grouped[key]): if row.indent == 1.0: @@ -258,17 +246,6 @@ class GrossProfitGenerator(object): if (flt(row.qty) or row.base_amount): row = self.set_average_rate(row) self.grouped_data.append(row) - self.add_to_totals(row) - - self.set_average_gross_profit(self.totals) - - if self.filters.get("group_by") == "Invoice": - self.totals.indent = 0.0 - self.totals.parent_invoice = "" - self.totals.invoice_or_item = "Total" - self.si_list.append(self.totals) - else: - self.grouped_data.append(self.totals) def is_not_invoice_row(self, row): return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get("group_by") != "Invoice" @@ -284,11 +261,6 @@ class GrossProfitGenerator(object): new_row.gross_profit_percent = flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision) \ if new_row.base_amount else 0 - def add_to_totals(self, new_row): - for key in self.totals: - if new_row.get(key): - self.totals[key] += new_row[key] - def get_returned_invoice_items(self): returned_invoices = frappe.db.sql(""" select @@ -389,7 +361,7 @@ class GrossProfitGenerator(object): if row.project: query.where( - purchase_invoice_item.item_code == row.project + purchase_invoice_item.project == row.project ) if row.cost_center: From 07bcbc6c7e10f977bc5a6ff8f5b48d91ec9b2b70 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 12 Feb 2022 19:05:03 +0530 Subject: [PATCH 032/447] fix: Remove unused param --- erpnext/accounts/report/gross_profit/gross_profit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index c403b76f87..ebb929aaac 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -172,7 +172,7 @@ class GrossProfitGenerator(object): buying_amount = 0 for row in reversed(self.si_list): - if self.skip_row(row, self.product_bundles): + if self.skip_row(row): continue row.base_amount = flt(row.base_net_amount, self.currency_precision) @@ -278,7 +278,7 @@ class GrossProfitGenerator(object): self.returned_invoices.setdefault(inv.return_against, frappe._dict())\ .setdefault(inv.item_code, []).append(inv) - def skip_row(self, row, product_bundles): + def skip_row(self, row): if self.filters.get("group_by") != "Invoice": if not row.get(scrub(self.filters.get("group_by", ""))): return True From ae613008be59334e5ff72882ef9d70355f56805e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 12 Feb 2022 21:54:22 +0530 Subject: [PATCH 033/447] fix: Error in consolidated financial statements --- .../consolidated_financial_statement.py | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 758e3e9337..62bf156219 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -367,7 +367,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies): accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0) def get_account_heads(root_type, companies, filters): - accounts = get_accounts(root_type, filters) + accounts = get_accounts(root_type, companies) if not accounts: return None, None, None @@ -396,7 +396,7 @@ def update_parent_account_names(accounts): for account in accounts: if account.parent_account: - account["parent_account_name"] = name_to_account_map[account.parent_account] + account["parent_account_name"] = name_to_account_map.get(account.parent_account) return accounts @@ -419,12 +419,21 @@ def get_subsidiary_companies(company): return frappe.db.sql_list("""select name from `tabCompany` where lft >= {0} and rgt <= {1} order by lft, rgt""".format(lft, rgt)) -def get_accounts(root_type, filters): - return frappe.db.sql(""" select name, is_group, company, - parent_account, lft, rgt, root_type, report_type, account_name, account_number - from - `tabAccount` where company = %s and root_type = %s - """ , (filters.get('company'), root_type), as_dict=1) +def get_accounts(root_type, companies): + accounts = [] + added_accounts = [] + + for company in companies: + for account in frappe.db.sql(""" select name, is_group, company, + parent_account, lft, rgt, root_type, report_type, account_name, account_number + from + `tabAccount` where company = %s and root_type = %s + """ , (company, root_type), as_dict=1): + if account.account_name not in added_accounts: + accounts.append(account) + added_accounts.append(account.account_name) + + return accounts def prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters): data = [] From dbd29da189145cb059ee88707e62c7d1888ed91a Mon Sep 17 00:00:00 2001 From: Wolfram Schmidt Date: Sun, 13 Feb 2022 13:11:31 +0100 Subject: [PATCH 034/447] Translation for DocType https://testsystem.frappe.cloud/app/milestone --- erpnext/translations/de.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index cf73564b9e..f345a87d03 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1597,6 +1597,7 @@ Method,Methode, Middle Income,Mittleres Einkommen, Middle Name,Zweiter Vorname, Middle Name (Optional),Weiterer Vorname (optional), +Milestonde,Meilenstein, Min Amt can not be greater than Max Amt,Min. Amt kann nicht größer als Max. Amt sein, Min Qty can not be greater than Max Qty,Mindestmenge kann nicht größer als Maximalmenge sein, Minimum Lead Age (Days),Mindest Lead-Alter (in Tagen), From 615dd9decd1947eb8203d0b2145138044c2522a5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 13 Feb 2022 19:24:10 +0530 Subject: [PATCH 035/447] fix: Patch fixes --- .../v14_0/update_opportunity_currency_fields.py | 12 ++++-------- erpnext/regional/saudi_arabia/setup.py | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/erpnext/patches/v14_0/update_opportunity_currency_fields.py b/erpnext/patches/v14_0/update_opportunity_currency_fields.py index 13071478c8..82213fff6c 100644 --- a/erpnext/patches/v14_0/update_opportunity_currency_fields.py +++ b/erpnext/patches/v14_0/update_opportunity_currency_fields.py @@ -6,8 +6,8 @@ from erpnext.setup.utils import get_exchange_rate def execute(): - frappe.reload_doc('crm', 'doctype', 'opportunity') - frappe.reload_doc('crm', 'doctype', 'opportunity_item') + frappe.reload_doc('crm', 'doctype', 'opportunity', force=True) + frappe.reload_doc('crm', 'doctype', 'opportunity_item', force=True) opportunities = frappe.db.get_list('Opportunity', filters={ 'opportunity_amount': ['>', 0] @@ -20,15 +20,11 @@ def execute(): if opportunity.currency != company_currency: conversion_rate = get_exchange_rate(opportunity.currency, company_currency) base_opportunity_amount = flt(conversion_rate) * flt(opportunity.opportunity_amount) - grand_total = flt(opportunity.opportunity_amount) - base_grand_total = flt(conversion_rate) * flt(opportunity.opportunity_amount) else: conversion_rate = 1 - base_opportunity_amount = grand_total = base_grand_total = flt(opportunity.opportunity_amount) + base_opportunity_amount = flt(opportunity.opportunity_amount) frappe.db.set_value('Opportunity', opportunity.name, { 'conversion_rate': conversion_rate, - 'base_opportunity_amount': base_opportunity_amount, - 'grand_total': grand_total, - 'base_grand_total': base_grand_total + 'base_opportunity_amount': base_opportunity_amount }, update_modified=False) diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py index 15d524d5b8..d2ef6f3f17 100644 --- a/erpnext/regional/saudi_arabia/setup.py +++ b/erpnext/regional/saudi_arabia/setup.py @@ -102,7 +102,7 @@ def make_custom_fields(): ] } - create_custom_fields(custom_fields, update=True) + create_custom_fields(custom_fields, ignore_validate=True, update=True) def update_regional_tax_settings(country, company): create_ksa_vat_setting(company) From bc244d074062d23be99922a370564bba13e15890 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 8 Feb 2022 18:53:08 +0530 Subject: [PATCH 036/447] refactor: currency field and code cleanup --- .../payment_terms_status_for_sales_order.py | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index aa2f757218..4eafa9b2ef 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -13,62 +13,60 @@ def get_columns(): "fieldname": "name", "fieldtype": "Link", "options": "Sales Order", - "read_only": 1, }, { - "label": _("Submitted"), + "label": _("Posting Date"), "fieldname": "submitted", "fieldtype": "Date", - "read_only": 1 }, { "label": _("Payment Term"), "fieldname": "payment_term", "fieldtype": "Data", - "read_only": 1 }, { "label": _("Description"), "fieldname": "description", "fieldtype": "Data", - "read_only": 1 }, { "label": _("Due Date"), "fieldname": "due_date", "fieldtype": "Date", - "read_only": 1 }, { "label": _("Invoice Portion"), "fieldname": "invoice_portion", "fieldtype": "Percent", - "read_only": 1, }, { "label": _("Payment Amount"), "fieldname": "payment_amount", "fieldtype": "Currency", - "read_only": 1, + "options": "currency", }, { "label": _("Paid Amount"), "fieldname": "paid_amount", "fieldtype": "Currency", - "read_only": 1 + "options": "currency", }, { "label": _("Invoices"), "fieldname": "invoices", "fieldtype": "Link", "options": "Sales Invoice", - "read_only": 1, }, { "label": _("Status"), "fieldname": "status", "fieldtype": "Data", - "read_only": 1 + }, + { + "label": _("Currency"), + "fieldname": "currency", + "fieldtype": "Currency", + "hidden": 1 } ] return columns @@ -152,12 +150,13 @@ def get_so_with_invoices(filters): return sorders, invoices -def set_payment_terms_statuses(sales_orders, invoices): +def set_payment_terms_statuses(sales_orders, invoices, filters): """ compute status for payment terms with associated sales invoice using FIFO """ for so in sales_orders: + so.currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency') for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]: if so.payment_amount - so.paid_amount > 0: amount = so.payment_amount - so.paid_amount @@ -200,7 +199,7 @@ def prepare_chart(s_orders): def execute(filters=None): columns = get_columns() sales_orders, so_invoices = get_so_with_invoices(filters) - sales_orders, so_invoices = set_payment_terms_statuses(sales_orders, so_invoices) + sales_orders, so_invoices = set_payment_terms_statuses(sales_orders, so_invoices, filters) prepare_chart(sales_orders) From 973f6b1bbd53594e5b2a51a1dcdf7d9e38dd46a8 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 14 Feb 2022 22:14:17 +0530 Subject: [PATCH 037/447] fix: Gross profit for credit notes --- erpnext/accounts/report/gross_profit/gross_profit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index ebb929aaac..b03bb9bb13 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -282,8 +282,8 @@ class GrossProfitGenerator(object): if self.filters.get("group_by") != "Invoice": if not row.get(scrub(self.filters.get("group_by", ""))): return True - elif row.get("is_return") == 1: - return True + + return False def get_buying_amount_from_product_bundle(self, row, product_bundle): buying_amount = 0.0 From 42cdd6d2379d68efb592a5c8a8148979dce8cf1e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 15 Feb 2022 12:05:51 +0530 Subject: [PATCH 038/447] fix: Remove commented out code --- .../consolidated_financial_statement.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 62bf156219..dad7384fea 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -354,9 +354,6 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies): if d.parent_account: account = d.parent_account_name - # if not accounts_by_name.get(account): - # continue - for company in companies: accounts_by_name[account][company] = \ accounts_by_name[account].get(company, 0.0) + d.get(company, 0.0) From fec40aac7a25c383e384f29471f9ea82382524b2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 15 Feb 2022 12:15:35 +0530 Subject: [PATCH 039/447] fix: Linting issues --- .../consolidated_financial_statement.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index dad7384fea..1e20f7be3e 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -421,11 +421,9 @@ def get_accounts(root_type, companies): added_accounts = [] for company in companies: - for account in frappe.db.sql(""" select name, is_group, company, - parent_account, lft, rgt, root_type, report_type, account_name, account_number - from - `tabAccount` where company = %s and root_type = %s - """ , (company, root_type), as_dict=1): + for account in frappe.get_all("Account", fields=["name", "is_group", "company", + "parent_account", "lft", "rgt", "root_type", "report_type", "account_name", "account_number"], + filters={"company": company, "root_type": root_type}): if account.account_name not in added_accounts: accounts.append(account) added_accounts.append(account.account_name) From 85ed0fb8d6ef45197bfef4a71cb8f02355d61930 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 15 Feb 2022 12:20:06 +0530 Subject: [PATCH 040/447] fix: default to company currency in report output --- .../payment_terms_status_for_sales_order.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index 4eafa9b2ef..d0902e111a 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -41,7 +41,7 @@ def get_columns(): }, { "label": _("Payment Amount"), - "fieldname": "payment_amount", + "fieldname": "base_payment_amount", "fieldtype": "Currency", "options": "currency", }, @@ -113,7 +113,7 @@ def get_so_with_invoices(filters): ps.description, ps.due_date, ps.invoice_portion, - ps.payment_amount, + ps.base_payment_amount, ps.paid_amount, ) .where( @@ -141,7 +141,7 @@ def get_so_with_invoices(filters): .on(si.name == sii.parent) .inner_join(soi) .on(soi.name == sii.so_detail) - .select(sii.sales_order, sii.parent.as_("invoice"), si.base_net_total.as_("invoice_amount")) + .select(sii.sales_order, sii.parent.as_("invoice"), si.base_grand_total.as_("invoice_amount")) .where((sii.sales_order.isin([x.name for x in sorders])) & (si.docstatus == 1)) .groupby(sii.parent) ) @@ -158,8 +158,8 @@ def set_payment_terms_statuses(sales_orders, invoices, filters): for so in sales_orders: so.currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency') for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]: - if so.payment_amount - so.paid_amount > 0: - amount = so.payment_amount - so.paid_amount + if so.base_payment_amount - so.paid_amount > 0: + amount = so.base_payment_amount - so.paid_amount if inv.invoice_amount >= amount: inv.invoice_amount -= amount so.paid_amount += amount @@ -187,7 +187,7 @@ def prepare_chart(s_orders): "data": { "labels": [term.payment_term for term in s_orders], "datasets": [ - {"name": "Payment Amount", "values": [x.payment_amount for x in s_orders],}, + {"name": "Payment Amount", "values": [x.base_payment_amount for x in s_orders],}, {"name": "Paid Amount", "values": [x.paid_amount for x in s_orders],}, ], }, From a4b8d673232fd313396788ef745e67572c235dcc Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 15 Feb 2022 12:20:41 +0530 Subject: [PATCH 041/447] refactor: create invoices list without if else --- .../payment_terms_status_for_sales_order.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index d0902e111a..e6a56eea31 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -157,25 +157,20 @@ def set_payment_terms_statuses(sales_orders, invoices, filters): for so in sales_orders: so.currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency') + so.invoices = "" for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]: if so.base_payment_amount - so.paid_amount > 0: amount = so.base_payment_amount - so.paid_amount if inv.invoice_amount >= amount: inv.invoice_amount -= amount so.paid_amount += amount - if so.invoices: - so.invoices = so.invoices + "," + inv.invoice - else: - so.invoices = inv.invoice + so.invoices += "," + inv.invoice so.status = "Completed" break else: so.paid_amount += inv.invoice_amount inv.invoice_amount = 0 - if so.invoices: - so.invoices = so.invoices + "," + inv.invoice - else: - so.invoices = inv.invoice + so.invoices += "," + inv.invoice so.status = "Partly Paid" return sales_orders, invoices From 18731622c43f3b8f7d792d4bb4139eb7cdda39d9 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Feb 2022 12:40:39 +0530 Subject: [PATCH 042/447] fix: Update SO via Work Order made from MR (attached to SO) - Add SO Item reference in WO from MR (that was made from SO) --- erpnext/stock/doctype/material_request/material_request.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 103e8d6a88..b39328f85b 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -533,6 +533,7 @@ def raise_work_orders(material_request): "stock_uom": d.stock_uom, "expected_delivery_date": d.schedule_date, "sales_order": d.sales_order, + "sales_order_item": d.get("sales_order_item"), "bom_no": get_item_details(d.item_code).bom_no, "material_request": mr.name, "material_request_item": d.name, From f9d52e73469ea298e3a2d39d893f2da5e6baf9aa Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Feb 2022 13:38:15 +0530 Subject: [PATCH 043/447] test: SO > MR > WO flow --- .../doctype/sales_order/test_sales_order.py | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 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 acf048e116..e6628d9518 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -6,7 +6,7 @@ import json import frappe import frappe.permissions from frappe.core.doctype.user_permission.test_user_permission import create_user -from frappe.utils import add_days, flt, getdate, nowdate +from frappe.utils import add_days, flt, getdate, nowdate, today from erpnext.controllers.accounts_controller import update_child_qty_rate from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import ( @@ -1399,6 +1399,48 @@ class TestSalesOrder(ERPNextTestCase): so.load_from_db() self.assertEqual(so.billing_status, 'Fully Billed') + def test_so_back_updated_from_wo_via_mr(self): + "SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO." + from erpnext.stock.doctype.material_request.material_request import raise_work_orders + from erpnext.manufacturing.doctype.work_order.work_order import ( + make_stock_entry as make_se_from_wo, + ) + + so = make_sales_order(item_list=[{"item_code": "_Test FG Item","qty": 2, "rate":100}]) + + mr = make_material_request(so.name) + mr.material_request_type = "Manufacture" + mr.schedule_date = today() + mr.submit() + + # WO from MR + wo_name = raise_work_orders(mr.name)[0] + wo = frappe.get_doc("Work Order", wo_name) + wo.wip_warehouse = "Work In Progress - _TC" + wo.skip_transfer = True + + self.assertEqual(wo.sales_order, so.name) + self.assertEqual(wo.sales_order_item, so.items[0].name) + + wo.submit() + make_stock_entry(item_code="_Test Item", # Stock RM + target="Work In Progress - _TC", + qty=4, basic_rate=100 + ) + make_stock_entry(item_code="_Test Item Home Desktop 100", # Stock RM + target="Work In Progress - _TC", + qty=4, basic_rate=100 + ) + + se = frappe.get_doc(make_se_from_wo(wo.name, "Manufacture", 2)) + se.submit() # Finish WO + + mr.reload() + wo.reload() + so.reload() + self.assertEqual(so.items[0].work_order_qty, wo.produced_qty) + self.assertEqual(mr.status, "Manufactured") + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") accounts_settings.automatically_fetch_payment_terms = enable From 0ca58d762715fd10c751c4497f3037908f4dfb20 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Feb 2022 14:20:54 +0530 Subject: [PATCH 044/447] chore: Patch to update SO work_order_qty and Linter fix --- erpnext/patches.txt | 1 + .../v14_0/set_work_order_qty_in_so_from_mr.py | 36 +++++++++++++++++++ .../doctype/sales_order/test_sales_order.py | 2 +- 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d104bc003c..c26451a30c 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -352,3 +352,4 @@ erpnext.patches.v13_0.shopping_cart_to_ecommerce erpnext.patches.v13_0.update_disbursement_account erpnext.patches.v13_0.update_reserved_qty_closed_wo erpnext.patches.v14_0.delete_amazon_mws_doctype +erpnext.patches.v14_0.set_work_order_qty_in_so_from_mr diff --git a/erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py b/erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py new file mode 100644 index 0000000000..f097ab9297 --- /dev/null +++ b/erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py @@ -0,0 +1,36 @@ +import frappe + + +def execute(): + """ + 1. Get submitted Work Orders with MR, MR Item and SO set + 2. Get SO Item detail from MR Item detail in WO, and set in WO + 3. Update work_order_qty in SO + """ + work_order = frappe.qb.DocType("Work Order") + query = ( + frappe.qb.from_(work_order) + .select( + work_order.name, work_order.produced_qty, + work_order.material_request, + work_order.material_request_item, + work_order.sales_order + ).where( + (work_order.material_request.isnotnull()) + & (work_order.material_request_item.isnotnull()) + & (work_order.sales_order.isnotnull()) + & (work_order.docstatus == 1) + & (work_order.produced_qty > 0) + ) + ) + results = query.run(as_dict=True) + + for row in results: + so_item = frappe.get_value( + "Material Request Item", row.material_request_item, "sales_order_item" + ) + frappe.db.set_value("Work Order", row.name, "sales_order_item", so_item) + + if so_item: + wo = frappe.get_doc("Work Order", row.name) + wo.update_work_order_qty_in_so() diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index e6628d9518..73c5bd299a 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1401,10 +1401,10 @@ class TestSalesOrder(ERPNextTestCase): def test_so_back_updated_from_wo_via_mr(self): "SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO." - from erpnext.stock.doctype.material_request.material_request import raise_work_orders from erpnext.manufacturing.doctype.work_order.work_order import ( make_stock_entry as make_se_from_wo, ) + from erpnext.stock.doctype.material_request.material_request import raise_work_orders so = make_sales_order(item_list=[{"item_code": "_Test FG Item","qty": 2, "rate":100}]) From 49fdc6c52e9752362b754f1615ca77ac9e09b418 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 15 Feb 2022 17:20:29 +0530 Subject: [PATCH 045/447] test: refactor and fix failing test case --- ...st_payment_terms_status_for_sales_order.py | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index 5d6e91e8a5..ee6cee3be8 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -15,8 +15,8 @@ test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Temp class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): - def test_payment_terms_status(self): - + def create_payment_terms_template(self): + # create template for 50-50 payments template = None if frappe.db.exists("Payment Terms Template", "_Test 50-50"): template = frappe.get_doc("Payment Terms Template", "_Test 50-50") @@ -46,8 +46,10 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): } ) template.insert() + self.template = template - # item = create_item(item_code="_Test Excavator", is_stock_item=0, valuation_rate=1000000) + def test_payment_terms_status(self): + self.create_payment_terms_template() item = create_item(item_code="_Test Excavator", is_stock_item=0) so = make_sales_order( transaction_date="2021-06-15", @@ -58,16 +60,19 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): do_not_save=True, ) so.po_no = "" - so.payment_terms_template = template.name + so.taxes_and_charges = "" + so.taxes = "" + so.payment_terms_template = self.template.name so.save() so.submit() # make invoice with 60% of the total sales order value sinv = make_sales_invoice(so.name) + sinv.taxes_and_charges = "" + sinv.taxes = "" sinv.items[0].qty = 6 sinv.insert() sinv.submit() - columns, data, message, chart = execute( { "company": "_Test Company", @@ -86,9 +91,10 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): "description": "_Test 50-50", "due_date": datetime.date(2021, 6, 30), "invoice_portion": 50.0, - "payment_amount": 500000.0, + "currency": "INR", + "base_payment_amount": 500000.0, "paid_amount": 500000.0, - "invoices": sinv.name, + "invoices": ","+sinv.name, }, { "name": so.name, @@ -98,10 +104,13 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): "description": "_Test 50-50", "due_date": datetime.date(2021, 7, 15), "invoice_portion": 50.0, - "payment_amount": 500000.0, + "currency": "INR", + "base_payment_amount": 500000.0, "paid_amount": 100000.0, - "invoices": sinv.name, + "invoices": ","+sinv.name, }, ] + self.assertEqual(data, expected_value) + self.assertEqual(data, expected_value) From 48f37c76594fad1cd64cd44b7126d6ef1ddd5bd1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 15 Feb 2022 17:21:08 +0530 Subject: [PATCH 046/447] test: added test for alternate currency - Sales Order and Invoice will be submitted in USD with exchange rate of 70 with the default company currency - Report will display in defauly company currency --- ...st_payment_terms_status_for_sales_order.py | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index ee6cee3be8..cad41e1dc0 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -112,5 +112,87 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): ] self.assertEqual(data, expected_value) + def create_exchange_rate(self, date): + # make an entry in Currency Exchange list. serves as a static exchange rate + if frappe.db.exists({'doctype': "Currency Exchange",'date': date,'from_currency': 'USD', 'to_currency':'INR'}): + return + else: + doc = frappe.get_doc({ + 'doctype': "Currency Exchange", + 'date': date, + 'from_currency': 'USD', + 'to_currency': frappe.get_cached_value("Company", '_Test Company','default_currency'), + 'exchange_rate': 70, + 'for_buying': True, + 'for_selling': True + }) + doc.insert() + def test_alternate_currency(self): + transaction_date = "2021-06-15" + self.create_payment_terms_template() + self.create_exchange_rate(transaction_date) + item = create_item(item_code="_Test Excavator", is_stock_item=0) + so = make_sales_order( + transaction_date=transaction_date, + currency="USD", + delivery_date=add_days(transaction_date, -30), + item=item.item_code, + qty=10, + rate=10000, + do_not_save=True, + ) + so.po_no = "" + so.taxes_and_charges = "" + so.taxes = "" + so.payment_terms_template = self.template.name + so.save() + so.submit() + + # make invoice with 60% of the total sales order value + sinv = make_sales_invoice(so.name) + sinv.currency = "USD" + sinv.taxes_and_charges = "" + sinv.taxes = "" + sinv.items[0].qty = 6 + sinv.insert() + sinv.submit() + columns, data, message, chart = execute( + { + "company": "_Test Company", + "period_start_date": "2021-06-01", + "period_end_date": "2021-06-30", + "sales_order": [so.name], + } + ) + + # report defaults to company currency. + expected_value = [ + { + "name": so.name, + "submitted": datetime.date(2021, 6, 15), + "status": "Completed", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 6, 30), + "invoice_portion": 50.0, + "currency": frappe.get_cached_value("Company", '_Test Company','default_currency'), + "base_payment_amount": 3500000.0, + "paid_amount": 3500000.0, + "invoices": ","+sinv.name, + }, + { + "name": so.name, + "submitted": datetime.date(2021, 6, 15), + "status": "Partly Paid", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 7, 15), + "invoice_portion": 50.0, + "currency": frappe.get_cached_value("Company", '_Test Company','default_currency'), + "base_payment_amount": 3500000.0, + "paid_amount": 700000.0, + "invoices": ","+sinv.name, + }, + ] self.assertEqual(data, expected_value) From 799671c7482fa8bca12a24636ea0000579ca9537 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Feb 2022 18:10:57 +0530 Subject: [PATCH 047/447] fix: Transfer Bucket logic for Repack Entry with split batch rows --- .../stock/report/stock_ageing/stock_ageing.py | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index a89a4038c2..9866e63fb5 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -286,10 +286,11 @@ class FIFOSlots: def __compute_incoming_stock(self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List): "Update FIFO Queue on inward stock." - if self.transferred_item_details.get(transfer_key): - # inward/outward from same voucher, item & warehouse - slot = self.transferred_item_details[transfer_key].pop(0) - fifo_queue.append(slot) + transfer_data = self.transferred_item_details.get(transfer_key) + if transfer_data: + # [Repack] inward/outward from same voucher, item & warehouse + # consume transfer data and add stock to fifo queue + self.__adjust_incoming_transfer_qty(transfer_data, fifo_queue, row) else: if not serial_nos: if fifo_queue and flt(fifo_queue[0][0]) < 0: @@ -333,6 +334,27 @@ class FIFOSlots: self.transferred_item_details[transfer_key].append([qty_to_pop, slot[1]]) qty_to_pop = 0 + def __adjust_incoming_transfer_qty(self, transfer_data: Dict, fifo_queue: List, row: Dict): + "Add previously removed stock back to FIFO Queue." + transfer_qty_to_pop = flt(row.actual_qty) + first_bucket_qty = transfer_data[0][0] + first_bucket_date = transfer_data[0][1] + + while transfer_qty_to_pop: + if transfer_data and 0 > first_bucket_qty <= transfer_qty_to_pop: + # bucket qty is not enough, consume whole + transfer_qty_to_pop -= first_bucket_qty + slot = transfer_data.pop(0) + fifo_queue.append(slot) + elif not transfer_data: + # transfer bucket is empty, extra incoming qty + fifo_queue.append([transfer_qty_to_pop, row.posting_date]) + else: + # ample bucket qty to consume + first_bucket_qty -= transfer_qty_to_pop + fifo_queue.append([transfer_qty_to_pop, first_bucket_date]) + transfer_qty_to_pop = 0 + def __update_balances(self, row: Dict, key: Union[Tuple, str]): self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction From ea3b7de867fdcc565567ec9ca1b7925116e16f2f Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Feb 2022 18:41:42 +0530 Subject: [PATCH 048/447] test: Stock Ageing FIFO buckets for Repack entry with same item --- .../report/stock_ageing/test_stock_ageing.py | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index 66d2f6b753..3055332540 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -236,6 +236,159 @@ class TestStockAgeing(ERPNextTestCase): item_wh_balances = [item_wh_wise_slots.get(i).get("qty_after_transaction") for i in item_wh_wise_slots] self.assertEqual(sum(item_wh_balances), item_result["qty_after_transaction"]) + def test_repack_entry_same_item_split_rows(self): + """ + Split consumption rows and have single repacked item row (same warehouse). + Ledger: + Item | Qty | Voucher + ------------------------ + Item 1 | 500 | 001 + Item 1 | -50 | 002 (repack) + Item 1 | -50 | 002 (repack) + Item 1 | 100 | 002 (repack) + + Case most likely for batch items. Test time bucket computation. + """ + sle = [ + frappe._dict( # stock up item + name="Flask Item", + actual_qty=500, qty_after_transaction=500, + warehouse="WH 1", + posting_date="2021-12-03", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-50), qty_after_transaction=450, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-50), qty_after_transaction=400, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=100, qty_after_transaction=500, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + ] + slots = FIFOSlots(self.filters, sle).generate() + item_result = slots["Flask Item"] + queue = item_result["fifo_queue"] + + self.assertEqual(item_result["total_qty"], 500.0) + self.assertEqual(queue[0][0], 400.0) + self.assertEqual(queue[1][0], 100.0) + # check if time buckets add up to balance qty + self.assertEqual(sum([i[0] for i in queue]), 500.0) + + def test_repack_entry_same_item_overconsume(self): + """ + Over consume item and have less repacked item qty (same warehouse). + Ledger: + Item | Qty | Voucher + ------------------------ + Item 1 | 500 | 001 + Item 1 | -100 | 002 (repack) + Item 1 | 50 | 002 (repack) + + Case most likely for batch items. Test time bucket computation. + """ + sle = [ + frappe._dict( # stock up item + name="Flask Item", + actual_qty=500, qty_after_transaction=500, + warehouse="WH 1", + posting_date="2021-12-03", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-100), qty_after_transaction=400, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=50, qty_after_transaction=450, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + ] + slots = FIFOSlots(self.filters, sle).generate() + item_result = slots["Flask Item"] + queue = item_result["fifo_queue"] + + self.assertEqual(item_result["total_qty"], 450.0) + self.assertEqual(queue[0][0], 400.0) + self.assertEqual(queue[1][0], 50.0) + # check if time buckets add up to balance qty + self.assertEqual(sum([i[0] for i in queue]), 450.0) + + def test_repack_entry_same_item_overproduce(self): + """ + Under consume item and have more repacked item qty (same warehouse). + Ledger: + Item | Qty | Voucher + ------------------------ + Item 1 | 500 | 001 + Item 1 | -50 | 002 (repack) + Item 1 | 100 | 002 (repack) + + Case most likely for batch items. Test time bucket computation. + """ + sle = [ + frappe._dict( # stock up item + name="Flask Item", + actual_qty=500, qty_after_transaction=500, + warehouse="WH 1", + posting_date="2021-12-03", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-50), qty_after_transaction=450, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=100, qty_after_transaction=550, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + ] + slots = FIFOSlots(self.filters, sle).generate() + item_result = slots["Flask Item"] + queue = item_result["fifo_queue"] + + self.assertEqual(item_result["total_qty"], 550.0) + self.assertEqual(queue[0][0], 450.0) + self.assertEqual(queue[1][0], 100.0) + # check if time buckets add up to balance qty + self.assertEqual(sum([i[0] for i in queue]), 550.0) + def generate_item_and_item_wh_wise_slots(filters, sle): "Return results with and without 'show_warehouse_wise_stock'" item_wise_slots = FIFOSlots(filters, sle).generate() From f6233e77c6c2cbfeec6aeb82a73c1bbcbaa8f5da Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Feb 2022 20:30:16 +0530 Subject: [PATCH 049/447] chore: Add transfer bucket working to .md file --- .../stock_ageing/stock_ageing_fifo_logic.md | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md b/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md index 9e9bed48e3..3d759dd998 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md +++ b/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md @@ -71,4 +71,39 @@ Date | Qty | Queue 2nd | -60 | [[-10, 1-12-2021]] 3rd | +5 | [[-5, 3-12-2021]] 4th | +10 | [[5, 4-12-2021]] -4th | +20 | [[5, 4-12-2021], [20, 4-12-2021]] \ No newline at end of file +4th | +20 | [[5, 4-12-2021], [20, 4-12-2021]] + +### Concept of Transfer Qty Bucket +In the case of **Repack**, Quantity that comes in, isn't really incoming. It is just new stock repurposed from old stock, due to incoming-outgoing of the same warehouse. + +Here, stock is consumed from the FIFO Queue. It is then re-added back to the queue. +While adding stock back to the queue we need to know how much to add. +For this we need to keep track of how much was previously consumed. +Hence we use **Transfer Qty Bucket**. + +While re-adding stock, we try to add buckets that were consumed earlier (date intact), to maintain correctness. + +#### Case 1: Same Item-Warehouse in Repack +Eg: +------------------------------------------------------------------------------------- +Date | Qty | Voucher | FIFO Queue | Transfer Qty Buckets +------------------------------------------------------------------------------------- +1st | +500 | PR | [[500, 1-12-2021]] | +2nd | -50 | Repack | [[450, 1-12-2021]] | [[50, 1-12-2021]] +2nd | +50 | Repack | [[450, 1-12-2021], [50, 1-12-2021]] | [] + +- The balance at the end is restored back to 500 +- However, the initial 500 qty bucket is now split into 450 and 50, with the same date +- The net effect is the same as that before the Repack + +#### Case 2: Same Item-Warehouse in Repack with Split Consumption rows +Eg: +------------------------------------------------------------------------------------- +Date | Qty | Voucher | FIFO Queue | Transfer Qty Buckets +------------------------------------------------------------------------------------- +1st | +500 | PR | [[500, 1-12-2021]] | +2nd | -50 | Repack | [[450, 1-12-2021]] | [[50, 1-12-2021]] +2nd | -50 | Repack | [[400, 1-12-2021]] | [[50, 1-12-2021], +- | | | |[50, 1-12-2021]] +2nd | +100 | Repack | [[400, 1-12-2021], [50, 1-12-2021], | [] +- | | | [50, 1-12-2021]] | From 29c576e144489072c992e9b5bdfe4c9359639ef8 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 16 Feb 2022 12:41:39 +0530 Subject: [PATCH 050/447] chore: Remove commented out code --- .../variant_selector/item_variants_cache.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/erpnext/e_commerce/variant_selector/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py index 3aefc446c2..3107c019e6 100644 --- a/erpnext/e_commerce/variant_selector/item_variants_cache.py +++ b/erpnext/e_commerce/variant_selector/item_variants_cache.py @@ -81,25 +81,9 @@ class ItemVariantsCacheManager: ) item_variants_data = query.run() - # item_variants_data = frappe.get_all( - # 'Item Variant Attribute', - # {'variant_of': parent_item_code}, - # ['parent', 'attribute', 'attribute_value'], - # order_by='name', - # as_list=1 - # ) - - # disabled_items = set( - # [i.name for i in frappe.db.get_all('Item', {'disabled': 1})] - # ) - attribute_value_item_map = frappe._dict() item_attribute_value_map = frappe._dict() - # dont consider variants that are disabled - # pull all other variants - # item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items] - for row in item_variants_data: item_code, attribute, attribute_value = row # (attr, value) => [item1, item2] From a26183e205effa11d1fae7a3d6cb96c7db100e07 Mon Sep 17 00:00:00 2001 From: Kenneth Sequeira <33246109+kennethsequeira@users.noreply.github.com> Date: Wed, 16 Feb 2022 13:02:36 +0530 Subject: [PATCH 051/447] fix: add supported currencies (#29805) --- .../doctype/gocardless_settings/gocardless_settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py index a8119ac86c..f02f76e18b 100644 --- a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py +++ b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py @@ -13,7 +13,7 @@ from frappe.utils import call_hook_method, cint, flt, get_url class GoCardlessSettings(Document): - supported_currencies = ["EUR", "DKK", "GBP", "SEK"] + supported_currencies = ["EUR", "DKK", "GBP", "SEK", "AUD", "NZD", "CAD", "USD"] def validate(self): self.initialize_client() @@ -80,7 +80,7 @@ class GoCardlessSettings(Document): def validate_transaction_currency(self, currency): if currency not in self.supported_currencies: - frappe.throw(_("Please select another payment method. Stripe does not support transactions in currency '{0}'").format(currency)) + frappe.throw(_("Please select another payment method. Go Cardless does not support transactions in currency '{0}'").format(currency)) def get_payment_url(self, **kwargs): return get_url("./integrations/gocardless_checkout?{0}".format(urlencode(kwargs))) From 235b0715bfed6197b26cd8d611daa75e9fd7cefb Mon Sep 17 00:00:00 2001 From: Wolfram Schmidt Date: Wed, 16 Feb 2022 12:50:07 +0100 Subject: [PATCH 052/447] fix: allow renaming and merging (#29830) --- .../opportunity_lost_reason/opportunity_lost_reason.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.json b/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.json index 8a8d4252da..0cfcf0e0ea 100644 --- a/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.json +++ b/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.json @@ -3,7 +3,7 @@ "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 0, - "allow_rename": 0, + "allow_rename": 1, "autoname": "field:lost_reason", "beta": 0, "creation": "2018-12-28 14:48:51.044975", @@ -57,7 +57,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-12-28 14:49:43.336437", + "modified": "2022-02-16 10:49:43.336437", "modified_by": "Administrator", "module": "CRM", "name": "Opportunity Lost Reason", @@ -150,4 +150,4 @@ "track_changes": 0, "track_seen": 0, "track_views": 0 -} \ No newline at end of file +} From 06d36c6143dbe834ca8f8a15dc81349f270b3d7d Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 16 Feb 2022 17:40:16 +0530 Subject: [PATCH 053/447] chore: Move patch that updates SO from WO to v13 --- erpnext/patches.txt | 2 +- .../{v14_0 => v13_0}/set_work_order_qty_in_so_from_mr.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename erpnext/patches/{v14_0 => v13_0}/set_work_order_qty_in_so_from_mr.py (100%) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index c26451a30c..9f6d0f5854 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -352,4 +352,4 @@ erpnext.patches.v13_0.shopping_cart_to_ecommerce erpnext.patches.v13_0.update_disbursement_account erpnext.patches.v13_0.update_reserved_qty_closed_wo erpnext.patches.v14_0.delete_amazon_mws_doctype -erpnext.patches.v14_0.set_work_order_qty_in_so_from_mr +erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr diff --git a/erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py b/erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py similarity index 100% rename from erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py rename to erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py From 02e77029faed67ffff3e395c1de132cf15a14a03 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 16 Feb 2022 18:15:57 +0530 Subject: [PATCH 054/447] fix: added item name in the excel sheet --- .../doctype/production_plan/production_plan.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 4290ca3e4c..676481ac0f 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -588,7 +588,8 @@ def download_raw_materials(doc, warehouses=None): if isinstance(doc, str): doc = frappe._dict(json.loads(doc)) - item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM', + item_list = [['Item Code', 'Item Name', 'Description', + 'Stock UOM', 'Warehouse', 'Required Qty as per BOM', 'Projected Qty', 'Available Qty In Hand', 'Ordered Qty', 'Planned Qty', 'Reserved Qty for Production', 'Safety Stock', 'Required Qty']] @@ -597,7 +598,8 @@ def download_raw_materials(doc, warehouses=None): items = get_items_for_material_requests(doc, warehouses=warehouses, get_parent_warehouse_data=True) for d in items: - item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('warehouse'), + item_list.append([d.get('item_code'), d.get('item_name'), + d.get('description'), d.get('stock_uom'), d.get('warehouse'), d.get('required_bom_qty'), d.get('projected_qty'), d.get('actual_qty'), d.get('ordered_qty'), d.get('planned_qty'), d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')]) From 72fe5590313f82ee22421339ca780ba9d4249ed1 Mon Sep 17 00:00:00 2001 From: ChillarAnand Date: Thu, 17 Feb 2022 08:33:15 +0530 Subject: [PATCH 055/447] refactor: Remove non profit templates --- .../templates/pages/non_profit/__init__.py | 0 .../pages/non_profit/join-chapter.html | 59 ------------------- .../pages/non_profit/join_chapter.js | 12 ---- .../pages/non_profit/join_chapter.py | 23 -------- .../pages/non_profit/leave-chapter.html | 42 ------------- .../pages/non_profit/leave_chapter.py | 8 --- 6 files changed, 144 deletions(-) delete mode 100644 erpnext/templates/pages/non_profit/__init__.py delete mode 100644 erpnext/templates/pages/non_profit/join-chapter.html delete mode 100644 erpnext/templates/pages/non_profit/join_chapter.js delete mode 100644 erpnext/templates/pages/non_profit/join_chapter.py delete mode 100644 erpnext/templates/pages/non_profit/leave-chapter.html delete mode 100644 erpnext/templates/pages/non_profit/leave_chapter.py diff --git a/erpnext/templates/pages/non_profit/__init__.py b/erpnext/templates/pages/non_profit/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/templates/pages/non_profit/join-chapter.html b/erpnext/templates/pages/non_profit/join-chapter.html deleted file mode 100644 index 4923efc4e8..0000000000 --- a/erpnext/templates/pages/non_profit/join-chapter.html +++ /dev/null @@ -1,59 +0,0 @@ -{% extends "templates/web.html" %} - -{% block page_content %} - -{% macro chapter_button() %} -

- Go to Chapter Page

-{% endmacro %} -{% if frappe.session.user=='Guest' %} -

Please signup and login to join this chapter

-

Login

-{% else %} - {% if already_member %} -

You are already a member of {{ chapter.name }}!

- {{ chapter_button() }} -

Leave Chapter

- {% else %} - {% if request.method=='POST' %} -

Welcome to chapter {{ chapter.name }}!

- {{ chapter_button() }} - {% else %} -
-
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
-
-
-
- {% endif %} - {% endif %} - -{% endif %} - -{% endblock %} diff --git a/erpnext/templates/pages/non_profit/join_chapter.js b/erpnext/templates/pages/non_profit/join_chapter.js deleted file mode 100644 index e2bc8bca71..0000000000 --- a/erpnext/templates/pages/non_profit/join_chapter.js +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2017, EOSSF and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Chapter Member', { - onsubmit: function (frm) { - console.log("here" + frappe.session.user) - // body... - } - refresh: function(frm) { - - } -}); diff --git a/erpnext/templates/pages/non_profit/join_chapter.py b/erpnext/templates/pages/non_profit/join_chapter.py deleted file mode 100644 index 7caf87db2b..0000000000 --- a/erpnext/templates/pages/non_profit/join_chapter.py +++ /dev/null @@ -1,23 +0,0 @@ -import frappe - - -def get_context(context): - context.no_cache = True - chapter = frappe.get_doc('Chapter', frappe.form_dict.name) - if frappe.session.user!='Guest': - if frappe.session.user in [d.user for d in chapter.members if d.enabled == 1]: - context.already_member = True - else: - if frappe.request.method=='GET': - pass - elif frappe.request.method=='POST': - chapter.append('members', dict( - user=frappe.session.user, - introduction=frappe.form_dict.introduction, - website_url=frappe.form_dict.website_url, - enabled=1 - )) - chapter.save(ignore_permissions=1) - frappe.db.commit() - - context.chapter = chapter diff --git a/erpnext/templates/pages/non_profit/leave-chapter.html b/erpnext/templates/pages/non_profit/leave-chapter.html deleted file mode 100644 index fd7658b3b1..0000000000 --- a/erpnext/templates/pages/non_profit/leave-chapter.html +++ /dev/null @@ -1,42 +0,0 @@ -{% extends "templates/web.html" %} -{% block page_content %} - - {% if member_deleted %} -

You are not a member of {{ chapter.name }}!

-
-
-
- - -
- -
-
-

Please signup and login to join this chapter

- -

Become Member agian

- {% endif %} - -{% endblock %} diff --git a/erpnext/templates/pages/non_profit/leave_chapter.py b/erpnext/templates/pages/non_profit/leave_chapter.py deleted file mode 100644 index 65908e1dd9..0000000000 --- a/erpnext/templates/pages/non_profit/leave_chapter.py +++ /dev/null @@ -1,8 +0,0 @@ -import frappe - - -def get_context(context): - context.no_cache = True - chapter = frappe.get_doc('Chapter', frappe.form_dict.name) - context.member_deleted = True - context.chapter = chapter From b000e93744c2730517172717ed63048bab50d62f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 16 Feb 2022 20:04:45 +0530 Subject: [PATCH 056/447] fix: avoid updating items table if no change due to putaway --- .../doctype/putaway_rule/putaway_rule.py | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 523ba120de..4e472a92dc 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -9,7 +9,7 @@ from collections import defaultdict import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cint, floor, flt, nowdate +from frappe.utils import cint, cstr, floor, flt, nowdate from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.utils import get_stock_balance @@ -142,11 +142,44 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): if items_not_accomodated: show_unassigned_items_message(items_not_accomodated) - items[:] = updated_table if updated_table else items # modify items table + if updated_table and _items_changed(items, updated_table, doctype): + items[:] = updated_table + frappe.msgprint(_("Applied putaway rules."), alert=True) if sync and json.loads(sync): # sync with client side return items +def _items_changed(old, new, doctype: str) -> bool: + """ Check if any items changed by application of putaway rules. + + If not, changing item table can have side effects since `name` items also changes. + """ + if len(old) != len(new): + return True + + old = [frappe._dict(item) if isinstance(item, dict) else item for item in old] + + if doctype == "Stock Entry": + compare_keys = ("item_code", "t_warehouse", "transfer_qty", "serial_no") + sort_key = lambda item: (item.item_code, cstr(item.t_warehouse), # noqa + flt(item.transfer_qty), cstr(item.serial_no)) + else: + # purchase receipt / invoice + compare_keys = ("item_code", "warehouse", "stock_qty", "received_qty", "serial_no") + sort_key = lambda item: (item.item_code, cstr(item.warehouse), # noqa + flt(item.stock_qty), flt(item.received_qty), cstr(item.serial_no)) + + old_sorted = sorted(old, key=sort_key) + new_sorted = sorted(new, key=sort_key) + + # Once sorted by all relevant keys both tables should align if they are same. + for old_item, new_item in zip(old_sorted, new_sorted): + for key in compare_keys: + if old_item.get(key) != new_item.get(key): + return True + return False + + def get_ordered_putaway_rules(item_code, company, source_warehouse=None): """Returns an ordered list of putaway rules to apply on an item.""" filters = { From d9fc3f3d902a98dc9b1c1ab6814c66b170e18a04 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 17 Feb 2022 11:08:50 +0530 Subject: [PATCH 057/447] test: putaway rule re-application shouldn't do anything --- .../doctype/putaway_rule/test_putaway_rule.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py index bd4d811e76..ff1c19a827 100644 --- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py @@ -35,6 +35,18 @@ class TestPutawayRule(ERPNextTestCase): new_uom.uom_name = "Bag" new_uom.save() + def assertUnchangedItemsOnResave(self, doc): + """ Check if same items remain even after reapplication of rules. + + This is required since some business logic like subcontracting + depends on `name` of items to be same if item isn't changed. + """ + doc.reload() + old_items = {d.name for d in doc.items} + doc.save() + new_items = {d.name for d in doc.items} + self.assertSetEqual(old_items, new_items) + def test_putaway_rules_priority(self): """Test if rule is applied by priority, irrespective of free space.""" rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, @@ -50,6 +62,8 @@ class TestPutawayRule(ERPNextTestCase): self.assertEqual(pr.items[1].qty, 100) self.assertEqual(pr.items[1].warehouse, self.warehouse_2) + self.assertUnchangedItemsOnResave(pr) + pr.delete() rule_1.delete() rule_2.delete() @@ -162,6 +176,8 @@ class TestPutawayRule(ERPNextTestCase): # leftover space was for 500 kg (0.5 Bag) # Since Bag is a whole UOM, 1(out of 2) Bag will be unassigned + self.assertUnchangedItemsOnResave(pr) + pr.delete() rule_1.delete() rule_2.delete() @@ -196,6 +212,8 @@ class TestPutawayRule(ERPNextTestCase): self.assertEqual(pr.items[1].warehouse, self.warehouse_1) self.assertEqual(pr.items[1].putaway_rule, rule_1.name) + self.assertUnchangedItemsOnResave(pr) + pr.delete() rule_1.delete() @@ -239,6 +257,8 @@ class TestPutawayRule(ERPNextTestCase): self.assertEqual(stock_entry_item.qty, 100) # unassigned 100 out of 200 Kg self.assertEqual(stock_entry_item.putaway_rule, rule_2.name) + self.assertUnchangedItemsOnResave(stock_entry) + stock_entry.delete() rule_1.delete() rule_2.delete() @@ -294,6 +314,8 @@ class TestPutawayRule(ERPNextTestCase): self.assertEqual(stock_entry.items[2].qty, 200) self.assertEqual(stock_entry.items[2].putaway_rule, rule_2.name) + self.assertUnchangedItemsOnResave(stock_entry) + stock_entry.delete() rule_1.delete() rule_2.delete() @@ -344,6 +366,8 @@ class TestPutawayRule(ERPNextTestCase): self.assertEqual(stock_entry.items[1].serial_no, "\n".join(serial_nos[3:])) self.assertEqual(stock_entry.items[1].batch_no, "BOTTL-BATCH-1") + self.assertUnchangedItemsOnResave(stock_entry) + stock_entry.delete() pr.cancel() rule_1.delete() @@ -366,6 +390,8 @@ class TestPutawayRule(ERPNextTestCase): self.assertEqual(stock_entry_item.qty, 100) self.assertEqual(stock_entry_item.putaway_rule, rule_1.name) + self.assertUnchangedItemsOnResave(stock_entry) + stock_entry.delete() rule_1.delete() rule_2.delete() From b5ff9b27bdf762c4118974d69ef8b78b2c228348 Mon Sep 17 00:00:00 2001 From: ChillarAnand Date: Thu, 17 Feb 2022 08:33:55 +0530 Subject: [PATCH 058/447] refactor: Clean up non-profit setup --- erpnext/__init__.py | 8 ------ erpnext/patches.txt | 2 +- .../v14_0/delete_non_profit_doctypes.py | 28 ++++++++++++++----- erpnext/regional/india/setup.py | 9 ------ 4 files changed, 22 insertions(+), 25 deletions(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 0b4696c803..a44c8fabe3 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -130,11 +130,3 @@ def allow_regional(fn): return fn(*args, **kwargs) return caller - -def get_last_membership(member): - '''Returns last membership if exists''' - last_membership = frappe.get_all('Membership', 'name,to_date,membership_type', - dict(member=member, paid=1), order_by='to_date desc', limit=1) - - if last_membership: - return last_membership[0] diff --git a/erpnext/patches.txt b/erpnext/patches.txt index a8134f20fc..7bc70714a2 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -334,7 +334,6 @@ erpnext.patches.v13_0.update_asset_quantity_field erpnext.patches.v13_0.delete_bank_reconciliation_detail erpnext.patches.v13_0.enable_provisional_accounting erpnext.patches.v13_0.non_profit_deprecation_warning -erpnext.patches.v14_0.delete_non_profit_doctypes [post_model_sync] erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents @@ -351,3 +350,4 @@ erpnext.patches.v12_0.add_company_link_to_einvoice_settings erpnext.patches.v14_0.migrate_cost_center_allocations erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template erpnext.patches.v13_0.shopping_cart_to_ecommerce +erpnext.patches.v14_0.delete_non_profit_doctypes diff --git a/erpnext/patches/v14_0/delete_non_profit_doctypes.py b/erpnext/patches/v14_0/delete_non_profit_doctypes.py index 86355a6426..39f2fecf08 100644 --- a/erpnext/patches/v14_0/delete_non_profit_doctypes.py +++ b/erpnext/patches/v14_0/delete_non_profit_doctypes.py @@ -30,10 +30,24 @@ def execute(): for doctype in doctypes: frappe.delete_doc("DocType", doctype, ignore_missing=True) - custom_fields = [ - {"dt": "Member", "fieldname": "pan_number"}, - {"dt": "Donor", "fieldname": "pan_number"}, - ] - for field in custom_fields: - custom_field = frappe.db.get_value("Custom Field", field) - frappe.delete_doc("Custom Field", custom_field, ignore_missing=True) + forms = ['grant-application', 'certification-application', 'certification-application-usd'] + for form in forms: + frappe.delete_doc("Web Form", form, ignore_missing=True) + + custom_fields = { + 'Member': ['pan_number'], + 'Donor': ['pan_number'], + 'Company': [ + 'non_profit_section', 'company_80g_number', 'with_effect_from', + 'non_profit_column_break', 'pan_details' + ], + } + + for doc, fields in custom_fields.items(): + filters = { + 'dt': doc, + 'fieldname': ['in', fields] + } + records = frappe.get_all('Custom Field', filters=filters, pluck='name') + for record in records: + frappe.delete_doc('Custom Field', record, ignore_missing=True, force=True) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index c2e83045c7..8f3ef62c12 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -610,15 +610,6 @@ def get_custom_fields(): dict(fieldname='hra_column_break', fieldtype='Column Break', insert_after='hra_component'), dict(fieldname='arrear_component', label='Arrear Component', fieldtype='Link', options='Salary Component', insert_after='hra_column_break'), - dict(fieldname='non_profit_section', label='Non Profit Settings', - fieldtype='Section Break', insert_after='arrear_component', collapsible=1), - dict(fieldname='company_80g_number', label='80G Number', - fieldtype='Data', insert_after='non_profit_section'), - dict(fieldname='with_effect_from', label='80G With Effect From', - fieldtype='Date', insert_after='company_80g_number'), - dict(fieldname='non_profit_column_break', fieldtype='Column Break', insert_after='with_effect_from'), - dict(fieldname='pan_details', label='PAN Number', - fieldtype='Data', insert_after='non_profit_column_break') ], 'Employee Tax Exemption Declaration':[ dict(fieldname='hra_section', label='HRA Exemption', From 60674e52b8a08dc5785da73e9ce418fad00d836c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 17 Feb 2022 14:14:47 +0530 Subject: [PATCH 059/447] fix: currency in bank reconciliation tool --- .../bank_reconciliation_tool/bank_reconciliation_tool.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js index dbf362234e..46ba27c004 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js @@ -64,6 +64,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", { "account_currency", (r) => { frm.currency = r.account_currency; + frm.trigger("render_chart"); } ); } @@ -128,7 +129,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", { } }, - render_chart(frm) { + render_chart: frappe.utils.debounce((frm) => { frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager( { $reconciliation_tool_cards: frm.get_field( @@ -140,7 +141,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", { currency: frm.currency, } ); - }, + }, 500), render(frm) { if (frm.doc.bank_account) { From db93f26f20fe315e46324bfb36de759637f918bc Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 17 Feb 2022 14:24:52 +0530 Subject: [PATCH 060/447] fix: production plan status should consider qty + WO status --- .../production_plan/production_plan.py | 24 +++++++++++++------ .../production_plan/test_production_plan.py | 17 ++++++++++--- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index b1c86bcbf8..80003dab78 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -319,7 +319,7 @@ class ProductionPlan(Document): if self.total_produced_qty > 0: self.status = "In Process" - if self.check_have_work_orders_completed(): + if self.all_items_completed(): self.status = "Completed" if self.status != 'Completed': @@ -591,14 +591,24 @@ class ProductionPlan(Document): self.append("sub_assembly_items", data) - def check_have_work_orders_completed(self): - wo_status = frappe.db.get_list( + def all_items_completed(self): + all_items_produced = all(flt(d.planned_qty) - flt(d.produced_qty) < 0.000001 + for d in self.po_items) + if not all_items_produced: + return False + + wo_status = frappe.get_all( "Work Order", - filters={"production_plan": self.name}, + filters={ + "production_plan": self.name, + "status": ("not in", ["Closed", "Stopped"]), + "docstatus": ("<", 2), + }, fields="status", - pluck="status" + pluck="status", ) - return all(s == "Completed" for s in wo_status) + all_work_orders_completed = all(s == "Completed" for s in wo_status) + return all_work_orders_completed @frappe.whitelist() def download_raw_materials(doc, warehouses=None): @@ -1046,4 +1056,4 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0): def set_default_warehouses(row, default_warehouses): for field in ['wip_warehouse', 'fg_warehouse']: if not row.get(field): - row[field] = default_warehouses.get(field) \ No newline at end of file + row[field] = default_warehouses.get(field) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index afa1501efc..d88e10a564 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -409,9 +409,6 @@ class TestProductionPlan(ERPNextTestCase): boms = { "Assembly": { "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},}, - "SubAssembly2": {"ChildPart3": {}}, - "SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}}, - "ChildPart5": {}, "ChildPart6": {}, "SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}}, }, @@ -591,6 +588,20 @@ class TestProductionPlan(ERPNextTestCase): pln.reload() self.assertEqual(pln.po_items[0].pending_qty, 1) + def test_qty_based_status(self): + pp = frappe.new_doc("Production Plan") + pp.po_items = [ + frappe._dict(planned_qty=5, produce_qty=4) + ] + self.assertFalse(pp.all_items_completed()) + + pp.po_items = [ + frappe._dict(planned_qty=5, produce_qty=10), + frappe._dict(planned_qty=5, produce_qty=4) + ] + self.assertFalse(pp.all_items_completed()) + + def create_production_plan(**args): """ sales_order (obj): Sales Order Doc Object From fb59247182a4511fbdec106cc0ca0f68253b8579 Mon Sep 17 00:00:00 2001 From: ChillarAnand Date: Thu, 17 Feb 2022 15:15:10 +0530 Subject: [PATCH 061/447] fix: Delete party type records in patch --- erpnext/patches/v14_0/delete_non_profit_doctypes.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/patches/v14_0/delete_non_profit_doctypes.py b/erpnext/patches/v14_0/delete_non_profit_doctypes.py index 39f2fecf08..5b1cfefa4b 100644 --- a/erpnext/patches/v14_0/delete_non_profit_doctypes.py +++ b/erpnext/patches/v14_0/delete_non_profit_doctypes.py @@ -34,6 +34,13 @@ def execute(): for form in forms: frappe.delete_doc("Web Form", form, ignore_missing=True) + custom_records = [ + {"doctype": "Party Type", "name": "Member"}, + {"doctype": "Party Type", "name": "Donor"}, + ] + for record in custom_records: + frappe.delete_doc(record['doctype'], record['name'], ignore_missing=True) + custom_fields = { 'Member': ['pan_number'], 'Donor': ['pan_number'], From 274399978572b1f2e80fd2a1db2663efa544fcf7 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 17 Feb 2022 15:59:12 +0530 Subject: [PATCH 062/447] fix: coupon code is applied even if ignore_pricing_rule is enabled --- erpnext/public/js/controllers/transaction.js | 20 +++++----------- .../selling/page/point_of_sale/pos_payment.js | 23 +++++++++++++++++++ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index aa3e2f30d7..136e1edb6b 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2284,20 +2284,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe coupon_code() { var me = this; - if (this.frm.doc.coupon_code) { - frappe.run_serially([ - () => this.frm.doc.ignore_pricing_rule=1, - () => me.ignore_pricing_rule(), - () => this.frm.doc.ignore_pricing_rule=0, - () => me.apply_pricing_rule(), - () => this.frm.save() - ]); - } else { - frappe.run_serially([ - () => this.frm.doc.ignore_pricing_rule=1, - () => me.ignore_pricing_rule() - ]); - } + frappe.run_serially([ + () => this.frm.doc.ignore_pricing_rule=1, + () => me.ignore_pricing_rule(), + () => this.frm.doc.ignore_pricing_rule=0, + () => me.apply_pricing_rule() + ]); } }; diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index b9b65591dc..9650bc88a4 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -169,6 +169,29 @@ erpnext.PointOfSale.Payment = class { } }); + frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => { + if (!frm.doc.ignore_pricing_rule) { + if (frm.doc.coupon_code) { + frappe.run_serially([ + () => frm.doc.ignore_pricing_rule=1, + () => frm.trigger('ignore_pricing_rule'), + () => frm.doc.ignore_pricing_rule=0, + () => frm.trigger('apply_pricing_rule'), + () => frm.save(), + () => this.update_totals_section(frm.doc) + ]); + } else { + frappe.run_serially([ + () => frm.doc.ignore_pricing_rule=1, + () => frm.trigger('ignore_pricing_rule'), + () => frm.doc.ignore_pricing_rule=0, + () => frm.save(), + () => this.update_totals_section(frm.doc) + ]); + } + } + }); + this.setup_listener_for_payments(); this.$payment_modes.on('click', '.shortcut', function() { From 229db14b7e28d2ac0179052e7b792e06c5c9e22d Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 17 Feb 2022 15:22:36 +0530 Subject: [PATCH 063/447] ci: move some tasks to background - wkhtml download - asset building --- .github/helper/install.sh | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/helper/install.sh b/.github/helper/install.sh index eab6d50e79..859146bbcd 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -40,10 +40,14 @@ if [ "$DB" == "postgres" ];then echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres; fi -wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz -tar -xf /tmp/wkhtmltox.tar.xz -C /tmp -sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf -sudo chmod o+x /usr/local/bin/wkhtmltopdf + +install_whktml() { + wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz + tar -xf /tmp/wkhtmltox.tar.xz -C /tmp + sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf + sudo chmod o+x /usr/local/bin/wkhtmltopdf +} +install_whktml & cd ~/frappe-bench || exit @@ -57,5 +61,5 @@ bench get-app erpnext "${GITHUB_WORKSPACE}" if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi bench start &> bench_run_logs.txt & +CI=Yes bench build --app frappe & bench --site test_site reinstall --yes -bench build --app frappe From 358734dceabbd503cbd2ae3d8401fda960140a80 Mon Sep 17 00:00:00 2001 From: ChillarAnand Date: Thu, 17 Feb 2022 16:31:27 +0530 Subject: [PATCH 064/447] fix: Skip deletion if linked docs exists --- erpnext/patches/v14_0/delete_non_profit_doctypes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v14_0/delete_non_profit_doctypes.py b/erpnext/patches/v14_0/delete_non_profit_doctypes.py index 5b1cfefa4b..d53aecca92 100644 --- a/erpnext/patches/v14_0/delete_non_profit_doctypes.py +++ b/erpnext/patches/v14_0/delete_non_profit_doctypes.py @@ -39,7 +39,10 @@ def execute(): {"doctype": "Party Type", "name": "Donor"}, ] for record in custom_records: - frappe.delete_doc(record['doctype'], record['name'], ignore_missing=True) + try: + frappe.delete_doc(record['doctype'], record['name'], ignore_missing=True) + except frappe.LinkExistsError: + pass custom_fields = { 'Member': ['pan_number'], From e2e998fbd9baa6015bc9c376dd5b6db7ae6cae49 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 17 Feb 2022 12:00:19 +0530 Subject: [PATCH 065/447] fix(Timesheet): convert time logs to datetime while checking for overlap --- .../projects/doctype/timesheet/timesheet.py | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index dd0b5f90f4..fa0411e0f8 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -7,7 +7,7 @@ import json import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import add_to_date, flt, getdate, time_diff_in_hours +from frappe.utils import add_to_date, flt, get_datetime, getdate, time_diff_in_hours from erpnext.controllers.queries import get_match_cond from erpnext.hr.utils import validate_active_employee @@ -145,7 +145,7 @@ class Timesheet(Document): if not (data.from_time and data.hours): return - _to_time = add_to_date(data.from_time, hours=data.hours, as_datetime=True) + _to_time = get_datetime(add_to_date(data.from_time, hours=data.hours, as_datetime=True)) if data.to_time != _to_time: data.to_time = _to_time @@ -186,24 +186,37 @@ class Timesheet(Document): and ts.docstatus < 2""".format(cond), { "val": value, - "from_time": args.from_time, - "to_time": args.to_time, + "from_time": get_datetime(args.from_time), + "to_time": get_datetime(args.to_time), "name": args.name or "No Name", "parent": args.parent or "No Name" }, as_dict=True) - # check internal overlap - for time_log in self.time_logs: - if not (time_log.from_time and time_log.to_time - and args.from_time and args.to_time): continue - if (fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and \ - args.idx != time_log.idx and ((args.from_time > time_log.from_time and args.from_time < time_log.to_time) or - (args.to_time > time_log.from_time and args.to_time < time_log.to_time) or - (args.from_time <= time_log.from_time and args.to_time >= time_log.to_time)): - return self + if self.check_internal_overlap(fieldname, args): + return self return existing[0] if existing else None + def check_internal_overlap(self, fieldname, args): + for time_log in self.time_logs: + if not (time_log.from_time and time_log.to_time + and args.from_time and args.to_time): + continue + + from_time = get_datetime(time_log.from_time) + to_time = get_datetime(time_log.to_time) + args_from_time = get_datetime(args.from_time) + args_to_time = get_datetime(args.to_time) + + if (fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and \ + args.idx != time_log.idx and ( + (args_from_time > from_time and args_from_time < to_time) + or (args_to_time > from_time and args_to_time < to_time) + or (args_from_time <= from_time and args_to_time >= to_time) + ): + return True + return False + def update_cost(self): for data in self.time_logs: if data.activity_type or data.is_billable: From 3ec9acf8f7c8fd08e5709ac0f352728f6a9d6cfa Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 17 Feb 2022 14:39:17 +0530 Subject: [PATCH 066/447] fix: convert overlap raw query to frappe.qb --- .../projects/doctype/timesheet/timesheet.py | 52 +++++++++++-------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index fa0411e0f8..c43be8cbd8 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -171,26 +171,35 @@ class Timesheet(Document): .format(args.idx, self.name, existing.name), OverlapError) def get_overlap_for(self, fieldname, args, value): - cond = "ts.`{0}`".format(fieldname) - if fieldname == 'workstation': - cond = "tsd.`{0}`".format(fieldname) + timesheet = frappe.qb.DocType("Timesheet") + timelog = frappe.qb.DocType("Timesheet Detail") - existing = frappe.db.sql("""select ts.name as name, tsd.from_time as from_time, tsd.to_time as to_time from - `tabTimesheet Detail` tsd, `tabTimesheet` ts where {0}=%(val)s and tsd.parent = ts.name and - ( - (%(from_time)s > tsd.from_time and %(from_time)s < tsd.to_time) or - (%(to_time)s > tsd.from_time and %(to_time)s < tsd.to_time) or - (%(from_time)s <= tsd.from_time and %(to_time)s >= tsd.to_time)) - and tsd.name!=%(name)s - and ts.name!=%(parent)s - and ts.docstatus < 2""".format(cond), - { - "val": value, - "from_time": get_datetime(args.from_time), - "to_time": get_datetime(args.to_time), - "name": args.name or "No Name", - "parent": args.parent or "No Name" - }, as_dict=True) + from_time = get_datetime(args.from_time) + to_time = get_datetime(args.to_time) + + query = ( + frappe.qb.from_(timesheet) + .join(timelog) + .on(timelog.parent == timesheet.name) + .select(timesheet.name.as_('name'), timelog.from_time.as_('from_time'), timelog.to_time.as_('to_time')) + .where( + (timelog.name != (args.name or "No Name")) + & (timesheet.name != (args.parent or "No Name")) + & (timesheet.docstatus < 2) + & ( + ((from_time > timelog.from_time) & (from_time < timelog.to_time)) + | ((to_time > timelog.from_time) & (to_time < timelog.to_time)) + | ((from_time <= timelog.from_time) & (to_time >= timelog.to_time)) + ) + ) + ) + + if fieldname == "workstation": + query = query.where(timelog[fieldname] == value) + else: + query = query.where(timesheet[fieldname] == value) + + existing = query.run(as_dict=True) if self.check_internal_overlap(fieldname, args): return self @@ -208,12 +217,13 @@ class Timesheet(Document): args_from_time = get_datetime(args.from_time) args_to_time = get_datetime(args.to_time) - if (fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and \ + if ((fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and args.idx != time_log.idx and ( (args_from_time > from_time and args_from_time < to_time) or (args_to_time > from_time and args_to_time < to_time) or (args_from_time <= from_time and args_to_time >= to_time) - ): + ) + ): return True return False From 47ff968253ff7c4e7ca4e7769ccc29d93a8f71f2 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 17 Feb 2022 14:39:26 +0530 Subject: [PATCH 067/447] test: timesheet not overlapping with continuous timelogs --- .../doctype/timesheet/test_timesheet.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index 989bcd1670..8b60357021 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -151,6 +151,35 @@ class TestTimesheet(unittest.TestCase): settings.ignore_employee_time_overlap = initial_setting settings.save() + def test_timesheet_not_overlapping_with_continuous_timelogs(self): + emp = make_employee("test_employee_6@salary.com") + + update_activity_type("_Test Activity Type") + timesheet = frappe.new_doc("Timesheet") + timesheet.employee = emp + timesheet.append( + 'time_logs', + { + "billable": 1, + "activity_type": "_Test Activity Type", + "from_time": now_datetime(), + "to_time": now_datetime() + datetime.timedelta(hours=3), + "company": "_Test Company" + } + ) + timesheet.append( + 'time_logs', + { + "billable": 1, + "activity_type": "_Test Activity Type", + "from_time": now_datetime() + datetime.timedelta(hours=3), + "to_time": now_datetime() + datetime.timedelta(hours=4), + "company": "_Test Company" + } + ) + + timesheet.save() # should not throw an error + def test_to_time(self): emp = make_employee("test_employee_6@salary.com") from_time = now_datetime() From bef46e2b645f17eca8c1cd6ebe74e2845f6ea64f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 17 Feb 2022 16:59:14 +0530 Subject: [PATCH 068/447] chore: remove unused code and fields related to workstation from Timesheet Detail --- .../projects/doctype/timesheet/timesheet.py | 22 +++------ .../timesheet_detail/timesheet_detail.json | 48 ++----------------- 2 files changed, 10 insertions(+), 60 deletions(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index c43be8cbd8..b44d501743 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -177,7 +177,7 @@ class Timesheet(Document): from_time = get_datetime(args.from_time) to_time = get_datetime(args.to_time) - query = ( + existing = ( frappe.qb.from_(timesheet) .join(timelog) .on(timelog.parent == timesheet.name) @@ -186,20 +186,14 @@ class Timesheet(Document): (timelog.name != (args.name or "No Name")) & (timesheet.name != (args.parent or "No Name")) & (timesheet.docstatus < 2) + & (timesheet[fieldname] == value) & ( ((from_time > timelog.from_time) & (from_time < timelog.to_time)) | ((to_time > timelog.from_time) & (to_time < timelog.to_time)) | ((from_time <= timelog.from_time) & (to_time >= timelog.to_time)) ) ) - ) - - if fieldname == "workstation": - query = query.where(timelog[fieldname] == value) - else: - query = query.where(timesheet[fieldname] == value) - - existing = query.run(as_dict=True) + ).run(as_dict=True) if self.check_internal_overlap(fieldname, args): return self @@ -217,12 +211,10 @@ class Timesheet(Document): args_from_time = get_datetime(args.from_time) args_to_time = get_datetime(args.to_time) - if ((fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and - args.idx != time_log.idx and ( - (args_from_time > from_time and args_from_time < to_time) - or (args_to_time > from_time and args_to_time < to_time) - or (args_from_time <= from_time and args_to_time >= to_time) - ) + if (args.get(fieldname) == time_log.get(fieldname)) and (args.idx != time_log.idx) and ( + (args_from_time > from_time and args_from_time < to_time) + or (args_to_time > from_time and args_to_time < to_time) + or (args_from_time <= from_time and args_to_time >= to_time) ): return True return False diff --git a/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json b/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json index ee04c612c9..90fdb83331 100644 --- a/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json +++ b/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json @@ -14,12 +14,6 @@ "to_time", "hours", "completed", - "section_break_7", - "completed_qty", - "workstation", - "column_break_12", - "operation", - "operation_id", "project_details", "project", "project_name", @@ -83,43 +77,6 @@ "fieldtype": "Check", "label": "Completed" }, - { - "fieldname": "section_break_7", - "fieldtype": "Section Break" - }, - { - "depends_on": "eval:parent.work_order", - "fieldname": "completed_qty", - "fieldtype": "Float", - "label": "Completed Qty" - }, - { - "depends_on": "eval:parent.work_order", - "fieldname": "workstation", - "fieldtype": "Link", - "label": "Workstation", - "options": "Workstation", - "read_only": 1 - }, - { - "fieldname": "column_break_12", - "fieldtype": "Column Break" - }, - { - "depends_on": "eval:parent.work_order", - "fieldname": "operation", - "fieldtype": "Link", - "label": "Operation", - "options": "Operation", - "read_only": 1 - }, - { - "depends_on": "eval:parent.work_order", - "fieldname": "operation_id", - "fieldtype": "Data", - "hidden": 1, - "label": "Operation Id" - }, { "fieldname": "project_details", "fieldtype": "Section Break" @@ -267,7 +224,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-05-18 12:19:33.205940", + "modified": "2022-02-17 16:53:34.878798", "modified_by": "Administrator", "module": "Projects", "name": "Timesheet Detail", @@ -275,5 +232,6 @@ "permissions": [], "quick_entry": 1, "sort_field": "modified", - "sort_order": "ASC" + "sort_order": "ASC", + "states": [] } \ No newline at end of file From 555b1335f65cca4f77c28294e153002a39e114a4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Feb 2022 19:15:30 +0530 Subject: [PATCH 069/447] feat: Bank Reconciliation for loan documents --- .../bank_reconciliation_tool.py | 73 ++++++++++++++++++- .../loan_disbursement/loan_disbursement.json | 47 ++++++++++-- .../loan_disbursement/loan_disbursement.py | 12 +-- .../loan_repayment/loan_repayment.json | 52 ++++++++++++- .../doctype/loan_repayment/loan_repayment.py | 24 +++--- .../dialog_manager.js | 17 ++++- 6 files changed, 190 insertions(+), 35 deletions(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index 4211bd0169..26078d6329 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -275,6 +275,10 @@ def check_matching(bank_account, company, transaction, document_types): } matching_vouchers = [] + + matching_vouchers.extend(get_loan_vouchers(bank_account, transaction, + document_types, filters)) + for query in subquery: matching_vouchers.extend( frappe.db.sql(query, filters,) @@ -311,6 +315,74 @@ def get_queries(bank_account, company, transaction, document_types): return queries +def get_loan_vouchers(bank_account, transaction, document_types, filters): + vouchers = [] + amount_condition = True if "exact_match" in document_types else False + + if transaction.withdrawal > 0 and "loan_disbursement" in document_types: + vouchers.append(get_ld_matching_query(bank_account, amount_condition, filters)) + + if transaction.deposit > 0 and "loan_repayment" in document_types: + vouchers.append(get_lr_matching_query(bank_account, amount_condition, filters)) + +def get_ld_matching_query(bank_account, amount_condition, filters): + loan_disbursement = frappe.qb.DocType("Loan Disbursement") + query = frappe.qb.from_(loan_disbursement).select( + loan_disbursement.name, + loan_disbursement.disbursed_amount, + loan_disbursement.reference_number, + loan_disbursement.reference_date, + loan_disbursement.applicant_type, + loan_disbursement.disbursement_date + ).where( + loan_disbursement.docstatus == 1 + ).where( + loan_disbursement.clearance_date.isnull() + ).where( + loan_disbursement.disbursement_account == bank_account + ) + + if amount_condition: + query.where( + loan_disbursement.disbursed_amount == filters.get('amount') + ) + else: + query.where( + loan_disbursement.disbursed_amount <= filters.get('amount') + ) + + vouchers = query.run(as_dict=1) + return vouchers + +def get_lr_matching_query(bank_account, amount_condition, filters): + loan_repayment = frappe.qb.DocType("Loan Repayment") + query = frappe.qb.from_(loan_repayment).select( + loan_repayment.name, + loan_repayment.paid_amount, + loan_repayment.reference_number, + loan_repayment.reference_date, + loan_repayment.applicant_type, + loan_repayment.posting_date + ).where( + loan_repayment.docstatus == 1 + ).where( + loan_repayment.clearance_date.isnull() + ).where( + loan_repayment.disbursement_account == bank_account + ) + + if amount_condition: + query.where( + loan_repayment.paid_amount == filters.get('amount') + ) + else: + query.where( + loan_repayment.paid_amount <= filters.get('amount') + ) + + vouchers = query.run(as_dict=1) + return vouchers + def get_pe_matching_query(amount_condition, account_from_to, transaction): # get matching payment entries query if transaction.deposit > 0: @@ -348,7 +420,6 @@ def get_je_matching_query(amount_condition, transaction): # We have mapping at the bank level # So one bank could have both types of bank accounts like asset and liability # So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type - company_account = frappe.get_value("Bank Account", transaction.bank_account, "account") cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit" return f""" diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json index 7811d56a75..50926d7726 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json @@ -14,11 +14,15 @@ "applicant", "section_break_7", "disbursement_date", + "clearance_date", "column_break_8", "disbursed_amount", "accounting_dimensions_section", "cost_center", - "customer_details_section", + "accounting_details", + "disbursement_account", + "column_break_16", + "loan_account", "bank_account", "disbursement_references_section", "reference_date", @@ -106,11 +110,6 @@ "fieldtype": "Section Break", "label": "Disbursement Details" }, - { - "fieldname": "customer_details_section", - "fieldtype": "Section Break", - "label": "Customer Details" - }, { "fetch_from": "against_loan.applicant_type", "fieldname": "applicant_type", @@ -149,15 +148,48 @@ "fieldname": "reference_number", "fieldtype": "Data", "label": "Reference Number" + }, + { + "fieldname": "clearance_date", + "fieldtype": "Date", + "label": "Clearance Date", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "accounting_details", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fetch_from": "against_loan.disbursement_account", + "fieldname": "disbursement_account", + "fieldtype": "Link", + "label": "Disbursement Account", + "options": "Account", + "read_only": 1 + }, + { + "fieldname": "column_break_16", + "fieldtype": "Column Break" + }, + { + "fetch_from": "against_loan.loan_account", + "fieldname": "loan_account", + "fieldtype": "Link", + "label": "Loan Account", + "options": "Account", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-04-19 18:09:32.175355", + "modified": "2022-02-17 18:23:44.157598", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Disbursement", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -194,5 +226,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index df3aadfb18..54a03b92b5 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -42,9 +42,6 @@ class LoanDisbursement(AccountsController): if not self.posting_date: self.posting_date = self.disbursement_date or nowdate() - if not self.bank_account and self.applicant_type == "Customer": - self.bank_account = frappe.db.get_value("Customer", self.applicant, "default_bank_account") - def validate_disbursal_amount(self): possible_disbursal_amount = get_disbursal_amount(self.against_loan) @@ -117,12 +114,11 @@ class LoanDisbursement(AccountsController): def make_gl_entries(self, cancel=0, adv_adj=0): gle_map = [] - loan_details = frappe.get_doc("Loan", self.against_loan) gle_map.append( self.get_gl_dict({ - "account": loan_details.loan_account, - "against": loan_details.disbursement_account, + "account": self.loan_account, + "against": self.disbursement_account, "debit": self.disbursed_amount, "debit_in_account_currency": self.disbursed_amount, "against_voucher_type": "Loan", @@ -137,8 +133,8 @@ class LoanDisbursement(AccountsController): gle_map.append( self.get_gl_dict({ - "account": loan_details.disbursement_account, - "against": loan_details.loan_account, + "account": self.disbursement_account, + "against": self.loan_account, "credit": self.disbursed_amount, "credit_in_account_currency": self.disbursed_amount, "against_voucher_type": "Loan", diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json index 93ef217042..766602de86 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json @@ -1,7 +1,7 @@ { "actions": [], "autoname": "LM-REP-.####", - "creation": "2019-09-03 14:44:39.977266", + "creation": "2022-01-25 10:30:02.767941", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -13,6 +13,7 @@ "column_break_3", "company", "posting_date", + "clearance_date", "rate_of_interest", "payroll_payable_account", "is_term_loan", @@ -37,7 +38,12 @@ "total_penalty_paid", "total_interest_paid", "repayment_details", - "amended_from" + "amended_from", + "accounting_details_section", + "repayment_account", + "penalty_income_account", + "column_break_36", + "loan_account" ], "fields": [ { @@ -260,12 +266,52 @@ "fieldname": "repay_from_salary", "fieldtype": "Check", "label": "Repay From Salary" + }, + { + "fieldname": "clearance_date", + "fieldtype": "Date", + "label": "Clearance Date", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "accounting_details_section", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fetch_from": "against_loan.payment_account", + "fieldname": "repayment_account", + "fieldtype": "Link", + "label": "Repayment Account", + "options": "Account", + "read_only": 1 + }, + { + "fieldname": "column_break_36", + "fieldtype": "Column Break" + }, + { + "fetch_from": "against_loan.loan_account", + "fieldname": "loan_account", + "fieldtype": "Link", + "label": "Loan Account", + "options": "Account", + "read_only": 1 + }, + { + "fetch_from": "against_loan.penalty_income_account", + "fieldname": "penalty_income_account", + "fieldtype": "Link", + "hidden": 1, + "label": "Penalty Income Account", + "options": "Account" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-01-06 01:51:06.707782", + "modified": "2022-02-17 19:10:07.742298", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment", diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index f3ed611255..67c2b1ee14 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -310,7 +310,6 @@ class LoanRepayment(AccountsController): def make_gl_entries(self, cancel=0, adv_adj=0): gle_map = [] - loan_details = frappe.get_doc("Loan", self.against_loan) if self.shortfall_amount and self.amount_paid > self.shortfall_amount: remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format(self.shortfall_amount, @@ -323,13 +322,13 @@ class LoanRepayment(AccountsController): if self.repay_from_salary: payment_account = self.payroll_payable_account else: - payment_account = loan_details.payment_account + payment_account = self.payment_account if self.total_penalty_paid: gle_map.append( self.get_gl_dict({ - "account": loan_details.loan_account, - "against": loan_details.payment_account, + "account": self.loan_account, + "against": payment_account, "debit": self.total_penalty_paid, "debit_in_account_currency": self.total_penalty_paid, "against_voucher_type": "Loan", @@ -344,8 +343,8 @@ class LoanRepayment(AccountsController): gle_map.append( self.get_gl_dict({ - "account": loan_details.penalty_income_account, - "against": loan_details.loan_account, + "account": self.penalty_income_account, + "against": self.loan_account, "credit": self.total_penalty_paid, "credit_in_account_currency": self.total_penalty_paid, "against_voucher_type": "Loan", @@ -359,8 +358,7 @@ class LoanRepayment(AccountsController): gle_map.append( self.get_gl_dict({ "account": payment_account, - "against": loan_details.loan_account + ", " + loan_details.interest_income_account - + ", " + loan_details.penalty_income_account, + "against": self.loan_account + ", " + self.penalty_income_account, "debit": self.amount_paid, "debit_in_account_currency": self.amount_paid, "against_voucher_type": "Loan", @@ -368,16 +366,16 @@ class LoanRepayment(AccountsController): "remarks": remarks, "cost_center": self.cost_center, "posting_date": getdate(self.posting_date), - "party_type": loan_details.applicant_type if self.repay_from_salary else '', - "party": loan_details.applicant if self.repay_from_salary else '' + "party_type": self.applicant_type if self.repay_from_salary else '', + "party": self.applicant if self.repay_from_salary else '' }) ) gle_map.append( self.get_gl_dict({ - "account": loan_details.loan_account, - "party_type": loan_details.applicant_type, - "party": loan_details.applicant, + "account": self.loan_account, + "party_type": self.applicant_type, + "party": self.applicant, "against": payment_account, "credit": self.amount_paid, "credit_in_account_currency": self.amount_paid, diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js index ca73393c54..214a1be134 100644 --- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js +++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js @@ -181,6 +181,12 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { fieldname: "journal_entry", onchange: () => this.update_options(), }, + { + fieldtype: "Check", + label: "Loan Repayment", + fieldname: "loan_repayment", + onchange: () => this.update_options(), + }, { fieldname: "column_break_5", fieldtype: "Column Break", @@ -191,13 +197,18 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { fieldname: "sales_invoice", onchange: () => this.update_options(), }, - { fieldtype: "Check", label: "Purchase Invoice", fieldname: "purchase_invoice", onchange: () => this.update_options(), }, + { + fieldtype: "Check", + label: "Show Only Exact Amount", + fieldname: "exact_match", + onchange: () => this.update_options(), + }, { fieldname: "column_break_5", fieldtype: "Column Break", @@ -210,8 +221,8 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { }, { fieldtype: "Check", - label: "Show Only Exact Amount", - fieldname: "exact_match", + label: "Loan Disbursement", + fieldname: "loan_disbursement", onchange: () => this.update_options(), }, { From c36bd7e1a6fe48c5fff4765e843571a0d6560dd1 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 17 Feb 2022 19:25:00 +0530 Subject: [PATCH 070/447] fix: avoid creating bins without item-wh Co-Authored-By: Shadrak Gurupnor <30501401+shadrak98@users.noreply.github.com> Co-Authored-By: Saurabh --- erpnext/controllers/accounts_controller.py | 3 ++- erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py | 2 ++ erpnext/patches/v4_2/repost_reserved_qty.py | 8 +++++--- erpnext/patches/v4_2/update_requested_and_ordered_qty.py | 2 ++ 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 994b903b32..d05787fdfb 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1955,7 +1955,8 @@ def update_bin_on_delete(row, doctype): qty_dict["ordered_qty"] = get_ordered_qty(row.item_code, row.warehouse) - update_bin_qty(row.item_code, row.warehouse, qty_dict) + if row.warehouse: + update_bin_qty(row.item_code, row.warehouse, qty_dict) def validate_and_delete_children(parent, data): deleted_children = [] diff --git a/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py b/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py index 9b083cafb3..8dec9ff381 100644 --- a/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py +++ b/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py @@ -9,6 +9,8 @@ def execute(): FROM `tabBin`""",as_dict=1) for entry in bin_details: + if not (entry.item_code and entry.warehouse): + continue update_bin_qty(entry.get("item_code"), entry.get("warehouse"), { "indented_qty": get_indented_qty(entry.get("item_code"), entry.get("warehouse")) }) diff --git a/erpnext/patches/v4_2/repost_reserved_qty.py b/erpnext/patches/v4_2/repost_reserved_qty.py index c2ca9be64a..ed4b19d07d 100644 --- a/erpnext/patches/v4_2/repost_reserved_qty.py +++ b/erpnext/patches/v4_2/repost_reserved_qty.py @@ -29,9 +29,11 @@ def execute(): """) for item_code, warehouse in repost_for: - update_bin_qty(item_code, warehouse, { - "reserved_qty": get_reserved_qty(item_code, warehouse) - }) + if not (item_code and warehouse): + continue + update_bin_qty(item_code, warehouse, { + "reserved_qty": get_reserved_qty(item_code, warehouse) + }) frappe.db.sql("""delete from tabBin where exists( diff --git a/erpnext/patches/v4_2/update_requested_and_ordered_qty.py b/erpnext/patches/v4_2/update_requested_and_ordered_qty.py index 42b0b04076..dd79410ba5 100644 --- a/erpnext/patches/v4_2/update_requested_and_ordered_qty.py +++ b/erpnext/patches/v4_2/update_requested_and_ordered_qty.py @@ -14,6 +14,8 @@ def execute(): union select item_code, warehouse from `tabStock Ledger Entry`) a"""): try: + if not (item_code and warehouse): + continue count += 1 update_bin_qty(item_code, warehouse, { "indented_qty": get_indented_qty(item_code, warehouse), From 87b074ac0966ab26bf776c720fcb96b92a451d55 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Feb 2022 22:01:00 +0530 Subject: [PATCH 071/447] fix: GSTIN filter for GSTR-1 report --- erpnext/regional/report/gstr_1/gstr_1.js | 23 ++++++++++++++++++++--- erpnext/regional/report/gstr_1/gstr_1.py | 23 ++++++++++++++++++++++- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.js b/erpnext/regional/report/gstr_1/gstr_1.js index 4b98978f13..1766fdb2ec 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.js +++ b/erpnext/regional/report/gstr_1/gstr_1.js @@ -17,7 +17,7 @@ frappe.query_reports["GSTR-1"] = { "fieldtype": "Link", "options": "Address", "get_query": function () { - var company = frappe.query_report.get_filter_value('company'); + let company = frappe.query_report.get_filter_value('company'); if (company) { return { "query": 'frappe.contacts.doctype.address.address.address_query', @@ -26,6 +26,11 @@ frappe.query_reports["GSTR-1"] = { } } }, + { + "fieldname": "company_gstin", + "label": __("Company GSTIN"), + "fieldtype": "Select" + }, { "fieldname": "from_date", "label": __("From Date"), @@ -60,10 +65,22 @@ frappe.query_reports["GSTR-1"] = { } ], onload: function (report) { + let filters = report.get_values(); + + frappe.call({ + method: 'erpnext.regional.report.gstr_1.gstr_1.get_company_gstins', + args: { + company: filters.company + }, + callback: function(r) { + console.log(r.message); + frappe.query_report.page.fields_dict.company_gstin.df.options = r.message; + frappe.query_report.page.fields_dict.company_gstin.refresh(); + } + }); + report.page.add_inner_button(__("Download as JSON"), function () { - var filters = report.get_values(); - frappe.call({ method: 'erpnext.regional.report.gstr_1.gstr_1.get_json', args: { diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index ce2ffb4010..8fcb6bb444 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -253,7 +253,8 @@ class Gstr1Report(object): for opts in (("company", " and company=%(company)s"), ("from_date", " and posting_date>=%(from_date)s"), ("to_date", " and posting_date<=%(to_date)s"), - ("company_address", " and company_address=%(company_address)s")): + ("company_address", " and company_address=%(company_address)s"), + ("company_gstin", " and company_gstin=%(company_gstin)s")): if self.filters.get(opts[0]): conditions += opts[1] @@ -1192,3 +1193,23 @@ def is_inter_state(invoice_detail): return True else: return False + + +@frappe.whitelist() +def get_company_gstins(company): + address = frappe.qb.DocType("Address") + links = frappe.qb.DocType("Dynamic Link") + + addresses = frappe.qb.from_(address).inner_join(links).on( + address.name == links.parent + ).select( + address.gstin + ).where( + links.link_doctype == 'Company' + ).where( + links.link_name == company + ).run(as_dict=1) + + address_list = [''] + [d.gstin for d in addresses] + + return address_list \ No newline at end of file From 1617e0d0e6d7f8f3cbffab4edaf388b5aa6db4b4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Feb 2022 22:52:53 +0530 Subject: [PATCH 072/447] fix: Remove reload doc --- erpnext/patches/v14_0/update_opportunity_currency_fields.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/patches/v14_0/update_opportunity_currency_fields.py b/erpnext/patches/v14_0/update_opportunity_currency_fields.py index 82213fff6c..75049a6e8a 100644 --- a/erpnext/patches/v14_0/update_opportunity_currency_fields.py +++ b/erpnext/patches/v14_0/update_opportunity_currency_fields.py @@ -6,9 +6,6 @@ from erpnext.setup.utils import get_exchange_rate def execute(): - frappe.reload_doc('crm', 'doctype', 'opportunity', force=True) - frappe.reload_doc('crm', 'doctype', 'opportunity_item', force=True) - opportunities = frappe.db.get_list('Opportunity', filters={ 'opportunity_amount': ['>', 0] }, fields=['name', 'company', 'currency', 'opportunity_amount']) From 3a966d4dbe3cd868bcb01d4951b236cad154605c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Feb 2022 23:18:07 +0530 Subject: [PATCH 073/447] fix: Move patch to post sync --- erpnext/patches.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d300340671..33366867f2 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -329,7 +329,6 @@ execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings' erpnext.patches.v14_0.set_payroll_cost_centers erpnext.patches.v13_0.agriculture_deprecation_warning erpnext.patches.v13_0.hospitality_deprecation_warning -erpnext.patches.v13_0.update_exchange_rate_settings erpnext.patches.v13_0.update_asset_quantity_field erpnext.patches.v13_0.delete_bank_reconciliation_detail erpnext.patches.v13_0.enable_provisional_accounting @@ -351,3 +350,4 @@ erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template erpnext.patches.v13_0.shopping_cart_to_ecommerce erpnext.patches.v13_0.update_disbursement_account erpnext.patches.v13_0.update_reserved_qty_closed_wo +erpnext.patches.v13_0.update_exchange_rate_settings From d3fbbcfed39570fbad52a77b2533c2b72da8679f Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 17 Feb 2022 14:30:00 +0530 Subject: [PATCH 074/447] fix: Precision of available qty and negative stock in transfer bucket - Maintain only positive values in transfer bucket - Use it to neutralize/add stock to fifo queue --- .../stock/report/stock_ageing/stock_ageing.py | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 9866e63fb5..60f9e959c8 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -28,6 +28,7 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li "Returns ordered, formatted data with ranges." _func = itemgetter(1) data = [] + precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) for item, item_dict in item_details.items(): earliest_age, latest_age = 0, 0 @@ -48,10 +49,13 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li if filters.get("show_warehouse_wise_stock"): row.append(details.warehouse) - row.extend([item_dict.get("total_qty"), average_age, + row.extend([ + flt(item_dict.get("total_qty"), precision), + average_age, range1, range2, range3, above_range3, earliest_age, latest_age, - details.stock_uom]) + details.stock_uom + ]) data.append(row) @@ -288,13 +292,14 @@ class FIFOSlots: transfer_data = self.transferred_item_details.get(transfer_key) if transfer_data: - # [Repack] inward/outward from same voucher, item & warehouse + # inward/outward from same voucher, item & warehouse + # eg: Repack with same item, Stock reco for batch item # consume transfer data and add stock to fifo queue self.__adjust_incoming_transfer_qty(transfer_data, fifo_queue, row) else: if not serial_nos: - if fifo_queue and flt(fifo_queue[0][0]) < 0: - # neutralize negative stock by adding positive stock + if fifo_queue and flt(fifo_queue[0][0]) <= 0: + # neutralize 0/negative stock by adding positive stock fifo_queue[0][0] += flt(row.actual_qty) fifo_queue[0][1] = row.posting_date else: @@ -325,7 +330,7 @@ class FIFOSlots: elif not fifo_queue: # negative stock, no balance but qty yet to consume fifo_queue.append([-(qty_to_pop), row.posting_date]) - self.transferred_item_details[transfer_key].append([row.actual_qty, row.posting_date]) + self.transferred_item_details[transfer_key].append([qty_to_pop, row.posting_date]) qty_to_pop = 0 else: # qty to pop < slot qty, ample balance @@ -337,22 +342,28 @@ class FIFOSlots: def __adjust_incoming_transfer_qty(self, transfer_data: Dict, fifo_queue: List, row: Dict): "Add previously removed stock back to FIFO Queue." transfer_qty_to_pop = flt(row.actual_qty) - first_bucket_qty = transfer_data[0][0] - first_bucket_date = transfer_data[0][1] + + def add_to_fifo_queue(slot): + if fifo_queue and flt(fifo_queue[0][0]) <= 0: + # neutralize 0/negative stock by adding positive stock + fifo_queue[0][0] += flt(slot[0]) + fifo_queue[0][1] = slot[1] + else: + fifo_queue.append(slot) while transfer_qty_to_pop: - if transfer_data and 0 > first_bucket_qty <= transfer_qty_to_pop: + if transfer_data and 0 < transfer_data[0][0] <= transfer_qty_to_pop: # bucket qty is not enough, consume whole - transfer_qty_to_pop -= first_bucket_qty - slot = transfer_data.pop(0) - fifo_queue.append(slot) + transfer_qty_to_pop -= transfer_data[0][0] + add_to_fifo_queue(transfer_data.pop(0)) elif not transfer_data: # transfer bucket is empty, extra incoming qty - fifo_queue.append([transfer_qty_to_pop, row.posting_date]) + add_to_fifo_queue([transfer_qty_to_pop, row.posting_date]) + transfer_qty_to_pop = 0 else: # ample bucket qty to consume - first_bucket_qty -= transfer_qty_to_pop - fifo_queue.append([transfer_qty_to_pop, first_bucket_date]) + transfer_data[0][0] -= transfer_qty_to_pop + add_to_fifo_queue([transfer_qty_to_pop, transfer_data[0][1]]) transfer_qty_to_pop = 0 def __update_balances(self, row: Dict, key: Union[Tuple, str]): From ed4a6c6cc63ca37a6033f9f87c35cd26aaa2cb43 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 18 Feb 2022 18:52:42 +0530 Subject: [PATCH 075/447] fix: Range Qty precision --- erpnext/stock/report/stock_ageing/stock_ageing.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 60f9e959c8..97a740e184 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -12,6 +12,7 @@ from frappe.utils import cint, date_diff, flt from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos Filters = frappe._dict +precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) def execute(filters: Filters = None) -> Tuple: to_date = filters["to_date"] @@ -28,7 +29,6 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li "Returns ordered, formatted data with ranges." _func = itemgetter(1) data = [] - precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) for item, item_dict in item_details.items(): earliest_age, latest_age = 0, 0 @@ -83,13 +83,13 @@ def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: D qty = flt(item[0]) if not item_dict["has_serial_no"] else 1.0 if age <= filters.range1: - range1 += qty + range1 = flt(range1 + qty, precision) elif age <= filters.range2: - range2 += qty + range2 = flt(range2 + qty, precision) elif age <= filters.range3: - range3 += qty + range3 = flt(range3 + qty, precision) else: - above_range3 += qty + above_range3 = flt(above_range3 + qty, precision) return range1, range2, range3, above_range3 From d5be536740642d0bef9ea23151a41ce2657b9cd2 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 18 Feb 2022 18:53:05 +0530 Subject: [PATCH 076/447] test: Negative Stock, over consumption & over production with split rows, balance precision --- .../report/stock_ageing/test_stock_ageing.py | 221 +++++++++++++++++- 1 file changed, 217 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index 3055332540..3fc357e8d4 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -3,7 +3,7 @@ import frappe -from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots +from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, format_report_data from erpnext.tests.utils import ERPNextTestCase @@ -11,7 +11,8 @@ class TestStockAgeing(ERPNextTestCase): def setUp(self) -> None: self.filters = frappe._dict( company="_Test Company", - to_date="2021-12-10" + to_date="2021-12-10", + range1=30, range2=60, range3=90 ) def test_normal_inward_outward_queue(self): @@ -289,7 +290,8 @@ class TestStockAgeing(ERPNextTestCase): self.assertEqual(item_result["total_qty"], 500.0) self.assertEqual(queue[0][0], 400.0) - self.assertEqual(queue[1][0], 100.0) + self.assertEqual(queue[1][0], 50.0) + self.assertEqual(queue[2][0], 50.0) # check if time buckets add up to balance qty self.assertEqual(sum([i[0] for i in queue]), 500.0) @@ -341,6 +343,63 @@ class TestStockAgeing(ERPNextTestCase): # check if time buckets add up to balance qty self.assertEqual(sum([i[0] for i in queue]), 450.0) + def test_repack_entry_same_item_overconsume_with_split_rows(self): + """ + Over consume item and have less repacked item qty (same warehouse). + Ledger: + Item | Qty | Voucher + ------------------------ + Item 1 | 20 | 001 + Item 1 | -50 | 002 (repack) + Item 1 | -50 | 002 (repack) + Item 1 | 50 | 002 (repack) + """ + sle = [ + frappe._dict( # stock up item + name="Flask Item", + actual_qty=20, qty_after_transaction=20, + warehouse="WH 1", + posting_date="2021-12-03", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-50), qty_after_transaction=(-30), + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-50), qty_after_transaction=(-80), + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=50, qty_after_transaction=(-30), + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + ] + fifo_slots = FIFOSlots(self.filters, sle) + slots = fifo_slots.generate() + item_result = slots["Flask Item"] + queue = item_result["fifo_queue"] + + self.assertEqual(item_result["total_qty"], -30.0) + self.assertEqual(queue[0][0], -30.0) + + # check transfer bucket + transfer_bucket = fifo_slots.transferred_item_details[('002', 'Flask Item', 'WH 1')] + self.assertEqual(transfer_bucket[0][0], 50) + def test_repack_entry_same_item_overproduce(self): """ Under consume item and have more repacked item qty (same warehouse). @@ -385,10 +444,164 @@ class TestStockAgeing(ERPNextTestCase): self.assertEqual(item_result["total_qty"], 550.0) self.assertEqual(queue[0][0], 450.0) - self.assertEqual(queue[1][0], 100.0) + self.assertEqual(queue[1][0], 50.0) + self.assertEqual(queue[2][0], 50.0) # check if time buckets add up to balance qty self.assertEqual(sum([i[0] for i in queue]), 550.0) + def test_repack_entry_same_item_overproduce_with_split_rows(self): + """ + Over consume item and have less repacked item qty (same warehouse). + Ledger: + Item | Qty | Voucher + ------------------------ + Item 1 | 20 | 001 + Item 1 | -50 | 002 (repack) + Item 1 | 50 | 002 (repack) + Item 1 | 50 | 002 (repack) + """ + sle = [ + frappe._dict( # stock up item + name="Flask Item", + actual_qty=20, qty_after_transaction=20, + warehouse="WH 1", + posting_date="2021-12-03", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-50), qty_after_transaction=(-30), + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=50, qty_after_transaction=20, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=50, qty_after_transaction=70, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + ] + fifo_slots = FIFOSlots(self.filters, sle) + slots = fifo_slots.generate() + item_result = slots["Flask Item"] + queue = item_result["fifo_queue"] + + self.assertEqual(item_result["total_qty"], 70.0) + self.assertEqual(queue[0][0], 20.0) + self.assertEqual(queue[1][0], 50.0) + + # check transfer bucket + transfer_bucket = fifo_slots.transferred_item_details[('002', 'Flask Item', 'WH 1')] + self.assertFalse(transfer_bucket) + + def test_negative_stock_same_voucher(self): + """ + Test negative stock scenario in transfer bucket via repack entry (same wh). + Ledger: + Item | Qty | Voucher + ------------------------ + Item 1 | -50 | 001 + Item 1 | -50 | 001 + Item 1 | 30 | 001 + Item 1 | 80 | 001 + """ + sle = [ + frappe._dict( # stock up item + name="Flask Item", + actual_qty=(-50), qty_after_transaction=(-50), + warehouse="WH 1", + posting_date="2021-12-01", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( # stock up item + name="Flask Item", + actual_qty=(-50), qty_after_transaction=(-100), + warehouse="WH 1", + posting_date="2021-12-01", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( # stock up item + name="Flask Item", + actual_qty=30, qty_after_transaction=(-70), + warehouse="WH 1", + posting_date="2021-12-01", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + ] + fifo_slots = FIFOSlots(self.filters, sle) + slots = fifo_slots.generate() + item_result = slots["Flask Item"] + + # check transfer bucket + transfer_bucket = fifo_slots.transferred_item_details[('001', 'Flask Item', 'WH 1')] + self.assertEqual(transfer_bucket[0][0], 20) + self.assertEqual(transfer_bucket[1][0], 50) + self.assertEqual(item_result["fifo_queue"][0][0], -70.0) + + sle.append(frappe._dict( + name="Flask Item", + actual_qty=80, qty_after_transaction=10, + warehouse="WH 1", + posting_date="2021-12-01", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + )) + + fifo_slots = FIFOSlots(self.filters, sle) + slots = fifo_slots.generate() + item_result = slots["Flask Item"] + + transfer_bucket = fifo_slots.transferred_item_details[('001', 'Flask Item', 'WH 1')] + self.assertFalse(transfer_bucket) + self.assertEqual(item_result["fifo_queue"][0][0], 10.0) + + def test_precision(self): + "Test if final balance qty is rounded off correctly." + sle = [ + frappe._dict( # stock up item + name="Flask Item", + actual_qty=0.3, qty_after_transaction=0.3, + warehouse="WH 1", + posting_date="2021-12-01", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( # stock up item + name="Flask Item", + actual_qty=0.6, qty_after_transaction=0.9, + warehouse="WH 1", + posting_date="2021-12-01", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + ] + + slots = FIFOSlots(self.filters, sle).generate() + report_data = format_report_data(self.filters, slots, self.filters["to_date"]) + row = report_data[0] # first row in report + bal_qty = row[5] + range_qty_sum = sum([i for i in row[7:11]]) # get sum of range balance + + # check if value of Available Qty column matches with range bucket post format + self.assertEqual(bal_qty, 0.9) + self.assertEqual(bal_qty, range_qty_sum) + def generate_item_and_item_wh_wise_slots(filters, sle): "Return results with and without 'show_warehouse_wise_stock'" item_wise_slots = FIFOSlots(filters, sle).generate() From 5a2b571aa9e0f448d2030e1901dfb9ec3e547d46 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 18 Feb 2022 20:05:49 +0530 Subject: [PATCH 077/447] fix: Validate party account with company --- .../accounts/doctype/payment_entry/payment_entry.py | 2 +- .../sales_taxes_and_charges_template.py | 2 +- erpnext/accounts/party.py | 5 ++++- erpnext/controllers/accounts_controller.py | 12 +++++------- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 02a144d3e7..0d8f079d7a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1077,7 +1077,7 @@ def get_outstanding_reference_documents(args): if d.voucher_type in ("Purchase Invoice"): d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no") - # Get all SO / PO which are not fully billed or aginst which full advance not paid + # Get all SO / PO which are not fully billed or against which full advance not paid orders_to_be_billed = [] if (args.get("party_type") != "Student"): orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"), diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py index b5909447dc..1d30934df9 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py @@ -46,7 +46,7 @@ def valdiate_taxes_and_charges_template(doc): for tax in doc.get("taxes"): validate_taxes_and_charges(tax) - validate_account_head(tax, doc) + validate_account_head(tax.idx, tax.account_head, doc.company) validate_cost_center(tax, doc) validate_inclusive_tax(tax, doc) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index c13bc23c15..d6f6c5bcb6 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -307,7 +307,7 @@ def validate_party_gle_currency(party_type, party, company, party_account_curren .format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency) def validate_party_accounts(doc): - + from erpnext.controllers.accounts_controller import validate_account_head companies = [] for account in doc.get("accounts"): @@ -330,6 +330,9 @@ def validate_party_accounts(doc): if doc.default_currency != party_account_currency and doc.default_currency != company_default_currency: frappe.throw(_("Billing currency must be equal to either default company's currency or party account currency")) + # validate if account is mapped for same company + validate_account_head(account.idx, account.account, account.company) + @frappe.whitelist() def get_due_date(posting_date, party_type, party, company=None, bill_date=None): diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index d05787fdfb..7913a39329 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1566,13 +1566,12 @@ def validate_taxes_and_charges(tax): tax.rate = None -def validate_account_head(tax, doc): - company = frappe.get_cached_value('Account', - tax.account_head, 'company') +def validate_account_head(idx, account, company): + account_company = frappe.get_cached_value('Account', account, 'company') - if company != doc.company: + if account_company != company: frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}') - .format(tax.idx, frappe.bold(tax.account_head), frappe.bold(doc.company)), title=_('Invalid Account')) + .format(idx, frappe.bold(account), frappe.bold(company)), title=_('Invalid Account')) def validate_cost_center(tax, doc): @@ -1955,8 +1954,7 @@ def update_bin_on_delete(row, doctype): qty_dict["ordered_qty"] = get_ordered_qty(row.item_code, row.warehouse) - if row.warehouse: - update_bin_qty(row.item_code, row.warehouse, qty_dict) + update_bin_qty(row.item_code, row.warehouse, qty_dict) def validate_and_delete_children(parent, data): deleted_children = [] From 1aa12fb3f1bee18a8a58d11954acd8112e96261d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 19 Feb 2022 19:19:32 +0530 Subject: [PATCH 078/447] fix: Ledger entries on LIA for term loans --- .../loan_interest_accrual.py | 33 ------------------- 1 file changed, 33 deletions(-) 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 0de073f85d..1c800a06da 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 @@ -74,39 +74,6 @@ class LoanInterestAccrual(AccountsController): }) ) - if self.payable_principal_amount: - gle_map.append( - self.get_gl_dict({ - "account": self.loan_account, - "party_type": self.applicant_type, - "party": self.applicant, - "against": self.interest_income_account, - "debit": self.payable_principal_amount, - "debit_in_account_currency": self.interest_amount, - "against_voucher_type": "Loan", - "against_voucher": self.loan, - "remarks": _("Interest accrued from {0} to {1} against loan: {2}").format( - self.last_accrual_date, self.posting_date, self.loan), - "cost_center": erpnext.get_default_cost_center(self.company), - "posting_date": self.posting_date - }) - ) - - gle_map.append( - self.get_gl_dict({ - "account": self.interest_income_account, - "against": self.loan_account, - "credit": self.payable_principal_amount, - "credit_in_account_currency": self.interest_amount, - "against_voucher_type": "Loan", - "against_voucher": self.loan, - "remarks": ("Interest accrued from {0} to {1} against loan: {2}").format( - self.last_accrual_date, self.posting_date, self.loan), - "cost_center": erpnext.get_default_cost_center(self.company), - "posting_date": self.posting_date - }) - ) - if gle_map: make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj) From a28ec89507fd42bf100b6a64c6bcdeef55f4b032 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Sat, 19 Feb 2022 19:35:57 +0530 Subject: [PATCH 079/447] Update gstr_1.js --- erpnext/regional/report/gstr_1/gstr_1.js | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.js b/erpnext/regional/report/gstr_1/gstr_1.js index 1766fdb2ec..9999a6d167 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.js +++ b/erpnext/regional/report/gstr_1/gstr_1.js @@ -73,7 +73,6 @@ frappe.query_reports["GSTR-1"] = { company: filters.company }, callback: function(r) { - console.log(r.message); frappe.query_report.page.fields_dict.company_gstin.df.options = r.message; frappe.query_report.page.fields_dict.company_gstin.refresh(); } From d188fcc06698c32342873db8cec32884434c53bd Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Sat, 19 Feb 2022 19:36:43 +0530 Subject: [PATCH 080/447] chore: remove console statements From fa38c291bd577b40f0d5007470108596d392f89b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Feb 2022 11:38:16 +0530 Subject: [PATCH 081/447] fix(pos): removal of coupon code --- erpnext/selling/page/point_of_sale/pos_payment.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 9650bc88a4..4d75e6ef1b 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -180,14 +180,6 @@ erpnext.PointOfSale.Payment = class { () => frm.save(), () => this.update_totals_section(frm.doc) ]); - } else { - frappe.run_serially([ - () => frm.doc.ignore_pricing_rule=1, - () => frm.trigger('ignore_pricing_rule'), - () => frm.doc.ignore_pricing_rule=0, - () => frm.save(), - () => this.update_totals_section(frm.doc) - ]); } } }); From a0bdcbd0cd551895af63955343f517051917c8eb Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 21 Feb 2022 11:44:00 +0530 Subject: [PATCH 082/447] fix: Add patch for account fields --- erpnext/patches.txt | 1 + .../v13_0/update_accounts_in_loan_docs.py | 37 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 erpnext/patches/v13_0/update_accounts_in_loan_docs.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d104bc003c..b24bf0a7e0 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -352,3 +352,4 @@ erpnext.patches.v13_0.shopping_cart_to_ecommerce erpnext.patches.v13_0.update_disbursement_account erpnext.patches.v13_0.update_reserved_qty_closed_wo erpnext.patches.v14_0.delete_amazon_mws_doctype +erpnext.patches.v13_0.update_accounts_in_loan_docs diff --git a/erpnext/patches/v13_0/update_accounts_in_loan_docs.py b/erpnext/patches/v13_0/update_accounts_in_loan_docs.py new file mode 100644 index 0000000000..440f912be2 --- /dev/null +++ b/erpnext/patches/v13_0/update_accounts_in_loan_docs.py @@ -0,0 +1,37 @@ +import frappe + + +def execute(): + ld = frappe.qb.DocType("Loan Disbursement").as_("ld") + lr = frappe.qb.DocType("Loan Repayment").as_("lr") + loan = frappe.qb.DocType("Loan") + + frappe.qb.update( + ld + ).inner_join( + loan + ).on( + loan.name == ld.against_loan + ).set( + ld.disbursement_account, loan.disbursement_account + ).set( + ld.loan_account, loan.loan_account + ).where( + ld.docstatus < 2 + ).run() + + frappe.qb.update( + lr + ).inner_join( + loan + ).on( + loan.name == lr.against_loan + ).set( + lr.payment_account, loan.payment_account + ).set( + lr.loan_account, loan.loan_account + ).set( + lr.penalty_income_account, loan.penalty_income_account + ).where( + lr.docstatus < 2 + ).run() From 295cbb0ff22b04c705148d727d96f70b836fee93 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 21 Feb 2022 11:45:23 +0530 Subject: [PATCH 083/447] fix: Update queries in Bank Reconciliation Tool --- .../bank_reconciliation_tool.py | 57 ++++++++++++++++--- .../bank_transaction/bank_transaction.py | 13 ++++- .../loan_repayment/loan_repayment.json | 6 +- 3 files changed, 63 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index 26078d6329..f3351ddcba 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -7,6 +7,7 @@ import json import frappe from frappe import _ from frappe.model.document import Document +from frappe.query_builder.custom import ConstantColumn from frappe.utils import flt from erpnext import get_company_currency @@ -320,14 +321,34 @@ def get_loan_vouchers(bank_account, transaction, document_types, filters): amount_condition = True if "exact_match" in document_types else False if transaction.withdrawal > 0 and "loan_disbursement" in document_types: - vouchers.append(get_ld_matching_query(bank_account, amount_condition, filters)) + vouchers.extend(get_ld_matching_query(bank_account, amount_condition, filters)) if transaction.deposit > 0 and "loan_repayment" in document_types: - vouchers.append(get_lr_matching_query(bank_account, amount_condition, filters)) + vouchers.extend(get_lr_matching_query(bank_account, amount_condition, filters)) + + return vouchers def get_ld_matching_query(bank_account, amount_condition, filters): loan_disbursement = frappe.qb.DocType("Loan Disbursement") + matching_reference = loan_disbursement.reference_number == filters.get("reference_number") + matching_party = loan_disbursement.applicant_type == filters.get("party_type") and \ + loan_disbursement.applicant == filters.get("party") + + rank = ( + frappe.qb.terms.Case() + .when(matching_reference, 1) + .else_(0) + ) + + rank1 = ( + frappe.qb.terms.Case() + .when(matching_party, 1) + .else_(0) + ) + query = frappe.qb.from_(loan_disbursement).select( + rank + rank1 + 1, + ConstantColumn("Loan Disbursement").as_("doctype"), loan_disbursement.name, loan_disbursement.disbursed_amount, loan_disbursement.reference_number, @@ -351,14 +372,33 @@ def get_ld_matching_query(bank_account, amount_condition, filters): loan_disbursement.disbursed_amount <= filters.get('amount') ) - vouchers = query.run(as_dict=1) + vouchers = query.run(as_list=True) + return vouchers def get_lr_matching_query(bank_account, amount_condition, filters): loan_repayment = frappe.qb.DocType("Loan Repayment") + matching_reference = loan_repayment.reference_number == filters.get("reference_number") + matching_party = loan_repayment.applicant_type == filters.get("party_type") and \ + loan_repayment.applicant == filters.get("party") + + rank = ( + frappe.qb.terms.Case() + .when(matching_reference, 1) + .else_(0) + ) + + rank1 = ( + frappe.qb.terms.Case() + .when(matching_party, 1) + .else_(0) + ) + query = frappe.qb.from_(loan_repayment).select( + rank + rank1 + 1, + ConstantColumn("Loan Repayment").as_("doctype"), loan_repayment.name, - loan_repayment.paid_amount, + loan_repayment.amount_paid, loan_repayment.reference_number, loan_repayment.reference_date, loan_repayment.applicant_type, @@ -368,19 +408,20 @@ def get_lr_matching_query(bank_account, amount_condition, filters): ).where( loan_repayment.clearance_date.isnull() ).where( - loan_repayment.disbursement_account == bank_account + loan_repayment.payment_account == bank_account ) if amount_condition: query.where( - loan_repayment.paid_amount == filters.get('amount') + loan_repayment.amount_paid == filters.get('amount') ) else: query.where( - loan_repayment.paid_amount <= filters.get('amount') + loan_repayment.amount_paid <= filters.get('amount') ) - vouchers = query.run(as_dict=1) + vouchers = query.run() + return vouchers def get_pe_matching_query(amount_condition, account_from_to, transaction): diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 51e1d6e9a0..da944fa4ce 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -49,7 +49,8 @@ class BankTransaction(StatusUpdater): def clear_linked_payment_entries(self, for_cancel=False): for payment_entry in self.payment_entries: - if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]: + if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim", "Loan Repayment", + "Loan Disbursement"]: self.clear_simple_entry(payment_entry, for_cancel=for_cancel) elif payment_entry.payment_document == "Sales Invoice": @@ -104,6 +105,7 @@ def get_total_allocated_amount(payment_entry): bt.docstatus = 1""", (payment_entry.payment_document, payment_entry.payment_entry), as_dict=True) def get_paid_amount(payment_entry, currency, bank_account): + print(payment_entry.payment_document, "#@#@#@") if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]: paid_amount_field = "paid_amount" @@ -116,11 +118,18 @@ def get_paid_amount(payment_entry, currency, bank_account): payment_entry.payment_entry, paid_amount_field) elif payment_entry.payment_document == "Journal Entry": - return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account}, "sum(credit_in_account_currency)") + return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account}, + "sum(credit_in_account_currency)") elif payment_entry.payment_document == "Expense Claim": return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed") + elif payment_entry.payment_document == "Loan Disbursement": + return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "disbursed_amount") + + elif payment_entry.payment_document == "Loan Repayment": + return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "amount_paid") + else: frappe.throw("Please reconcile {0}: {1} manually".format(payment_entry.payment_document, payment_entry.payment_entry)) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json index 766602de86..480e010b49 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json @@ -40,7 +40,7 @@ "repayment_details", "amended_from", "accounting_details_section", - "repayment_account", + "payment_account", "penalty_income_account", "column_break_36", "loan_account" @@ -281,7 +281,7 @@ }, { "fetch_from": "against_loan.payment_account", - "fieldname": "repayment_account", + "fieldname": "payment_account", "fieldtype": "Link", "label": "Repayment Account", "options": "Account", @@ -311,7 +311,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-02-17 19:10:07.742298", + "modified": "2022-02-18 19:10:07.742298", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment", From 0b5e618e3ab206f7ae080f570a736a87fcbccf2d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 21 Feb 2022 11:46:44 +0530 Subject: [PATCH 084/447] fix: Update bank reconciliation statement --- .../bank_reconciliation_statement.py | 105 ++++++++++++++++-- 1 file changed, 95 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py index 6c401fb8f3..b72d266977 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py +++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py @@ -4,7 +4,12 @@ import frappe from frappe import _ -from frappe.utils import flt, getdate, nowdate +from frappe.query_builder.custom import ConstantColumn +from frappe.query_builder.functions import Sum +from frappe.utils import flt, getdate +from pypika import CustomFunction + +from erpnext.accounts.utils import get_balance_on def execute(filters=None): @@ -18,7 +23,6 @@ def execute(filters=None): data = get_entries(filters) - from erpnext.accounts.utils import get_balance_on balance_as_per_system = get_balance_on(filters["account"], filters["report_date"]) total_debit, total_credit = 0,0 @@ -118,7 +122,21 @@ def get_columns(): ] def get_entries(filters): - journal_entries = frappe.db.sql(""" + journal_entries = get_journal_entries(filters) + + payment_entries = get_payment_entries(filters) + + loan_entries = get_loan_entries(filters) + + pos_entries = [] + if filters.include_pos_transactions: + pos_entries = get_pos_entries(filters) + + return sorted(list(payment_entries)+list(journal_entries+list(pos_entries) + list(loan_entries)), + key=lambda k: getdate(k['posting_date'])) + +def get_journal_entries(filters): + return frappe.db.sql(""" select "Journal Entry" as payment_document, jv.posting_date, jv.name as payment_entry, jvd.debit_in_account_currency as debit, jvd.credit_in_account_currency as credit, jvd.against_account, @@ -130,7 +148,8 @@ def get_entries(filters): and ifnull(jv.clearance_date, '4000-01-01') > %(report_date)s and ifnull(jv.is_opening, 'No') = 'No'""", filters, as_dict=1) - payment_entries = frappe.db.sql(""" +def get_payment_entries(filters): + return frappe.db.sql(""" select "Payment Entry" as payment_document, name as payment_entry, reference_no, reference_date as ref_date, @@ -145,9 +164,8 @@ def get_entries(filters): and ifnull(clearance_date, '4000-01-01') > %(report_date)s """, filters, as_dict=1) - pos_entries = [] - if filters.include_pos_transactions: - pos_entries = frappe.db.sql(""" +def get_pos_entries(filters): + return frappe.db.sql(""" select "Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit, si.posting_date, si.debit_to as against_account, sip.clearance_date, @@ -161,8 +179,42 @@ def get_entries(filters): si.posting_date ASC, si.name DESC """, filters, as_dict=1) - return sorted(list(payment_entries)+list(journal_entries+list(pos_entries)), - key=lambda k: k['posting_date'] or getdate(nowdate())) +def get_loan_entries(filters): + loan_docs = [] + for doctype in ["Loan Disbursement", "Loan Repayment"]: + loan_doc = frappe.qb.DocType(doctype) + ifnull = CustomFunction('IFNULL', ['value', 'default']) + + if doctype == "Loan Disbursement": + amount_field = (loan_doc.disbursed_amount).as_("credit") + posting_date = (loan_doc.disbursement_date).as_("posting_date") + account = loan_doc.disbursement_account + else: + amount_field = (loan_doc.amount_paid).as_("debit") + posting_date = (loan_doc.posting_date).as_("posting_date") + account = loan_doc.payment_account + + entries = frappe.qb.from_(loan_doc).select( + ConstantColumn(doctype).as_("payment_document"), + (loan_doc.name).as_("payment_entry"), + (loan_doc.reference_number).as_("reference_no"), + (loan_doc.reference_date).as_("ref_date"), + amount_field, + posting_date, + ).where( + loan_doc.docstatus == 1 + ).where( + account == filters.get('account') + ).where( + posting_date <= getdate(filters.get('report_date')) + ).where( + ifnull(loan_doc.clearance_date, '4000-01-01') > getdate(filters.get('report_date')) + ).run(as_dict=1) + + loan_docs.extend(entries) + + return loan_docs + def get_amounts_not_reflected_in_system(filters): je_amount = frappe.db.sql(""" @@ -182,7 +234,40 @@ def get_amounts_not_reflected_in_system(filters): pe_amount = flt(pe_amount[0][0]) if pe_amount else 0.0 - return je_amount + pe_amount + loan_amount = get_loan_amount(filters) + + return je_amount + pe_amount + loan_amount + +def get_loan_amount(filters): + total_amount = 0 + for doctype in ["Loan Disbursement", "Loan Repayment"]: + loan_doc = frappe.qb.DocType(doctype) + ifnull = CustomFunction('IFNULL', ['value', 'default']) + + if doctype == "Loan Disbursement": + amount_field = Sum(loan_doc.disbursed_amount) + posting_date = (loan_doc.disbursement_date).as_("posting_date") + account = loan_doc.disbursement_account + else: + amount_field = Sum(loan_doc.amount_paid) + posting_date = (loan_doc.posting_date).as_("posting_date") + account = loan_doc.payment_account + + amount = frappe.qb.from_(loan_doc).select( + amount_field + ).where( + loan_doc.docstatus == 1 + ).where( + account == filters.get('account') + ).where( + posting_date > getdate(filters.get('report_date')) + ).where( + ifnull(loan_doc.clearance_date, '4000-01-01') <= getdate(filters.get('report_date')) + ).run()[0][0] + + total_amount += flt(amount) + + return amount def get_balance_row(label, amount, account_currency): if amount > 0: From c5808543c83ea43f62784331fb7c513543e454f0 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Feb 2022 12:41:08 +0530 Subject: [PATCH 085/447] fix(asset): no. of depreciation booked cannot be equal to total no. of depreciations --- erpnext/assets/doctype/asset/asset.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 6e87426ccb..ea473fa7bb 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -417,11 +417,12 @@ class Asset(AccountsController): def validate_asset_finance_books(self, row): if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount): frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount") - .format(row.idx)) + .format(row.idx), title=_("Invalid Schedule")) if not row.depreciation_start_date: if not self.available_for_use_date: - frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx)) + frappe.throw(_("Row {0}: Depreciation Start Date is required") + .format(row.idx), title=_("Invalid Schedule")) row.depreciation_start_date = get_last_day(self.available_for_use_date) if not self.is_existing_asset: @@ -439,8 +440,9 @@ class Asset(AccountsController): else: self.number_of_depreciations_booked = 0 - if cint(self.number_of_depreciations_booked) > cint(row.total_number_of_depreciations): - frappe.throw(_("Number of Depreciations Booked cannot be greater than Total Number of Depreciations")) + if flt(row.total_number_of_depreciations) <= cint(self.number_of_depreciations_booked): + frappe.throw(_("Row {0}: Total Number of Depreciations cannot be less than or equal to Number of Depreciations Booked") + .format(row.idx), title=_("Invalid Schedule")) if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date): frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date") From 780694f6e2d686ca7d037556a52e097802814266 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Feb 2022 12:45:52 +0530 Subject: [PATCH 086/447] test: number_of_depr_booked = total_number_of_depr --- erpnext/assets/doctype/asset/test_asset.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index c08dc21a8f..ddbff89fc7 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -873,8 +873,9 @@ class TestDepreciationBasics(AssetSetup): self.assertRaises(frappe.ValidationError, asset.save) def test_number_of_depreciations(self): - """Tests if an error is raised when number_of_depreciations_booked > total_number_of_depreciations.""" + """Tests if an error is raised when number_of_depreciations_booked >= total_number_of_depreciations.""" + # number_of_depreciations_booked > total_number_of_depreciations asset = create_asset( item_code = "Macbook Pro", calculate_depreciation = 1, @@ -889,6 +890,21 @@ class TestDepreciationBasics(AssetSetup): self.assertRaises(frappe.ValidationError, asset.save) + # number_of_depreciations_booked = total_number_of_depreciations + asset_2 = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = "2019-12-31", + total_number_of_depreciations = 5, + expected_value_after_useful_life = 10000, + depreciation_start_date = "2020-07-01", + opening_accumulated_depreciation = 10000, + number_of_depreciations_booked = 5, + do_not_save = 1 + ) + + self.assertRaises(frappe.ValidationError, asset_2.save) + def test_depreciation_start_date_is_before_purchase_date(self): asset = create_asset( item_code = "Macbook Pro", From a82cf7214e301a3f70513e308d1625a726a1beea Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 21 Feb 2022 13:58:56 +0530 Subject: [PATCH 087/447] fix: Total Credit amount in TDS Payable monthly report --- .../accounts/report/tds_payable_monthly/tds_payable_monthly.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index 57f79748f0..e6cbff5d42 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -43,7 +43,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map): if entry.account in tds_accounts: tds_deducted += (entry.credit - entry.debit) - total_amount_credited += (entry.credit - entry.debit) + total_amount_credited += entry.credit if tds_deducted: row = { From e952cce17d8931054575de2e430f6000ae80ef9f Mon Sep 17 00:00:00 2001 From: Marica Date: Mon, 21 Feb 2022 14:22:14 +0530 Subject: [PATCH 088/447] chore: Show 'Produced Qty' field in Sales Order Item (#29903) --- .../selling/doctype/sales_order_item/sales_order_item.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 95f6c4e96d..080d517d13 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -83,8 +83,8 @@ "planned_qty", "column_break_69", "work_order_qty", - "delivered_qty", "produced_qty", + "delivered_qty", "returned_qty", "shopping_cart_section", "additional_notes", @@ -701,10 +701,8 @@ "width": "50px" }, { - "description": "For Production", "fieldname": "produced_qty", "fieldtype": "Float", - "hidden": 1, "label": "Produced Quantity", "oldfieldname": "produced_qty", "oldfieldtype": "Currency", @@ -802,7 +800,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-10-05 12:27:25.014789", + "modified": "2022-02-21 13:55:08.883104", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", @@ -811,5 +809,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From 1f9ce92011b4bfff27efeb8bf8542c9b716b5251 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 21 Feb 2022 14:29:54 +0530 Subject: [PATCH 089/447] ci: moar backport labels [skip ci] --- .mergify.yml | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/.mergify.yml b/.mergify.yml index f3d04096cf..b7d1df4524 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -14,9 +14,39 @@ pull_request_rules: close: comment: message: | - @{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch. + @{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch. https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch + - name: backport to develop + conditions: + - label="backport develop" + actions: + backport: + branches: + - develop + assignees: + - "{{ author }}" + + - name: backport to version-14-hotfix + conditions: + - label="backport version-14-hotfix" + actions: + backport: + branches: + - version-14-hotfix + assignees: + - "{{ author }}" + + - name: backport to version-14-pre-release + conditions: + - label="backport version-14-pre-release" + actions: + backport: + branches: + - version-14-pre-release + assignees: + - "{{ author }}" + - name: backport to version-13-hotfix conditions: - label="backport version-13-hotfix" @@ -55,4 +85,4 @@ pull_request_rules: branches: - version-12-pre-release assignees: - - "{{ author }}" \ No newline at end of file + - "{{ author }}" From 3a5dbfab505866fb84d02ea61aecc7d4456fa251 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 21 Feb 2022 10:55:55 +0530 Subject: [PATCH 090/447] fix: make cashflow mapping template child doctype --- .../cash_flow_mapping_template_details.json | 118 +++++------------- 1 file changed, 29 insertions(+), 89 deletions(-) diff --git a/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.json b/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.json index 22cf797fc3..a2487c5543 100644 --- a/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.json +++ b/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.json @@ -1,94 +1,34 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:mapping", - "beta": 0, - "creation": "2018-02-08 10:18:48.513608", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2022-02-11 11:25:05.336846", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "mapping" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "mapping", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Mapping", - "length": 0, - "no_copy": 0, - "options": "Cash Flow Mapping", - "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, - "unique": 0 + "fieldname": "mapping", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Mapping", + "options": "Cash Flow Mapping", + "reqd": 1, + "unique": 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": 0, - "max_attachments": 0, - "modified": "2018-02-08 10:33:39.413930", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Cash Flow Mapping Template Details", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "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 + ], + "istable": 1, + "links": [], + "modified": "2022-02-21 03:34:57.902332", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Cash Flow Mapping Template Details", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 } \ No newline at end of file From e3ea431ef39074b77e9089b19bac4bffc1a54e6e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 21 Feb 2022 10:56:14 +0530 Subject: [PATCH 091/447] test: test all form loads --- erpnext/tests/test_zform_loads.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 erpnext/tests/test_zform_loads.py diff --git a/erpnext/tests/test_zform_loads.py b/erpnext/tests/test_zform_loads.py new file mode 100644 index 0000000000..8414acf7d8 --- /dev/null +++ b/erpnext/tests/test_zform_loads.py @@ -0,0 +1,29 @@ +""" dumb test to check all function calls on known form loads """ + +import unittest + +import frappe +from frappe.desk.form.load import getdoc + + +class TestFormLoads(unittest.TestCase): + + def test_load(self): + doctypes = frappe.get_all("DocType", {"istable": 0, "issingle": 0, "is_virtual": 0}, pluck="name") + + for doctype in doctypes: + last_doc = frappe.db.get_value(doctype, {}, "name", order_by="modified desc") + if not last_doc: + continue + with self.subTest(msg=f"Loading {doctype} - {last_doc}", doctype=doctype, last_doc=last_doc): + try: + # reset previous response + frappe.response = frappe._dict({"docs":[]}) + frappe.response.docinfo = None + + getdoc(doctype, last_doc) + except Exception as e: + self.fail(f"Failed to load {doctype} - {last_doc}: {e}") + + self.assertTrue(frappe.response.docs, msg=f"expected document in reponse, found: {frappe.response.docs}") + self.assertTrue(frappe.response.docinfo, msg=f"expected docinfo in reponse, found: {frappe.response.docinfo}") From afc81351b7daa2c245f9ac96a42c54c302da1e8f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 21 Feb 2022 12:49:06 +0530 Subject: [PATCH 092/447] test: only test erpnext doctypes Co-authored-by: gavin --- .../cash_flow_mapping_template_details.json | 2 +- erpnext/tests/test_zform_loads.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.json b/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.json index a2487c5543..02c6875fb3 100644 --- a/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.json +++ b/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.json @@ -1,6 +1,6 @@ { "actions": [], - "creation": "2022-02-11 11:25:05.336846", + "creation": "2018-02-08 10:18:48.513608", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", diff --git a/erpnext/tests/test_zform_loads.py b/erpnext/tests/test_zform_loads.py index 8414acf7d8..b6fb636687 100644 --- a/erpnext/tests/test_zform_loads.py +++ b/erpnext/tests/test_zform_loads.py @@ -9,7 +9,8 @@ from frappe.desk.form.load import getdoc class TestFormLoads(unittest.TestCase): def test_load(self): - doctypes = frappe.get_all("DocType", {"istable": 0, "issingle": 0, "is_virtual": 0}, pluck="name") + erpnext_modules = frappe.get_all("Module Def", filters={"app_name": "erpnext"}, pluck="name") + doctypes = frappe.get_all("DocType", {"istable": 0, "issingle": 0, "is_virtual": 0, "module": ("in", erpnext_modules)}, pluck="name") for doctype in doctypes: last_doc = frappe.db.get_value(doctype, {}, "name", order_by="modified desc") From 28cc2dbb72fc3d716ffcb19b039dccd67c13eb33 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 21 Feb 2022 16:14:40 +0530 Subject: [PATCH 093/447] fix: Block merging items if both have product bundles --- erpnext/stock/doctype/item/item.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index b9e8b3f2f1..d984d6eb99 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -398,6 +398,7 @@ class Item(Document): if merge: self.validate_properties_before_merge(new_name) + self.validate_duplicate_product_bundles_before_merge(old_name, new_name) self.validate_duplicate_website_item_before_merge(old_name, new_name) def after_rename(self, old_name, new_name, merge): @@ -462,6 +463,18 @@ class Item(Document): msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list]) frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError) + def validate_duplicate_product_bundles_before_merge(self, old_name, new_name): + "Block merge if both old and new items have product bundles." + bundle = frappe.get_value("Product Bundle",filters={"new_item_code": old_name}) + if bundle: + bundle_link = get_link_to_form("Product Bundle", bundle) + old_name, new_name = frappe.bold(old_name), frappe.bold(new_name) + + msg = _("Please delete Product Bundle {0}, before merging {1} into {2}").format( + bundle_link, old_name, new_name + ) + frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError) + def validate_duplicate_website_item_before_merge(self, old_name, new_name): """ Block merge if both old and new items have website items against them. @@ -479,8 +492,9 @@ class Item(Document): old_web_item = [d.get("name") for d in web_items if d.get("item_code") == old_name][0] web_item_link = get_link_to_form("Website Item", old_web_item) + old_name, new_name = frappe.bold(old_name), frappe.bold(new_name) - msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} and {new_name}" + msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} into {new_name}" frappe.throw(_(msg), title=_("Cannot Merge"), exc=DataValidationError) def set_last_purchase_rate(self, new_name): From 530f9f70291758d51babd7ec4f52eefe1a899ef1 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 21 Feb 2022 16:48:04 +0530 Subject: [PATCH 094/447] test: Item Merging with Product Bundles --- erpnext/stock/doctype/item/test_item.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index fd4df42187..6f5f1ff786 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -15,6 +15,7 @@ from erpnext.controllers.item_variant import ( get_variant, ) from erpnext.stock.doctype.item.item import ( + DataValidationError, InvalidBarcode, StockExistsForTemplate, get_item_attribute, @@ -388,6 +389,25 @@ class TestItem(ERPNextTestCase): self.assertTrue(frappe.db.get_value("Bin", {"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"})) + def test_item_merging_with_product_bundle(self): + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + + create_item("Test Item Bundle Item 1", is_stock_item=False) + create_item("Test Item Bundle Item 2", is_stock_item=False) + create_item("Test Item inside Bundle") + bundle_items = ["Test Item inside Bundle"] + + bundle1 = make_product_bundle("Test Item Bundle Item 1", bundle_items, qty=2) + make_product_bundle("Test Item Bundle Item 2", bundle_items, qty=2) + + with self.assertRaises(DataValidationError): + frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True) + + bundle1.delete() + frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True) + + self.assertFalse(frappe.db.exists("Item", "Test Item Bundle Item 1")) + def test_uom_conversion_factor(self): if frappe.db.exists('Item', 'Test Item UOM'): frappe.delete_doc('Item', 'Test Item UOM') From a4c6cb9f12f0ff931909a15b657b62a4bc85a20b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 21 Feb 2022 17:08:25 +0530 Subject: [PATCH 095/447] fix: Remove print statements --- erpnext/accounts/doctype/bank_transaction/bank_transaction.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index da944fa4ce..a476cab55f 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -105,7 +105,6 @@ def get_total_allocated_amount(payment_entry): bt.docstatus = 1""", (payment_entry.payment_document, payment_entry.payment_entry), as_dict=True) def get_paid_amount(payment_entry, currency, bank_account): - print(payment_entry.payment_document, "#@#@#@") if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]: paid_amount_field = "paid_amount" From 00e8565868e3bb8a1547abeedd2d158a9b7e5bf4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 21 Feb 2022 17:41:23 +0530 Subject: [PATCH 096/447] fix: round off increments in numeric item variant --- erpnext/stock/doctype/item/item.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index dfc09181ca..ffea9c2d6e 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -594,7 +594,7 @@ $.extend(erpnext.item, { const increment = r.message.increment; let values = []; - for(var i = from; i <= to; i += increment) { + for(var i = from; i <= to; i = flt(i + increment, 6)) { values.push(i); } attr_val_fields[d.attribute] = values; From f4af75f60b7bb594df4f9a6e6d0cb1ad949dfa33 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 15 Feb 2022 11:51:52 +0530 Subject: [PATCH 097/447] feat: batchwise valuation flag This is required to avoid breaking behaviour in valuation of old batches --- erpnext/patches.txt | 1 + .../patches/v14_0/update_batch_valuation_flag.py | 12 ++++++++++++ erpnext/stock/doctype/batch/batch.json | 16 +++++++++++++++- 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v14_0/update_batch_valuation_flag.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index a93ceca437..52c29b22b9 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -353,3 +353,4 @@ erpnext.patches.v13_0.update_reserved_qty_closed_wo erpnext.patches.v13_0.update_exchange_rate_settings erpnext.patches.v14_0.delete_amazon_mws_doctype erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr +erpnext.patches.v14_0.update_batch_valuation_flag diff --git a/erpnext/patches/v14_0/update_batch_valuation_flag.py b/erpnext/patches/v14_0/update_batch_valuation_flag.py new file mode 100644 index 0000000000..d9f08d8d97 --- /dev/null +++ b/erpnext/patches/v14_0/update_batch_valuation_flag.py @@ -0,0 +1,12 @@ +import frappe + + +def execute(): + """ + - Don't use batchwise valuation for existing batches. + - Only batches created after this patch shoule use it. + """ + frappe.db.sql(""" + UPDATE `tabBatch` + SET use_batchwise_valuation=0 + """) diff --git a/erpnext/stock/doctype/batch/batch.json b/erpnext/stock/doctype/batch/batch.json index fc4cf1dbdb..0d28ea0919 100644 --- a/erpnext/stock/doctype/batch/batch.json +++ b/erpnext/stock/doctype/batch/batch.json @@ -9,6 +9,8 @@ "field_order": [ "sb_disabled", "disabled", + "column_break_24", + "use_batchwise_valuation", "sb_batch", "batch_id", "item", @@ -186,6 +188,18 @@ "fieldtype": "Float", "label": "Produced Qty", "read_only": 1 + }, + { + "fieldname": "column_break_24", + "fieldtype": "Column Break" + }, + { + "default": "1", + "fieldname": "use_batchwise_valuation", + "fieldtype": "Check", + "label": "Use Batch-wise Valuation", + "read_only": 1, + "set_only_once": 1 } ], "icon": "fa fa-archive", @@ -193,7 +207,7 @@ "image_field": "image", "links": [], "max_attachments": 5, - "modified": "2021-07-08 16:22:01.343105", + "modified": "2021-10-11 13:38:12.806976", "modified_by": "Administrator", "module": "Stock", "name": "Batch", From ce0514c8db17d59f2f84b3f6c263cd7e5877a049 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 15 Feb 2022 11:41:41 +0530 Subject: [PATCH 098/447] feat: batch wise valuation rates start with most used case: negative inventory isn't enabled - simple addition of qty and value when new batch qty is added - fetch outgoing rate from stock movement of specific batch --- erpnext/stock/doctype/batch/test_batch.py | 46 ++++++++++++++++++++ erpnext/stock/stock_ledger.py | 52 +++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index 0a663c2a18..e7d04db454 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -7,6 +7,7 @@ from frappe.utils import cint, flt from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.stock.doctype.batch.batch import UnableToSelectBatchError, get_batch_no, get_batch_qty +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.get_item_details import get_item_details from erpnext.tests.utils import ERPNextTestCase @@ -300,6 +301,51 @@ class TestBatch(ERPNextTestCase): details = get_item_details(args) self.assertEqual(details.get('price_list_rate'), 400) + + def test_basic_batch_wise_valuation(self, batch_qty = 100): + item_code = "_TestBatchWiseVal" + warehouse = "_Test Warehouse - _TC" + self.make_batch_item(item_code) + + rates = [42, 420] + + batches = {} + for rate in rates: + se = make_stock_entry(item_code=item_code, qty=10, rate=rate, target=warehouse) + batches[se.items[0].batch_no] = rate + + LOW, HIGH = list(batches.keys()) + + # consume things out of order + consumption_plan = [ + (HIGH, 1), + (LOW, 2), + (HIGH, 2), + (HIGH, 4), + (LOW, 6), + ] + + stock_value = sum(rates) * 10 + qty_after_transaction = 20 + for batch, qty in consumption_plan: + # consume out of order + se = make_stock_entry(item_code=item_code, source=warehouse, qty=qty, batch_no=batch) + + sle = frappe.get_last_doc("Stock Ledger Entry", {"is_cancelled": 0, "voucher_no": se.name}) + + stock_value_difference = sle.actual_qty * batches[sle.batch_no] + self.assertAlmostEqual(sle.stock_value_difference, stock_value_difference) + + stock_value += stock_value_difference + self.assertAlmostEqual(sle.stock_value, stock_value) + + qty_after_transaction += sle.actual_qty + self.assertAlmostEqual(sle.qty_after_transaction, qty_after_transaction) + self.assertAlmostEqual(sle.valuation_rate, stock_value / qty_after_transaction) + + self.assertEqual(sle.stock_queue, []) # queues don't apply on batched items + + def create_batch(item_code, rate, create_item_price_for_batch): pi = make_purchase_invoice(company="_Test Company", warehouse= "Stores - _TC", cost_center = "Main - _TC", update_stock=1, diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 00ca81f2b4..c33cc12c2f 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -447,6 +447,8 @@ class update_entries_after(object): self.wh_data.qty_after_transaction = sle.qty_after_transaction self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate) + elif sle.batch_no and frappe.db.get_value("Batch", sle.batch_no, "use_batchwise_valuation", cache=True): + self.update_batched_values(sle) else: if sle.voucher_type=="Stock Reconciliation" and not sle.batch_no: # assert @@ -481,6 +483,7 @@ class update_entries_after(object): if not self.args.get("sle_id"): self.update_outgoing_rate_on_transaction(sle) + def validate_negative_stock(self, sle): """ validate negative stock for entries current datetime onwards @@ -736,7 +739,22 @@ class update_entries_after(object): 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 update_batched_values(self, sle): + incoming_rate = flt(sle.incoming_rate) + actual_qty = flt(sle.actual_qty) + self.wh_data.qty_after_transaction += actual_qty + + if actual_qty > 0: + stock_value_difference = incoming_rate * actual_qty + self.wh_data.stock_value += stock_value_difference + else: + outgoing_rate = _get_batch_outgoing_rate(item_code=sle.item_code, warehouse=sle.warehouse, batch_no=sle.batch_no, posting_date=sle.posting_date, posting_time=sle.posting_time, creation=sle.creation) + stock_value_difference = outgoing_rate * actual_qty + self.wh_data.stock_value += stock_value_difference + + if self.wh_data.qty_after_transaction: + self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no): ref_item_dt = "" @@ -897,6 +915,40 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None): ['item_code', 'warehouse', 'posting_date', 'posting_time', 'timestamp(posting_date, posting_time) as timestamp'], as_dict=1) +def _get_batch_outgoing_rate(item_code, warehouse, batch_no, posting_date, posting_time, creation): + + batch_details = frappe.db.sql(""" + select sum(stock_value_difference) as batch_value, sum(actual_qty) as batch_qty + from `tabStock Ledger Entry` + where + item_code = %(item_code)s + and warehouse = %(warehouse)s + and batch_no = %(batch_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 + ) + ) + """, + { + "item_code": item_code, + "warehouse": warehouse, + "batch_no": batch_no, + "posting_date": posting_date, + "posting_time": posting_time, + "creation": creation, + }, + as_dict=True + ) + + if batch_details and batch_details[0].batch_qty: + return batch_details[0].batch_value / batch_details[0].batch_qty + + + 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): From 342d09a671c522031f73ba777950c70983cea31a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Feb 2022 14:28:51 +0530 Subject: [PATCH 099/447] feat: get_valuation_rate batch wise This function is used to show valuation rate on frontend and also as fallback in case values aren't available. Add "batch_no" param to get batch specific valuation rates. Co-Authored-By: Alan Tom <2.alan.tom@gmail.com> --- erpnext/controllers/buying_controller.py | 1 + .../controllers/sales_and_purchase_return.py | 1 + erpnext/controllers/selling_controller.py | 1 + erpnext/public/js/controllers/transaction.js | 1 + erpnext/stock/doctype/batch/test_batch.py | 39 +++++++++++++++++ .../stock/doctype/stock_entry/stock_entry.js | 3 ++ .../stock/doctype/stock_entry/stock_entry.py | 3 +- erpnext/stock/stock_ledger.py | 43 +++++++++++++------ erpnext/stock/utils.py | 2 +- 9 files changed, 79 insertions(+), 15 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index a181af7313..b831557200 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -249,6 +249,7 @@ class BuyingController(StockController, Subcontracting): "posting_time": self.get('posting_time'), "qty": -1 * flt(d.get('stock_qty')), "serial_no": d.get('serial_no'), + "batch_no": d.get("batch_no"), "company": self.company, "voucher_type": self.doctype, "voucher_no": self.name, diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index df3c5f10c1..8c3aab442b 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -420,6 +420,7 @@ def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None "posting_time": sle.get('posting_time'), "qty": sle.actual_qty, "serial_no": sle.get('serial_no'), + "batch_no": sle.get("batch_no"), "company": sle.company, "voucher_type": sle.voucher_type, "voucher_no": sle.voucher_no diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 31b2209399..e918cde7c4 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -394,6 +394,7 @@ class SellingController(StockController): "posting_time": self.get('posting_time') or nowtime(), "qty": qty if cint(self.get("is_return")) else (-1 * qty), "serial_no": d.get('serial_no'), + "batch_no": d.get("batch_no"), "company": self.company, "voucher_type": self.doctype, "voucher_no": self.name, diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 136e1edb6b..933ced0bd7 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -719,6 +719,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe 'posting_time': posting_time, 'qty': item.qty * item.conversion_factor, 'serial_no': item.serial_no, + 'batch_no': item.batch_no, 'voucher_type': voucher_type, 'company': company, 'allow_zero_valuation_rate': item.allow_zero_valuation_rate diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index e7d04db454..73a48b3f13 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -8,7 +8,11 @@ from frappe.utils import cint, flt from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.stock.doctype.batch.batch import UnableToSelectBatchError, get_batch_no, get_batch_qty 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.get_item_details import get_item_details +from erpnext.stock.stock_ledger import get_valuation_rate from erpnext.tests.utils import ERPNextTestCase @@ -345,6 +349,41 @@ class TestBatch(ERPNextTestCase): self.assertEqual(sle.stock_queue, []) # queues don't apply on batched items + def test_moving_batch_valuation_rates(self): + item_code = "_TestBatchWiseVal" + warehouse = "_Test Warehouse - _TC" + self.make_batch_item(item_code) + + def assertValuation(expected): + actual = get_valuation_rate(item_code, warehouse, "voucher_type", "voucher_no", batch_no=batch_no) + self.assertAlmostEqual(actual, expected) + + se = make_stock_entry(item_code=item_code, qty=100, rate=10, target=warehouse) + batch_no = se.items[0].batch_no + assertValuation(10) + + # consumption should never affect current valuation rate + make_stock_entry(item_code=item_code, qty=20, source=warehouse) + assertValuation(10) + + make_stock_entry(item_code=item_code, qty=30, source=warehouse) + assertValuation(10) + + # 50 * 10 = 500 current value, add more item with higher valuation + make_stock_entry(item_code=item_code, qty=50, rate=20, target=warehouse, batch_no=batch_no) + assertValuation(15) + + # consuming again shouldn't do anything + make_stock_entry(item_code=item_code, qty=20, source=warehouse) + assertValuation(15) + + # reset rate with stock reconiliation + create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=10, rate=25, batch_no=batch_no) + assertValuation(25) + + make_stock_entry(item_code=item_code, qty=20, rate=20, target=warehouse, batch_no=batch_no) + assertValuation((20 * 20 + 10 * 25) / (10 + 20)) + def create_batch(item_code, rate, create_item_price_for_batch): pi = make_purchase_invoice(company="_Test Company", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index c4b8131305..5c9da3a205 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -425,6 +425,7 @@ frappe.ui.form.on('Stock Entry', { 'posting_time' : frm.doc.posting_time, 'warehouse' : cstr(item.s_warehouse) || cstr(item.t_warehouse), 'serial_no' : item.serial_no, + 'batch_no' : item.batch_no, 'company' : frm.doc.company, 'qty' : item.s_warehouse ? -1*flt(item.transfer_qty) : flt(item.transfer_qty), 'voucher_type' : frm.doc.doctype, @@ -457,6 +458,7 @@ frappe.ui.form.on('Stock Entry', { 'warehouse': cstr(child.s_warehouse) || cstr(child.t_warehouse), 'transfer_qty': child.transfer_qty, 'serial_no': child.serial_no, + 'batch_no': child.batch_no, 'qty': child.s_warehouse ? -1* child.transfer_qty : child.transfer_qty, 'posting_date': frm.doc.posting_date, 'posting_time': frm.doc.posting_time, @@ -680,6 +682,7 @@ frappe.ui.form.on('Stock Entry Detail', { 'warehouse' : cstr(d.s_warehouse) || cstr(d.t_warehouse), 'transfer_qty' : d.transfer_qty, 'serial_no' : d.serial_no, + 'batch_no' : d.batch_no, 'bom_no' : d.bom_no, 'expense_account' : d.expense_account, 'cost_center' : d.cost_center, diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 9ba007a186..99cf4de5de 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -510,7 +510,7 @@ class StockEntry(StockController): 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) + raise_error_if_no_rate=raise_error_if_no_rate, batch_no=d.batch_no) d.basic_rate = flt(d.basic_rate, d.precision("basic_rate")) if d.is_process_loss: @@ -541,6 +541,7 @@ class StockEntry(StockController): "posting_time": self.posting_time, "qty": item.s_warehouse and -1*flt(item.transfer_qty) or flt(item.transfer_qty), "serial_no": item.serial_no, + "batch_no": item.batch_no, "voucher_type": self.doctype, "voucher_no": self.name, "company": self.company, diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index c33cc12c2f..53bfed8722 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -634,7 +634,7 @@ class update_entries_after(object): if not allow_zero_rate: 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), company=sle.company) + currency=erpnext.get_company_currency(sle.company), company=sle.company, batch_no=sle.batch_no) def get_incoming_value_for_serial_nos(self, sle, serial_nos): # get rate from serial nos within same company @@ -702,7 +702,7 @@ class update_entries_after(object): if not allow_zero_valuation_rate: 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), company=sle.company) + currency=erpnext.get_company_currency(sle.company), company=sle.company, batch_no=sle.batch_no) def update_queue_values(self, sle): incoming_rate = flt(sle.incoming_rate) @@ -722,7 +722,7 @@ class update_entries_after(object): if not allow_zero_valuation_rate: return 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), company=sle.company) + currency=erpnext.get_company_currency(sle.company), company=sle.company, batch_no=sle.batch_no) else: return 0.0 @@ -950,21 +950,38 @@ def _get_batch_outgoing_rate(item_code, warehouse, batch_no, posting_date, posti 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): + allow_zero_rate=False, currency=None, company=None, raise_error_if_no_rate=True, batch_no=None): if not company: company = frappe.get_cached_value("Warehouse", warehouse, "company") + last_valuation_rate = None + + # Get moving average rate of a specific batch number + if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"): + last_valuation_rate = frappe.db.sql(""" + select sum(stock_value_difference) / sum(actual_qty) + from `tabStock Ledger Entry` + where + item_code = %s + AND warehouse = %s + AND batch_no = %s + AND is_cancelled = 0 + AND NOT (voucher_no = %s AND voucher_type = %s) + """, + (item_code, warehouse, batch_no, voucher_no, voucher_type)) + # Get valuation rate from last sle for the same item and warehouse - last_valuation_rate = frappe.db.sql("""select valuation_rate - from `tabStock Ledger Entry` force index (item_warehouse) - where - item_code = %s - AND warehouse = %s - AND valuation_rate >= 0 - AND is_cancelled = 0 - AND NOT (voucher_no = %s AND voucher_type = %s) - order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type)) + if not last_valuation_rate or last_valuation_rate[0][0] is None: + last_valuation_rate = frappe.db.sql("""select valuation_rate + from `tabStock Ledger Entry` force index (item_warehouse) + where + item_code = %s + AND warehouse = %s + AND valuation_rate >= 0 + AND is_cancelled = 0 + AND NOT (voucher_no = %s AND voucher_type = %s) + order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type)) if not last_valuation_rate: # Get valuation rate from last sle for the item against any warehouse diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 7263e39cc9..3be252e593 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -231,7 +231,7 @@ def get_incoming_rate(args, raise_error_if_no_rate=True): in_rate = get_valuation_rate(args.get('item_code'), args.get('warehouse'), args.get('voucher_type'), voucher_no, args.get('allow_zero_valuation'), currency=erpnext.get_company_currency(args.get('company')), company=args.get('company'), - raise_error_if_no_rate=raise_error_if_no_rate) + raise_error_if_no_rate=raise_error_if_no_rate, batch_no=args.get("batch_no")) return flt(in_rate) From ab926521bd0c9802666032cb3c32aa803655bde0 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Feb 2022 15:37:03 +0530 Subject: [PATCH 100/447] fix: correct incoming rate for batched items --- erpnext/stock/stock_ledger.py | 5 ++--- erpnext/stock/utils.py | 22 ++++++++++++++++++---- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 53bfed8722..4748ad4e46 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -749,7 +749,7 @@ class update_entries_after(object): stock_value_difference = incoming_rate * actual_qty self.wh_data.stock_value += stock_value_difference else: - outgoing_rate = _get_batch_outgoing_rate(item_code=sle.item_code, warehouse=sle.warehouse, batch_no=sle.batch_no, posting_date=sle.posting_date, posting_time=sle.posting_time, creation=sle.creation) + outgoing_rate = get_batch_incoming_rate(item_code=sle.item_code, warehouse=sle.warehouse, batch_no=sle.batch_no, posting_date=sle.posting_date, posting_time=sle.posting_time, creation=sle.creation) stock_value_difference = outgoing_rate * actual_qty self.wh_data.stock_value += stock_value_difference @@ -915,7 +915,7 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None): ['item_code', 'warehouse', 'posting_date', 'posting_time', 'timestamp(posting_date, posting_time) as timestamp'], as_dict=1) -def _get_batch_outgoing_rate(item_code, warehouse, batch_no, posting_date, posting_time, creation): +def get_batch_incoming_rate(item_code, warehouse, batch_no, posting_date, posting_time, creation=None): batch_details = frappe.db.sql(""" select sum(stock_value_difference) as batch_value, sum(actual_qty) as batch_qty @@ -948,7 +948,6 @@ def _get_batch_outgoing_rate(item_code, warehouse, batch_no, posting_date, posti return batch_details[0].batch_value / batch_details[0].batch_qty - 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, batch_no=None): diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 3be252e593..e2bd2f197d 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -209,13 +209,28 @@ def _create_bin(item_code, warehouse): @frappe.whitelist() def get_incoming_rate(args, raise_error_if_no_rate=True): """Get Incoming Rate based on valuation method""" - from erpnext.stock.stock_ledger import get_previous_sle, get_valuation_rate + from erpnext.stock.stock_ledger import ( + get_batch_incoming_rate, + get_previous_sle, + get_valuation_rate, + ) if isinstance(args, str): args = json.loads(args) - in_rate = 0 + voucher_no = args.get('voucher_no') or args.get('name') + + in_rate = None if (args.get("serial_no") or "").strip(): in_rate = get_avg_purchase_rate(args.get("serial_no")) + elif args.get("batch_no") and \ + frappe.db.get_value("Batch", args.get("batch_no"), "use_batchwise_valuation", cache=True): + in_rate = get_batch_incoming_rate( + item_code=args.get('item_code'), + warehouse=args.get('warehouse'), + batch_no=args.get("batch_no"), + posting_date=args.get("posting_date"), + posting_time=args.get("posting_time"), + ) else: valuation_method = get_valuation_method(args.get("item_code")) previous_sle = get_previous_sle(args) @@ -226,8 +241,7 @@ def get_incoming_rate(args, raise_error_if_no_rate=True): elif valuation_method == 'Moving Average': in_rate = previous_sle.get('valuation_rate') or 0 - if not in_rate: - voucher_no = args.get('voucher_no') or args.get('name') + if in_rate is None: in_rate = get_valuation_rate(args.get('item_code'), args.get('warehouse'), args.get('voucher_type'), voucher_no, args.get('allow_zero_valuation'), currency=erpnext.get_company_currency(args.get('company')), company=args.get('company'), From 102fff24c886b49d08776307d513d68ffd56e918 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Feb 2022 15:51:04 +0530 Subject: [PATCH 101/447] refactor: convert query to QB and make creation optional --- erpnext/stock/doctype/batch/test_batch.py | 4 +- erpnext/stock/stock_ledger.py | 53 ++++++++++++----------- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index 73a48b3f13..6495b56e92 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -1,6 +1,8 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +import json + import frappe from frappe.exceptions import ValidationError from frappe.utils import cint, flt @@ -347,7 +349,7 @@ class TestBatch(ERPNextTestCase): self.assertAlmostEqual(sle.qty_after_transaction, qty_after_transaction) self.assertAlmostEqual(sle.valuation_rate, stock_value / qty_after_transaction) - self.assertEqual(sle.stock_queue, []) # queues don't apply on batched items + self.assertEqual(json.loads(sle.stock_queue), []) # queues don't apply on batched items def test_moving_batch_valuation_rates(self): item_code = "_TestBatchWiseVal" diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 4748ad4e46..cacec408ce 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -8,7 +8,9 @@ from typing import Optional import frappe from frappe import _ from frappe.model.meta import get_field_precision +from frappe.query_builder.functions import Sum from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate +from pypika import CustomFunction import erpnext from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty @@ -24,7 +26,6 @@ class NegativeStockError(frappe.ValidationError): pass class SerialNoExistsInFutureTransaction(frappe.ValidationError): pass -_exceptions = frappe.local('stockledger_exceptions') def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): from erpnext.controllers.stock_controller import future_sle_exists @@ -917,32 +918,32 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None): def get_batch_incoming_rate(item_code, warehouse, batch_no, posting_date, posting_time, creation=None): - batch_details = frappe.db.sql(""" - select sum(stock_value_difference) as batch_value, sum(actual_qty) as batch_qty - from `tabStock Ledger Entry` - where - item_code = %(item_code)s - and warehouse = %(warehouse)s - and batch_no = %(batch_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 - ) + Timestamp = CustomFunction('timestamp', ['date', 'time']) + + sle = frappe.qb.DocType("Stock Ledger Entry") + + timestamp_condition = (Timestamp(sle.posting_date, sle.posting_time) < Timestamp(posting_date, posting_time)) + if creation: + timestamp_condition |= ( + (Timestamp(sle.posting_date, sle.posting_time) == Timestamp(posting_date, posting_time)) + & (sle.creation < creation) ) - """, - { - "item_code": item_code, - "warehouse": warehouse, - "batch_no": batch_no, - "posting_date": posting_date, - "posting_time": posting_time, - "creation": creation, - }, - as_dict=True - ) + + batch_details = ( + frappe.qb + .from_(sle) + .select( + Sum(sle.stock_value_difference).as_("batch_value"), + Sum(sle.actual_qty).as_("batch_qty") + ) + .where( + (sle.item_code == item_code) + & (sle.warehouse == warehouse) + & (sle.batch_no == batch_no) + & (sle.is_cancelled == 0) + ) + .where(timestamp_condition) + ).run(as_dict=True) if batch_details and batch_details[0].batch_qty: return batch_details[0].batch_value / batch_details[0].batch_qty From d130233ffc79d085b61bc1b63956d18c03de7a88 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Feb 2022 16:14:15 +0530 Subject: [PATCH 102/447] test: fix expected test failures --- .../stock_reconciliation/test_stock_reconciliation.py | 11 ++++++----- erpnext/stock/stock_ledger.py | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 86af0a0cf3..2ffe127d9a 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -200,7 +200,6 @@ class TestStockReconciliation(ERPNextTestCase): def test_stock_reco_for_batch_item(self): to_delete_records = [] - to_delete_serial_nos = [] # Add new serial nos item_code = "Stock-Reco-batch-Item-1" @@ -208,20 +207,22 @@ class TestStockReconciliation(ERPNextTestCase): sr = create_stock_reconciliation(item_code=item_code, warehouse = warehouse, qty=5, rate=200, do_not_submit=1) - sr.save(ignore_permissions=True) + sr.save() sr.submit() - self.assertTrue(sr.items[0].batch_no) + batch_no = sr.items[0].batch_no + self.assertTrue(batch_no) to_delete_records.append(sr.name) sr1 = create_stock_reconciliation(item_code=item_code, - warehouse = warehouse, qty=6, rate=300, batch_no=sr.items[0].batch_no) + warehouse = warehouse, qty=6, rate=300, batch_no=batch_no) args = { "item_code": item_code, "warehouse": warehouse, "posting_date": nowdate(), "posting_time": nowtime(), + "batch_no": batch_no, } valuation_rate = get_incoming_rate(args) @@ -230,7 +231,7 @@ class TestStockReconciliation(ERPNextTestCase): sr2 = create_stock_reconciliation(item_code=item_code, - warehouse = warehouse, qty=0, rate=0, batch_no=sr.items[0].batch_no) + warehouse = warehouse, qty=0, rate=0, batch_no=batch_no) stock_value = get_stock_value_on(warehouse, nowdate(), item_code) self.assertEqual(stock_value, 0) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index cacec408ce..2dd26643f7 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -751,6 +751,7 @@ class update_entries_after(object): self.wh_data.stock_value += stock_value_difference else: outgoing_rate = get_batch_incoming_rate(item_code=sle.item_code, warehouse=sle.warehouse, batch_no=sle.batch_no, posting_date=sle.posting_date, posting_time=sle.posting_time, creation=sle.creation) + # TODO: negative stock handling stock_value_difference = outgoing_rate * actual_qty self.wh_data.stock_value += stock_value_difference From 312db429e4605d6d0ce47d1034662fdf0ec053b7 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Feb 2022 16:26:17 +0530 Subject: [PATCH 103/447] refactor: use qb for patching flag --- erpnext/patches/v14_0/update_batch_valuation_flag.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/patches/v14_0/update_batch_valuation_flag.py b/erpnext/patches/v14_0/update_batch_valuation_flag.py index d9f08d8d97..55c8c48aa2 100644 --- a/erpnext/patches/v14_0/update_batch_valuation_flag.py +++ b/erpnext/patches/v14_0/update_batch_valuation_flag.py @@ -6,7 +6,6 @@ def execute(): - Don't use batchwise valuation for existing batches. - Only batches created after this patch shoule use it. """ - frappe.db.sql(""" - UPDATE `tabBatch` - SET use_batchwise_valuation=0 - """) + + batch = frappe.qb.DocType("Batch") + frappe.qb.update(batch).set(batch.use_batchwise_valuation, 0).run() From 683ef8a60397b728bd18e1a3c3c317e2f155793c Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Sat, 19 Feb 2022 16:19:30 +0530 Subject: [PATCH 104/447] test: more tests for batchwise valuation Co-Authored-By: Ankush Menat --- .../purchase_receipt/test_purchase_receipt.py | 1 + .../test_stock_ledger_entry.py | 278 ++++++++++++++++++ 2 files changed, 279 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 5ab7929a2a..d481689c13 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1540,6 +1540,7 @@ def make_purchase_receipt(**args): "conversion_factor": args.conversion_factor or 1.0, "stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0), "serial_no": args.serial_no, + "batch_no": args.batch_no, "stock_uom": args.stock_uom or "_Test UOM", "uom": uom, "cost_center": args.cost_center or frappe.get_cached_value('Company', pr.company, 'cost_center'), 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 a1030d5496..60fea9613a 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 @@ -1,6 +1,10 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt +import json +from operator import itemgetter +from uuid import uuid4 + import frappe from frappe.core.page.permission_manager.permission_manager import reset from frappe.utils import add_days, today @@ -349,6 +353,170 @@ class TestStockLedgerEntry(ERPNextTestCase): frappe.set_user("Administrator") user.remove_roles("Stock Manager") + def test_batchwise_item_valuation_moving_average(self): + suffix = get_unique_suffix() + item, warehouses, batches = setup_item_valuation_test( + valuation_method="Moving Average", suffix=suffix + ) + + # Incoming Entries for Stock Value check + pr_entry_list = [ + (item, warehouses[0], batches[0], 1, 100), + (item, warehouses[0], batches[1], 1, 50), + (item, warehouses[0], batches[0], 1, 150), + (item, warehouses[0], batches[1], 1, 100), + ] + prs = create_purchase_receipt_entries_for_batchwise_item_valuation_test(pr_entry_list) + sle_details = fetch_sle_details_for_doc_list(prs, ['stock_value']) + sv_list = [d['stock_value'] for d in sle_details] + expected_sv = [100, 150, 300, 400] + self.assertEqual(expected_sv, sv_list, "Incorrect 'Stock Value' values") + + # Outgoing Entries for Stock Value Difference check + dn_entry_list = [ + (item, warehouses[0], batches[1], 1, 200), + (item, warehouses[0], batches[0], 1, 200), + (item, warehouses[0], batches[1], 1, 200), + (item, warehouses[0], batches[0], 1, 200) + ] + dns = create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list) + sle_details = fetch_sle_details_for_doc_list(dns, ['stock_value_difference']) + svd_list = [-1 * d['stock_value_difference'] for d in sle_details] + expected_incoming_rates = expected_abs_svd = [75, 125, 75, 125] + + self.assertEqual(expected_abs_svd, svd_list, "Incorrect 'Stock Value Difference' values") + for dn, incoming_rate in zip(dns, expected_incoming_rates): + self.assertEqual( + dn.items[0].incoming_rate, incoming_rate, + "Incorrect 'Incoming Rate' values fetched for DN items" + ) + + + def assertSLEs(self, doc, expected_sles): + """ Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line""" + sles = frappe.get_all("Stock Ledger Entry", fields=["*"], + filters={"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled":0}, + order_by="timestamp(posting_date, posting_time), creation") + + for exp_sle, act_sle in zip(expected_sles, sles): + for k, v in exp_sle.items(): + self.assertEqual(v, act_sle[k], msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}") + + def test_batchwise_item_valuation_stock_reco(self): + suffix = get_unique_suffix() + item, warehouses, batches = setup_item_valuation_test( + valuation_method="FIFO", suffix=suffix + ) + state = { + "stock_value" : 0.0, + "qty": 0.0 + } + def update_invariants(exp_sles): + for sle in exp_sles: + state["stock_value"] += sle["stock_value_difference"] + state["qty"] += sle["actual_qty"] + sle["stock_value"] = state["stock_value"] + sle["qty_after_transaction"] = state["qty"] + + osr1 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=10, rate=100, batch_no=batches[1]) + expected_sles = [ + {"actual_qty": 10, "stock_value_difference": 1000}, + ] + update_invariants(expected_sles) + self.assertSLEs(osr1, expected_sles) + + osr2 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=13, rate=200, batch_no=batches[0]) + expected_sles = [ + {"actual_qty": 13, "stock_value_difference": 200*13}, + ] + update_invariants(expected_sles) + self.assertSLEs(osr2, expected_sles) + + sr1 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=5, rate=50, batch_no=batches[1]) + + expected_sles = [ + {"actual_qty": -10, "stock_value_difference": -10 * 100}, + {"actual_qty": 5, "stock_value_difference": 250} + ] + update_invariants(expected_sles) + self.assertSLEs(sr1, expected_sles) + + sr2 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=20, rate=75, batch_no=batches[0]) + expected_sles = [ + {"actual_qty": -13, "stock_value_difference": -13 * 200}, + {"actual_qty": 20, "stock_value_difference": 20 * 75} + ] + update_invariants(expected_sles) + self.assertSLEs(sr2, expected_sles) + + def test_legacy_item_valuation_stock_entry(self): + suffix = get_unique_suffix() + columns = [ + 'stock_value_difference', + 'stock_value', + 'actual_qty', + 'qty_after_transaction', + 'stock_queue', + ] + item, warehouses, batches = setup_item_valuation_test( + valuation_method="FIFO", suffix=suffix, use_batchwise_valuation=0 + ) + + def check_sle_details_against_expected(sle_details, expected_sle_details, detail, columns): + for i, (sle_vals, ex_sle_vals) in enumerate(zip(sle_details, expected_sle_details)): + for col, sle_val, ex_sle_val in zip(columns, sle_vals, ex_sle_vals): + if col == 'stock_queue': + sle_val = get_stock_value_from_q(sle_val) + ex_sle_val = get_stock_value_from_q(ex_sle_val) + self.assertEqual( + sle_val, ex_sle_val, + f"Incorrect {col} value on transaction #: {i} in {detail}" + ) + + # List used to defer assertions to prevent commits cause of error skipped rollback + details_list = [] + + + # Test Material Receipt Entries + se_entry_list_mr = [ + (item, None, warehouses[0], batches[0], 1, 50, "2021-01-21"), + (item, None, warehouses[0], batches[1], 1, 100, "2021-01-23"), + ] + ses = create_stock_entry_entries_for_batchwise_item_valuation_test( + se_entry_list_mr, "Material Receipt" + ) + sle_details = fetch_sle_details_for_doc_list(ses, columns=columns, as_dict=0) + expected_sle_details = [ + (50.0, 50.0, 1.0, 1.0, '[[1.0, 50.0]]'), + (100.0, 150.0, 1.0, 2.0, '[[1.0, 50.0], [1.0, 100.0]]'), + ] + details_list.append(( + sle_details, expected_sle_details, + "Material Receipt Entries", columns + )) + + + # Test Material Issue Entries + se_entry_list_mi = [ + (item, warehouses[0], None, batches[1], 1, None, "2021-01-29"), + ] + ses = create_stock_entry_entries_for_batchwise_item_valuation_test( + se_entry_list_mi, "Material Issue" + ) + sle_details = fetch_sle_details_for_doc_list(ses, columns=columns, as_dict=0) + expected_sle_details = [ + (-50.0, 100.0, -1.0, 1.0, '[[1, 100.0]]') + ] + details_list.append(( + sle_details, expected_sle_details, + "Material Issue Entries", columns + )) + + + # Run assertions + for details in details_list: + check_sle_details_against_expected(*details) + def create_repack_entry(**args): args = frappe._dict(args) @@ -412,3 +580,113 @@ def create_items(): make_item(d, properties=properties) return items + +def setup_item_valuation_test(valuation_method, suffix, use_batchwise_valuation=1, batches_list=['X', 'Y']): + from erpnext.stock.doctype.batch.batch import make_batch + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + item = make_item( + f"IV - Test Item {valuation_method} {suffix}", + dict(valuation_method=valuation_method, has_batch_no=1) + ) + warehouses = [create_warehouse(f"IV - Test Warehouse {i}") for i in ['J', 'K']] + batches = [f"IV - Test Batch {i} {valuation_method} {suffix}" for i in batches_list] + + for i, batch_id in enumerate(batches): + if not frappe.db.exists("Batch", batch_id): + ubw = use_batchwise_valuation + if isinstance(use_batchwise_valuation, (list, tuple)): + ubw = use_batchwise_valuation[i] + make_batch( + frappe._dict( + batch_id=batch_id, + item=item.item_code, + use_batchwise_valuation=ubw + ) + ) + + return item.item_code, warehouses, batches + +def create_purchase_receipt_entries_for_batchwise_item_valuation_test(pr_entry_list): + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + prs = [] + + for item, warehouse, batch_no, qty, rate in pr_entry_list: + pr = make_purchase_receipt(item=item, warehouse=warehouse, qty=qty, rate=rate, batch_no=batch_no) + prs.append(pr) + + return prs + +def create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list): + from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note + from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + dns = [] + for item, warehouse, batch_no, qty, rate in dn_entry_list: + so = make_sales_order( + rate=rate, + qty=qty, + item=item, + warehouse=warehouse, + against_blanket_order=0 + ) + + dn = make_delivery_note(so.name) + dn.items[0].batch_no = batch_no + dn.insert() + dn.submit() + dns.append(dn) + return dns + +def fetch_sle_details_for_doc_list(doc_list, columns, as_dict=1): + return frappe.db.sql(f""" + SELECT { ', '.join(columns)} + FROM `tabStock Ledger Entry` + WHERE + voucher_no IN %(voucher_nos)s + and docstatus = 1 + ORDER BY timestamp(posting_date, posting_time) ASC, CREATION ASC + """, dict( + voucher_nos=[doc.name for doc in doc_list] + ), as_dict=as_dict) + +def get_stock_value_from_q(q): + return sum(r*q for r,q in json.loads(q)) + +def create_stock_entry_entries_for_batchwise_item_valuation_test(se_entry_list, purpose): + ses = [] + for item, source, target, batch, qty, rate, posting_date in se_entry_list: + args = dict( + item_code=item, + qty=qty, + company="_Test Company", + batch_no=batch, + posting_date=posting_date, + purpose=purpose + ) + + if purpose == "Material Receipt": + args.update( + dict(to_warehouse=target, rate=rate) + ) + + elif purpose == "Material Issue": + args.update( + dict(from_warehouse=source) + ) + + elif purpose == "Material Transfer": + args.update( + dict(from_warehouse=source, to_warehouse=target) + ) + + else: + raise ValueError(f"Invalid purpose: {purpose}") + ses.append(make_stock_entry(**args)) + + return ses + +def get_unique_suffix(): + # Used to isolate valuation sensitive + # tests to prevent future tests from failing. + return str(uuid4())[:8].upper() From 5718777a2b3018e07ea310e87e5a2ea26ff3eb1b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Feb 2022 18:36:16 +0530 Subject: [PATCH 105/447] fix: consider batch_no when getting incoming rate --- erpnext/controllers/buying_controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index b831557200..b740476481 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -279,7 +279,8 @@ class BuyingController(StockController, Subcontracting): "posting_date": self.posting_date, "posting_time": self.posting_time, "qty": -1 * d.consumed_qty, - "serial_no": d.serial_no + "serial_no": d.serial_no, + "batch_no": d.batch_no, }) if rate > 0: From 60b8bae85f00b6a6bf4a26c7604e28e0b075bb52 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Feb 2022 19:18:35 +0530 Subject: [PATCH 106/447] test: batch wise valuation for transfer and intermediate --- .../test_stock_ledger_entry.py | 99 ++++++++++++++++--- 1 file changed, 86 insertions(+), 13 deletions(-) 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 60fea9613a..c298b5a096 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 @@ -354,10 +354,7 @@ class TestStockLedgerEntry(ERPNextTestCase): user.remove_roles("Stock Manager") def test_batchwise_item_valuation_moving_average(self): - suffix = get_unique_suffix() - item, warehouses, batches = setup_item_valuation_test( - valuation_method="Moving Average", suffix=suffix - ) + item, warehouses, batches = setup_item_valuation_test(valuation_method="Moving Average") # Incoming Entries for Stock Value check pr_entry_list = [ @@ -403,10 +400,7 @@ class TestStockLedgerEntry(ERPNextTestCase): self.assertEqual(v, act_sle[k], msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}") def test_batchwise_item_valuation_stock_reco(self): - suffix = get_unique_suffix() - item, warehouses, batches = setup_item_valuation_test( - valuation_method="FIFO", suffix=suffix - ) + item, warehouses, batches = setup_item_valuation_test() state = { "stock_value" : 0.0, "qty": 0.0 @@ -449,8 +443,86 @@ class TestStockLedgerEntry(ERPNextTestCase): update_invariants(expected_sles) self.assertSLEs(sr2, expected_sles) + def test_batch_wise_valuation_across_warehouse(self): + item_code, warehouses, batches = setup_item_valuation_test() + source = warehouses[0] + target = warehouses[1] + + unrelated_batch = make_stock_entry(item_code=item_code, target=source, batch_no=batches[1], + qty=5, rate=10) + self.assertSLEs(unrelated_batch, [ + {"actual_qty": 5, "stock_value_difference": 10 * 5}, + ]) + + reciept = make_stock_entry(item_code=item_code, target=source, batch_no=batches[0], qty=5, rate=10) + self.assertSLEs(reciept, [ + {"actual_qty": 5, "stock_value_difference": 10 * 5}, + ]) + + transfer = make_stock_entry(item_code=item_code, source=source, target=target, batch_no=batches[0], qty=5) + self.assertSLEs(transfer, [ + {"actual_qty": -5, "stock_value_difference": -10 * 5, "warehouse": source}, + {"actual_qty": 5, "stock_value_difference": 10 * 5, "warehouse": target} + ]) + + backdated_receipt = make_stock_entry(item_code=item_code, target=source, batch_no=batches[0], + qty=5, rate=20, posting_date=add_days(today(), -1)) + self.assertSLEs(backdated_receipt, [ + {"actual_qty": 5, "stock_value_difference": 20 * 5}, + ]) + + # check reposted average rate in *future* transfer + self.assertSLEs(transfer, [ + {"actual_qty": -5, "stock_value_difference": -15 * 5, "warehouse": source, "stock_value": 15 * 5 + 10 * 5}, + {"actual_qty": 5, "stock_value_difference": 15 * 5, "warehouse": target, "stock_value": 15 * 5} + ]) + + transfer_unrelated = make_stock_entry(item_code=item_code, source=source, + target=target, batch_no=batches[1], qty=5) + self.assertSLEs(transfer_unrelated, [ + {"actual_qty": -5, "stock_value_difference": -10 * 5, "warehouse": source, "stock_value": 15 * 5}, + {"actual_qty": 5, "stock_value_difference": 10 * 5, "warehouse": target, "stock_value": 15 * 5 + 10 * 5} + ]) + + def test_intermediate_average_batch_wise_valuation(self): + """ A batch has moving average up until posting time, + check if same is respected when backdated entry is inserted in middle""" + item_code, warehouses, batches = setup_item_valuation_test() + warehouse = warehouses[0] + + batch = batches[0] + + yesterday = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batch, + qty=1, rate=10, posting_date=add_days(today(), -1)) + self.assertSLEs(yesterday, [ + {"actual_qty": 1, "stock_value_difference": 10}, + ]) + + tomorrow = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0], + qty=1, rate=30, posting_date=add_days(today(), 1)) + self.assertSLEs(tomorrow, [ + {"actual_qty": 1, "stock_value_difference": 30}, + ]) + + create_today = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0], + qty=1, rate=20) + self.assertSLEs(create_today, [ + {"actual_qty": 1, "stock_value_difference": 20}, + ]) + + consume_today = make_stock_entry(item_code=item_code, source=warehouse, batch_no=batches[0], + qty=1) + self.assertSLEs(consume_today, [ + {"actual_qty": -1, "stock_value_difference": -15}, + ]) + + consume_tomorrow = make_stock_entry(item_code=item_code, source=warehouse, batch_no=batches[0], + qty=2, posting_date=add_days(today(), 2)) + self.assertSLEs(consume_tomorrow, [ + {"stock_value_difference": -(30 + 15), "stock_value": 0, "qty_after_transaction": 0}, + ]) + def test_legacy_item_valuation_stock_entry(self): - suffix = get_unique_suffix() columns = [ 'stock_value_difference', 'stock_value', @@ -458,9 +530,7 @@ class TestStockLedgerEntry(ERPNextTestCase): 'qty_after_transaction', 'stock_queue', ] - item, warehouses, batches = setup_item_valuation_test( - valuation_method="FIFO", suffix=suffix, use_batchwise_valuation=0 - ) + item, warehouses, batches = setup_item_valuation_test(use_batchwise_valuation=0) def check_sle_details_against_expected(sle_details, expected_sle_details, detail, columns): for i, (sle_vals, ex_sle_vals) in enumerate(zip(sle_details, expected_sle_details)): @@ -581,11 +651,14 @@ def create_items(): return items -def setup_item_valuation_test(valuation_method, suffix, use_batchwise_valuation=1, batches_list=['X', 'Y']): +def setup_item_valuation_test(valuation_method="FIFO", suffix=None, use_batchwise_valuation=1, batches_list=['X', 'Y']): from erpnext.stock.doctype.batch.batch import make_batch from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + if not suffix: + suffix = get_unique_suffix() + item = make_item( f"IV - Test Item {valuation_method} {suffix}", dict(valuation_method=valuation_method, has_batch_no=1) From c5bd34d2383982e99db825cef1b5ec8215ccabee Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Feb 2022 19:21:12 +0530 Subject: [PATCH 107/447] test: multi-batch stock entry --- .../doctype/stock_entry/test_stock_entry.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 306f2c3e69..6c6513beff 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -1107,6 +1107,52 @@ class TestStockEntry(ERPNextTestCase): posting_date='2021-09-02', # backdated consumption of 2nd batch purpose='Material Issue') + def test_multi_batch_value_diff(self): + """ Test value difference on stock entry in case of multi-batch. + | Stock entry | batch | qty | rate | value diff on SE | + | --- | --- | --- | --- | --- | + | receipt | A | 1 | 10 | 30 | + | receipt | B | 1 | 20 | | + | issue | A | -1 | 10 | -30 (to assert after submit) | + | issue | B | -1 | 20 | | + """ + from erpnext.stock.doctype.batch.test_batch import TestBatch + + batch_nos = [] + + item_code = '_TestMultibatchFifo' + TestBatch.make_batch_item(item_code) + warehouse = '_Test Warehouse - _TC' + receipt = make_stock_entry( + item_code=item_code, + qty=1, + rate=10, + to_warehouse=warehouse, + purpose='Material Receipt', + do_not_save=True + ) + receipt.append("items", frappe.copy_doc(receipt.items[0], ignore_no_copy=False).update({"basic_rate": 20}) ) + receipt.save() + receipt.submit() + batch_nos.extend(row.batch_no for row in receipt.items) + self.assertEqual(receipt.value_difference, 30) + + issue = make_stock_entry( + item_code=item_code, + qty=1, + from_warehouse=warehouse, + purpose='Material Issue', + do_not_save=True + ) + issue.append("items", frappe.copy_doc(issue.items[0], ignore_no_copy=False)) + for row, batch_no in zip(issue.items, batch_nos): + row.batch_no = batch_no + issue.save() + issue.submit() + + issue.reload() # reload because reposting current voucher updates rate + self.assertEqual(issue.value_difference, -30) + def make_serialized_item(**args): args = frappe._dict(args) se = frappe.copy_doc(test_records[0]) From d7ca83ef0b42af42bca94e43c18c26cbf8e19ed3 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Feb 2022 19:35:33 +0530 Subject: [PATCH 108/447] refactor: code duplication for fallback rates --- erpnext/stock/stock_ledger.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 2dd26643f7..9339b3ea23 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -633,9 +633,7 @@ class update_entries_after(object): 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.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), company=sle.company, batch_no=sle.batch_no) + self.wh_data.valuation_rate = self.get_fallback_rate(sle) def get_incoming_value_for_serial_nos(self, sle, serial_nos): # get rate from serial nos within same company @@ -701,9 +699,7 @@ class update_entries_after(object): 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.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), company=sle.company, batch_no=sle.batch_no) + self.wh_data.valuation_rate = self.get_fallback_rate(sle) def update_queue_values(self, sle): incoming_rate = flt(sle.incoming_rate) @@ -721,9 +717,7 @@ class update_entries_after(object): def rate_generator() -> float: allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no) if not allow_zero_valuation_rate: - return 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), company=sle.company, batch_no=sle.batch_no) + return self.get_fallback_rate(sle) else: return 0.0 @@ -771,6 +765,13 @@ class update_entries_after(object): else: return 0 + def get_fallback_rate(self, sle) -> float: + """When exact incoming rate isn't available use any of other "average" rates as fallback. + This should only get used for negative stock.""" + return 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), company=sle.company, batch_no=sle.batch_no) + def get_sle_before_datetime(self, args): """get previous stock ledger entry before current time-bucket""" sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False) From aba7a7ce4e4dc1fb264023db0034df5e906b5571 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Feb 2022 19:36:28 +0530 Subject: [PATCH 109/447] fix: handle negative inventory inside a batch --- erpnext/stock/stock_ledger.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 9339b3ea23..edbe755329 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -742,13 +742,17 @@ class update_entries_after(object): if actual_qty > 0: stock_value_difference = incoming_rate * actual_qty - self.wh_data.stock_value += stock_value_difference else: outgoing_rate = get_batch_incoming_rate(item_code=sle.item_code, warehouse=sle.warehouse, batch_no=sle.batch_no, posting_date=sle.posting_date, posting_time=sle.posting_time, creation=sle.creation) - # TODO: negative stock handling + if outgoing_rate is None: + # This can *only* happen if qty available for the batch is zero. + # in such case fall back various other rates. + # future entries will correct the overall accounting as each + # batch individually uses moving average rates. + outgoing_rate = self.get_fallback_rate(sle) stock_value_difference = outgoing_rate * actual_qty - self.wh_data.stock_value += stock_value_difference + self.wh_data.stock_value += stock_value_difference if self.wh_data.qty_after_transaction: self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction From b534fee2c7220390ed749d9ee87759663558a019 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Feb 2022 20:58:36 +0530 Subject: [PATCH 110/447] refactor: use queue difference instead of actual values --- erpnext/stock/stock_ledger.py | 19 ++++++++++++------- erpnext/stock/tests/test_valuation.py | 12 ++++++------ erpnext/stock/valuation.py | 12 ++++++------ 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index edbe755329..677266ee0c 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -19,7 +19,7 @@ from erpnext.stock.utils import ( get_or_make_bin, get_valuation_method, ) -from erpnext.stock.valuation import FIFOValuation, LIFOValuation +from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero class NegativeStockError(frappe.ValidationError): pass @@ -465,7 +465,6 @@ class update_entries_after(object): self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate) else: self.update_queue_values(sle) - self.wh_data.qty_after_transaction += flt(sle.actual_qty) # rounding as per precision self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision) @@ -706,11 +705,15 @@ class update_entries_after(object): actual_qty = flt(sle.actual_qty) outgoing_rate = flt(sle.outgoing_rate) + self.wh_data.qty_after_transaction = round_off_if_near_zero(self.wh_data.qty_after_transaction + actual_qty) + if self.valuation_method == "LIFO": stock_queue = LIFOValuation(self.wh_data.stock_queue) else: stock_queue = FIFOValuation(self.wh_data.stock_queue) + _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value() + if actual_qty > 0: stock_queue.add_stock(qty=actual_qty, rate=incoming_rate) else: @@ -723,17 +726,19 @@ class update_entries_after(object): stock_queue.remove_stock(qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator) - stock_qty, stock_value = stock_queue.get_total_stock_and_value() + _qty, stock_value = stock_queue.get_total_stock_and_value() + + stock_value_difference = stock_value - prev_stock_value self.wh_data.stock_queue = stock_queue.state - self.wh_data.stock_value = stock_value - if stock_qty: - self.wh_data.valuation_rate = stock_value / stock_qty - + self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + stock_value_difference) 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]) + if self.wh_data.qty_after_transaction: + self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction + def update_batched_values(self, sle): incoming_rate = flt(sle.incoming_rate) actual_qty = flt(sle.actual_qty) diff --git a/erpnext/stock/tests/test_valuation.py b/erpnext/stock/tests/test_valuation.py index 648d4406ca..bdb768f1ad 100644 --- a/erpnext/stock/tests/test_valuation.py +++ b/erpnext/stock/tests/test_valuation.py @@ -7,7 +7,7 @@ from hypothesis import strategies as st from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry -from erpnext.stock.valuation import FIFOValuation, LIFOValuation, _round_off_if_near_zero +from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero from erpnext.tests.utils import ERPNextTestCase qty_gen = st.floats(min_value=-1e6, max_value=1e6) @@ -113,11 +113,11 @@ class TestFIFOValuation(unittest.TestCase): self.assertTotalQty(0) def test_rounding_off_near_zero(self): - self.assertEqual(_round_off_if_near_zero(0), 0) - self.assertEqual(_round_off_if_near_zero(1), 1) - self.assertEqual(_round_off_if_near_zero(-1), -1) - self.assertEqual(_round_off_if_near_zero(-1e-8), 0) - self.assertEqual(_round_off_if_near_zero(1e-8), 0) + self.assertEqual(round_off_if_near_zero(0), 0) + self.assertEqual(round_off_if_near_zero(1), 1) + self.assertEqual(round_off_if_near_zero(-1), -1) + self.assertEqual(round_off_if_near_zero(-1e-8), 0) + self.assertEqual(round_off_if_near_zero(1e-8), 0) def test_totals(self): self.queue.add_stock(1, 10) diff --git a/erpnext/stock/valuation.py b/erpnext/stock/valuation.py index ee9477ed74..e2bd1ad4df 100644 --- a/erpnext/stock/valuation.py +++ b/erpnext/stock/valuation.py @@ -34,7 +34,7 @@ class BinWiseValuation(ABC): total_qty += flt(qty) total_value += flt(qty) * flt(rate) - return _round_off_if_near_zero(total_qty), _round_off_if_near_zero(total_value) + return round_off_if_near_zero(total_qty), round_off_if_near_zero(total_value) def __repr__(self): return str(self.state) @@ -136,7 +136,7 @@ class FIFOValuation(BinWiseValuation): fifo_bin = self.queue[index] if qty >= fifo_bin[QTY]: # consume current bin - qty = _round_off_if_near_zero(qty - fifo_bin[QTY]) + qty = round_off_if_near_zero(qty - fifo_bin[QTY]) to_consume = self.queue.pop(index) consumed_bins.append(list(to_consume)) @@ -148,7 +148,7 @@ class FIFOValuation(BinWiseValuation): break else: # qty found in current bin consume it and exit - fifo_bin[QTY] = _round_off_if_near_zero(fifo_bin[QTY] - qty) + fifo_bin[QTY] = round_off_if_near_zero(fifo_bin[QTY] - qty) consumed_bins.append([qty, fifo_bin[RATE]]) qty = 0 @@ -231,7 +231,7 @@ class LIFOValuation(BinWiseValuation): stock_bin = self.stack[index] if qty >= stock_bin[QTY]: # consume current bin - qty = _round_off_if_near_zero(qty - stock_bin[QTY]) + qty = round_off_if_near_zero(qty - stock_bin[QTY]) to_consume = self.stack.pop(index) consumed_bins.append(list(to_consume)) @@ -243,14 +243,14 @@ class LIFOValuation(BinWiseValuation): break else: # qty found in current bin consume it and exit - stock_bin[QTY] = _round_off_if_near_zero(stock_bin[QTY] - qty) + stock_bin[QTY] = round_off_if_near_zero(stock_bin[QTY] - qty) consumed_bins.append([qty, stock_bin[RATE]]) qty = 0 return consumed_bins -def _round_off_if_near_zero(number: float, precision: int = 7) -> float: +def round_off_if_near_zero(number: float, precision: int = 7) -> float: """Rounds off the number to zero only if number is close to zero for decimal specified in precision. Precision defaults to 7. """ From b1555fd477923a968a203c2fde68e754777a1e08 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Feb 2022 21:21:39 +0530 Subject: [PATCH 111/447] chore: batch flag and consumption rate in invariant report --- .../stock_ledger_invariant_check.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py index cb35bf75d1..7826d34422 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py @@ -60,6 +60,9 @@ def add_invariant_check_fields(sles): fifo_qty += qty fifo_value += qty * rate + if sle.actual_qty < 0: + sle.consumption_rate = sle.stock_value_difference / sle.actual_qty + balance_qty += sle.actual_qty balance_stock_value += sle.stock_value_difference if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no: @@ -90,6 +93,9 @@ def add_invariant_check_fields(sles): sle.fifo_stock_diff = sle.fifo_stock_value - sles[idx - 1].fifo_stock_value sle.fifo_difference_diff = sle.fifo_stock_diff - sle.stock_value_difference + if sle.batch_no: + sle.use_batchwise_valuation = frappe.db.get_value("Batch", sle.batch_no, "use_batchwise_valuation", cache=True) + return sles @@ -134,6 +140,11 @@ def get_columns(): "label": "Batch", "options": "Batch", }, + { + "fieldname": "use_batchwise_valuation", + "fieldtype": "Check", + "label": "Batchwise Valuation", + }, { "fieldname": "actual_qty", "fieldtype": "Float", @@ -145,9 +156,9 @@ def get_columns(): "label": "Incoming Rate", }, { - "fieldname": "outgoing_rate", + "fieldname": "consumption_rate", "fieldtype": "Float", - "label": "Outgoing Rate", + "label": "Consumption Rate", }, { "fieldname": "qty_after_transaction", From 76b395d62ee5f9ffb96e3c3e4920fa6eebaec175 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Feb 2022 22:01:34 +0530 Subject: [PATCH 112/447] test: old/new mix batches valuation consumption --- .../test_stock_ledger_entry.py | 83 ++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) 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 c298b5a096..b0df45ffd4 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 @@ -397,7 +397,15 @@ class TestStockLedgerEntry(ERPNextTestCase): for exp_sle, act_sle in zip(expected_sles, sles): for k, v in exp_sle.items(): - self.assertEqual(v, act_sle[k], msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}") + act_value = act_sle[k] + if k == "stock_queue": + act_value = json.loads(act_value) + if act_value and act_value[0][0] == 0: + # ignore empty fifo bins + continue + + self.assertEqual(v, act_value, msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}") + def test_batchwise_item_valuation_stock_reco(self): item, warehouses, batches = setup_item_valuation_test() @@ -587,6 +595,77 @@ class TestStockLedgerEntry(ERPNextTestCase): for details in details_list: check_sle_details_against_expected(*details) + def test_mixed_valuation_batches(self): + item_code, warehouses, batches = setup_item_valuation_test(use_batchwise_valuation=0) + warehouse = warehouses[0] + + state = { + "qty": 0.0, + "stock_value": 0.0 + } + def update_invariants(exp_sles): + for sle in exp_sles: + state["stock_value"] += sle["stock_value_difference"] + state["qty"] += sle["actual_qty"] + sle["stock_value"] = state["stock_value"] + sle["qty_after_transaction"] = state["qty"] + return exp_sles + + old1 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0], + qty=10, rate=10) + self.assertSLEs(old1, update_invariants([ + {"actual_qty": 10, "stock_value_difference": 10*10, "stock_queue": [[10, 10]]}, + ])) + old2 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[1], + qty=10, rate=20) + self.assertSLEs(old2, update_invariants([ + {"actual_qty": 10, "stock_value_difference": 10*20, "stock_queue": [[10, 10], [10, 20]]}, + ])) + old3 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0], + qty=5, rate=15) + + self.assertSLEs(old3, update_invariants([ + {"actual_qty": 5, "stock_value_difference": 5*15, "stock_queue": [[10, 10], [10, 20], [5, 15]]}, + ])) + + new1 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=40) + batches.append(new1.items[0].batch_no) + # assert old queue remains + self.assertSLEs(new1, update_invariants([ + {"actual_qty": 10, "stock_value_difference": 10*40, "stock_queue": [[10, 10], [10, 20], [5, 15]]}, + ])) + + new2 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=42) + batches.append(new2.items[0].batch_no) + self.assertSLEs(new2, update_invariants([ + {"actual_qty": 10, "stock_value_difference": 10*42, "stock_queue": [[10, 10], [10, 20], [5, 15]]}, + ])) + + # consume old batch as per FIFO + consume_old1 = make_stock_entry(item_code=item_code, source=warehouse, qty=15, batch_no=batches[0]) + self.assertSLEs(consume_old1, update_invariants([ + {"actual_qty": -15, "stock_value_difference": -10*10 - 5*20, "stock_queue": [[5, 20], [5, 15]]}, + ])) + + # consume new batch as per batch + consume_new2 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-1]) + self.assertSLEs(consume_new2, update_invariants([ + {"actual_qty": -10, "stock_value_difference": -10*42, "stock_queue": [[5, 20], [5, 15]]}, + ])) + + # finish all old batches + consume_old2 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[1]) + self.assertSLEs(consume_old2, update_invariants([ + {"actual_qty": -10, "stock_value_difference": -5*20 - 5*15, "stock_queue": []}, + ])) + + # finish all new batches + consume_new1 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-2]) + self.assertSLEs(consume_new1, update_invariants([ + {"actual_qty": -10, "stock_value_difference": -10*40, "stock_queue": []}, + ])) + + def create_repack_entry(**args): args = frappe._dict(args) @@ -661,7 +740,7 @@ def setup_item_valuation_test(valuation_method="FIFO", suffix=None, use_batchwis item = make_item( f"IV - Test Item {valuation_method} {suffix}", - dict(valuation_method=valuation_method, has_batch_no=1) + dict(valuation_method=valuation_method, has_batch_no=1, create_new_batch=1) ) warehouses = [create_warehouse(f"IV - Test Warehouse {i}") for i in ['J', 'K']] batches = [f"IV - Test Batch {i} {valuation_method} {suffix}" for i in batches_list] From 35483242b3864e09c635979afe7793aac7f12596 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Feb 2022 22:22:27 +0530 Subject: [PATCH 113/447] fix: extend round_off_if_near_zero fix to other methods --- erpnext/stock/stock_ledger.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 677266ee0c..de6c409d7c 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -743,12 +743,14 @@ class update_entries_after(object): incoming_rate = flt(sle.incoming_rate) actual_qty = flt(sle.actual_qty) - self.wh_data.qty_after_transaction += actual_qty + self.wh_data.qty_after_transaction = round_off_if_near_zero(self.wh_data.qty_after_transaction + actual_qty) if actual_qty > 0: stock_value_difference = incoming_rate * actual_qty else: - outgoing_rate = get_batch_incoming_rate(item_code=sle.item_code, warehouse=sle.warehouse, batch_no=sle.batch_no, posting_date=sle.posting_date, posting_time=sle.posting_time, creation=sle.creation) + outgoing_rate = get_batch_incoming_rate(item_code=sle.item_code, + warehouse=sle.warehouse, batch_no=sle.batch_no, posting_date=sle.posting_date, + posting_time=sle.posting_time, creation=sle.creation) if outgoing_rate is None: # This can *only* happen if qty available for the batch is zero. # in such case fall back various other rates. @@ -757,7 +759,7 @@ class update_entries_after(object): outgoing_rate = self.get_fallback_rate(sle) stock_value_difference = outgoing_rate * actual_qty - self.wh_data.stock_value += stock_value_difference + self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + stock_value_difference) if self.wh_data.qty_after_transaction: self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction From 609d2fccad2a1b60a1e7ffd93f504f0e1329136d Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 20 Feb 2022 11:35:53 +0530 Subject: [PATCH 114/447] fix: reset stock value if no qty --- erpnext/stock/stock_ledger.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index de6c409d7c..1b90086440 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -468,6 +468,8 @@ class update_entries_after(object): # rounding as per precision self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision) + if not self.wh_data.qty_after_transaction: + self.wh_data.stock_value = 0.0 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 From 6b0bc350636776fbec3edc254086462a7670649c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 20 Feb 2022 12:05:58 +0530 Subject: [PATCH 115/447] test: mixed moving average items --- .../test_stock_ledger_entry.py | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) 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 b0df45ffd4..9e819dd658 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 @@ -595,7 +595,7 @@ class TestStockLedgerEntry(ERPNextTestCase): for details in details_list: check_sle_details_against_expected(*details) - def test_mixed_valuation_batches(self): + def test_mixed_valuation_batches_fifo(self): item_code, warehouses, batches = setup_item_valuation_test(use_batchwise_valuation=0) warehouse = warehouses[0] @@ -665,6 +665,34 @@ class TestStockLedgerEntry(ERPNextTestCase): {"actual_qty": -10, "stock_value_difference": -10*40, "stock_queue": []}, ])) + def test_mixed_valuation_batches_moving_average(self): + item_code, warehouses, batches = setup_item_valuation_test(use_batchwise_valuation=0, valuation_method="Moving Average") + warehouse = warehouses[0] + + make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0], + qty=10, rate=10) + make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[1], + qty=10, rate=20) + make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0], + qty=5, rate=15) + + new1 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=40) + batches.append(new1.items[0].batch_no) + new2 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=42) + batches.append(new2.items[0].batch_no) + + # consume old batch as per FIFO + make_stock_entry(item_code=item_code, source=warehouse, qty=15, batch_no=batches[0]) + # consume new batch as per batch + make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-1]) + # finish all old batches + make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[1]) + + # finish all new batches + consume_new1 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-2]) + self.assertSLEs(consume_new1, ([ + {"stock_value": 0}, + ])) def create_repack_entry(**args): From f38690f7037c75bb1c5a5d946d686b40392a111a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 20 Feb 2022 12:58:53 +0530 Subject: [PATCH 116/447] fix: check if Moving average item can use batchwise valuation --- erpnext/stock/doctype/batch/batch.py | 32 ++++++++++++++++++++++++++++ erpnext/stock/utils.py | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 96751d6eae..b5e56ad301 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -6,6 +6,7 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.model.naming import make_autoname, revert_series_if_last +from frappe.query_builder.functions import Sum from frappe.utils import cint, flt, get_link_to_form from frappe.utils.data import add_days from frappe.utils.jinja import render_template @@ -110,11 +111,15 @@ class Batch(Document): def validate(self): self.item_has_batch_enabled() + self.set_batchwise_valuation() def item_has_batch_enabled(self): if frappe.db.get_value("Item", self.item, "has_batch_no") == 0: frappe.throw(_("The selected item cannot have Batch")) + def set_batchwise_valuation(self): + self.use_batchwise_valuation = int(can_use_batchwise_valuation(self.item)) + def before_save(self): has_expiry_date, shelf_life_in_days = frappe.db.get_value('Item', self.item, ['has_expiry_date', 'shelf_life_in_days']) if not self.expiry_date and has_expiry_date and shelf_life_in_days: @@ -338,3 +343,30 @@ def get_pos_reserved_batch_qty(filters): flt_reserved_batch_qty = flt(reserved_batch_qty[0][0]) return flt_reserved_batch_qty + +def can_use_batchwise_valuation(item_code: str) -> bool: + """ Check if item can use batchwise valuation. + + Note: Item with existing moving average batches can't use batchwise valuation + until they are exhausted. + """ + from erpnext.stock.stock_ledger import get_valuation_method + batch = frappe.qb.DocType("Batch") + + if get_valuation_method(item_code) != "Moving Average": + return True + + batch_qty = ( + frappe.qb + .from_(batch) + .select(Sum(batch.batch_qty)) + .where( + (batch.use_batchwise_valuation == 0) + & (batch.item == item_code) + ) + ).run() + + if batch_qty and batch_qty[0][0]: + return False + + return True diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index e2bd2f197d..f85a04f944 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -261,7 +261,7 @@ def get_valuation_method(item_code): """get valuation method from item or default""" val_method = frappe.db.get_value('Item', item_code, 'valuation_method', cache=True) if not val_method: - val_method = frappe.db.get_value("Stock Settings", None, "valuation_method") or "FIFO" + val_method = frappe.db.get_value("Stock Settings", None, "valuation_method", cache=True) or "FIFO" return val_method def get_fifo_rate(previous_stock_queue, qty): From 75fb5616987066b83b69455b4eb59d1a715b280e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 21 Feb 2022 11:08:57 +0530 Subject: [PATCH 117/447] test: force correct flag in test data --- .../doctype/stock_ledger_entry/test_stock_ledger_entry.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 9e819dd658..c65ed2888e 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 @@ -778,13 +778,15 @@ def setup_item_valuation_test(valuation_method="FIFO", suffix=None, use_batchwis ubw = use_batchwise_valuation if isinstance(use_batchwise_valuation, (list, tuple)): ubw = use_batchwise_valuation[i] - make_batch( - frappe._dict( + batch = frappe.get_doc(frappe._dict( + doctype="Batch", batch_id=batch_id, item=item.item_code, use_batchwise_valuation=ubw ) - ) + ).insert() + batch.use_batchwise_valuation = ubw + batch.db_update() return item.item_code, warehouses, batches From af9fa049c749c9f72f0b21a5960111cb6ec57c12 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 21 Feb 2022 12:28:19 +0530 Subject: [PATCH 118/447] fix: batchwise valuation can only be used by FIFO/LIFO --- erpnext/stock/doctype/batch/batch.py | 24 ++------------- .../test_stock_ledger_entry.py | 30 ------------------- 2 files changed, 2 insertions(+), 52 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index b5e56ad301..93e8d41367 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -6,7 +6,6 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.model.naming import make_autoname, revert_series_if_last -from frappe.query_builder.functions import Sum from frappe.utils import cint, flt, get_link_to_form from frappe.utils.data import add_days from frappe.utils.jinja import render_template @@ -347,26 +346,7 @@ def get_pos_reserved_batch_qty(filters): def can_use_batchwise_valuation(item_code: str) -> bool: """ Check if item can use batchwise valuation. - Note: Item with existing moving average batches can't use batchwise valuation - until they are exhausted. - """ + Note: Moving average valuation method can not use batch_wise_valuation.""" from erpnext.stock.stock_ledger import get_valuation_method - batch = frappe.qb.DocType("Batch") - if get_valuation_method(item_code) != "Moving Average": - return True - - batch_qty = ( - frappe.qb - .from_(batch) - .select(Sum(batch.batch_qty)) - .where( - (batch.use_batchwise_valuation == 0) - & (batch.item == item_code) - ) - ).run() - - if batch_qty and batch_qty[0][0]: - return False - - return True + return get_valuation_method(item_code) != "Moving Average" 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 c65ed2888e..0864ece995 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 @@ -665,36 +665,6 @@ class TestStockLedgerEntry(ERPNextTestCase): {"actual_qty": -10, "stock_value_difference": -10*40, "stock_queue": []}, ])) - def test_mixed_valuation_batches_moving_average(self): - item_code, warehouses, batches = setup_item_valuation_test(use_batchwise_valuation=0, valuation_method="Moving Average") - warehouse = warehouses[0] - - make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0], - qty=10, rate=10) - make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[1], - qty=10, rate=20) - make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0], - qty=5, rate=15) - - new1 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=40) - batches.append(new1.items[0].batch_no) - new2 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=42) - batches.append(new2.items[0].batch_no) - - # consume old batch as per FIFO - make_stock_entry(item_code=item_code, source=warehouse, qty=15, batch_no=batches[0]) - # consume new batch as per batch - make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-1]) - # finish all old batches - make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[1]) - - # finish all new batches - consume_new1 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-2]) - self.assertSLEs(consume_new1, ([ - {"stock_value": 0}, - ])) - - def create_repack_entry(**args): args = frappe._dict(args) repack = frappe.new_doc("Stock Entry") From 9661058cc7daf9802e054f3fcd99c7852ff935a4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 21 Feb 2022 18:16:10 +0530 Subject: [PATCH 119/447] fix: only set batchwise valuation flag if new batch --- erpnext/stock/doctype/batch/batch.json | 6 ++++-- erpnext/stock/doctype/batch/batch.py | 13 ++++--------- erpnext/stock/doctype/batch/test_batch.py | 20 ++++++++++++++++++++ 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.json b/erpnext/stock/doctype/batch/batch.json index 0d28ea0919..967c5729bf 100644 --- a/erpnext/stock/doctype/batch/batch.json +++ b/erpnext/stock/doctype/batch/batch.json @@ -194,7 +194,7 @@ "fieldtype": "Column Break" }, { - "default": "1", + "default": "0", "fieldname": "use_batchwise_valuation", "fieldtype": "Check", "label": "Use Batch-wise Valuation", @@ -207,10 +207,11 @@ "image_field": "image", "links": [], "max_attachments": 5, - "modified": "2021-10-11 13:38:12.806976", + "modified": "2022-02-21 08:08:23.999236", "modified_by": "Administrator", "module": "Stock", "name": "Batch", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -231,6 +232,7 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "batch_id", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 93e8d41367..c9b4c147f1 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -117,7 +117,10 @@ class Batch(Document): frappe.throw(_("The selected item cannot have Batch")) def set_batchwise_valuation(self): - self.use_batchwise_valuation = int(can_use_batchwise_valuation(self.item)) + from erpnext.stock.stock_ledger import get_valuation_method + + if self.is_new() and get_valuation_method(self.item) != "Moving Average": + self.use_batchwise_valuation = 1 def before_save(self): has_expiry_date, shelf_life_in_days = frappe.db.get_value('Item', self.item, ['has_expiry_date', 'shelf_life_in_days']) @@ -342,11 +345,3 @@ def get_pos_reserved_batch_qty(filters): flt_reserved_batch_qty = flt(reserved_batch_qty[0][0]) return flt_reserved_batch_qty - -def can_use_batchwise_valuation(item_code: str) -> bool: - """ Check if item can use batchwise valuation. - - Note: Moving average valuation method can not use batch_wise_valuation.""" - from erpnext.stock.stock_ledger import get_valuation_method - - return get_valuation_method(item_code) != "Moving Average" diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index 6495b56e92..baa03024af 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -6,6 +6,7 @@ import json import frappe from frappe.exceptions import ValidationError from frappe.utils import cint, flt +from frappe.utils.data import add_to_date, getdate from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.stock.doctype.batch.batch import UnableToSelectBatchError, get_batch_no, get_batch_qty @@ -387,6 +388,25 @@ class TestBatch(ERPNextTestCase): assertValuation((20 * 20 + 10 * 25) / (10 + 20)) + def test_update_batch_properties(self): + item_code = "_TestBatchWiseVal" + self.make_batch_item(item_code) + + se = make_stock_entry(item_code=item_code, qty=100, rate=10, target="_Test Warehouse - _TC") + batch_no = se.items[0].batch_no + batch = frappe.get_doc("Batch", batch_no) + + expiry_date = add_to_date(batch.manufacturing_date, days=30) + + batch.expiry_date = expiry_date + batch.save() + + batch.reload() + + self.assertEqual(getdate(batch.expiry_date), getdate(expiry_date)) + + + def create_batch(item_code, rate, create_item_price_for_batch): pi = make_purchase_invoice(company="_Test Company", warehouse= "Stores - _TC", cost_center = "Main - _TC", update_stock=1, From e4c4dc402e75d3ec501095fa3e914553fcd07a4d Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Mon, 21 Feb 2022 19:49:19 +0530 Subject: [PATCH 120/447] fix: JobCard TimeLog to_date (#29872) --- erpnext/manufacturing/doctype/job_card/job_card.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 8d00019b7d..9f4ace296e 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -62,7 +62,7 @@ class JobCard(Document): if self.get('time_logs'): for d in self.get('time_logs'): - if get_datetime(d.from_time) > get_datetime(d.to_time): + if d.to_time and get_datetime(d.from_time) > get_datetime(d.to_time): frappe.throw(_("Row {0}: From time must be less than to time").format(d.idx)) data = self.get_overlap_for(d) From 87b59fc96c7bb37fcfbce097bd7c8184fce967ba Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 21 Feb 2022 22:53:29 +0530 Subject: [PATCH 121/447] fix(LMS): program enrollment does not give any feedback (#29922) --- erpnext/www/lms/macros/hero.html | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/erpnext/www/lms/macros/hero.html b/erpnext/www/lms/macros/hero.html index e72bfc8175..95ba8f7df2 100644 --- a/erpnext/www/lms/macros/hero.html +++ b/erpnext/www/lms/macros/hero.html @@ -11,7 +11,7 @@ {% if frappe.session.user == 'Guest' %} {{_('Sign Up')}} {% elif not has_access %} - + {% endif %}

@@ -20,34 +20,35 @@ From 4738367d6407e9ffc22ba2c9ef1649573608be50 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 21 Feb 2022 22:54:46 +0530 Subject: [PATCH 122/447] fix: boarding task dates not set when activity begins on is set to 0 (#29921) --- .../employee_boarding_controller.py | 4 +-- .../test_employee_onboarding.py | 32 +++++++++++++------ .../doctype/salary_slip/test_salary_slip.py | 6 ++-- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/erpnext/controllers/employee_boarding_controller.py b/erpnext/controllers/employee_boarding_controller.py index ae2c73758c..dd02ce1748 100644 --- a/erpnext/controllers/employee_boarding_controller.py +++ b/erpnext/controllers/employee_boarding_controller.py @@ -104,11 +104,11 @@ class EmployeeBoardingController(Document): def get_task_dates(self, activity, holiday_list): start_date = end_date = None - if activity.begin_on: + if activity.begin_on is not None: start_date = add_days(self.boarding_begins_on, activity.begin_on) start_date = self.update_if_holiday(start_date, holiday_list) - if activity.duration: + if activity.duration is not None: end_date = add_days(self.boarding_begins_on, activity.begin_on + activity.duration) end_date = self.update_if_holiday(end_date, holiday_list) diff --git a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py index 2d129c8acf..0fb821ddb2 100644 --- a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py +++ b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py @@ -4,7 +4,7 @@ import unittest import frappe -from frappe.utils import getdate +from frappe.utils import add_days, getdate from erpnext.hr.doctype.employee_onboarding.employee_onboarding import ( IncompleteTaskError, @@ -35,6 +35,15 @@ class TestEmployeeOnboarding(unittest.TestCase): # boarding status self.assertEqual(onboarding.boarding_status, 'Pending') + # start and end dates + start_date, end_date = frappe.db.get_value('Task', onboarding.activities[0].task, ['exp_start_date', 'exp_end_date']) + self.assertEqual(getdate(start_date), getdate(onboarding.boarding_begins_on)) + self.assertEqual(getdate(end_date), add_days(start_date, onboarding.activities[0].duration)) + + start_date, end_date = frappe.db.get_value('Task', onboarding.activities[1].task, ['exp_start_date', 'exp_end_date']) + self.assertEqual(getdate(start_date), add_days(onboarding.boarding_begins_on, onboarding.activities[0].duration)) + self.assertEqual(getdate(end_date), add_days(start_date, onboarding.activities[1].duration)) + # complete the task project = frappe.get_doc('Project', onboarding.project) for task in frappe.get_all('Task', dict(project=project.name)): @@ -57,10 +66,7 @@ class TestEmployeeOnboarding(unittest.TestCase): self.assertEqual(employee.employee_name, 'Test Researcher') def tearDown(self): - for entry in frappe.get_all('Employee Onboarding'): - doc = frappe.get_doc('Employee Onboarding', entry.name) - doc.cancel() - doc.delete() + frappe.db.rollback() def get_job_applicant(): @@ -87,23 +93,31 @@ def get_job_offer(applicant_name): def create_employee_onboarding(): applicant = get_job_applicant() job_offer = get_job_offer(applicant.name) - holiday_list = make_holiday_list() + + holiday_list = make_holiday_list('_Test Employee Boarding') + holiday_list = frappe.get_doc('Holiday List', holiday_list) + holiday_list.holidays = [] + holiday_list.save() onboarding = frappe.new_doc('Employee Onboarding') onboarding.job_applicant = applicant.name onboarding.job_offer = job_offer.name onboarding.date_of_joining = onboarding.boarding_begins_on = getdate() onboarding.company = '_Test Company' - onboarding.holiday_list = holiday_list + onboarding.holiday_list = holiday_list.name onboarding.designation = 'Researcher' onboarding.append('activities', { 'activity_name': 'Assign ID Card', 'role': 'HR User', - 'required_for_employee_creation': 1 + 'required_for_employee_creation': 1, + 'begin_on': 0, + 'duration': 1 }) onboarding.append('activities', { 'activity_name': 'Assign a laptop', - 'role': 'HR User' + 'role': 'HR User', + 'begin_on': 1, + 'duration': 1 }) onboarding.status = 'Pending' onboarding.insert() diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index daa0f8952b..6a5debf998 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -1019,13 +1019,13 @@ def setup_test(): frappe.db.set_value('HR Settings', None, 'leave_status_notification_template', None) frappe.db.set_value('HR Settings', None, 'leave_approval_notification_template', None) -def make_holiday_list(): +def make_holiday_list(holiday_list_name=None): fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company()) - holiday_list = frappe.db.exists("Holiday List", "Salary Slip Test Holiday List") + holiday_list = frappe.db.exists("Holiday List", holiday_list_name or "Salary Slip Test Holiday List") if not holiday_list: holiday_list = frappe.get_doc({ "doctype": "Holiday List", - "holiday_list_name": "Salary Slip Test Holiday List", + "holiday_list_name": holiday_list_name or "Salary Slip Test Holiday List", "from_date": fiscal_year[1], "to_date": fiscal_year[2], "weekly_off": "Sunday" From 70b960e650bbc1c418eecd14ac42d64a3103a43c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 22 Feb 2022 09:16:08 +0530 Subject: [PATCH 123/447] fix: Account filter in PSOA --- .../process_statement_of_accounts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 09aa72352e..1b34d6d1f2 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -73,7 +73,7 @@ def get_report_pdf(doc, consolidated=True): 'to_date': doc.to_date, 'company': doc.company, 'finance_book': doc.finance_book if doc.finance_book else None, - 'account': doc.account if doc.account else None, + 'account': [doc.account] if doc.account else None, 'party_type': 'Customer', 'party': [entry.customer], 'presentation_currency': presentation_currency, From d011a3f82c5cf9c1dc4fe0561194d47cff6099d0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 22 Feb 2022 11:41:09 +0530 Subject: [PATCH 124/447] fix(Salary Slip): TypeError while clearing any amount field in components (#29931) --- 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 f727ff4378..d2a39989a6 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -1268,7 +1268,7 @@ class SalarySlip(TransactionBase): for i, earning in enumerate(self.earnings): if earning.salary_component == salary_component: self.earnings[i].amount = wages_amount - self.gross_pay += self.earnings[i].amount + self.gross_pay += flt(self.earnings[i].amount, earning.precision("amount")) self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) def compute_year_to_date(self): From 235fc127b3ecf943176ed9c208425f9bda100798 Mon Sep 17 00:00:00 2001 From: Marica Date: Tue, 22 Feb 2022 12:53:46 +0530 Subject: [PATCH 125/447] fix: Fetch conversion factor even if it already existed in row, on item change (#29917) * fix: Fetch conversion factor even if it already existed in row, on item change * fix: Retain manually changed conversion factor - If item code changes, reset conversion factor on client side - Keep API behavious consistent, if conversion factor is sent, same must come back - API should not ideally reset values in most cases --- erpnext/public/js/controllers/transaction.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 933ced0bd7..ae8c0c8c6d 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -525,6 +525,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe item.weight_per_unit = 0; item.weight_uom = ''; + item.conversion_factor = 0; if(['Sales Invoice'].includes(this.frm.doc.doctype)) { update_stock = cint(me.frm.doc.update_stock); From 8005fee6569abed607c57d31b26531925fd7e15b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 22 Feb 2022 15:06:19 +0530 Subject: [PATCH 126/447] feat: update ordered qty for packed items --- .../doctype/purchase_order/purchase_order.py | 10 +++++++++ .../purchase_order_item.json | 13 ++++++++++- .../doctype/sales_order/sales_order.js | 22 ++++++++++++++++++- .../doctype/sales_order/sales_order.py | 5 +++++ .../doctype/packed_item/packed_item.json | 11 +++++++++- 5 files changed, 58 insertions(+), 3 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 1b5f35efbb..2e7d3063cc 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -316,6 +316,16 @@ class PurchaseOrder(BuyingController): 'target_ref_field': 'stock_qty', 'source_field': 'stock_qty' }) + self.status_updater.append({ + 'source_dt': 'Purchase Order Item', + 'target_dt': 'Packed Item', + 'target_field': 'ordered_qty', + 'target_parent_dt': 'Sales Order', + 'target_parent_field': '', + 'join_field': 'sales_order_packed_item', + 'target_ref_field': 'qty', + 'source_field': 'stock_qty' + }) def update_delivered_qty_in_sales_order(self): """Update delivered qty in Sales Order for drop ship""" 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 87cd57517e..c26d592e3e 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -63,6 +63,7 @@ "material_request_item", "sales_order", "sales_order_item", + "sales_order_packed_item", "supplier_quotation", "supplier_quotation_item", "col_break5", @@ -837,21 +838,31 @@ "label": "Product Bundle", "options": "Product Bundle", "read_only": 1 + }, + { + "fieldname": "sales_order_packed_item", + "fieldtype": "Data", + "label": "Sales Order Packed Item", + "no_copy": 1, + "print_hide": 1, + "search_index": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-08-30 20:06:26.712097", + "modified": "2022-02-02 13:10:18.398976", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "quick_entry": 1, "search_fields": "item_name", "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index eb98e6c0bf..f80eaf2757 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -562,6 +562,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex var me = this; var dialog = new frappe.ui.Dialog({ title: __("Select Items"), + size: "large", fields: [ { "fieldtype": "Check", @@ -663,7 +664,8 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex } else { let po_items = []; me.frm.doc.items.forEach(d => { - let pending_qty = (flt(d.stock_qty) - flt(d.ordered_qty)) / flt(d.conversion_factor); + let ordered_qty = me.get_ordered_qty(d, me.frm.doc); + let pending_qty = (flt(d.stock_qty) - ordered_qty) / flt(d.conversion_factor); if (pending_qty > 0) { po_items.push({ "doctype": "Sales Order Item", @@ -689,6 +691,24 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex dialog.show(); } + get_ordered_qty(item, so) { + let ordered_qty = item.ordered_qty; + if (so.packed_items) { + // calculate ordered qty based on packed items in case of product bundle + let packed_items = so.packed_items.filter( + (pi) => pi.parent_detail_docname == item.name + ); + if (packed_items) { + ordered_qty = packed_items.reduce( + (sum, pi) => sum + flt(pi.ordered_qty), + 0 + ); + ordered_qty = ordered_qty / packed_items.length; + } + } + return ordered_qty; + } + hold_sales_order(){ var me = this; var d = new frappe.ui.Dialog({ diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 0f5b1e3b89..abbb3c9b90 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -877,6 +877,9 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty)) target.project = source_parent.project + def update_item_for_packed_item(source, target, source_parent): + target.qty = flt(source.qty) - flt(source.ordered_qty) + # po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")}) doc = get_mapped_doc("Sales Order", source_name, { "Sales Order": { @@ -920,6 +923,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): "Packed Item": { "doctype": "Purchase Order Item", "field_map": [ + ["name", "sales_order_packed_item"], ["parent", "sales_order"], ["uom", "uom"], ["conversion_factor", "conversion_factor"], @@ -934,6 +938,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): "supplier", "pricing_rules" ], + "postprocess": update_item_for_packed_item, "condition": lambda doc: doc.parent_item in items_to_map } }, target_doc, set_missing_values) diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index d2d4789765..d6e2e9ce2d 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -26,6 +26,7 @@ "section_break_13", "actual_qty", "projected_qty", + "ordered_qty", "column_break_16", "incoming_rate", "page_break", @@ -224,13 +225,21 @@ "label": "Rate", "print_hide": 1, "read_only": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "ordered_qty", + "fieldtype": "Float", + "label": "Ordered Qty", + "no_copy": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-01-28 16:03:30.780111", + "modified": "2022-02-22 12:57:45.325488", "modified_by": "Administrator", "module": "Stock", "name": "Packed Item", From 8e3f1e306d705109a51271ba262b46fe4798a793 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 22 Feb 2022 15:34:26 +0530 Subject: [PATCH 127/447] test: po updates packed item's ordered_qty --- .../doctype/sales_order/test_sales_order.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 73c5bd299a..e56e56cea1 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -959,6 +959,42 @@ class TestSalesOrder(ERPNextTestCase): self.assertEqual(purchase_order.items[0].item_code, "_Test Bundle Item 1") self.assertEqual(purchase_order.items[1].item_code, "_Test Bundle Item 2") + def test_purchase_order_updates_packed_item_ordered_qty(self): + """ + Tests if the packed item's `ordered_qty` is updated with the quantity of the Purchase Order + """ + from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order + + product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0}) + make_item("_Test Bundle Item 1", {"is_stock_item": 1}) + make_item("_Test Bundle Item 2", {"is_stock_item": 1}) + + make_product_bundle("_Test Product Bundle", + ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + + so_items = [ + { + "item_code": product_bundle.item_code, + "warehouse": "", + "qty": 2, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier' + } + ] + + so = make_sales_order(item_list=so_items) + + purchase_order = make_purchase_order(so.name, selected_items=so_items) + purchase_order.supplier = "_Test Supplier" + purchase_order.set_warehouse = "_Test Warehouse - _TC" + purchase_order.save() + purchase_order.submit() + + so.reload() + self.assertEqual(so.packed_items[0].ordered_qty, 2) + self.assertEqual(so.packed_items[1].ordered_qty, 2) + 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 3a547cb0d965b8012136d06adc9d7c7b94700660 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 22 Feb 2022 16:25:32 +0530 Subject: [PATCH 128/447] fix: Item discounts for quotation --- erpnext/controllers/taxes_and_totals.py | 2 +- erpnext/selling/doctype/quotation/quotation.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 2776628227..52190765c9 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -116,7 +116,7 @@ class calculate_taxes_and_totals(object): if item.discount_percentage == 100: item.rate = 0.0 elif item.price_list_rate: - if not item.rate or (item.pricing_rules and item.discount_percentage > 0): + if item.pricing_rules or item.discount_percentage > 0: item.rate = flt(item.price_list_rate * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate")) item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0) diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 0e1a915deb..34e9a52e11 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -40,7 +40,6 @@ frappe.ui.form.on('Quotation', { erpnext.selling.QuotationController = class QuotationController extends erpnext.selling.SellingController { onload(doc, dt, dn) { - var me = this; super.onload(doc, dt, dn); } party_name() { From 7f55226a5807645db4f93c8038f1cc03a6fc0ce6 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 22 Feb 2022 16:55:43 +0530 Subject: [PATCH 129/447] fix: remove customer field value when MR is not customer provided (#29938) --- .../stock/doctype/material_request/material_request.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index b39328f85b..51209acb27 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -56,14 +56,13 @@ class MaterialRequest(BuyingController): if actual_so_qty and (flt(so_items[so_no][item]) + already_indented > actual_so_qty): frappe.throw(_("Material Request of maximum {0} can be made for Item {1} against Sales Order {2}").format(actual_so_qty - already_indented, item, so_no)) - # Validate - # --------------------- def validate(self): super(MaterialRequest, self).validate() self.validate_schedule_date() self.check_for_on_hold_or_closed_status('Sales Order', 'sales_order') self.validate_uom_is_integer("uom", "qty") + self.validate_material_request_type() if not self.status: self.status = "Draft" @@ -83,6 +82,12 @@ class MaterialRequest(BuyingController): self.reset_default_field_value("set_warehouse", "items", "warehouse") self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") + def validate_material_request_type(self): + """ Validate fields in accordance with selected type """ + + if self.material_request_type != "Customer Provided": + self.customer = None + def set_title(self): '''Set title as comma separated list of items''' if not self.title: From 745f7bc5f0fd014dcc837c41e2058be91166e1b4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 22 Feb 2022 17:03:11 +0530 Subject: [PATCH 130/447] docs: add human readable specifications for stock ledger (#29308) * docs: add human readable specifications for stock ledger * docs: reposting technical implementation notes --- erpnext/stock/spec/README.md | 103 ++++++++++++++++++++++++++++++++ erpnext/stock/spec/reposting.md | 38 ++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 erpnext/stock/spec/README.md create mode 100644 erpnext/stock/spec/reposting.md diff --git a/erpnext/stock/spec/README.md b/erpnext/stock/spec/README.md new file mode 100644 index 0000000000..f5a3501fe4 --- /dev/null +++ b/erpnext/stock/spec/README.md @@ -0,0 +1,103 @@ +# Implementation notes for Stock Ledger + + +## Important files + +- `stock/stock_ledger.py` +- `controllers/stock_controller.py` +- `stock/valuation.py` + +## What is in an Stock Ledger Entry (SLE)? + +Stock Ledger Entry is a single row in the Stock Ledger. It signifies some +modification of stock for a particular Item in the specified warehouse. + +- `item_code`: item for which ledger entry is made +- `warehouse`: warehouse where inventory is affected +- `actual_qty`: change in qty +- `qty_after_transaction`: quantity available after the transaction is processed +- `incoming_rate`: rate at which inventory was received. +- `is_cancelled`: if 1 then stock ledger entry is cancelled and should not be used +for any business logic except for the code that handles cancellation. +- `posting_date` & `posting_time`: Specify the temporal ordering of stock ledger + entries. Ties are broken by `creation` timestamp. +- `voucher_type`: Many transaction can create SLE, e.g. Stock Entry, Purchase + Invoice +- `voucher_no`: `name` of the transaction that created SLE +- `voucher_detail_no`: `name` of the child table row from parent transaction + that created the SLE. +- `dependant_sle_voucher_detail_no`: cross-warehouse transfers need this + reference in order to update dependent warehouse rates in case of change in + rate. +- `recalculate_rate`: if this is checked in/out rates are recomputed on + transactions. +- `valuation_rate`: current average valuation rate. +- `stock_value`: current total stock value +- `stock_value_difference`: stock value difference made between last and current + entry. This value is booked in accounting ledger. +- `stock_queue`: if FIFO/LIFO is used this represents queue/stack maintained for + computing incoming rate for inventory getting consumed. +- `batch_no`: batch no for which stock entry is made; each stock entry can only + affect one batch number. +- `serial_no`: newline separated list of serial numbers that were added (if + actual_qty > 0) or else removed. Currently multiple serial nos can have single + SLE but this will likely change in future. + + +## Implementation of Stock Ledger + +Stock Ledger Entry affects stock of combinations of (item_code, warehouse) and +optionally batch no if specified. For simplicity, lets avoid batch no. for now. + + +Stock Ledger Entry table stores stock ledger for all combinations of item_code +and warehouse. So whenever any operations are to be performed on said +item-warehouse combination stock ledger is filtered and sorted by posting +datetime. A typical query that will give you individual ledger looks like this: + +```sql +select * +from `tabStock Ledger Entry` as sle +where + is_cancelled = 0 --- cancelled entries don't affect ledger + and item_code = 'item_code' and warehouse = 'warehouse_name' +order by timestamp(posting_date, posting_time), creation +``` + +New entry is just an update to the last entry which is found by looking at last +row in the filter ledger. + + +### Serial nos + +Serial numbers do not follow any valuation method configuration and they are +consumed at rate they were produced unless they are grouped in which case they +are consumed at weighted average rate. + + +### Batch Nos + +Batches are currently NOT consumed as per batch wise valuation rate, instead +global FIFO queue for the item is used for valuation rate. + + +## Creation process of SLEs + +- SLE creation is usually triggered by Stock Transactions using a method + conventionally named `update_stock_ledger()` This might not be defined for + stock transaction and could be specified somewhere in inheritance hierarchy of + controllers. +- This method produces SLE objects which are processed by `make_sl_entries` in + `stock_ledger.py` which commits the SLE to database. +- `update_entries_after` class is used to process ONLY the inserted SLE's queue + and valuation. +- The change in qty is propagated to future entries immediately. Valuation and + queue for future entries is processed in background using repost item + valuation. + + +## Accounting impact + +- Accounting impact for stock transaction is handled by `get_gl_entries()` + method on controllers. Each transaction has different business logic for + booking the accounting impact. diff --git a/erpnext/stock/spec/reposting.md b/erpnext/stock/spec/reposting.md new file mode 100644 index 0000000000..b0d59fe9bb --- /dev/null +++ b/erpnext/stock/spec/reposting.md @@ -0,0 +1,38 @@ +# Stock Reposting + +Stock "reposting" is process of re-processing Stock Ledger Entry and GL Entries +in event of backdated stock transaction. + +*Backdated stock transaction*: Any stock transaction for which some +item-warehouse combination has a future transactions. + +## Why is this required? +Stock Ledger is stateful, it maintains queue, qty at any +point in time. So if you do a backdated transaction all future values change, +queues need to be re-evaluated etc. Watch Nabin and Rohit's conference +presentation for explanation: https://www.youtube.com/watch?v=mw3WAnekGIM + +## How is this implemented? +Whenever backdated transaction is detected, instead of +fully processing it while submitting, the processing is queued using "Repost +Item Valuation" doctype. Every hour a scheduled job runs and processes this +queue (for up to maximum of 25 minutes) + + +## Queue implementation +- "Repost item valuation" (RIV) is automatically submitted from backdated transactions. (check stock_controller.py) +- Draft and cancelled RIV are ignored. +- Keep filter of "submitted" documents when doing anything with RIVs. +- The default status is "Queued". +- When background job runs, it picks the oldest pending reposts and changes the status to "In Progress" and when it finishes it +changes to "Completed" +- There are two more status: "Failed" when reposting failed and "Skipped" when reposting is deemed not necessary so it's skipped. +- technical detail: Entry point for whole process is "repost_entries" function in repost_item_valuation.py + + +## How to identify broken stock data: +There are 4 major reports for checking broken stock data: +- Incorrect balance qty after the transaction - to check if the running total of qty isn't correct. +- Incorrect stock value report - to check incorrect value books in accounts for stock transactions +- Incorrect serial no valuation -specific to serial nos +- Stock ledger invariant check - combined report for checking qty, running total, queue, balance value etc From 1682a26fe69b9b3fa64293e692e79a553b842ca2 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Tue, 22 Feb 2022 17:20:48 +0530 Subject: [PATCH 131/447] fix: Taxjar minor fixes --- .../taxjar_integration.py | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/erpnext/erpnext_integrations/taxjar_integration.py b/erpnext/erpnext_integrations/taxjar_integration.py index a4e21579e3..14c86d5632 100644 --- a/erpnext/erpnext_integrations/taxjar_integration.py +++ b/erpnext/erpnext_integrations/taxjar_integration.py @@ -8,10 +8,6 @@ from frappe.utils import cint, flt from erpnext import get_default_company, get_region -TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head") -SHIP_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "shipping_account_head") -TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions") -TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax") SUPPORTED_COUNTRY_CODES = ["AT", "AU", "BE", "BG", "CA", "CY", "CZ", "DE", "DK", "EE", "ES", "FI", "FR", "GB", "GR", "HR", "HU", "IE", "IT", "LT", "LU", "LV", "MT", "NL", "PL", "PT", "RO", "SE", "SI", "SK", "US"] @@ -35,12 +31,14 @@ def get_client(): if api_key and api_url: client = taxjar.Client(api_key=api_key, api_url=api_url) client.set_api_config('headers', { - 'x-api-version': '2020-08-07' + 'x-api-version': '2022-01-24' }) return client def create_transaction(doc, method): + TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions") + """Create an order transaction in TaxJar""" if not TAXJAR_CREATE_TRANSACTIONS: @@ -51,6 +49,7 @@ def create_transaction(doc, method): if not client: return + TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head") sales_tax = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == TAX_ACCOUNT_HEAD]) if not sales_tax: @@ -79,6 +78,7 @@ def create_transaction(doc, method): def delete_transaction(doc, method): """Delete an existing TaxJar order transaction""" + TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions") if not TAXJAR_CREATE_TRANSACTIONS: return @@ -92,6 +92,8 @@ def delete_transaction(doc, method): def get_tax_data(doc): + SHIP_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "shipping_account_head") + from_address = get_company_address_details(doc) from_shipping_state = from_address.get("state") from_country_code = frappe.db.get_value("Country", from_address.country, "code") @@ -113,20 +115,20 @@ def get_tax_data(doc): to_shipping_state = get_state_code(to_address, 'Shipping') tax_dict = { - 'from_country': from_country_code, - 'from_zip': from_address.pincode, - 'from_state': from_shipping_state, - 'from_city': from_address.city, - 'from_street': from_address.address_line1, - 'to_country': to_country_code, - 'to_zip': to_address.pincode, - 'to_city': to_address.city, - 'to_street': to_address.address_line1, - 'to_state': to_shipping_state, - 'shipping': shipping, - 'amount': doc.net_total, - 'plugin': 'erpnext', - 'line_items': line_items + "from_country": from_country_code, + "from_zip": from_address.pincode, + "from_state": from_shipping_state, + "from_city": from_address.city, + "from_street": from_address.address_line1, + "to_country": to_country_code, + "to_zip": to_address.pincode, + "to_city": to_address.city, + "to_street": to_address.address_line1, + "to_state": to_shipping_state, + "shipping": shipping, + "amount": doc.net_total, + "plugin": "erpnext", + "line_items": line_items } return tax_dict @@ -156,6 +158,9 @@ def get_line_item_dict(item, docstatus): return tax_dict def set_sales_tax(doc, method): + TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head") + TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax") + if not TAXJAR_CALCULATE_TAX: return @@ -206,6 +211,7 @@ def set_sales_tax(doc, method): doc.run_method("calculate_taxes_and_totals") def check_for_nexus(doc, tax_dict): + TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head") if not frappe.db.get_value('TaxJar Nexus', {'region_code': tax_dict["to_state"]}): for item in doc.get("items"): item.tax_collectable = flt(0) @@ -218,6 +224,8 @@ def check_for_nexus(doc, tax_dict): def check_sales_tax_exemption(doc): # if the party is exempt from sales tax, then set all tax account heads to zero + TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head") + sales_tax_exempted = hasattr(doc, "exempt_from_sales_tax") and doc.exempt_from_sales_tax \ or frappe.db.has_column("Customer", "exempt_from_sales_tax") \ and frappe.db.get_value("Customer", doc.customer, "exempt_from_sales_tax") From 5d403449bdcbe514c33b8807b674fd23ba24d93a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 22 Feb 2022 19:24:49 +0530 Subject: [PATCH 132/447] test: move report tests to subttest (#29945) Basically failfast=False but for sub-tests --- erpnext/accounts/test/test_reports.py | 15 ++++++++------- erpnext/manufacturing/report/test_reports.py | 15 ++++++++------- erpnext/stock/report/test_reports.py | 15 ++++++++------- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/erpnext/accounts/test/test_reports.py b/erpnext/accounts/test/test_reports.py index 78c109ab94..4ed966dcb9 100644 --- a/erpnext/accounts/test/test_reports.py +++ b/erpnext/accounts/test/test_reports.py @@ -39,10 +39,11 @@ class TestReports(unittest.TestCase): def test_execute_all_accounts_reports(self): """Test that all script report in stock modules are executable with supported filters""" for report, filter in REPORT_FILTER_TEST_CASES: - execute_script_report( - report_name=report, - module="Accounts", - filters=filter, - default_filters=DEFAULT_FILTERS, - optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None, - ) + with self.subTest(report=report): + execute_script_report( + report_name=report, + module="Accounts", + filters=filter, + default_filters=DEFAULT_FILTERS, + optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None, + ) diff --git a/erpnext/manufacturing/report/test_reports.py b/erpnext/manufacturing/report/test_reports.py index 9f51ded6c7..e436fdca64 100644 --- a/erpnext/manufacturing/report/test_reports.py +++ b/erpnext/manufacturing/report/test_reports.py @@ -55,10 +55,11 @@ class TestManufacturingReports(unittest.TestCase): def test_execute_all_manufacturing_reports(self): """Test that all script report in manufacturing modules are executable with supported filters""" for report, filter in REPORT_FILTER_TEST_CASES: - execute_script_report( - report_name=report, - module="Manufacturing", - filters=filter, - default_filters=DEFAULT_FILTERS, - optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None, - ) + with self.subTest(report=report): + execute_script_report( + report_name=report, + module="Manufacturing", + filters=filter, + default_filters=DEFAULT_FILTERS, + optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None, + ) diff --git a/erpnext/stock/report/test_reports.py b/erpnext/stock/report/test_reports.py index 525af40b41..76c20798bf 100644 --- a/erpnext/stock/report/test_reports.py +++ b/erpnext/stock/report/test_reports.py @@ -73,10 +73,11 @@ class TestReports(unittest.TestCase): def test_execute_all_stock_reports(self): """Test that all script report in stock modules are executable with supported filters""" for report, filter in REPORT_FILTER_TEST_CASES: - execute_script_report( - report_name=report, - module="Stock", - filters=filter, - default_filters=DEFAULT_FILTERS, - optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None, - ) + with self.subTest(report=report): + execute_script_report( + report_name=report, + module="Stock", + filters=filter, + default_filters=DEFAULT_FILTERS, + optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None, + ) From a61790c00fa2b3c53ba49d930c7d08b3f0213b65 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 22 Feb 2022 20:58:10 +0530 Subject: [PATCH 133/447] fix: Remove unintended changes --- erpnext/controllers/accounts_controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 7913a39329..a94af10cde 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1954,7 +1954,8 @@ def update_bin_on_delete(row, doctype): qty_dict["ordered_qty"] = get_ordered_qty(row.item_code, row.warehouse) - update_bin_qty(row.item_code, row.warehouse, qty_dict) + if row.warehouse: + update_bin_qty(row.item_code, row.warehouse, qty_dict) def validate_and_delete_children(parent, data): deleted_children = [] From b0a1cd6a7bd9f0900d6f723c3b2cbf9037989fcc Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 23 Feb 2022 00:13:44 +0530 Subject: [PATCH 134/447] chore: Change heart icon to `icon-heart` and change var `icon-stroke` to accomodate changes in frappe icon - `icon-heart` got a stroke colour that needs to be overriden via var `icon-stroke - Use `icon-heart` instead of `icon-heart-active` as the latter has a color fill now --- erpnext/public/scss/shopping_cart.scss | 8 ++++---- erpnext/templates/includes/navbar/navbar_items.html | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index 4b645b9dde..666043b219 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -338,14 +338,14 @@ body.product-page { .btn-add-to-wishlist { svg use { - stroke: #F47A7A; + --icon-stroke: #F47A7A; } } .btn-view-in-wishlist { svg use { fill: #F47A7A; - stroke: none; + --icon-stroke: none; } } @@ -1022,7 +1022,7 @@ body.product-page { .not-wished { cursor: pointer; - stroke: #F47A7A !important; + --icon-stroke: #F47A7A !important; &:hover { fill: #F47A7A; @@ -1030,7 +1030,7 @@ body.product-page { } .wished { - stroke: none; + --icon-stroke: none; fill: #F47A7A !important; } diff --git a/erpnext/templates/includes/navbar/navbar_items.html b/erpnext/templates/includes/navbar/navbar_items.html index 327552117b..d7adae562e 100644 --- a/erpnext/templates/includes/navbar/navbar_items.html +++ b/erpnext/templates/includes/navbar/navbar_items.html @@ -13,7 +13,7 @@
  • {}
  • '.format(err) for err in errors]) - message = '''{} has following errors:
    -
      {}
    '''.format(docname, error_list) + error_list = "".join(["
  • {}
  • ".format(err) for err in errors]) + message = """{} has following errors:
    +
      {}
    """.format( + docname, error_list + ) else: - message = '{} - {}'.format(docname, message) + message = "{} - {}".format(docname, message) + + frappe.msgprint(message, title=_("Bulk E-Invoice Generation Complete"), indicator="red") - frappe.msgprint( - message, - title=_('Bulk E-Invoice Generation Complete'), - indicator='red' - ) @frappe.whitelist() def cancel_irns(docnames, reason, remark): @@ -1124,21 +1339,22 @@ def cancel_irns(docnames, reason, remark): success = len(docnames) - len(failures) frappe.msgprint( - _('{} e-invoices cancelled successfully').format(success), - title=_('Bulk E-Invoice Cancellation Complete') + _("{} e-invoices cancelled successfully").format(success), + title=_("Bulk E-Invoice Cancellation Complete"), ) else: enqueue_bulk_action(schedule_bulk_cancel_irn, docnames=docnames, reason=reason, remark=remark) + def schedule_bulk_cancel_irn(docnames, reason, remark): failures = GSPConnector.bulk_cancel_irn(docnames, reason, remark) frappe.local.message_log = [] - frappe.publish_realtime("bulk_einvoice_cancellation_complete", { - "user": frappe.session.user, - "failures": failures, - "invoices": docnames - }) + frappe.publish_realtime( + "bulk_einvoice_cancellation_complete", + {"user": frappe.session.user, "failures": failures, "invoices": docnames}, + ) + def enqueue_bulk_action(job, **kwargs): check_scheduler_status() @@ -1153,16 +1369,18 @@ def enqueue_bulk_action(job, **kwargs): ) if job == schedule_bulk_generate_irn: - msg = _('E-Invoices will be generated in a background process.') + msg = _("E-Invoices will be generated in a background process.") else: - msg = _('E-Invoices will be cancelled in a background process.') + msg = _("E-Invoices will be cancelled in a background process.") frappe.msgprint(msg, alert=1) + def check_scheduler_status(): if is_scheduler_inactive() and not frappe.flags.in_test: frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive")) + def job_already_enqueued(job_name): enqueued_jobs = [d.get("job_name") for d in get_info()] if job_name in enqueued_jobs: diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 12b10bb4d9..40fa6cd097 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -10,104 +10,118 @@ from erpnext.regional.india import states from erpnext.accounts.utils import get_fiscal_year, FiscalYearError from frappe.utils import today + def setup(company=None, patch=True): # Company independent fixtures should be called only once at the first company setup - if patch or frappe.db.count('Company', {'country': 'India'}) <=1: + if patch or frappe.db.count("Company", {"country": "India"}) <= 1: setup_company_independent_fixtures(patch=patch) if not patch: make_fixtures(company) + # TODO: for all countries def setup_company_independent_fixtures(patch=False): make_custom_fields() make_property_setters(patch=patch) add_permissions() add_custom_roles_for_reports() - frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test) + frappe.enqueue("erpnext.regional.india.setup.add_hsn_sac_codes", now=frappe.flags.in_test) create_gratuity_rule() add_print_formats() update_accounts_settings_for_taxes() + def add_hsn_sac_codes(): if frappe.flags.in_test and frappe.flags.created_hsn_codes: return # HSN codes - with open(os.path.join(os.path.dirname(__file__), 'hsn_code_data.json'), 'r') as f: + with open(os.path.join(os.path.dirname(__file__), "hsn_code_data.json"), "r") as f: hsn_codes = json.loads(f.read()) create_hsn_codes(hsn_codes, code_field="hsn_code") # SAC Codes - with open(os.path.join(os.path.dirname(__file__), 'sac_code_data.json'), 'r') as f: + with open(os.path.join(os.path.dirname(__file__), "sac_code_data.json"), "r") as f: sac_codes = json.loads(f.read()) create_hsn_codes(sac_codes, code_field="sac_code") if frappe.flags.in_test: frappe.flags.created_hsn_codes = True + def create_hsn_codes(data, code_field): for d in data: - hsn_code = frappe.new_doc('GST HSN Code') + hsn_code = frappe.new_doc("GST HSN Code") hsn_code.description = d["description"] hsn_code.hsn_code = d[code_field] hsn_code.name = d[code_field] hsn_code.db_insert(ignore_if_duplicate=True) + def add_custom_roles_for_reports(): - for report_name in ('GST Sales Register', 'GST Purchase Register', - 'GST Itemised Sales Register', 'GST Itemised Purchase Register', 'Eway Bill', 'E-Invoice Summary'): + for report_name in ( + "GST Sales Register", + "GST Purchase Register", + "GST Itemised Sales Register", + "GST Itemised Purchase Register", + "Eway Bill", + "E-Invoice Summary", + ): - if not frappe.db.get_value('Custom Role', dict(report=report_name)): - frappe.get_doc(dict( - doctype='Custom Role', - report=report_name, - roles= [ - dict(role='Accounts User'), - dict(role='Accounts Manager') - ] - )).insert() + if not frappe.db.get_value("Custom Role", dict(report=report_name)): + frappe.get_doc( + dict( + doctype="Custom Role", + report=report_name, + roles=[dict(role="Accounts User"), dict(role="Accounts Manager")], + ) + ).insert() - for report_name in ('Professional Tax Deductions', 'Provident Fund Deductions'): + for report_name in ("Professional Tax Deductions", "Provident Fund Deductions"): - if not frappe.db.get_value('Custom Role', dict(report=report_name)): - frappe.get_doc(dict( - doctype='Custom Role', - report=report_name, - roles= [ - dict(role='HR User'), - dict(role='HR Manager'), - dict(role='Employee') - ] - )).insert() + if not frappe.db.get_value("Custom Role", dict(report=report_name)): + frappe.get_doc( + dict( + doctype="Custom Role", + report=report_name, + roles=[dict(role="HR User"), dict(role="HR Manager"), dict(role="Employee")], + ) + ).insert() - for report_name in ('HSN-wise-summary of outward supplies', 'GSTR-1', 'GSTR-2'): + for report_name in ("HSN-wise-summary of outward supplies", "GSTR-1", "GSTR-2"): + + if not frappe.db.get_value("Custom Role", dict(report=report_name)): + frappe.get_doc( + dict( + doctype="Custom Role", + report=report_name, + roles=[dict(role="Accounts User"), dict(role="Accounts Manager"), dict(role="Auditor")], + ) + ).insert() - if not frappe.db.get_value('Custom Role', dict(report=report_name)): - frappe.get_doc(dict( - doctype='Custom Role', - report=report_name, - roles= [ - dict(role='Accounts User'), - dict(role='Accounts Manager'), - dict(role='Auditor') - ] - )).insert() def add_permissions(): - 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'): + 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) - update_permission_property(doctype, role, 0, 'write', 1) - update_permission_property(doctype, role, 0, 'create', 1) + update_permission_property(doctype, role, 0, "write", 1) + update_permission_property(doctype, role, 0, "create", 1) - if doctype == 'GST HSN Code': - for role in ('Item Manager', 'Stock Manager'): + if doctype == "GST HSN Code": + for role in ("Item Manager", "Stock Manager"): add_permission(doctype, role, 0) - update_permission_property(doctype, role, 0, 'write', 1) - update_permission_property(doctype, role, 0, 'create', 1) + update_permission_property(doctype, role, 0, "write", 1) + update_permission_property(doctype, role, 0, "create", 1) + def add_print_formats(): frappe.reload_doc("regional", "print_format", "gst_tax_invoice") @@ -118,602 +132,1081 @@ def add_print_formats(): frappe.db.set_value("Print Format", "GST Tax Invoice", "disabled", 0) frappe.db.set_value("Print Format", "GST E-Invoice", "disabled", 0) + def make_property_setters(patch=False): # GST rules do not allow for an invoice no. bigger than 16 characters - journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + ['Reversal Of ITC'] - sales_invoice_series = ['SINV-.YY.-', 'SRET-.YY.-', ''] + frappe.get_meta("Sales Invoice").get_options("naming_series").split("\n") - purchase_invoice_series = ['PINV-.YY.-', 'PRET-.YY.-', ''] + frappe.get_meta("Purchase Invoice").get_options("naming_series").split("\n") + journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + [ + "Reversal Of ITC" + ] + sales_invoice_series = ["SINV-.YY.-", "SRET-.YY.-", ""] + frappe.get_meta( + "Sales Invoice" + ).get_options("naming_series").split("\n") + purchase_invoice_series = ["PINV-.YY.-", "PRET-.YY.-", ""] + frappe.get_meta( + "Purchase Invoice" + ).get_options("naming_series").split("\n") if not patch: - make_property_setter('Sales Invoice', 'naming_series', 'options', '\n'.join(sales_invoice_series), '') - make_property_setter('Purchase Invoice', 'naming_series', 'options', '\n'.join(purchase_invoice_series), '') - make_property_setter('Journal Entry', 'voucher_type', 'options', '\n'.join(journal_entry_types), '') + make_property_setter( + "Sales Invoice", "naming_series", "options", "\n".join(sales_invoice_series), "" + ) + make_property_setter( + "Purchase Invoice", "naming_series", "options", "\n".join(purchase_invoice_series), "" + ) + make_property_setter( + "Journal Entry", "voucher_type", "options", "\n".join(journal_entry_types), "" + ) + def make_custom_fields(update=True): custom_fields = get_custom_fields() create_custom_fields(custom_fields, update=update) + def get_custom_fields(): - hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC', - fieldtype='Data', fetch_from='item_code.gst_hsn_code', insert_after='description', - allow_on_submit=1, print_hide=1, fetch_if_empty=1) - nil_rated_exempt = dict(fieldname='is_nil_exempt', label='Is Nil Rated or Exempted', - fieldtype='Check', fetch_from='item_code.is_nil_exempt', insert_after='gst_hsn_code', - print_hide=1) - is_non_gst = dict(fieldname='is_non_gst', label='Is Non GST', - fieldtype='Check', fetch_from='item_code.is_non_gst', insert_after='is_nil_exempt', - print_hide=1) - taxable_value = dict(fieldname='taxable_value', label='Taxable Value', - fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency", - print_hide=1) + hsn_sac_field = dict( + fieldname="gst_hsn_code", + label="HSN/SAC", + fieldtype="Data", + fetch_from="item_code.gst_hsn_code", + insert_after="description", + allow_on_submit=1, + print_hide=1, + fetch_if_empty=1, + ) + nil_rated_exempt = dict( + fieldname="is_nil_exempt", + label="Is Nil Rated or Exempted", + fieldtype="Check", + fetch_from="item_code.is_nil_exempt", + insert_after="gst_hsn_code", + print_hide=1, + ) + is_non_gst = dict( + fieldname="is_non_gst", + label="Is Non GST", + fieldtype="Check", + fetch_from="item_code.is_non_gst", + insert_after="is_nil_exempt", + print_hide=1, + ) + taxable_value = dict( + fieldname="taxable_value", + label="Taxable Value", + fieldtype="Currency", + insert_after="base_net_amount", + hidden=1, + options="Company:company:default_currency", + print_hide=1, + ) purchase_invoice_gst_category = [ - dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', - insert_after='language', print_hide=1, collapsible=1), - dict(fieldname='gst_category', label='GST Category', - fieldtype='Select', insert_after='gst_section', print_hide=1, - options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nUIN Holders', - fetch_from='supplier.gst_category', fetch_if_empty=1), - dict(fieldname='export_type', label='Export Type', - fieldtype='Select', insert_after='gst_category', print_hide=1, + dict( + fieldname="gst_section", + label="GST Details", + fieldtype="Section Break", + insert_after="language", + print_hide=1, + collapsible=1, + ), + dict( + fieldname="gst_category", + label="GST Category", + fieldtype="Select", + insert_after="gst_section", + print_hide=1, + options="\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nUIN Holders", + fetch_from="supplier.gst_category", + fetch_if_empty=1, + ), + dict( + fieldname="export_type", + label="Export Type", + fieldtype="Select", + insert_after="gst_category", + print_hide=1, depends_on='eval:in_list(["SEZ", "Overseas"], doc.gst_category)', - options='\nWith Payment of Tax\nWithout Payment of Tax', fetch_from='supplier.export_type', - fetch_if_empty=1), + options="\nWith Payment of Tax\nWithout Payment of Tax", + fetch_from="supplier.export_type", + fetch_if_empty=1, + ), ] sales_invoice_gst_category = [ - dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', - insert_after='language', print_hide=1, collapsible=1), - dict(fieldname='gst_category', label='GST Category', - fieldtype='Select', insert_after='gst_section', print_hide=1, - options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders', - fetch_from='customer.gst_category', fetch_if_empty=1, length=25), - dict(fieldname='export_type', label='Export Type', - fieldtype='Select', insert_after='gst_category', print_hide=1, + dict( + fieldname="gst_section", + label="GST Details", + fieldtype="Section Break", + insert_after="language", + print_hide=1, + collapsible=1, + ), + dict( + fieldname="gst_category", + label="GST Category", + fieldtype="Select", + insert_after="gst_section", + print_hide=1, + options="\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders", + fetch_from="customer.gst_category", + fetch_if_empty=1, + length=25, + ), + dict( + fieldname="export_type", + label="Export Type", + fieldtype="Select", + insert_after="gst_category", + print_hide=1, depends_on='eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', - options='\nWith Payment of Tax\nWithout Payment of Tax', fetch_from='customer.export_type', - fetch_if_empty=1, length=25), + options="\nWith Payment of Tax\nWithout Payment of Tax", + fetch_from="customer.export_type", + fetch_if_empty=1, + length=25, + ), ] delivery_note_gst_category = [ - dict(fieldname='gst_category', label='GST Category', - fieldtype='Select', insert_after='gst_vehicle_type', print_hide=1, - options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders', - fetch_from='customer.gst_category', fetch_if_empty=1), + dict( + fieldname="gst_category", + label="GST Category", + fieldtype="Select", + insert_after="gst_vehicle_type", + print_hide=1, + options="\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders", + fetch_from="customer.gst_category", + fetch_if_empty=1, + ), ] invoice_gst_fields = [ - dict(fieldname='invoice_copy', label='Invoice Copy', length=30, - fieldtype='Select', insert_after='export_type', print_hide=1, allow_on_submit=1, - options='Original for Recipient\nDuplicate for Transporter\nDuplicate for Supplier\nTriplicate for Supplier'), - dict(fieldname='reverse_charge', label='Reverse Charge', length=2, - fieldtype='Select', insert_after='invoice_copy', print_hide=1, - options='Y\nN', default='N'), - dict(fieldname='ecommerce_gstin', label='E-commerce GSTIN', length=15, - fieldtype='Data', insert_after='export_type', print_hide=1), - dict(fieldname='gst_col_break', fieldtype='Column Break', insert_after='ecommerce_gstin'), - dict(fieldname='reason_for_issuing_document', label='Reason For Issuing document', - fieldtype='Select', insert_after='gst_col_break', print_hide=1, - depends_on='eval:doc.is_return==1', length=45, - options='\n01-Sales Return\n02-Post Sale Discount\n03-Deficiency in services\n04-Correction in Invoice\n05-Change in POS\n06-Finalization of Provisional assessment\n07-Others') + dict( + fieldname="invoice_copy", + label="Invoice Copy", + length=30, + fieldtype="Select", + insert_after="export_type", + print_hide=1, + allow_on_submit=1, + options="Original for Recipient\nDuplicate for Transporter\nDuplicate for Supplier\nTriplicate for Supplier", + ), + dict( + fieldname="reverse_charge", + label="Reverse Charge", + length=2, + fieldtype="Select", + insert_after="invoice_copy", + print_hide=1, + options="Y\nN", + default="N", + ), + dict( + fieldname="ecommerce_gstin", + label="E-commerce GSTIN", + length=15, + fieldtype="Data", + insert_after="export_type", + print_hide=1, + ), + dict(fieldname="gst_col_break", fieldtype="Column Break", insert_after="ecommerce_gstin"), + dict( + fieldname="reason_for_issuing_document", + label="Reason For Issuing document", + fieldtype="Select", + insert_after="gst_col_break", + print_hide=1, + depends_on="eval:doc.is_return==1", + length=45, + options="\n01-Sales Return\n02-Post Sale Discount\n03-Deficiency in services\n04-Correction in Invoice\n05-Change in POS\n06-Finalization of Provisional assessment\n07-Others", + ), ] purchase_invoice_gst_fields = [ - dict(fieldname='supplier_gstin', label='Supplier GSTIN', - fieldtype='Data', insert_after='supplier_address', - fetch_from='supplier_address.gstin', print_hide=1, read_only=1), - dict(fieldname='company_gstin', label='Company GSTIN', - fieldtype='Data', insert_after='shipping_address_display', - fetch_from='shipping_address.gstin', print_hide=1, read_only=1), - dict(fieldname='place_of_supply', label='Place of Supply', - fieldtype='Data', insert_after='shipping_address', - print_hide=1, read_only=1), - ] + dict( + fieldname="supplier_gstin", + label="Supplier GSTIN", + fieldtype="Data", + insert_after="supplier_address", + fetch_from="supplier_address.gstin", + print_hide=1, + read_only=1, + ), + dict( + fieldname="company_gstin", + label="Company GSTIN", + fieldtype="Data", + insert_after="shipping_address_display", + fetch_from="shipping_address.gstin", + print_hide=1, + read_only=1, + ), + dict( + fieldname="place_of_supply", + label="Place of Supply", + fieldtype="Data", + insert_after="shipping_address", + print_hide=1, + read_only=1, + ), + ] purchase_invoice_itc_fields = [ - dict(fieldname='eligibility_for_itc', label='Eligibility For ITC', - fieldtype='Select', insert_after='reason_for_issuing_document', print_hide=1, - options='Input Service Distributor\nImport Of Service\nImport Of Capital Goods\nITC on Reverse Charge\nIneligible As Per Section 17(5)\nIneligible Others\nAll Other ITC', - default="All Other ITC"), - dict(fieldname='itc_integrated_tax', label='Availed ITC Integrated Tax', - fieldtype='Currency', insert_after='eligibility_for_itc', - options='Company:company:default_currency', print_hide=1), - dict(fieldname='itc_central_tax', label='Availed ITC Central Tax', - fieldtype='Currency', insert_after='itc_integrated_tax', - options='Company:company:default_currency', print_hide=1), - dict(fieldname='itc_state_tax', label='Availed ITC State/UT Tax', - fieldtype='Currency', insert_after='itc_central_tax', - options='Company:company:default_currency', print_hide=1), - dict(fieldname='itc_cess_amount', label='Availed ITC Cess', - fieldtype='Currency', insert_after='itc_state_tax', - options='Company:company:default_currency', print_hide=1), - ] + dict( + fieldname="eligibility_for_itc", + label="Eligibility For ITC", + fieldtype="Select", + insert_after="reason_for_issuing_document", + print_hide=1, + options="Input Service Distributor\nImport Of Service\nImport Of Capital Goods\nITC on Reverse Charge\nIneligible As Per Section 17(5)\nIneligible Others\nAll Other ITC", + default="All Other ITC", + ), + dict( + fieldname="itc_integrated_tax", + label="Availed ITC Integrated Tax", + fieldtype="Currency", + insert_after="eligibility_for_itc", + options="Company:company:default_currency", + print_hide=1, + ), + dict( + fieldname="itc_central_tax", + label="Availed ITC Central Tax", + fieldtype="Currency", + insert_after="itc_integrated_tax", + options="Company:company:default_currency", + print_hide=1, + ), + dict( + fieldname="itc_state_tax", + label="Availed ITC State/UT Tax", + fieldtype="Currency", + insert_after="itc_central_tax", + options="Company:company:default_currency", + print_hide=1, + ), + dict( + fieldname="itc_cess_amount", + label="Availed ITC Cess", + fieldtype="Currency", + insert_after="itc_state_tax", + options="Company:company:default_currency", + print_hide=1, + ), + ] sales_invoice_gst_fields = [ - dict(fieldname='billing_address_gstin', label='Billing Address GSTIN', - fieldtype='Data', insert_after='customer_address', read_only=1, - fetch_from='customer_address.gstin', print_hide=1, length=15), - dict(fieldname='customer_gstin', label='Customer GSTIN', - fieldtype='Data', insert_after='shipping_address_name', - fetch_from='shipping_address_name.gstin', print_hide=1, length=15), - dict(fieldname='place_of_supply', label='Place of Supply', - fieldtype='Data', insert_after='customer_gstin', - print_hide=1, read_only=1, length=50), - dict(fieldname='company_gstin', label='Company GSTIN', - fieldtype='Data', insert_after='company_address', - fetch_from='company_address.gstin', print_hide=1, read_only=1, length=15), - ] + dict( + fieldname="billing_address_gstin", + label="Billing Address GSTIN", + fieldtype="Data", + insert_after="customer_address", + read_only=1, + fetch_from="customer_address.gstin", + print_hide=1, + length=15, + ), + dict( + fieldname="customer_gstin", + label="Customer GSTIN", + fieldtype="Data", + insert_after="shipping_address_name", + fetch_from="shipping_address_name.gstin", + print_hide=1, + length=15, + ), + dict( + fieldname="place_of_supply", + label="Place of Supply", + fieldtype="Data", + insert_after="customer_gstin", + print_hide=1, + read_only=1, + length=50, + ), + dict( + fieldname="company_gstin", + label="Company GSTIN", + fieldtype="Data", + insert_after="company_address", + fetch_from="company_address.gstin", + print_hide=1, + read_only=1, + length=15, + ), + ] sales_invoice_shipping_fields = [ - dict(fieldname='port_code', label='Port Code', - fieldtype='Data', insert_after='reason_for_issuing_document', print_hide=1, - depends_on="eval:doc.gst_category=='Overseas' ", length=15), - dict(fieldname='shipping_bill_number', label=' Shipping Bill Number', - fieldtype='Data', insert_after='port_code', print_hide=1, - depends_on="eval:doc.gst_category=='Overseas' ", length=50), - dict(fieldname='shipping_bill_date', label='Shipping Bill Date', - fieldtype='Date', insert_after='shipping_bill_number', print_hide=1, - depends_on="eval:doc.gst_category=='Overseas' "), - ] + dict( + fieldname="port_code", + label="Port Code", + fieldtype="Data", + insert_after="reason_for_issuing_document", + print_hide=1, + depends_on="eval:doc.gst_category=='Overseas' ", + length=15, + ), + dict( + fieldname="shipping_bill_number", + label=" Shipping Bill Number", + fieldtype="Data", + insert_after="port_code", + print_hide=1, + depends_on="eval:doc.gst_category=='Overseas' ", + length=50, + ), + dict( + fieldname="shipping_bill_date", + label="Shipping Bill Date", + fieldtype="Date", + insert_after="shipping_bill_number", + print_hide=1, + depends_on="eval:doc.gst_category=='Overseas' ", + ), + ] journal_entry_fields = [ - dict(fieldname='reversal_type', label='Reversal Type', - fieldtype='Select', insert_after='voucher_type', print_hide=1, + dict( + fieldname="reversal_type", + label="Reversal Type", + fieldtype="Select", + insert_after="voucher_type", + print_hide=1, options="As per rules 42 & 43 of CGST Rules\nOthers", depends_on="eval:doc.voucher_type=='Reversal Of ITC'", - mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"), - dict(fieldname='company_address', label='Company Address', - fieldtype='Link', options='Address', insert_after='reversal_type', - print_hide=1, depends_on="eval:doc.voucher_type=='Reversal Of ITC'", - mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"), - dict(fieldname='company_gstin', label='Company GSTIN', - fieldtype='Data', read_only=1, insert_after='company_address', print_hide=1, - fetch_from='company_address.gstin', + mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'", + ), + dict( + fieldname="company_address", + label="Company Address", + fieldtype="Link", + options="Address", + insert_after="reversal_type", + print_hide=1, depends_on="eval:doc.voucher_type=='Reversal Of ITC'", - mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'") + mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'", + ), + dict( + fieldname="company_gstin", + label="Company GSTIN", + fieldtype="Data", + read_only=1, + insert_after="company_address", + print_hide=1, + fetch_from="company_address.gstin", + depends_on="eval:doc.voucher_type=='Reversal Of ITC'", + mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'", + ), ] inter_state_gst_field = [ - dict(fieldname='is_inter_state', label='Is Inter State', - fieldtype='Check', insert_after='disabled', print_hide=1), - dict(fieldname='is_reverse_charge', label='Is Reverse Charge', fieldtype='Check', - insert_after='is_inter_state', print_hide=1), - dict(fieldname='tax_category_column_break', fieldtype='Column Break', - insert_after='is_reverse_charge'), - dict(fieldname='gst_state', label='Source State', fieldtype='Select', - options='\n'.join(states), insert_after='company') + dict( + fieldname="is_inter_state", + label="Is Inter State", + fieldtype="Check", + insert_after="disabled", + print_hide=1, + ), + dict( + fieldname="is_reverse_charge", + label="Is Reverse Charge", + fieldtype="Check", + insert_after="is_inter_state", + print_hide=1, + ), + dict( + fieldname="tax_category_column_break", + fieldtype="Column Break", + insert_after="is_reverse_charge", + ), + dict( + fieldname="gst_state", + label="Source State", + fieldtype="Select", + options="\n".join(states), + insert_after="company", + ), ] ewaybill_fields = [ { - 'fieldname': 'distance', - 'label': 'Distance (in km)', - 'fieldtype': 'Float', - 'insert_after': 'vehicle_no', - 'print_hide': 1 + "fieldname": "distance", + "label": "Distance (in km)", + "fieldtype": "Float", + "insert_after": "vehicle_no", + "print_hide": 1, }, { - 'fieldname': 'gst_transporter_id', - 'label': 'GST Transporter ID', - 'fieldtype': 'Data', - 'insert_after': 'transporter', - 'fetch_from': 'transporter.gst_transporter_id', - 'print_hide': 1, - 'translatable': 0 + "fieldname": "gst_transporter_id", + "label": "GST Transporter ID", + "fieldtype": "Data", + "insert_after": "transporter", + "fetch_from": "transporter.gst_transporter_id", + "print_hide": 1, + "translatable": 0, }, { - 'fieldname': 'mode_of_transport', - 'label': 'Mode of Transport', - 'fieldtype': 'Select', - 'options': '\nRoad\nAir\nRail\nShip', - 'default': 'Road', - 'insert_after': 'transporter_name', - 'print_hide': 1, - 'translatable': 0 + "fieldname": "mode_of_transport", + "label": "Mode of Transport", + "fieldtype": "Select", + "options": "\nRoad\nAir\nRail\nShip", + "default": "Road", + "insert_after": "transporter_name", + "print_hide": 1, + "translatable": 0, }, { - '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': 'Regular', - 'insert_after': 'lr_date', - 'print_hide': 1, - 'translatable': 0 + "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": "Regular", + "insert_after": "lr_date", + "print_hide": 1, + "translatable": 0, }, { - 'fieldname': 'ewaybill', - 'label': 'E-Way Bill No.', - 'fieldtype': 'Data', - 'depends_on': 'eval:(doc.docstatus === 1)', - 'allow_on_submit': 1, - 'insert_after': 'customer_name_in_arabic', - 'translatable': 0, - } + "fieldname": "ewaybill", + "label": "E-Way Bill No.", + "fieldtype": "Data", + "depends_on": "eval:(doc.docstatus === 1)", + "allow_on_submit": 1, + "insert_after": "customer_name_in_arabic", + "translatable": 0, + }, ] si_ewaybill_fields = [ { - 'fieldname': 'transporter_info', - 'label': 'Transporter Info', - 'fieldtype': 'Section Break', - 'insert_after': 'terms', - 'collapsible': 1, - 'collapsible_depends_on': 'transporter', - 'print_hide': 1 + "fieldname": "transporter_info", + "label": "Transporter Info", + "fieldtype": "Section Break", + "insert_after": "terms", + "collapsible": 1, + "collapsible_depends_on": "transporter", + "print_hide": 1, }, { - 'fieldname': 'transporter', - 'label': 'Transporter', - 'fieldtype': 'Link', - 'insert_after': 'transporter_info', - 'options': 'Supplier', - 'print_hide': 1 + "fieldname": "transporter", + "label": "Transporter", + "fieldtype": "Link", + "insert_after": "transporter_info", + "options": "Supplier", + "print_hide": 1, }, { - 'fieldname': 'gst_transporter_id', - 'label': 'GST Transporter ID', - 'fieldtype': 'Data', - 'insert_after': 'transporter', - 'fetch_from': 'transporter.gst_transporter_id', - 'print_hide': 1, - 'translatable': 0, - 'length': 20 + "fieldname": "gst_transporter_id", + "label": "GST Transporter ID", + "fieldtype": "Data", + "insert_after": "transporter", + "fetch_from": "transporter.gst_transporter_id", + "print_hide": 1, + "translatable": 0, + "length": 20, }, { - 'fieldname': 'driver', - 'label': 'Driver', - 'fieldtype': 'Link', - 'insert_after': 'gst_transporter_id', - 'options': 'Driver', - 'print_hide': 1 + "fieldname": "driver", + "label": "Driver", + "fieldtype": "Link", + "insert_after": "gst_transporter_id", + "options": "Driver", + "print_hide": 1, }, { - 'fieldname': 'lr_no', - 'label': 'Transport Receipt No', - 'fieldtype': 'Data', - 'insert_after': 'driver', - 'print_hide': 1, - 'translatable': 0, - 'length': 30 + "fieldname": "lr_no", + "label": "Transport Receipt No", + "fieldtype": "Data", + "insert_after": "driver", + "print_hide": 1, + "translatable": 0, + "length": 30, }, { - 'fieldname': 'vehicle_no', - 'label': 'Vehicle No', - 'fieldtype': 'Data', - 'insert_after': 'lr_no', - 'print_hide': 1, - 'translatable': 0, - 'length': 10 + "fieldname": "vehicle_no", + "label": "Vehicle No", + "fieldtype": "Data", + "insert_after": "lr_no", + "print_hide": 1, + "translatable": 0, + "length": 10, }, { - 'fieldname': 'distance', - 'label': 'Distance (in km)', - 'fieldtype': 'Float', - 'insert_after': 'vehicle_no', - 'print_hide': 1 + "fieldname": "distance", + "label": "Distance (in km)", + "fieldtype": "Float", + "insert_after": "vehicle_no", + "print_hide": 1, + }, + {"fieldname": "transporter_col_break", "fieldtype": "Column Break", "insert_after": "distance"}, + { + "fieldname": "transporter_name", + "label": "Transporter Name", + "fieldtype": "Small Text", + "insert_after": "transporter_col_break", + "fetch_from": "transporter.name", + "read_only": 1, + "print_hide": 1, + "translatable": 0, }, { - 'fieldname': 'transporter_col_break', - 'fieldtype': 'Column Break', - 'insert_after': 'distance' + "fieldname": "mode_of_transport", + "label": "Mode of Transport", + "fieldtype": "Select", + "options": "\nRoad\nAir\nRail\nShip", + "insert_after": "transporter_name", + "print_hide": 1, + "translatable": 0, + "length": 5, }, { - 'fieldname': 'transporter_name', - 'label': 'Transporter Name', - 'fieldtype': 'Small Text', - 'insert_after': 'transporter_col_break', - 'fetch_from': 'transporter.name', - 'read_only': 1, - 'print_hide': 1, - 'translatable': 0 + "fieldname": "driver_name", + "label": "Driver Name", + "fieldtype": "Small Text", + "insert_after": "mode_of_transport", + "fetch_from": "driver.full_name", + "print_hide": 1, + "translatable": 0, }, { - 'fieldname': 'mode_of_transport', - 'label': 'Mode of Transport', - 'fieldtype': 'Select', - 'options': '\nRoad\nAir\nRail\nShip', - 'insert_after': 'transporter_name', - 'print_hide': 1, - 'translatable': 0, - 'length': 5 + "fieldname": "lr_date", + "label": "Transport Receipt Date", + "fieldtype": "Date", + "insert_after": "driver_name", + "default": "Today", + "print_hide": 1, }, { - 'fieldname': 'driver_name', - 'label': 'Driver Name', - 'fieldtype': 'Small Text', - 'insert_after': 'mode_of_transport', - 'fetch_from': 'driver.full_name', - 'print_hide': 1, - 'translatable': 0 + "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": "Regular", + "insert_after": "lr_date", + "print_hide": 1, + "translatable": 0, + "length": 30, }, { - 'fieldname': 'lr_date', - 'label': 'Transport Receipt Date', - 'fieldtype': 'Date', - 'insert_after': 'driver_name', - 'default': 'Today', - 'print_hide': 1 + "fieldname": "ewaybill", + "label": "E-Way Bill No.", + "fieldtype": "Data", + "depends_on": "eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)", + "allow_on_submit": 1, + "insert_after": "tax_id", + "translatable": 0, + "length": 20, }, - { - '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': 'Regular', - 'insert_after': 'lr_date', - 'print_hide': 1, - 'translatable': 0, - 'length': 30 - }, - { - 'fieldname': 'ewaybill', - 'label': 'E-Way Bill No.', - 'fieldtype': 'Data', - 'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)', - 'allow_on_submit': 1, - 'insert_after': 'tax_id', - 'translatable': 0, - 'length': 20 - } ] payment_entry_fields = [ - dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', insert_after='deductions', - print_hide=1, collapsible=1), - dict(fieldname='company_address', label='Company Address', fieldtype='Link', insert_after='gst_section', - print_hide=1, options='Address'), - dict(fieldname='company_gstin', label='Company GSTIN', - fieldtype='Data', insert_after='company_address', - fetch_from='company_address.gstin', print_hide=1, read_only=1), - dict(fieldname='place_of_supply', label='Place of Supply', - fieldtype='Data', insert_after='company_gstin', - print_hide=1, read_only=1), - dict(fieldname='gst_column_break', fieldtype='Column Break', - insert_after='place_of_supply'), - dict(fieldname='customer_address', label='Customer Address', fieldtype='Link', insert_after='gst_column_break', - print_hide=1, options='Address', depends_on = 'eval:doc.party_type == "Customer"'), - dict(fieldname='customer_gstin', label='Customer GSTIN', - fieldtype='Data', insert_after='customer_address', - fetch_from='customer_address.gstin', print_hide=1, read_only=1) + dict( + fieldname="gst_section", + label="GST Details", + fieldtype="Section Break", + insert_after="deductions", + print_hide=1, + collapsible=1, + ), + dict( + fieldname="company_address", + label="Company Address", + fieldtype="Link", + insert_after="gst_section", + print_hide=1, + options="Address", + ), + dict( + fieldname="company_gstin", + label="Company GSTIN", + fieldtype="Data", + insert_after="company_address", + fetch_from="company_address.gstin", + print_hide=1, + read_only=1, + ), + dict( + fieldname="place_of_supply", + label="Place of Supply", + fieldtype="Data", + insert_after="company_gstin", + print_hide=1, + read_only=1, + ), + dict(fieldname="gst_column_break", fieldtype="Column Break", insert_after="place_of_supply"), + dict( + fieldname="customer_address", + label="Customer Address", + fieldtype="Link", + insert_after="gst_column_break", + print_hide=1, + options="Address", + depends_on='eval:doc.party_type == "Customer"', + ), + dict( + fieldname="customer_gstin", + label="Customer GSTIN", + fieldtype="Data", + insert_after="customer_address", + fetch_from="customer_address.gstin", + print_hide=1, + read_only=1, + ), ] 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='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1, - depends_on='eval: doc.irn', allow_on_submit=1, insert_after='customer'), - - dict(fieldname='eway_bill_validity', label='E-Way Bill Validity', fieldtype='Data', no_copy=1, print_hide=1, - depends_on='ewaybill', read_only=1, allow_on_submit=1, insert_after='ewaybill'), - - 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='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type', - print_hide=1, hidden=1), - - dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section', - 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_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date', - no_copy=1, print_hide=1), - - dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date', - no_copy=1, print_hide=1, read_only=1), - - dict(fieldname='signed_qr_code', label='Signed QRCode', fieldtype='Code', options='JSON', hidden=1, insert_after='signed_einvoice', - no_copy=1, print_hide=1, read_only=1), - - dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, insert_after='signed_qr_code', - no_copy=1, print_hide=1, read_only=1), - - dict(fieldname='einvoice_status', label='E-Invoice Status', fieldtype='Select', insert_after='qrcode_image', - options='\nPending\nGenerated\nCancelled\nFailed', default=None, hidden=1, no_copy=1, print_hide=1, read_only=1), - - dict(fieldname='failure_description', label='E-Invoice Failure Description', fieldtype='Code', options='JSON', - hidden=1, insert_after='einvoice_status', no_copy=1, print_hide=1, read_only=1) + 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="irn_cancelled", + label="IRN Cancelled", + fieldtype="Check", + no_copy=1, + print_hide=1, + depends_on="eval: doc.irn", + allow_on_submit=1, + insert_after="customer", + ), + dict( + fieldname="eway_bill_validity", + label="E-Way Bill Validity", + fieldtype="Data", + no_copy=1, + print_hide=1, + depends_on="ewaybill", + read_only=1, + allow_on_submit=1, + insert_after="ewaybill", + ), + 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="einvoice_section", + label="E-Invoice Fields", + fieldtype="Section Break", + insert_after="gst_vehicle_type", + print_hide=1, + hidden=1, + ), + dict( + fieldname="ack_no", + label="Ack. No.", + fieldtype="Data", + read_only=1, + hidden=1, + insert_after="einvoice_section", + 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_cancel_date", + label="Cancel Date", + fieldtype="Data", + read_only=1, + hidden=1, + insert_after="ack_date", + no_copy=1, + print_hide=1, + ), + dict( + fieldname="signed_einvoice", + label="Signed E-Invoice", + fieldtype="Code", + options="JSON", + hidden=1, + insert_after="irn_cancel_date", + no_copy=1, + print_hide=1, + read_only=1, + ), + dict( + fieldname="signed_qr_code", + label="Signed QRCode", + fieldtype="Code", + options="JSON", + hidden=1, + insert_after="signed_einvoice", + no_copy=1, + print_hide=1, + read_only=1, + ), + dict( + fieldname="qrcode_image", + label="QRCode", + fieldtype="Attach Image", + hidden=1, + insert_after="signed_qr_code", + no_copy=1, + print_hide=1, + read_only=1, + ), + dict( + fieldname="einvoice_status", + label="E-Invoice Status", + fieldtype="Select", + insert_after="qrcode_image", + options="\nPending\nGenerated\nCancelled\nFailed", + default=None, + hidden=1, + no_copy=1, + print_hide=1, + read_only=1, + ), + dict( + fieldname="failure_description", + label="E-Invoice Failure Description", + fieldtype="Code", + options="JSON", + hidden=1, + insert_after="einvoice_status", + no_copy=1, + print_hide=1, + read_only=1, + ), ] custom_fields = { - 'Address': [ - dict(fieldname='gstin', label='Party GSTIN', fieldtype='Data', - insert_after='fax'), - dict(fieldname='gst_state', label='GST State', fieldtype='Select', - options='\n'.join(states), insert_after='gstin'), - dict(fieldname='gst_state_number', label='GST State Number', - fieldtype='Data', insert_after='gst_state', read_only=1), + "Address": [ + dict(fieldname="gstin", label="Party GSTIN", fieldtype="Data", insert_after="fax"), + dict( + fieldname="gst_state", + label="GST State", + fieldtype="Select", + options="\n".join(states), + insert_after="gstin", + ), + dict( + fieldname="gst_state_number", + label="GST State Number", + fieldtype="Data", + insert_after="gst_state", + read_only=1, + ), ], - '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 + si_einvoice_fields, - 'POS Invoice': sales_invoice_gst_fields, - 'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields + delivery_note_gst_category, - 'Payment Entry': payment_entry_fields, - 'Journal Entry': journal_entry_fields, - 'Sales Order': sales_invoice_gst_fields, - 'Tax Category': inter_state_gst_field, - 'Item': [ - dict(fieldname='gst_hsn_code', label='HSN/SAC', - fieldtype='Link', options='GST HSN Code', insert_after='item_group'), - dict(fieldname='is_nil_exempt', label='Is Nil Rated or Exempted', - fieldtype='Check', insert_after='gst_hsn_code'), - dict(fieldname='is_non_gst', label='Is Non GST ', - fieldtype='Check', insert_after='is_nil_exempt') + "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 + + si_einvoice_fields, + "POS Invoice": sales_invoice_gst_fields, + "Delivery Note": sales_invoice_gst_fields + + ewaybill_fields + + sales_invoice_shipping_fields + + delivery_note_gst_category, + "Payment Entry": payment_entry_fields, + "Journal Entry": journal_entry_fields, + "Sales Order": sales_invoice_gst_fields, + "Tax Category": inter_state_gst_field, + "Item": [ + dict( + fieldname="gst_hsn_code", + label="HSN/SAC", + fieldtype="Link", + options="GST HSN Code", + insert_after="item_group", + ), + dict( + fieldname="is_nil_exempt", + label="Is Nil Rated or Exempted", + fieldtype="Check", + insert_after="gst_hsn_code", + ), + dict( + fieldname="is_non_gst", label="Is Non GST ", fieldtype="Check", insert_after="is_nil_exempt" + ), ], - 'Quotation Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], - 'Supplier Quotation Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], - 'Sales Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], - 'Delivery Note Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], - 'Sales Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], - 'POS Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], - 'Purchase Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], - 'Purchase Receipt Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], - 'Purchase Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], - 'Material Request Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], - 'Salary Component': [ - dict(fieldname= 'component_type', - label= 'Component Type', - fieldtype= 'Select', - insert_after= 'description', - options= "\nProvident Fund\nAdditional Provident Fund\nProvident Fund Loan\nProfessional Tax", - depends_on = 'eval:doc.type == "Deduction"' + "Quotation Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], + "Supplier Quotation Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], + "Sales Order Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], + "Delivery Note Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], + "Sales Invoice Item": [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], + "POS Invoice Item": [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], + "Purchase Order Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], + "Purchase Receipt Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], + "Purchase Invoice Item": [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], + "Material Request Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], + "Salary Component": [ + dict( + fieldname="component_type", + label="Component Type", + fieldtype="Select", + insert_after="description", + options="\nProvident Fund\nAdditional Provident Fund\nProvident Fund Loan\nProfessional Tax", + depends_on='eval:doc.type == "Deduction"', ) ], - 'Employee': [ - dict(fieldname='ifsc_code', - label='IFSC Code', - fieldtype='Data', - insert_after='bank_ac_no', + "Employee": [ + dict( + fieldname="ifsc_code", + label="IFSC Code", + fieldtype="Data", + insert_after="bank_ac_no", print_hide=1, - depends_on='eval:doc.salary_mode == "Bank"' - ), - dict( - fieldname = 'pan_number', - label = 'PAN Number', - fieldtype = 'Data', - insert_after = 'payroll_cost_center', - print_hide = 1 + depends_on='eval:doc.salary_mode == "Bank"', ), dict( - fieldname = 'micr_code', - label = 'MICR Code', - fieldtype = 'Data', - insert_after = 'ifsc_code', - print_hide = 1, - depends_on='eval:doc.salary_mode == "Bank"' + fieldname="pan_number", + label="PAN Number", + fieldtype="Data", + insert_after="payroll_cost_center", + print_hide=1, ), dict( - fieldname = 'provident_fund_account', - label = 'Provident Fund Account', - fieldtype = 'Data', - insert_after = 'pan_number' - ) - + fieldname="micr_code", + label="MICR Code", + fieldtype="Data", + insert_after="ifsc_code", + print_hide=1, + depends_on='eval:doc.salary_mode == "Bank"', + ), + dict( + fieldname="provident_fund_account", + label="Provident Fund Account", + fieldtype="Data", + insert_after="pan_number", + ), ], - 'Company': [ - dict(fieldname='hra_section', label='HRA Settings', - fieldtype='Section Break', insert_after='asset_received_but_not_billed', collapsible=1), - dict(fieldname='basic_component', label='Basic Component', - fieldtype='Link', options='Salary Component', insert_after='hra_section'), - dict(fieldname='hra_component', label='HRA Component', - fieldtype='Link', options='Salary Component', insert_after='basic_component'), - dict(fieldname='hra_column_break', fieldtype='Column Break', insert_after='hra_component'), - dict(fieldname='arrear_component', label='Arrear Component', - fieldtype='Link', options='Salary Component', insert_after='hra_column_break'), + "Company": [ + dict( + fieldname="hra_section", + label="HRA Settings", + fieldtype="Section Break", + insert_after="asset_received_but_not_billed", + collapsible=1, + ), + dict( + fieldname="basic_component", + label="Basic Component", + fieldtype="Link", + options="Salary Component", + insert_after="hra_section", + ), + dict( + fieldname="hra_component", + label="HRA Component", + fieldtype="Link", + options="Salary Component", + insert_after="basic_component", + ), + dict(fieldname="hra_column_break", fieldtype="Column Break", insert_after="hra_component"), + dict( + fieldname="arrear_component", + label="Arrear Component", + fieldtype="Link", + options="Salary Component", + insert_after="hra_column_break", + ), ], - 'Employee Tax Exemption Declaration':[ - dict(fieldname='hra_section', label='HRA Exemption', - fieldtype='Section Break', insert_after='declarations'), - dict(fieldname='monthly_house_rent', label='Monthly House Rent', - fieldtype='Currency', insert_after='hra_section'), - dict(fieldname='rented_in_metro_city', label='Rented in Metro City', - fieldtype='Check', insert_after='monthly_house_rent', depends_on='monthly_house_rent'), - dict(fieldname='salary_structure_hra', label='HRA as per Salary Structure', - fieldtype='Currency', insert_after='rented_in_metro_city', read_only=1, depends_on='monthly_house_rent'), - dict(fieldname='hra_column_break', fieldtype='Column Break', - insert_after='salary_structure_hra', depends_on='monthly_house_rent'), - dict(fieldname='annual_hra_exemption', label='Annual HRA Exemption', - fieldtype='Currency', insert_after='hra_column_break', read_only=1, depends_on='monthly_house_rent'), - dict(fieldname='monthly_hra_exemption', label='Monthly HRA Exemption', - fieldtype='Currency', insert_after='annual_hra_exemption', read_only=1, depends_on='monthly_house_rent') + "Employee Tax Exemption Declaration": [ + dict( + fieldname="hra_section", + label="HRA Exemption", + fieldtype="Section Break", + insert_after="declarations", + ), + dict( + fieldname="monthly_house_rent", + label="Monthly House Rent", + fieldtype="Currency", + insert_after="hra_section", + ), + dict( + fieldname="rented_in_metro_city", + label="Rented in Metro City", + fieldtype="Check", + insert_after="monthly_house_rent", + depends_on="monthly_house_rent", + ), + dict( + fieldname="salary_structure_hra", + label="HRA as per Salary Structure", + fieldtype="Currency", + insert_after="rented_in_metro_city", + read_only=1, + depends_on="monthly_house_rent", + ), + dict( + fieldname="hra_column_break", + fieldtype="Column Break", + insert_after="salary_structure_hra", + depends_on="monthly_house_rent", + ), + dict( + fieldname="annual_hra_exemption", + label="Annual HRA Exemption", + fieldtype="Currency", + insert_after="hra_column_break", + read_only=1, + depends_on="monthly_house_rent", + ), + dict( + fieldname="monthly_hra_exemption", + label="Monthly HRA Exemption", + fieldtype="Currency", + insert_after="annual_hra_exemption", + read_only=1, + depends_on="monthly_house_rent", + ), ], - 'Employee Tax Exemption Proof Submission': [ - dict(fieldname='hra_section', label='HRA Exemption', - fieldtype='Section Break', insert_after='tax_exemption_proofs'), - dict(fieldname='house_rent_payment_amount', label='House Rent Payment Amount', - fieldtype='Currency', insert_after='hra_section'), - dict(fieldname='rented_in_metro_city', label='Rented in Metro City', - fieldtype='Check', insert_after='house_rent_payment_amount', depends_on='house_rent_payment_amount'), - dict(fieldname='rented_from_date', label='Rented From Date', - fieldtype='Date', insert_after='rented_in_metro_city', depends_on='house_rent_payment_amount'), - dict(fieldname='rented_to_date', label='Rented To Date', - fieldtype='Date', insert_after='rented_from_date', depends_on='house_rent_payment_amount'), - dict(fieldname='hra_column_break', fieldtype='Column Break', - insert_after='rented_to_date', depends_on='house_rent_payment_amount'), - dict(fieldname='monthly_house_rent', label='Monthly House Rent', - fieldtype='Currency', insert_after='hra_column_break', read_only=1, depends_on='house_rent_payment_amount'), - dict(fieldname='monthly_hra_exemption', label='Monthly Eligible Amount', - fieldtype='Currency', insert_after='monthly_house_rent', read_only=1, depends_on='house_rent_payment_amount'), - dict(fieldname='total_eligible_hra_exemption', label='Total Eligible HRA Exemption', - fieldtype='Currency', insert_after='monthly_hra_exemption', read_only=1, depends_on='house_rent_payment_amount') + "Employee Tax Exemption Proof Submission": [ + dict( + fieldname="hra_section", + label="HRA Exemption", + fieldtype="Section Break", + insert_after="tax_exemption_proofs", + ), + dict( + fieldname="house_rent_payment_amount", + label="House Rent Payment Amount", + fieldtype="Currency", + insert_after="hra_section", + ), + dict( + fieldname="rented_in_metro_city", + label="Rented in Metro City", + fieldtype="Check", + insert_after="house_rent_payment_amount", + depends_on="house_rent_payment_amount", + ), + dict( + fieldname="rented_from_date", + label="Rented From Date", + fieldtype="Date", + insert_after="rented_in_metro_city", + depends_on="house_rent_payment_amount", + ), + dict( + fieldname="rented_to_date", + label="Rented To Date", + fieldtype="Date", + insert_after="rented_from_date", + depends_on="house_rent_payment_amount", + ), + dict( + fieldname="hra_column_break", + fieldtype="Column Break", + insert_after="rented_to_date", + depends_on="house_rent_payment_amount", + ), + dict( + fieldname="monthly_house_rent", + label="Monthly House Rent", + fieldtype="Currency", + insert_after="hra_column_break", + read_only=1, + depends_on="house_rent_payment_amount", + ), + dict( + fieldname="monthly_hra_exemption", + label="Monthly Eligible Amount", + fieldtype="Currency", + insert_after="monthly_house_rent", + read_only=1, + depends_on="house_rent_payment_amount", + ), + dict( + fieldname="total_eligible_hra_exemption", + label="Total Eligible HRA Exemption", + fieldtype="Currency", + insert_after="monthly_hra_exemption", + read_only=1, + depends_on="house_rent_payment_amount", + ), ], - 'Supplier': [ + "Supplier": [ + {"fieldname": "pan", "label": "PAN", "fieldtype": "Data", "insert_after": "supplier_type"}, { - 'fieldname': 'pan', - 'label': 'PAN', - 'fieldtype': 'Data', - 'insert_after': 'supplier_type' + "fieldname": "gst_transporter_id", + "label": "GST Transporter ID", + "fieldtype": "Data", + "insert_after": "pan", + "depends_on": "eval:doc.is_transporter", }, { - 'fieldname': 'gst_transporter_id', - 'label': 'GST Transporter ID', - 'fieldtype': 'Data', - 'insert_after': 'pan', - 'depends_on': 'eval:doc.is_transporter' + "fieldname": "gst_category", + "label": "GST Category", + "fieldtype": "Select", + "insert_after": "gst_transporter_id", + "options": "Registered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nUIN Holders", + "default": "Unregistered", }, { - 'fieldname': 'gst_category', - 'label': 'GST Category', - 'fieldtype': 'Select', - 'insert_after': 'gst_transporter_id', - 'options': 'Registered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nUIN Holders', - 'default': 'Unregistered' + "fieldname": "export_type", + "label": "Export Type", + "fieldtype": "Select", + "insert_after": "gst_category", + "depends_on": 'eval:in_list(["SEZ", "Overseas"], doc.gst_category)', + "options": "\nWith Payment of Tax\nWithout Payment of Tax", + "mandatory_depends_on": 'eval:in_list(["SEZ", "Overseas"], doc.gst_category)', + }, + ], + "Customer": [ + {"fieldname": "pan", "label": "PAN", "fieldtype": "Data", "insert_after": "customer_type"}, + { + "fieldname": "gst_category", + "label": "GST Category", + "fieldtype": "Select", + "insert_after": "pan", + "options": "Registered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders", + "default": "Unregistered", }, { - 'fieldname': 'export_type', - 'label': 'Export Type', - 'fieldtype': 'Select', - 'insert_after': 'gst_category', - 'depends_on':'eval:in_list(["SEZ", "Overseas"], doc.gst_category)', - 'options': '\nWith Payment of Tax\nWithout Payment of Tax', - 'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas"], doc.gst_category)' + "fieldname": "export_type", + "label": "Export Type", + "fieldtype": "Select", + "insert_after": "gst_category", + "depends_on": 'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', + "options": "\nWith Payment of Tax\nWithout Payment of Tax", + "mandatory_depends_on": 'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', + }, + ], + "Finance Book": [ + { + "fieldname": "for_income_tax", + "label": "For Income Tax", + "fieldtype": "Check", + "insert_after": "finance_book_name", + "description": "If the asset is put to use for less than 180 days, the first Depreciation Rate will be reduced by 50%.", } ], - 'Customer': [ - { - 'fieldname': 'pan', - 'label': 'PAN', - 'fieldtype': 'Data', - 'insert_after': 'customer_type' - }, - { - 'fieldname': 'gst_category', - 'label': 'GST Category', - 'fieldtype': 'Select', - 'insert_after': 'pan', - 'options': 'Registered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders', - 'default': 'Unregistered' - }, - { - 'fieldname': 'export_type', - 'label': 'Export Type', - 'fieldtype': 'Select', - 'insert_after': 'gst_category', - 'depends_on':'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', - 'options': '\nWith Payment of Tax\nWithout Payment of Tax', - 'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)' - } - ], - 'Finance Book': [ - { - 'fieldname': 'for_income_tax', - 'label': 'For Income Tax', - 'fieldtype': 'Check', - 'insert_after': 'finance_book_name', - 'description': 'If the asset is put to use for less than 180 days, the first Depreciation Rate will be reduced by 50%.' - } - ] } return custom_fields + def make_fixtures(company=None): docs = [] company = company or frappe.db.get_value("Global Defaults", None, "default_company") @@ -734,33 +1227,47 @@ def make_fixtures(company=None): # create records for Tax Withholding Category set_tax_withholding_category(company) + def update_regional_tax_settings(country, company): # Will only add default GST accounts if present - input_account_names = ['Input Tax CGST', 'Input Tax SGST', 'Input Tax IGST'] - output_account_names = ['Output Tax CGST', 'Output Tax SGST', 'Output Tax IGST'] - rcm_accounts = ['Input Tax CGST RCM', 'Input Tax SGST RCM', 'Input Tax IGST RCM'] - gst_settings = frappe.get_single('GST Settings') + input_account_names = ["Input Tax CGST", "Input Tax SGST", "Input Tax IGST"] + output_account_names = ["Output Tax CGST", "Output Tax SGST", "Output Tax IGST"] + rcm_accounts = ["Input Tax CGST RCM", "Input Tax SGST RCM", "Input Tax IGST RCM"] + gst_settings = frappe.get_single("GST Settings") existing_account_list = [] - for account in gst_settings.get('gst_accounts'): - for key in ['cgst_account', 'sgst_account', 'igst_account']: + for account in gst_settings.get("gst_accounts"): + for key in ["cgst_account", "sgst_account", "igst_account"]: existing_account_list.append(account.get(key)) - gst_accounts = frappe._dict(frappe.get_all("Account", - {'company': company, 'account_name': ('in', input_account_names + - output_account_names + rcm_accounts)}, ['account_name', 'name'], as_list=1)) + gst_accounts = frappe._dict( + frappe.get_all( + "Account", + { + "company": company, + "account_name": ("in", input_account_names + output_account_names + rcm_accounts), + }, + ["account_name", "name"], + as_list=1, + ) + ) - add_accounts_in_gst_settings(company, input_account_names, gst_accounts, - existing_account_list, gst_settings) - add_accounts_in_gst_settings(company, output_account_names, gst_accounts, - existing_account_list, gst_settings) - add_accounts_in_gst_settings(company, rcm_accounts, gst_accounts, - existing_account_list, gst_settings, is_reverse_charge=1) + add_accounts_in_gst_settings( + company, input_account_names, gst_accounts, existing_account_list, gst_settings + ) + add_accounts_in_gst_settings( + company, output_account_names, gst_accounts, existing_account_list, gst_settings + ) + add_accounts_in_gst_settings( + company, rcm_accounts, gst_accounts, existing_account_list, gst_settings, is_reverse_charge=1 + ) gst_settings.save() -def add_accounts_in_gst_settings(company, account_names, gst_accounts, - existing_account_list, gst_settings, is_reverse_charge=0): + +def add_accounts_in_gst_settings( + company, account_names, gst_accounts, existing_account_list, gst_settings, is_reverse_charge=0 +): accounts_not_added = 1 for account in account_names: @@ -773,35 +1280,72 @@ def add_accounts_in_gst_settings(company, account_names, gst_accounts, accounts_not_added = 0 if accounts_not_added: - gst_settings.append('gst_accounts', { - 'company': company, - 'cgst_account': gst_accounts.get(account_names[0]), - 'sgst_account': gst_accounts.get(account_names[1]), - 'igst_account': gst_accounts.get(account_names[2]), - 'is_reverse_charge_account': is_reverse_charge - }) + gst_settings.append( + "gst_accounts", + { + "company": company, + "cgst_account": gst_accounts.get(account_names[0]), + "sgst_account": gst_accounts.get(account_names[1]), + "igst_account": gst_accounts.get(account_names[2]), + "is_reverse_charge_account": is_reverse_charge, + }, + ) + def set_salary_components(docs): - docs.extend([ - {'doctype': 'Salary Component', 'salary_component': 'Professional Tax', - 'description': 'Professional Tax', 'type': 'Deduction', 'exempted_from_income_tax': 1}, - {'doctype': 'Salary Component', 'salary_component': 'Provident Fund', - 'description': 'Provident fund', 'type': 'Deduction', 'is_tax_applicable': 1}, - {'doctype': 'Salary Component', 'salary_component': 'House Rent Allowance', - 'description': 'House Rent Allowance', 'type': 'Earning', 'is_tax_applicable': 1}, - {'doctype': 'Salary Component', 'salary_component': 'Basic', - 'description': 'Basic', 'type': 'Earning', 'is_tax_applicable': 1}, - {'doctype': 'Salary Component', 'salary_component': 'Arrear', - 'description': 'Arrear', 'type': 'Earning', 'is_tax_applicable': 1}, - {'doctype': 'Salary Component', 'salary_component': 'Leave Encashment', - 'description': 'Leave Encashment', 'type': 'Earning', 'is_tax_applicable': 1} - ]) + docs.extend( + [ + { + "doctype": "Salary Component", + "salary_component": "Professional Tax", + "description": "Professional Tax", + "type": "Deduction", + "exempted_from_income_tax": 1, + }, + { + "doctype": "Salary Component", + "salary_component": "Provident Fund", + "description": "Provident fund", + "type": "Deduction", + "is_tax_applicable": 1, + }, + { + "doctype": "Salary Component", + "salary_component": "House Rent Allowance", + "description": "House Rent Allowance", + "type": "Earning", + "is_tax_applicable": 1, + }, + { + "doctype": "Salary Component", + "salary_component": "Basic", + "description": "Basic", + "type": "Earning", + "is_tax_applicable": 1, + }, + { + "doctype": "Salary Component", + "salary_component": "Arrear", + "description": "Arrear", + "type": "Earning", + "is_tax_applicable": 1, + }, + { + "doctype": "Salary Component", + "salary_component": "Leave Encashment", + "description": "Leave Encashment", + "type": "Earning", + "is_tax_applicable": 1, + }, + ] + ) + def set_tax_withholding_category(company): accounts = [] fiscal_year_details = None abbr = frappe.get_value("Company", company, "abbr") - tds_account = frappe.get_value("Account", 'TDS Payable - {0}'.format(abbr), 'name') + tds_account = frappe.get_value("Account", "TDS Payable - {0}".format(abbr), "name") if company and tds_account: accounts = [dict(company=company, account=tds_account)] @@ -828,10 +1372,13 @@ def set_tax_withholding_category(company): if fiscal_year_details: # 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('from_date') <= fiscal_year_details[1] \ - and k.get('to_date') >= fiscal_year_details[2]] + fy_exist = [ + k + for k in doc.get("rates") + if k.get("from_date") <= fiscal_year_details[1] and k.get("to_date") >= fiscal_year_details[2] + ] if not fy_exist: - doc.append("rates", d.get('rates')[0]) + doc.append("rates", d.get("rates")[0]) doc.flags.ignore_permissions = True doc.flags.ignore_validate = True @@ -839,164 +1386,451 @@ def set_tax_withholding_category(company): doc.flags.ignore_links = True doc.save() + def set_tds_account(docs, company): - parent_account = frappe.db.get_value("Account", filters = {"account_name": "Duties and Taxes", "company": company}) + parent_account = frappe.db.get_value( + "Account", filters={"account_name": "Duties and Taxes", "company": company} + ) if parent_account: - docs.extend([ - { - "doctype": "Account", - "account_name": "TDS Payable", - "account_type": "Tax", - "parent_account": parent_account, - "company": company - } - ]) + docs.extend( + [ + { + "doctype": "Account", + "account_name": "TDS Payable", + "account_type": "Tax", + "parent_account": parent_account, + "company": company, + } + ] + ) + def get_tds_details(accounts, fiscal_year_details): # bootstrap default tax withholding sections return [ - dict(name="TDS - 194C - Company", + dict( + name="TDS - 194C - Company", category_name="Payment to Contractors (Single / Aggregate)", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 2, "single_threshold": 30000, "cumulative_threshold": 100000}]), - dict(name="TDS - 194C - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 2, + "single_threshold": 30000, + "cumulative_threshold": 100000, + } + ], + ), + dict( + name="TDS - 194C - Individual", category_name="Payment to Contractors (Single / Aggregate)", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 1, "single_threshold": 30000, "cumulative_threshold": 100000}]), - dict(name="TDS - 194C - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 1, + "single_threshold": 30000, + "cumulative_threshold": 100000, + } + ], + ), + dict( + name="TDS - 194C - No PAN / Invalid PAN", category_name="Payment to Contractors (Single / Aggregate)", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 30000, "cumulative_threshold": 100000}]), - dict(name="TDS - 194D - Company", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 30000, + "cumulative_threshold": 100000, + } + ], + ), + dict( + name="TDS - 194D - Company", category_name="Insurance Commission", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]), - dict(name="TDS - 194D - Company Assessee", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 5, + "single_threshold": 15000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194D - Company Assessee", category_name="Insurance Commission", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 15000, "cumulative_threshold": 0}]), - dict(name="TDS - 194D - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 15000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194D - Individual", category_name="Insurance Commission", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]), - dict(name="TDS - 194D - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 5, + "single_threshold": 15000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194D - No PAN / Invalid PAN", category_name="Insurance Commission", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 15000, "cumulative_threshold": 0}]), - dict(name="TDS - 194DA - Company", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 15000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194DA - Company", category_name="Non-exempt payments made under a life insurance policy", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 1, "single_threshold": 100000, "cumulative_threshold": 0}]), - dict(name="TDS - 194DA - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 1, + "single_threshold": 100000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194DA - Individual", category_name="Non-exempt payments made under a life insurance policy", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 1, "single_threshold": 100000, "cumulative_threshold": 0}]), - dict(name="TDS - 194DA - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 1, + "single_threshold": 100000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194DA - No PAN / Invalid PAN", category_name="Non-exempt payments made under a life insurance policy", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 100000, "cumulative_threshold": 0}]), - dict(name="TDS - 194H - Company", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 100000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194H - Company", category_name="Commission / Brokerage", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]), - dict(name="TDS - 194H - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 5, + "single_threshold": 15000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194H - Individual", category_name="Commission / Brokerage", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]), - dict(name="TDS - 194H - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 5, + "single_threshold": 15000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194H - No PAN / Invalid PAN", category_name="Commission / Brokerage", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 15000, "cumulative_threshold": 0}]), - dict(name="TDS - 194I - Rent - Company", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 15000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194I - Rent - Company", category_name="Rent", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 180000, "cumulative_threshold": 0}]), - dict(name="TDS - 194I - Rent - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 180000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194I - Rent - Individual", category_name="Rent", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 180000, "cumulative_threshold": 0}]), - dict(name="TDS - 194I - Rent - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 180000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194I - Rent - No PAN / Invalid PAN", category_name="Rent", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 180000, "cumulative_threshold": 0}]), - dict(name="TDS - 194I - Rent/Machinery - Company", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 180000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194I - Rent/Machinery - Company", category_name="Rent-Plant / Machinery", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 2, "single_threshold": 180000, "cumulative_threshold": 0}]), - dict(name="TDS - 194I - Rent/Machinery - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 2, + "single_threshold": 180000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194I - Rent/Machinery - Individual", category_name="Rent-Plant / Machinery", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 2, "single_threshold": 180000, "cumulative_threshold": 0}]), - dict(name="TDS - 194I - Rent/Machinery - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 2, + "single_threshold": 180000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194I - Rent/Machinery - No PAN / Invalid PAN", category_name="Rent-Plant / Machinery", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 180000, "cumulative_threshold": 0}]), - dict(name="TDS - 194J - Professional Fees - Company", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 180000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194J - Professional Fees - Company", category_name="Professional Fees", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 30000, "cumulative_threshold": 0}]), - dict(name="TDS - 194J - Professional Fees - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 30000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194J - Professional Fees - Individual", category_name="Professional Fees", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 30000, "cumulative_threshold": 0}]), - dict(name="TDS - 194J - Professional Fees - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 30000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194J - Professional Fees - No PAN / Invalid PAN", category_name="Professional Fees", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 30000, "cumulative_threshold": 0}]), - dict(name="TDS - 194J - Director Fees - Company", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 30000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194J - Director Fees - Company", category_name="Director Fees", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 0, "cumulative_threshold": 0}]), - dict(name="TDS - 194J - Director Fees - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 0, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194J - Director Fees - Individual", category_name="Director Fees", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 0, "cumulative_threshold": 0}]), - dict(name="TDS - 194J - Director Fees - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 0, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194J - Director Fees - No PAN / Invalid PAN", category_name="Director Fees", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 0, "cumulative_threshold": 0}]), - dict(name="TDS - 194 - Dividends - Company", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 0, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194 - Dividends - Company", category_name="Dividends", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 2500, "cumulative_threshold": 0}]), - dict(name="TDS - 194 - Dividends - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 2500, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194 - Dividends - Individual", category_name="Dividends", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 2500, "cumulative_threshold": 0}]), - dict(name="TDS - 194 - Dividends - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 2500, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194 - Dividends - No PAN / Invalid PAN", category_name="Dividends", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 2500, "cumulative_threshold": 0}]) + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 2500, + "cumulative_threshold": 0, + } + ], + ), ] + def create_gratuity_rule(): # Standard Indain Gratuity Rule if not frappe.db.exists("Gratuity Rule", "Indian Standard Gratuity Rule"): @@ -1006,16 +1840,16 @@ def create_gratuity_rule(): rule.work_experience_calculation_method = "Round Off Work Experience" rule.minimum_year_for_gratuity = 5 - fraction = 15/26 - rule.append("gratuity_rule_slabs", { - "from_year": 0, - "to_year":0, - "fraction_of_applicable_earnings": fraction - }) + fraction = 15 / 26 + rule.append( + "gratuity_rule_slabs", + {"from_year": 0, "to_year": 0, "fraction_of_applicable_earnings": fraction}, + ) rule.flags.ignore_mandatory = True rule.save() + def update_accounts_settings_for_taxes(): - if frappe.db.count('Company') == 1: - frappe.db.set_value('Accounts Settings', None, "add_taxes_from_item_tax_template", 0) + if frappe.db.count("Company") == 1: + frappe.db.set_value("Accounts Settings", None, "add_taxes_from_item_tax_template", 0) diff --git a/erpnext/regional/india/test_utils.py b/erpnext/regional/india/test_utils.py index c95a0b3cc6..5c248307ec 100644 --- a/erpnext/regional/india/test_utils.py +++ b/erpnext/regional/india/test_utils.py @@ -12,14 +12,12 @@ class TestIndiaUtils(unittest.TestCase): mock_get_cached.return_value = "India" # mock country posting_date = "2021-05-01" - invalid_names = ["SI$1231", "012345678901234567", "SI 2020 05", - "SI.2020.0001", "PI2021 - 001"] + invalid_names = ["SI$1231", "012345678901234567", "SI 2020 05", "SI.2020.0001", "PI2021 - 001"] for name in invalid_names: doc = frappe._dict(name=name, posting_date=posting_date) self.assertRaises(frappe.ValidationError, validate_document_name, doc) - valid_names = ["012345678901236", "SI/2020/0001", "SI/2020-0001", - "2020-PI-0001", "PI2020-0001"] + valid_names = ["012345678901236", "SI/2020/0001", "SI/2020-0001", "2020-PI-0001", "PI2020-0001"] for name in valid_names: doc = frappe._dict(name=name, posting_date=posting_date) try: diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 55b563e1cd..47e6ae67f4 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -12,43 +12,51 @@ from erpnext.hr.utils import get_salary_assignment from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.regional.india import number_state_mapping, state_numbers, states -GST_INVOICE_NUMBER_FORMAT = re.compile(r"^[a-zA-Z0-9\-/]+$") #alphanumeric and - / -GSTIN_FORMAT = re.compile("^[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}$") +GST_INVOICE_NUMBER_FORMAT = re.compile(r"^[a-zA-Z0-9\-/]+$") # alphanumeric and - / +GSTIN_FORMAT = re.compile( + "^[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}$" +) GSTIN_UIN_FORMAT = re.compile("^[0-9]{4}[A-Z]{3}[0-9]{5}[0-9A-Z]{3}") PAN_NUMBER_FORMAT = re.compile("[A-Z]{5}[0-9]{4}[A-Z]{1}") def validate_gstin_for_india(doc, method): - if hasattr(doc, 'gst_state'): + if hasattr(doc, "gst_state"): set_gst_state_and_state_number(doc) - if not hasattr(doc, 'gstin') or not doc.gstin: + if not hasattr(doc, "gstin") or not doc.gstin: return gst_category = [] - if hasattr(doc, 'gst_category'): + if hasattr(doc, "gst_category"): if len(doc.links): link_doctype = doc.links[0].get("link_doctype") link_name = doc.links[0].get("link_name") if link_doctype in ["Customer", "Supplier"]: - gst_category = frappe.db.get_value(link_doctype, {'name': link_name}, ['gst_category']) + gst_category = frappe.db.get_value(link_doctype, {"name": link_name}, ["gst_category"]) doc.gstin = doc.gstin.upper().strip() - if not doc.gstin or doc.gstin == 'NA': + if not doc.gstin or doc.gstin == "NA": return if len(doc.gstin) != 15: frappe.throw(_("A GSTIN must have 15 characters."), title=_("Invalid GSTIN")) - if gst_category and gst_category == 'UIN Holders': + if gst_category and gst_category == "UIN Holders": if not GSTIN_UIN_FORMAT.match(doc.gstin): - frappe.throw(_("The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers"), - title=_("Invalid GSTIN")) + frappe.throw( + _( + "The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers" + ), + title=_("Invalid GSTIN"), + ) else: if not GSTIN_FORMAT.match(doc.gstin): - frappe.throw(_("The input you've entered doesn't match the format of GSTIN."), title=_("Invalid GSTIN")) + frappe.throw( + _("The input you've entered doesn't match the format of GSTIN."), title=_("Invalid GSTIN") + ) validate_gstin_check_digit(doc.gstin) @@ -56,46 +64,68 @@ def validate_gstin_for_india(doc, method): frappe.throw(_("Please enter GST state"), title=_("Invalid State")) if doc.gst_state_number != doc.gstin[:2]: - frappe.throw(_("First 2 digits of GSTIN should match with State number {0}.") - .format(doc.gst_state_number), title=_("Invalid GSTIN")) + frappe.throw( + _("First 2 digits of GSTIN should match with State number {0}.").format(doc.gst_state_number), + title=_("Invalid GSTIN"), + ) + def validate_pan_for_india(doc, method): - if doc.get('country') != 'India' or not doc.get('pan'): + if doc.get("country") != "India" or not doc.get("pan"): return if not PAN_NUMBER_FORMAT.match(doc.pan): frappe.throw(_("Invalid PAN No. The input you've entered doesn't match the format of PAN.")) + 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, - 'is_reverse_charge': doc.is_reverse_charge}): + if doc.get("gst_state") and frappe.db.get_value( + "Tax Category", + { + "gst_state": doc.gst_state, + "is_inter_state": doc.is_inter_state, + "is_reverse_charge": doc.is_reverse_charge, + }, + ): if doc.is_inter_state: - frappe.throw(_("Inter State tax category for GST State {0} already exists").format(doc.gst_state)) + frappe.throw( + _("Inter State tax category for GST State {0} already exists").format(doc.gst_state) + ) else: - frappe.throw(_("Intra State tax category for GST State {0} already exists").format(doc.gst_state)) + frappe.throw( + _("Intra State tax category for GST State {0} already exists").format(doc.gst_state) + ) + def update_gst_category(doc, method): for link in doc.links: - if link.link_doctype in ['Customer', 'Supplier']: + if link.link_doctype in ["Customer", "Supplier"]: meta = frappe.get_meta(link.link_doctype) - if doc.get('gstin') and meta.has_field('gst_category'): - frappe.db.set_value(link.link_doctype, {'name': link.link_name, 'gst_category': 'Unregistered'}, 'gst_category', 'Registered Regular') + if doc.get("gstin") and meta.has_field("gst_category"): + frappe.db.set_value( + link.link_doctype, + {"name": link.link_name, "gst_category": "Unregistered"}, + "gst_category", + "Registered Regular", + ) + def set_gst_state_and_state_number(doc): if not doc.gst_state and doc.state: state = doc.state.lower() - states_lowercase = {s.lower():s for s in states} + states_lowercase = {s.lower(): s for s in states} if state in states_lowercase: doc.gst_state = states_lowercase[state] else: return doc.gst_state_number = state_numbers.get(doc.gst_state) -def validate_gstin_check_digit(gstin, label='GSTIN'): - ''' Function to validate the check digit of the GSTIN.''' + +def validate_gstin_check_digit(gstin, label="GSTIN"): + """Function to validate the check digit of the GSTIN.""" factor = 1 total = 0 - code_point_chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' + code_point_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" mod = len(code_point_chars) input_chars = gstin[:-1] for char in input_chars: @@ -104,24 +134,30 @@ def validate_gstin_check_digit(gstin, label='GSTIN'): total += digit factor = 2 if factor == 1 else 1 if gstin[-1] != code_point_chars[((mod - (total % mod)) % mod)]: - frappe.throw(_("""Invalid {0}! The check digit validation has failed. Please ensure you've typed the {0} correctly.""").format(label)) + frappe.throw( + _( + """Invalid {0}! The check digit validation has failed. Please ensure you've typed the {0} correctly.""" + ).format(label) + ) + def get_itemised_tax_breakup_header(item_doctype, tax_accounts): - hsn_wise_in_gst_settings = frappe.db.get_single_value('GST Settings','hsn_wise_tax_breakup') - if frappe.get_meta(item_doctype).has_field('gst_hsn_code') and hsn_wise_in_gst_settings: + hsn_wise_in_gst_settings = frappe.db.get_single_value("GST Settings", "hsn_wise_tax_breakup") + if frappe.get_meta(item_doctype).has_field("gst_hsn_code") and hsn_wise_in_gst_settings: return [_("HSN/SAC"), _("Taxable Amount")] + tax_accounts else: return [_("Item"), _("Taxable Amount")] + tax_accounts + def get_itemised_tax_breakup_data(doc, account_wise=False, hsn_wise=False): itemised_tax = get_itemised_tax(doc.taxes, with_tax_account=account_wise) itemised_taxable_amount = get_itemised_taxable_amount(doc.items) - if not frappe.get_meta(doc.doctype + " Item").has_field('gst_hsn_code'): + if not frappe.get_meta(doc.doctype + " Item").has_field("gst_hsn_code"): return itemised_tax, itemised_taxable_amount - hsn_wise_in_gst_settings = frappe.db.get_single_value('GST Settings','hsn_wise_tax_breakup') + hsn_wise_in_gst_settings = frappe.db.get_single_value("GST Settings", "hsn_wise_tax_breakup") tax_breakup_hsn_wise = hsn_wise or hsn_wise_in_gst_settings if tax_breakup_hsn_wise: @@ -136,7 +172,7 @@ def get_itemised_tax_breakup_data(doc, account_wise=False, hsn_wise=False): for tax_desc, tax_detail in taxes.items(): key = tax_desc if account_wise: - key = tax_detail.get('tax_account') + key = tax_detail.get("tax_account") hsn_tax[item_or_hsn].setdefault(key, {"tax_rate": 0, "tax_amount": 0}) hsn_tax[item_or_hsn][key]["tax_rate"] = tax_detail.get("tax_rate") hsn_tax[item_or_hsn][key]["tax_amount"] += tax_detail.get("tax_amount") @@ -150,9 +186,11 @@ def get_itemised_tax_breakup_data(doc, account_wise=False, hsn_wise=False): return hsn_tax, hsn_taxable_amount + def set_place_of_supply(doc, method=None): doc.place_of_supply = get_place_of_supply(doc, doc.doctype) + def validate_document_name(doc, method=None): """Validate GST invoice number requirements.""" @@ -163,18 +201,29 @@ def validate_document_name(doc, method=None): return if len(doc.name) > 16: - frappe.throw(_("Maximum length of document number should be 16 characters as per GST rules. Please change the naming series.")) + frappe.throw( + _( + "Maximum length of document number should be 16 characters as per GST rules. Please change the naming series." + ) + ) if not GST_INVOICE_NUMBER_FORMAT.match(doc.name): - frappe.throw(_("Document name should only contain alphanumeric values, dash(-) and slash(/) characters as per GST rules. Please change the naming series.")) + frappe.throw( + _( + "Document name should only contain alphanumeric values, dash(-) and slash(/) characters as per GST rules. Please change the naming series." + ) + ) + # don't remove this function it is used in tests def test_method(): - '''test function''' - return 'overridden' + """test function""" + return "overridden" + def get_place_of_supply(party_details, doctype): - if not frappe.get_meta('Address').has_field('gst_state'): return + if not frappe.get_meta("Address").has_field("gst_state"): + return if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): address_name = party_details.customer_address or party_details.shipping_address_name @@ -182,11 +231,14 @@ def get_place_of_supply(party_details, doctype): address_name = party_details.shipping_address or party_details.supplier_address if address_name: - address = frappe.db.get_value("Address", address_name, ["gst_state", "gst_state_number", "gstin"], as_dict=1) + address = frappe.db.get_value( + "Address", address_name, ["gst_state", "gst_state_number", "gstin"], as_dict=1 + ) if address and address.gst_state and address.gst_state_number: party_details.gstin = address.gstin return cstr(address.gst_state_number) + "-" + cstr(address.gst_state) + @frappe.whitelist() def get_regional_address_details(party_details, doctype, company): if isinstance(party_details, str): @@ -198,28 +250,40 @@ def get_regional_address_details(party_details, doctype, company): party_details.place_of_supply = get_place_of_supply(party_details, doctype) if is_internal_transfer(party_details, doctype): - party_details.taxes_and_charges = '' + party_details.taxes_and_charges = "" party_details.taxes = [] return party_details if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): master_doctype = "Sales Taxes and Charges Template" - tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details) + tax_template_by_category = get_tax_template_based_on_category( + master_doctype, company, party_details + ) elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"): master_doctype = "Purchase Taxes and Charges Template" - tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details) + tax_template_by_category = get_tax_template_based_on_category( + master_doctype, company, party_details + ) if tax_template_by_category: - party_details['taxes_and_charges'] = tax_template_by_category + party_details["taxes_and_charges"] = tax_template_by_category return party_details - if not party_details.place_of_supply: return party_details - if not party_details.company_gstin: return party_details + if not party_details.place_of_supply: + return party_details + if not party_details.company_gstin: + return party_details - if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin - and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice", - "Purchase Order", "Purchase Receipt") and party_details.supplier_gstin and party_details.supplier_gstin[:2] != party_details.place_of_supply[:2])): + if ( + doctype in ("Sales Invoice", "Delivery Note", "Sales Order") + and party_details.company_gstin + and party_details.company_gstin[:2] != party_details.place_of_supply[:2] + ) or ( + doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt") + and party_details.supplier_gstin + and party_details.supplier_gstin[:2] != party_details.place_of_supply[:2] + ): default_tax = get_tax_template(master_doctype, company, 1, party_details.company_gstin[:2]) else: default_tax = get_tax_template(master_doctype, company, 0, party_details.company_gstin[:2]) @@ -232,11 +296,19 @@ def get_regional_address_details(party_details, doctype, company): return party_details + def update_party_details(party_details, doctype): - for address_field in ['shipping_address', 'company_address', 'supplier_address', 'shipping_address_name', 'customer_address']: + for address_field in [ + "shipping_address", + "company_address", + "supplier_address", + "shipping_address_name", + "customer_address", + ]: if party_details.get(address_field): party_details.update(get_fetch_values(doctype, address_field, party_details.get(address_field))) + def is_internal_transfer(party_details, doctype): if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): destination_gstin = party_details.company_gstin @@ -251,66 +323,93 @@ def is_internal_transfer(party_details, doctype): else: False + def get_tax_template_based_on_category(master_doctype, company, party_details): - if not party_details.get('tax_category'): + if not party_details.get("tax_category"): return - default_tax = frappe.db.get_value(master_doctype, {'company': company, 'tax_category': party_details.get('tax_category')}, - 'name') + default_tax = frappe.db.get_value( + master_doctype, {"company": company, "tax_category": party_details.get("tax_category")}, "name" + ) return default_tax + def get_tax_template(master_doctype, company, is_inter_state, state_code): - tax_categories = frappe.get_all('Tax Category', fields = ['name', 'is_inter_state', 'gst_state'], - filters = {'is_inter_state': is_inter_state, 'is_reverse_charge': 0}) + tax_categories = frappe.get_all( + "Tax Category", + fields=["name", "is_inter_state", "gst_state"], + filters={"is_inter_state": is_inter_state, "is_reverse_charge": 0}, + ) - default_tax = '' + default_tax = "" for tax_category in tax_categories: - if tax_category.gst_state == number_state_mapping[state_code] or \ - (not default_tax and not tax_category.gst_state): - default_tax = frappe.db.get_value(master_doctype, - {'company': company, 'disabled': 0, 'tax_category': tax_category.name}, 'name') + if tax_category.gst_state == number_state_mapping[state_code] or ( + not default_tax and not tax_category.gst_state + ): + default_tax = frappe.db.get_value( + master_doctype, {"company": company, "disabled": 0, "tax_category": tax_category.name}, "name" + ) return default_tax + def calculate_annual_eligible_hra_exemption(doc): - basic_component, hra_component = frappe.db.get_value('Company', doc.company, ["basic_component", "hra_component"]) + basic_component, hra_component = frappe.db.get_value( + "Company", doc.company, ["basic_component", "hra_component"] + ) if not (basic_component and hra_component): frappe.throw(_("Please mention Basic and HRA component in Company")) annual_exemption, monthly_exemption, hra_amount = 0, 0, 0 if hra_component and basic_component: assignment = get_salary_assignment(doc.employee, nowdate()) if assignment: - hra_component_exists = frappe.db.exists("Salary Detail", { - "parent": assignment.salary_structure, - "salary_component": hra_component, - "parentfield": "earnings", - "parenttype": "Salary Structure" - }) + hra_component_exists = frappe.db.exists( + "Salary Detail", + { + "parent": assignment.salary_structure, + "salary_component": hra_component, + "parentfield": "earnings", + "parenttype": "Salary Structure", + }, + ) if hra_component_exists: - basic_amount, hra_amount = get_component_amt_from_salary_slip(doc.employee, - assignment.salary_structure, basic_component, hra_component) + basic_amount, hra_amount = get_component_amt_from_salary_slip( + doc.employee, assignment.salary_structure, basic_component, hra_component + ) if hra_amount: if doc.monthly_house_rent: - annual_exemption = calculate_hra_exemption(assignment.salary_structure, - basic_amount, hra_amount, doc.monthly_house_rent, doc.rented_in_metro_city) + annual_exemption = calculate_hra_exemption( + assignment.salary_structure, + basic_amount, + hra_amount, + doc.monthly_house_rent, + doc.rented_in_metro_city, + ) if annual_exemption > 0: monthly_exemption = annual_exemption / 12 else: annual_exemption = 0 elif doc.docstatus == 1: - frappe.throw(_("Salary Structure must be submitted before submission of Tax Ememption Declaration")) + frappe.throw( + _("Salary Structure must be submitted before submission of Tax Ememption Declaration") + ) + + return frappe._dict( + { + "hra_amount": hra_amount, + "annual_exemption": annual_exemption, + "monthly_exemption": monthly_exemption, + } + ) - return frappe._dict({ - "hra_amount": hra_amount, - "annual_exemption": annual_exemption, - "monthly_exemption": monthly_exemption - }) def get_component_amt_from_salary_slip(employee, salary_structure, basic_component, hra_component): - salary_slip = make_salary_slip(salary_structure, employee=employee, for_preview=1, ignore_permissions=True) + salary_slip = make_salary_slip( + salary_structure, employee=employee, for_preview=1, ignore_permissions=True + ) basic_amt, hra_amt = 0, 0 for earning in salary_slip.earnings: if earning.salary_component == basic_component: @@ -321,7 +420,10 @@ def get_component_amt_from_salary_slip(employee, salary_structure, basic_compone return basic_amt, hra_amt return basic_amt, hra_amt -def calculate_hra_exemption(salary_structure, basic, monthly_hra, monthly_house_rent, rented_in_metro_city): + +def calculate_hra_exemption( + salary_structure, basic, monthly_hra, monthly_house_rent, rented_in_metro_city +): # TODO make this configurable exemptions = [] frequency = frappe.get_value("Salary Structure", salary_structure, "payroll_frequency") @@ -338,6 +440,7 @@ def calculate_hra_exemption(salary_structure, basic, monthly_hra, monthly_house_ # return minimum of 3 cases return min(exemptions) + def get_annual_component_pay(frequency, amount): if frequency == "Daily": return amount * 365 @@ -350,6 +453,7 @@ def get_annual_component_pay(frequency, amount): elif frequency == "Bimonthly": return amount * 6 + def validate_house_rent_dates(doc): if not doc.rented_to_date or not doc.rented_from_date: frappe.throw(_("House rented dates required for exemption calculation")) @@ -357,30 +461,34 @@ def validate_house_rent_dates(doc): if date_diff(doc.rented_to_date, doc.rented_from_date) < 14: frappe.throw(_("House rented dates should be atleast 15 days apart")) - proofs = frappe.db.sql(""" + proofs = frappe.db.sql( + """ select name from `tabEmployee Tax Exemption Proof Submission` where docstatus=1 and employee=%(employee)s and payroll_period=%(payroll_period)s and (rented_from_date between %(from_date)s and %(to_date)s or rented_to_date between %(from_date)s and %(to_date)s) - """, { - "employee": doc.employee, - "payroll_period": doc.payroll_period, - "from_date": doc.rented_from_date, - "to_date": doc.rented_to_date - }) + """, + { + "employee": doc.employee, + "payroll_period": doc.payroll_period, + "from_date": doc.rented_from_date, + "to_date": doc.rented_to_date, + }, + ) if proofs: frappe.throw(_("House rent paid days overlapping with {0}").format(proofs[0][0])) + def calculate_hra_exemption_for_period(doc): monthly_rent, eligible_hra = 0, 0 if doc.house_rent_payment_amount: validate_house_rent_dates(doc) # TODO receive rented months or validate dates are start and end of months? # Calc monthly rent, round to nearest .5 - factor = flt(date_diff(doc.rented_to_date, doc.rented_from_date) + 1)/30 - factor = round(factor * 2)/2 + factor = flt(date_diff(doc.rented_to_date, doc.rented_from_date) + 1) / 30 + factor = round(factor * 2) / 2 monthly_rent = doc.house_rent_payment_amount / factor # update field used by calculate_annual_eligible_hra_exemption doc.monthly_house_rent = monthly_rent @@ -393,6 +501,7 @@ def calculate_hra_exemption_for_period(doc): exemptions["total_eligible_hra_exemption"] = eligible_hra return exemptions + def get_ewb_data(dt, dn): ewaybills = [] @@ -401,32 +510,38 @@ def get_ewb_data(dt, dn): validate_doc(doc) - data = frappe._dict({ - "transporterId": "", - "TotNonAdvolVal": 0, - }) + data = frappe._dict( + { + "transporterId": "", + "TotNonAdvolVal": 0, + } + ) data.userGstin = data.fromGstin = doc.company_gstin - data.supplyType = 'O' + data.supplyType = "O" - if dt == 'Delivery Note': + if dt == "Delivery Note": data.subSupplyType = 1 - elif doc.gst_category in ['Registered Regular', 'SEZ']: + elif doc.gst_category in ["Registered Regular", "SEZ"]: data.subSupplyType = 1 - elif doc.gst_category in ['Overseas', 'Deemed Export']: + elif doc.gst_category in ["Overseas", "Deemed Export"]: data.subSupplyType = 3 else: - frappe.throw(_('Unsupported GST Category for E-Way Bill JSON generation')) + frappe.throw(_("Unsupported GST Category for E-Way Bill JSON generation")) - data.docType = 'INV' - data.docDate = frappe.utils.formatdate(doc.posting_date, 'dd/mm/yyyy') + data.docType = "INV" + data.docDate = frappe.utils.formatdate(doc.posting_date, "dd/mm/yyyy") - company_address = frappe.get_doc('Address', doc.company_address) - billing_address = frappe.get_doc('Address', doc.customer_address) + company_address = frappe.get_doc("Address", doc.company_address) + billing_address = frappe.get_doc("Address", doc.customer_address) - #added dispatch address - dispatch_address = frappe.get_doc('Address', doc.dispatch_address_name) if doc.dispatch_address_name else company_address - shipping_address = frappe.get_doc('Address', doc.shipping_address_name) + # added dispatch address + dispatch_address = ( + frappe.get_doc("Address", doc.dispatch_address_name) + if doc.dispatch_address_name + else company_address + ) + shipping_address = frappe.get_doc("Address", doc.shipping_address_name) data = get_address_details(data, doc, company_address, billing_address, dispatch_address) @@ -435,75 +550,78 @@ def get_ewb_data(dt, dn): data = get_item_list(data, doc, hsn_wise=True) - disable_rounded = frappe.db.get_single_value('Global Defaults', 'disable_rounded_total') + disable_rounded = frappe.db.get_single_value("Global Defaults", "disable_rounded_total") data.totInvValue = doc.grand_total if disable_rounded else doc.rounded_total data = get_transport_details(data, doc) fields = { "/. -": { - 'docNo': doc.name, - 'fromTrdName': doc.company, - 'toTrdName': doc.customer_name, - 'transDocNo': doc.lr_no, + "docNo": doc.name, + "fromTrdName": doc.company, + "toTrdName": doc.customer_name, + "transDocNo": doc.lr_no, }, "@#/,&. -": { - 'fromAddr1': company_address.address_line1, - 'fromAddr2': company_address.address_line2, - 'fromPlace': company_address.city, - 'toAddr1': shipping_address.address_line1, - 'toAddr2': shipping_address.address_line2, - 'toPlace': shipping_address.city, - 'transporterName': doc.transporter_name - } + "fromAddr1": company_address.address_line1, + "fromAddr2": company_address.address_line2, + "fromPlace": company_address.city, + "toAddr1": shipping_address.address_line1, + "toAddr2": shipping_address.address_line2, + "toPlace": shipping_address.city, + "transporterName": doc.transporter_name, + }, } for allowed_chars, field_map in fields.items(): for key, value in field_map.items(): if not value: - data[key] = '' + data[key] = "" else: - data[key] = re.sub(r'[^\w' + allowed_chars + ']', '', value) + data[key] = re.sub(r"[^\w" + allowed_chars + "]", "", value) ewaybills.append(data) - data = { - 'version': '1.0.0421', - 'billLists': ewaybills - } + data = {"version": "1.0.0421", "billLists": ewaybills} return data + @frappe.whitelist() def generate_ewb_json(dt, dn): dn = json.loads(dn) return get_ewb_data(dt, dn) + @frappe.whitelist() def download_ewb_json(): data = json.loads(frappe.local.form_dict.data) frappe.local.response.filecontent = json.dumps(data, indent=4, sort_keys=True) - frappe.local.response.type = 'download' + frappe.local.response.type = "download" - filename_prefix = 'Bulk' + filename_prefix = "Bulk" docname = frappe.local.form_dict.docname if docname: - if docname.startswith('['): + if docname.startswith("["): docname = json.loads(docname) if len(docname) == 1: docname = docname[0] if not isinstance(docname, list): # removes characters not allowed in a filename (https://stackoverflow.com/a/38766141/4767738) - filename_prefix = re.sub(r'[^\w_.)( -]', '', docname) + filename_prefix = re.sub(r"[^\w_.)( -]", "", docname) + + frappe.local.response.filename = "{0}_e-WayBill_Data_{1}.json".format( + filename_prefix, frappe.utils.random_string(5) + ) - frappe.local.response.filename = '{0}_e-WayBill_Data_{1}.json'.format(filename_prefix, frappe.utils.random_string(5)) @frappe.whitelist() def get_gstins_for_company(company): - company_gstins =[] + company_gstins = [] if company: - company_gstins = frappe.db.sql("""select + company_gstins = frappe.db.sql( + """select distinct `tabAddress`.gstin from `tabAddress`, `tabDynamic Link` @@ -511,56 +629,66 @@ def get_gstins_for_company(company): `tabDynamic Link`.parent = `tabAddress`.name and `tabDynamic Link`.parenttype = 'Address' and `tabDynamic Link`.link_doctype = 'Company' and - `tabDynamic Link`.link_name = %(company)s""", {"company": company}) + `tabDynamic Link`.link_name = %(company)s""", + {"company": company}, + ) return company_gstins + def get_address_details(data, doc, company_address, billing_address, dispatch_address): - data.fromPincode = validate_pincode(company_address.pincode, 'Company Address') - data.fromStateCode = validate_state_code(company_address.gst_state_number, 'Company Address') - data.actualFromStateCode = validate_state_code(dispatch_address.gst_state_number, 'Dispatch Address') + data.fromPincode = validate_pincode(company_address.pincode, "Company Address") + data.fromStateCode = validate_state_code(company_address.gst_state_number, "Company Address") + data.actualFromStateCode = validate_state_code( + dispatch_address.gst_state_number, "Dispatch Address" + ) if not doc.billing_address_gstin or len(doc.billing_address_gstin) < 15: - data.toGstin = 'URP' + data.toGstin = "URP" set_gst_state_and_state_number(billing_address) else: data.toGstin = doc.billing_address_gstin - data.toPincode = validate_pincode(billing_address.pincode, 'Customer Address') - data.toStateCode = validate_state_code(billing_address.gst_state_number, 'Customer Address') + data.toPincode = validate_pincode(billing_address.pincode, "Customer Address") + data.toStateCode = validate_state_code(billing_address.gst_state_number, "Customer Address") if doc.customer_address != doc.shipping_address_name: data.transType = 2 - shipping_address = frappe.get_doc('Address', doc.shipping_address_name) + shipping_address = frappe.get_doc("Address", doc.shipping_address_name) set_gst_state_and_state_number(shipping_address) - data.toPincode = validate_pincode(shipping_address.pincode, 'Shipping Address') - data.actualToStateCode = validate_state_code(shipping_address.gst_state_number, 'Shipping Address') + data.toPincode = validate_pincode(shipping_address.pincode, "Shipping Address") + data.actualToStateCode = validate_state_code( + shipping_address.gst_state_number, "Shipping Address" + ) else: data.transType = 1 data.actualToStateCode = data.toStateCode shipping_address = billing_address - if doc.gst_category == 'SEZ': + if doc.gst_category == "SEZ": data.toStateCode = 99 return data + def get_item_list(data, doc, hsn_wise=False): - for attr in ['cgstValue', 'sgstValue', 'igstValue', 'cessValue', 'OthValue']: + for attr in ["cgstValue", "sgstValue", "igstValue", "cessValue", "OthValue"]: data[attr] = 0 gst_accounts = get_gst_accounts(doc.company, account_wise=True) tax_map = { - 'sgst_account': ['sgstRate', 'sgstValue'], - 'cgst_account': ['cgstRate', 'cgstValue'], - 'igst_account': ['igstRate', 'igstValue'], - 'cess_account': ['cessRate', 'cessValue'] + "sgst_account": ["sgstRate", "sgstValue"], + "cgst_account": ["cgstRate", "cgstValue"], + "igst_account": ["igstRate", "igstValue"], + "cess_account": ["cessRate", "cessValue"], } - item_data_attrs = ['sgstRate', 'cgstRate', 'igstRate', 'cessRate', 'cessNonAdvol'] - hsn_wise_charges, hsn_taxable_amount = get_itemised_tax_breakup_data(doc, account_wise=True, hsn_wise=hsn_wise) + item_data_attrs = ["sgstRate", "cgstRate", "igstRate", "cessRate", "cessNonAdvol"] + hsn_wise_charges, hsn_taxable_amount = get_itemised_tax_breakup_data( + doc, account_wise=True, hsn_wise=hsn_wise + ) for item_or_hsn, taxable_amount in hsn_taxable_amount.items(): item_data = frappe._dict() if not item_or_hsn: - frappe.throw(_('GST HSN Code does not exist for one or more items')) + frappe.throw(_("GST HSN Code does not exist for one or more items")) item_data.hsnCode = int(item_or_hsn) if hsn_wise else item_or_hsn item_data.taxableAmount = taxable_amount item_data.qtyUnit = "" @@ -568,87 +696,89 @@ def get_item_list(data, doc, hsn_wise=False): item_data[attr] = 0 for account, tax_detail in hsn_wise_charges.get(item_or_hsn, {}).items(): - account_type = gst_accounts.get(account, '') + account_type = gst_accounts.get(account, "") for tax_acc, attrs in tax_map.items(): if account_type == tax_acc: - item_data[attrs[0]] = tax_detail.get('tax_rate') - data[attrs[1]] += tax_detail.get('tax_amount') + item_data[attrs[0]] = tax_detail.get("tax_rate") + data[attrs[1]] += tax_detail.get("tax_amount") break else: - data.OthValue += tax_detail.get('tax_amount') + data.OthValue += tax_detail.get("tax_amount") data.itemList.append(item_data) # Tax amounts rounded to 2 decimals to avoid exceeding max character limit - for attr in ['sgstValue', 'cgstValue', 'igstValue', 'cessValue']: + for attr in ["sgstValue", "cgstValue", "igstValue", "cessValue"]: data[attr] = flt(data[attr], 2) return data + def validate_doc(doc): if doc.docstatus != 1: - frappe.throw(_('E-Way Bill JSON can only be generated from submitted document')) + frappe.throw(_("E-Way Bill JSON can only be generated from submitted document")) if doc.is_return: - frappe.throw(_('E-Way Bill JSON cannot be generated for Sales Return as of now')) + frappe.throw(_("E-Way Bill JSON cannot be generated for Sales Return as of now")) if doc.ewaybill: - frappe.throw(_('e-Way Bill already exists for this document')) + frappe.throw(_("e-Way Bill already exists for this document")) - reqd_fields = ['company_gstin', 'company_address', 'customer_address', - 'shipping_address_name', 'mode_of_transport', 'distance'] + reqd_fields = [ + "company_gstin", + "company_address", + "customer_address", + "shipping_address_name", + "mode_of_transport", + "distance", + ] for fieldname in reqd_fields: if not doc.get(fieldname): - frappe.throw(_('{} is required to generate E-Way Bill JSON').format( - doc.meta.get_label(fieldname) - )) + frappe.throw( + _("{} is required to generate E-Way Bill JSON").format(doc.meta.get_label(fieldname)) + ) if len(doc.company_gstin) < 15: - frappe.throw(_('You must be a registered supplier to generate e-Way Bill')) + frappe.throw(_("You must be a registered supplier to generate e-Way Bill")) + def get_transport_details(data, doc): if doc.distance > 4000: - frappe.throw(_('Distance cannot be greater than 4000 kms')) + frappe.throw(_("Distance cannot be greater than 4000 kms")) data.transDistance = int(round(doc.distance)) - transport_modes = { - 'Road': 1, - 'Rail': 2, - 'Air': 3, - 'Ship': 4 - } + transport_modes = {"Road": 1, "Rail": 2, "Air": 3, "Ship": 4} - vehicle_types = { - 'Regular': 'R', - 'Over Dimensional Cargo (ODC)': 'O' - } + vehicle_types = {"Regular": "R", "Over Dimensional Cargo (ODC)": "O"} data.transMode = transport_modes.get(doc.mode_of_transport) - if doc.mode_of_transport == 'Road': + if doc.mode_of_transport == "Road": if not doc.gst_transporter_id and not doc.vehicle_no: - frappe.throw(_('Either GST Transporter ID or Vehicle No is required if Mode of Transport is Road')) + frappe.throw( + _("Either GST Transporter ID or Vehicle No is required if Mode of Transport is Road") + ) if doc.vehicle_no: - data.vehicleNo = doc.vehicle_no.replace(' ', '') + data.vehicleNo = doc.vehicle_no.replace(" ", "") if not doc.gst_vehicle_type: - frappe.throw(_('Vehicle Type is required if Mode of Transport is Road')) + frappe.throw(_("Vehicle Type is required if Mode of Transport is Road")) else: data.vehicleType = vehicle_types.get(doc.gst_vehicle_type) else: if not doc.lr_no or not doc.lr_date: - frappe.throw(_('Transport Receipt No and Date are mandatory for your chosen Mode of Transport')) + frappe.throw(_("Transport Receipt No and Date are mandatory for your chosen Mode of Transport")) if doc.lr_no: data.transDocNo = doc.lr_no if doc.lr_date: - data.transDocDate = frappe.utils.formatdate(doc.lr_date, 'dd/mm/yyyy') + data.transDocDate = frappe.utils.formatdate(doc.lr_date, "dd/mm/yyyy") if doc.gst_transporter_id: if doc.gst_transporter_id[0:2] != "88": - validate_gstin_check_digit(doc.gst_transporter_id, label='GST Transporter ID') + validate_gstin_check_digit(doc.gst_transporter_id, label="GST Transporter ID") data.transporterId = doc.gst_transporter_id return data @@ -661,12 +791,13 @@ def validate_pincode(pincode, address): if not pincode: frappe.throw(_(pin_not_found.format(address))) - pincode = pincode.replace(' ', '') + pincode = pincode.replace(" ", "") if not pincode.isdigit() or len(pincode) != 6: frappe.throw(_(incorrect_pin.format(address))) else: return int(pincode) + def validate_state_code(state_code, address): no_state_code = "GST State Code not found for {0}. Please set GST State in {0}" if not state_code: @@ -674,21 +805,26 @@ def validate_state_code(state_code, address): else: return int(state_code) + @frappe.whitelist() -def get_gst_accounts(company=None, account_wise=False, only_reverse_charge=0, only_non_reverse_charge=0): - filters={"parent": "GST Settings"} +def get_gst_accounts( + company=None, account_wise=False, only_reverse_charge=0, only_non_reverse_charge=0 +): + filters = {"parent": "GST Settings"} if company: - filters.update({'company': company}) + filters.update({"company": company}) if only_reverse_charge: - filters.update({'is_reverse_charge_account': 1}) + filters.update({"is_reverse_charge_account": 1}) elif only_non_reverse_charge: - filters.update({'is_reverse_charge_account': 0}) + filters.update({"is_reverse_charge_account": 0}) gst_accounts = frappe._dict() - gst_settings_accounts = frappe.get_all("GST Account", + gst_settings_accounts = frappe.get_all( + "GST Account", filters=filters, - fields=["cgst_account", "sgst_account", "igst_account", "cess_account"]) + fields=["cgst_account", "sgst_account", "igst_account", "cess_account"], + ) if not gst_settings_accounts and not frappe.flags.in_test and not frappe.flags.in_migrate: frappe.throw(_("Please set GST Accounts in GST Settings")) @@ -702,32 +838,39 @@ def get_gst_accounts(company=None, account_wise=False, only_reverse_charge=0, on return gst_accounts -def validate_reverse_charge_transaction(doc, method): - country = frappe.get_cached_value('Company', doc.company, 'country') - if country != 'India': +def validate_reverse_charge_transaction(doc, method): + country = frappe.get_cached_value("Company", doc.company, "country") + + if country != "India": return base_gst_tax = 0 base_reverse_charge_booked = 0 - if doc.reverse_charge == 'Y': + if doc.reverse_charge == "Y": gst_accounts = get_gst_accounts(doc.company, only_reverse_charge=1) - reverse_charge_accounts = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \ - + gst_accounts.get('igst_account') + reverse_charge_accounts = ( + gst_accounts.get("cgst_account") + + gst_accounts.get("sgst_account") + + gst_accounts.get("igst_account") + ) gst_accounts = get_gst_accounts(doc.company, only_non_reverse_charge=1) - non_reverse_charge_accounts = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \ - + gst_accounts.get('igst_account') + non_reverse_charge_accounts = ( + gst_accounts.get("cgst_account") + + gst_accounts.get("sgst_account") + + gst_accounts.get("igst_account") + ) - for tax in doc.get('taxes'): + for tax in doc.get("taxes"): if tax.account_head in non_reverse_charge_accounts: - if tax.add_deduct_tax == 'Add': + if tax.add_deduct_tax == "Add": base_gst_tax += tax.base_tax_amount_after_discount_amount else: base_gst_tax += tax.base_tax_amount_after_discount_amount elif tax.account_head in reverse_charge_accounts: - if tax.add_deduct_tax == 'Add': + if tax.add_deduct_tax == "Add": base_reverse_charge_booked += tax.base_tax_amount_after_discount_amount else: base_reverse_charge_booked += tax.base_tax_amount_after_discount_amount @@ -735,57 +878,65 @@ def validate_reverse_charge_transaction(doc, method): if base_gst_tax != base_reverse_charge_booked: msg = _("Booked reverse charge is not equal to applied tax amount") msg += "
    " - msg += _("Please refer {gst_document_link} to learn more about how to setup and create reverse charge invoice").format( - gst_document_link='GST Documentation') + msg += _( + "Please refer {gst_document_link} to learn more about how to setup and create reverse charge invoice" + ).format( + gst_document_link='GST Documentation' + ) frappe.throw(msg) -def update_itc_availed_fields(doc, method): - country = frappe.get_cached_value('Company', doc.company, 'country') - if country != 'India': +def update_itc_availed_fields(doc, method): + country = frappe.get_cached_value("Company", doc.company, "country") + + if country != "India": return # Initialize values doc.itc_integrated_tax = doc.itc_state_tax = doc.itc_central_tax = doc.itc_cess_amount = 0 gst_accounts = get_gst_accounts(doc.company, only_non_reverse_charge=1) - for tax in doc.get('taxes'): - if tax.account_head in gst_accounts.get('igst_account', []): + for tax in doc.get("taxes"): + if tax.account_head in gst_accounts.get("igst_account", []): doc.itc_integrated_tax += flt(tax.base_tax_amount_after_discount_amount) - if tax.account_head in gst_accounts.get('sgst_account', []): + if tax.account_head in gst_accounts.get("sgst_account", []): doc.itc_state_tax += flt(tax.base_tax_amount_after_discount_amount) - if tax.account_head in gst_accounts.get('cgst_account', []): + if tax.account_head in gst_accounts.get("cgst_account", []): doc.itc_central_tax += flt(tax.base_tax_amount_after_discount_amount) - if tax.account_head in gst_accounts.get('cess_account', []): + if tax.account_head in gst_accounts.get("cess_account", []): doc.itc_cess_amount += flt(tax.base_tax_amount_after_discount_amount) + def update_place_of_supply(doc, method): - country = frappe.get_cached_value('Company', doc.company, 'country') - if country != 'India': + country = frappe.get_cached_value("Company", doc.company, "country") + if country != "India": return - address = frappe.db.get_value("Address", doc.get('customer_address'), ["gst_state", "gst_state_number"], as_dict=1) + address = frappe.db.get_value( + "Address", doc.get("customer_address"), ["gst_state", "gst_state_number"], as_dict=1 + ) if address and address.gst_state and address.gst_state_number: doc.place_of_supply = cstr(address.gst_state_number) + "-" + cstr(address.gst_state) + @frappe.whitelist() def get_regional_round_off_accounts(company, account_list): - country = frappe.get_cached_value('Company', company, 'country') + country = frappe.get_cached_value("Company", company, "country") - if country != 'India': + if country != "India": return if isinstance(account_list, str): account_list = json.loads(account_list) - if not frappe.db.get_single_value('GST Settings', 'round_off_gst_values'): + if not frappe.db.get_single_value("GST Settings", "round_off_gst_values"): return gst_accounts = get_gst_accounts(company) gst_account_list = [] - for account in ['cgst_account', 'sgst_account', 'igst_account']: + for account in ["cgst_account", "sgst_account", "igst_account"]: if account in gst_accounts: gst_account_list += gst_accounts.get(account) @@ -793,94 +944,113 @@ def get_regional_round_off_accounts(company, account_list): return account_list -def update_taxable_values(doc, method): - country = frappe.get_cached_value('Company', doc.company, 'country') - if country != 'India': +def update_taxable_values(doc, method): + country = frappe.get_cached_value("Company", doc.company, "country") + + if country != "India": return gst_accounts = get_gst_accounts(doc.company) # Only considering sgst account to avoid inflating taxable value - gst_account_list = gst_accounts.get('sgst_account', []) + gst_accounts.get('sgst_account', []) \ - + gst_accounts.get('igst_account', []) + gst_account_list = ( + gst_accounts.get("sgst_account", []) + + gst_accounts.get("sgst_account", []) + + gst_accounts.get("igst_account", []) + ) additional_taxes = 0 total_charges = 0 item_count = 0 considered_rows = [] - for tax in doc.get('taxes'): + for tax in doc.get("taxes"): prev_row_id = cint(tax.row_id) - 1 if tax.account_head in gst_account_list and prev_row_id not in considered_rows: - if tax.charge_type == 'On Previous Row Amount': - additional_taxes += doc.get('taxes')[prev_row_id].tax_amount_after_discount_amount + if tax.charge_type == "On Previous Row Amount": + additional_taxes += doc.get("taxes")[prev_row_id].tax_amount_after_discount_amount considered_rows.append(prev_row_id) - if tax.charge_type == 'On Previous Row Total': - additional_taxes += doc.get('taxes')[prev_row_id].base_total - doc.base_net_total + if tax.charge_type == "On Previous Row Total": + additional_taxes += doc.get("taxes")[prev_row_id].base_total - doc.base_net_total considered_rows.append(prev_row_id) - for item in doc.get('items'): + for item in doc.get("items"): proportionate_value = item.base_net_amount if doc.base_net_total else item.qty total_value = doc.base_net_total if doc.base_net_total else doc.total_qty - applicable_charges = flt(flt(proportionate_value * (flt(additional_taxes) / flt(total_value)), - item.precision('taxable_value'))) + applicable_charges = flt( + flt( + proportionate_value * (flt(additional_taxes) / flt(total_value)), + item.precision("taxable_value"), + ) + ) item.taxable_value = applicable_charges + proportionate_value total_charges += applicable_charges item_count += 1 if total_charges != additional_taxes: diff = additional_taxes - total_charges - doc.get('items')[item_count - 1].taxable_value += diff + doc.get("items")[item_count - 1].taxable_value += diff + def get_depreciation_amount(asset, depreciable_value, row): if row.depreciation_method in ("Straight Line", "Manual"): # if the Depreciation Schedule is being prepared for the first time if not asset.flags.increase_in_asset_life: - depreciation_amount = (flt(asset.gross_purchase_amount) - - flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations) + depreciation_amount = ( + flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life) + ) / flt(row.total_number_of_depreciations) # if the Depreciation Schedule is being modified after Asset Repair else: - depreciation_amount = (flt(row.value_after_depreciation) - - flt(row.expected_value_after_useful_life)) / (date_diff(asset.to_date, asset.available_for_use_date) / 365) + depreciation_amount = ( + flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) + ) / (date_diff(asset.to_date, asset.available_for_use_date) / 365) else: rate_of_depreciation = row.rate_of_depreciation # if its the first depreciation if depreciable_value == asset.gross_purchase_amount: - if row.finance_book and frappe.db.get_value('Finance Book', row.finance_book, 'for_income_tax'): + if row.finance_book and frappe.db.get_value("Finance Book", row.finance_book, "for_income_tax"): # as per IT act, if the asset is purchased in the 2nd half of fiscal year, then rate is divided by 2 diff = date_diff(row.depreciation_start_date, asset.available_for_use_date) if diff <= 180: rate_of_depreciation = rate_of_depreciation / 2 frappe.msgprint( - _('As per IT Act, the rate of depreciation for the first depreciation entry is reduced by 50%.')) + _( + "As per IT Act, the rate of depreciation for the first depreciation entry is reduced by 50%." + ) + ) depreciation_amount = flt(depreciable_value * (flt(rate_of_depreciation) / 100)) return depreciation_amount + def set_item_tax_from_hsn_code(item): if not item.taxes and item.gst_hsn_code: hsn_doc = frappe.get_doc("GST HSN Code", item.gst_hsn_code) for tax in hsn_doc.taxes: - item.append('taxes', { - 'item_tax_template': tax.item_tax_template, - 'tax_category': tax.tax_category, - 'valid_from': tax.valid_from - }) + item.append( + "taxes", + { + "item_tax_template": tax.item_tax_template, + "tax_category": tax.tax_category, + "valid_from": tax.valid_from, + }, + ) + def delete_gst_settings_for_company(doc, method): - if doc.country != 'India': + if doc.country != "India": return gst_settings = frappe.get_doc("GST Settings") records_to_delete = [] - for d in reversed(gst_settings.get('gst_accounts')): + for d in reversed(gst_settings.get("gst_accounts")): if d.company == doc.name: records_to_delete.append(d) diff --git a/erpnext/regional/italy/__init__.py b/erpnext/regional/italy/__init__.py index eb20f65afb..643e955704 100644 --- a/erpnext/regional/italy/__init__.py +++ b/erpnext/regional/italy/__init__.py @@ -1,77 +1,172 @@ fiscal_regimes = [ - "RF01-Ordinario", - "RF02-Contribuenti minimi (art.1, c.96-117, L. 244/07)", - "RF04-Agricoltura e attività connesse e pesca (artt.34 e 34-bis, DPR 633/72)", - "RF05-Vendita sali e tabacchi (art.74, c.1, DPR. 633/72)", - "RF06-Commercio fiammiferi (art.74, c.1, DPR 633/72)", - "RF07-Editoria (art.74, c.1, DPR 633/72)", - "RF08-Gestione servizi telefonia pubblica (art.74, c.1, DPR 633/72)", - "RF09-Rivendita documenti di trasporto pubblico e di sosta (art.74, c.1, DPR 633/72)", - "RF10-Intrattenimenti, giochi e altre attività di cui alla tariffa allegata al DPR 640/72 (art.74, c.6, DPR 633/72)", - "RF11-Agenzie viaggi e turismo (art.74-ter, DPR 633/72)", - "RF12-Agriturismo (art.5, c.2, L. 413/91)", - "RF13-Vendite a domicilio (art.25-bis, c.6, DPR 600/73)", - "RF14-Rivendita beni usati, oggetti d’arte, d’antiquariato o da collezione (art.36, DL 41/95)", - "RF15-Agenzie di vendite all’asta di oggetti d’arte, antiquariato o da collezione (art.40-bis, DL 41/95)", - "RF16-IVA per cassa P.A. (art.6, c.5, DPR 633/72)", - "RF17-IVA per cassa (art. 32-bis, DL 83/2012)", - "RF18-Altro", - "RF19-Regime forfettario (art.1, c.54-89, L. 190/2014)" + "RF01-Ordinario", + "RF02-Contribuenti minimi (art.1, c.96-117, L. 244/07)", + "RF04-Agricoltura e attività connesse e pesca (artt.34 e 34-bis, DPR 633/72)", + "RF05-Vendita sali e tabacchi (art.74, c.1, DPR. 633/72)", + "RF06-Commercio fiammiferi (art.74, c.1, DPR 633/72)", + "RF07-Editoria (art.74, c.1, DPR 633/72)", + "RF08-Gestione servizi telefonia pubblica (art.74, c.1, DPR 633/72)", + "RF09-Rivendita documenti di trasporto pubblico e di sosta (art.74, c.1, DPR 633/72)", + "RF10-Intrattenimenti, giochi e altre attività di cui alla tariffa allegata al DPR 640/72 (art.74, c.6, DPR 633/72)", + "RF11-Agenzie viaggi e turismo (art.74-ter, DPR 633/72)", + "RF12-Agriturismo (art.5, c.2, L. 413/91)", + "RF13-Vendite a domicilio (art.25-bis, c.6, DPR 600/73)", + "RF14-Rivendita beni usati, oggetti d’arte, d’antiquariato o da collezione (art.36, DL 41/95)", + "RF15-Agenzie di vendite all’asta di oggetti d’arte, antiquariato o da collezione (art.40-bis, DL 41/95)", + "RF16-IVA per cassa P.A. (art.6, c.5, DPR 633/72)", + "RF17-IVA per cassa (art. 32-bis, DL 83/2012)", + "RF18-Altro", + "RF19-Regime forfettario (art.1, c.54-89, L. 190/2014)", ] tax_exemption_reasons = [ - "N1-Escluse ex art. 15", - "N2-Non Soggette", - "N3-Non Imponibili", - "N4-Esenti", - "N5-Regime del margine / IVA non esposta in fattura", - "N6-Inversione Contabile", - "N7-IVA assolta in altro stato UE" + "N1-Escluse ex art. 15", + "N2-Non Soggette", + "N3-Non Imponibili", + "N4-Esenti", + "N5-Regime del margine / IVA non esposta in fattura", + "N6-Inversione Contabile", + "N7-IVA assolta in altro stato UE", ] mode_of_payment_codes = [ - "MP01-Contanti", - "MP02-Assegno", - "MP03-Assegno circolare", - "MP04-Contanti presso Tesoreria", - "MP05-Bonifico", - "MP06-Vaglia cambiario", - "MP07-Bollettino bancario", - "MP08-Carta di pagamento", - "MP09-RID", - "MP10-RID utenze", - "MP11-RID veloce", - "MP12-RIBA", - "MP13-MAV", - "MP14-Quietanza erario", - "MP15-Giroconto su conti di contabilità speciale", - "MP16-Domiciliazione bancaria", - "MP17-Domiciliazione postale", - "MP18-Bollettino di c/c postale", - "MP19-SEPA Direct Debit", - "MP20-SEPA Direct Debit CORE", - "MP21-SEPA Direct Debit B2B", - "MP22-Trattenuta su somme già riscosse" + "MP01-Contanti", + "MP02-Assegno", + "MP03-Assegno circolare", + "MP04-Contanti presso Tesoreria", + "MP05-Bonifico", + "MP06-Vaglia cambiario", + "MP07-Bollettino bancario", + "MP08-Carta di pagamento", + "MP09-RID", + "MP10-RID utenze", + "MP11-RID veloce", + "MP12-RIBA", + "MP13-MAV", + "MP14-Quietanza erario", + "MP15-Giroconto su conti di contabilità speciale", + "MP16-Domiciliazione bancaria", + "MP17-Domiciliazione postale", + "MP18-Bollettino di c/c postale", + "MP19-SEPA Direct Debit", + "MP20-SEPA Direct Debit CORE", + "MP21-SEPA Direct Debit B2B", + "MP22-Trattenuta su somme già riscosse", ] -vat_collectability_options = [ - "I-Immediata", - "D-Differita", - "S-Scissione dei Pagamenti" -] +vat_collectability_options = ["I-Immediata", "D-Differita", "S-Scissione dei Pagamenti"] -state_codes = {'Siracusa': 'SR', 'Bologna': 'BO', 'Grosseto': 'GR', 'Caserta': 'CE', 'Alessandria': 'AL', 'Ancona': 'AN', 'Pavia': 'PV', - 'Benevento or Beneventum': 'BN', 'Modena': 'MO', 'Lodi': 'LO', 'Novara': 'NO', 'Avellino': 'AV', 'Verona': 'VR', 'Forli-Cesena': 'FC', - 'Caltanissetta': 'CL', 'Brescia': 'BS', 'Rieti': 'RI', 'Treviso': 'TV', 'Ogliastra': 'OG', 'Olbia-Tempio': 'OT', 'Bergamo': 'BG', - 'Napoli': 'NA', 'Campobasso': 'CB', 'Fermo': 'FM', 'Roma': 'RM', 'Lucca': 'LU', 'Rovigo': 'RO', 'Piacenza': 'PC', 'Monza and Brianza': 'MB', - 'La Spezia': 'SP', 'Pescara': 'PE', 'Vercelli': 'VC', 'Enna': 'EN', 'Nuoro': 'NU', 'Medio Campidano': 'MD', 'Trieste': 'TS', 'Aosta': 'AO', - 'Firenze': 'FI', 'Trapani': 'TP', 'Messina': 'ME', 'Teramo': 'TE', 'Udine': 'UD', 'Verbano-Cusio-Ossola': 'VB', 'Padua': 'PD', - 'Reggio Emilia': 'RE', 'Frosinone': 'FR', 'Taranto': 'TA', 'Catanzaro': 'CZ', 'Belluno': 'BL', 'Pordenone': 'PN', 'Viterbo': 'VT', - 'Gorizia': 'GO', 'Vatican City': 'SCV', 'Ferrara': 'FE', 'Chieti': 'CH', 'Crotone': 'KR', 'Foggia': 'FG', 'Perugia': 'PG', 'Bari': 'BA', - 'Massa-Carrara': 'MS', 'Pisa': 'PI', 'Latina': 'LT', 'Salerno': 'SA', 'Turin': 'TO', 'Lecco': 'LC', 'Lecce': 'LE', 'Pistoia': 'PT', 'Como': 'CO', - 'Barletta-Andria-Trani': 'BT', 'Mantua': 'MN', 'Ragusa': 'RG', 'Macerata': 'MC', 'Imperia': 'IM', 'Palermo': 'PA', 'Matera': 'MT', "L'Aquila": 'AQ', - 'Milano': 'MI', 'Catania': 'CT', 'Pesaro e Urbino': 'PU', 'Potenza': 'PZ', 'Republic of San Marino': 'RSM', 'Genoa': 'GE', 'Brindisi': 'BR', - 'Cagliari': 'CA', 'Siena': 'SI', 'Vibo Valentia': 'VV', 'Reggio Calabria': 'RC', 'Ascoli Piceno': 'AP', 'Carbonia-Iglesias': 'CI', 'Oristano': 'OR', - 'Asti': 'AT', 'Ravenna': 'RA', 'Vicenza': 'VI', 'Savona': 'SV', 'Biella': 'BI', 'Rimini': 'RN', 'Agrigento': 'AG', 'Prato': 'PO', 'Cuneo': 'CN', - 'Cosenza': 'CS', 'Livorno or Leghorn': 'LI', 'Sondrio': 'SO', 'Cremona': 'CR', 'Isernia': 'IS', 'Trento': 'TN', 'Terni': 'TR', 'Bolzano/Bozen': 'BZ', - 'Parma': 'PR', 'Varese': 'VA', 'Venezia': 'VE', 'Sassari': 'SS', 'Arezzo': 'AR'} +state_codes = { + "Siracusa": "SR", + "Bologna": "BO", + "Grosseto": "GR", + "Caserta": "CE", + "Alessandria": "AL", + "Ancona": "AN", + "Pavia": "PV", + "Benevento or Beneventum": "BN", + "Modena": "MO", + "Lodi": "LO", + "Novara": "NO", + "Avellino": "AV", + "Verona": "VR", + "Forli-Cesena": "FC", + "Caltanissetta": "CL", + "Brescia": "BS", + "Rieti": "RI", + "Treviso": "TV", + "Ogliastra": "OG", + "Olbia-Tempio": "OT", + "Bergamo": "BG", + "Napoli": "NA", + "Campobasso": "CB", + "Fermo": "FM", + "Roma": "RM", + "Lucca": "LU", + "Rovigo": "RO", + "Piacenza": "PC", + "Monza and Brianza": "MB", + "La Spezia": "SP", + "Pescara": "PE", + "Vercelli": "VC", + "Enna": "EN", + "Nuoro": "NU", + "Medio Campidano": "MD", + "Trieste": "TS", + "Aosta": "AO", + "Firenze": "FI", + "Trapani": "TP", + "Messina": "ME", + "Teramo": "TE", + "Udine": "UD", + "Verbano-Cusio-Ossola": "VB", + "Padua": "PD", + "Reggio Emilia": "RE", + "Frosinone": "FR", + "Taranto": "TA", + "Catanzaro": "CZ", + "Belluno": "BL", + "Pordenone": "PN", + "Viterbo": "VT", + "Gorizia": "GO", + "Vatican City": "SCV", + "Ferrara": "FE", + "Chieti": "CH", + "Crotone": "KR", + "Foggia": "FG", + "Perugia": "PG", + "Bari": "BA", + "Massa-Carrara": "MS", + "Pisa": "PI", + "Latina": "LT", + "Salerno": "SA", + "Turin": "TO", + "Lecco": "LC", + "Lecce": "LE", + "Pistoia": "PT", + "Como": "CO", + "Barletta-Andria-Trani": "BT", + "Mantua": "MN", + "Ragusa": "RG", + "Macerata": "MC", + "Imperia": "IM", + "Palermo": "PA", + "Matera": "MT", + "L'Aquila": "AQ", + "Milano": "MI", + "Catania": "CT", + "Pesaro e Urbino": "PU", + "Potenza": "PZ", + "Republic of San Marino": "RSM", + "Genoa": "GE", + "Brindisi": "BR", + "Cagliari": "CA", + "Siena": "SI", + "Vibo Valentia": "VV", + "Reggio Calabria": "RC", + "Ascoli Piceno": "AP", + "Carbonia-Iglesias": "CI", + "Oristano": "OR", + "Asti": "AT", + "Ravenna": "RA", + "Vicenza": "VI", + "Savona": "SV", + "Biella": "BI", + "Rimini": "RN", + "Agrigento": "AG", + "Prato": "PO", + "Cuneo": "CN", + "Cosenza": "CS", + "Livorno or Leghorn": "LI", + "Sondrio": "SO", + "Cremona": "CR", + "Isernia": "IS", + "Trento": "TN", + "Terni": "TR", + "Bolzano/Bozen": "BZ", + "Parma": "PR", + "Varese": "VA", + "Venezia": "VE", + "Sassari": "SS", + "Arezzo": "AR", +} diff --git a/erpnext/regional/italy/setup.py b/erpnext/regional/italy/setup.py index 531f10d3f9..1f66b36122 100644 --- a/erpnext/regional/italy/setup.py +++ b/erpnext/regional/italy/setup.py @@ -7,213 +7,489 @@ import frappe from frappe import _ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.permissions import add_permission, update_permission_property -from erpnext.regional.italy import fiscal_regimes, tax_exemption_reasons, mode_of_payment_codes, vat_collectability_options +from erpnext.regional.italy import ( + fiscal_regimes, + tax_exemption_reasons, + mode_of_payment_codes, + vat_collectability_options, +) + def setup(company=None, patch=True): make_custom_fields() setup_report() add_permissions() + def make_custom_fields(update=True): invoice_item_fields = [ - dict(fieldname='tax_rate', label='Tax Rate', - fieldtype='Float', insert_after='description', - print_hide=1, hidden=1, read_only=1), - dict(fieldname='tax_amount', label='Tax Amount', - fieldtype='Currency', insert_after='tax_rate', - print_hide=1, hidden=1, read_only=1, options="currency"), - dict(fieldname='total_amount', label='Total Amount', - fieldtype='Currency', insert_after='tax_amount', - print_hide=1, hidden=1, read_only=1, options="currency") + dict( + fieldname="tax_rate", + label="Tax Rate", + fieldtype="Float", + insert_after="description", + print_hide=1, + hidden=1, + read_only=1, + ), + dict( + fieldname="tax_amount", + label="Tax Amount", + fieldtype="Currency", + insert_after="tax_rate", + print_hide=1, + hidden=1, + read_only=1, + options="currency", + ), + dict( + fieldname="total_amount", + label="Total Amount", + fieldtype="Currency", + insert_after="tax_amount", + print_hide=1, + hidden=1, + read_only=1, + options="currency", + ), ] customer_po_fields = [ - dict(fieldname='customer_po_details', label='Customer PO', - fieldtype='Section Break', insert_after='image'), - dict(fieldname='customer_po_no', label='Customer PO No', - fieldtype='Data', insert_after='customer_po_details', - fetch_from = 'sales_order.po_no', - print_hide=1, allow_on_submit=1, fetch_if_empty= 1, read_only=1, no_copy=1), - dict(fieldname='customer_po_clm_brk', label='', - fieldtype='Column Break', insert_after='customer_po_no', - print_hide=1, read_only=1), - dict(fieldname='customer_po_date', label='Customer PO Date', - fieldtype='Date', insert_after='customer_po_clm_brk', - fetch_from = 'sales_order.po_date', - print_hide=1, allow_on_submit=1, fetch_if_empty= 1, read_only=1, no_copy=1) + dict( + fieldname="customer_po_details", + label="Customer PO", + fieldtype="Section Break", + insert_after="image", + ), + dict( + fieldname="customer_po_no", + label="Customer PO No", + fieldtype="Data", + insert_after="customer_po_details", + fetch_from="sales_order.po_no", + print_hide=1, + allow_on_submit=1, + fetch_if_empty=1, + read_only=1, + no_copy=1, + ), + dict( + fieldname="customer_po_clm_brk", + label="", + fieldtype="Column Break", + insert_after="customer_po_no", + print_hide=1, + read_only=1, + ), + dict( + fieldname="customer_po_date", + label="Customer PO Date", + fieldtype="Date", + insert_after="customer_po_clm_brk", + fetch_from="sales_order.po_date", + print_hide=1, + allow_on_submit=1, + fetch_if_empty=1, + read_only=1, + no_copy=1, + ), ] custom_fields = { - 'Company': [ - dict(fieldname='sb_e_invoicing', label='E-Invoicing', - fieldtype='Section Break', insert_after='date_of_establishment', print_hide=1), - dict(fieldname='fiscal_regime', label='Fiscal Regime', - fieldtype='Select', insert_after='sb_e_invoicing', print_hide=1, - options="\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), fiscal_regimes))), - dict(fieldname='fiscal_code', label='Fiscal Code', fieldtype='Data', insert_after='fiscal_regime', print_hide=1, - description=_("Applicable if the company is an Individual or a Proprietorship")), - dict(fieldname='vat_collectability', label='VAT Collectability', - fieldtype='Select', insert_after='fiscal_code', print_hide=1, - options="\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), vat_collectability_options))), - dict(fieldname='cb_e_invoicing1', fieldtype='Column Break', insert_after='vat_collectability', print_hide=1), - dict(fieldname='registrar_office_province', label='Province of the Registrar Office', - fieldtype='Data', insert_after='cb_e_invoicing1', print_hide=1, length=2), - dict(fieldname='registration_number', label='Registration Number', - fieldtype='Data', insert_after='registrar_office_province', print_hide=1, length=20), - dict(fieldname='share_capital_amount', label='Share Capital', - fieldtype='Currency', insert_after='registration_number', print_hide=1, - description=_('Applicable if the company is SpA, SApA or SRL')), - dict(fieldname='no_of_members', label='No of Members', - fieldtype='Select', insert_after='share_capital_amount', print_hide=1, - options="\nSU-Socio Unico\nSM-Piu Soci", description=_("Applicable if the company is a limited liability company")), - dict(fieldname='liquidation_state', label='Liquidation State', - fieldtype='Select', insert_after='no_of_members', print_hide=1, - options="\nLS-In Liquidazione\nLN-Non in Liquidazione") + "Company": [ + dict( + fieldname="sb_e_invoicing", + label="E-Invoicing", + fieldtype="Section Break", + insert_after="date_of_establishment", + print_hide=1, + ), + dict( + fieldname="fiscal_regime", + label="Fiscal Regime", + fieldtype="Select", + insert_after="sb_e_invoicing", + print_hide=1, + options="\n".join(map(lambda x: frappe.safe_decode(x, encoding="utf-8"), fiscal_regimes)), + ), + dict( + fieldname="fiscal_code", + label="Fiscal Code", + fieldtype="Data", + insert_after="fiscal_regime", + print_hide=1, + description=_("Applicable if the company is an Individual or a Proprietorship"), + ), + dict( + fieldname="vat_collectability", + label="VAT Collectability", + fieldtype="Select", + insert_after="fiscal_code", + print_hide=1, + options="\n".join( + map(lambda x: frappe.safe_decode(x, encoding="utf-8"), vat_collectability_options) + ), + ), + dict( + fieldname="cb_e_invoicing1", + fieldtype="Column Break", + insert_after="vat_collectability", + print_hide=1, + ), + dict( + fieldname="registrar_office_province", + label="Province of the Registrar Office", + fieldtype="Data", + insert_after="cb_e_invoicing1", + print_hide=1, + length=2, + ), + dict( + fieldname="registration_number", + label="Registration Number", + fieldtype="Data", + insert_after="registrar_office_province", + print_hide=1, + length=20, + ), + dict( + fieldname="share_capital_amount", + label="Share Capital", + fieldtype="Currency", + insert_after="registration_number", + print_hide=1, + description=_("Applicable if the company is SpA, SApA or SRL"), + ), + dict( + fieldname="no_of_members", + label="No of Members", + fieldtype="Select", + insert_after="share_capital_amount", + print_hide=1, + options="\nSU-Socio Unico\nSM-Piu Soci", + description=_("Applicable if the company is a limited liability company"), + ), + dict( + fieldname="liquidation_state", + label="Liquidation State", + fieldtype="Select", + insert_after="no_of_members", + print_hide=1, + options="\nLS-In Liquidazione\nLN-Non in Liquidazione", + ), ], - 'Sales Taxes and Charges': [ - dict(fieldname='tax_exemption_reason', label='Tax Exemption Reason', - fieldtype='Select', insert_after='included_in_print_rate', print_hide=1, + "Sales Taxes and Charges": [ + dict( + fieldname="tax_exemption_reason", + label="Tax Exemption Reason", + fieldtype="Select", + insert_after="included_in_print_rate", + print_hide=1, depends_on='eval:doc.charge_type!="Actual" && doc.rate==0.0', - options="\n" + "\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), tax_exemption_reasons))), - dict(fieldname='tax_exemption_law', label='Tax Exempt Under', - fieldtype='Text', insert_after='tax_exemption_reason', print_hide=1, - depends_on='eval:doc.charge_type!="Actual" && doc.rate==0.0') + options="\n" + + "\n".join(map(lambda x: frappe.safe_decode(x, encoding="utf-8"), tax_exemption_reasons)), + ), + dict( + fieldname="tax_exemption_law", + label="Tax Exempt Under", + fieldtype="Text", + insert_after="tax_exemption_reason", + print_hide=1, + depends_on='eval:doc.charge_type!="Actual" && doc.rate==0.0', + ), ], - 'Customer': [ - dict(fieldname='fiscal_code', label='Fiscal Code', fieldtype='Data', insert_after='tax_id', print_hide=1), - dict(fieldname='recipient_code', label='Recipient Code', - fieldtype='Data', insert_after='fiscal_code', print_hide=1, default="0000000"), - dict(fieldname='pec', label='Recipient PEC', - fieldtype='Data', insert_after='fiscal_code', print_hide=1), - dict(fieldname='is_public_administration', label='Is Public Administration', - fieldtype='Check', insert_after='is_internal_customer', print_hide=1, + "Customer": [ + dict( + fieldname="fiscal_code", + label="Fiscal Code", + fieldtype="Data", + insert_after="tax_id", + print_hide=1, + ), + dict( + fieldname="recipient_code", + label="Recipient Code", + fieldtype="Data", + insert_after="fiscal_code", + print_hide=1, + default="0000000", + ), + dict( + fieldname="pec", + label="Recipient PEC", + fieldtype="Data", + insert_after="fiscal_code", + print_hide=1, + ), + dict( + fieldname="is_public_administration", + label="Is Public Administration", + fieldtype="Check", + insert_after="is_internal_customer", + print_hide=1, description=_("Set this if the customer is a Public Administration company."), - depends_on='eval:doc.customer_type=="Company"'), - dict(fieldname='first_name', label='First Name', fieldtype='Data', - insert_after='salutation', print_hide=1, depends_on='eval:doc.customer_type!="Company"'), - dict(fieldname='last_name', label='Last Name', fieldtype='Data', - insert_after='first_name', print_hide=1, depends_on='eval:doc.customer_type!="Company"') + depends_on='eval:doc.customer_type=="Company"', + ), + dict( + fieldname="first_name", + label="First Name", + fieldtype="Data", + insert_after="salutation", + print_hide=1, + depends_on='eval:doc.customer_type!="Company"', + ), + dict( + fieldname="last_name", + label="Last Name", + fieldtype="Data", + insert_after="first_name", + print_hide=1, + depends_on='eval:doc.customer_type!="Company"', + ), ], - 'Mode of Payment': [ - dict(fieldname='mode_of_payment_code', label='Code', - fieldtype='Select', insert_after='included_in_print_rate', print_hide=1, - options="\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), mode_of_payment_codes))) + "Mode of Payment": [ + dict( + fieldname="mode_of_payment_code", + label="Code", + fieldtype="Select", + insert_after="included_in_print_rate", + print_hide=1, + options="\n".join( + map(lambda x: frappe.safe_decode(x, encoding="utf-8"), mode_of_payment_codes) + ), + ) ], - 'Payment Schedule': [ - dict(fieldname='mode_of_payment_code', label='Code', - fieldtype='Select', insert_after='mode_of_payment', print_hide=1, - options="\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), mode_of_payment_codes)), - fetch_from="mode_of_payment.mode_of_payment_code", read_only=1), - dict(fieldname='bank_account', label='Bank Account', - fieldtype='Link', insert_after='mode_of_payment_code', print_hide=1, - options="Bank Account"), - dict(fieldname='bank_account_name', label='Bank Name', - fieldtype='Data', insert_after='bank_account', print_hide=1, - fetch_from="bank_account.bank", read_only=1), - dict(fieldname='bank_account_no', label='Bank Account No', - fieldtype='Data', insert_after='bank_account_name', print_hide=1, - fetch_from="bank_account.bank_account_no", read_only=1), - dict(fieldname='bank_account_iban', label='IBAN', - fieldtype='Data', insert_after='bank_account_name', print_hide=1, - fetch_from="bank_account.iban", read_only=1), - dict(fieldname='bank_account_swift_number', label='Swift Code (BIC)', - fieldtype='Data', insert_after='bank_account_iban', print_hide=1, - fetch_from="bank_account.swift_number", read_only=1), + "Payment Schedule": [ + dict( + fieldname="mode_of_payment_code", + label="Code", + fieldtype="Select", + insert_after="mode_of_payment", + print_hide=1, + options="\n".join( + map(lambda x: frappe.safe_decode(x, encoding="utf-8"), mode_of_payment_codes) + ), + fetch_from="mode_of_payment.mode_of_payment_code", + read_only=1, + ), + dict( + fieldname="bank_account", + label="Bank Account", + fieldtype="Link", + insert_after="mode_of_payment_code", + print_hide=1, + options="Bank Account", + ), + dict( + fieldname="bank_account_name", + label="Bank Name", + fieldtype="Data", + insert_after="bank_account", + print_hide=1, + fetch_from="bank_account.bank", + read_only=1, + ), + dict( + fieldname="bank_account_no", + label="Bank Account No", + fieldtype="Data", + insert_after="bank_account_name", + print_hide=1, + fetch_from="bank_account.bank_account_no", + read_only=1, + ), + dict( + fieldname="bank_account_iban", + label="IBAN", + fieldtype="Data", + insert_after="bank_account_name", + print_hide=1, + fetch_from="bank_account.iban", + read_only=1, + ), + dict( + fieldname="bank_account_swift_number", + label="Swift Code (BIC)", + fieldtype="Data", + insert_after="bank_account_iban", + print_hide=1, + fetch_from="bank_account.swift_number", + read_only=1, + ), ], "Sales Invoice": [ - dict(fieldname='vat_collectability', label='VAT Collectability', - fieldtype='Select', insert_after='taxes_and_charges', print_hide=1, - options="\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), vat_collectability_options)), - fetch_from="company.vat_collectability"), - dict(fieldname='sb_e_invoicing_reference', label='E-Invoicing', - fieldtype='Section Break', insert_after='against_income_account', print_hide=1), - dict(fieldname='company_fiscal_code', label='Company Fiscal Code', - fieldtype='Data', insert_after='sb_e_invoicing_reference', print_hide=1, read_only=1, - fetch_from="company.fiscal_code"), - dict(fieldname='company_fiscal_regime', label='Company Fiscal Regime', - fieldtype='Data', insert_after='company_fiscal_code', print_hide=1, read_only=1, - fetch_from="company.fiscal_regime"), - dict(fieldname='cb_e_invoicing_reference', fieldtype='Column Break', - insert_after='company_fiscal_regime', print_hide=1), - dict(fieldname='customer_fiscal_code', label='Customer Fiscal Code', - fieldtype='Data', insert_after='cb_e_invoicing_reference', read_only=1, - fetch_from="customer.fiscal_code"), - dict(fieldname='type_of_document', label='Type of Document', - fieldtype='Select', insert_after='customer_fiscal_code', - options='\nTD01\nTD02\nTD03\nTD04\nTD05\nTD06\nTD16\nTD17\nTD18\nTD19\nTD20\nTD21\nTD22\nTD23\nTD24\nTD25\nTD26\nTD27'), - ], - 'Purchase Invoice Item': invoice_item_fields, - 'Sales Order Item': invoice_item_fields, - 'Delivery Note Item': invoice_item_fields, - 'Sales Invoice Item': invoice_item_fields + customer_po_fields, - 'Quotation Item': invoice_item_fields, - 'Purchase Order Item': invoice_item_fields, - 'Purchase Receipt Item': invoice_item_fields, - 'Supplier Quotation Item': invoice_item_fields, - 'Address': [ - dict(fieldname='country_code', label='Country Code', - fieldtype='Data', insert_after='country', print_hide=1, read_only=0, - fetch_from="country.code"), - dict(fieldname='state_code', label='State Code', - fieldtype='Data', insert_after='state', print_hide=1) - ], - 'Purchase Invoice': [ - dict(fieldname='document_type', label='Document Type', - fieldtype='Data', insert_after='company', print_hide=1, read_only=1 + dict( + fieldname="vat_collectability", + label="VAT Collectability", + fieldtype="Select", + insert_after="taxes_and_charges", + print_hide=1, + options="\n".join( + map(lambda x: frappe.safe_decode(x, encoding="utf-8"), vat_collectability_options) ), - dict(fieldname='destination_code', label='Destination Code', - fieldtype='Data', insert_after='company', print_hide=1, read_only=1 - ), - dict(fieldname='imported_grand_total', label='Imported Grand Total', - fieldtype='Data', insert_after='update_auto_repeat_reference', print_hide=1, read_only=1 - ) + fetch_from="company.vat_collectability", + ), + dict( + fieldname="sb_e_invoicing_reference", + label="E-Invoicing", + fieldtype="Section Break", + insert_after="against_income_account", + print_hide=1, + ), + dict( + fieldname="company_fiscal_code", + label="Company Fiscal Code", + fieldtype="Data", + insert_after="sb_e_invoicing_reference", + print_hide=1, + read_only=1, + fetch_from="company.fiscal_code", + ), + dict( + fieldname="company_fiscal_regime", + label="Company Fiscal Regime", + fieldtype="Data", + insert_after="company_fiscal_code", + print_hide=1, + read_only=1, + fetch_from="company.fiscal_regime", + ), + dict( + fieldname="cb_e_invoicing_reference", + fieldtype="Column Break", + insert_after="company_fiscal_regime", + print_hide=1, + ), + dict( + fieldname="customer_fiscal_code", + label="Customer Fiscal Code", + fieldtype="Data", + insert_after="cb_e_invoicing_reference", + read_only=1, + fetch_from="customer.fiscal_code", + ), + dict( + fieldname="type_of_document", + label="Type of Document", + fieldtype="Select", + insert_after="customer_fiscal_code", + options="\nTD01\nTD02\nTD03\nTD04\nTD05\nTD06\nTD16\nTD17\nTD18\nTD19\nTD20\nTD21\nTD22\nTD23\nTD24\nTD25\nTD26\nTD27", + ), ], - 'Purchase Taxes and Charges': [ - dict(fieldname='tax_rate', label='Tax Rate', - fieldtype='Data', insert_after='parenttype', print_hide=1, read_only=0 - ) + "Purchase Invoice Item": invoice_item_fields, + "Sales Order Item": invoice_item_fields, + "Delivery Note Item": invoice_item_fields, + "Sales Invoice Item": invoice_item_fields + customer_po_fields, + "Quotation Item": invoice_item_fields, + "Purchase Order Item": invoice_item_fields, + "Purchase Receipt Item": invoice_item_fields, + "Supplier Quotation Item": invoice_item_fields, + "Address": [ + dict( + fieldname="country_code", + label="Country Code", + fieldtype="Data", + insert_after="country", + print_hide=1, + read_only=0, + fetch_from="country.code", + ), + dict( + fieldname="state_code", + label="State Code", + fieldtype="Data", + insert_after="state", + print_hide=1, + ), + ], + "Purchase Invoice": [ + dict( + fieldname="document_type", + label="Document Type", + fieldtype="Data", + insert_after="company", + print_hide=1, + read_only=1, + ), + dict( + fieldname="destination_code", + label="Destination Code", + fieldtype="Data", + insert_after="company", + print_hide=1, + read_only=1, + ), + dict( + fieldname="imported_grand_total", + label="Imported Grand Total", + fieldtype="Data", + insert_after="update_auto_repeat_reference", + print_hide=1, + read_only=1, + ), + ], + "Purchase Taxes and Charges": [ + dict( + fieldname="tax_rate", + label="Tax Rate", + fieldtype="Data", + insert_after="parenttype", + print_hide=1, + read_only=0, + ) + ], + "Supplier": [ + dict( + fieldname="fiscal_code", + label="Fiscal Code", + fieldtype="Data", + insert_after="tax_id", + print_hide=1, + read_only=1, + ), + dict( + fieldname="fiscal_regime", + label="Fiscal Regime", + fieldtype="Select", + insert_after="fiscal_code", + print_hide=1, + read_only=1, + options="\nRF01\nRF02\nRF04\nRF05\nRF06\nRF07\nRF08\nRF09\nRF10\nRF11\nRF12\nRF13\nRF14\nRF15\nRF16\nRF17\nRF18\nRF19", + ), ], - 'Supplier': [ - dict(fieldname='fiscal_code', label='Fiscal Code', - fieldtype='Data', insert_after='tax_id', print_hide=1, read_only=1 - ), - dict(fieldname='fiscal_regime', label='Fiscal Regime', - fieldtype='Select', insert_after='fiscal_code', print_hide=1, read_only=1, - options= "\nRF01\nRF02\nRF04\nRF05\nRF06\nRF07\nRF08\nRF09\nRF10\nRF11\nRF12\nRF13\nRF14\nRF15\nRF16\nRF17\nRF18\nRF19" - ) - ] } - create_custom_fields(custom_fields, ignore_validate = frappe.flags.in_patch, update=update) + create_custom_fields(custom_fields, ignore_validate=frappe.flags.in_patch, update=update) + def setup_report(): - report_name = 'Electronic Invoice Register' + report_name = "Electronic Invoice Register" frappe.db.set_value("Report", report_name, "disabled", 0) - if not frappe.db.get_value('Custom Role', dict(report=report_name)): - frappe.get_doc(dict( - doctype='Custom Role', - report=report_name, - roles= [ - dict(role='Accounts User'), - dict(role='Accounts Manager') - ] - )).insert() + if not frappe.db.get_value("Custom Role", dict(report=report_name)): + frappe.get_doc( + dict( + doctype="Custom Role", + report=report_name, + roles=[dict(role="Accounts User"), dict(role="Accounts Manager")], + ) + ).insert() + def add_permissions(): - doctype = 'Import Supplier Invoice' - add_permission(doctype, 'All', 0) + doctype = "Import Supplier Invoice" + add_permission(doctype, "All", 0) - for role in ('Accounts Manager', 'Accounts User','Purchase User', 'Auditor'): + for role in ("Accounts Manager", "Accounts User", "Purchase User", "Auditor"): add_permission(doctype, role, 0) - update_permission_property(doctype, role, 0, 'print', 1) - update_permission_property(doctype, role, 0, 'report', 1) + update_permission_property(doctype, role, 0, "print", 1) + update_permission_property(doctype, role, 0, "report", 1) - if role in ('Accounts Manager', 'Accounts User'): - update_permission_property(doctype, role, 0, 'write', 1) - update_permission_property(doctype, role, 0, 'create', 1) + if role in ("Accounts Manager", "Accounts User"): + update_permission_property(doctype, role, 0, "write", 1) + update_permission_property(doctype, role, 0, "create", 1) - update_permission_property(doctype, 'Accounts Manager', 0, 'delete', 1) - add_permission(doctype, 'Accounts Manager', 1) - update_permission_property(doctype, 'Accounts Manager', 1, 'write', 1) - update_permission_property(doctype, 'Accounts Manager', 1, 'create', 1) + update_permission_property(doctype, "Accounts Manager", 0, "delete", 1) + add_permission(doctype, "Accounts Manager", 1) + update_permission_property(doctype, "Accounts Manager", 1, "write", 1) + update_permission_property(doctype, "Accounts Manager", 1, "create", 1) diff --git a/erpnext/regional/italy/utils.py b/erpnext/regional/italy/utils.py index c82557b9de..f5b2e2d96b 100644 --- a/erpnext/regional/italy/utils.py +++ b/erpnext/regional/italy/utils.py @@ -11,41 +11,41 @@ from erpnext.regional.italy import state_codes def update_itemised_tax_data(doc): - if not doc.taxes: return + if not doc.taxes: + return - if doc.doctype == "Purchase Invoice": return + if doc.doctype == "Purchase Invoice": + return itemised_tax = get_itemised_tax(doc.taxes) for row in doc.items: tax_rate = 0.0 if itemised_tax.get(row.item_code): - tax_rate = sum([tax.get('tax_rate', 0) for d, tax in itemised_tax.get(row.item_code).items()]) + tax_rate = sum([tax.get("tax_rate", 0) for d, tax in itemised_tax.get(row.item_code).items()]) row.tax_rate = flt(tax_rate, row.precision("tax_rate")) row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount")) row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount")) + @frappe.whitelist() def export_invoices(filters=None): - frappe.has_permission('Sales Invoice', throw=True) + frappe.has_permission("Sales Invoice", throw=True) invoices = frappe.get_all( - "Sales Invoice", - filters=get_conditions(filters), - fields=["name", "company_tax_id"] + "Sales Invoice", filters=get_conditions(filters), fields=["name", "company_tax_id"] ) attachments = get_e_invoice_attachments(invoices) - zip_filename = "{0}-einvoices.zip".format( - frappe.utils.get_datetime().strftime("%Y%m%d_%H%M%S")) + zip_filename = "{0}-einvoices.zip".format(frappe.utils.get_datetime().strftime("%Y%m%d_%H%M%S")) download_zip(attachments, zip_filename) def prepare_invoice(invoice, progressive_number): - #set company information + # set company information company = frappe.get_doc("Company", invoice.company) invoice.progressive_number = progressive_number @@ -54,15 +54,17 @@ def prepare_invoice(invoice, progressive_number): company_address = frappe.get_doc("Address", invoice.company_address) invoice.company_address_data = company_address - #Set invoice type + # Set invoice type if not invoice.type_of_document: if invoice.is_return and invoice.return_against: - invoice.type_of_document = "TD04" #Credit Note (Nota di Credito) - invoice.return_against_unamended = get_unamended_name(frappe.get_doc("Sales Invoice", invoice.return_against)) + invoice.type_of_document = "TD04" # Credit Note (Nota di Credito) + invoice.return_against_unamended = get_unamended_name( + frappe.get_doc("Sales Invoice", invoice.return_against) + ) else: - invoice.type_of_document = "TD01" #Sales Invoice (Fattura) + invoice.type_of_document = "TD01" # Sales Invoice (Fattura) - #set customer information + # set customer information invoice.customer_data = frappe.get_doc("Customer", invoice.customer) customer_address = frappe.get_doc("Address", invoice.customer_address) invoice.customer_address_data = customer_address @@ -79,8 +81,10 @@ def prepare_invoice(invoice, progressive_number): tax_data = get_invoice_summary(invoice.e_invoice_items, invoice.taxes) invoice.tax_data = tax_data - #Check if stamp duty (Bollo) of 2 EUR exists. - stamp_duty_charge_row = next((tax for tax in invoice.taxes if tax.charge_type == "Actual" and tax.tax_amount == 2.0 ), None) + # Check if stamp duty (Bollo) of 2 EUR exists. + stamp_duty_charge_row = next( + (tax for tax in invoice.taxes if tax.charge_type == "Actual" and tax.tax_amount == 2.0), None + ) if stamp_duty_charge_row: invoice.stamp_duty = stamp_duty_charge_row.tax_amount @@ -90,24 +94,28 @@ def prepare_invoice(invoice, progressive_number): customer_po_data = {} for d in invoice.e_invoice_items: - if (d.customer_po_no and d.customer_po_date - and d.customer_po_no not in customer_po_data): + if d.customer_po_no and d.customer_po_date and d.customer_po_no not in customer_po_data: customer_po_data[d.customer_po_no] = d.customer_po_date invoice.customer_po_data = customer_po_data return invoice + def get_conditions(filters): filters = json.loads(filters) conditions = {"docstatus": 1, "company_tax_id": ("!=", "")} - if filters.get("company"): conditions["company"] = filters["company"] - if filters.get("customer"): conditions["customer"] = filters["customer"] + if filters.get("company"): + conditions["company"] = filters["company"] + if filters.get("customer"): + conditions["customer"] = filters["customer"] - if filters.get("from_date"): conditions["posting_date"] = (">=", filters["from_date"]) - if filters.get("to_date"): conditions["posting_date"] = ("<=", filters["to_date"]) + if filters.get("from_date"): + conditions["posting_date"] = (">=", filters["from_date"]) + if filters.get("to_date"): + conditions["posting_date"] = ("<=", filters["to_date"]) if filters.get("from_date") and filters.get("to_date"): conditions["posting_date"] = ("between", [filters.get("from_date"), filters.get("to_date")]) @@ -119,10 +127,9 @@ def download_zip(files, output_filename): import zipfile zip_stream = io.BytesIO() - with zipfile.ZipFile(zip_stream, 'w', zipfile.ZIP_DEFLATED) as zip_file: + with zipfile.ZipFile(zip_stream, "w", zipfile.ZIP_DEFLATED) as zip_file: for file in files: - file_path = frappe.utils.get_files_path( - file.file_name, is_private=file.is_private) + file_path = frappe.utils.get_files_path(file.file_name, is_private=file.is_private) zip_file.write(file_path, arcname=file.file_name) @@ -131,20 +138,21 @@ def download_zip(files, output_filename): frappe.local.response.type = "download" zip_stream.close() + def get_invoice_summary(items, taxes): summary_data = frappe._dict() for tax in taxes: - #Include only VAT charges. + # Include only VAT charges. if tax.charge_type == "Actual": continue - #Charges to appear as items in the e-invoice. + # Charges to appear as items in the e-invoice. if tax.charge_type in ["On Previous Row Total", "On Previous Row Amount"]: reference_row = next((row for row in taxes if row.idx == int(tax.row_id or 0)), None) if reference_row: items.append( frappe._dict( - idx=len(items)+1, + idx=len(items) + 1, item_code=reference_row.description, item_name=reference_row.description, description=reference_row.description, @@ -157,11 +165,11 @@ def get_invoice_summary(items, taxes): net_amount=reference_row.tax_amount, taxable_amount=reference_row.tax_amount, item_tax_rate={tax.account_head: tax.rate}, - charges=True + charges=True, ) ) - #Check item tax rates if tax rate is zero. + # Check item tax rates if tax rate is zero. if tax.rate == 0: for item in items: item_tax_rate = item.item_tax_rate @@ -171,8 +179,15 @@ def get_invoice_summary(items, taxes): if item_tax_rate and tax.account_head in item_tax_rate: key = cstr(item_tax_rate[tax.account_head]) if key not in summary_data: - summary_data.setdefault(key, {"tax_amount": 0.0, "taxable_amount": 0.0, - "tax_exemption_reason": "", "tax_exemption_law": ""}) + summary_data.setdefault( + key, + { + "tax_amount": 0.0, + "taxable_amount": 0.0, + "tax_exemption_reason": "", + "tax_exemption_law": "", + }, + ) summary_data[key]["tax_amount"] += item.tax_amount summary_data[key]["taxable_amount"] += item.net_amount @@ -180,93 +195,138 @@ def get_invoice_summary(items, taxes): summary_data[key]["tax_exemption_reason"] = tax.tax_exemption_reason summary_data[key]["tax_exemption_law"] = tax.tax_exemption_law - if summary_data.get("0.0") and tax.charge_type in ["On Previous Row Total", - "On Previous Row Amount"]: + if summary_data.get("0.0") and tax.charge_type in [ + "On Previous Row Total", + "On Previous Row Amount", + ]: summary_data[key]["taxable_amount"] = tax.total - if summary_data == {}: #Implies that Zero VAT has not been set on any item. - summary_data.setdefault("0.0", {"tax_amount": 0.0, "taxable_amount": tax.total, - "tax_exemption_reason": tax.tax_exemption_reason, "tax_exemption_law": tax.tax_exemption_law}) + if summary_data == {}: # Implies that Zero VAT has not been set on any item. + summary_data.setdefault( + "0.0", + { + "tax_amount": 0.0, + "taxable_amount": tax.total, + "tax_exemption_reason": tax.tax_exemption_reason, + "tax_exemption_law": tax.tax_exemption_law, + }, + ) else: item_wise_tax_detail = json.loads(tax.item_wise_tax_detail) - for rate_item in [tax_item for tax_item in item_wise_tax_detail.items() if tax_item[1][0] == tax.rate]: + for rate_item in [ + tax_item for tax_item in item_wise_tax_detail.items() if tax_item[1][0] == tax.rate + ]: key = cstr(tax.rate) - if not summary_data.get(key): summary_data.setdefault(key, {"tax_amount": 0.0, "taxable_amount": 0.0}) + if not summary_data.get(key): + summary_data.setdefault(key, {"tax_amount": 0.0, "taxable_amount": 0.0}) summary_data[key]["tax_amount"] += rate_item[1][1] - summary_data[key]["taxable_amount"] += sum([item.net_amount for item in items if item.item_code == rate_item[0]]) + summary_data[key]["taxable_amount"] += sum( + [item.net_amount for item in items if item.item_code == rate_item[0]] + ) for item in items: key = cstr(tax.rate) if item.get("charges"): - if not summary_data.get(key): summary_data.setdefault(key, {"taxable_amount": 0.0}) + if not summary_data.get(key): + summary_data.setdefault(key, {"taxable_amount": 0.0}) summary_data[key]["taxable_amount"] += item.taxable_amount return summary_data -#Preflight for successful e-invoice export. + +# Preflight for successful e-invoice export. def sales_invoice_validate(doc): - #Validate company - if doc.doctype != 'Sales Invoice': + # Validate company + if doc.doctype != "Sales Invoice": return if not doc.company_address: - frappe.throw(_("Please set an Address on the Company '%s'" % doc.company), title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Please set an Address on the Company '%s'" % doc.company), + title=_("E-Invoicing Information Missing"), + ) else: validate_address(doc.company_address) - company_fiscal_regime = frappe.get_cached_value("Company", doc.company, 'fiscal_regime') + company_fiscal_regime = frappe.get_cached_value("Company", doc.company, "fiscal_regime") if not company_fiscal_regime: - frappe.throw(_("Fiscal Regime is mandatory, kindly set the fiscal regime in the company {0}") - .format(doc.company)) + frappe.throw( + _("Fiscal Regime is mandatory, kindly set the fiscal regime in the company {0}").format( + doc.company + ) + ) else: doc.company_fiscal_regime = company_fiscal_regime - doc.company_tax_id = frappe.get_cached_value("Company", doc.company, 'tax_id') - doc.company_fiscal_code = frappe.get_cached_value("Company", doc.company, 'fiscal_code') + doc.company_tax_id = frappe.get_cached_value("Company", doc.company, "tax_id") + doc.company_fiscal_code = frappe.get_cached_value("Company", doc.company, "fiscal_code") if not doc.company_tax_id and not doc.company_fiscal_code: - frappe.throw(_("Please set either the Tax ID or Fiscal Code on Company '%s'" % doc.company), title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Please set either the Tax ID or Fiscal Code on Company '%s'" % doc.company), + title=_("E-Invoicing Information Missing"), + ) - #Validate customer details + # Validate customer details customer = frappe.get_doc("Customer", doc.customer) if customer.customer_type == "Individual": doc.customer_fiscal_code = customer.fiscal_code if not doc.customer_fiscal_code: - frappe.throw(_("Please set Fiscal Code for the customer '%s'" % doc.customer), title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Please set Fiscal Code for the customer '%s'" % doc.customer), + title=_("E-Invoicing Information Missing"), + ) else: if customer.is_public_administration: doc.customer_fiscal_code = customer.fiscal_code if not doc.customer_fiscal_code: - frappe.throw(_("Please set Fiscal Code for the public administration '%s'" % doc.customer), title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Please set Fiscal Code for the public administration '%s'" % doc.customer), + title=_("E-Invoicing Information Missing"), + ) else: doc.tax_id = customer.tax_id if not doc.tax_id: - frappe.throw(_("Please set Tax ID for the customer '%s'" % doc.customer), title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Please set Tax ID for the customer '%s'" % doc.customer), + title=_("E-Invoicing Information Missing"), + ) if not doc.customer_address: - frappe.throw(_("Please set the Customer Address"), title=_("E-Invoicing Information Missing")) + frappe.throw(_("Please set the Customer Address"), title=_("E-Invoicing Information Missing")) else: validate_address(doc.customer_address) if not len(doc.taxes): - frappe.throw(_("Please set at least one row in the Taxes and Charges Table"), title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Please set at least one row in the Taxes and Charges Table"), + title=_("E-Invoicing Information Missing"), + ) else: for row in doc.taxes: if row.rate == 0 and row.tax_amount == 0 and not row.tax_exemption_reason: - frappe.throw(_("Row {0}: Please set at Tax Exemption Reason in Sales Taxes and Charges").format(row.idx), - title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Row {0}: Please set at Tax Exemption Reason in Sales Taxes and Charges").format(row.idx), + title=_("E-Invoicing Information Missing"), + ) for schedule in doc.payment_schedule: if schedule.mode_of_payment and not schedule.mode_of_payment_code: - schedule.mode_of_payment_code = frappe.get_cached_value('Mode of Payment', - schedule.mode_of_payment, 'mode_of_payment_code') + schedule.mode_of_payment_code = frappe.get_cached_value( + "Mode of Payment", schedule.mode_of_payment, "mode_of_payment_code" + ) -#Ensure payment details are valid for e-invoice. + +# Ensure payment details are valid for e-invoice. def sales_invoice_on_submit(doc, method): - #Validate payment details - if get_company_country(doc.company) not in ['Italy', - 'Italia', 'Italian Republic', 'Repubblica Italiana']: + # Validate payment details + if get_company_country(doc.company) not in [ + "Italy", + "Italia", + "Italian Republic", + "Repubblica Italiana", + ]: return if not len(doc.payment_schedule): @@ -274,38 +334,53 @@ def sales_invoice_on_submit(doc, method): else: for schedule in doc.payment_schedule: if not schedule.mode_of_payment: - frappe.throw(_("Row {0}: Please set the Mode of Payment in Payment Schedule").format(schedule.idx), - title=_("E-Invoicing Information Missing")) - elif not frappe.db.get_value("Mode of Payment", schedule.mode_of_payment, "mode_of_payment_code"): - frappe.throw(_("Row {0}: Please set the correct code on Mode of Payment {1}").format(schedule.idx, schedule.mode_of_payment), - title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Row {0}: Please set the Mode of Payment in Payment Schedule").format(schedule.idx), + title=_("E-Invoicing Information Missing"), + ) + elif not frappe.db.get_value( + "Mode of Payment", schedule.mode_of_payment, "mode_of_payment_code" + ): + frappe.throw( + _("Row {0}: Please set the correct code on Mode of Payment {1}").format( + schedule.idx, schedule.mode_of_payment + ), + title=_("E-Invoicing Information Missing"), + ) prepare_and_attach_invoice(doc) + def prepare_and_attach_invoice(doc, replace=False): progressive_name, progressive_number = get_progressive_name_and_number(doc, replace) invoice = prepare_invoice(doc, progressive_number) item_meta = frappe.get_meta("Sales Invoice Item") - invoice_xml = frappe.render_template('erpnext/regional/italy/e-invoice.xml', - context={"doc": invoice, "item_meta": item_meta}, is_path=True) + invoice_xml = frappe.render_template( + "erpnext/regional/italy/e-invoice.xml", + context={"doc": invoice, "item_meta": item_meta}, + is_path=True, + ) invoice_xml = invoice_xml.replace("&", "&") xml_filename = progressive_name + ".xml" - _file = frappe.get_doc({ - "doctype": "File", - "file_name": xml_filename, - "attached_to_doctype": doc.doctype, - "attached_to_name": doc.name, - "is_private": True, - "content": invoice_xml - }) + _file = frappe.get_doc( + { + "doctype": "File", + "file_name": xml_filename, + "attached_to_doctype": doc.doctype, + "attached_to_name": doc.name, + "is_private": True, + "content": invoice_xml, + } + ) _file.save() return _file + @frappe.whitelist() def generate_single_invoice(docname): doc = frappe.get_doc("Sales Invoice", docname) @@ -314,17 +389,24 @@ def generate_single_invoice(docname): e_invoice = prepare_and_attach_invoice(doc, True) return e_invoice.file_url + # Delete e-invoice attachment on cancel. def sales_invoice_on_cancel(doc, method): - if get_company_country(doc.company) not in ['Italy', - 'Italia', 'Italian Republic', 'Repubblica Italiana']: + if get_company_country(doc.company) not in [ + "Italy", + "Italia", + "Italian Republic", + "Repubblica Italiana", + ]: return for attachment in get_e_invoice_attachments(doc): remove_file(attachment.name, attached_to_doctype=doc.doctype, attached_to_name=doc.name) + def get_company_country(company): - return frappe.get_cached_value('Company', company, 'country') + return frappe.get_cached_value("Company", company, "country") + def get_e_invoice_attachments(invoices): if not isinstance(invoices, list): @@ -338,16 +420,14 @@ def get_e_invoice_attachments(invoices): invoice.company_tax_id if invoice.company_tax_id.startswith("IT") else "IT" + invoice.company_tax_id - ) for invoice in invoices + ) + for invoice in invoices } attachments = frappe.get_all( "File", fields=("name", "file_name", "attached_to_name", "is_private"), - filters= { - "attached_to_name": ('in', tax_id_map), - "attached_to_doctype": 'Sales Invoice' - } + filters={"attached_to_name": ("in", tax_id_map), "attached_to_doctype": "Sales Invoice"}, ) out = [] @@ -355,21 +435,24 @@ def get_e_invoice_attachments(invoices): if ( attachment.file_name and attachment.file_name.endswith(".xml") - and attachment.file_name.startswith( - tax_id_map.get(attachment.attached_to_name)) + and attachment.file_name.startswith(tax_id_map.get(attachment.attached_to_name)) ): out.append(attachment) return out + def validate_address(address_name): fields = ["pincode", "city", "country_code"] data = frappe.get_cached_value("Address", address_name, fields, as_dict=1) or {} for field in fields: if not data.get(field): - frappe.throw(_("Please set {0} for address {1}").format(field.replace('-',''), address_name), - title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Please set {0} for address {1}").format(field.replace("-", ""), address_name), + title=_("E-Invoicing Information Missing"), + ) + def get_unamended_name(doc): attributes = ["naming_series", "amended_from"] @@ -382,6 +465,7 @@ def get_unamended_name(doc): else: return doc.name + def get_progressive_name_and_number(doc, replace=False): if replace: for attachment in get_e_invoice_attachments(doc): @@ -389,24 +473,30 @@ def get_progressive_name_and_number(doc, replace=False): filename = attachment.file_name.split(".xml")[0] return filename, filename.split("_")[1] - company_tax_id = doc.company_tax_id if doc.company_tax_id.startswith("IT") else "IT" + doc.company_tax_id + company_tax_id = ( + doc.company_tax_id if doc.company_tax_id.startswith("IT") else "IT" + doc.company_tax_id + ) progressive_name = frappe.model.naming.make_autoname(company_tax_id + "_.#####") progressive_number = progressive_name.split("_")[1] return progressive_name, progressive_number + def set_state_code(doc, method): - if doc.get('country_code'): + if doc.get("country_code"): doc.country_code = doc.country_code.upper() - if not doc.get('state'): + if not doc.get("state"): return - if not (hasattr(doc, "state_code") and doc.country in ["Italy", "Italia", "Italian Republic", "Repubblica Italiana"]): + if not ( + hasattr(doc, "state_code") + and doc.country in ["Italy", "Italia", "Italian Republic", "Repubblica Italiana"] + ): return - state_codes_lower = {key.lower():value for key,value in state_codes.items()} + state_codes_lower = {key.lower(): value for key, value in state_codes.items()} - state = doc.get('state','').lower() + state = doc.get("state", "").lower() if state_codes_lower.get(state): doc.state_code = state_codes_lower.get(state) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 92a10c288f..2d888a8762 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -25,134 +25,104 @@ COLUMNS = [ "label": "Umsatz (ohne Soll/Haben-Kz)", "fieldname": "Umsatz (ohne Soll/Haben-Kz)", "fieldtype": "Currency", - "width": 100 + "width": 100, }, { "label": "Soll/Haben-Kennzeichen", "fieldname": "Soll/Haben-Kennzeichen", "fieldtype": "Data", - "width": 100 - }, - { - "label": "Konto", - "fieldname": "Konto", - "fieldtype": "Data", - "width": 100 + "width": 100, }, + {"label": "Konto", "fieldname": "Konto", "fieldtype": "Data", "width": 100}, { "label": "Gegenkonto (ohne BU-Schlüssel)", "fieldname": "Gegenkonto (ohne BU-Schlüssel)", "fieldtype": "Data", - "width": 100 - }, - { - "label": "BU-Schlüssel", - "fieldname": "BU-Schlüssel", - "fieldtype": "Data", - "width": 100 - }, - { - "label": "Belegdatum", - "fieldname": "Belegdatum", - "fieldtype": "Date", - "width": 100 - }, - { - "label": "Belegfeld 1", - "fieldname": "Belegfeld 1", - "fieldtype": "Data", - "width": 150 - }, - { - "label": "Buchungstext", - "fieldname": "Buchungstext", - "fieldtype": "Text", - "width": 300 + "width": 100, }, + {"label": "BU-Schlüssel", "fieldname": "BU-Schlüssel", "fieldtype": "Data", "width": 100}, + {"label": "Belegdatum", "fieldname": "Belegdatum", "fieldtype": "Date", "width": 100}, + {"label": "Belegfeld 1", "fieldname": "Belegfeld 1", "fieldtype": "Data", "width": 150}, + {"label": "Buchungstext", "fieldname": "Buchungstext", "fieldtype": "Text", "width": 300}, { "label": "Beleginfo - Art 1", "fieldname": "Beleginfo - Art 1", "fieldtype": "Link", "options": "DocType", - "width": 100 + "width": 100, }, { "label": "Beleginfo - Inhalt 1", "fieldname": "Beleginfo - Inhalt 1", "fieldtype": "Dynamic Link", "options": "Beleginfo - Art 1", - "width": 150 + "width": 150, }, { "label": "Beleginfo - Art 2", "fieldname": "Beleginfo - Art 2", "fieldtype": "Link", "options": "DocType", - "width": 100 + "width": 100, }, { "label": "Beleginfo - Inhalt 2", "fieldname": "Beleginfo - Inhalt 2", "fieldtype": "Dynamic Link", "options": "Beleginfo - Art 2", - "width": 150 + "width": 150, }, { "label": "Beleginfo - Art 3", "fieldname": "Beleginfo - Art 3", "fieldtype": "Link", "options": "DocType", - "width": 100 + "width": 100, }, { "label": "Beleginfo - Inhalt 3", "fieldname": "Beleginfo - Inhalt 3", "fieldtype": "Dynamic Link", "options": "Beleginfo - Art 3", - "width": 150 + "width": 150, }, { "label": "Beleginfo - Art 4", "fieldname": "Beleginfo - Art 4", "fieldtype": "Data", - "width": 100 + "width": 100, }, { "label": "Beleginfo - Inhalt 4", "fieldname": "Beleginfo - Inhalt 4", "fieldtype": "Data", - "width": 150 + "width": 150, }, { "label": "Beleginfo - Art 5", "fieldname": "Beleginfo - Art 5", "fieldtype": "Data", - "width": 150 + "width": 150, }, { "label": "Beleginfo - Inhalt 5", "fieldname": "Beleginfo - Inhalt 5", "fieldtype": "Data", - "width": 100 + "width": 100, }, { "label": "Beleginfo - Art 6", "fieldname": "Beleginfo - Art 6", "fieldtype": "Data", - "width": 150 + "width": 150, }, { "label": "Beleginfo - Inhalt 6", "fieldname": "Beleginfo - Inhalt 6", "fieldtype": "Date", - "width": 100 + "width": 100, }, - { - "label": "Fälligkeit", - "fieldname": "Fälligkeit", - "fieldtype": "Date", - "width": 100 - } + {"label": "Fälligkeit", "fieldname": "Fälligkeit", "fieldtype": "Date", "width": 100}, ] @@ -160,8 +130,8 @@ def execute(filters=None): """Entry point for frappe.""" data = [] if filters and validate(filters): - fn = 'temporary_against_account_number' - filters[fn] = frappe.get_value('DATEV Settings', filters.get('company'), fn) + fn = "temporary_against_account_number" + filters[fn] = frappe.get_value("DATEV Settings", filters.get("company"), fn) data = get_transactions(filters, as_dict=0) return COLUMNS, data @@ -169,23 +139,23 @@ def execute(filters=None): def validate(filters): """Make sure all mandatory filters and settings are present.""" - company = filters.get('company') + company = filters.get("company") if not company: - frappe.throw(_('Company is a mandatory filter.')) + frappe.throw(_("Company is a mandatory filter.")) - from_date = filters.get('from_date') + from_date = filters.get("from_date") if not from_date: - frappe.throw(_('From Date is a mandatory filter.')) + frappe.throw(_("From Date is a mandatory filter.")) - to_date = filters.get('to_date') + to_date = filters.get("to_date") if not to_date: - frappe.throw(_('To Date is a mandatory filter.')) + frappe.throw(_("To Date is a mandatory filter.")) validate_fiscal_year(from_date, to_date, company) - if not frappe.db.exists('DATEV Settings', filters.get('company')): - msg = 'Please create DATEV Settings for Company {}'.format(filters.get('company')) - frappe.log_error(msg, title='DATEV Settings missing') + if not frappe.db.exists("DATEV Settings", filters.get("company")): + msg = "Please create DATEV Settings for Company {}".format(filters.get("company")) + frappe.log_error(msg, title="DATEV Settings missing") return False return True @@ -195,7 +165,7 @@ def validate_fiscal_year(from_date, to_date, company): from_fiscal_year = get_fiscal_year(date=from_date, company=company) to_fiscal_year = get_fiscal_year(date=to_date, company=company) if from_fiscal_year != to_fiscal_year: - frappe.throw(_('Dates {} and {} are not in the same fiscal year.').format(from_date, to_date)) + frappe.throw(_("Dates {} and {} are not in the same fiscal year.").format(from_date, to_date)) def get_transactions(filters, as_dict=1): @@ -211,7 +181,7 @@ def get_transactions(filters, as_dict=1): # specific query methods for some voucher types "Payment Entry": get_payment_entry_params, "Sales Invoice": get_sales_invoice_params, - "Purchase Invoice": get_purchase_invoice_params + "Purchase Invoice": get_purchase_invoice_params, } only_voucher_type = filters.get("voucher_type") @@ -307,7 +277,9 @@ def get_generic_params(filters): if filters.get("exclude_voucher_types"): # exclude voucher types that are queried by a dedicated method - exclude = "({})".format(', '.join("'{}'".format(key) for key in filters.get("exclude_voucher_types"))) + exclude = "({})".format( + ", ".join("'{}'".format(key) for key in filters.get("exclude_voucher_types")) + ) extra_filters = "AND gl.voucher_type NOT IN {}".format(exclude) # if voucher type filter is set, allow only this type @@ -379,10 +351,8 @@ def run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=1): {extra_filters} ORDER BY 'Belegdatum', gl.voucher_no""".format( - extra_fields=extra_fields, - extra_joins=extra_joins, - extra_filters=extra_filters - ) + extra_fields=extra_fields, extra_joins=extra_joins, extra_filters=extra_filters + ) gl_entries = frappe.db.sql(query, filters, as_dict=as_dict) @@ -396,7 +366,8 @@ def get_customers(filters): Arguments: filters -- dict of filters to be passed to the sql query """ - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT par.debtor_creditor_number as 'Konto', @@ -448,7 +419,10 @@ def get_customers(filters): on country.name = adr.country WHERE adr.is_primary_address = '1' - """, filters, as_dict=1) + """, + filters, + as_dict=1, + ) def get_suppliers(filters): @@ -458,7 +432,8 @@ def get_suppliers(filters): Arguments: filters -- dict of filters to be passed to the sql query """ - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT par.debtor_creditor_number as 'Konto', @@ -511,11 +486,15 @@ def get_suppliers(filters): on country.name = adr.country WHERE adr.is_primary_address = '1' - """, filters, as_dict=1) + """, + filters, + as_dict=1, + ) def get_account_names(filters): - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT account_number as 'Konto', @@ -526,7 +505,10 @@ def get_account_names(filters): WHERE company = %(company)s AND is_group = 0 AND account_number != '' - """, filters, as_dict=1) + """, + filters, + as_dict=1, + ) @frappe.whitelist() @@ -546,40 +528,43 @@ def download_datev_csv(filters): filters = json.loads(filters) validate(filters) - company = filters.get('company') + company = filters.get("company") - fiscal_year = get_fiscal_year(date=filters.get('from_date'), company=company) - filters['fiscal_year_start'] = fiscal_year[1] + fiscal_year = get_fiscal_year(date=filters.get("from_date"), company=company) + filters["fiscal_year_start"] = fiscal_year[1] # set chart of accounts used - coa = frappe.get_value('Company', company, 'chart_of_accounts') - filters['skr'] = '04' if 'SKR04' in coa else ('03' if 'SKR03' in coa else '') + coa = frappe.get_value("Company", company, "chart_of_accounts") + filters["skr"] = "04" if "SKR04" in coa else ("03" if "SKR03" in coa else "") - datev_settings = frappe.get_doc('DATEV Settings', company) - filters['account_number_length'] = datev_settings.account_number_length - filters['temporary_against_account_number'] = datev_settings.temporary_against_account_number + datev_settings = frappe.get_doc("DATEV Settings", company) + filters["account_number_length"] = datev_settings.account_number_length + filters["temporary_against_account_number"] = datev_settings.temporary_against_account_number transactions = get_transactions(filters) account_names = get_account_names(filters) customers = get_customers(filters) suppliers = get_suppliers(filters) - zip_name = '{} DATEV.zip'.format(frappe.utils.datetime.date.today()) - zip_and_download(zip_name, [ - { - 'file_name': 'EXTF_Buchungsstapel.csv', - 'csv_data': get_datev_csv(transactions, filters, csv_class=Transactions) - }, - { - 'file_name': 'EXTF_Kontenbeschriftungen.csv', - 'csv_data': get_datev_csv(account_names, filters, csv_class=AccountNames) - }, - { - 'file_name': 'EXTF_Kunden.csv', - 'csv_data': get_datev_csv(customers, filters, csv_class=DebtorsCreditors) - }, - { - 'file_name': 'EXTF_Lieferanten.csv', - 'csv_data': get_datev_csv(suppliers, filters, csv_class=DebtorsCreditors) - }, - ]) + zip_name = "{} DATEV.zip".format(frappe.utils.datetime.date.today()) + zip_and_download( + zip_name, + [ + { + "file_name": "EXTF_Buchungsstapel.csv", + "csv_data": get_datev_csv(transactions, filters, csv_class=Transactions), + }, + { + "file_name": "EXTF_Kontenbeschriftungen.csv", + "csv_data": get_datev_csv(account_names, filters, csv_class=AccountNames), + }, + { + "file_name": "EXTF_Kunden.csv", + "csv_data": get_datev_csv(customers, filters, csv_class=DebtorsCreditors), + }, + { + "file_name": "EXTF_Lieferanten.csv", + "csv_data": get_datev_csv(suppliers, filters, csv_class=DebtorsCreditors), + }, + ], + ) diff --git a/erpnext/regional/report/datev/test_datev.py b/erpnext/regional/report/datev/test_datev.py index 052fb2a724..0df8c0607d 100644 --- a/erpnext/regional/report/datev/test_datev.py +++ b/erpnext/regional/report/datev/test_datev.py @@ -23,15 +23,17 @@ from erpnext.regional.report.datev.datev import ( def make_company(company_name, abbr): if not frappe.db.exists("Company", company_name): - company = frappe.get_doc({ - "doctype": "Company", - "company_name": company_name, - "abbr": abbr, - "default_currency": "EUR", - "country": "Germany", - "create_chart_of_accounts_based_on": "Standard Template", - "chart_of_accounts": "SKR04 mit Kontonummern" - }) + company = frappe.get_doc( + { + "doctype": "Company", + "company_name": company_name, + "abbr": abbr, + "default_currency": "EUR", + "country": "Germany", + "create_chart_of_accounts_based_on": "Standard Template", + "chart_of_accounts": "SKR04 mit Kontonummern", + } + ) company.insert() else: company = frappe.get_doc("Company", company_name) @@ -45,17 +47,20 @@ def make_company(company_name, abbr): company.save() return company + def setup_fiscal_year(): fiscal_year = None year = cstr(now_datetime().year) if not frappe.db.get_value("Fiscal Year", {"year": year}, "name"): try: - fiscal_year = frappe.get_doc({ - "doctype": "Fiscal Year", - "year": year, - "year_start_date": "{0}-01-01".format(year), - "year_end_date": "{0}-12-31".format(year) - }) + fiscal_year = frappe.get_doc( + { + "doctype": "Fiscal Year", + "year": year, + "year_start_date": "{0}-01-01".format(year), + "year_end_date": "{0}-12-31".format(year), + } + ) fiscal_year.insert() except frappe.NameError: pass @@ -63,75 +68,78 @@ def setup_fiscal_year(): if fiscal_year: fiscal_year.set_as_default() + def make_customer_with_account(customer_name, company): - acc_name = frappe.db.get_value("Account", { - "account_name": customer_name, - "company": company.name - }, "name") + acc_name = frappe.db.get_value( + "Account", {"account_name": customer_name, "company": company.name}, "name" + ) if not acc_name: - acc = frappe.get_doc({ - "doctype": "Account", - "parent_account": "1 - Forderungen aus Lieferungen und Leistungen - _TG", - "account_name": customer_name, - "company": company.name, - "account_type": "Receivable", - "account_number": "10001" - }) + acc = frappe.get_doc( + { + "doctype": "Account", + "parent_account": "1 - Forderungen aus Lieferungen und Leistungen - _TG", + "account_name": customer_name, + "company": company.name, + "account_type": "Receivable", + "account_number": "10001", + } + ) acc.insert() acc_name = acc.name if not frappe.db.exists("Customer", customer_name): - customer = frappe.get_doc({ - "doctype": "Customer", - "customer_name": customer_name, - "customer_type": "Company", - "accounts": [{ - "company": company.name, - "account": acc_name - }] - }) + customer = frappe.get_doc( + { + "doctype": "Customer", + "customer_name": customer_name, + "customer_type": "Company", + "accounts": [{"company": company.name, "account": acc_name}], + } + ) customer.insert() else: customer = frappe.get_doc("Customer", customer_name) return customer + def make_item(item_code, company): - warehouse_name = frappe.db.get_value("Warehouse", { - "warehouse_name": "Stores", - "company": company.name - }, "name") + warehouse_name = frappe.db.get_value( + "Warehouse", {"warehouse_name": "Stores", "company": company.name}, "name" + ) if not frappe.db.exists("Item", item_code): - item = frappe.get_doc({ - "doctype": "Item", - "item_code": item_code, - "item_name": item_code, - "description": item_code, - "item_group": "All Item Groups", - "is_stock_item": 0, - "is_purchase_item": 0, - "is_customer_provided_item": 0, - "item_defaults": [{ - "default_warehouse": warehouse_name, - "company": company.name - }] - }) + item = frappe.get_doc( + { + "doctype": "Item", + "item_code": item_code, + "item_name": item_code, + "description": item_code, + "item_group": "All Item Groups", + "is_stock_item": 0, + "is_purchase_item": 0, + "is_customer_provided_item": 0, + "item_defaults": [{"default_warehouse": warehouse_name, "company": company.name}], + } + ) item.insert() else: item = frappe.get_doc("Item", item_code) return item + def make_datev_settings(company): if not frappe.db.exists("DATEV Settings", company.name): - frappe.get_doc({ - "doctype": "DATEV Settings", - "client": company.name, - "client_number": "12345", - "consultant_number": "67890", - "temporary_against_account_number": "9999" - }).insert() + frappe.get_doc( + { + "doctype": "DATEV Settings", + "client": company.name, + "client_number": "12345", + "consultant_number": "67890", + "temporary_against_account_number": "9999", + } + ).insert() class TestDatev(TestCase): @@ -142,27 +150,24 @@ class TestDatev(TestCase): "company": self.company.name, "from_date": today(), "to_date": today(), - "temporary_against_account_number": "9999" + "temporary_against_account_number": "9999", } make_datev_settings(self.company) item = make_item("_Test Item", self.company) setup_fiscal_year() - warehouse = frappe.db.get_value("Item Default", { - "parent": item.name, - "company": self.company.name - }, "default_warehouse") + warehouse = frappe.db.get_value( + "Item Default", {"parent": item.name, "company": self.company.name}, "default_warehouse" + ) - income_account = frappe.db.get_value("Account", { - "account_number": "4200", - "company": self.company.name - }, "name") + income_account = frappe.db.get_value( + "Account", {"account_number": "4200", "company": self.company.name}, "name" + ) - tax_account = frappe.db.get_value("Account", { - "account_number": "3806", - "company": self.company.name - }, "name") + tax_account = frappe.db.get_value( + "Account", {"account_number": "3806", "company": self.company.name}, "name" + ) si = create_sales_invoice( company=self.company.name, @@ -174,16 +179,19 @@ class TestDatev(TestCase): cost_center=self.company.cost_center, warehouse=warehouse, item=item.name, - do_not_save=1 + do_not_save=1, ) - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": tax_account, - "description": "Umsatzsteuer 19 %", - "rate": 19, - "cost_center": self.company.cost_center - }) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": tax_account, + "description": "Umsatzsteuer 19 %", + "rate": 19, + "cost_center": self.company.cost_center, + }, + ) si.cost_center = self.company.cost_center @@ -219,16 +227,18 @@ class TestDatev(TestCase): self.assertTrue(DebtorsCreditors.DATA_CATEGORY in get_header(self.filters, DebtorsCreditors)) def test_csv(self): - test_data = [{ - "Umsatz (ohne Soll/Haben-Kz)": 100, - "Soll/Haben-Kennzeichen": "H", - "Kontonummer": "4200", - "Gegenkonto (ohne BU-Schlüssel)": "10000", - "Belegdatum": today(), - "Buchungstext": "No remark", - "Beleginfo - Art 1": "Sales Invoice", - "Beleginfo - Inhalt 1": "SINV-0001" - }] + test_data = [ + { + "Umsatz (ohne Soll/Haben-Kz)": 100, + "Soll/Haben-Kennzeichen": "H", + "Kontonummer": "4200", + "Gegenkonto (ohne BU-Schlüssel)": "10000", + "Belegdatum": today(), + "Buchungstext": "No remark", + "Beleginfo - Art 1": "Sales Invoice", + "Beleginfo - Inhalt 1": "SINV-0001", + } + ] get_datev_csv(data=test_data, filters=self.filters, csv_class=Transactions) def test_download(self): @@ -237,6 +247,6 @@ class TestDatev(TestCase): # zipfile.is_zipfile() expects a file-like object zip_buffer = BytesIO() - zip_buffer.write(frappe.response['filecontent']) + zip_buffer.write(frappe.response["filecontent"]) self.assertTrue(zipfile.is_zipfile(zip_buffer)) diff --git a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py index 2110c44447..255bb92a1e 100644 --- a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py +++ b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py @@ -13,98 +13,70 @@ def execute(filters=None): return columns, data + def validate_filters(filters=None): if filters is None: filters = {} filters = frappe._dict(filters) if not filters.company: - frappe.throw(_('{} is mandatory for generating E-Invoice Summary Report').format(_('Company')), title=_('Invalid Filter')) + frappe.throw( + _("{} is mandatory for generating E-Invoice Summary Report").format(_("Company")), + title=_("Invalid Filter"), + ) if filters.company: # validate if company has e-invoicing enabled pass if not filters.from_date or not filters.to_date: - frappe.throw(_('From Date & To Date is mandatory for generating E-Invoice Summary Report'), title=_('Invalid Filter')) + frappe.throw( + _("From Date & To Date is mandatory for generating E-Invoice Summary Report"), + title=_("Invalid Filter"), + ) if filters.from_date > filters.to_date: - frappe.throw(_('From Date must be before To Date'), title=_('Invalid Filter')) + frappe.throw(_("From Date must be before To Date"), title=_("Invalid Filter")) + def get_data(filters=None): if filters is None: filters = {} query_filters = { - 'posting_date': ['between', [filters.from_date, filters.to_date]], - 'einvoice_status': ['is', 'set'], - 'company': filters.company + "posting_date": ["between", [filters.from_date, filters.to_date]], + "einvoice_status": ["is", "set"], + "company": filters.company, } if filters.customer: - query_filters['customer'] = filters.customer + query_filters["customer"] = filters.customer if filters.status: - query_filters['einvoice_status'] = filters.status + query_filters["einvoice_status"] = filters.status data = frappe.get_all( - 'Sales Invoice', - filters=query_filters, - fields=[d.get('fieldname') for d in get_columns()] + "Sales Invoice", filters=query_filters, fields=[d.get("fieldname") for d in get_columns()] ) return data + def get_columns(): return [ - { - "fieldtype": "Date", - "fieldname": "posting_date", - "label": _("Posting Date"), - "width": 0 - }, + {"fieldtype": "Date", "fieldname": "posting_date", "label": _("Posting Date"), "width": 0}, { "fieldtype": "Link", "fieldname": "name", "label": _("Sales Invoice"), "options": "Sales Invoice", - "width": 140 - }, - { - "fieldtype": "Data", - "fieldname": "einvoice_status", - "label": _("Status"), - "width": 100 - }, - { - "fieldtype": "Link", - "fieldname": "customer", - "options": "Customer", - "label": _("Customer") - }, - { - "fieldtype": "Check", - "fieldname": "is_return", - "label": _("Is Return"), - "width": 85 - }, - { - "fieldtype": "Data", - "fieldname": "ack_no", - "label": "Ack. No.", - "width": 145 - }, - { - "fieldtype": "Data", - "fieldname": "ack_date", - "label": "Ack. Date", - "width": 165 - }, - { - "fieldtype": "Data", - "fieldname": "irn", - "label": _("IRN No."), - "width": 250 + "width": 140, }, + {"fieldtype": "Data", "fieldname": "einvoice_status", "label": _("Status"), "width": 100}, + {"fieldtype": "Link", "fieldname": "customer", "options": "Customer", "label": _("Customer")}, + {"fieldtype": "Check", "fieldname": "is_return", "label": _("Is Return"), "width": 85}, + {"fieldtype": "Data", "fieldname": "ack_no", "label": "Ack. No.", "width": 145}, + {"fieldtype": "Data", "fieldname": "ack_date", "label": "Ack. Date", "width": 165}, + {"fieldtype": "Data", "fieldname": "irn", "label": _("IRN No."), "width": 250}, { "fieldtype": "Currency", "options": "Company:company:default_currency", "fieldname": "base_grand_total", "label": _("Grand Total"), - "width": 120 - } + "width": 120, + }, ] diff --git a/erpnext/regional/report/eway_bill/eway_bill.py b/erpnext/regional/report/eway_bill/eway_bill.py index f3fe5e8848..8dcd6a365d 100644 --- a/erpnext/regional/report/eway_bill/eway_bill.py +++ b/erpnext/regional/report/eway_bill/eway_bill.py @@ -11,35 +11,41 @@ from frappe.utils import nowdate def execute(filters=None): - if not filters: filters.setdefault('posting_date', [nowdate(), nowdate()]) + if not filters: + filters.setdefault("posting_date", [nowdate(), nowdate()]) columns, data = [], [] columns = get_columns() data = get_data(filters) return columns, data + def get_data(filters): conditions = get_conditions(filters) - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT dn.name as dn_id, dn.posting_date, dn.company, dn.company_gstin, dn.customer, dn.customer_gstin, dni.item_code, dni.item_name, dni.description, dni.gst_hsn_code, dni.uom, dni.qty, dni.amount, dn.mode_of_transport, dn.distance, dn.transporter_name, dn.gst_transporter_id, dn.lr_no, dn.lr_date, dn.vehicle_no, dn.gst_vehicle_type, dn.company_address, dn.shipping_address_name FROM `tabDelivery Note` AS dn join `tabDelivery Note Item` AS dni on (dni.parent = dn.name) WHERE dn.docstatus < 2 - %s """ % conditions, as_dict=1) + %s """ + % conditions, + as_dict=1, + ) unit = { - 'Bag': "BAGS", - 'Bottle': "BOTTLES", - 'Kg': "KILOGRAMS", - 'Liter': "LITERS", - 'Meter': "METERS", - 'Nos': "NUMBERS", - 'PKT': "PACKS", - 'Roll': "ROLLS", - 'Set': "SETS" + "Bag": "BAGS", + "Bottle": "BOTTLES", + "Kg": "KILOGRAMS", + "Liter": "LITERS", + "Meter": "METERS", + "Nos": "NUMBERS", + "PKT": "PACKS", + "Roll": "ROLLS", + "Set": "SETS", } # Regular expression set to remove all the special characters @@ -51,11 +57,11 @@ def get_data(filters): set_address_details(row, special_characters) # Eway Bill accepts date as dd/mm/yyyy and not dd-mm-yyyy - row.posting_date = '/'.join(str(row.posting_date).replace("-", "/").split('/')[::-1]) - row.lr_date = '/'.join(str(row.lr_date).replace("-", "/").split('/')[::-1]) + row.posting_date = "/".join(str(row.posting_date).replace("-", "/").split("/")[::-1]) + row.lr_date = "/".join(str(row.lr_date).replace("-", "/").split("/")[::-1]) - if row.gst_vehicle_type == 'Over Dimensional Cargo (ODC)': - row.gst_vehicle_type = 'ODC' + if row.gst_vehicle_type == "Over Dimensional Cargo (ODC)": + row.gst_vehicle_type = "ODC" row.item_name = re.sub(special_characters, " ", row.item_name) row.description = row.item_name @@ -67,58 +73,80 @@ def get_data(filters): return data + def get_conditions(filters): conditions = "" - conditions += filters.get('company') and " AND dn.company = '%s' " % filters.get('company') or "" - conditions += filters.get('posting_date') and " AND dn.posting_date >= '%s' AND dn.posting_date <= '%s' " % (filters.get('posting_date')[0], filters.get('posting_date')[1]) or "" - conditions += filters.get('delivery_note') and " AND dn.name = '%s' " % filters.get('delivery_note') or "" - conditions += filters.get('customer') and " AND dn.customer = '%s' " % filters.get('customer').replace("'", "\'") or "" + conditions += filters.get("company") and " AND dn.company = '%s' " % filters.get("company") or "" + conditions += ( + filters.get("posting_date") + and " AND dn.posting_date >= '%s' AND dn.posting_date <= '%s' " + % (filters.get("posting_date")[0], filters.get("posting_date")[1]) + or "" + ) + conditions += ( + filters.get("delivery_note") and " AND dn.name = '%s' " % filters.get("delivery_note") or "" + ) + conditions += ( + filters.get("customer") + and " AND dn.customer = '%s' " % filters.get("customer").replace("'", "'") + or "" + ) return conditions + def set_defaults(row): - row.setdefault(u'supply_type', "Outward") - row.setdefault(u'sub_type', "Supply") - row.setdefault(u'doc_type', "Delivery Challan") + row.setdefault("supply_type", "Outward") + row.setdefault("sub_type", "Supply") + row.setdefault("doc_type", "Delivery Challan") + def set_address_details(row, special_characters): - if row.get('company_address'): - address_line1, address_line2, city, pincode, state = frappe.db.get_value("Address", row.get('company_address'), ['address_line1', 'address_line2', 'city', 'pincode', 'state']) + if row.get("company_address"): + address_line1, address_line2, city, pincode, state = frappe.db.get_value( + "Address", + row.get("company_address"), + ["address_line1", "address_line2", "city", "pincode", "state"], + ) - row.update({'from_address_1': re.sub(special_characters, "", address_line1 or '')}) - row.update({'from_address_2': re.sub(special_characters, "", address_line2 or '')}) - row.update({'from_place': city and city.upper() or ''}) - row.update({'from_pin_code': pincode and pincode.replace(" ", "") or ''}) - row.update({'from_state': state and state.upper() or ''}) - row.update({'dispatch_state': row.from_state}) + row.update({"from_address_1": re.sub(special_characters, "", address_line1 or "")}) + row.update({"from_address_2": re.sub(special_characters, "", address_line2 or "")}) + row.update({"from_place": city and city.upper() or ""}) + row.update({"from_pin_code": pincode and pincode.replace(" ", "") or ""}) + row.update({"from_state": state and state.upper() or ""}) + row.update({"dispatch_state": row.from_state}) - if row.get('shipping_address_name'): - address_line1, address_line2, city, pincode, state = frappe.db.get_value("Address", row.get('shipping_address_name'), ['address_line1', 'address_line2', 'city', 'pincode', 'state']) + if row.get("shipping_address_name"): + address_line1, address_line2, city, pincode, state = frappe.db.get_value( + "Address", + row.get("shipping_address_name"), + ["address_line1", "address_line2", "city", "pincode", "state"], + ) + + row.update({"to_address_1": re.sub(special_characters, "", address_line1 or "")}) + row.update({"to_address_2": re.sub(special_characters, "", address_line2 or "")}) + row.update({"to_place": city and city.upper() or ""}) + row.update({"to_pin_code": pincode and pincode.replace(" ", "") or ""}) + row.update({"to_state": state and state.upper() or ""}) + row.update({"ship_to_state": row.to_state}) - row.update({'to_address_1': re.sub(special_characters, "", address_line1 or '')}) - row.update({'to_address_2': re.sub(special_characters, "", address_line2 or '')}) - row.update({'to_place': city and city.upper() or ''}) - row.update({'to_pin_code': pincode and pincode.replace(" ", "") or ''}) - row.update({'to_state': state and state.upper() or ''}) - row.update({'ship_to_state': row.to_state}) def set_taxes(row, filters): - taxes = frappe.get_all("Sales Taxes and Charges", - filters={ - 'parent': row.dn_id - }, - fields=('item_wise_tax_detail', 'account_head')) + taxes = frappe.get_all( + "Sales Taxes and Charges", + filters={"parent": row.dn_id}, + fields=("item_wise_tax_detail", "account_head"), + ) account_list = ["cgst_account", "sgst_account", "igst_account", "cess_account"] - taxes_list = frappe.get_all("GST Account", - filters={ - "parent": "GST Settings", - "company": filters.company - }, - fields=account_list) + taxes_list = frappe.get_all( + "GST Account", + filters={"parent": "GST Settings", "company": filters.company}, + fields=account_list, + ) if not taxes_list: frappe.throw(_("Please set GST Accounts in GST Settings")) @@ -141,253 +169,89 @@ def set_taxes(row, filters): item_tax_rate.pop(tax[key]) row.amount = float(row.amount) + sum(i[1] for i in item_tax_rate.values()) - row.update({'tax_rate': '+'.join(tax_rate)}) + row.update({"tax_rate": "+".join(tax_rate)}) + def get_columns(): columns = [ - { - "fieldname": "supply_type", - "label": _("Supply Type"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "sub_type", - "label": _("Sub Type"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "doc_type", - "label": _("Doc Type"), - "fieldtype": "Data", - "width": 100 - }, + {"fieldname": "supply_type", "label": _("Supply Type"), "fieldtype": "Data", "width": 100}, + {"fieldname": "sub_type", "label": _("Sub Type"), "fieldtype": "Data", "width": 100}, + {"fieldname": "doc_type", "label": _("Doc Type"), "fieldtype": "Data", "width": 100}, { "fieldname": "dn_id", "label": _("Doc Name"), "fieldtype": "Link", "options": "Delivery Note", - "width": 140 - }, - { - "fieldname": "posting_date", - "label": _("Doc Date"), - "fieldtype": "Data", - "width": 100 + "width": 140, }, + {"fieldname": "posting_date", "label": _("Doc Date"), "fieldtype": "Data", "width": 100}, { "fieldname": "company", "label": _("From Party Name"), "fieldtype": "Link", "options": "Company", - "width": 120 - }, - { - "fieldname": "company_gstin", - "label": _("From GSTIN"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "from_address_1", - "label": _("From Address 1"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "from_address_2", - "label": _("From Address 2"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "from_place", - "label": _("From Place"), - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "from_pin_code", - "label": _("From Pin Code"), - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "from_state", - "label": _("From State"), - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "dispatch_state", - "label": _("Dispatch State"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "customer", - "label": _("To Party Name"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "customer_gstin", - "label": _("To GSTIN"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "to_address_1", - "label": _("To Address 1"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "to_address_2", - "label": _("To Address 2"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "to_place", - "label": _("To Place"), - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "to_pin_code", - "label": _("To Pin Code"), - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "to_state", - "label": _("To State"), - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "ship_to_state", - "label": _("Ship To State"), - "fieldtype": "Data", - "width": 100 + "width": 120, }, + {"fieldname": "company_gstin", "label": _("From GSTIN"), "fieldtype": "Data", "width": 100}, + {"fieldname": "from_address_1", "label": _("From Address 1"), "fieldtype": "Data", "width": 120}, + {"fieldname": "from_address_2", "label": _("From Address 2"), "fieldtype": "Data", "width": 120}, + {"fieldname": "from_place", "label": _("From Place"), "fieldtype": "Data", "width": 80}, + {"fieldname": "from_pin_code", "label": _("From Pin Code"), "fieldtype": "Data", "width": 80}, + {"fieldname": "from_state", "label": _("From State"), "fieldtype": "Data", "width": 80}, + {"fieldname": "dispatch_state", "label": _("Dispatch State"), "fieldtype": "Data", "width": 100}, + {"fieldname": "customer", "label": _("To Party Name"), "fieldtype": "Data", "width": 120}, + {"fieldname": "customer_gstin", "label": _("To GSTIN"), "fieldtype": "Data", "width": 120}, + {"fieldname": "to_address_1", "label": _("To Address 1"), "fieldtype": "Data", "width": 120}, + {"fieldname": "to_address_2", "label": _("To Address 2"), "fieldtype": "Data", "width": 120}, + {"fieldname": "to_place", "label": _("To Place"), "fieldtype": "Data", "width": 80}, + {"fieldname": "to_pin_code", "label": _("To Pin Code"), "fieldtype": "Data", "width": 80}, + {"fieldname": "to_state", "label": _("To State"), "fieldtype": "Data", "width": 80}, + {"fieldname": "ship_to_state", "label": _("Ship To State"), "fieldtype": "Data", "width": 100}, { "fieldname": "item_name", "label": _("Product"), "fieldtype": "Link", "options": "Item", - "width": 120 - }, - { - "fieldname": "description", - "label": _("Description"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "gst_hsn_code", - "label": _("HSN"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "uom", - "label": _("Unit"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "qty", - "label": _("Qty"), - "fieldtype": "Float", - "width": 100 - }, - { - "fieldname": "amount", - "label": _("Accessable Value"), - "fieldtype": "Float", - "width": 120 - }, - { - "fieldname": "tax_rate", - "label": _("Tax Rate"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "cgst_amount", - "label": _("CGST Amount"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "sgst_amount", - "label": _("SGST Amount"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "igst_amount", - "label": _("IGST Amount"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "cess_amount", - "label": _("CESS Amount"), - "fieldtype": "Data", - "width": 100 + "width": 120, }, + {"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 100}, + {"fieldname": "gst_hsn_code", "label": _("HSN"), "fieldtype": "Data", "width": 120}, + {"fieldname": "uom", "label": _("Unit"), "fieldtype": "Data", "width": 100}, + {"fieldname": "qty", "label": _("Qty"), "fieldtype": "Float", "width": 100}, + {"fieldname": "amount", "label": _("Accessable Value"), "fieldtype": "Float", "width": 120}, + {"fieldname": "tax_rate", "label": _("Tax Rate"), "fieldtype": "Data", "width": 100}, + {"fieldname": "cgst_amount", "label": _("CGST Amount"), "fieldtype": "Data", "width": 100}, + {"fieldname": "sgst_amount", "label": _("SGST Amount"), "fieldtype": "Data", "width": 100}, + {"fieldname": "igst_amount", "label": _("IGST Amount"), "fieldtype": "Data", "width": 100}, + {"fieldname": "cess_amount", "label": _("CESS Amount"), "fieldtype": "Data", "width": 100}, { "fieldname": "mode_of_transport", "label": _("Mode of Transport"), "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "distance", - "label": _("Distance"), - "fieldtype": "Data", - "width": 100 + "width": 100, }, + {"fieldname": "distance", "label": _("Distance"), "fieldtype": "Data", "width": 100}, { "fieldname": "transporter_name", "label": _("Transporter Name"), "fieldtype": "Data", - "width": 120 + "width": 120, }, { "fieldname": "gst_transporter_id", "label": _("Transporter ID"), "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "lr_no", - "label": _("Transport Receipt No"), - "fieldtype": "Data", - "width": 120 + "width": 100, }, + {"fieldname": "lr_no", "label": _("Transport Receipt No"), "fieldtype": "Data", "width": 120}, { "fieldname": "lr_date", "label": _("Transport Receipt Date"), "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "vehicle_no", - "label": _("Vehicle No"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "gst_vehicle_type", - "label": _("Vehicle Type"), - "fieldtype": "Data", - "width": 100 + "width": 120, }, + {"fieldname": "vehicle_no", "label": _("Vehicle No"), "fieldtype": "Data", "width": 100}, + {"fieldname": "gst_vehicle_type", "label": _("Vehicle Type"), "fieldtype": "Data", "width": 100}, ] return columns diff --git a/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py b/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py index 59888ff94e..c75179ee5d 100644 --- a/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py +++ b/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py @@ -26,31 +26,42 @@ def execute(filters=None): def validate_filters(filters, account_details): - if not filters.get('company'): - frappe.throw(_('{0} is mandatory').format(_('Company'))) + if not filters.get("company"): + frappe.throw(_("{0} is mandatory").format(_("Company"))) - if not filters.get('fiscal_year'): - frappe.throw(_('{0} is mandatory').format(_('Fiscal Year'))) + if not filters.get("fiscal_year"): + frappe.throw(_("{0} is mandatory").format(_("Fiscal Year"))) def set_account_currency(filters): - filters["company_currency"] = frappe.get_cached_value('Company', filters.company, "default_currency") + filters["company_currency"] = frappe.get_cached_value( + "Company", filters.company, "default_currency" + ) return filters def get_columns(filters): columns = [ - "JournalCode" + "::90", "JournalLib" + "::90", - "EcritureNum" + ":Dynamic Link:90", "EcritureDate" + "::90", - "CompteNum" + ":Link/Account:100", "CompteLib" + ":Link/Account:200", - "CompAuxNum" + "::90", "CompAuxLib" + "::90", - "PieceRef" + "::90", "PieceDate" + "::90", - "EcritureLib" + "::90", "Debit" + "::90", "Credit" + "::90", - "EcritureLet" + "::90", "DateLet" + - "::90", "ValidDate" + "::90", - "Montantdevise" + "::90", "Idevise" + "::90" + "JournalCode" + "::90", + "JournalLib" + "::90", + "EcritureNum" + ":Dynamic Link:90", + "EcritureDate" + "::90", + "CompteNum" + ":Link/Account:100", + "CompteLib" + ":Link/Account:200", + "CompAuxNum" + "::90", + "CompAuxLib" + "::90", + "PieceRef" + "::90", + "PieceDate" + "::90", + "EcritureLib" + "::90", + "Debit" + "::90", + "Credit" + "::90", + "EcritureLet" + "::90", + "DateLet" + "::90", + "ValidDate" + "::90", + "Montantdevise" + "::90", + "Idevise" + "::90", ] return columns @@ -66,10 +77,14 @@ def get_result(filters): def get_gl_entries(filters): - group_by_condition = "group by voucher_type, voucher_no, account" \ - if filters.get("group_by_voucher") else "group by gl.name" + group_by_condition = ( + "group by voucher_type, voucher_no, account" + if filters.get("group_by_voucher") + else "group by gl.name" + ) - gl_entries = frappe.db.sql(""" + gl_entries = frappe.db.sql( + """ select gl.posting_date as GlPostDate, gl.name as GlName, gl.account, gl.transaction_date, sum(gl.debit) as debit, sum(gl.credit) as credit, @@ -99,8 +114,12 @@ def get_gl_entries(filters): left join `tabMember` mem on gl.party = mem.name where gl.company=%(company)s and gl.fiscal_year=%(fiscal_year)s {group_by_condition} - order by GlPostDate, voucher_no"""\ - .format(group_by_condition=group_by_condition), filters, as_dict=1) + order by GlPostDate, voucher_no""".format( + group_by_condition=group_by_condition + ), + filters, + as_dict=1, + ) return gl_entries @@ -108,25 +127,37 @@ def get_gl_entries(filters): def get_result_as_list(data, filters): result = [] - company_currency = frappe.get_cached_value('Company', filters.company, "default_currency") - accounts = frappe.get_all("Account", filters={"Company": filters.company}, fields=["name", "account_number"]) + company_currency = frappe.get_cached_value("Company", filters.company, "default_currency") + accounts = frappe.get_all( + "Account", filters={"Company": filters.company}, fields=["name", "account_number"] + ) for d in data: JournalCode = re.split("-|/|[0-9]", d.get("voucher_no"))[0] - if d.get("voucher_no").startswith("{0}-".format(JournalCode)) or d.get("voucher_no").startswith("{0}/".format(JournalCode)): + if d.get("voucher_no").startswith("{0}-".format(JournalCode)) or d.get("voucher_no").startswith( + "{0}/".format(JournalCode) + ): EcritureNum = re.split("-|/", d.get("voucher_no"))[1] else: - EcritureNum = re.search(r"{0}(\d+)".format(JournalCode), d.get("voucher_no"), re.IGNORECASE).group(1) + EcritureNum = re.search( + r"{0}(\d+)".format(JournalCode), d.get("voucher_no"), re.IGNORECASE + ).group(1) EcritureDate = format_datetime(d.get("GlPostDate"), "yyyyMMdd") - account_number = [account.account_number for account in accounts if account.name == d.get("account")] + account_number = [ + account.account_number for account in accounts if account.name == d.get("account") + ] if account_number[0] is not None: - CompteNum = account_number[0] + CompteNum = account_number[0] else: - frappe.throw(_("Account number for account {0} is not available.
    Please setup your Chart of Accounts correctly.").format(d.get("account"))) + frappe.throw( + _( + "Account number for account {0} is not available.
    Please setup your Chart of Accounts correctly." + ).format(d.get("account")) + ) if d.get("party_type") == "Customer": CompAuxNum = d.get("cusName") @@ -172,19 +203,45 @@ def get_result_as_list(data, filters): PieceDate = format_datetime(d.get("GlPostDate"), "yyyyMMdd") - debit = '{:.2f}'.format(d.get("debit")).replace(".", ",") + debit = "{:.2f}".format(d.get("debit")).replace(".", ",") - credit = '{:.2f}'.format(d.get("credit")).replace(".", ",") + credit = "{:.2f}".format(d.get("credit")).replace(".", ",") Idevise = d.get("account_currency") if Idevise != company_currency: - Montantdevise = '{:.2f}'.format(d.get("debitCurr")).replace(".", ",") if d.get("debitCurr") != 0 else '{:.2f}'.format(d.get("creditCurr")).replace(".", ",") + Montantdevise = ( + "{:.2f}".format(d.get("debitCurr")).replace(".", ",") + if d.get("debitCurr") != 0 + else "{:.2f}".format(d.get("creditCurr")).replace(".", ",") + ) else: - Montantdevise = '{:.2f}'.format(d.get("debit")).replace(".", ",") if d.get("debit") != 0 else '{:.2f}'.format(d.get("credit")).replace(".", ",") + Montantdevise = ( + "{:.2f}".format(d.get("debit")).replace(".", ",") + if d.get("debit") != 0 + else "{:.2f}".format(d.get("credit")).replace(".", ",") + ) - row = [JournalCode, d.get("voucher_type"), EcritureNum, EcritureDate, CompteNum, d.get("account"), CompAuxNum, CompAuxLib, - PieceRef, PieceDate, EcritureLib, debit, credit, "", "", ValidDate, Montantdevise, Idevise] + row = [ + JournalCode, + d.get("voucher_type"), + EcritureNum, + EcritureDate, + CompteNum, + d.get("account"), + CompAuxNum, + CompAuxLib, + PieceRef, + PieceDate, + EcritureLib, + debit, + credit, + "", + "", + ValidDate, + Montantdevise, + Idevise, + ] result.append(row) diff --git a/erpnext/regional/report/gst_itemised_purchase_register/gst_itemised_purchase_register.py b/erpnext/regional/report/gst_itemised_purchase_register/gst_itemised_purchase_register.py index 528868cf17..fec63f2f18 100644 --- a/erpnext/regional/report/gst_itemised_purchase_register/gst_itemised_purchase_register.py +++ b/erpnext/regional/report/gst_itemised_purchase_register/gst_itemised_purchase_register.py @@ -8,24 +8,28 @@ from erpnext.accounts.report.item_wise_purchase_register.item_wise_purchase_regi def execute(filters=None): - return _execute(filters, additional_table_columns=[ - dict(fieldtype='Data', label='Supplier GSTIN', fieldname="supplier_gstin", width=120), - dict(fieldtype='Data', label='Company GSTIN', fieldname="company_gstin", width=120), - dict(fieldtype='Data', label='Reverse Charge', fieldname="reverse_charge", width=120), - dict(fieldtype='Data', label='GST Category', fieldname="gst_category", width=120), - dict(fieldtype='Data', label='Export Type', fieldname="export_type", width=120), - dict(fieldtype='Data', label='E-Commerce GSTIN', fieldname="ecommerce_gstin", width=130), - dict(fieldtype='Data', label='HSN Code', fieldname="gst_hsn_code", width=120), - dict(fieldtype='Data', label='Supplier Invoice No', fieldname="bill_no", width=120), - dict(fieldtype='Date', label='Supplier Invoice Date', fieldname="bill_date", width=100) - ], additional_query_columns=[ - 'supplier_gstin', - 'company_gstin', - 'reverse_charge', - 'gst_category', - 'export_type', - 'ecommerce_gstin', - 'gst_hsn_code', - 'bill_no', - 'bill_date' - ]) + return _execute( + filters, + additional_table_columns=[ + dict(fieldtype="Data", label="Supplier GSTIN", fieldname="supplier_gstin", width=120), + dict(fieldtype="Data", label="Company GSTIN", fieldname="company_gstin", width=120), + dict(fieldtype="Data", label="Reverse Charge", fieldname="reverse_charge", width=120), + dict(fieldtype="Data", label="GST Category", fieldname="gst_category", width=120), + dict(fieldtype="Data", label="Export Type", fieldname="export_type", width=120), + dict(fieldtype="Data", label="E-Commerce GSTIN", fieldname="ecommerce_gstin", width=130), + dict(fieldtype="Data", label="HSN Code", fieldname="gst_hsn_code", width=120), + dict(fieldtype="Data", label="Supplier Invoice No", fieldname="bill_no", width=120), + dict(fieldtype="Date", label="Supplier Invoice Date", fieldname="bill_date", width=100), + ], + additional_query_columns=[ + "supplier_gstin", + "company_gstin", + "reverse_charge", + "gst_category", + "export_type", + "ecommerce_gstin", + "gst_hsn_code", + "bill_no", + "bill_date", + ], + ) diff --git a/erpnext/regional/report/gst_itemised_sales_register/gst_itemised_sales_register.py b/erpnext/regional/report/gst_itemised_sales_register/gst_itemised_sales_register.py index 386e219756..bb1843f1bd 100644 --- a/erpnext/regional/report/gst_itemised_sales_register/gst_itemised_sales_register.py +++ b/erpnext/regional/report/gst_itemised_sales_register/gst_itemised_sales_register.py @@ -6,24 +6,30 @@ from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register i def execute(filters=None): - return _execute(filters, additional_table_columns=[ - dict(fieldtype='Data', label='Customer GSTIN', fieldname="customer_gstin", width=120), - dict(fieldtype='Data', label='Billing Address GSTIN', fieldname="billing_address_gstin", width=140), - dict(fieldtype='Data', label='Company GSTIN', fieldname="company_gstin", width=120), - dict(fieldtype='Data', label='Place of Supply', fieldname="place_of_supply", width=120), - dict(fieldtype='Data', label='Reverse Charge', fieldname="reverse_charge", width=120), - dict(fieldtype='Data', label='GST Category', fieldname="gst_category", width=120), - dict(fieldtype='Data', label='Export Type', fieldname="export_type", width=120), - dict(fieldtype='Data', label='E-Commerce GSTIN', fieldname="ecommerce_gstin", width=130), - dict(fieldtype='Data', label='HSN Code', fieldname="gst_hsn_code", width=120) - ], additional_query_columns=[ - 'customer_gstin', - 'billing_address_gstin', - 'company_gstin', - 'place_of_supply', - 'reverse_charge', - 'gst_category', - 'export_type', - 'ecommerce_gstin', - 'gst_hsn_code' - ]) + return _execute( + filters, + additional_table_columns=[ + dict(fieldtype="Data", label="Customer GSTIN", fieldname="customer_gstin", width=120), + dict( + fieldtype="Data", label="Billing Address GSTIN", fieldname="billing_address_gstin", width=140 + ), + dict(fieldtype="Data", label="Company GSTIN", fieldname="company_gstin", width=120), + dict(fieldtype="Data", label="Place of Supply", fieldname="place_of_supply", width=120), + dict(fieldtype="Data", label="Reverse Charge", fieldname="reverse_charge", width=120), + dict(fieldtype="Data", label="GST Category", fieldname="gst_category", width=120), + dict(fieldtype="Data", label="Export Type", fieldname="export_type", width=120), + dict(fieldtype="Data", label="E-Commerce GSTIN", fieldname="ecommerce_gstin", width=130), + dict(fieldtype="Data", label="HSN Code", fieldname="gst_hsn_code", width=120), + ], + additional_query_columns=[ + "customer_gstin", + "billing_address_gstin", + "company_gstin", + "place_of_supply", + "reverse_charge", + "gst_category", + "export_type", + "ecommerce_gstin", + "gst_hsn_code", + ], + ) diff --git a/erpnext/regional/report/gst_purchase_register/gst_purchase_register.py b/erpnext/regional/report/gst_purchase_register/gst_purchase_register.py index 2d994082c3..609dbbaf73 100644 --- a/erpnext/regional/report/gst_purchase_register/gst_purchase_register.py +++ b/erpnext/regional/report/gst_purchase_register/gst_purchase_register.py @@ -6,18 +6,22 @@ from erpnext.accounts.report.purchase_register.purchase_register import _execute def execute(filters=None): - return _execute(filters, additional_table_columns=[ - dict(fieldtype='Data', label='Supplier GSTIN', fieldname="supplier_gstin", width=120), - dict(fieldtype='Data', label='Company GSTIN', fieldname="company_gstin", width=120), - dict(fieldtype='Data', label='Reverse Charge', fieldname="reverse_charge", width=120), - dict(fieldtype='Data', label='GST Category', fieldname="gst_category", width=120), - dict(fieldtype='Data', label='Export Type', fieldname="export_type", width=120), - dict(fieldtype='Data', label='E-Commerce GSTIN', fieldname="ecommerce_gstin", width=130) - ], additional_query_columns=[ - 'supplier_gstin', - 'company_gstin', - 'reverse_charge', - 'gst_category', - 'export_type', - 'ecommerce_gstin' - ]) + return _execute( + filters, + additional_table_columns=[ + dict(fieldtype="Data", label="Supplier GSTIN", fieldname="supplier_gstin", width=120), + dict(fieldtype="Data", label="Company GSTIN", fieldname="company_gstin", width=120), + dict(fieldtype="Data", label="Reverse Charge", fieldname="reverse_charge", width=120), + dict(fieldtype="Data", label="GST Category", fieldname="gst_category", width=120), + dict(fieldtype="Data", label="Export Type", fieldname="export_type", width=120), + dict(fieldtype="Data", label="E-Commerce GSTIN", fieldname="ecommerce_gstin", width=130), + ], + additional_query_columns=[ + "supplier_gstin", + "company_gstin", + "reverse_charge", + "gst_category", + "export_type", + "ecommerce_gstin", + ], + ) diff --git a/erpnext/regional/report/gst_sales_register/gst_sales_register.py b/erpnext/regional/report/gst_sales_register/gst_sales_register.py index a6f2b3dbf4..94ceb197b1 100644 --- a/erpnext/regional/report/gst_sales_register/gst_sales_register.py +++ b/erpnext/regional/report/gst_sales_register/gst_sales_register.py @@ -6,22 +6,28 @@ from erpnext.accounts.report.sales_register.sales_register import _execute def execute(filters=None): - return _execute(filters, additional_table_columns=[ - dict(fieldtype='Data', label='Customer GSTIN', fieldname="customer_gstin", width=120), - dict(fieldtype='Data', label='Billing Address GSTIN', fieldname="billing_address_gstin", width=140), - dict(fieldtype='Data', label='Company GSTIN', fieldname="company_gstin", width=120), - dict(fieldtype='Data', label='Place of Supply', fieldname="place_of_supply", width=120), - dict(fieldtype='Data', label='Reverse Charge', fieldname="reverse_charge", width=120), - dict(fieldtype='Data', label='GST Category', fieldname="gst_category", width=120), - dict(fieldtype='Data', label='Export Type', fieldname="export_type", width=120), - dict(fieldtype='Data', label='E-Commerce GSTIN', fieldname="ecommerce_gstin", width=130) - ], additional_query_columns=[ - 'customer_gstin', - 'billing_address_gstin', - 'company_gstin', - 'place_of_supply', - 'reverse_charge', - 'gst_category', - 'export_type', - 'ecommerce_gstin' - ]) + return _execute( + filters, + additional_table_columns=[ + dict(fieldtype="Data", label="Customer GSTIN", fieldname="customer_gstin", width=120), + dict( + fieldtype="Data", label="Billing Address GSTIN", fieldname="billing_address_gstin", width=140 + ), + dict(fieldtype="Data", label="Company GSTIN", fieldname="company_gstin", width=120), + dict(fieldtype="Data", label="Place of Supply", fieldname="place_of_supply", width=120), + dict(fieldtype="Data", label="Reverse Charge", fieldname="reverse_charge", width=120), + dict(fieldtype="Data", label="GST Category", fieldname="gst_category", width=120), + dict(fieldtype="Data", label="Export Type", fieldname="export_type", width=120), + dict(fieldtype="Data", label="E-Commerce GSTIN", fieldname="ecommerce_gstin", width=130), + ], + additional_query_columns=[ + "customer_gstin", + "billing_address_gstin", + "company_gstin", + "place_of_supply", + "reverse_charge", + "gst_category", + "export_type", + "ecommerce_gstin", + ], + ) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 8fcb6bb444..0fe0647eb7 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -15,6 +15,7 @@ from erpnext.regional.india.utils import get_gst_accounts def execute(filters=None): return Gstr1Report(filters).run() + class Gstr1Report(object): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) @@ -59,7 +60,7 @@ class Gstr1Report(object): return self.columns, self.data def get_data(self): - if self.filters.get("type_of_business") in ("B2C Small", "B2C Large"): + if self.filters.get("type_of_business") in ("B2C Small", "B2C Large"): self.get_b2c_data() elif self.filters.get("type_of_business") == "Advances": self.get_advance_data() @@ -83,15 +84,17 @@ class Gstr1Report(object): advances = self.get_advance_entries() for entry in advances: # only consider IGST and SGST so as to avoid duplication of taxable amount - if entry.account_head in self.gst_accounts.igst_account or \ - entry.account_head in self.gst_accounts.sgst_account: + if ( + entry.account_head in self.gst_accounts.igst_account + or entry.account_head in self.gst_accounts.sgst_account + ): advances_data.setdefault((entry.place_of_supply, entry.rate), [0.0, 0.0]) - advances_data[(entry.place_of_supply, entry.rate)][0] += (entry.amount * 100 / entry.rate) + advances_data[(entry.place_of_supply, entry.rate)][0] += entry.amount * 100 / entry.rate elif entry.account_head in self.gst_accounts.cess_account: advances_data[(entry.place_of_supply, entry.rate)][1] += entry.amount for key, value in advances_data.items(): - row= [key[0], key[1], value[0], value[1]] + row = [key[0], key[1], value[0], value[1]] self.data.append(row) def get_nil_rated_invoices(self): @@ -100,31 +103,31 @@ class Gstr1Report(object): "description": "Inter-State supplies to registered persons", "nil_rated": 0.0, "exempted": 0.0, - "non_gst": 0.0 + "non_gst": 0.0, }, { "description": "Intra-State supplies to registered persons", "nil_rated": 0.0, "exempted": 0.0, - "non_gst": 0.0 + "non_gst": 0.0, }, { "description": "Inter-State supplies to unregistered persons", "nil_rated": 0.0, "exempted": 0.0, - "non_gst": 0.0 + "non_gst": 0.0, }, { "description": "Intra-State supplies to unregistered persons", "nil_rated": 0.0, "exempted": 0.0, - "non_gst": 0.0 - } + "non_gst": 0.0, + }, ] for invoice, details in self.nil_exempt_non_gst.items(): invoice_detail = self.invoices.get(invoice) - if invoice_detail.get('gst_category') in ("Registered Regular", "Deemed Export", "SEZ"): + if invoice_detail.get("gst_category") in ("Registered Regular", "Deemed Export", "SEZ"): if is_inter_state(invoice_detail): nil_exempt_output[0]["nil_rated"] += details[0] nil_exempt_output[0]["exempted"] += details[1] @@ -153,26 +156,34 @@ class Gstr1Report(object): invoice_details = self.invoices.get(inv) for rate, items in items_based_on_rate.items(): place_of_supply = invoice_details.get("place_of_supply") - ecommerce_gstin = invoice_details.get("ecommerce_gstin") + ecommerce_gstin = invoice_details.get("ecommerce_gstin") - b2cs_output.setdefault((rate, place_of_supply, ecommerce_gstin), { - "place_of_supply": "", - "ecommerce_gstin": "", - "rate": "", - "taxable_value": 0, - "cess_amount": 0, - "type": "", - "invoice_number": invoice_details.get("invoice_number"), - "posting_date": invoice_details.get("posting_date"), - "invoice_value": invoice_details.get("base_grand_total"), - }) + b2cs_output.setdefault( + (rate, place_of_supply, ecommerce_gstin), + { + "place_of_supply": "", + "ecommerce_gstin": "", + "rate": "", + "taxable_value": 0, + "cess_amount": 0, + "type": "", + "invoice_number": invoice_details.get("invoice_number"), + "posting_date": invoice_details.get("posting_date"), + "invoice_value": invoice_details.get("base_grand_total"), + }, + ) row = b2cs_output.get((rate, place_of_supply, ecommerce_gstin)) row["place_of_supply"] = place_of_supply row["ecommerce_gstin"] = ecommerce_gstin row["rate"] = rate - row["taxable_value"] += sum([abs(net_amount) - for item_code, net_amount in self.invoice_items.get(inv).items() if item_code in items]) + row["taxable_value"] += sum( + [ + abs(net_amount) + for item_code, net_amount in self.invoice_items.get(inv).items() + if item_code in items + ] + ) row["cess_amount"] += flt(self.invoice_cess.get(inv), 2) row["type"] = "E" if ecommerce_gstin else "OE" @@ -182,14 +193,17 @@ class Gstr1Report(object): def get_row_data_for_invoice(self, invoice, invoice_details, tax_rate, items): row = [] for fieldname in self.invoice_fields: - if self.filters.get("type_of_business") in ("CDNR-REG", "CDNR-UNREG") and fieldname == "invoice_value": + if ( + self.filters.get("type_of_business") in ("CDNR-REG", "CDNR-UNREG") + and fieldname == "invoice_value" + ): row.append(abs(invoice_details.base_rounded_total) or abs(invoice_details.base_grand_total)) elif fieldname == "invoice_value": row.append(invoice_details.base_rounded_total or invoice_details.base_grand_total) - elif fieldname in ('posting_date', 'shipping_bill_date'): - row.append(formatdate(invoice_details.get(fieldname), 'dd-MMM-YY')) + elif fieldname in ("posting_date", "shipping_bill_date"): + row.append(formatdate(invoice_details.get(fieldname), "dd-MMM-YY")) elif fieldname == "export_type": - export_type = "WPAY" if invoice_details.get(fieldname)=="With Payment of Tax" else "WOPAY" + export_type = "WPAY" if invoice_details.get(fieldname) == "With Payment of Tax" else "WOPAY" row.append(export_type) else: row.append(invoice_details.get(fieldname)) @@ -202,20 +216,25 @@ class Gstr1Report(object): for item_code, net_amount in self.invoice_items.get(invoice).items(): if item_code in items: - if self.item_tax_rate.get(invoice) and tax_rate/division_factor in self.item_tax_rate.get(invoice, {}).get(item_code, []): + if self.item_tax_rate.get(invoice) and tax_rate / division_factor in self.item_tax_rate.get( + invoice, {} + ).get(item_code, []): taxable_value += abs(net_amount) elif not self.item_tax_rate.get(invoice): taxable_value += abs(net_amount) elif tax_rate: taxable_value += abs(net_amount) - elif not tax_rate and self.filters.get('type_of_business') == 'EXPORT' \ - and invoice_details.get('export_type') == "Without Payment of Tax": + elif ( + not tax_rate + and self.filters.get("type_of_business") == "EXPORT" + and invoice_details.get("export_type") == "Without Payment of Tax" + ): taxable_value += abs(net_amount) row += [tax_rate or 0, taxable_value] for column in self.other_columns: - if column.get('fieldname') == 'cess_amount': + if column.get("fieldname") == "cess_amount": row.append(flt(self.invoice_cess.get(invoice), 2)) return row, taxable_value @@ -224,68 +243,82 @@ class Gstr1Report(object): self.invoices = frappe._dict() conditions = self.get_conditions() - invoice_data = frappe.db.sql(""" + invoice_data = frappe.db.sql( + """ select {select_columns} from `tab{doctype}` where docstatus = 1 {where_conditions} and is_opening = 'No' order by posting_date desc - """.format(select_columns=self.select_columns, doctype=self.doctype, - where_conditions=conditions), self.filters, as_dict=1) + """.format( + select_columns=self.select_columns, doctype=self.doctype, where_conditions=conditions + ), + self.filters, + as_dict=1, + ) for d in invoice_data: self.invoices.setdefault(d.invoice_number, d) def get_advance_entries(self): - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT SUM(a.base_tax_amount) as amount, a.account_head, a.rate, p.place_of_supply FROM `tabPayment Entry` p, `tabAdvance Taxes and Charges` a WHERE p.docstatus = 1 AND p.name = a.parent AND posting_date between %s and %s GROUP BY a.account_head, p.place_of_supply, a.rate - """, (self.filters.get('from_date'), self.filters.get('to_date')), as_dict=1) + """, + (self.filters.get("from_date"), self.filters.get("to_date")), + as_dict=1, + ) def get_conditions(self): conditions = "" - for opts in (("company", " and company=%(company)s"), + for opts in ( + ("company", " and company=%(company)s"), ("from_date", " and posting_date>=%(from_date)s"), ("to_date", " and posting_date<=%(to_date)s"), ("company_address", " and company_address=%(company_address)s"), - ("company_gstin", " and company_gstin=%(company_gstin)s")): - if self.filters.get(opts[0]): - conditions += opts[1] + ("company_gstin", " and company_gstin=%(company_gstin)s"), + ): + if self.filters.get(opts[0]): + conditions += opts[1] - - if self.filters.get("type_of_business") == "B2B": + if self.filters.get("type_of_business") == "B2B": conditions += "AND IFNULL(gst_category, '') in ('Registered Regular', 'Registered Composition', 'Deemed Export', 'SEZ') AND is_return != 1 AND is_debit_note !=1" if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"): - b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit') + b2c_limit = frappe.db.get_single_value("GST Settings", "b2c_limit") if not b2c_limit: frappe.throw(_("Please set B2C Limit in GST Settings.")) - if self.filters.get("type_of_business") == "B2C Large": + if self.filters.get("type_of_business") == "B2C Large": conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'') - AND grand_total > {0} AND is_return != 1 AND is_debit_note !=1 AND gst_category ='Unregistered' """.format(flt(b2c_limit)) + AND grand_total > {0} AND is_return != 1 AND is_debit_note !=1 AND gst_category ='Unregistered' """.format( + flt(b2c_limit) + ) - elif self.filters.get("type_of_business") == "B2C Small": + elif self.filters.get("type_of_business") == "B2C Small": conditions += """ AND ( SUBSTR(place_of_supply, 1, 2) = SUBSTR(company_gstin, 1, 2) - OR grand_total <= {0}) and is_return != 1 AND gst_category ='Unregistered' """.format(flt(b2c_limit)) + OR grand_total <= {0}) and is_return != 1 AND gst_category ='Unregistered' """.format( + flt(b2c_limit) + ) elif self.filters.get("type_of_business") == "CDNR-REG": conditions += """ AND (is_return = 1 OR is_debit_note = 1) AND IFNULL(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ')""" elif self.filters.get("type_of_business") == "CDNR-UNREG": - b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit') + b2c_limit = frappe.db.get_single_value("GST Settings", "b2c_limit") conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'') AND (is_return = 1 OR is_debit_note = 1) AND IFNULL(gst_category, '') in ('Unregistered', 'Overseas')""" - elif self.filters.get("type_of_business") == "EXPORT": + elif self.filters.get("type_of_business") == "EXPORT": conditions += """ AND is_return !=1 and gst_category = 'Overseas' """ conditions += " AND IFNULL(billing_address_gstin, '') != company_gstin" @@ -297,15 +330,22 @@ class Gstr1Report(object): self.item_tax_rate = frappe._dict() self.nil_exempt_non_gst = {} - items = frappe.db.sql(""" + items = frappe.db.sql( + """ select item_code, parent, taxable_value, base_net_amount, item_tax_rate, is_nil_exempt, is_non_gst from `tab%s Item` where parent in (%s) - """ % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1) + """ + % (self.doctype, ", ".join(["%s"] * len(self.invoices))), + tuple(self.invoices), + as_dict=1, + ) for d in items: self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0) - self.invoice_items[d.parent][d.item_code] += d.get('taxable_value', 0) or d.get('base_net_amount', 0) + self.invoice_items[d.parent][d.item_code] += d.get("taxable_value", 0) or d.get( + "base_net_amount", 0 + ) item_tax_rate = {} @@ -319,15 +359,16 @@ class Gstr1Report(object): if d.is_nil_exempt: self.nil_exempt_non_gst.setdefault(d.parent, [0.0, 0.0, 0.0]) if item_tax_rate: - self.nil_exempt_non_gst[d.parent][0] += d.get('taxable_value', 0) + self.nil_exempt_non_gst[d.parent][0] += d.get("taxable_value", 0) else: - self.nil_exempt_non_gst[d.parent][1] += d.get('taxable_value', 0) + self.nil_exempt_non_gst[d.parent][1] += d.get("taxable_value", 0) elif d.is_non_gst: self.nil_exempt_non_gst.setdefault(d.parent, [0.0, 0.0, 0.0]) - self.nil_exempt_non_gst[d.parent][2] += d.get('taxable_value', 0) + self.nil_exempt_non_gst[d.parent][2] += d.get("taxable_value", 0) def get_items_based_on_tax_rate(self): - self.tax_details = frappe.db.sql(""" + self.tax_details = frappe.db.sql( + """ select parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount from `tab%s` @@ -335,8 +376,10 @@ class Gstr1Report(object): parenttype = %s and docstatus = 1 and parent in (%s) order by account_head - """ % (self.tax_doctype, '%s', ', '.join(['%s']*len(self.invoices.keys()))), - tuple([self.doctype] + list(self.invoices.keys()))) + """ + % (self.tax_doctype, "%s", ", ".join(["%s"] * len(self.invoices.keys()))), + tuple([self.doctype] + list(self.invoices.keys())), + ) self.items_based_on_tax_rate = {} self.invoice_cess = frappe._dict() @@ -352,8 +395,7 @@ class Gstr1Report(object): try: item_wise_tax_detail = json.loads(item_wise_tax_detail) cgst_or_sgst = False - if account in self.gst_accounts.cgst_account \ - or account in self.gst_accounts.sgst_account: + if account in self.gst_accounts.cgst_account or account in self.gst_accounts.sgst_account: cgst_or_sgst = True if not (cgst_or_sgst or account in self.gst_accounts.igst_account): @@ -370,22 +412,30 @@ class Gstr1Report(object): if parent not in self.cgst_sgst_invoices: self.cgst_sgst_invoices.append(parent) - rate_based_dict = self.items_based_on_tax_rate\ - .setdefault(parent, {}).setdefault(tax_rate, []) + rate_based_dict = self.items_based_on_tax_rate.setdefault(parent, {}).setdefault( + tax_rate, [] + ) if item_code not in rate_based_dict: rate_based_dict.append(item_code) except ValueError: continue if unidentified_gst_accounts: - frappe.msgprint(_("Following accounts might be selected in GST Settings:") - + "
    " + "
    ".join(unidentified_gst_accounts), alert=True) + frappe.msgprint( + _("Following accounts might be selected in GST Settings:") + + "
    " + + "
    ".join(unidentified_gst_accounts), + alert=True, + ) # Build itemised tax for export invoices where tax table is blank for invoice, items in self.invoice_items.items(): - if invoice not in self.items_based_on_tax_rate and invoice not in unidentified_gst_accounts_invoice \ - and self.invoices.get(invoice, {}).get('export_type') == "Without Payment of Tax" \ - and self.invoices.get(invoice, {}).get('gst_category') in ("Overseas", "SEZ"): - self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) + if ( + invoice not in self.items_based_on_tax_rate + and invoice not in unidentified_gst_accounts_invoice + and self.invoices.get(invoice, {}).get("export_type") == "Without Payment of Tax" + and self.invoices.get(invoice, {}).get("gst_category") in ("Overseas", "SEZ") + ): + self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) def get_columns(self): self.other_columns = [] @@ -393,417 +443,258 @@ class Gstr1Report(object): if self.filters.get("type_of_business") != "NIL Rated": self.tax_columns = [ - { - "fieldname": "rate", - "label": "Rate", - "fieldtype": "Int", - "width": 60 - }, + {"fieldname": "rate", "label": "Rate", "fieldtype": "Int", "width": 60}, { "fieldname": "taxable_value", "label": "Taxable Value", "fieldtype": "Currency", - "width": 100 - } + "width": 100, + }, ] - if self.filters.get("type_of_business") == "B2B": + if self.filters.get("type_of_business") == "B2B": self.invoice_columns = [ { "fieldname": "billing_address_gstin", "label": "GSTIN/UIN of Recipient", "fieldtype": "Data", - "width": 150 - }, - { - "fieldname": "customer_name", - "label": "Receiver Name", - "fieldtype": "Data", - "width":100 + "width": 150, }, + {"fieldname": "customer_name", "label": "Receiver Name", "fieldtype": "Data", "width": 100}, { "fieldname": "invoice_number", "label": "Invoice Number", "fieldtype": "Link", "options": "Sales Invoice", - "width":100 - }, - { - "fieldname": "posting_date", - "label": "Invoice date", - "fieldtype": "Data", - "width":80 + "width": 100, }, + {"fieldname": "posting_date", "label": "Invoice date", "fieldtype": "Data", "width": 80}, { "fieldname": "invoice_value", "label": "Invoice Value", "fieldtype": "Currency", - "width":100 + "width": 100, }, { "fieldname": "place_of_supply", "label": "Place Of Supply", "fieldtype": "Data", - "width":100 - }, - { - "fieldname": "reverse_charge", - "label": "Reverse Charge", - "fieldtype": "Data" - }, - { - "fieldname": "gst_category", - "label": "Invoice Type", - "fieldtype": "Data" + "width": 100, }, + {"fieldname": "reverse_charge", "label": "Reverse Charge", "fieldtype": "Data"}, + {"fieldname": "gst_category", "label": "Invoice Type", "fieldtype": "Data"}, { "fieldname": "ecommerce_gstin", "label": "E-Commerce GSTIN", "fieldtype": "Data", - "width":120 - } + "width": 120, + }, ] self.other_columns = [ - { - "fieldname": "cess_amount", - "label": "Cess Amount", - "fieldtype": "Currency", - "width": 100 - } - ] + {"fieldname": "cess_amount", "label": "Cess Amount", "fieldtype": "Currency", "width": 100} + ] - elif self.filters.get("type_of_business") == "B2C Large": + elif self.filters.get("type_of_business") == "B2C Large": self.invoice_columns = [ { "fieldname": "invoice_number", "label": "Invoice Number", "fieldtype": "Link", "options": "Sales Invoice", - "width": 120 - }, - { - "fieldname": "posting_date", - "label": "Invoice date", - "fieldtype": "Data", - "width": 100 + "width": 120, }, + {"fieldname": "posting_date", "label": "Invoice date", "fieldtype": "Data", "width": 100}, { "fieldname": "invoice_value", "label": "Invoice Value", "fieldtype": "Currency", - "width": 100 + "width": 100, }, { "fieldname": "place_of_supply", "label": "Place Of Supply", "fieldtype": "Data", - "width": 120 + "width": 120, }, { "fieldname": "ecommerce_gstin", "label": "E-Commerce GSTIN", "fieldtype": "Data", - "width": 130 - } + "width": 130, + }, ] self.other_columns = [ - { - "fieldname": "cess_amount", - "label": "Cess Amount", - "fieldtype": "Currency", - "width": 100 - } - ] + {"fieldname": "cess_amount", "label": "Cess Amount", "fieldtype": "Currency", "width": 100} + ] elif self.filters.get("type_of_business") == "CDNR-REG": self.invoice_columns = [ { "fieldname": "billing_address_gstin", "label": "GSTIN/UIN of Recipient", "fieldtype": "Data", - "width": 150 - }, - { - "fieldname": "customer_name", - "label": "Receiver Name", - "fieldtype": "Data", - "width": 120 + "width": 150, }, + {"fieldname": "customer_name", "label": "Receiver Name", "fieldtype": "Data", "width": 120}, { "fieldname": "return_against", "label": "Invoice/Advance Receipt Number", "fieldtype": "Link", "options": "Sales Invoice", - "width": 120 + "width": 120, }, { "fieldname": "posting_date", "label": "Invoice/Advance Receipt date", "fieldtype": "Data", - "width": 120 + "width": 120, }, { "fieldname": "invoice_number", "label": "Invoice/Advance Receipt Number", "fieldtype": "Link", "options": "Sales Invoice", - "width":120 - }, - { - "fieldname": "reverse_charge", - "label": "Reverse Charge", - "fieldtype": "Data" - }, - { - "fieldname": "export_type", - "label": "Export Type", - "fieldtype": "Data", - "hidden": 1 + "width": 120, }, + {"fieldname": "reverse_charge", "label": "Reverse Charge", "fieldtype": "Data"}, + {"fieldname": "export_type", "label": "Export Type", "fieldtype": "Data", "hidden": 1}, { "fieldname": "reason_for_issuing_document", "label": "Reason For Issuing document", "fieldtype": "Data", - "width": 140 + "width": 140, }, { "fieldname": "place_of_supply", "label": "Place Of Supply", "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "gst_category", - "label": "GST Category", - "fieldtype": "Data" + "width": 120, }, + {"fieldname": "gst_category", "label": "GST Category", "fieldtype": "Data"}, { "fieldname": "invoice_value", "label": "Invoice Value", "fieldtype": "Currency", - "width": 120 - } + "width": 120, + }, ] self.other_columns = [ - { - "fieldname": "cess_amount", - "label": "Cess Amount", - "fieldtype": "Currency", - "width": 100 - }, - { - "fieldname": "pre_gst", - "label": "PRE GST", - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "document_type", - "label": "Document Type", - "fieldtype": "Data", - "width": 80 - } + {"fieldname": "cess_amount", "label": "Cess Amount", "fieldtype": "Currency", "width": 100}, + {"fieldname": "pre_gst", "label": "PRE GST", "fieldtype": "Data", "width": 80}, + {"fieldname": "document_type", "label": "Document Type", "fieldtype": "Data", "width": 80}, ] elif self.filters.get("type_of_business") == "CDNR-UNREG": self.invoice_columns = [ - { - "fieldname": "customer_name", - "label": "Receiver Name", - "fieldtype": "Data", - "width": 120 - }, + {"fieldname": "customer_name", "label": "Receiver Name", "fieldtype": "Data", "width": 120}, { "fieldname": "return_against", "label": "Issued Against", "fieldtype": "Link", "options": "Sales Invoice", - "width": 120 - }, - { - "fieldname": "posting_date", - "label": "Note Date", - "fieldtype": "Date", - "width": 120 + "width": 120, }, + {"fieldname": "posting_date", "label": "Note Date", "fieldtype": "Date", "width": 120}, { "fieldname": "invoice_number", "label": "Note Number", "fieldtype": "Link", "options": "Sales Invoice", - "width":120 - }, - { - "fieldname": "export_type", - "label": "Export Type", - "fieldtype": "Data", - "hidden": 1 + "width": 120, }, + {"fieldname": "export_type", "label": "Export Type", "fieldtype": "Data", "hidden": 1}, { "fieldname": "reason_for_issuing_document", "label": "Reason For Issuing document", "fieldtype": "Data", - "width": 140 + "width": 140, }, { "fieldname": "place_of_supply", "label": "Place Of Supply", "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "gst_category", - "label": "GST Category", - "fieldtype": "Data" + "width": 120, }, + {"fieldname": "gst_category", "label": "GST Category", "fieldtype": "Data"}, { "fieldname": "invoice_value", "label": "Invoice Value", "fieldtype": "Currency", - "width": 120 - } + "width": 120, + }, ] self.other_columns = [ - { - "fieldname": "cess_amount", - "label": "Cess Amount", - "fieldtype": "Currency", - "width": 100 - }, - { - "fieldname": "pre_gst", - "label": "PRE GST", - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "document_type", - "label": "Document Type", - "fieldtype": "Data", - "width": 80 - } + {"fieldname": "cess_amount", "label": "Cess Amount", "fieldtype": "Currency", "width": 100}, + {"fieldname": "pre_gst", "label": "PRE GST", "fieldtype": "Data", "width": 80}, + {"fieldname": "document_type", "label": "Document Type", "fieldtype": "Data", "width": 80}, ] - elif self.filters.get("type_of_business") == "B2C Small": + elif self.filters.get("type_of_business") == "B2C Small": self.invoice_columns = [ { "fieldname": "place_of_supply", "label": "Place Of Supply", "fieldtype": "Data", - "width": 120 + "width": 120, }, { "fieldname": "ecommerce_gstin", "label": "E-Commerce GSTIN", "fieldtype": "Data", - "width": 130 - } + "width": 130, + }, ] self.other_columns = [ - { - "fieldname": "cess_amount", - "label": "Cess Amount", - "fieldtype": "Currency", - "width": 100 - }, - { - "fieldname": "type", - "label": "Type", - "fieldtype": "Data", - "width": 50 - } + {"fieldname": "cess_amount", "label": "Cess Amount", "fieldtype": "Currency", "width": 100}, + {"fieldname": "type", "label": "Type", "fieldtype": "Data", "width": 50}, ] - elif self.filters.get("type_of_business") == "EXPORT": + elif self.filters.get("type_of_business") == "EXPORT": self.invoice_columns = [ - { - "fieldname": "export_type", - "label": "Export Type", - "fieldtype": "Data", - "width":120 - }, + {"fieldname": "export_type", "label": "Export Type", "fieldtype": "Data", "width": 120}, { "fieldname": "invoice_number", "label": "Invoice Number", "fieldtype": "Link", "options": "Sales Invoice", - "width":120 - }, - { - "fieldname": "posting_date", - "label": "Invoice date", - "fieldtype": "Data", - "width": 120 + "width": 120, }, + {"fieldname": "posting_date", "label": "Invoice date", "fieldtype": "Data", "width": 120}, { "fieldname": "invoice_value", "label": "Invoice Value", "fieldtype": "Currency", - "width": 120 - }, - { - "fieldname": "port_code", - "label": "Port Code", - "fieldtype": "Data", - "width": 120 + "width": 120, }, + {"fieldname": "port_code", "label": "Port Code", "fieldtype": "Data", "width": 120}, { "fieldname": "shipping_bill_number", "label": "Shipping Bill Number", "fieldtype": "Data", - "width": 120 + "width": 120, }, { "fieldname": "shipping_bill_date", "label": "Shipping Bill Date", "fieldtype": "Data", - "width": 120 - } + "width": 120, + }, ] elif self.filters.get("type_of_business") == "Advances": self.invoice_columns = [ - { - "fieldname": "place_of_supply", - "label": "Place Of Supply", - "fieldtype": "Data", - "width": 120 - } + {"fieldname": "place_of_supply", "label": "Place Of Supply", "fieldtype": "Data", "width": 120} ] self.other_columns = [ - { - "fieldname": "cess_amount", - "label": "Cess Amount", - "fieldtype": "Currency", - "width": 100 - } + {"fieldname": "cess_amount", "label": "Cess Amount", "fieldtype": "Currency", "width": 100} ] elif self.filters.get("type_of_business") == "NIL Rated": self.invoice_columns = [ - { - "fieldname": "description", - "label": "Description", - "fieldtype": "Data", - "width": 420 - }, - { - "fieldname": "nil_rated", - "label": "Nil Rated", - "fieldtype": "Currency", - "width": 200 - }, - { - "fieldname": "exempted", - "label": "Exempted", - "fieldtype": "Currency", - "width": 200 - }, - { - "fieldname": "non_gst", - "label": "Non GST", - "fieldtype": "Currency", - "width": 200 - } + {"fieldname": "description", "label": "Description", "fieldtype": "Data", "width": 420}, + {"fieldname": "nil_rated", "label": "Nil Rated", "fieldtype": "Currency", "width": 200}, + {"fieldname": "exempted", "label": "Exempted", "fieldtype": "Currency", "width": 200}, + {"fieldname": "non_gst", "label": "Non GST", "fieldtype": "Currency", "width": 200}, ] self.columns = self.invoice_columns + self.tax_columns + self.other_columns + @frappe.whitelist() def get_json(filters, report_name, data): filters = json.loads(filters) @@ -812,13 +703,14 @@ def get_json(filters, report_name, data): fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) - gst_json = {"version": "GST3.0.4", - "hash": "hash", "gstin": gstin, "fp": fp} + gst_json = {"version": "GST3.0.4", "hash": "hash", "gstin": gstin, "fp": fp} res = {} if filters["type_of_business"] == "B2B": for item in report_data[:-1]: - res.setdefault(item["billing_address_gstin"], {}).setdefault(item["invoice_number"],[]).append(item) + res.setdefault(item["billing_address_gstin"], {}).setdefault(item["invoice_number"], []).append( + item + ) out = get_b2b_json(res, gstin) gst_json["b2b"] = out @@ -842,13 +734,15 @@ def get_json(filters, report_name, data): gst_json["exp"] = out elif filters["type_of_business"] == "CDNR-REG": for item in report_data[:-1]: - res.setdefault(item["billing_address_gstin"], {}).setdefault(item["invoice_number"],[]).append(item) + res.setdefault(item["billing_address_gstin"], {}).setdefault(item["invoice_number"], []).append( + item + ) out = get_cdnr_reg_json(res, gstin) gst_json["cdnr"] = out elif filters["type_of_business"] == "CDNR-UNREG": for item in report_data[:-1]: - res.setdefault(item["invoice_number"],[]).append(item) + res.setdefault(item["invoice_number"], []).append(item) out = get_cdnr_unreg_json(res, gstin) gst_json["cdnur"] = out @@ -856,10 +750,14 @@ def get_json(filters, report_name, data): elif filters["type_of_business"] == "Advances": for item in report_data[:-1]: if not item.get("place_of_supply"): - frappe.throw(_("""{0} not entered in some entries. - Please update and try again""").format(frappe.bold("Place Of Supply"))) + frappe.throw( + _( + """{0} not entered in some entries. + Please update and try again""" + ).format(frappe.bold("Place Of Supply")) + ) - res.setdefault(item["place_of_supply"],[]).append(item) + res.setdefault(item["place_of_supply"], []).append(item) out = get_advances_json(res, gstin) gst_json["at"] = out @@ -869,30 +767,34 @@ def get_json(filters, report_name, data): out = get_exempted_json(res) gst_json["nil"] = out - return { - 'report_name': report_name, - 'report_type': filters['type_of_business'], - 'data': gst_json - } + return {"report_name": report_name, "report_type": filters["type_of_business"], "data": gst_json} + def get_b2b_json(res, gstin): out = [] for gst_in in res: b2b_item, inv = {"ctin": gst_in, "inv": []}, [] - if not gst_in: continue + if not gst_in: + continue for number, invoice in res[gst_in].items(): if not invoice[0]["place_of_supply"]: - frappe.throw(_("""{0} not entered in Invoice {1}. - Please update and try again""").format(frappe.bold("Place Of Supply"), - frappe.bold(invoice[0]['invoice_number']))) + frappe.throw( + _( + """{0} not entered in Invoice {1}. + Please update and try again""" + ).format( + frappe.bold("Place Of Supply"), frappe.bold(invoice[0]["invoice_number"]) + ) + ) inv_item = get_basic_invoice_detail(invoice[0]) - inv_item["pos"] = "%02d" % int(invoice[0]["place_of_supply"].split('-')[0]) + inv_item["pos"] = "%02d" % int(invoice[0]["place_of_supply"].split("-")[0]) inv_item["rchrg"] = invoice[0]["reverse_charge"] inv_item["inv_typ"] = get_invoice_type(invoice[0]) - if inv_item["pos"]=="00": continue + if inv_item["pos"] == "00": + continue inv_item["itms"] = [] for item in invoice: @@ -900,95 +802,101 @@ def get_b2b_json(res, gstin): inv.append(inv_item) - if not inv: continue + if not inv: + continue b2b_item["inv"] = inv out.append(b2b_item) return out + def get_b2cs_json(data, gstin): company_state_number = gstin[0:2] out = [] for d in data: if not d.get("place_of_supply"): - frappe.throw(_("""{0} not entered in some invoices. - Please update and try again""").format(frappe.bold("Place Of Supply"))) + frappe.throw( + _( + """{0} not entered in some invoices. + Please update and try again""" + ).format(frappe.bold("Place Of Supply")) + ) - pos = d.get('place_of_supply').split('-')[0] + pos = d.get("place_of_supply").split("-")[0] tax_details = {} - rate = d.get('rate', 0) - tax = flt((d["taxable_value"]*rate)/100.0, 2) + rate = d.get("rate", 0) + tax = flt((d["taxable_value"] * rate) / 100.0, 2) if company_state_number == pos: - tax_details.update({"camt": flt(tax/2.0, 2), "samt": flt(tax/2.0, 2)}) + tax_details.update({"camt": flt(tax / 2.0, 2), "samt": flt(tax / 2.0, 2)}) else: tax_details.update({"iamt": tax}) inv = { "sply_ty": "INTRA" if company_state_number == pos else "INTER", "pos": pos, - "typ": d.get('type'), - "txval": flt(d.get('taxable_value'), 2), + "typ": d.get("type"), + "txval": flt(d.get("taxable_value"), 2), "rt": rate, - "iamt": flt(tax_details.get('iamt'), 2), - "camt": flt(tax_details.get('camt'), 2), - "samt": flt(tax_details.get('samt'), 2), - "csamt": flt(d.get('cess_amount'), 2) + "iamt": flt(tax_details.get("iamt"), 2), + "camt": flt(tax_details.get("camt"), 2), + "samt": flt(tax_details.get("samt"), 2), + "csamt": flt(d.get("cess_amount"), 2), } - if d.get('type') == "E" and d.get('ecommerce_gstin'): - inv.update({ - "etin": d.get('ecommerce_gstin') - }) + if d.get("type") == "E" and d.get("ecommerce_gstin"): + inv.update({"etin": d.get("ecommerce_gstin")}) out.append(inv) return out + def get_advances_json(data, gstin): company_state_number = gstin[0:2] out = [] for place_of_supply, items in data.items(): - supply_type = "INTRA" if company_state_number == place_of_supply.split('-')[0] else "INTER" - row = { - "pos": place_of_supply.split('-')[0], - "itms": [], - "sply_ty": supply_type - } + supply_type = "INTRA" if company_state_number == place_of_supply.split("-")[0] else "INTER" + row = {"pos": place_of_supply.split("-")[0], "itms": [], "sply_ty": supply_type} for item in items: itms = { - 'rt': item['rate'], - 'ad_amount': flt(item.get('taxable_value')), - 'csamt': flt(item.get('cess_amount')) + "rt": item["rate"], + "ad_amount": flt(item.get("taxable_value")), + "csamt": flt(item.get("cess_amount")), } if supply_type == "INTRA": - itms.update({ - "samt": flt((itms["ad_amount"] * itms["rt"]) / 100), - "camt": flt((itms["ad_amount"] * itms["rt"]) / 100), - "rt": itms["rt"] * 2 - }) + itms.update( + { + "samt": flt((itms["ad_amount"] * itms["rt"]) / 100), + "camt": flt((itms["ad_amount"] * itms["rt"]) / 100), + "rt": itms["rt"] * 2, + } + ) else: - itms.update({ - "iamt": flt((itms["ad_amount"] * itms["rt"]) / 100) - }) + itms.update({"iamt": flt((itms["ad_amount"] * itms["rt"]) / 100)}) - row['itms'].append(itms) + row["itms"].append(itms) out.append(row) return out + def get_b2cl_json(res, gstin): out = [] for pos in res: if not pos: - frappe.throw(_("""{0} not entered in some invoices. - Please update and try again""").format(frappe.bold("Place Of Supply"))) + frappe.throw( + _( + """{0} not entered in some invoices. + Please update and try again""" + ).format(frappe.bold("Place Of Supply")) + ) - b2cl_item, inv = {"pos": "%02d" % int(pos.split('-')[0]), "inv": []}, [] + b2cl_item, inv = {"pos": "%02d" % int(pos.split("-")[0]), "inv": []}, [] for row in res[pos]: inv_item = get_basic_invoice_detail(row) @@ -1004,6 +912,7 @@ def get_b2cl_json(res, gstin): return out + def get_export_json(res): out = [] for exp_type in res: @@ -1011,12 +920,9 @@ def get_export_json(res): for row in res[exp_type]: inv_item = get_basic_invoice_detail(row) - inv_item["itms"] = [{ - "txval": flt(row["taxable_value"], 2), - "rt": row["rate"] or 0, - "iamt": 0, - "csamt": 0 - }] + inv_item["itms"] = [ + {"txval": flt(row["taxable_value"], 2), "rt": row["rate"] or 0, "iamt": 0, "csamt": 0} + ] inv.append(inv_item) @@ -1025,27 +931,34 @@ def get_export_json(res): return out + def get_cdnr_reg_json(res, gstin): out = [] for gst_in in res: cdnr_item, inv = {"ctin": gst_in, "nt": []}, [] - if not gst_in: continue + if not gst_in: + continue for number, invoice in res[gst_in].items(): if not invoice[0]["place_of_supply"]: - frappe.throw(_("""{0} not entered in Invoice {1}. - Please update and try again""").format(frappe.bold("Place Of Supply"), - frappe.bold(invoice[0]['invoice_number']))) + frappe.throw( + _( + """{0} not entered in Invoice {1}. + Please update and try again""" + ).format( + frappe.bold("Place Of Supply"), frappe.bold(invoice[0]["invoice_number"]) + ) + ) inv_item = { "nt_num": invoice[0]["invoice_number"], - "nt_dt": getdate(invoice[0]["posting_date"]).strftime('%d-%m-%Y'), + "nt_dt": getdate(invoice[0]["posting_date"]).strftime("%d-%m-%Y"), "val": abs(flt(invoice[0]["invoice_value"])), "ntty": invoice[0]["document_type"], - "pos": "%02d" % int(invoice[0]["place_of_supply"].split('-')[0]), + "pos": "%02d" % int(invoice[0]["place_of_supply"].split("-")[0]), "rchrg": invoice[0]["reverse_charge"], - "inv_typ": get_invoice_type(invoice[0]) + "inv_typ": get_invoice_type(invoice[0]), } inv_item["itms"] = [] @@ -1054,23 +967,25 @@ def get_cdnr_reg_json(res, gstin): inv.append(inv_item) - if not inv: continue + if not inv: + continue cdnr_item["nt"] = inv out.append(cdnr_item) return out + def get_cdnr_unreg_json(res, gstin): out = [] for invoice, items in res.items(): inv_item = { "nt_num": items[0]["invoice_number"], - "nt_dt": getdate(items[0]["posting_date"]).strftime('%d-%m-%Y'), + "nt_dt": getdate(items[0]["posting_date"]).strftime("%d-%m-%Y"), "val": abs(flt(items[0]["invoice_value"])), "ntty": items[0]["document_type"], - "pos": "%02d" % int(items[0]["place_of_supply"].split('-')[0]), - "typ": get_invoice_type(items[0]) + "pos": "%02d" % int(items[0]["place_of_supply"].split("-")[0]), + "typ": get_invoice_type(items[0]), } inv_item["itms"] = [] @@ -1081,63 +996,62 @@ def get_cdnr_unreg_json(res, gstin): return out + def get_exempted_json(data): out = { "inv": [ - { - "sply_ty": "INTRB2B" - }, - { - "sply_ty": "INTRAB2B" - }, - { - "sply_ty": "INTRB2C" - }, - { - "sply_ty": "INTRAB2C" - } + {"sply_ty": "INTRB2B"}, + {"sply_ty": "INTRAB2B"}, + {"sply_ty": "INTRB2C"}, + {"sply_ty": "INTRAB2C"}, ] } for i, v in enumerate(data): - if data[i].get('nil_rated'): - out['inv'][i]['nil_amt'] = data[i]['nil_rated'] + if data[i].get("nil_rated"): + out["inv"][i]["nil_amt"] = data[i]["nil_rated"] - if data[i].get('exempted'): - out['inv'][i]['expt_amt'] = data[i]['exempted'] + if data[i].get("exempted"): + out["inv"][i]["expt_amt"] = data[i]["exempted"] - if data[i].get('non_gst'): - out['inv'][i]['ngsup_amt'] = data[i]['non_gst'] + if data[i].get("non_gst"): + out["inv"][i]["ngsup_amt"] = data[i]["non_gst"] return out + def get_invoice_type(row): - gst_category = row.get('gst_category') + gst_category = row.get("gst_category") - if gst_category == 'SEZ': - return 'SEWP' if row.get('export_type') == 'WPAY' else 'SEWOP' + if gst_category == "SEZ": + return "SEWP" if row.get("export_type") == "WPAY" else "SEWOP" - if gst_category == 'Overseas': - return 'EXPWP' if row.get('export_type') == 'WPAY' else 'EXPWOP' + if gst_category == "Overseas": + return "EXPWP" if row.get("export_type") == "WPAY" else "EXPWOP" + + return ( + { + "Deemed Export": "DE", + "Registered Regular": "R", + "Registered Composition": "R", + "Unregistered": "B2CL", + } + ).get(gst_category) - return ({ - 'Deemed Export': 'DE', - 'Registered Regular': 'R', - 'Registered Composition': 'R', - 'Unregistered': 'B2CL' - }).get(gst_category) def get_basic_invoice_detail(row): return { "inum": row["invoice_number"], - "idt": getdate(row["posting_date"]).strftime('%d-%m-%Y'), - "val": flt(row["invoice_value"], 2) + "idt": getdate(row["posting_date"]).strftime("%d-%m-%Y"), + "val": flt(row["invoice_value"], 2), } + def get_rate_and_tax_details(row, gstin): - itm_det = {"txval": flt(row["taxable_value"], 2), + itm_det = { + "txval": flt(row["taxable_value"], 2), "rt": row["rate"], - "csamt": (flt(row.get("cess_amount"), 2) or 0) + "csamt": (flt(row.get("cess_amount"), 2) or 0), } # calculate rate @@ -1145,17 +1059,18 @@ def get_rate_and_tax_details(row, gstin): rate = row.get("rate") or 0 # calculate tax amount added - tax = flt((row["taxable_value"]*rate)/100.0, 2) - frappe.errprint([tax, tax/2]) + tax = flt((row["taxable_value"] * rate) / 100.0, 2) + frappe.errprint([tax, tax / 2]) if row.get("billing_address_gstin") and gstin[0:2] == row["billing_address_gstin"][0:2]: - itm_det.update({"camt": flt(tax/2.0, 2), "samt": flt(tax/2.0, 2)}) + itm_det.update({"camt": flt(tax / 2.0, 2), "samt": flt(tax / 2.0, 2)}) else: itm_det.update({"iamt": tax}) return {"num": int(num), "itm_det": itm_det} + def get_company_gstin_number(company, address=None, all_gstins=False): - gstin = '' + gstin = "" if address: gstin = frappe.db.get_value("Address", address, "gstin") @@ -1165,28 +1080,36 @@ def get_company_gstin_number(company, address=None, all_gstins=False): ["Dynamic Link", "link_doctype", "=", "Company"], ["Dynamic Link", "link_name", "=", company], ["Dynamic Link", "parenttype", "=", "Address"], - ["gstin", "!=", ''] + ["gstin", "!=", ""], ] - gstin = frappe.get_all("Address", filters=filters, pluck="gstin", order_by="is_primary_address desc") + gstin = frappe.get_all( + "Address", filters=filters, pluck="gstin", order_by="is_primary_address desc" + ) if gstin and not all_gstins: gstin = gstin[0] if not gstin: address = frappe.bold(address) if address else "" - frappe.throw(_("Please set valid GSTIN No. in Company Address {} for company {}").format( - address, frappe.bold(company) - )) + frappe.throw( + _("Please set valid GSTIN No. in Company Address {} for company {}").format( + address, frappe.bold(company) + ) + ) return gstin + @frappe.whitelist() def download_json_file(): - ''' download json content in a file ''' + """download json content in a file""" data = frappe._dict(frappe.local.form_dict) - frappe.response['filename'] = frappe.scrub("{0} {1}".format(data['report_name'], data['report_type'])) + '.json' - frappe.response['filecontent'] = data['data'] - frappe.response['content_type'] = 'application/json' - frappe.response['type'] = 'download' + frappe.response["filename"] = ( + frappe.scrub("{0} {1}".format(data["report_name"], data["report_type"])) + ".json" + ) + frappe.response["filecontent"] = data["data"] + frappe.response["content_type"] = "application/json" + frappe.response["type"] = "download" + def is_inter_state(invoice_detail): if invoice_detail.place_of_supply.split("-")[0] != invoice_detail.company_gstin[:2]: @@ -1200,16 +1123,16 @@ def get_company_gstins(company): address = frappe.qb.DocType("Address") links = frappe.qb.DocType("Dynamic Link") - addresses = frappe.qb.from_(address).inner_join(links).on( - address.name == links.parent - ).select( - address.gstin - ).where( - links.link_doctype == 'Company' - ).where( - links.link_name == company - ).run(as_dict=1) + addresses = ( + frappe.qb.from_(address) + .inner_join(links) + .on(address.name == links.parent) + .select(address.gstin) + .where(links.link_doctype == "Company") + .where(links.link_name == company) + .run(as_dict=1) + ) - address_list = [''] + [d.gstin for d in addresses] + address_list = [""] + [d.gstin for d in addresses] - return address_list \ No newline at end of file + return address_list diff --git a/erpnext/regional/report/gstr_2/gstr_2.py b/erpnext/regional/report/gstr_2/gstr_2.py index 47c856dfaa..a189d2a500 100644 --- a/erpnext/regional/report/gstr_2/gstr_2.py +++ b/erpnext/regional/report/gstr_2/gstr_2.py @@ -12,6 +12,7 @@ from erpnext.regional.report.gstr_1.gstr_1 import Gstr1Report def execute(filters=None): return Gstr2Report(filters).run() + class Gstr2Report(Gstr1Report): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) @@ -47,7 +48,7 @@ class Gstr2Report(Gstr1Report): for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): invoice_details = self.invoices.get(inv) for rate, items in items_based_on_rate.items(): - if rate or invoice_details.get('gst_category') == 'Registered Composition': + if rate or invoice_details.get("gst_category") == "Registered Composition": if inv not in self.igst_invoices: rate = rate / 2 row, taxable_value = self.get_row_data_for_invoice(inv, invoice_details, rate, items) @@ -60,13 +61,13 @@ class Gstr2Report(Gstr1Report): row += [ self.invoice_cess.get(inv), - invoice_details.get('eligibility_for_itc'), - invoice_details.get('itc_integrated_tax'), - invoice_details.get('itc_central_tax'), - invoice_details.get('itc_state_tax'), - invoice_details.get('itc_cess_amount') + invoice_details.get("eligibility_for_itc"), + invoice_details.get("itc_integrated_tax"), + invoice_details.get("itc_central_tax"), + invoice_details.get("itc_state_tax"), + invoice_details.get("itc_cess_amount"), ] - if self.filters.get("type_of_business") == "CDNR": + if self.filters.get("type_of_business") == "CDNR": row.append("Y" if invoice_details.posting_date <= date(2017, 7, 1) else "N") row.append("C" if invoice_details.return_against else "R") @@ -82,201 +83,158 @@ class Gstr2Report(Gstr1Report): def get_conditions(self): conditions = "" - for opts in (("company", " and company=%(company)s"), + for opts in ( + ("company", " and company=%(company)s"), ("from_date", " and posting_date>=%(from_date)s"), - ("to_date", " and posting_date<=%(to_date)s")): - if self.filters.get(opts[0]): - conditions += opts[1] + ("to_date", " and posting_date<=%(to_date)s"), + ): + if self.filters.get(opts[0]): + conditions += opts[1] - if self.filters.get("type_of_business") == "B2B": + if self.filters.get("type_of_business") == "B2B": conditions += "and ifnull(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ', 'Registered Composition') and is_return != 1 " - elif self.filters.get("type_of_business") == "CDNR": + elif self.filters.get("type_of_business") == "CDNR": conditions += """ and is_return = 1 """ return conditions def get_columns(self): self.tax_columns = [ - { - "fieldname": "rate", - "label": "Rate", - "fieldtype": "Int", - "width": 60 - }, - { - "fieldname": "taxable_value", - "label": "Taxable Value", - "fieldtype": "Currency", - "width": 100 - }, + {"fieldname": "rate", "label": "Rate", "fieldtype": "Int", "width": 60}, + {"fieldname": "taxable_value", "label": "Taxable Value", "fieldtype": "Currency", "width": 100}, { "fieldname": "integrated_tax_paid", "label": "Integrated Tax Paid", "fieldtype": "Currency", - "width": 100 + "width": 100, }, { "fieldname": "central_tax_paid", "label": "Central Tax Paid", "fieldtype": "Currency", - "width": 100 + "width": 100, }, { "fieldname": "state_tax_paid", "label": "State/UT Tax Paid", "fieldtype": "Currency", - "width": 100 - }, - { - "fieldname": "cess_amount", - "label": "Cess Paid", - "fieldtype": "Currency", - "width": 100 + "width": 100, }, + {"fieldname": "cess_amount", "label": "Cess Paid", "fieldtype": "Currency", "width": 100}, { "fieldname": "eligibility_for_itc", "label": "Eligibility For ITC", "fieldtype": "Data", - "width": 100 + "width": 100, }, { "fieldname": "itc_integrated_tax", "label": "Availed ITC Integrated Tax", "fieldtype": "Currency", - "width": 100 + "width": 100, }, { "fieldname": "itc_central_tax", "label": "Availed ITC Central Tax", "fieldtype": "Currency", - "width": 100 + "width": 100, }, { "fieldname": "itc_state_tax", "label": "Availed ITC State/UT Tax", "fieldtype": "Currency", - "width": 100 + "width": 100, }, { "fieldname": "itc_cess_amount", "label": "Availed ITC Cess ", "fieldtype": "Currency", - "width": 100 - } + "width": 100, + }, ] self.other_columns = [] - if self.filters.get("type_of_business") == "B2B": + if self.filters.get("type_of_business") == "B2B": self.invoice_columns = [ { "fieldname": "supplier_gstin", "label": "GSTIN of Supplier", "fieldtype": "Data", - "width": 120 + "width": 120, }, { "fieldname": "invoice_number", "label": "Invoice Number", "fieldtype": "Link", "options": "Purchase Invoice", - "width": 120 - }, - { - "fieldname": "posting_date", - "label": "Invoice date", - "fieldtype": "Date", - "width": 120 + "width": 120, }, + {"fieldname": "posting_date", "label": "Invoice date", "fieldtype": "Date", "width": 120}, { "fieldname": "invoice_value", "label": "Invoice Value", "fieldtype": "Currency", - "width": 120 + "width": 120, }, { "fieldname": "place_of_supply", "label": "Place of Supply", "fieldtype": "Data", - "width": 120 + "width": 120, }, - { - "fieldname": "reverse_charge", - "label": "Reverse Charge", - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "gst_category", - "label": "Invoice Type", - "fieldtype": "Data", - "width": 80 - } + {"fieldname": "reverse_charge", "label": "Reverse Charge", "fieldtype": "Data", "width": 80}, + {"fieldname": "gst_category", "label": "Invoice Type", "fieldtype": "Data", "width": 80}, ] - elif self.filters.get("type_of_business") == "CDNR": + elif self.filters.get("type_of_business") == "CDNR": self.invoice_columns = [ { "fieldname": "supplier_gstin", "label": "GSTIN of Supplier", "fieldtype": "Data", - "width": 120 + "width": 120, }, { "fieldname": "invoice_number", "label": "Note/Refund Voucher Number", "fieldtype": "Link", - "options": "Purchase Invoice" + "options": "Purchase Invoice", }, { "fieldname": "posting_date", "label": "Note/Refund Voucher date", "fieldtype": "Date", - "width": 120 + "width": 120, }, { "fieldname": "return_against", "label": "Invoice/Advance Payment Voucher Number", "fieldtype": "Link", "options": "Purchase Invoice", - "width": 120 + "width": 120, }, { "fieldname": "posting_date", "label": "Invoice/Advance Payment Voucher date", "fieldtype": "Date", - "width": 120 + "width": 120, }, { "fieldname": "reason_for_issuing_document", "label": "Reason For Issuing document", "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "supply_type", - "label": "Supply Type", - "fieldtype": "Data", - "width": 120 + "width": 120, }, + {"fieldname": "supply_type", "label": "Supply Type", "fieldtype": "Data", "width": 120}, { "fieldname": "invoice_value", "label": "Invoice Value", "fieldtype": "Currency", - "width": 120 - } + "width": 120, + }, ] self.other_columns = [ - { - "fieldname": "pre_gst", - "label": "PRE GST", - "fieldtype": "Data", - "width": 50 - }, - { - "fieldname": "document_type", - "label": "Document Type", - "fieldtype": "Data", - "width": 50 - } + {"fieldname": "pre_gst", "label": "PRE GST", "fieldtype": "Data", "width": 50}, + {"fieldname": "document_type", "label": "Document Type", "fieldtype": "Data", "width": 50}, ] self.columns = self.invoice_columns + self.tax_columns + self.other_columns diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py index 1c1335ebe0..09f2df1226 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py @@ -17,8 +17,10 @@ from erpnext.regional.report.gstr_1.gstr_1 import get_company_gstin_number def execute(filters=None): return _execute(filters) + def _execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns() company_currency = erpnext.get_company_currency(filters.company) @@ -46,9 +48,10 @@ def _execute(filters=None): data.append(row) added_item.append((d.parent, d.item_code)) if data: - data = get_merged_data(columns, data) # merge same hsn code data + data = get_merged_data(columns, data) # merge same hsn code data return columns, data + def get_columns(): columns = [ { @@ -56,63 +59,47 @@ def get_columns(): "label": _("HSN/SAC"), "fieldtype": "Link", "options": "GST HSN Code", - "width": 100 - }, - { - "fieldname": "description", - "label": _("Description"), - "fieldtype": "Data", - "width": 300 - }, - { - "fieldname": "stock_uom", - "label": _("Stock UOM"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "stock_qty", - "label": _("Stock Qty"), - "fieldtype": "Float", - "width": 90 - }, - { - "fieldname": "total_amount", - "label": _("Total Amount"), - "fieldtype": "Currency", - "width": 120 + "width": 100, }, + {"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 300}, + {"fieldname": "stock_uom", "label": _("Stock UOM"), "fieldtype": "Data", "width": 100}, + {"fieldname": "stock_qty", "label": _("Stock Qty"), "fieldtype": "Float", "width": 90}, + {"fieldname": "total_amount", "label": _("Total Amount"), "fieldtype": "Currency", "width": 120}, { "fieldname": "taxable_amount", "label": _("Total Taxable Amount"), "fieldtype": "Currency", - "width": 170 - } + "width": 170, + }, ] return columns + def get_conditions(filters): conditions = "" - for opts in (("company", " and company=%(company)s"), + for opts in ( + ("company", " and company=%(company)s"), ("gst_hsn_code", " and gst_hsn_code=%(gst_hsn_code)s"), ("company_gstin", " and company_gstin=%(company_gstin)s"), ("from_date", " and posting_date >= %(from_date)s"), - ("to_date", "and posting_date <= %(to_date)s")): - if filters.get(opts[0]): - conditions += opts[1] + ("to_date", "and posting_date <= %(to_date)s"), + ): + if filters.get(opts[0]): + conditions += opts[1] return conditions + def get_items(filters): conditions = get_conditions(filters) match_conditions = frappe.build_match_conditions("Sales Invoice") if match_conditions: match_conditions = " and {0} ".format(match_conditions) - - items = frappe.db.sql(""" + items = frappe.db.sql( + """ select `tabSales Invoice Item`.gst_hsn_code, `tabSales Invoice Item`.stock_uom, @@ -129,25 +116,41 @@ def get_items(filters): group by `tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code - """ % (conditions, match_conditions), filters, as_dict=1) + """ + % (conditions, match_conditions), + filters, + as_dict=1, + ) return items -def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoice", tax_doctype="Sales Taxes and Charges"): + +def get_tax_accounts( + item_list, + columns, + company_currency, + doctype="Sales Invoice", + tax_doctype="Sales Taxes and Charges", +): item_row_map = {} tax_columns = [] invoice_item_row = {} itemised_tax = {} conditions = "" - tax_amount_precision = get_field_precision(frappe.get_meta(tax_doctype).get_field("tax_amount"), - currency=company_currency) or 2 + tax_amount_precision = ( + get_field_precision( + frappe.get_meta(tax_doctype).get_field("tax_amount"), currency=company_currency + ) + or 2 + ) for d in item_list: invoice_item_row.setdefault(d.parent, []).append(d) item_row_map.setdefault(d.parent, {}).setdefault(d.item_code or d.item_name, []).append(d) - tax_details = frappe.db.sql(""" + tax_details = frappe.db.sql( + """ select parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount @@ -158,8 +161,10 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic and parent in (%s) %s order by description - """ % (tax_doctype, '%s', ', '.join(['%s']*len(invoice_item_row)), conditions), - tuple([doctype] + list(invoice_item_row))) + """ + % (tax_doctype, "%s", ", ".join(["%s"] * len(invoice_item_row)), conditions), + tuple([doctype] + list(invoice_item_row)), + ) for parent, account_head, item_wise_tax_detail, tax_amount in tax_details: @@ -183,74 +188,74 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic for d in item_row_map.get(parent, {}).get(item_code, []): item_tax_amount = tax_amount if item_tax_amount: - itemised_tax.setdefault((parent, item_code), {})[account_head] = frappe._dict({ - "tax_amount": flt(item_tax_amount, tax_amount_precision) - }) + itemised_tax.setdefault((parent, item_code), {})[account_head] = frappe._dict( + {"tax_amount": flt(item_tax_amount, tax_amount_precision)} + ) except ValueError: continue tax_columns.sort() for account_head in tax_columns: - columns.append({ - "label": account_head, - "fieldname": frappe.scrub(account_head), - "fieldtype": "Float", - "width": 110 - }) + columns.append( + { + "label": account_head, + "fieldname": frappe.scrub(account_head), + "fieldtype": "Float", + "width": 110, + } + ) return itemised_tax, tax_columns + def get_merged_data(columns, data): - merged_hsn_dict = {} # to group same hsn under one key and perform row addition + merged_hsn_dict = {} # to group same hsn under one key and perform row addition result = [] for row in data: merged_hsn_dict.setdefault(row[0], {}) for i, d in enumerate(columns): - if d['fieldtype'] not in ('Int', 'Float', 'Currency'): - merged_hsn_dict[row[0]][d['fieldname']] = row[i] + if d["fieldtype"] not in ("Int", "Float", "Currency"): + merged_hsn_dict[row[0]][d["fieldname"]] = row[i] else: - if merged_hsn_dict.get(row[0], {}).get(d['fieldname'], ''): - merged_hsn_dict[row[0]][d['fieldname']] += row[i] + if merged_hsn_dict.get(row[0], {}).get(d["fieldname"], ""): + merged_hsn_dict[row[0]][d["fieldname"]] += row[i] else: - merged_hsn_dict[row[0]][d['fieldname']] = row[i] + merged_hsn_dict[row[0]][d["fieldname"]] = row[i] for key, value in merged_hsn_dict.items(): result.append(value) return result + @frappe.whitelist() def get_json(filters, report_name, data): filters = json.loads(filters) report_data = json.loads(data) - gstin = filters.get('company_gstin') or get_company_gstin_number(filters["company"]) + gstin = filters.get("company_gstin") or get_company_gstin_number(filters["company"]) - if not filters.get('from_date') or not filters.get('to_date'): + if not filters.get("from_date") or not filters.get("to_date"): frappe.throw(_("Please enter From Date and To Date to generate JSON")) fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) - gst_json = {"version": "GST2.3.4", - "hash": "hash", "gstin": gstin, "fp": fp} + gst_json = {"version": "GST2.3.4", "hash": "hash", "gstin": gstin, "fp": fp} - gst_json["hsn"] = { - "data": get_hsn_wise_json_data(filters, report_data) - } + gst_json["hsn"] = {"data": get_hsn_wise_json_data(filters, report_data)} + + return {"report_name": report_name, "data": gst_json} - return { - 'report_name': report_name, - 'data': gst_json - } @frappe.whitelist() def download_json_file(): - '''download json content in a file''' + """download json content in a file""" data = frappe._dict(frappe.local.form_dict) - frappe.response['filename'] = frappe.scrub("{0}".format(data['report_name'])) + '.json' - frappe.response['filecontent'] = data['data'] - frappe.response['content_type'] = 'application/json' - frappe.response['type'] = 'download' + frappe.response["filename"] = frappe.scrub("{0}".format(data["report_name"])) + ".json" + frappe.response["filecontent"] = data["data"] + frappe.response["content_type"] = "application/json" + frappe.response["type"] = "download" + def get_hsn_wise_json_data(filters, report_data): @@ -271,23 +276,22 @@ def get_hsn_wise_json_data(filters, report_data): "iamt": 0.0, "camt": 0.0, "samt": 0.0, - "csamt": 0.0 - + "csamt": 0.0, } - for account in gst_accounts.get('igst_account'): - row['iamt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + for account in gst_accounts.get("igst_account"): + row["iamt"] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) - for account in gst_accounts.get('cgst_account'): - row['camt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + for account in gst_accounts.get("cgst_account"): + row["camt"] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) - for account in gst_accounts.get('sgst_account'): - row['samt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + for account in gst_accounts.get("sgst_account"): + row["samt"] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) - for account in gst_accounts.get('cess_account'): - row['csamt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + for account in gst_accounts.get("cess_account"): + row["csamt"] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) data.append(row) - count +=1 + count += 1 return data diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py index 86dc458bdb..090473f4fd 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py @@ -28,7 +28,7 @@ class TestHSNWiseSummaryReport(TestCase): setup_company() setup_customers() setup_gst_settings() - make_item("Golf Car", properties={ "gst_hsn_code": "999900" }) + make_item("Golf Car", properties={"gst_hsn_code": "999900"}) @classmethod def tearDownClass(cls): @@ -37,53 +37,66 @@ class TestHSNWiseSummaryReport(TestCase): def test_hsn_summary_for_invoice_with_duplicate_items(self): si = create_sales_invoice( company="_Test Company GST", - customer = "_Test GST Customer", - currency = "INR", - warehouse = "Finished Goods - _GST", - debit_to = "Debtors - _GST", - income_account = "Sales - _GST", - expense_account = "Cost of Goods Sold - _GST", - cost_center = "Main - _GST", - do_not_save=1 + customer="_Test GST Customer", + currency="INR", + warehouse="Finished Goods - _GST", + debit_to="Debtors - _GST", + income_account="Sales - _GST", + expense_account="Cost of Goods Sold - _GST", + cost_center="Main - _GST", + do_not_save=1, ) si.items = [] - si.append("items", { - "item_code": "Golf Car", - "gst_hsn_code": "999900", - "qty": "1", - "rate": "120", - "cost_center": "Main - _GST" - }) - si.append("items", { - "item_code": "Golf Car", - "gst_hsn_code": "999900", - "qty": "1", - "rate": "140", - "cost_center": "Main - _GST" - }) - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "Output Tax IGST - _GST", - "cost_center": "Main - _GST", - "description": "IGST @ 18.0", - "rate": 18 - }) + si.append( + "items", + { + "item_code": "Golf Car", + "gst_hsn_code": "999900", + "qty": "1", + "rate": "120", + "cost_center": "Main - _GST", + }, + ) + si.append( + "items", + { + "item_code": "Golf Car", + "gst_hsn_code": "999900", + "qty": "1", + "rate": "140", + "cost_center": "Main - _GST", + }, + ) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "Output Tax IGST - _GST", + "cost_center": "Main - _GST", + "description": "IGST @ 18.0", + "rate": 18, + }, + ) si.posting_date = "2020-11-17" si.submit() si.reload() - [columns, data] = run_report(filters=frappe._dict({ - "company": "_Test Company GST", - "gst_hsn_code": "999900", - "company_gstin": si.company_gstin, - "from_date": si.posting_date, - "to_date": si.posting_date - })) + [columns, data] = run_report( + filters=frappe._dict( + { + "company": "_Test Company GST", + "gst_hsn_code": "999900", + "company_gstin": si.company_gstin, + "from_date": si.posting_date, + "to_date": si.posting_date, + } + ) + ) - filtered_rows = list(filter(lambda row: row['gst_hsn_code'] == "999900", data)) + filtered_rows = list(filter(lambda row: row["gst_hsn_code"] == "999900", data)) self.assertTrue(filtered_rows) hsn_row = filtered_rows[0] - self.assertEquals(hsn_row['stock_qty'], 2.0) - self.assertEquals(hsn_row['total_amount'], 306.8) + self.assertEquals(hsn_row["stock_qty"], 2.0) + self.assertEquals(hsn_row["total_amount"], 306.8) diff --git a/erpnext/regional/report/irs_1099/irs_1099.py b/erpnext/regional/report/irs_1099/irs_1099.py index 147a59fb01..92aeb5ee6f 100644 --- a/erpnext/regional/report/irs_1099/irs_1099.py +++ b/erpnext/regional/report/irs_1099/irs_1099.py @@ -20,22 +20,21 @@ IRS_1099_FORMS_FILE_EXTENSION = ".pdf" def execute(filters=None): 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")) + filters.setdefault("fiscal_year", get_fiscal_year(nowdate())[0]) + filters.setdefault("company", frappe.db.get_default("company")) - region = frappe.db.get_value("Company", - filters={"name": filters.company}, - fieldname=["country"]) + region = frappe.db.get_value("Company", filters={"name": filters.company}, fieldname=["country"]) - if region != 'United States': + if region != "United States": return [], [] columns = get_columns() conditions = "" if filters.supplier_group: - conditions += "AND s.supplier_group = %s" %frappe.db.escape(filters.get("supplier_group")) + conditions += "AND s.supplier_group = %s" % frappe.db.escape(filters.get("supplier_group")) - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT s.supplier_group as "supplier_group", gl.party AS "supplier", @@ -56,10 +55,12 @@ def execute(filters=None): gl.party ORDER BY - gl.party DESC""".format(conditions=conditions), { - "fiscal_year": filters.fiscal_year, - "company": filters.company - }, as_dict=True) + gl.party DESC""".format( + conditions=conditions + ), + {"fiscal_year": filters.fiscal_year, "company": filters.company}, + as_dict=True, + ) return columns, data @@ -71,37 +72,29 @@ def get_columns(): "label": _("Supplier Group"), "fieldtype": "Link", "options": "Supplier Group", - "width": 200 + "width": 200, }, { "fieldname": "supplier", "label": _("Supplier"), "fieldtype": "Link", "options": "Supplier", - "width": 200 + "width": 200, }, - { - "fieldname": "tax_id", - "label": _("Tax ID"), - "fieldtype": "Data", - "width": 200 - }, - { - "fieldname": "payments", - "label": _("Total Payments"), - "fieldtype": "Currency", - "width": 200 - } + {"fieldname": "tax_id", "label": _("Tax ID"), "fieldtype": "Data", "width": 200}, + {"fieldname": "payments", "label": _("Total Payments"), "fieldtype": "Currency", "width": 200}, ] @frappe.whitelist() def irs_1099_print(filters): if not filters: - frappe._dict({ - "company": frappe.db.get_default("Company"), - "fiscal_year": frappe.db.get_default("Fiscal Year") - }) + frappe._dict( + { + "company": frappe.db.get_default("Company"), + "fiscal_year": frappe.db.get_default("Fiscal Year"), + } + ) else: filters = frappe._dict(json.loads(filters)) @@ -121,17 +114,21 @@ def irs_1099_print(filters): 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) + "Supplier", row.supplier + ) row["payments"] = fmt_money(row["payments"], precision=0, currency="USD") pdf = get_pdf(render_template(template, row), output=output if output else None) - frappe.local.response.filename = f"{filters.fiscal_year} {filters.company} IRS 1099 Forms{IRS_1099_FORMS_FILE_EXTENSION}" + 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" def get_payer_address_html(company): - address_list = frappe.db.sql(""" + address_list = frappe.db.sql( + """ SELECT name FROM @@ -141,7 +138,10 @@ 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: @@ -152,7 +152,8 @@ def get_payer_address_html(company): def get_street_address_html(party_type, party): - address_list = frappe.db.sql(""" + address_list = frappe.db.sql( + """ SELECT link.parent FROM @@ -165,7 +166,10 @@ def get_street_address_html(party_type, party): 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: diff --git a/erpnext/regional/report/ksa_vat/ksa_vat.py b/erpnext/regional/report/ksa_vat/ksa_vat.py index cc26bd7a57..15996d2d1f 100644 --- a/erpnext/regional/report/ksa_vat/ksa_vat.py +++ b/erpnext/regional/report/ksa_vat/ksa_vat.py @@ -14,6 +14,7 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_columns(): return [ { @@ -48,101 +49,136 @@ def get_columns(): "label": _("Currency"), "fieldtype": "Currency", "width": 150, - "hidden": 1 - } + "hidden": 1, + }, ] + def get_data(filters): data = [] # Validate if vat settings exist - company = filters.get('company') - company_currency = frappe.get_cached_value('Company', company, "default_currency") + company = filters.get("company") + company_currency = frappe.get_cached_value("Company", company, "default_currency") - if frappe.db.exists('KSA VAT Setting', company) is None: - url = get_url_to_list('KSA VAT Setting') + if frappe.db.exists("KSA VAT Setting", company) is None: + url = get_url_to_list("KSA VAT Setting") frappe.msgprint(_('Create KSA VAT Setting for this company').format(url)) return data - ksa_vat_setting = frappe.get_doc('KSA VAT Setting', company) + ksa_vat_setting = frappe.get_doc("KSA VAT Setting", company) # Sales Heading - append_data(data, 'VAT on Sales', '', '', '', company_currency) + append_data(data, "VAT on Sales", "", "", "", company_currency) grand_total_taxable_amount = 0 grand_total_taxable_adjustment_amount = 0 grand_total_tax = 0 for vat_setting in ksa_vat_setting.ksa_vat_sales_accounts: - total_taxable_amount, total_taxable_adjustment_amount, \ - total_tax = get_tax_data_for_each_vat_setting(vat_setting, filters, 'Sales Invoice') + ( + total_taxable_amount, + total_taxable_adjustment_amount, + total_tax, + ) = get_tax_data_for_each_vat_setting(vat_setting, filters, "Sales Invoice") # Adding results to data - append_data(data, vat_setting.title, total_taxable_amount, - total_taxable_adjustment_amount, total_tax, company_currency) + append_data( + data, + vat_setting.title, + total_taxable_amount, + total_taxable_adjustment_amount, + total_tax, + company_currency, + ) grand_total_taxable_amount += total_taxable_amount grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount grand_total_tax += total_tax # Sales Grand Total - append_data(data, 'Grand Total', grand_total_taxable_amount, - grand_total_taxable_adjustment_amount, grand_total_tax, company_currency) + append_data( + data, + "Grand Total", + grand_total_taxable_amount, + grand_total_taxable_adjustment_amount, + grand_total_tax, + company_currency, + ) # Blank Line - append_data(data, '', '', '', '', company_currency) + append_data(data, "", "", "", "", company_currency) # Purchase Heading - append_data(data, 'VAT on Purchases', '', '', '', company_currency) + append_data(data, "VAT on Purchases", "", "", "", company_currency) grand_total_taxable_amount = 0 grand_total_taxable_adjustment_amount = 0 grand_total_tax = 0 for vat_setting in ksa_vat_setting.ksa_vat_purchase_accounts: - total_taxable_amount, total_taxable_adjustment_amount, \ - total_tax = get_tax_data_for_each_vat_setting(vat_setting, filters, 'Purchase Invoice') + ( + total_taxable_amount, + total_taxable_adjustment_amount, + total_tax, + ) = get_tax_data_for_each_vat_setting(vat_setting, filters, "Purchase Invoice") # Adding results to data - append_data(data, vat_setting.title, total_taxable_amount, - total_taxable_adjustment_amount, total_tax, company_currency) + append_data( + data, + vat_setting.title, + total_taxable_amount, + total_taxable_adjustment_amount, + total_tax, + company_currency, + ) grand_total_taxable_amount += total_taxable_amount grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount grand_total_tax += total_tax # Purchase Grand Total - append_data(data, 'Grand Total', grand_total_taxable_amount, - grand_total_taxable_adjustment_amount, grand_total_tax, company_currency) + append_data( + data, + "Grand Total", + grand_total_taxable_amount, + grand_total_taxable_adjustment_amount, + grand_total_tax, + company_currency, + ) return data + def get_tax_data_for_each_vat_setting(vat_setting, filters, doctype): - ''' + """ (KSA, {filters}, 'Sales Invoice') => 500, 153, 10 \n calculates and returns \n - total_taxable_amount, total_taxable_adjustment_amount, total_tax''' - from_date = filters.get('from_date') - to_date = filters.get('to_date') + total_taxable_amount, total_taxable_adjustment_amount, total_tax""" + from_date = filters.get("from_date") + to_date = filters.get("to_date") # Initiate variables total_taxable_amount = 0 total_taxable_adjustment_amount = 0 total_tax = 0 # Fetch All Invoices - invoices = frappe.get_all(doctype, - filters ={ - 'docstatus': 1, - 'posting_date': ['between', [from_date, to_date]] - }, fields =['name', 'is_return']) + invoices = frappe.get_all( + doctype, + filters={"docstatus": 1, "posting_date": ["between", [from_date, to_date]]}, + fields=["name", "is_return"], + ) for invoice in invoices: - invoice_items = frappe.get_all(f'{doctype} Item', - filters ={ - 'docstatus': 1, - 'parent': invoice.name, - 'item_tax_template': vat_setting.item_tax_template - }, fields =['item_code', 'net_amount']) + invoice_items = frappe.get_all( + f"{doctype} Item", + filters={ + "docstatus": 1, + "parent": invoice.name, + "item_tax_template": vat_setting.item_tax_template, + }, + fields=["item_code", "net_amount"], + ) for item in invoice_items: # Summing up total taxable amount @@ -158,24 +194,31 @@ def get_tax_data_for_each_vat_setting(vat_setting, filters, doctype): return total_taxable_amount, total_taxable_adjustment_amount, total_tax - def append_data(data, title, amount, adjustment_amount, vat_amount, company_currency): """Returns data with appended value.""" - data.append({"title": _(title), "amount": amount, "adjustment_amount": adjustment_amount, "vat_amount": vat_amount, - "currency": company_currency}) + data.append( + { + "title": _(title), + "amount": amount, + "adjustment_amount": adjustment_amount, + "vat_amount": vat_amount, + "currency": company_currency, + } + ) + def get_tax_amount(item_code, account_head, doctype, parent): - if doctype == 'Sales Invoice': - tax_doctype = 'Sales Taxes and Charges' + if doctype == "Sales Invoice": + tax_doctype = "Sales Taxes and Charges" - elif doctype == 'Purchase Invoice': - tax_doctype = 'Purchase Taxes and Charges' + elif doctype == "Purchase Invoice": + tax_doctype = "Purchase Taxes and Charges" - item_wise_tax_detail = frappe.get_value(tax_doctype, { - 'docstatus': 1, - 'parent': parent, - 'account_head': account_head - }, 'item_wise_tax_detail') + item_wise_tax_detail = frappe.get_value( + tax_doctype, + {"docstatus": 1, "parent": parent, "account_head": account_head}, + "item_wise_tax_detail", + ) tax_amount = 0 if item_wise_tax_detail and len(item_wise_tax_detail) > 0: diff --git a/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.py b/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.py index def4379828..17a62d5e5d 100644 --- a/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.py +++ b/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.py @@ -16,6 +16,7 @@ def execute(filters=None): return columns, data + def get_columns(filters): columns = [ { @@ -23,53 +24,54 @@ def get_columns(filters): "options": "Employee", "fieldname": "employee", "fieldtype": "Link", - "width": 200 + "width": 200, }, { "label": _("Employee Name"), "options": "Employee", "fieldname": "employee_name", "fieldtype": "Link", - "width": 160 + "width": 160, }, - { - "label": _("Amount"), - "fieldname": "amount", - "fieldtype": "Currency", - "width": 140 - } + {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 140}, ] return columns + def get_data(filters): data = [] - component_type_dict = frappe._dict(frappe.db.sql(""" select name, component_type from `tabSalary Component` - where component_type = 'Professional Tax' """)) + component_type_dict = frappe._dict( + frappe.db.sql( + """ select name, component_type from `tabSalary Component` + where component_type = 'Professional Tax' """ + ) + ) if not len(component_type_dict): return [] conditions = get_conditions(filters) - entry = frappe.db.sql(""" select sal.employee, sal.employee_name, ded.salary_component, ded.amount + entry = frappe.db.sql( + """ select sal.employee, sal.employee_name, ded.salary_component, ded.amount from `tabSalary Slip` sal, `tabSalary Detail` ded where sal.name = ded.parent and ded.parentfield = 'deductions' and ded.parenttype = 'Salary Slip' and sal.docstatus = 1 %s and ded.salary_component in (%s) - """ % (conditions , ", ".join(['%s']*len(component_type_dict))), tuple(component_type_dict.keys()), as_dict=1) + """ + % (conditions, ", ".join(["%s"] * len(component_type_dict))), + tuple(component_type_dict.keys()), + as_dict=1, + ) for d in entry: - employee = { - "employee": d.employee, - "employee_name": d.employee_name, - "amount": d.amount - } + employee = {"employee": d.employee, "employee_name": d.employee_name, "amount": d.amount} data.append(employee) diff --git a/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.py b/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.py index 190f408fe0..ab4b6e73b8 100644 --- a/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.py +++ b/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.py @@ -13,6 +13,7 @@ def execute(filters=None): return columns, data + def get_columns(filters): columns = [ { @@ -20,57 +21,38 @@ def get_columns(filters): "options": "Employee", "fieldname": "employee", "fieldtype": "Link", - "width": 200 + "width": 200, }, { "label": _("Employee Name"), "options": "Employee", "fieldname": "employee_name", "fieldtype": "Link", - "width": 160 - }, - { - "label": _("PF Account"), - "fieldname": "pf_account", - "fieldtype": "Data", - "width": 140 - }, - { - "label": _("PF Amount"), - "fieldname": "pf_amount", - "fieldtype": "Currency", - "width": 140 + "width": 160, }, + {"label": _("PF Account"), "fieldname": "pf_account", "fieldtype": "Data", "width": 140}, + {"label": _("PF Amount"), "fieldname": "pf_amount", "fieldtype": "Currency", "width": 140}, { "label": _("Additional PF"), "fieldname": "additional_pf", "fieldtype": "Currency", - "width": 140 + "width": 140, }, - { - "label": _("PF Loan"), - "fieldname": "pf_loan", - "fieldtype": "Currency", - "width": 140 - }, - { - "label": _("Total"), - "fieldname": "total", - "fieldtype": "Currency", - "width": 140 - } + {"label": _("PF Loan"), "fieldname": "pf_loan", "fieldtype": "Currency", "width": 140}, + {"label": _("Total"), "fieldname": "total", "fieldtype": "Currency", "width": 140}, ] return columns + def get_conditions(filters): conditions = [""] if filters.get("department"): - conditions.append("sal.department = '%s' " % (filters["department"]) ) + conditions.append("sal.department = '%s' " % (filters["department"])) if filters.get("branch"): - conditions.append("sal.branch = '%s' " % (filters["branch"]) ) + conditions.append("sal.branch = '%s' " % (filters["branch"])) if filters.get("company"): conditions.append("sal.company = '%s' " % (filters["company"])) @@ -86,10 +68,13 @@ def get_conditions(filters): return " and ".join(conditions) -def prepare_data(entry,component_type_dict): + +def prepare_data(entry, component_type_dict): data_list = {} - employee_account_dict = frappe._dict(frappe.db.sql(""" select name, provident_fund_account from `tabEmployee`""")) + employee_account_dict = frappe._dict( + frappe.db.sql(""" select name, provident_fund_account from `tabEmployee`""") + ) for d in entry: @@ -98,40 +83,57 @@ def prepare_data(entry,component_type_dict): if data_list.get(d.name): data_list[d.name][component_type] = d.amount else: - data_list.setdefault(d.name,{ - "employee": d.employee, - "employee_name": d.employee_name, - "pf_account": employee_account_dict.get(d.employee), - component_type: d.amount - }) + data_list.setdefault( + d.name, + { + "employee": d.employee, + "employee_name": d.employee_name, + "pf_account": employee_account_dict.get(d.employee), + component_type: d.amount, + }, + ) return data_list + def get_data(filters): data = [] conditions = get_conditions(filters) - salary_slips = frappe.db.sql(""" select sal.name from `tabSalary Slip` sal + salary_slips = frappe.db.sql( + """ select sal.name from `tabSalary Slip` sal where docstatus = 1 %s - """ % (conditions), as_dict=1) + """ + % (conditions), + as_dict=1, + ) - component_type_dict = frappe._dict(frappe.db.sql(""" select name, component_type from `tabSalary Component` - where component_type in ('Provident Fund', 'Additional Provident Fund', 'Provident Fund Loan')""")) + component_type_dict = frappe._dict( + frappe.db.sql( + """ select name, component_type from `tabSalary Component` + where component_type in ('Provident Fund', 'Additional Provident Fund', 'Provident Fund Loan')""" + ) + ) if not len(component_type_dict): return [] - entry = frappe.db.sql(""" select sal.name, sal.employee, sal.employee_name, ded.salary_component, ded.amount + entry = frappe.db.sql( + """ select sal.name, sal.employee, sal.employee_name, ded.salary_component, ded.amount from `tabSalary Slip` sal, `tabSalary Detail` ded where sal.name = ded.parent and ded.parentfield = 'deductions' and ded.parenttype = 'Salary Slip' and sal.docstatus = 1 %s and ded.salary_component in (%s) - """ % (conditions, ", ".join(['%s']*len(component_type_dict))), tuple(component_type_dict.keys()), as_dict=1) + """ + % (conditions, ", ".join(["%s"] * len(component_type_dict))), + tuple(component_type_dict.keys()), + as_dict=1, + ) - data_list = prepare_data(entry,component_type_dict) + data_list = prepare_data(entry, component_type_dict) for d in salary_slips: total = 0 @@ -139,7 +141,7 @@ def get_data(filters): employee = { "employee": data_list.get(d.name).get("employee"), "employee_name": data_list.get(d.name).get("employee_name"), - "pf_account": data_list.get(d.name).get("pf_account") + "pf_account": data_list.get(d.name).get("pf_account"), } if data_list.get(d.name).get("Provident Fund"): @@ -160,9 +162,12 @@ def get_data(filters): return data + @frappe.whitelist() def get_years(): - year_list = frappe.db.sql_list("""select distinct YEAR(end_date) from `tabSalary Slip` ORDER BY YEAR(end_date) DESC""") + year_list = frappe.db.sql_list( + """select distinct YEAR(end_date) from `tabSalary Slip` ORDER BY YEAR(end_date) DESC""" + ) if not year_list: year_list = [getdate().year] diff --git a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py index 464939f39e..2966b0657a 100644 --- a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py @@ -18,6 +18,7 @@ from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse_account test_dependencies = ["Territory", "Customer Group", "Supplier Group", "Item"] + class TestUaeVat201(TestCase): def setUp(self): frappe.set_user("Administrator") @@ -34,9 +35,9 @@ class TestUaeVat201(TestCase): create_warehouse("_Test UAE VAT Supplier Warehouse", company="_Test Company UAE VAT") - make_item("_Test UAE VAT Item", properties = {"is_zero_rated": 0, "is_exempt": 0}) - make_item("_Test UAE VAT Zero Rated Item", properties = {"is_zero_rated": 1, "is_exempt": 0}) - make_item("_Test UAE VAT Exempt Item", properties = {"is_zero_rated": 0, "is_exempt": 1}) + make_item("_Test UAE VAT Item", properties={"is_zero_rated": 0, "is_exempt": 0}) + make_item("_Test UAE VAT Zero Rated Item", properties={"is_zero_rated": 1, "is_exempt": 0}) + make_item("_Test UAE VAT Exempt Item", properties={"is_zero_rated": 0, "is_exempt": 1}) make_sales_invoices() @@ -52,27 +53,30 @@ class TestUaeVat201(TestCase): "raw_amount": amount, "raw_vat_amount": vat, } - self.assertEqual(amounts_by_emirate["Sharjah"]["raw_amount"],100) - self.assertEqual(amounts_by_emirate["Sharjah"]["raw_vat_amount"],5) - self.assertEqual(amounts_by_emirate["Dubai"]["raw_amount"],200) - self.assertEqual(amounts_by_emirate["Dubai"]["raw_vat_amount"],10) - self.assertEqual(get_tourist_tax_return_total(filters),100) - self.assertEqual(get_tourist_tax_return_tax(filters),2) - self.assertEqual(get_zero_rated_total(filters),100) - self.assertEqual(get_exempt_total(filters),100) - self.assertEqual(get_standard_rated_expenses_total(filters),250) - self.assertEqual(get_standard_rated_expenses_tax(filters),1) + self.assertEqual(amounts_by_emirate["Sharjah"]["raw_amount"], 100) + self.assertEqual(amounts_by_emirate["Sharjah"]["raw_vat_amount"], 5) + self.assertEqual(amounts_by_emirate["Dubai"]["raw_amount"], 200) + self.assertEqual(amounts_by_emirate["Dubai"]["raw_vat_amount"], 10) + self.assertEqual(get_tourist_tax_return_total(filters), 100) + self.assertEqual(get_tourist_tax_return_tax(filters), 2) + self.assertEqual(get_zero_rated_total(filters), 100) + self.assertEqual(get_exempt_total(filters), 100) + self.assertEqual(get_standard_rated_expenses_total(filters), 250) + self.assertEqual(get_standard_rated_expenses_tax(filters), 1) + def make_company(company_name, abbr): if not frappe.db.exists("Company", company_name): - company = frappe.get_doc({ - "doctype": "Company", - "company_name": company_name, - "abbr": abbr, - "default_currency": "AED", - "country": "United Arab Emirates", - "create_chart_of_accounts_based_on": "Standard Template", - }) + company = frappe.get_doc( + { + "doctype": "Company", + "company_name": company_name, + "abbr": abbr, + "default_currency": "AED", + "country": "United Arab Emirates", + "create_chart_of_accounts_based_on": "Standard Template", + } + ) company.insert() else: company = frappe.get_doc("Company", company_name) @@ -85,49 +89,51 @@ def make_company(company_name, abbr): company.save() return company + def set_vat_accounts(): if not frappe.db.exists("UAE VAT Settings", "_Test Company UAE VAT"): vat_accounts = frappe.get_all( "Account", fields=["name"], - filters = { - "company": "_Test Company UAE VAT", - "is_group": 0, - "account_type": "Tax" - } + filters={"company": "_Test Company UAE VAT", "is_group": 0, "account_type": "Tax"}, ) uae_vat_accounts = [] for account in vat_accounts: - uae_vat_accounts.append({ - "doctype": "UAE VAT Account", - "account": account.name - }) + uae_vat_accounts.append({"doctype": "UAE VAT Account", "account": account.name}) + + frappe.get_doc( + { + "company": "_Test Company UAE VAT", + "uae_vat_accounts": uae_vat_accounts, + "doctype": "UAE VAT Settings", + } + ).insert() - frappe.get_doc({ - "company": "_Test Company UAE VAT", - "uae_vat_accounts": uae_vat_accounts, - "doctype": "UAE VAT Settings", - }).insert() def make_customer(): if not frappe.db.exists("Customer", "_Test UAE Customer"): - customer = frappe.get_doc({ - "doctype": "Customer", - "customer_name": "_Test UAE Customer", - "customer_type": "Company", - }) + customer = frappe.get_doc( + { + "doctype": "Customer", + "customer_name": "_Test UAE Customer", + "customer_type": "Company", + } + ) customer.insert() def make_supplier(): if not frappe.db.exists("Supplier", "_Test UAE Supplier"): - frappe.get_doc({ - "supplier_group": "Local", - "supplier_name": "_Test UAE Supplier", - "supplier_type": "Individual", - "doctype": "Supplier", - }).insert() + frappe.get_doc( + { + "supplier_group": "Local", + "supplier_name": "_Test UAE Supplier", + "supplier_type": "Individual", + "doctype": "Supplier", + } + ).insert() + def create_warehouse(warehouse_name, properties=None, company=None): if not company: @@ -147,17 +153,20 @@ def create_warehouse(warehouse_name, properties=None, company=None): else: return warehouse_id + def make_item(item_code, properties=None): if frappe.db.exists("Item", item_code): return frappe.get_doc("Item", item_code) - item = frappe.get_doc({ - "doctype": "Item", - "item_code": item_code, - "item_name": item_code, - "description": item_code, - "item_group": "Products" - }) + item = frappe.get_doc( + { + "doctype": "Item", + "item_code": item_code, + "item_name": item_code, + "description": item_code, + "item_group": "Products", + } + ) if properties: item.update(properties) @@ -166,71 +175,77 @@ def make_item(item_code, properties=None): return item + def make_sales_invoices(): - def make_sales_invoices_wrapper(emirate, item, tax = True, tourist_tax= False): + def make_sales_invoices_wrapper(emirate, item, tax=True, tourist_tax=False): si = create_sales_invoice( company="_Test Company UAE VAT", - customer = '_Test UAE Customer', - currency = 'AED', - warehouse = 'Finished Goods - _TCUV', - debit_to = 'Debtors - _TCUV', - income_account = 'Sales - _TCUV', - expense_account = 'Cost of Goods Sold - _TCUV', - cost_center = 'Main - _TCUV', - item = item, - do_not_save=1 + customer="_Test UAE Customer", + currency="AED", + warehouse="Finished Goods - _TCUV", + debit_to="Debtors - _TCUV", + income_account="Sales - _TCUV", + expense_account="Cost of Goods Sold - _TCUV", + cost_center="Main - _TCUV", + item=item, + do_not_save=1, ) si.vat_emirate = emirate if tax: si.append( - "taxes", { + "taxes", + { "charge_type": "On Net Total", "account_head": "VAT 5% - _TCUV", "cost_center": "Main - _TCUV", "description": "VAT 5% @ 5.0", - "rate": 5.0 - } + "rate": 5.0, + }, ) if tourist_tax: si.tourist_tax_return = 2 si.submit() - #Define Item Names + # Define Item Names uae_item = "_Test UAE VAT Item" uae_exempt_item = "_Test UAE VAT Exempt Item" uae_zero_rated_item = "_Test UAE VAT Zero Rated Item" - #Sales Invoice with standard rated expense in Dubai - make_sales_invoices_wrapper('Dubai', uae_item) - #Sales Invoice with standard rated expense in Sharjah - make_sales_invoices_wrapper('Sharjah', uae_item) - #Sales Invoice with Tourist Tax Return - make_sales_invoices_wrapper('Dubai', uae_item, True, True) - #Sales Invoice with Exempt Item - make_sales_invoices_wrapper('Sharjah', uae_exempt_item, False) - #Sales Invoice with Zero Rated Item - make_sales_invoices_wrapper('Sharjah', uae_zero_rated_item, False) + # Sales Invoice with standard rated expense in Dubai + make_sales_invoices_wrapper("Dubai", uae_item) + # Sales Invoice with standard rated expense in Sharjah + make_sales_invoices_wrapper("Sharjah", uae_item) + # Sales Invoice with Tourist Tax Return + make_sales_invoices_wrapper("Dubai", uae_item, True, True) + # Sales Invoice with Exempt Item + make_sales_invoices_wrapper("Sharjah", uae_exempt_item, False) + # Sales Invoice with Zero Rated Item + make_sales_invoices_wrapper("Sharjah", uae_zero_rated_item, False) + def create_purchase_invoices(): pi = make_purchase_invoice( company="_Test Company UAE VAT", - supplier = '_Test UAE Supplier', - supplier_warehouse = '_Test UAE VAT Supplier Warehouse - _TCUV', - warehouse = '_Test UAE VAT Supplier Warehouse - _TCUV', - currency = 'AED', - cost_center = 'Main - _TCUV', - expense_account = 'Cost of Goods Sold - _TCUV', - item = "_Test UAE VAT Item", + supplier="_Test UAE Supplier", + supplier_warehouse="_Test UAE VAT Supplier Warehouse - _TCUV", + warehouse="_Test UAE VAT Supplier Warehouse - _TCUV", + currency="AED", + cost_center="Main - _TCUV", + expense_account="Cost of Goods Sold - _TCUV", + item="_Test UAE VAT Item", do_not_save=1, - uom = "Nos" + uom="Nos", + ) + pi.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "VAT 5% - _TCUV", + "cost_center": "Main - _TCUV", + "description": "VAT 5% @ 5.0", + "rate": 5.0, + }, ) - pi.append("taxes", { - "charge_type": "On Net Total", - "account_head": "VAT 5% - _TCUV", - "cost_center": "Main - _TCUV", - "description": "VAT 5% @ 5.0", - "rate": 5.0 - }) pi.recoverable_standard_rated_expenses = 1 diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index f8379aa17a..59ef58bfde 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -11,21 +11,12 @@ def execute(filters=None): data, emirates, amounts_by_emirate = get_data(filters) return columns, data + def get_columns(): """Creates a list of dictionaries that are used to generate column headers of the data table.""" return [ - { - "fieldname": "no", - "label": _("No"), - "fieldtype": "Data", - "width": 50 - }, - { - "fieldname": "legend", - "label": _("Legend"), - "fieldtype": "Data", - "width": 300 - }, + {"fieldname": "no", "label": _("No"), "fieldtype": "Data", "width": 50}, + {"fieldname": "legend", "label": _("Legend"), "fieldtype": "Data", "width": 300}, { "fieldname": "amount", "label": _("Amount (AED)"), @@ -37,41 +28,53 @@ def get_columns(): "label": _("VAT Amount (AED)"), "fieldtype": "Currency", "width": 150, - } + }, ] -def get_data(filters = None): + +def get_data(filters=None): """Returns the list of dictionaries. Each dictionary is a row in the datatable and chart data.""" data = [] emirates, amounts_by_emirate = append_vat_on_sales(data, filters) append_vat_on_expenses(data, filters) return data, emirates, amounts_by_emirate + def append_vat_on_sales(data, filters): """Appends Sales and All Other Outputs.""" - append_data(data, '', _('VAT on Sales and All Other Outputs'), '', '') + append_data(data, "", _("VAT on Sales and All Other Outputs"), "", "") emirates, amounts_by_emirate = standard_rated_expenses_emiratewise(data, filters) - append_data(data, '2', - _('Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme'), - frappe.format((-1) * get_tourist_tax_return_total(filters), 'Currency'), - frappe.format((-1) * get_tourist_tax_return_tax(filters), 'Currency')) + append_data( + data, + "2", + _("Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme"), + frappe.format((-1) * get_tourist_tax_return_total(filters), "Currency"), + frappe.format((-1) * get_tourist_tax_return_tax(filters), "Currency"), + ) - append_data(data, '3', _('Supplies subject to the reverse charge provision'), - frappe.format(get_reverse_charge_total(filters), 'Currency'), - frappe.format(get_reverse_charge_tax(filters), 'Currency')) + append_data( + data, + "3", + _("Supplies subject to the reverse charge provision"), + frappe.format(get_reverse_charge_total(filters), "Currency"), + frappe.format(get_reverse_charge_tax(filters), "Currency"), + ) - append_data(data, '4', _('Zero Rated'), - frappe.format(get_zero_rated_total(filters), 'Currency'), "-") + append_data( + data, "4", _("Zero Rated"), frappe.format(get_zero_rated_total(filters), "Currency"), "-" + ) - append_data(data, '5', _('Exempt Supplies'), - frappe.format(get_exempt_total(filters), 'Currency'),"-") + append_data( + data, "5", _("Exempt Supplies"), frappe.format(get_exempt_total(filters), "Currency"), "-" + ) - append_data(data, '', '', '', '') + append_data(data, "", "", "", "") return emirates, amounts_by_emirate + def standard_rated_expenses_emiratewise(data, filters): """Append emiratewise standard rated expenses and vat.""" total_emiratewise = get_total_emiratewise(filters) @@ -82,44 +85,61 @@ def standard_rated_expenses_emiratewise(data, filters): "legend": emirate, "raw_amount": amount, "raw_vat_amount": vat, - "amount": frappe.format(amount, 'Currency'), - "vat_amount": frappe.format(vat, 'Currency'), + "amount": frappe.format(amount, "Currency"), + "vat_amount": frappe.format(vat, "Currency"), } amounts_by_emirate = append_emiratewise_expenses(data, emirates, amounts_by_emirate) return emirates, amounts_by_emirate + def append_emiratewise_expenses(data, emirates, amounts_by_emirate): """Append emiratewise standard rated expenses and vat.""" for no, emirate in enumerate(emirates, 97): if emirate in amounts_by_emirate: - amounts_by_emirate[emirate]["no"] = _('1{0}').format(chr(no)) - amounts_by_emirate[emirate]["legend"] = _('Standard rated supplies in {0}').format(emirate) + amounts_by_emirate[emirate]["no"] = _("1{0}").format(chr(no)) + amounts_by_emirate[emirate]["legend"] = _("Standard rated supplies in {0}").format(emirate) data.append(amounts_by_emirate[emirate]) else: - append_data(data, _('1{0}').format(chr(no)), - _('Standard rated supplies in {0}').format(emirate), - frappe.format(0, 'Currency'), frappe.format(0, 'Currency')) + append_data( + data, + _("1{0}").format(chr(no)), + _("Standard rated supplies in {0}").format(emirate), + frappe.format(0, "Currency"), + frappe.format(0, "Currency"), + ) return amounts_by_emirate + def append_vat_on_expenses(data, filters): """Appends Expenses and All Other Inputs.""" - append_data(data, '', _('VAT on Expenses and All Other Inputs'), '', '') - append_data(data, '9', _('Standard Rated Expenses'), - frappe.format(get_standard_rated_expenses_total(filters), 'Currency'), - frappe.format(get_standard_rated_expenses_tax(filters), 'Currency')) - append_data(data, '10', _('Supplies subject to the reverse charge provision'), - frappe.format(get_reverse_charge_recoverable_total(filters), 'Currency'), - frappe.format(get_reverse_charge_recoverable_tax(filters), 'Currency')) + append_data(data, "", _("VAT on Expenses and All Other Inputs"), "", "") + append_data( + data, + "9", + _("Standard Rated Expenses"), + frappe.format(get_standard_rated_expenses_total(filters), "Currency"), + frappe.format(get_standard_rated_expenses_tax(filters), "Currency"), + ) + append_data( + data, + "10", + _("Supplies subject to the reverse charge provision"), + frappe.format(get_reverse_charge_recoverable_total(filters), "Currency"), + frappe.format(get_reverse_charge_recoverable_tax(filters), "Currency"), + ) + def append_data(data, no, legend, amount, vat_amount): """Returns data with appended value.""" - data.append({"no": no, "legend":legend, "amount": amount, "vat_amount": vat_amount}) + data.append({"no": no, "legend": legend, "amount": amount, "vat_amount": vat_amount}) + def get_total_emiratewise(filters): """Returns Emiratewise Amount and Taxes.""" conditions = get_conditions(filters) try: - return frappe.db.sql(""" + return frappe.db.sql( + """ select s.vat_emirate as emirate, sum(i.base_amount) as total, sum(i.tax_amount) from @@ -131,52 +151,54 @@ def get_total_emiratewise(filters): {where_conditions} group by s.vat_emirate; - """.format(where_conditions=conditions), filters) + """.format( + where_conditions=conditions + ), + filters, + ) except (IndexError, TypeError): return 0 + def get_emirates(): """Returns a List of emirates in the order that they are to be displayed.""" - return [ - 'Abu Dhabi', - 'Dubai', - 'Sharjah', - 'Ajman', - 'Umm Al Quwain', - 'Ras Al Khaimah', - 'Fujairah' - ] + return ["Abu Dhabi", "Dubai", "Sharjah", "Ajman", "Umm Al Quwain", "Ras Al Khaimah", "Fujairah"] + def get_filters(filters): """The conditions to be used to filter data to calculate the total sale.""" query_filters = [] if filters.get("company"): - query_filters.append(["company", '=', filters['company']]) + query_filters.append(["company", "=", filters["company"]]) if filters.get("from_date"): - query_filters.append(["posting_date", '>=', filters['from_date']]) + query_filters.append(["posting_date", ">=", filters["from_date"]]) if filters.get("from_date"): - query_filters.append(["posting_date", '<=', filters['to_date']]) + query_filters.append(["posting_date", "<=", filters["to_date"]]) return query_filters + def get_reverse_charge_total(filters): """Returns the sum of the total of each Purchase invoice made.""" query_filters = get_filters(filters) - query_filters.append(['reverse_charge', '=', 'Y']) - query_filters.append(['docstatus', '=', 1]) + query_filters.append(["reverse_charge", "=", "Y"]) + query_filters.append(["docstatus", "=", 1]) try: - return frappe.db.get_all('Purchase Invoice', - filters = query_filters, - fields = ['sum(total)'], - as_list=True, - limit = 1 - )[0][0] or 0 + return ( + frappe.db.get_all( + "Purchase Invoice", filters=query_filters, fields=["sum(total)"], as_list=True, limit=1 + )[0][0] + or 0 + ) except (IndexError, TypeError): return 0 + def get_reverse_charge_tax(filters): """Returns the sum of the tax of each Purchase invoice made.""" conditions = get_conditions_join(filters) - return frappe.db.sql(""" + return ( + frappe.db.sql( + """ select sum(debit) from `tabPurchase Invoice` p inner join `tabGL Entry` gl on @@ -187,28 +209,38 @@ def get_reverse_charge_tax(filters): and gl.docstatus = 1 and account in (select account from `tabUAE VAT Account` where parent=%(company)s) {where_conditions} ; - """.format(where_conditions=conditions), filters)[0][0] or 0 + """.format( + where_conditions=conditions + ), + filters, + )[0][0] + or 0 + ) + def get_reverse_charge_recoverable_total(filters): """Returns the sum of the total of each Purchase invoice made with recoverable reverse charge.""" query_filters = get_filters(filters) - query_filters.append(['reverse_charge', '=', 'Y']) - query_filters.append(['recoverable_reverse_charge', '>', '0']) - query_filters.append(['docstatus', '=', 1]) + query_filters.append(["reverse_charge", "=", "Y"]) + query_filters.append(["recoverable_reverse_charge", ">", "0"]) + query_filters.append(["docstatus", "=", 1]) try: - return frappe.db.get_all('Purchase Invoice', - filters = query_filters, - fields = ['sum(total)'], - as_list=True, - limit = 1 - )[0][0] or 0 + return ( + frappe.db.get_all( + "Purchase Invoice", filters=query_filters, fields=["sum(total)"], as_list=True, limit=1 + )[0][0] + or 0 + ) except (IndexError, TypeError): return 0 + def get_reverse_charge_recoverable_tax(filters): """Returns the sum of the tax of each Purchase invoice made.""" conditions = get_conditions_join(filters) - return frappe.db.sql(""" + return ( + frappe.db.sql( + """ select sum(debit * p.recoverable_reverse_charge / 100) from @@ -222,83 +254,107 @@ def get_reverse_charge_recoverable_tax(filters): and gl.docstatus = 1 and account in (select account from `tabUAE VAT Account` where parent=%(company)s) {where_conditions} ; - """.format(where_conditions=conditions), filters)[0][0] or 0 + """.format( + where_conditions=conditions + ), + filters, + )[0][0] + or 0 + ) + def get_conditions_join(filters): """The conditions to be used to filter data to calculate the total vat.""" conditions = "" - for opts in (("company", " and p.company=%(company)s"), + for opts in ( + ("company", " and p.company=%(company)s"), ("from_date", " and p.posting_date>=%(from_date)s"), - ("to_date", " and p.posting_date<=%(to_date)s")): + ("to_date", " and p.posting_date<=%(to_date)s"), + ): if filters.get(opts[0]): conditions += opts[1] return conditions + def get_standard_rated_expenses_total(filters): """Returns the sum of the total of each Purchase invoice made with recoverable reverse charge.""" query_filters = get_filters(filters) - query_filters.append(['recoverable_standard_rated_expenses', '>', 0]) - query_filters.append(['docstatus', '=', 1]) + query_filters.append(["recoverable_standard_rated_expenses", ">", 0]) + query_filters.append(["docstatus", "=", 1]) try: - return frappe.db.get_all('Purchase Invoice', - filters = query_filters, - fields = ['sum(total)'], - as_list=True, - limit = 1 - )[0][0] or 0 + return ( + frappe.db.get_all( + "Purchase Invoice", filters=query_filters, fields=["sum(total)"], as_list=True, limit=1 + )[0][0] + or 0 + ) except (IndexError, TypeError): return 0 + def get_standard_rated_expenses_tax(filters): """Returns the sum of the tax of each Purchase invoice made.""" query_filters = get_filters(filters) - query_filters.append(['recoverable_standard_rated_expenses', '>', 0]) - query_filters.append(['docstatus', '=', 1]) + query_filters.append(["recoverable_standard_rated_expenses", ">", 0]) + query_filters.append(["docstatus", "=", 1]) try: - return frappe.db.get_all('Purchase Invoice', - filters = query_filters, - fields = ['sum(recoverable_standard_rated_expenses)'], - as_list=True, - limit = 1 - )[0][0] or 0 + return ( + frappe.db.get_all( + "Purchase Invoice", + filters=query_filters, + fields=["sum(recoverable_standard_rated_expenses)"], + as_list=True, + limit=1, + )[0][0] + or 0 + ) except (IndexError, TypeError): return 0 + def get_tourist_tax_return_total(filters): """Returns the sum of the total of each Sales invoice with non zero tourist_tax_return.""" query_filters = get_filters(filters) - query_filters.append(['tourist_tax_return', '>', 0]) - query_filters.append(['docstatus', '=', 1]) + query_filters.append(["tourist_tax_return", ">", 0]) + query_filters.append(["docstatus", "=", 1]) try: - return frappe.db.get_all('Sales Invoice', - filters = query_filters, - fields = ['sum(total)'], - as_list=True, - limit = 1 - )[0][0] or 0 + return ( + frappe.db.get_all( + "Sales Invoice", filters=query_filters, fields=["sum(total)"], as_list=True, limit=1 + )[0][0] + or 0 + ) except (IndexError, TypeError): return 0 + def get_tourist_tax_return_tax(filters): """Returns the sum of the tax of each Sales invoice with non zero tourist_tax_return.""" query_filters = get_filters(filters) - query_filters.append(['tourist_tax_return', '>', 0]) - query_filters.append(['docstatus', '=', 1]) + query_filters.append(["tourist_tax_return", ">", 0]) + query_filters.append(["docstatus", "=", 1]) try: - return frappe.db.get_all('Sales Invoice', - filters = query_filters, - fields = ['sum(tourist_tax_return)'], - as_list=True, - limit = 1 - )[0][0] or 0 + return ( + frappe.db.get_all( + "Sales Invoice", + filters=query_filters, + fields=["sum(tourist_tax_return)"], + as_list=True, + limit=1, + )[0][0] + or 0 + ) except (IndexError, TypeError): return 0 + def get_zero_rated_total(filters): """Returns the sum of each Sales Invoice Item Amount which is zero rated.""" conditions = get_conditions(filters) try: - return frappe.db.sql(""" + return ( + frappe.db.sql( + """ select sum(i.base_amount) as total from @@ -308,15 +364,24 @@ def get_zero_rated_total(filters): where s.docstatus = 1 and i.is_zero_rated = 1 {where_conditions} ; - """.format(where_conditions=conditions), filters)[0][0] or 0 + """.format( + where_conditions=conditions + ), + filters, + )[0][0] + or 0 + ) except (IndexError, TypeError): return 0 + def get_exempt_total(filters): """Returns the sum of each Sales Invoice Item Amount which is Vat Exempt.""" conditions = get_conditions(filters) try: - return frappe.db.sql(""" + return ( + frappe.db.sql( + """ select sum(i.base_amount) as total from @@ -326,15 +391,25 @@ def get_exempt_total(filters): where s.docstatus = 1 and i.is_exempt = 1 {where_conditions} ; - """.format(where_conditions=conditions), filters)[0][0] or 0 + """.format( + where_conditions=conditions + ), + filters, + )[0][0] + or 0 + ) except (IndexError, TypeError): return 0 + + def get_conditions(filters): """The conditions to be used to filter data to calculate the total sale.""" conditions = "" - for opts in (("company", " and company=%(company)s"), + for opts in ( + ("company", " and company=%(company)s"), ("from_date", " and posting_date>=%(from_date)s"), - ("to_date", " and posting_date<=%(to_date)s")): + ("to_date", " and posting_date<=%(to_date)s"), + ): if filters.get(opts[0]): conditions += opts[1] return conditions diff --git a/erpnext/regional/report/vat_audit_report/test_vat_audit_report.py b/erpnext/regional/report/vat_audit_report/test_vat_audit_report.py index f22abae1ff..a898a25104 100644 --- a/erpnext/regional/report/vat_audit_report/test_vat_audit_report.py +++ b/erpnext/regional/report/vat_audit_report/test_vat_audit_report.py @@ -18,14 +18,22 @@ class TestVATAuditReport(TestCase): frappe.set_user("Administrator") make_company("_Test Company SA VAT", "_TCSV") - create_account(account_name="VAT - 0%", account_type="Tax", - parent_account="Duties and Taxes - _TCSV", company="_Test Company SA VAT") - create_account(account_name="VAT - 15%", account_type="Tax", - parent_account="Duties and Taxes - _TCSV", company="_Test Company SA VAT") + create_account( + account_name="VAT - 0%", + account_type="Tax", + parent_account="Duties and Taxes - _TCSV", + company="_Test Company SA VAT", + ) + create_account( + account_name="VAT - 15%", + account_type="Tax", + parent_account="Duties and Taxes - _TCSV", + company="_Test Company SA VAT", + ) set_sa_vat_accounts() make_item("_Test SA VAT Item") - make_item("_Test SA VAT Zero Rated Item", properties = {"is_zero_rated": 1}) + make_item("_Test SA VAT Zero Rated Item", properties={"is_zero_rated": 1}) make_customer() make_supplier() @@ -38,34 +46,33 @@ class TestVATAuditReport(TestCase): frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company SA VAT'") def test_vat_audit_report(self): - filters = { - "company": "_Test Company SA VAT", - "from_date": today(), - "to_date": today() - } + filters = {"company": "_Test Company SA VAT", "from_date": today(), "to_date": today()} columns, data = execute(filters) total_tax_amount = 0 total_row_tax = 0 for row in data: keys = row.keys() # skips total row tax_amount in if.. and skips section header in elif.. - if 'voucher_no' in keys: - total_tax_amount = total_tax_amount + row['tax_amount'] - elif 'tax_amount' in keys: - total_row_tax = total_row_tax + row['tax_amount'] + if "voucher_no" in keys: + total_tax_amount = total_tax_amount + row["tax_amount"] + elif "tax_amount" in keys: + total_row_tax = total_row_tax + row["tax_amount"] self.assertEqual(total_tax_amount, total_row_tax) + def make_company(company_name, abbr): if not frappe.db.exists("Company", company_name): - company = frappe.get_doc({ - "doctype": "Company", - "company_name": company_name, - "abbr": abbr, - "default_currency": "ZAR", - "country": "South Africa", - "create_chart_of_accounts_based_on": "Standard Template" - }) + company = frappe.get_doc( + { + "doctype": "Company", + "company_name": company_name, + "abbr": abbr, + "default_currency": "ZAR", + "country": "South Africa", + "create_chart_of_accounts_based_on": "Standard Template", + } + ) company.insert() else: company = frappe.get_doc("Company", company_name) @@ -79,86 +86,95 @@ def make_company(company_name, abbr): return company + def set_sa_vat_accounts(): if not frappe.db.exists("South Africa VAT Settings", "_Test Company SA VAT"): vat_accounts = frappe.get_all( "Account", fields=["name"], - filters = { - "company": "_Test Company SA VAT", - "is_group": 0, - "account_type": "Tax" - } + filters={"company": "_Test Company SA VAT", "is_group": 0, "account_type": "Tax"}, ) sa_vat_accounts = [] for account in vat_accounts: - sa_vat_accounts.append({ - "doctype": "South Africa VAT Account", - "account": account.name - }) + sa_vat_accounts.append({"doctype": "South Africa VAT Account", "account": account.name}) + + frappe.get_doc( + { + "company": "_Test Company SA VAT", + "vat_accounts": sa_vat_accounts, + "doctype": "South Africa VAT Settings", + } + ).insert() - frappe.get_doc({ - "company": "_Test Company SA VAT", - "vat_accounts": sa_vat_accounts, - "doctype": "South Africa VAT Settings", - }).insert() def make_customer(): if not frappe.db.exists("Customer", "_Test SA Customer"): - frappe.get_doc({ - "doctype": "Customer", - "customer_name": "_Test SA Customer", - "customer_type": "Company", - }).insert() + frappe.get_doc( + { + "doctype": "Customer", + "customer_name": "_Test SA Customer", + "customer_type": "Company", + } + ).insert() + def make_supplier(): if not frappe.db.exists("Supplier", "_Test SA Supplier"): - frappe.get_doc({ - "doctype": "Supplier", - "supplier_name": "_Test SA Supplier", - "supplier_type": "Company", - "supplier_group":"All Supplier Groups" - }).insert() + frappe.get_doc( + { + "doctype": "Supplier", + "supplier_name": "_Test SA Supplier", + "supplier_type": "Company", + "supplier_group": "All Supplier Groups", + } + ).insert() + def make_item(item_code, properties=None): if not frappe.db.exists("Item", item_code): - item = frappe.get_doc({ - "doctype": "Item", - "item_code": item_code, - "item_name": item_code, - "description": item_code, - "item_group": "Products" - }) + item = frappe.get_doc( + { + "doctype": "Item", + "item_code": item_code, + "item_name": item_code, + "description": item_code, + "item_group": "Products", + } + ) if properties: item.update(properties) item.insert() + def make_sales_invoices(): def make_sales_invoices_wrapper(item, rate, tax_account, tax_rate, tax=True): si = create_sales_invoice( company="_Test Company SA VAT", - customer = "_Test SA Customer", - currency = "ZAR", + customer="_Test SA Customer", + currency="ZAR", item=item, rate=rate, - warehouse = "Finished Goods - _TCSV", - debit_to = "Debtors - _TCSV", - income_account = "Sales - _TCSV", - expense_account = "Cost of Goods Sold - _TCSV", - cost_center = "Main - _TCSV", - do_not_save=1 + warehouse="Finished Goods - _TCSV", + debit_to="Debtors - _TCSV", + income_account="Sales - _TCSV", + expense_account="Cost of Goods Sold - _TCSV", + cost_center="Main - _TCSV", + do_not_save=1, ) if tax: - si.append("taxes", { + si.append( + "taxes", + { "charge_type": "On Net Total", "account_head": tax_account, "cost_center": "Main - _TCSV", "description": "VAT 15% @ 15.0", - "rate": tax_rate - }) + "rate": tax_rate, + }, + ) si.submit() @@ -168,27 +184,31 @@ def make_sales_invoices(): make_sales_invoices_wrapper(test_item, 100.0, "VAT - 15% - _TCSV", 15.0) make_sales_invoices_wrapper(test_zero_rated_item, 100.0, "VAT - 0% - _TCSV", 0.0) + def create_purchase_invoices(): pi = make_purchase_invoice( - company = "_Test Company SA VAT", - supplier = "_Test SA Supplier", - supplier_warehouse = "Finished Goods - _TCSV", - warehouse = "Finished Goods - _TCSV", - currency = "ZAR", - cost_center = "Main - _TCSV", - expense_account = "Cost of Goods Sold - _TCSV", - item = "_Test SA VAT Item", - qty = 1, - rate = 100, - uom = "Nos", - do_not_save = 1 + company="_Test Company SA VAT", + supplier="_Test SA Supplier", + supplier_warehouse="Finished Goods - _TCSV", + warehouse="Finished Goods - _TCSV", + currency="ZAR", + cost_center="Main - _TCSV", + expense_account="Cost of Goods Sold - _TCSV", + item="_Test SA VAT Item", + qty=1, + rate=100, + uom="Nos", + do_not_save=1, + ) + pi.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "VAT - 15% - _TCSV", + "cost_center": "Main - _TCSV", + "description": "VAT 15% @ 15.0", + "rate": 15.0, + }, ) - pi.append("taxes", { - "charge_type": "On Net Total", - "account_head": "VAT - 15% - _TCSV", - "cost_center": "Main - _TCSV", - "description": "VAT 15% @ 15.0", - "rate": 15.0 - }) pi.submit() diff --git a/erpnext/regional/report/vat_audit_report/vat_audit_report.py b/erpnext/regional/report/vat_audit_report/vat_audit_report.py index 17e50648b3..6e5982465c 100644 --- a/erpnext/regional/report/vat_audit_report/vat_audit_report.py +++ b/erpnext/regional/report/vat_audit_report/vat_audit_report.py @@ -12,8 +12,8 @@ from frappe.utils import formatdate, get_link_to_form def execute(filters=None): return VATAuditReport(filters).run() -class VATAuditReport(object): +class VATAuditReport(object): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) self.columns = [] @@ -27,8 +27,11 @@ class VATAuditReport(object): self.select_columns = """ name as voucher_no, posting_date, remarks""" - columns = ", supplier as party, credit_to as account" if doctype=="Purchase Invoice" \ + columns = ( + ", supplier as party, credit_to as account" + if doctype == "Purchase Invoice" else ", customer as party, debit_to as account" + ) self.select_columns += columns self.get_invoice_data(doctype) @@ -41,17 +44,21 @@ class VATAuditReport(object): return self.columns, self.data def get_sa_vat_accounts(self): - self.sa_vat_accounts = frappe.get_all("South Africa VAT Account", - filters = {"parent": self.filters.company}, pluck="account") + self.sa_vat_accounts = frappe.get_all( + "South Africa VAT Account", filters={"parent": self.filters.company}, pluck="account" + ) if not self.sa_vat_accounts and not frappe.flags.in_test and not frappe.flags.in_migrate: - link_to_settings = get_link_to_form("South Africa VAT Settings", "", label="South Africa VAT Settings") + link_to_settings = get_link_to_form( + "South Africa VAT Settings", "", label="South Africa VAT Settings" + ) frappe.throw(_("Please set VAT Accounts in {0}").format(link_to_settings)) def get_invoice_data(self, doctype): conditions = self.get_conditions() self.invoices = frappe._dict() - invoice_data = frappe.db.sql(""" + invoice_data = frappe.db.sql( + """ SELECT {select_columns} FROM @@ -61,8 +68,12 @@ class VATAuditReport(object): and is_opening = "No" ORDER BY posting_date DESC - """.format(select_columns=self.select_columns, doctype=doctype, - where_conditions=conditions), self.filters, as_dict=1) + """.format( + select_columns=self.select_columns, doctype=doctype, where_conditions=conditions + ), + self.filters, + as_dict=1, + ) for d in invoice_data: self.invoices.setdefault(d.voucher_no, d) @@ -70,28 +81,34 @@ class VATAuditReport(object): def get_invoice_items(self, doctype): self.invoice_items = frappe._dict() - items = frappe.db.sql(""" + items = frappe.db.sql( + """ SELECT item_code, parent, base_net_amount, is_zero_rated FROM `tab%s Item` WHERE parent in (%s) - """ % (doctype, ", ".join(["%s"]*len(self.invoices))), tuple(self.invoices), as_dict=1) + """ + % (doctype, ", ".join(["%s"] * len(self.invoices))), + tuple(self.invoices), + as_dict=1, + ) for d in items: if d.item_code not in self.invoice_items.get(d.parent, {}): - self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, { - 'net_amount': 0.0}) - self.invoice_items[d.parent][d.item_code]['net_amount'] += d.get('base_net_amount', 0) - self.invoice_items[d.parent][d.item_code]['is_zero_rated'] = d.is_zero_rated + self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, {"net_amount": 0.0}) + self.invoice_items[d.parent][d.item_code]["net_amount"] += d.get("base_net_amount", 0) + self.invoice_items[d.parent][d.item_code]["is_zero_rated"] = d.is_zero_rated def get_items_based_on_tax_rate(self, doctype): self.items_based_on_tax_rate = frappe._dict() self.item_tax_rate = frappe._dict() - self.tax_doctype = "Purchase Taxes and Charges" if doctype=="Purchase Invoice" \ - else "Sales Taxes and Charges" + self.tax_doctype = ( + "Purchase Taxes and Charges" if doctype == "Purchase Invoice" else "Sales Taxes and Charges" + ) - self.tax_details = frappe.db.sql(""" + self.tax_details = frappe.db.sql( + """ SELECT parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount FROM @@ -101,8 +118,10 @@ class VATAuditReport(object): and parent in (%s) ORDER BY account_head - """ % (self.tax_doctype, "%s", ", ".join(["%s"]*len(self.invoices.keys()))), - tuple([doctype] + list(self.invoices.keys()))) + """ + % (self.tax_doctype, "%s", ", ".join(["%s"] * len(self.invoices.keys()))), + tuple([doctype] + list(self.invoices.keys())), + ) for parent, account, item_wise_tax_detail, tax_amount in self.tax_details: if item_wise_tax_detail: @@ -113,14 +132,15 @@ class VATAuditReport(object): continue for item_code, taxes in item_wise_tax_detail.items(): is_zero_rated = self.invoice_items.get(parent).get(item_code).get("is_zero_rated") - #to skip items with non-zero tax rate in multiple rows + # to skip items with non-zero tax rate in multiple rows if taxes[0] == 0 and not is_zero_rated: continue tax_rate, item_amount_map = self.get_item_amount_map(parent, item_code, taxes) if tax_rate is not None: - rate_based_dict = self.items_based_on_tax_rate.setdefault(parent, {}) \ - .setdefault(tax_rate, []) + rate_based_dict = self.items_based_on_tax_rate.setdefault(parent, {}).setdefault( + tax_rate, [] + ) if item_code not in rate_based_dict: rate_based_dict.append(item_code) except ValueError: @@ -131,13 +151,12 @@ class VATAuditReport(object): tax_rate = taxes[0] tax_amount = taxes[1] gross_amount = net_amount + tax_amount - item_amount_map = self.item_tax_rate.setdefault(parent, {}) \ - .setdefault(item_code, []) + item_amount_map = self.item_tax_rate.setdefault(parent, {}).setdefault(item_code, []) amount_dict = { "tax_rate": tax_rate, "gross_amount": gross_amount, "tax_amount": tax_amount, - "net_amount": net_amount + "net_amount": net_amount, } item_amount_map.append(amount_dict) @@ -145,9 +164,11 @@ class VATAuditReport(object): def get_conditions(self): conditions = "" - for opts in (("company", " and company=%(company)s"), + for opts in ( + ("company", " and company=%(company)s"), ("from_date", " and posting_date>=%(from_date)s"), - ("to_date", " and posting_date<=%(to_date)s")): + ("to_date", " and posting_date<=%(to_date)s"), + ): if self.filters.get(opts[0]): conditions += opts[1] @@ -174,13 +195,13 @@ class VATAuditReport(object): "gross_amount": total_gross, "tax_amount": total_tax, "net_amount": total_net, - "bold":1 + "bold": 1, } self.data.append(total) self.data.append({}) def get_consolidated_data(self, doctype): - consolidated_data_map={} + consolidated_data_map = {} for inv, inv_data in self.invoices.items(): if self.items_based_on_tax_rate.get(inv): for rate, items in self.items_based_on_tax_rate.get(inv).items(): @@ -195,78 +216,53 @@ class VATAuditReport(object): row["party_type"] = "Customer" if doctype == "Sales Invoice" else "Supplier" row["party"] = inv_data.get("party") row["remarks"] = inv_data.get("remarks") - row["gross_amount"]= item_details[0].get("gross_amount") - row["tax_amount"]= item_details[0].get("tax_amount") - row["net_amount"]= item_details[0].get("net_amount") + row["gross_amount"] = item_details[0].get("gross_amount") + row["tax_amount"] = item_details[0].get("tax_amount") + row["net_amount"] = item_details[0].get("net_amount") consolidated_data_map[rate]["data"].append(row) return consolidated_data_map def get_columns(self): self.columns = [ - { - "fieldname": "posting_date", - "label": "Posting Date", - "fieldtype": "Data", - "width": 200 - }, + {"fieldname": "posting_date", "label": "Posting Date", "fieldtype": "Data", "width": 200}, { "fieldname": "account", "label": "Account", "fieldtype": "Link", "options": "Account", - "width": 150 + "width": 150, }, { "fieldname": "voucher_type", "label": "Voucher Type", "fieldtype": "Data", "width": 140, - "hidden": 1 + "hidden": 1, }, { "fieldname": "voucher_no", "label": "Reference", "fieldtype": "Dynamic Link", "options": "voucher_type", - "width": 150 + "width": 150, }, { "fieldname": "party_type", "label": "Party Type", "fieldtype": "Data", "width": 140, - "hidden": 1 + "hidden": 1, }, { "fieldname": "party", "label": "Party", "fieldtype": "Dynamic Link", "options": "party_type", - "width": 150 - }, - { - "fieldname": "remarks", - "label": "Details", - "fieldtype": "Data", - "width": 150 - }, - { - "fieldname": "net_amount", - "label": "Net Amount", - "fieldtype": "Currency", - "width": 130 - }, - { - "fieldname": "tax_amount", - "label": "Tax Amount", - "fieldtype": "Currency", - "width": 130 - }, - { - "fieldname": "gross_amount", - "label": "Gross Amount", - "fieldtype": "Currency", - "width": 130 + "width": 150, }, + {"fieldname": "remarks", "label": "Details", "fieldtype": "Data", "width": 150}, + {"fieldname": "net_amount", "label": "Net Amount", "fieldtype": "Currency", "width": 130}, + {"fieldname": "tax_amount", "label": "Tax Amount", "fieldtype": "Currency", "width": 130}, + {"fieldname": "gross_amount", "label": "Gross Amount", "fieldtype": "Currency", "width": 130}, ] diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py index d2ef6f3f17..7f41c462cc 100644 --- a/erpnext/regional/saudi_arabia/setup.py +++ b/erpnext/regional/saudi_arabia/setup.py @@ -3,14 +3,18 @@ import frappe from frappe.permissions import add_permission, update_permission_property -from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import create_ksa_vat_setting +from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import ( + create_ksa_vat_setting, +) from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + def setup(company=None, patch=True): add_print_formats() add_permissions() make_custom_fields() + def add_print_formats(): frappe.reload_doc("regional", "print_format", "detailed_tax_invoice", force=True) frappe.reload_doc("regional", "print_format", "simplified_tax_invoice", force=True) @@ -18,19 +22,27 @@ def add_print_formats(): frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True) frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True) - for d in ('Simplified Tax Invoice', 'Detailed Tax Invoice', 'Tax Invoice', 'KSA VAT Invoice', 'KSA POS Invoice'): + for d in ( + "Simplified Tax Invoice", + "Detailed Tax Invoice", + "Tax Invoice", + "KSA VAT Invoice", + "KSA POS Invoice", + ): frappe.db.set_value("Print Format", d, "disabled", 0) + def add_permissions(): """Add Permissions for KSA VAT Setting.""" - add_permission('KSA VAT Setting', 'All', 0) - for role in ('Accounts Manager', 'Accounts User', 'System Manager'): - add_permission('KSA VAT Setting', role, 0) - update_permission_property('KSA VAT Setting', role, 0, 'write', 1) - update_permission_property('KSA VAT Setting', role, 0, 'create', 1) + add_permission("KSA VAT Setting", "All", 0) + for role in ("Accounts Manager", "Accounts User", "System Manager"): + add_permission("KSA VAT Setting", role, 0) + update_permission_property("KSA VAT Setting", role, 0, "write", 1) + update_permission_property("KSA VAT Setting", role, 0, "create", 1) """Enable KSA VAT Report""" - frappe.db.set_value('Report', 'KSA VAT', 'disabled', 0) + frappe.db.set_value("Report", "KSA VAT", "disabled", 0) + def make_custom_fields(): """Create Custom fields @@ -38,71 +50,124 @@ def make_custom_fields(): - Company Name in Arabic - Address in Arabic """ - is_zero_rated = dict(fieldname='is_zero_rated', label='Is Zero Rated', - fieldtype='Check', fetch_from='item_code.is_zero_rated', insert_after='description', - print_hide=1) + is_zero_rated = dict( + fieldname="is_zero_rated", + label="Is Zero Rated", + fieldtype="Check", + fetch_from="item_code.is_zero_rated", + insert_after="description", + print_hide=1, + ) - is_exempt = dict(fieldname='is_exempt', label='Is Exempt', - fieldtype='Check', fetch_from='item_code.is_exempt', insert_after='is_zero_rated', - print_hide=1) + is_exempt = dict( + fieldname="is_exempt", + label="Is Exempt", + fieldtype="Check", + fetch_from="item_code.is_exempt", + insert_after="is_zero_rated", + print_hide=1, + ) purchase_invoice_fields = [ - dict(fieldname='company_trn', label='Company TRN', - fieldtype='Read Only', insert_after='shipping_address', - fetch_from='company.tax_id', print_hide=1), - dict(fieldname='supplier_name_in_arabic', label='Supplier Name in Arabic', - fieldtype='Read Only', insert_after='supplier_name', - fetch_from='supplier.supplier_name_in_arabic', print_hide=1) - ] + dict( + fieldname="company_trn", + label="Company TRN", + fieldtype="Read Only", + insert_after="shipping_address", + fetch_from="company.tax_id", + print_hide=1, + ), + dict( + fieldname="supplier_name_in_arabic", + label="Supplier Name in Arabic", + fieldtype="Read Only", + insert_after="supplier_name", + fetch_from="supplier.supplier_name_in_arabic", + print_hide=1, + ), + ] sales_invoice_fields = [ - dict(fieldname='company_trn', label='Company TRN', - fieldtype='Read Only', insert_after='company_address', - fetch_from='company.tax_id', print_hide=1), - dict(fieldname='customer_name_in_arabic', label='Customer Name in Arabic', - fieldtype='Read Only', insert_after='customer_name', - fetch_from='customer.customer_name_in_arabic', print_hide=1), - dict(fieldname='ksa_einv_qr', label='KSA E-Invoicing QR', - fieldtype='Attach Image', read_only=1, no_copy=1, hidden=1) - ] + dict( + fieldname="company_trn", + label="Company TRN", + fieldtype="Read Only", + insert_after="company_address", + fetch_from="company.tax_id", + print_hide=1, + ), + dict( + fieldname="customer_name_in_arabic", + label="Customer Name in Arabic", + fieldtype="Read Only", + insert_after="customer_name", + fetch_from="customer.customer_name_in_arabic", + print_hide=1, + ), + dict( + fieldname="ksa_einv_qr", + label="KSA E-Invoicing QR", + fieldtype="Attach Image", + read_only=1, + no_copy=1, + hidden=1, + ), + ] custom_fields = { - 'Item': [is_zero_rated, is_exempt], - 'Customer': [ - dict(fieldname='customer_name_in_arabic', label='Customer Name in Arabic', - fieldtype='Data', insert_after='customer_name'), + "Item": [is_zero_rated, is_exempt], + "Customer": [ + dict( + fieldname="customer_name_in_arabic", + label="Customer Name in Arabic", + fieldtype="Data", + insert_after="customer_name", + ), ], - 'Supplier': [ - dict(fieldname='supplier_name_in_arabic', label='Supplier Name in Arabic', - fieldtype='Data', insert_after='supplier_name'), + "Supplier": [ + dict( + fieldname="supplier_name_in_arabic", + label="Supplier Name in Arabic", + fieldtype="Data", + insert_after="supplier_name", + ), ], - 'Purchase Invoice': purchase_invoice_fields, - 'Purchase Order': purchase_invoice_fields, - 'Purchase Receipt': purchase_invoice_fields, - 'Sales Invoice': sales_invoice_fields, - 'POS Invoice': sales_invoice_fields, - 'Sales Order': sales_invoice_fields, - 'Delivery Note': sales_invoice_fields, - 'Sales Invoice Item': [is_zero_rated, is_exempt], - 'POS Invoice Item': [is_zero_rated, is_exempt], - 'Purchase Invoice Item': [is_zero_rated, is_exempt], - 'Sales Order Item': [is_zero_rated, is_exempt], - 'Delivery Note Item': [is_zero_rated, is_exempt], - 'Quotation Item': [is_zero_rated, is_exempt], - 'Purchase Order Item': [is_zero_rated, is_exempt], - 'Purchase Receipt Item': [is_zero_rated, is_exempt], - 'Supplier Quotation Item': [is_zero_rated, is_exempt], - 'Address': [ - dict(fieldname='address_in_arabic', label='Address in Arabic', - fieldtype='Data',insert_after='address_line2') + "Purchase Invoice": purchase_invoice_fields, + "Purchase Order": purchase_invoice_fields, + "Purchase Receipt": purchase_invoice_fields, + "Sales Invoice": sales_invoice_fields, + "POS Invoice": sales_invoice_fields, + "Sales Order": sales_invoice_fields, + "Delivery Note": sales_invoice_fields, + "Sales Invoice Item": [is_zero_rated, is_exempt], + "POS Invoice Item": [is_zero_rated, is_exempt], + "Purchase Invoice Item": [is_zero_rated, is_exempt], + "Sales Order Item": [is_zero_rated, is_exempt], + "Delivery Note Item": [is_zero_rated, is_exempt], + "Quotation Item": [is_zero_rated, is_exempt], + "Purchase Order Item": [is_zero_rated, is_exempt], + "Purchase Receipt Item": [is_zero_rated, is_exempt], + "Supplier Quotation Item": [is_zero_rated, is_exempt], + "Address": [ + dict( + fieldname="address_in_arabic", + label="Address in Arabic", + fieldtype="Data", + insert_after="address_line2", + ) + ], + "Company": [ + dict( + fieldname="company_name_in_arabic", + label="Company Name In Arabic", + fieldtype="Data", + insert_after="company_name", + ) ], - 'Company': [ - dict(fieldname='company_name_in_arabic', label='Company Name In Arabic', - fieldtype='Data', insert_after='company_name') - ] } create_custom_fields(custom_fields, ignore_validate=True, update=True) + def update_regional_tax_settings(country, company): create_ksa_vat_setting(company) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index 515862d06a..b47adc95f7 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -13,21 +13,25 @@ from erpnext import get_region def create_qr_code(doc, method=None): region = get_region(doc.company) - if region not in ['Saudi Arabia']: + if region not in ["Saudi Arabia"]: return # if QR Code field not present, create it. Invoices without QR are invalid as per law. - if not hasattr(doc, 'ksa_einv_qr'): - create_custom_fields({ - doc.doctype: [ - dict( - fieldname='ksa_einv_qr', - label='KSA E-Invoicing QR', - fieldtype='Attach Image', - read_only=1, no_copy=1, hidden=1 - ) - ] - }) + if not hasattr(doc, "ksa_einv_qr"): + create_custom_fields( + { + doc.doctype: [ + dict( + fieldname="ksa_einv_qr", + label="KSA E-Invoicing QR", + fieldtype="Attach Image", + read_only=1, + no_copy=1, + hidden=1, + ) + ] + } + ) # Don't create QR Code if it already exists qr_code = doc.get("ksa_einv_qr") @@ -37,129 +41,129 @@ def create_qr_code(doc, method=None): meta = frappe.get_meta(doc.doctype) if "ksa_einv_qr" in [d.fieldname for d in meta.get_image_fields()]: - ''' TLV conversion for + """TLV conversion for 1. Seller's Name 2. VAT Number 3. Time Stamp 4. Invoice Amount 5. VAT Amount - ''' + """ tlv_array = [] # Sellers Name - seller_name = frappe.db.get_value( - 'Company', - doc.company, - 'company_name_in_arabic') + seller_name = frappe.db.get_value("Company", doc.company, "company_name_in_arabic") if not seller_name: - frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company)) + frappe.throw(_("Arabic name missing for {} in the company document").format(doc.company)) tag = bytes([1]).hex() - length = bytes([len(seller_name.encode('utf-8'))]).hex() - value = seller_name.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + length = bytes([len(seller_name.encode("utf-8"))]).hex() + value = seller_name.encode("utf-8").hex() + tlv_array.append("".join([tag, length, value])) # VAT Number - tax_id = frappe.db.get_value('Company', doc.company, 'tax_id') + tax_id = frappe.db.get_value("Company", doc.company, "tax_id") if not tax_id: - frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company)) + frappe.throw(_("Tax ID missing for {} in the company document").format(doc.company)) tag = bytes([2]).hex() length = bytes([len(tax_id)]).hex() - value = tax_id.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + value = tax_id.encode("utf-8").hex() + tlv_array.append("".join([tag, length, value])) # Time Stamp posting_date = getdate(doc.posting_date) time = get_time(doc.posting_time) seconds = time.hour * 60 * 60 + time.minute * 60 + time.second time_stamp = add_to_date(posting_date, seconds=seconds) - time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ') + time_stamp = time_stamp.strftime("%Y-%m-%dT%H:%M:%SZ") tag = bytes([3]).hex() length = bytes([len(time_stamp)]).hex() - value = time_stamp.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + value = time_stamp.encode("utf-8").hex() + tlv_array.append("".join([tag, length, value])) # Invoice Amount invoice_amount = str(doc.grand_total) tag = bytes([4]).hex() length = bytes([len(invoice_amount)]).hex() - value = invoice_amount.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + value = invoice_amount.encode("utf-8").hex() + tlv_array.append("".join([tag, length, value])) # VAT Amount vat_amount = str(get_vat_amount(doc)) tag = bytes([5]).hex() length = bytes([len(vat_amount)]).hex() - value = vat_amount.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + value = vat_amount.encode("utf-8").hex() + tlv_array.append("".join([tag, length, value])) # Joining bytes into one - tlv_buff = ''.join(tlv_array) + tlv_buff = "".join(tlv_array) # base64 conversion for QR Code base64_string = b64encode(bytes.fromhex(tlv_buff)).decode() qr_image = io.BytesIO() - url = qr_create(base64_string, error='L') + url = qr_create(base64_string, error="L") url.png(qr_image, scale=2, quiet_zone=1) name = frappe.generate_hash(doc.name, 5) # making file filename = f"QRCode-{name}.png".replace(os.path.sep, "__") - _file = frappe.get_doc({ - "doctype": "File", - "file_name": filename, - "is_private": 0, - "content": qr_image.getvalue(), - "attached_to_doctype": doc.get("doctype"), - "attached_to_name": doc.get("name"), - "attached_to_field": "ksa_einv_qr" - }) + _file = frappe.get_doc( + { + "doctype": "File", + "file_name": filename, + "is_private": 0, + "content": qr_image.getvalue(), + "attached_to_doctype": doc.get("doctype"), + "attached_to_name": doc.get("name"), + "attached_to_field": "ksa_einv_qr", + } + ) _file.save() # assigning to document - doc.db_set('ksa_einv_qr', _file.file_url) + doc.db_set("ksa_einv_qr", _file.file_url) doc.notify_update() + def get_vat_amount(doc): - vat_settings = frappe.db.get_value('KSA VAT Setting', {'company': doc.company}) + vat_settings = frappe.db.get_value("KSA VAT Setting", {"company": doc.company}) vat_accounts = [] vat_amount = 0 if vat_settings: - vat_settings_doc = frappe.get_cached_doc('KSA VAT Setting', vat_settings) + vat_settings_doc = frappe.get_cached_doc("KSA VAT Setting", vat_settings) - for row in vat_settings_doc.get('ksa_vat_sales_accounts'): + for row in vat_settings_doc.get("ksa_vat_sales_accounts"): vat_accounts.append(row.account) - for tax in doc.get('taxes'): + for tax in doc.get("taxes"): if tax.account_head in vat_accounts: vat_amount += tax.tax_amount return vat_amount + def delete_qr_code_file(doc, method=None): region = get_region(doc.company) - if region not in ['Saudi Arabia']: + if region not in ["Saudi Arabia"]: return - if hasattr(doc, 'ksa_einv_qr'): - if doc.get('ksa_einv_qr'): - file_doc = frappe.get_list('File', { - 'file_url': doc.get('ksa_einv_qr') - }) + if hasattr(doc, "ksa_einv_qr"): + if doc.get("ksa_einv_qr"): + file_doc = frappe.get_list("File", {"file_url": doc.get("ksa_einv_qr")}) if len(file_doc): - frappe.delete_doc('File', file_doc[0].name) + frappe.delete_doc("File", file_doc[0].name) + def delete_vat_settings_for_company(doc, method=None): - if doc.country != 'Saudi Arabia': + if doc.country != "Saudi Arabia": return - if frappe.db.exists('KSA VAT Setting', doc.name): - frappe.delete_doc('KSA VAT Setting', doc.name) + if frappe.db.exists("KSA VAT Setting", doc.name): + frappe.delete_doc("KSA VAT Setting", doc.name) diff --git a/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py b/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py index 97300dc378..66d9df224e 100644 --- a/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py +++ b/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py @@ -5,39 +5,42 @@ import frappe def create_ksa_vat_setting(company): - """On creation of first company. Creates KSA VAT Setting""" + """On creation of first company. Creates KSA VAT Setting""" - company = frappe.get_doc('Company', company) + company = frappe.get_doc("Company", company) - file_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'ksa_vat_settings.json') - with open(file_path, 'r') as json_file: - account_data = json.load(json_file) + file_path = os.path.join(os.path.dirname(__file__), "..", "data", "ksa_vat_settings.json") + with open(file_path, "r") as json_file: + account_data = json.load(json_file) - # Creating KSA VAT Setting - ksa_vat_setting = frappe.get_doc({ - 'doctype': 'KSA VAT Setting', - 'company': company.name - }) + # Creating KSA VAT Setting + ksa_vat_setting = frappe.get_doc({"doctype": "KSA VAT Setting", "company": company.name}) - for data in account_data: - if data['type'] == 'Sales Account': - for row in data['accounts']: - item_tax_template = row['item_tax_template'] - account = row['account'] - ksa_vat_setting.append('ksa_vat_sales_accounts', { - 'title': row['title'], - 'item_tax_template': f'{item_tax_template} - {company.abbr}', - 'account': f'{account} - {company.abbr}' - }) + for data in account_data: + if data["type"] == "Sales Account": + for row in data["accounts"]: + item_tax_template = row["item_tax_template"] + account = row["account"] + ksa_vat_setting.append( + "ksa_vat_sales_accounts", + { + "title": row["title"], + "item_tax_template": f"{item_tax_template} - {company.abbr}", + "account": f"{account} - {company.abbr}", + }, + ) - elif data['type'] == 'Purchase Account': - for row in data['accounts']: - item_tax_template = row['item_tax_template'] - account = row['account'] - ksa_vat_setting.append('ksa_vat_purchase_accounts', { - 'title': row['title'], - 'item_tax_template': f'{item_tax_template} - {company.abbr}', - 'account': f'{account} - {company.abbr}' - }) + elif data["type"] == "Purchase Account": + for row in data["accounts"]: + item_tax_template = row["item_tax_template"] + account = row["account"] + ksa_vat_setting.append( + "ksa_vat_purchase_accounts", + { + "title": row["title"], + "item_tax_template": f"{item_tax_template} - {company.abbr}", + "account": f"{account} - {company.abbr}", + }, + ) - ksa_vat_setting.save() + ksa_vat_setting.save() diff --git a/erpnext/regional/south_africa/setup.py b/erpnext/regional/south_africa/setup.py index 6af135b960..289f2726e9 100644 --- a/erpnext/regional/south_africa/setup.py +++ b/erpnext/regional/south_africa/setup.py @@ -6,44 +6,53 @@ import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.permissions import add_permission, update_permission_property + def setup(company=None, patch=True): make_custom_fields() add_permissions() + def make_custom_fields(update=True): - is_zero_rated = dict(fieldname='is_zero_rated', label='Is Zero Rated', - fieldtype='Check', fetch_from='item_code.is_zero_rated', - insert_after='description', print_hide=1) + is_zero_rated = dict( + fieldname="is_zero_rated", + label="Is Zero Rated", + fieldtype="Check", + fetch_from="item_code.is_zero_rated", + insert_after="description", + print_hide=1, + ) custom_fields = { - 'Item': [ - dict(fieldname='is_zero_rated', label='Is Zero Rated', - fieldtype='Check', insert_after='item_group', - print_hide=1) + "Item": [ + dict( + fieldname="is_zero_rated", + label="Is Zero Rated", + fieldtype="Check", + insert_after="item_group", + print_hide=1, + ) ], - 'Sales Invoice Item': is_zero_rated, - 'Purchase Invoice Item': is_zero_rated + "Sales Invoice Item": is_zero_rated, + "Purchase Invoice Item": is_zero_rated, } create_custom_fields(custom_fields, update=update) + def add_permissions(): """Add Permissions for South Africa VAT Settings and South Africa VAT Account - and VAT Audit Report""" - for doctype in ('South Africa VAT Settings', 'South Africa VAT Account'): - add_permission(doctype, 'All', 0) - for role in ('Accounts Manager', 'Accounts User', 'System Manager'): + and VAT Audit Report""" + for doctype in ("South Africa VAT Settings", "South Africa VAT Account"): + add_permission(doctype, "All", 0) + for role in ("Accounts Manager", "Accounts User", "System Manager"): add_permission(doctype, role, 0) - update_permission_property(doctype, role, 0, 'write', 1) - update_permission_property(doctype, role, 0, 'create', 1) + update_permission_property(doctype, role, 0, "write", 1) + update_permission_property(doctype, role, 0, "create", 1) - - if not frappe.db.get_value('Custom Role', dict(report="VAT Audit Report")): - frappe.get_doc(dict( - doctype='Custom Role', - report="VAT Audit Report", - roles= [ - dict(role='Accounts User'), - dict(role='Accounts Manager'), - dict(role='Auditor') - ] - )).insert() + if not frappe.db.get_value("Custom Role", dict(report="VAT Audit Report")): + frappe.get_doc( + dict( + doctype="Custom Role", + report="VAT Audit Report", + roles=[dict(role="Accounts User"), dict(role="Accounts Manager"), dict(role="Auditor")], + ) + ).insert() diff --git a/erpnext/regional/turkey/setup.py b/erpnext/regional/turkey/setup.py index 1d3770aefc..c915189352 100644 --- a/erpnext/regional/turkey/setup.py +++ b/erpnext/regional/turkey/setup.py @@ -1,2 +1,2 @@ def setup(company=None, patch=True): - pass + pass diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 922443b924..cc647f8519 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -7,6 +7,7 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.permissions import add_permission, update_permission_property from erpnext.payroll.doctype.gratuity_rule.gratuity_rule import get_gratuity_rule + def setup(company=None, patch=True): make_custom_fields() add_print_formats() @@ -14,145 +15,270 @@ def setup(company=None, patch=True): add_permissions() create_gratuity_rule() + def make_custom_fields(): - is_zero_rated = dict(fieldname='is_zero_rated', label='Is Zero Rated', - fieldtype='Check', fetch_from='item_code.is_zero_rated', insert_after='description', - print_hide=1) - is_exempt = dict(fieldname='is_exempt', label='Is Exempt', - fieldtype='Check', fetch_from='item_code.is_exempt', insert_after='is_zero_rated', - print_hide=1) + is_zero_rated = dict( + fieldname="is_zero_rated", + label="Is Zero Rated", + fieldtype="Check", + fetch_from="item_code.is_zero_rated", + insert_after="description", + print_hide=1, + ) + is_exempt = dict( + fieldname="is_exempt", + label="Is Exempt", + fieldtype="Check", + fetch_from="item_code.is_exempt", + insert_after="is_zero_rated", + print_hide=1, + ) invoice_fields = [ - dict(fieldname='vat_section', label='VAT Details', fieldtype='Section Break', - insert_after='group_same_items', print_hide=1, collapsible=1), - dict(fieldname='permit_no', label='Permit Number', - fieldtype='Data', insert_after='vat_section', print_hide=1), + dict( + fieldname="vat_section", + label="VAT Details", + fieldtype="Section Break", + insert_after="group_same_items", + print_hide=1, + collapsible=1, + ), + dict( + fieldname="permit_no", + label="Permit Number", + fieldtype="Data", + insert_after="vat_section", + print_hide=1, + ), ] purchase_invoice_fields = [ - dict(fieldname='company_trn', label='Company TRN', - fieldtype='Read Only', insert_after='shipping_address', - fetch_from='company.tax_id', print_hide=1), - dict(fieldname='supplier_name_in_arabic', label='Supplier Name in Arabic', - fieldtype='Read Only', insert_after='supplier_name', - fetch_from='supplier.supplier_name_in_arabic', print_hide=1), - dict(fieldname='recoverable_standard_rated_expenses', print_hide=1, default='0', - label='Recoverable Standard Rated Expenses (AED)', insert_after='permit_no', - fieldtype='Currency', ), - dict(fieldname='reverse_charge', label='Reverse Charge Applicable', - fieldtype='Select', insert_after='recoverable_standard_rated_expenses', print_hide=1, - options='Y\nN', default='N'), - dict(fieldname='recoverable_reverse_charge', label='Recoverable Reverse Charge (Percentage)', - insert_after='reverse_charge', fieldtype='Percent', print_hide=1, - depends_on="eval:doc.reverse_charge=='Y'", default='100.000'), - ] + dict( + fieldname="company_trn", + label="Company TRN", + fieldtype="Read Only", + insert_after="shipping_address", + fetch_from="company.tax_id", + print_hide=1, + ), + dict( + fieldname="supplier_name_in_arabic", + label="Supplier Name in Arabic", + fieldtype="Read Only", + insert_after="supplier_name", + fetch_from="supplier.supplier_name_in_arabic", + print_hide=1, + ), + dict( + fieldname="recoverable_standard_rated_expenses", + print_hide=1, + default="0", + label="Recoverable Standard Rated Expenses (AED)", + insert_after="permit_no", + fieldtype="Currency", + ), + dict( + fieldname="reverse_charge", + label="Reverse Charge Applicable", + fieldtype="Select", + insert_after="recoverable_standard_rated_expenses", + print_hide=1, + options="Y\nN", + default="N", + ), + dict( + fieldname="recoverable_reverse_charge", + label="Recoverable Reverse Charge (Percentage)", + insert_after="reverse_charge", + fieldtype="Percent", + print_hide=1, + depends_on="eval:doc.reverse_charge=='Y'", + default="100.000", + ), + ] sales_invoice_fields = [ - dict(fieldname='company_trn', label='Company TRN', - fieldtype='Read Only', insert_after='company_address', - fetch_from='company.tax_id', print_hide=1), - dict(fieldname='customer_name_in_arabic', label='Customer Name in Arabic', - fieldtype='Read Only', insert_after='customer_name', - fetch_from='customer.customer_name_in_arabic', print_hide=1), - dict(fieldname='vat_emirate', label='VAT Emirate', insert_after='permit_no', fieldtype='Select', - options='\nAbu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain', - fetch_from='company_address.emirate'), - dict(fieldname='tourist_tax_return', label='Tax Refund provided to Tourists (AED)', - insert_after='vat_emirate', fieldtype='Currency', print_hide=1, default='0'), - ] + dict( + fieldname="company_trn", + label="Company TRN", + fieldtype="Read Only", + insert_after="company_address", + fetch_from="company.tax_id", + print_hide=1, + ), + dict( + fieldname="customer_name_in_arabic", + label="Customer Name in Arabic", + fieldtype="Read Only", + insert_after="customer_name", + fetch_from="customer.customer_name_in_arabic", + print_hide=1, + ), + dict( + fieldname="vat_emirate", + label="VAT Emirate", + insert_after="permit_no", + fieldtype="Select", + options="\nAbu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain", + fetch_from="company_address.emirate", + ), + dict( + fieldname="tourist_tax_return", + label="Tax Refund provided to Tourists (AED)", + insert_after="vat_emirate", + fieldtype="Currency", + print_hide=1, + default="0", + ), + ] invoice_item_fields = [ - dict(fieldname='tax_code', label='Tax Code', - fieldtype='Read Only', fetch_from='item_code.tax_code', insert_after='description', - allow_on_submit=1, print_hide=1), - dict(fieldname='tax_rate', label='Tax Rate', - fieldtype='Float', insert_after='tax_code', - print_hide=1, hidden=1, read_only=1), - dict(fieldname='tax_amount', label='Tax Amount', - fieldtype='Currency', insert_after='tax_rate', - print_hide=1, hidden=1, read_only=1, options="currency"), - dict(fieldname='total_amount', label='Total Amount', - fieldtype='Currency', insert_after='tax_amount', - print_hide=1, hidden=1, read_only=1, options="currency"), + dict( + fieldname="tax_code", + label="Tax Code", + fieldtype="Read Only", + fetch_from="item_code.tax_code", + insert_after="description", + allow_on_submit=1, + print_hide=1, + ), + dict( + fieldname="tax_rate", + label="Tax Rate", + fieldtype="Float", + insert_after="tax_code", + print_hide=1, + hidden=1, + read_only=1, + ), + dict( + fieldname="tax_amount", + label="Tax Amount", + fieldtype="Currency", + insert_after="tax_rate", + print_hide=1, + hidden=1, + read_only=1, + options="currency", + ), + dict( + fieldname="total_amount", + label="Total Amount", + fieldtype="Currency", + insert_after="tax_amount", + print_hide=1, + hidden=1, + read_only=1, + options="currency", + ), ] delivery_date_field = [ - dict(fieldname='delivery_date', label='Delivery Date', - fieldtype='Date', insert_after='item_name', print_hide=1) + dict( + fieldname="delivery_date", + label="Delivery Date", + fieldtype="Date", + insert_after="item_name", + print_hide=1, + ) ] custom_fields = { - 'Item': [ - dict(fieldname='tax_code', label='Tax Code', - fieldtype='Data', insert_after='item_group'), - dict(fieldname='is_zero_rated', label='Is Zero Rated', - fieldtype='Check', insert_after='tax_code', - print_hide=1), - dict(fieldname='is_exempt', label='Is Exempt', - fieldtype='Check', insert_after='is_zero_rated', - print_hide=1) + "Item": [ + dict(fieldname="tax_code", label="Tax Code", fieldtype="Data", insert_after="item_group"), + dict( + fieldname="is_zero_rated", + label="Is Zero Rated", + fieldtype="Check", + insert_after="tax_code", + print_hide=1, + ), + dict( + fieldname="is_exempt", + label="Is Exempt", + fieldtype="Check", + insert_after="is_zero_rated", + print_hide=1, + ), ], - 'Customer': [ - dict(fieldname='customer_name_in_arabic', label='Customer Name in Arabic', - fieldtype='Data', insert_after='customer_name'), + "Customer": [ + dict( + fieldname="customer_name_in_arabic", + label="Customer Name in Arabic", + fieldtype="Data", + insert_after="customer_name", + ), ], - 'Supplier': [ - dict(fieldname='supplier_name_in_arabic', label='Supplier Name in Arabic', - fieldtype='Data', insert_after='supplier_name'), + "Supplier": [ + dict( + fieldname="supplier_name_in_arabic", + label="Supplier Name in Arabic", + fieldtype="Data", + insert_after="supplier_name", + ), ], - 'Address': [ - dict(fieldname='emirate', label='Emirate', fieldtype='Select', insert_after='state', - options='\nAbu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain') + "Address": [ + dict( + fieldname="emirate", + label="Emirate", + fieldtype="Select", + insert_after="state", + options="\nAbu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain", + ) ], - 'Purchase Invoice': purchase_invoice_fields + invoice_fields, - 'Purchase Order': purchase_invoice_fields + invoice_fields, - 'Purchase Receipt': purchase_invoice_fields + invoice_fields, - 'Sales Invoice': sales_invoice_fields + invoice_fields, - 'POS Invoice': sales_invoice_fields + invoice_fields, - 'Sales Order': sales_invoice_fields + invoice_fields, - 'Delivery Note': sales_invoice_fields + invoice_fields, - 'Sales Invoice Item': invoice_item_fields + delivery_date_field + [is_zero_rated, is_exempt], - 'POS Invoice Item': invoice_item_fields + delivery_date_field + [is_zero_rated, is_exempt], - 'Purchase Invoice Item': invoice_item_fields, - 'Sales Order Item': invoice_item_fields, - 'Delivery Note Item': invoice_item_fields, - 'Quotation Item': invoice_item_fields, - 'Purchase Order Item': invoice_item_fields, - 'Purchase Receipt Item': invoice_item_fields, - 'Supplier Quotation Item': invoice_item_fields, + "Purchase Invoice": purchase_invoice_fields + invoice_fields, + "Purchase Order": purchase_invoice_fields + invoice_fields, + "Purchase Receipt": purchase_invoice_fields + invoice_fields, + "Sales Invoice": sales_invoice_fields + invoice_fields, + "POS Invoice": sales_invoice_fields + invoice_fields, + "Sales Order": sales_invoice_fields + invoice_fields, + "Delivery Note": sales_invoice_fields + invoice_fields, + "Sales Invoice Item": invoice_item_fields + delivery_date_field + [is_zero_rated, is_exempt], + "POS Invoice Item": invoice_item_fields + delivery_date_field + [is_zero_rated, is_exempt], + "Purchase Invoice Item": invoice_item_fields, + "Sales Order Item": invoice_item_fields, + "Delivery Note Item": invoice_item_fields, + "Quotation Item": invoice_item_fields, + "Purchase Order Item": invoice_item_fields, + "Purchase Receipt Item": invoice_item_fields, + "Supplier Quotation Item": invoice_item_fields, } create_custom_fields(custom_fields) + def add_print_formats(): frappe.reload_doc("regional", "print_format", "detailed_tax_invoice") frappe.reload_doc("regional", "print_format", "simplified_tax_invoice") frappe.reload_doc("regional", "print_format", "tax_invoice") - frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where - name in('Simplified Tax Invoice', 'Detailed Tax Invoice', 'Tax Invoice') """) + frappe.db.sql( + """ update `tabPrint Format` set disabled = 0 where + name in('Simplified Tax Invoice', 'Detailed Tax Invoice', 'Tax Invoice') """ + ) + def add_custom_roles_for_reports(): """Add Access Control to UAE VAT 201.""" - if not frappe.db.get_value('Custom Role', dict(report='UAE VAT 201')): - frappe.get_doc(dict( - doctype='Custom Role', - report='UAE VAT 201', - roles= [ - dict(role='Accounts User'), - dict(role='Accounts Manager'), - dict(role='Auditor') - ] - )).insert() + if not frappe.db.get_value("Custom Role", dict(report="UAE VAT 201")): + frappe.get_doc( + dict( + doctype="Custom Role", + report="UAE VAT 201", + roles=[dict(role="Accounts User"), dict(role="Accounts Manager"), dict(role="Auditor")], + ) + ).insert() + def add_permissions(): """Add Permissions for UAE VAT Settings and UAE VAT Account.""" - for doctype in ('UAE VAT Settings', 'UAE VAT Account'): - add_permission(doctype, 'All', 0) - for role in ('Accounts Manager', 'Accounts User', 'System Manager'): + for doctype in ("UAE VAT Settings", "UAE VAT Account"): + add_permission(doctype, "All", 0) + for role in ("Accounts Manager", "Accounts User", "System Manager"): add_permission(doctype, role, 0) - update_permission_property(doctype, role, 0, 'write', 1) - update_permission_property(doctype, role, 0, 'create', 1) + update_permission_property(doctype, role, 0, "write", 1) + update_permission_property(doctype, role, 0, "create", 1) + def create_gratuity_rule(): rule_1 = rule_2 = rule_3 = None @@ -160,7 +286,11 @@ def create_gratuity_rule(): # Rule Under Limited Contract slabs = get_slab_for_limited_contract() if not frappe.db.exists("Gratuity Rule", "Rule Under Limited Contract (UAE)"): - rule_1 = get_gratuity_rule("Rule Under Limited Contract (UAE)", slabs, calculate_gratuity_amount_based_on="Sum of all previous slabs") + rule_1 = get_gratuity_rule( + "Rule Under Limited Contract (UAE)", + slabs, + calculate_gratuity_amount_based_on="Sum of all previous slabs", + ) # Rule Under Unlimited Contract on termination slabs = get_slab_for_unlimited_contract_on_termination() @@ -172,7 +302,7 @@ def create_gratuity_rule(): if not frappe.db.exists("Gratuity Rule", "Rule Under Unlimited Contract on resignation (UAE)"): rule_3 = get_gratuity_rule("Rule Under Unlimited Contract on resignation (UAE)", slabs) - #for applicable salary component user need to set this by its own + # for applicable salary component user need to set this by its own if rule_1: rule_1.flags.ignore_mandatory = True rule_1.save() @@ -185,61 +315,29 @@ def create_gratuity_rule(): def get_slab_for_limited_contract(): - return [{ - "from_year": 0, - "to_year":1, - "fraction_of_applicable_earnings": 0 - }, - { - "from_year": 1, - "to_year":5, - "fraction_of_applicable_earnings": 21/30 - }, - { - "from_year": 5, - "to_year":0, - "fraction_of_applicable_earnings": 1 - }] + return [ + {"from_year": 0, "to_year": 1, "fraction_of_applicable_earnings": 0}, + {"from_year": 1, "to_year": 5, "fraction_of_applicable_earnings": 21 / 30}, + {"from_year": 5, "to_year": 0, "fraction_of_applicable_earnings": 1}, + ] + def get_slab_for_unlimited_contract_on_termination(): - return [{ - "from_year": 0, - "to_year":1, - "fraction_of_applicable_earnings": 0 - }, - { - "from_year": 1, - "to_year":5, - "fraction_of_applicable_earnings": 21/30 - }, - { - "from_year": 5, - "to_year":0, - "fraction_of_applicable_earnings": 1 - }] + return [ + {"from_year": 0, "to_year": 1, "fraction_of_applicable_earnings": 0}, + {"from_year": 1, "to_year": 5, "fraction_of_applicable_earnings": 21 / 30}, + {"from_year": 5, "to_year": 0, "fraction_of_applicable_earnings": 1}, + ] + def get_slab_for_unlimited_contract_on_resignation(): - fraction_1 = 1/3 * 21/30 - fraction_2 = 2/3 * 21/30 - fraction_3 = 21/30 + fraction_1 = 1 / 3 * 21 / 30 + fraction_2 = 2 / 3 * 21 / 30 + fraction_3 = 21 / 30 - return [{ - "from_year": 0, - "to_year":1, - "fraction_of_applicable_earnings": 0 - }, - { - "from_year": 1, - "to_year":3, - "fraction_of_applicable_earnings": fraction_1 - }, - { - "from_year": 3, - "to_year":5, - "fraction_of_applicable_earnings": fraction_2 - }, - { - "from_year": 5, - "to_year":0, - "fraction_of_applicable_earnings": fraction_3 - }] + return [ + {"from_year": 0, "to_year": 1, "fraction_of_applicable_earnings": 0}, + {"from_year": 1, "to_year": 3, "fraction_of_applicable_earnings": fraction_1}, + {"from_year": 3, "to_year": 5, "fraction_of_applicable_earnings": fraction_2}, + {"from_year": 5, "to_year": 0, "fraction_of_applicable_earnings": fraction_3}, + ] diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index bdede846fe..a910af6a1d 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -7,7 +7,8 @@ from erpnext.controllers.taxes_and_totals import get_itemised_tax def update_itemised_tax_data(doc): - if not doc.taxes: return + if not doc.taxes: + return itemised_tax = get_itemised_tax(doc.taxes) @@ -24,40 +25,39 @@ def update_itemised_tax_data(doc): for account, rate in item_tax_rate.items(): tax_rate += rate elif row.item_code and itemised_tax.get(row.item_code): - tax_rate = sum([tax.get('tax_rate', 0) for d, tax in itemised_tax.get(row.item_code).items()]) + tax_rate = sum([tax.get("tax_rate", 0) for d, tax in itemised_tax.get(row.item_code).items()]) meta = frappe.get_meta(row.doctype) - if meta.has_field('tax_rate'): + if meta.has_field("tax_rate"): row.tax_rate = flt(tax_rate, row.precision("tax_rate")) row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount")) row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount")) + def get_account_currency(account): """Helper function to get account currency.""" if not account: return + def generator(): account_currency, company = frappe.get_cached_value( - "Account", - account, - ["account_currency", - "company"] + "Account", account, ["account_currency", "company"] ) if not account_currency: - account_currency = frappe.get_cached_value('Company', company, "default_currency") + account_currency = frappe.get_cached_value("Company", company, "default_currency") return account_currency return frappe.local_cache("account_currency", account, generator) + def get_tax_accounts(company): """Get the list of tax accounts for a specific company.""" tax_accounts_dict = frappe._dict() - tax_accounts_list = frappe.get_all("UAE VAT Account", - filters={"parent": company}, - fields=["Account"] - ) + tax_accounts_list = frappe.get_all( + "UAE VAT Account", filters={"parent": company}, fields=["Account"] + ) if not tax_accounts_list and not frappe.flags.in_test: frappe.throw(_('Please set Vat Accounts for Company: "{0}" in UAE VAT Settings').format(company)) @@ -67,23 +67,24 @@ def get_tax_accounts(company): return tax_accounts_dict + def update_grand_total_for_rcm(doc, method): """If the Reverse Charge is Applicable subtract the tax amount from the grand total and update in the form.""" - country = frappe.get_cached_value('Company', doc.company, 'country') + country = frappe.get_cached_value("Company", doc.company, "country") - if country != 'United Arab Emirates': + if country != "United Arab Emirates": return if not doc.total_taxes_and_charges: return - if doc.reverse_charge == 'Y': + if doc.reverse_charge == "Y": tax_accounts = get_tax_accounts(doc.company) base_vat_tax = 0 vat_tax = 0 - for tax in doc.get('taxes'): + for tax in doc.get("taxes"): if tax.category not in ("Total", "Valuation and Total"): continue @@ -98,6 +99,7 @@ def update_grand_total_for_rcm(doc, method): update_totals(vat_tax, base_vat_tax, doc) + def update_totals(vat_tax, base_vat_tax, doc): """Update the grand total values in the form.""" doc.base_grand_total -= base_vat_tax @@ -109,56 +111,67 @@ def update_totals(vat_tax, base_vat_tax, doc): doc.outstanding_amount = doc.grand_total else: - doc.rounded_total = round_based_on_smallest_currency_fraction(doc.grand_total, - doc.currency, doc.precision("rounded_total")) - doc.rounding_adjustment = flt(doc.rounded_total - doc.grand_total, - doc.precision("rounding_adjustment")) + doc.rounded_total = round_based_on_smallest_currency_fraction( + doc.grand_total, doc.currency, doc.precision("rounded_total") + ) + doc.rounding_adjustment = flt( + doc.rounded_total - doc.grand_total, doc.precision("rounding_adjustment") + ) doc.outstanding_amount = doc.rounded_total or doc.grand_total doc.in_words = money_in_words(doc.grand_total, doc.currency) - doc.base_in_words = money_in_words(doc.base_grand_total, erpnext.get_company_currency(doc.company)) + doc.base_in_words = money_in_words( + doc.base_grand_total, erpnext.get_company_currency(doc.company) + ) doc.set_payment_schedule() + def make_regional_gl_entries(gl_entries, doc): """Hooked to make_regional_gl_entries in Purchase Invoice.It appends the region specific general ledger entries to the list of GL Entries.""" - country = frappe.get_cached_value('Company', doc.company, 'country') + country = frappe.get_cached_value("Company", doc.company, "country") - if country != 'United Arab Emirates': + if country != "United Arab Emirates": return gl_entries - if doc.reverse_charge == 'Y': + if doc.reverse_charge == "Y": tax_accounts = get_tax_accounts(doc.company) - for tax in doc.get('taxes'): + for tax in doc.get("taxes"): if tax.category not in ("Total", "Valuation and Total"): continue gl_entries = make_gl_entry(tax, gl_entries, doc, tax_accounts) return gl_entries + def make_gl_entry(tax, gl_entries, doc, tax_accounts): dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" - if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in tax_accounts: + if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in tax_accounts: account_currency = get_account_currency(tax.account_head) - gl_entries.append(doc.get_gl_dict({ - "account": tax.account_head, - "cost_center": tax.cost_center, - "posting_date": doc.posting_date, - "against": doc.supplier, - dr_or_cr: tax.base_tax_amount_after_discount_amount, - dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount \ - if account_currency==doc.company_currency \ - else tax.tax_amount_after_discount_amount - }, account_currency, item=tax - )) + gl_entries.append( + doc.get_gl_dict( + { + "account": tax.account_head, + "cost_center": tax.cost_center, + "posting_date": doc.posting_date, + "against": doc.supplier, + dr_or_cr: tax.base_tax_amount_after_discount_amount, + dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount + if account_currency == doc.company_currency + else tax.tax_amount_after_discount_amount, + }, + account_currency, + item=tax, + ) + ) return gl_entries def validate_returns(doc, method): """Standard Rated expenses should not be set when Reverse Charge Applicable is set.""" - country = frappe.get_cached_value('Company', doc.company, 'country') - if country != 'United Arab Emirates': + country = frappe.get_cached_value("Company", doc.company, "country") + if country != "United Arab Emirates": return - if doc.reverse_charge == 'Y' and flt(doc.recoverable_standard_rated_expenses) != 0: - frappe.throw(_( - "Recoverable Standard Rated expenses should not be set when Reverse Charge Applicable is Y" - )) + if doc.reverse_charge == "Y" and flt(doc.recoverable_standard_rated_expenses) != 0: + frappe.throw( + _("Recoverable Standard Rated expenses should not be set when Reverse Charge Applicable is Y") + ) diff --git a/erpnext/regional/united_states/setup.py b/erpnext/regional/united_states/setup.py index db6a9c3547..b8f85723f5 100644 --- a/erpnext/regional/united_states/setup.py +++ b/erpnext/regional/united_states/setup.py @@ -7,40 +7,64 @@ import json from frappe.permissions import add_permission, update_permission_property from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + def setup(company=None, patch=True): # Company independent fixtures should be called only once at the first company setup - if frappe.db.count('Company', {'country': 'United States'}) <=1: + if frappe.db.count("Company", {"country": "United States"}) <= 1: setup_company_independent_fixtures(patch=patch) + def setup_company_independent_fixtures(company=None, patch=True): make_custom_fields() add_print_formats() + def make_custom_fields(update=True): custom_fields = { - 'Supplier': [ - dict(fieldname='irs_1099', fieldtype='Check', insert_after='tax_id', - label='Is IRS 1099 reporting required for supplier?') + "Supplier": [ + dict( + fieldname="irs_1099", + fieldtype="Check", + insert_after="tax_id", + label="Is IRS 1099 reporting required for supplier?", + ) ], - 'Sales Order': [ - dict(fieldname='exempt_from_sales_tax', fieldtype='Check', insert_after='taxes_and_charges', - label='Is customer exempted from sales tax?') + "Sales Order": [ + dict( + fieldname="exempt_from_sales_tax", + fieldtype="Check", + insert_after="taxes_and_charges", + label="Is customer exempted from sales tax?", + ) ], - 'Sales Invoice': [ - dict(fieldname='exempt_from_sales_tax', fieldtype='Check', insert_after='taxes_section', - label='Is customer exempted from sales tax?') + "Sales Invoice": [ + dict( + fieldname="exempt_from_sales_tax", + fieldtype="Check", + insert_after="taxes_section", + label="Is customer exempted from sales tax?", + ) ], - 'Customer': [ - dict(fieldname='exempt_from_sales_tax', fieldtype='Check', insert_after='represents_company', - label='Is customer exempted from sales tax?') + "Customer": [ + dict( + fieldname="exempt_from_sales_tax", + fieldtype="Check", + insert_after="represents_company", + label="Is customer exempted from sales tax?", + ) + ], + "Quotation": [ + dict( + fieldname="exempt_from_sales_tax", + fieldtype="Check", + insert_after="taxes_and_charges", + label="Is customer exempted from sales tax?", + ) ], - 'Quotation': [ - dict(fieldname='exempt_from_sales_tax', fieldtype='Check', insert_after='taxes_and_charges', - label='Is customer exempted from sales tax?') - ] } create_custom_fields(custom_fields, update=update) + def add_print_formats(): frappe.reload_doc("regional", "print_format", "irs_1099_form") frappe.db.set_value("Print Format", "IRS 1099 Form", "disabled", 0) diff --git a/erpnext/regional/united_states/test_united_states.py b/erpnext/regional/united_states/test_united_states.py index 652b4835a1..83ba6ed3ad 100644 --- a/erpnext/regional/united_states/test_united_states.py +++ b/erpnext/regional/united_states/test_united_states.py @@ -9,49 +9,51 @@ from erpnext.regional.report.irs_1099.irs_1099 import execute as execute_1099_re class TestUnitedStates(unittest.TestCase): - def test_irs_1099_custom_field(self): + def test_irs_1099_custom_field(self): - if not frappe.db.exists("Supplier", "_US 1099 Test Supplier"): - doc = frappe.new_doc("Supplier") - doc.supplier_name = "_US 1099 Test Supplier" - doc.supplier_group = "Services" - doc.supplier_type = "Company" - doc.country = "United States" - doc.tax_id = "04-1234567" - doc.irs_1099 = 1 - doc.save() - frappe.db.commit() - supplier = frappe.get_doc('Supplier', "_US 1099 Test Supplier") - self.assertEqual(supplier.irs_1099, 1) + if not frappe.db.exists("Supplier", "_US 1099 Test Supplier"): + doc = frappe.new_doc("Supplier") + doc.supplier_name = "_US 1099 Test Supplier" + doc.supplier_group = "Services" + doc.supplier_type = "Company" + doc.country = "United States" + doc.tax_id = "04-1234567" + doc.irs_1099 = 1 + doc.save() + frappe.db.commit() + supplier = frappe.get_doc("Supplier", "_US 1099 Test Supplier") + self.assertEqual(supplier.irs_1099, 1) - def test_irs_1099_report(self): - make_payment_entry_to_irs_1099_supplier() - filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company 1"}) - columns, data = execute_1099_report(filters) - expected_row = {'supplier': '_US 1099 Test Supplier', - 'supplier_group': 'Services', - 'payments': 100.0, - 'tax_id': '04-1234567'} - self.assertEqual(data[0], expected_row) + def test_irs_1099_report(self): + make_payment_entry_to_irs_1099_supplier() + filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company 1"}) + columns, data = execute_1099_report(filters) + expected_row = { + "supplier": "_US 1099 Test Supplier", + "supplier_group": "Services", + "payments": 100.0, + "tax_id": "04-1234567", + } + self.assertEqual(data[0], expected_row) def make_payment_entry_to_irs_1099_supplier(): - frappe.db.sql("delete from `tabGL Entry` where party='_US 1099 Test Supplier'") - frappe.db.sql("delete from `tabGL Entry` where against='_US 1099 Test Supplier'") - frappe.db.sql("delete from `tabPayment Entry` where party='_US 1099 Test Supplier'") + frappe.db.sql("delete from `tabGL Entry` where party='_US 1099 Test Supplier'") + frappe.db.sql("delete from `tabGL Entry` where against='_US 1099 Test Supplier'") + frappe.db.sql("delete from `tabPayment Entry` where party='_US 1099 Test Supplier'") - pe = frappe.new_doc("Payment Entry") - pe.payment_type = "Pay" - pe.company = "_Test Company 1" - pe.posting_date = "2016-01-10" - pe.paid_from = "_Test Bank USD - _TC1" - pe.paid_to = "_Test Payable USD - _TC1" - pe.paid_amount = 100 - pe.received_amount = 100 - pe.reference_no = "For IRS 1099 testing" - pe.reference_date = "2016-01-10" - pe.party_type = "Supplier" - pe.party = "_US 1099 Test Supplier" - pe.insert() - pe.submit() + pe = frappe.new_doc("Payment Entry") + pe.payment_type = "Pay" + pe.company = "_Test Company 1" + pe.posting_date = "2016-01-10" + pe.paid_from = "_Test Bank USD - _TC1" + pe.paid_to = "_Test Payable USD - _TC1" + pe.paid_amount = 100 + pe.received_amount = 100 + pe.reference_no = "For IRS 1099 testing" + pe.reference_date = "2016-01-10" + pe.party_type = "Supplier" + pe.party = "_US 1099 Test Supplier" + pe.insert() + pe.submit() diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 634c4819e6..2e5cbb80cb 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -37,13 +37,13 @@ class Customer(TransactionBase): def load_dashboard_info(self): info = get_dashboard_info(self.doctype, self.name, self.loyalty_program) - self.set_onload('dashboard_info', info) + self.set_onload("dashboard_info", info) def autoname(self): - cust_master_name = frappe.defaults.get_global_default('cust_master_name') - if cust_master_name == 'Customer Name': + cust_master_name = frappe.defaults.get_global_default("cust_master_name") + if cust_master_name == "Customer Name": self.name = self.get_customer_name() - elif cust_master_name == 'Naming Series': + elif cust_master_name == "Naming Series": set_name_by_naming_series(self) else: self.name = set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self) @@ -51,22 +51,30 @@ class Customer(TransactionBase): def get_customer_name(self): if frappe.db.get_value("Customer", self.customer_name) and not frappe.flags.in_import: - count = frappe.db.sql("""select ifnull(MAX(CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED)), 0) from tabCustomer - where name like %s""", "%{0} - %".format(self.customer_name), as_list=1)[0][0] + count = frappe.db.sql( + """select ifnull(MAX(CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED)), 0) from tabCustomer + where name like %s""", + "%{0} - %".format(self.customer_name), + as_list=1, + )[0][0] count = cint(count) + 1 new_customer_name = "{0} - {1}".format(self.customer_name, cstr(count)) - msgprint(_("Changed customer name to '{}' as '{}' already exists.") - .format(new_customer_name, self.customer_name), - title=_("Note"), indicator="yellow") + msgprint( + _("Changed customer name to '{}' as '{}' already exists.").format( + new_customer_name, self.customer_name + ), + title=_("Note"), + indicator="yellow", + ) return new_customer_name return self.customer_name def after_insert(self): - '''If customer created from Lead, update customer id in quotations, opportunities''' + """If customer created from Lead, update customer id in quotations, opportunities""" self.update_lead_status() def validate(self): @@ -80,8 +88,8 @@ class Customer(TransactionBase): self.validate_internal_customer() # set loyalty program tier - if frappe.db.exists('Customer', self.name): - customer = frappe.get_doc('Customer', self.name) + if frappe.db.exists("Customer", self.name): + customer = frappe.get_doc("Customer", self.name) if self.loyalty_program == customer.loyalty_program and not self.loyalty_program_tier: self.loyalty_program_tier = customer.loyalty_program_tier @@ -91,7 +99,7 @@ class Customer(TransactionBase): @frappe.whitelist() def get_customer_group_details(self): - doc = frappe.get_doc('Customer Group', self.customer_group) + doc = frappe.get_doc("Customer Group", self.customer_group) self.accounts = self.credit_limits = [] self.payment_terms = self.default_price_list = "" @@ -100,14 +108,16 @@ class Customer(TransactionBase): for row in tables: table, field = row[0], row[1] - if not doc.get(table): continue + if not doc.get(table): + continue for entry in doc.get(table): child = self.append(table) child.update({"company": entry.company, field: entry.get(field)}) for field in fields: - if not doc.get(field): continue + if not doc.get(field): + continue self.update({field: doc.get(field)}) self.save() @@ -115,23 +125,37 @@ class Customer(TransactionBase): def check_customer_group_change(self): frappe.flags.customer_group_changed = False - if not self.get('__islocal'): - if self.customer_group != frappe.db.get_value('Customer', self.name, 'customer_group'): + if not self.get("__islocal"): + if self.customer_group != frappe.db.get_value("Customer", self.name, "customer_group"): frappe.flags.customer_group_changed = True def validate_default_bank_account(self): if self.default_bank_account: - is_company_account = frappe.db.get_value('Bank Account', self.default_bank_account, 'is_company_account') + is_company_account = frappe.db.get_value( + "Bank Account", self.default_bank_account, "is_company_account" + ) if not is_company_account: - frappe.throw(_("{0} is not a company bank account").format(frappe.bold(self.default_bank_account))) + frappe.throw( + _("{0} is not a company bank account").format(frappe.bold(self.default_bank_account)) + ) def validate_internal_customer(self): - internal_customer = frappe.db.get_value("Customer", - {"is_internal_customer": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name") + internal_customer = frappe.db.get_value( + "Customer", + { + "is_internal_customer": 1, + "represents_company": self.represents_company, + "name": ("!=", self.name), + }, + "name", + ) if internal_customer: - frappe.throw(_("Internal Customer for company {0} already exists").format( - frappe.bold(self.represents_company))) + frappe.throw( + _("Internal Customer for company {0} already exists").format( + frappe.bold(self.represents_company) + ) + ) def on_update(self): self.validate_name_with_customer_group() @@ -149,21 +173,22 @@ class Customer(TransactionBase): def update_customer_groups(self): ignore_doctypes = ["Lead", "Opportunity", "POS Profile", "Tax Rule", "Pricing Rule"] if frappe.flags.customer_group_changed: - update_linked_doctypes('Customer', self.name, 'Customer Group', - self.customer_group, ignore_doctypes) + update_linked_doctypes( + "Customer", self.name, "Customer Group", self.customer_group, ignore_doctypes + ) def create_primary_contact(self): if not self.customer_primary_contact and not self.lead_name: if self.mobile_no or self.email_id: contact = make_contact(self) - self.db_set('customer_primary_contact', contact.name) - self.db_set('mobile_no', self.mobile_no) - self.db_set('email_id', self.email_id) + self.db_set("customer_primary_contact", contact.name) + self.db_set("mobile_no", self.mobile_no) + self.db_set("email_id", self.email_id) def create_primary_address(self): from frappe.contacts.doctype.address.address import get_address_display - if self.flags.is_new_doc and self.get('address_line1'): + if self.flags.is_new_doc and self.get("address_line1"): address = make_address(self) address_display = get_address_display(address.name) @@ -171,8 +196,8 @@ class Customer(TransactionBase): self.db_set("primary_address", address_display) def update_lead_status(self): - '''If Customer created from Lead, update lead status to "Converted" - update Customer link in Quotation, Opportunity''' + """If Customer created from Lead, update lead status to "Converted" + update Customer link in Quotation, Opportunity""" if self.lead_name: frappe.db.set_value("Lead", self.lead_name, "status", "Converted") @@ -191,22 +216,36 @@ class Customer(TransactionBase): for row in linked_contacts_and_addresses: linked_doc = frappe.get_doc(row.doctype, row.name) - if not linked_doc.has_link('Customer', self.name): - linked_doc.append('links', dict(link_doctype='Customer', link_name=self.name)) + if not linked_doc.has_link("Customer", self.name): + linked_doc.append("links", dict(link_doctype="Customer", link_name=self.name)) linked_doc.save(ignore_permissions=self.flags.ignore_permissions) def validate_name_with_customer_group(self): if frappe.db.exists("Customer Group", self.name): - frappe.throw(_("A Customer Group exists with same name please change the Customer name or rename the Customer Group"), frappe.NameError) + frappe.throw( + _( + "A Customer Group exists with same name please change the Customer name or rename the Customer Group" + ), + frappe.NameError, + ) def validate_credit_limit_on_change(self): if self.get("__islocal") or not self.credit_limits: return - past_credit_limits = [d.credit_limit - for d in frappe.db.get_all("Customer Credit Limit", filters={'parent': self.name}, fields=["credit_limit"], order_by="company")] + past_credit_limits = [ + d.credit_limit + for d in frappe.db.get_all( + "Customer Credit Limit", + filters={"parent": self.name}, + fields=["credit_limit"], + order_by="company", + ) + ] - current_credit_limits = [d.credit_limit for d in sorted(self.credit_limits, key=lambda k: k.company)] + current_credit_limits = [ + d.credit_limit for d in sorted(self.credit_limits, key=lambda k: k.company) + ] if past_credit_limits == current_credit_limits: return @@ -214,7 +253,9 @@ class Customer(TransactionBase): company_record = [] for limit in self.credit_limits: if limit.company in company_record: - frappe.throw(_("Credit limit is already defined for the Company {0}").format(limit.company, self.name)) + frappe.throw( + _("Credit limit is already defined for the Company {0}").format(limit.company, self.name) + ) else: company_record.append(limit.company) @@ -222,11 +263,16 @@ class Customer(TransactionBase): self.name, limit.company, ignore_outstanding_sales_order=limit.bypass_credit_limit_check ) if flt(limit.credit_limit) < outstanding_amt: - frappe.throw(_("""New credit limit is less than current outstanding amount for the customer. Credit limit has to be atleast {0}""").format(outstanding_amt)) + frappe.throw( + _( + """New credit limit is less than current outstanding amount for the customer. Credit limit has to be atleast {0}""" + ).format(outstanding_amt) + ) def on_trash(self): if self.customer_primary_contact: - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabCustomer` SET customer_primary_contact=null, @@ -234,14 +280,16 @@ class Customer(TransactionBase): mobile_no=null, email_id=null, primary_address=null - WHERE name=%(name)s""", {"name": self.name}) + WHERE name=%(name)s""", + {"name": self.name}, + ) - delete_contact_and_address('Customer', self.name) + delete_contact_and_address("Customer", self.name) if self.lead_name: frappe.db.sql("update `tabLead` set status='Interested' where name=%s", self.lead_name) def after_rename(self, olddn, newdn, merge=False): - if frappe.defaults.get_global_default('cust_master_name') == 'Customer Name': + if frappe.defaults.get_global_default("cust_master_name") == "Customer Name": frappe.db.set(self, "customer_name", newdn) def set_loyalty_program(self): @@ -256,43 +304,49 @@ class Customer(TransactionBase): self.loyalty_program = loyalty_program[0] else: frappe.msgprint( - _("Multiple Loyalty Programs found for Customer {}. Please select manually.") - .format(frappe.bold(self.customer_name)) + _("Multiple Loyalty Programs found for Customer {}. Please select manually.").format( + frappe.bold(self.customer_name) + ) ) + def create_contact(contact, party_type, party, email): """Create contact based on given contact name""" - contact = contact.split(' ') + contact = contact.split(" ") - contact = frappe.get_doc({ - 'doctype': 'Contact', - 'first_name': contact[0], - 'last_name': len(contact) > 1 and contact[1] or "" - }) - contact.append('email_ids', dict(email_id=email, is_primary=1)) - contact.append('links', dict(link_doctype=party_type, link_name=party)) + contact = frappe.get_doc( + { + "doctype": "Contact", + "first_name": contact[0], + "last_name": len(contact) > 1 and contact[1] or "", + } + ) + contact.append("email_ids", dict(email_id=email, is_primary=1)) + contact.append("links", dict(link_doctype=party_type, link_name=party)) contact.insert() + @frappe.whitelist() def make_quotation(source_name, target_doc=None): - def set_missing_values(source, target): _set_missing_values(source, target) - target_doc = get_mapped_doc("Customer", source_name, - {"Customer": { - "doctype": "Quotation", - "field_map": { - "name":"party_name" - } - }}, target_doc, set_missing_values) + target_doc = get_mapped_doc( + "Customer", + source_name, + {"Customer": {"doctype": "Quotation", "field_map": {"name": "party_name"}}}, + target_doc, + set_missing_values, + ) target_doc.quotation_to = "Customer" target_doc.run_method("set_missing_values") target_doc.run_method("set_other_charges") target_doc.run_method("calculate_taxes_and_totals") - price_list, currency = frappe.db.get_value("Customer", {'name': source_name}, ['default_price_list', 'default_currency']) + price_list, currency = frappe.db.get_value( + "Customer", {"name": source_name}, ["default_price_list", "default_currency"] + ) if price_list: target_doc.selling_price_list = price_list if currency: @@ -300,34 +354,53 @@ def make_quotation(source_name, target_doc=None): return target_doc + @frappe.whitelist() def make_opportunity(source_name, target_doc=None): def set_missing_values(source, target): _set_missing_values(source, target) - target_doc = get_mapped_doc("Customer", source_name, - {"Customer": { - "doctype": "Opportunity", - "field_map": { - "name": "party_name", - "doctype": "opportunity_from", + target_doc = get_mapped_doc( + "Customer", + source_name, + { + "Customer": { + "doctype": "Opportunity", + "field_map": { + "name": "party_name", + "doctype": "opportunity_from", + }, } - }}, target_doc, set_missing_values) + }, + target_doc, + set_missing_values, + ) return target_doc -def _set_missing_values(source, target): - address = frappe.get_all('Dynamic Link', { - 'link_doctype': source.doctype, - 'link_name': source.name, - 'parenttype': 'Address', - }, ['parent'], limit=1) - contact = frappe.get_all('Dynamic Link', { - 'link_doctype': source.doctype, - 'link_name': source.name, - 'parenttype': 'Contact', - }, ['parent'], limit=1) +def _set_missing_values(source, target): + address = frappe.get_all( + "Dynamic Link", + { + "link_doctype": source.doctype, + "link_name": source.name, + "parenttype": "Address", + }, + ["parent"], + limit=1, + ) + + contact = frappe.get_all( + "Dynamic Link", + { + "link_doctype": source.doctype, + "link_name": source.name, + "parenttype": "Contact", + }, + ["parent"], + limit=1, + ) if address: target.customer_address = address[0].parent @@ -335,35 +408,41 @@ def _set_missing_values(source, target): if contact: target.contact_person = contact[0].parent + @frappe.whitelist() def get_loyalty_programs(doc): - ''' returns applicable loyalty programs for a customer ''' + """returns applicable loyalty programs for a customer""" lp_details = [] - loyalty_programs = frappe.get_all("Loyalty Program", + loyalty_programs = frappe.get_all( + "Loyalty Program", fields=["name", "customer_group", "customer_territory"], - filters={"auto_opt_in": 1, "from_date": ["<=", today()], - "ifnull(to_date, '2500-01-01')": [">=", today()]}) + filters={ + "auto_opt_in": 1, + "from_date": ["<=", today()], + "ifnull(to_date, '2500-01-01')": [">=", today()], + }, + ) for loyalty_program in loyalty_programs: if ( - (not loyalty_program.customer_group - or doc.customer_group in get_nested_links( - "Customer Group", - loyalty_program.customer_group, - doc.flags.ignore_permissions - )) - and (not loyalty_program.customer_territory - or doc.territory in get_nested_links( - "Territory", - loyalty_program.customer_territory, - doc.flags.ignore_permissions - )) + not loyalty_program.customer_group + or doc.customer_group + in get_nested_links( + "Customer Group", loyalty_program.customer_group, doc.flags.ignore_permissions + ) + ) and ( + not loyalty_program.customer_territory + or doc.territory + in get_nested_links( + "Territory", loyalty_program.customer_territory, doc.flags.ignore_permissions + ) ): lp_details.append(loyalty_program.name) return lp_details + def get_nested_links(link_doctype, link_name, ignore_permissions=False): from frappe.desk.treeview import _get_children @@ -373,10 +452,12 @@ def get_nested_links(link_doctype, link_name, ignore_permissions=False): return links + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None): from erpnext.controllers.queries import get_fields + fields = ["name", "customer_name", "customer_group", "territory"] if frappe.db.get_default("cust_master_name") == "Customer Name": @@ -391,7 +472,8 @@ def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None): filter_conditions = get_filters_cond(doctype, filters, []) match_conditions += "{}".format(filter_conditions) - return frappe.db.sql(""" + return frappe.db.sql( + """ select %s from `tabCustomer` where docstatus < 2 @@ -401,8 +483,12 @@ def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None): case when name like %s then 0 else 1 end, case when customer_name like %s then 0 else 1 end, name, customer_name limit %s, %s - """.format(match_conditions=match_conditions) % (", ".join(fields), searchfield, "%s", "%s", "%s", "%s", "%s", "%s"), - ("%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, start, page_len)) + """.format( + match_conditions=match_conditions + ) + % (", ".join(fields), searchfield, "%s", "%s", "%s", "%s", "%s", "%s"), + ("%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, start, page_len), + ) def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, extra_amount=0): @@ -415,63 +501,87 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, customer_outstanding += flt(extra_amount) if credit_limit > 0 and flt(customer_outstanding) > credit_limit: - msgprint(_("Credit limit has been crossed for customer {0} ({1}/{2})") - .format(customer, customer_outstanding, credit_limit)) + msgprint( + _("Credit limit has been crossed for customer {0} ({1}/{2})").format( + customer, customer_outstanding, credit_limit + ) + ) # If not authorized person raise exception - credit_controller_role = frappe.db.get_single_value('Accounts Settings', 'credit_controller') + credit_controller_role = frappe.db.get_single_value("Accounts Settings", "credit_controller") if not credit_controller_role or credit_controller_role not in frappe.get_roles(): # form a list of emails for the credit controller users credit_controller_users = get_users_with_role(credit_controller_role or "Sales Master Manager") # form a list of emails and names to show to the user - credit_controller_users_formatted = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users] + credit_controller_users_formatted = [ + get_formatted_email(user).replace("<", "(").replace(">", ")") + for user in credit_controller_users + ] if not credit_controller_users_formatted: - frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.").format(customer)) + frappe.throw( + _("Please contact your administrator to extend the credit limits for {0}.").format(customer) + ) message = """Please contact any of the following users to extend the credit limits for {0}: -

    • {1}
    """.format(customer, '
  • '.join(credit_controller_users_formatted)) +

    • {1}
    """.format( + customer, "
  • ".join(credit_controller_users_formatted) + ) # if the current user does not have permissions to override credit limit, # prompt them to send out an email to the controller users - frappe.msgprint(message, + frappe.msgprint( + message, title="Notify", raise_exception=1, primary_action={ - 'label': 'Send Email', - 'server_action': 'erpnext.selling.doctype.customer.customer.send_emails', - 'args': { - 'customer': customer, - 'customer_outstanding': customer_outstanding, - 'credit_limit': credit_limit, - 'credit_controller_users_list': credit_controller_users - } - } + "label": "Send Email", + "server_action": "erpnext.selling.doctype.customer.customer.send_emails", + "args": { + "customer": customer, + "customer_outstanding": customer_outstanding, + "credit_limit": credit_limit, + "credit_controller_users_list": credit_controller_users, + }, + }, ) + @frappe.whitelist() def send_emails(args): args = json.loads(args) - subject = (_("Credit limit reached for customer {0}").format(args.get('customer'))) - message = (_("Credit limit has been crossed for customer {0} ({1}/{2})") - .format(args.get('customer'), args.get('customer_outstanding'), args.get('credit_limit'))) - frappe.sendmail(recipients=args.get('credit_controller_users_list'), subject=subject, message=message) + subject = _("Credit limit reached for customer {0}").format(args.get("customer")) + message = _("Credit limit has been crossed for customer {0} ({1}/{2})").format( + args.get("customer"), args.get("customer_outstanding"), args.get("credit_limit") + ) + frappe.sendmail( + recipients=args.get("credit_controller_users_list"), subject=subject, message=message + ) -def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=False, cost_center=None): + +def get_customer_outstanding( + customer, company, ignore_outstanding_sales_order=False, cost_center=None +): # Outstanding based on GL Entries cond = "" if cost_center: - lft, rgt = frappe.get_cached_value("Cost Center", - cost_center, ['lft', 'rgt']) + lft, rgt = frappe.get_cached_value("Cost Center", cost_center, ["lft", "rgt"]) cond = """ and cost_center in (select name from `tabCost Center` where - lft >= {0} and rgt <= {1})""".format(lft, rgt) + lft >= {0} and rgt <= {1})""".format( + lft, rgt + ) - outstanding_based_on_gle = frappe.db.sql(""" + outstanding_based_on_gle = frappe.db.sql( + """ select sum(debit) - sum(credit) from `tabGL Entry` where party_type = 'Customer' - and party = %s and company=%s {0}""".format(cond), (customer, company)) + and party = %s and company=%s {0}""".format( + cond + ), + (customer, company), + ) outstanding_based_on_gle = flt(outstanding_based_on_gle[0][0]) if outstanding_based_on_gle else 0 @@ -481,18 +591,22 @@ def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=F # if credit limit check is bypassed at sales order level, # we should not consider outstanding Sales Orders, when customer credit balance report is run if not ignore_outstanding_sales_order: - outstanding_based_on_so = frappe.db.sql(""" + outstanding_based_on_so = frappe.db.sql( + """ select sum(base_grand_total*(100 - per_billed)/100) from `tabSales Order` where customer=%s and docstatus = 1 and company=%s - and per_billed < 100 and status != 'Closed'""", (customer, company)) + and per_billed < 100 and status != 'Closed'""", + (customer, company), + ) outstanding_based_on_so = flt(outstanding_based_on_so[0][0]) if outstanding_based_on_so else 0 # Outstanding based on Delivery Note, which are not created against Sales Order outstanding_based_on_dn = 0 - unmarked_delivery_note_items = frappe.db.sql("""select + unmarked_delivery_note_items = frappe.db.sql( + """select dn_item.name, dn_item.amount, dn.base_net_total, dn.base_grand_total from `tabDelivery Note` dn, `tabDelivery Note Item` dn_item where @@ -501,21 +615,24 @@ def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=F and dn.docstatus = 1 and dn.status not in ('Closed', 'Stopped') and ifnull(dn_item.against_sales_order, '') = '' and ifnull(dn_item.against_sales_invoice, '') = '' - """, (customer, company), as_dict=True) + """, + (customer, company), + as_dict=True, + ) if not unmarked_delivery_note_items: return outstanding_based_on_gle + outstanding_based_on_so - si_amounts = frappe.db.sql(""" + si_amounts = frappe.db.sql( + """ SELECT dn_detail, sum(amount) from `tabSales Invoice Item` WHERE docstatus = 1 and dn_detail in ({}) - GROUP BY dn_detail""".format(", ".join( - frappe.db.escape(dn_item.name) - for dn_item in unmarked_delivery_note_items - )) + GROUP BY dn_detail""".format( + ", ".join(frappe.db.escape(dn_item.name) for dn_item in unmarked_delivery_note_items) + ) ) si_amounts = {si_item[0]: si_item[1] for si_item in si_amounts} @@ -525,8 +642,9 @@ def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=F si_amount = flt(si_amounts.get(dn_item.name)) if dn_amount > si_amount and dn_item.base_net_total: - outstanding_based_on_dn += ((dn_amount - si_amount) - / dn_item.base_net_total) * dn_item.base_grand_total + outstanding_based_on_dn += ( + (dn_amount - si_amount) / dn_item.base_net_total + ) * dn_item.base_grand_total return outstanding_based_on_gle + outstanding_based_on_so + outstanding_based_on_dn @@ -535,75 +653,84 @@ def get_credit_limit(customer, company): credit_limit = None if customer: - credit_limit = frappe.db.get_value("Customer Credit Limit", - {'parent': customer, 'parenttype': 'Customer', 'company': company}, 'credit_limit') + credit_limit = frappe.db.get_value( + "Customer Credit Limit", + {"parent": customer, "parenttype": "Customer", "company": company}, + "credit_limit", + ) if not credit_limit: - customer_group = frappe.get_cached_value("Customer", customer, 'customer_group') - credit_limit = frappe.db.get_value("Customer Credit Limit", - {'parent': customer_group, 'parenttype': 'Customer Group', 'company': company}, 'credit_limit') + customer_group = frappe.get_cached_value("Customer", customer, "customer_group") + credit_limit = frappe.db.get_value( + "Customer Credit Limit", + {"parent": customer_group, "parenttype": "Customer Group", "company": company}, + "credit_limit", + ) if not credit_limit: - credit_limit = frappe.get_cached_value('Company', company, "credit_limit") + credit_limit = frappe.get_cached_value("Company", company, "credit_limit") return flt(credit_limit) + def make_contact(args, is_primary_contact=1): - contact = frappe.get_doc({ - 'doctype': 'Contact', - 'first_name': args.get('name'), - 'is_primary_contact': is_primary_contact, - 'links': [{ - 'link_doctype': args.get('doctype'), - 'link_name': args.get('name') - }] - }) - if args.get('email_id'): - contact.add_email(args.get('email_id'), is_primary=True) - if args.get('mobile_no'): - contact.add_phone(args.get('mobile_no'), is_primary_mobile_no=True) + contact = frappe.get_doc( + { + "doctype": "Contact", + "first_name": args.get("name"), + "is_primary_contact": is_primary_contact, + "links": [{"link_doctype": args.get("doctype"), "link_name": args.get("name")}], + } + ) + if args.get("email_id"): + contact.add_email(args.get("email_id"), is_primary=True) + if args.get("mobile_no"): + contact.add_phone(args.get("mobile_no"), is_primary_mobile_no=True) contact.insert() return contact + def make_address(args, is_primary_address=1): reqd_fields = [] - for field in ['city', 'country']: + for field in ["city", "country"]: if not args.get(field): - reqd_fields.append( '
  • ' + field.title() + '
  • ') + reqd_fields.append("
  • " + field.title() + "
  • ") if reqd_fields: msg = _("Following fields are mandatory to create address:") - frappe.throw("{0}

      {1}
    ".format(msg, '\n'.join(reqd_fields)), - title = _("Missing Values Required")) + frappe.throw( + "{0}

      {1}
    ".format(msg, "\n".join(reqd_fields)), + title=_("Missing Values Required"), + ) - address = frappe.get_doc({ - 'doctype': 'Address', - 'address_title': args.get('name'), - 'address_line1': args.get('address_line1'), - 'address_line2': args.get('address_line2'), - 'city': args.get('city'), - 'state': args.get('state'), - 'pincode': args.get('pincode'), - 'country': args.get('country'), - 'links': [{ - 'link_doctype': args.get('doctype'), - 'link_name': args.get('name') - }] - }).insert() + address = frappe.get_doc( + { + "doctype": "Address", + "address_title": args.get("name"), + "address_line1": args.get("address_line1"), + "address_line2": args.get("address_line2"), + "city": args.get("city"), + "state": args.get("state"), + "pincode": args.get("pincode"), + "country": args.get("country"), + "links": [{"link_doctype": args.get("doctype"), "link_name": args.get("name")}], + } + ).insert() return address + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, filters): - customer = filters.get('customer') - return frappe.db.sql(""" + customer = filters.get("customer") + return frappe.db.sql( + """ select `tabContact`.name from `tabContact`, `tabDynamic Link` where `tabContact`.name = `tabDynamic Link`.parent and `tabDynamic Link`.link_name = %(customer)s and `tabDynamic Link`.link_doctype = 'Customer' and `tabContact`.name like %(txt)s - """, { - 'customer': customer, - 'txt': '%%%s%%' % txt - }) + """, + {"customer": customer, "txt": "%%%s%%" % txt}, + ) diff --git a/erpnext/selling/doctype/customer/customer_dashboard.py b/erpnext/selling/doctype/customer/customer_dashboard.py index 58394d0acb..1b2296381e 100644 --- a/erpnext/selling/doctype/customer/customer_dashboard.py +++ b/erpnext/selling/doctype/customer/customer_dashboard.py @@ -3,47 +3,29 @@ from frappe import _ def get_data(): return { - 'heatmap': True, - 'heatmap_message': _('This is based on transactions against this Customer. See timeline below for details'), - 'fieldname': 'customer', - 'non_standard_fieldnames': { - 'Payment Entry': 'party', - 'Quotation': 'party_name', - 'Opportunity': 'party_name', - 'Bank Account': 'party', - 'Subscription': 'party' + "heatmap": True, + "heatmap_message": _( + "This is based on transactions against this Customer. See timeline below for details" + ), + "fieldname": "customer", + "non_standard_fieldnames": { + "Payment Entry": "party", + "Quotation": "party_name", + "Opportunity": "party_name", + "Bank Account": "party", + "Subscription": "party", }, - 'dynamic_links': { - 'party_name': ['Customer', 'quotation_to'] - }, - 'transactions': [ + "dynamic_links": {"party_name": ["Customer", "quotation_to"]}, + "transactions": [ + {"label": _("Pre Sales"), "items": ["Opportunity", "Quotation"]}, + {"label": _("Orders"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]}, + {"label": _("Payments"), "items": ["Payment Entry", "Bank Account"]}, { - 'label': _('Pre Sales'), - 'items': ['Opportunity', 'Quotation'] + "label": _("Support"), + "items": ["Issue", "Maintenance Visit", "Installation Note", "Warranty Claim"], }, - { - 'label': _('Orders'), - 'items': ['Sales Order', 'Delivery Note', 'Sales Invoice'] - }, - { - 'label': _('Payments'), - 'items': ['Payment Entry', 'Bank Account'] - }, - { - 'label': _('Support'), - 'items': ['Issue', 'Maintenance Visit', 'Installation Note', 'Warranty Claim'] - }, - { - 'label': _('Projects'), - 'items': ['Project'] - }, - { - 'label': _('Pricing'), - 'items': ['Pricing Rule'] - }, - { - 'label': _('Subscriptions'), - 'items': ['Subscription'] - } - ] + {"label": _("Projects"), "items": ["Project"]}, + {"label": _("Pricing"), "items": ["Pricing Rule"]}, + {"label": _("Subscriptions"), "items": ["Subscription"]}, + ], } diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 165ee81872..4027d2ee14 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -13,18 +13,17 @@ from erpnext.selling.doctype.customer.customer import get_credit_limit, get_cust from erpnext.tests.utils import create_test_contact_and_address test_ignore = ["Price List"] -test_dependencies = ['Payment Term', 'Payment Terms Template'] -test_records = frappe.get_test_records('Customer') - +test_dependencies = ["Payment Term", "Payment Terms Template"] +test_records = frappe.get_test_records("Customer") class TestCustomer(FrappeTestCase): def setUp(self): - if not frappe.get_value('Item', '_Test Item'): - make_test_records('Item') + if not frappe.get_value("Item", "_Test Item"): + make_test_records("Item") def tearDown(self): - set_credit_limit('_Test Customer', '_Test Company', 0) + set_credit_limit("_Test Customer", "_Test Company", 0) def test_get_customer_group_details(self): doc = frappe.new_doc("Customer Group") @@ -37,10 +36,7 @@ class TestCustomer(FrappeTestCase): "company": "_Test Company", "account": "Creditors - _TC", } - test_credit_limits = { - "company": "_Test Company", - "credit_limit": 350000 - } + test_credit_limits = {"company": "_Test Company", "credit_limit": 350000} doc.append("accounts", test_account_details) doc.append("credit_limits", test_credit_limits) doc.insert() @@ -49,7 +45,7 @@ class TestCustomer(FrappeTestCase): c_doc.customer_name = "Testing Customer" c_doc.customer_group = "_Testing Customer Group" c_doc.payment_terms = c_doc.default_price_list = "" - c_doc.accounts = c_doc.credit_limits= [] + c_doc.accounts = c_doc.credit_limits = [] c_doc.insert() c_doc.get_customer_group_details() self.assertEqual(c_doc.payment_terms, "_Test Payment Term Template 3") @@ -66,25 +62,26 @@ class TestCustomer(FrappeTestCase): from erpnext.accounts.party import get_party_details to_check = { - 'selling_price_list': None, - 'customer_group': '_Test Customer Group', - 'contact_designation': None, - 'customer_address': '_Test Address for Customer-Office', - 'contact_department': None, - 'contact_email': 'test_contact_customer@example.com', - 'contact_mobile': None, - 'sales_team': [], - 'contact_display': '_Test Contact for _Test Customer', - 'contact_person': '_Test Contact for _Test Customer-_Test Customer', - 'territory': u'_Test Territory', - 'contact_phone': '+91 0000000000', - 'customer_name': '_Test Customer' + "selling_price_list": None, + "customer_group": "_Test Customer Group", + "contact_designation": None, + "customer_address": "_Test Address for Customer-Office", + "contact_department": None, + "contact_email": "test_contact_customer@example.com", + "contact_mobile": None, + "sales_team": [], + "contact_display": "_Test Contact for _Test Customer", + "contact_person": "_Test Contact for _Test Customer-_Test Customer", + "territory": "_Test Territory", + "contact_phone": "+91 0000000000", + "customer_name": "_Test Customer", } create_test_contact_and_address() - frappe.db.set_value("Contact", "_Test Contact for _Test Customer-_Test Customer", - "is_primary_contact", 1) + frappe.db.set_value( + "Contact", "_Test Contact for _Test Customer-_Test Customer", "is_primary_contact", 1 + ) details = get_party_details("_Test Customer") @@ -105,32 +102,30 @@ class TestCustomer(FrappeTestCase): details = get_party_details("_Test Customer With Tax Category") self.assertEqual(details.tax_category, "_Test Tax Category 1") - billing_address = frappe.get_doc(dict( - doctype='Address', - address_title='_Test Address With Tax Category', - tax_category='_Test Tax Category 2', - address_type='Billing', - address_line1='Station Road', - city='_Test City', - country='India', - links=[dict( - link_doctype='Customer', - link_name='_Test Customer With Tax Category' - )] - )).insert() - shipping_address = frappe.get_doc(dict( - doctype='Address', - address_title='_Test Address With Tax Category', - tax_category='_Test Tax Category 3', - address_type='Shipping', - address_line1='Station Road', - city='_Test City', - country='India', - links=[dict( - link_doctype='Customer', - link_name='_Test Customer With Tax Category' - )] - )).insert() + billing_address = frappe.get_doc( + dict( + doctype="Address", + address_title="_Test Address With Tax Category", + tax_category="_Test Tax Category 2", + address_type="Billing", + address_line1="Station Road", + city="_Test City", + country="India", + links=[dict(link_doctype="Customer", link_name="_Test Customer With Tax Category")], + ) + ).insert() + shipping_address = frappe.get_doc( + dict( + doctype="Address", + address_title="_Test Address With Tax Category", + tax_category="_Test Tax Category 3", + address_type="Shipping", + address_line1="Station Road", + city="_Test City", + country="India", + links=[dict(link_doctype="Customer", link_name="_Test Customer With Tax Category")], + ) + ).insert() settings = frappe.get_single("Accounts Settings") rollback_setting = settings.determine_address_tax_category_from @@ -158,12 +153,16 @@ class TestCustomer(FrappeTestCase): new_name = "_Test Customer 1 Renamed" for name in ("_Test Customer 1", new_name): - frappe.db.sql("""delete from `tabComment` + frappe.db.sql( + """delete from `tabComment` where reference_doctype=%s and reference_name=%s""", - ("Customer", name)) + ("Customer", name), + ) # add comments - comment = frappe.get_doc("Customer", "_Test Customer 1").add_comment("Comment", "Test Comment for Rename") + comment = frappe.get_doc("Customer", "_Test Customer 1").add_comment( + "Comment", "Test Comment for Rename" + ) # rename frappe.rename_doc("Customer", "_Test Customer 1", new_name) @@ -173,11 +172,17 @@ class TestCustomer(FrappeTestCase): self.assertFalse(frappe.db.exists("Customer", "_Test Customer 1")) # test that comment gets linked to renamed doc - self.assertEqual(frappe.db.get_value("Comment", { - "reference_doctype": "Customer", - "reference_name": new_name, - "content": "Test Comment for Rename" - }), comment.name) + self.assertEqual( + frappe.db.get_value( + "Comment", + { + "reference_doctype": "Customer", + "reference_name": new_name, + "content": "Test Comment for Rename", + }, + ), + comment.name, + ) # rename back to original frappe.rename_doc("Customer", new_name, "_Test Customer 1") @@ -191,7 +196,7 @@ class TestCustomer(FrappeTestCase): from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order - so = make_sales_order(do_not_save= True) + so = make_sales_order(do_not_save=True) self.assertRaises(PartyFrozen, so.save) @@ -200,13 +205,14 @@ class TestCustomer(FrappeTestCase): so.save() def test_delete_customer_contact(self): - customer = frappe.get_doc( - get_customer_dict('_Test Customer for delete')).insert(ignore_permissions=True) + customer = frappe.get_doc(get_customer_dict("_Test Customer for delete")).insert( + ignore_permissions=True + ) customer.mobile_no = "8989889890" customer.save() self.assertTrue(customer.customer_primary_contact) - frappe.delete_doc('Customer', customer.name) + frappe.delete_doc("Customer", customer.name) def test_disabled_customer(self): make_test_records("Item") @@ -227,13 +233,15 @@ class TestCustomer(FrappeTestCase): frappe.db.sql("delete from `tabCustomer` where customer_name='_Test Customer 1'") if not frappe.db.get_value("Customer", "_Test Customer 1"): - test_customer_1 = frappe.get_doc( - get_customer_dict('_Test Customer 1')).insert(ignore_permissions=True) + test_customer_1 = frappe.get_doc(get_customer_dict("_Test Customer 1")).insert( + ignore_permissions=True + ) else: test_customer_1 = frappe.get_doc("Customer", "_Test Customer 1") - duplicate_customer = frappe.get_doc( - get_customer_dict('_Test Customer 1')).insert(ignore_permissions=True) + duplicate_customer = frappe.get_doc(get_customer_dict("_Test Customer 1")).insert( + ignore_permissions=True + ) self.assertEqual("_Test Customer 1", test_customer_1.name) self.assertEqual("_Test Customer 1 - 1", duplicate_customer.name) @@ -241,15 +249,16 @@ class TestCustomer(FrappeTestCase): def get_customer_outstanding_amount(self): from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order - outstanding_amt = get_customer_outstanding('_Test Customer', '_Test Company') + + outstanding_amt = get_customer_outstanding("_Test Customer", "_Test Company") # If outstanding is negative make a transaction to get positive outstanding amount if outstanding_amt > 0.0: return outstanding_amt - item_qty = int((abs(outstanding_amt) + 200)/100) + item_qty = int((abs(outstanding_amt) + 200) / 100) make_sales_order(qty=item_qty) - return get_customer_outstanding('_Test Customer', '_Test Company') + return get_customer_outstanding("_Test Customer", "_Test Company") def test_customer_credit_limit(self): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice @@ -258,14 +267,14 @@ class TestCustomer(FrappeTestCase): from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note outstanding_amt = self.get_customer_outstanding_amount() - credit_limit = get_credit_limit('_Test Customer', '_Test Company') + credit_limit = get_credit_limit("_Test Customer", "_Test Company") if outstanding_amt <= 0.0: - item_qty = int((abs(outstanding_amt) + 200)/100) + item_qty = int((abs(outstanding_amt) + 200) / 100) make_sales_order(qty=item_qty) if not credit_limit: - set_credit_limit('_Test Customer', '_Test Company', outstanding_amt - 50) + set_credit_limit("_Test Customer", "_Test Company", outstanding_amt - 50) # Sales Order so = make_sales_order(do_not_submit=True) @@ -280,7 +289,7 @@ class TestCustomer(FrappeTestCase): self.assertRaises(frappe.ValidationError, si.submit) if credit_limit > outstanding_amt: - set_credit_limit('_Test Customer', '_Test Company', credit_limit) + set_credit_limit("_Test Customer", "_Test Company", credit_limit) # Makes Sales invoice from Sales Order so.save(ignore_permissions=True) @@ -290,16 +299,21 @@ class TestCustomer(FrappeTestCase): def test_customer_credit_limit_on_change(self): outstanding_amt = self.get_customer_outstanding_amount() - customer = frappe.get_doc("Customer", '_Test Customer') - customer.append('credit_limits', {'credit_limit': flt(outstanding_amt - 100), 'company': '_Test Company'}) + customer = frappe.get_doc("Customer", "_Test Customer") + customer.append( + "credit_limits", {"credit_limit": flt(outstanding_amt - 100), "company": "_Test Company"} + ) - ''' define new credit limit for same company ''' - customer.append('credit_limits', {'credit_limit': flt(outstanding_amt - 100), 'company': '_Test Company'}) + """ define new credit limit for same company """ + customer.append( + "credit_limits", {"credit_limit": flt(outstanding_amt - 100), "company": "_Test Company"} + ) self.assertRaises(frappe.ValidationError, customer.save) def test_customer_payment_terms(self): frappe.db.set_value( - "Customer", "_Test Customer With Template", "payment_terms", "_Test Payment Term Template 3") + "Customer", "_Test Customer With Template", "payment_terms", "_Test Payment Term Template 3" + ) due_date = get_due_date("2016-01-22", "Customer", "_Test Customer With Template") self.assertEqual(due_date, "2016-02-21") @@ -308,7 +322,8 @@ class TestCustomer(FrappeTestCase): self.assertEqual(due_date, "2017-02-21") frappe.db.set_value( - "Customer", "_Test Customer With Template", "payment_terms", "_Test Payment Term Template 1") + "Customer", "_Test Customer With Template", "payment_terms", "_Test Payment Term Template 1" + ) due_date = get_due_date("2016-01-22", "Customer", "_Test Customer With Template") self.assertEqual(due_date, "2016-02-29") @@ -328,13 +343,14 @@ class TestCustomer(FrappeTestCase): def get_customer_dict(customer_name): return { - "customer_group": "_Test Customer Group", - "customer_name": customer_name, - "customer_type": "Individual", - "doctype": "Customer", - "territory": "_Test Territory" + "customer_group": "_Test Customer Group", + "customer_name": customer_name, + "customer_type": "Individual", + "doctype": "Customer", + "territory": "_Test Territory", } + def set_credit_limit(customer, company, credit_limit): customer = frappe.get_doc("Customer", customer) existing_row = None @@ -346,27 +362,25 @@ def set_credit_limit(customer, company, credit_limit): break if not existing_row: - customer.append('credit_limits', { - 'company': company, - 'credit_limit': credit_limit - }) + customer.append("credit_limits", {"company": company, "credit_limit": credit_limit}) customer.credit_limits[-1].db_insert() + def create_internal_customer(customer_name, represents_company, allowed_to_interact_with): if not frappe.db.exists("Customer", customer_name): - customer = frappe.get_doc({ - "doctype": "Customer", - "customer_group": "_Test Customer Group", - "customer_name": customer_name, - "customer_type": "Individual", - "territory": "_Test Territory", - "is_internal_customer": 1, - "represents_company": represents_company - }) + customer = frappe.get_doc( + { + "doctype": "Customer", + "customer_group": "_Test Customer Group", + "customer_name": customer_name, + "customer_type": "Individual", + "territory": "_Test Territory", + "is_internal_customer": 1, + "represents_company": represents_company, + } + ) - customer.append("companies", { - "company": allowed_to_interact_with - }) + customer.append("companies", {"company": allowed_to_interact_with}) customer.insert() customer_name = customer.name diff --git a/erpnext/selling/doctype/industry_type/test_industry_type.py b/erpnext/selling/doctype/industry_type/test_industry_type.py index 250c2bec48..eb5f905f10 100644 --- a/erpnext/selling/doctype/industry_type/test_industry_type.py +++ b/erpnext/selling/doctype/industry_type/test_industry_type.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Industry Type') +test_records = frappe.get_test_records("Industry Type") diff --git a/erpnext/selling/doctype/installation_note/installation_note.py b/erpnext/selling/doctype/installation_note/installation_note.py index 36acdbea61..dd0b1e8751 100644 --- a/erpnext/selling/doctype/installation_note/installation_note.py +++ b/erpnext/selling/doctype/installation_note/installation_note.py @@ -13,26 +13,29 @@ from erpnext.utilities.transaction_base import TransactionBase class InstallationNote(TransactionBase): def __init__(self, *args, **kwargs): super(InstallationNote, self).__init__(*args, **kwargs) - self.status_updater = [{ - 'source_dt': 'Installation Note Item', - 'target_dt': 'Delivery Note Item', - 'target_field': 'installed_qty', - 'target_ref_field': 'qty', - 'join_field': 'prevdoc_detail_docname', - 'target_parent_dt': 'Delivery Note', - 'target_parent_field': 'per_installed', - 'source_field': 'qty', - 'percent_join_field': 'prevdoc_docname', - 'status_field': 'installation_status', - 'keyword': 'Installed', - 'overflow_type': 'installation' - }] + self.status_updater = [ + { + "source_dt": "Installation Note Item", + "target_dt": "Delivery Note Item", + "target_field": "installed_qty", + "target_ref_field": "qty", + "join_field": "prevdoc_detail_docname", + "target_parent_dt": "Delivery Note", + "target_parent_field": "per_installed", + "source_field": "qty", + "percent_join_field": "prevdoc_docname", + "status_field": "installation_status", + "keyword": "Installed", + "overflow_type": "installation", + } + ] def validate(self): self.validate_installation_date() self.check_item_table() from erpnext.controllers.selling_controller import set_default_income_account_for_item + set_default_income_account_for_item(self) def is_serial_no_added(self, item_code, serial_no): @@ -48,18 +51,19 @@ class InstallationNote(TransactionBase): frappe.throw(_("Serial No {0} does not exist").format(x)) def get_prevdoc_serial_no(self, prevdoc_detail_docname): - serial_nos = frappe.db.get_value("Delivery Note Item", - prevdoc_detail_docname, "serial_no") + serial_nos = frappe.db.get_value("Delivery Note Item", prevdoc_detail_docname, "serial_no") return get_valid_serial_nos(serial_nos) def is_serial_no_match(self, cur_s_no, prevdoc_s_no, prevdoc_docname): for sr in cur_s_no: if sr not in prevdoc_s_no: - frappe.throw(_("Serial No {0} does not belong to Delivery Note {1}").format(sr, prevdoc_docname)) + frappe.throw( + _("Serial No {0} does not belong to Delivery Note {1}").format(sr, prevdoc_docname) + ) def validate_serial_no(self): prevdoc_s_no, sr_list = [], [] - for d in self.get('items'): + for d in self.get("items"): self.is_serial_no_added(d.item_code, d.serial_no) if d.serial_no: sr_list = get_valid_serial_nos(d.serial_no, d.qty, d.item_code) @@ -69,26 +73,27 @@ class InstallationNote(TransactionBase): if prevdoc_s_no: self.is_serial_no_match(sr_list, prevdoc_s_no, d.prevdoc_docname) - def validate_installation_date(self): - for d in self.get('items'): + for d in self.get("items"): if d.prevdoc_docname: d_date = frappe.db.get_value("Delivery Note", d.prevdoc_docname, "posting_date") if d_date > getdate(self.inst_date): - frappe.throw(_("Installation date cannot be before delivery date for Item {0}").format(d.item_code)) + frappe.throw( + _("Installation date cannot be before delivery date for Item {0}").format(d.item_code) + ) def check_item_table(self): - if not(self.get('items')): + if not (self.get("items")): frappe.throw(_("Please pull items from Delivery Note")) def on_update(self): - frappe.db.set(self, 'status', 'Draft') + frappe.db.set(self, "status", "Draft") def on_submit(self): self.validate_serial_no() self.update_prevdoc_status() - frappe.db.set(self, 'status', 'Submitted') + frappe.db.set(self, "status", "Submitted") def on_cancel(self): self.update_prevdoc_status() - frappe.db.set(self, 'status', 'Cancelled') + frappe.db.set(self, "status", "Cancelled") diff --git a/erpnext/selling/doctype/installation_note/test_installation_note.py b/erpnext/selling/doctype/installation_note/test_installation_note.py index d3c8be5357..56e0fe160a 100644 --- a/erpnext/selling/doctype/installation_note/test_installation_note.py +++ b/erpnext/selling/doctype/installation_note/test_installation_note.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Installation Note') + class TestInstallationNote(unittest.TestCase): pass diff --git a/erpnext/selling/doctype/party_specific_item/party_specific_item.py b/erpnext/selling/doctype/party_specific_item/party_specific_item.py index a408af5642..0aef7d362e 100644 --- a/erpnext/selling/doctype/party_specific_item/party_specific_item.py +++ b/erpnext/selling/doctype/party_specific_item/party_specific_item.py @@ -8,12 +8,14 @@ from frappe.model.document import Document class PartySpecificItem(Document): def validate(self): - exists = frappe.db.exists({ - 'doctype': 'Party Specific Item', - 'party_type': self.party_type, - 'party': self.party, - 'restrict_based_on': self.restrict_based_on, - 'based_on': self.based_on_value, - }) + exists = frappe.db.exists( + { + "doctype": "Party Specific Item", + "party_type": self.party_type, + "party": self.party, + "restrict_based_on": self.restrict_based_on, + "based_on": self.based_on_value, + } + ) if exists: frappe.throw(_("This item filter has already been applied for the {0}").format(self.party_type)) diff --git a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py index 9b672b4b5d..f98cbd7e9a 100644 --- a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py +++ b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py @@ -6,16 +6,18 @@ from frappe.tests.utils import FrappeTestCase from erpnext.controllers.queries import item_query -test_dependencies = ['Item', 'Customer', 'Supplier'] +test_dependencies = ["Item", "Customer", "Supplier"] + def create_party_specific_item(**args): psi = frappe.new_doc("Party Specific Item") - psi.party_type = args.get('party_type') - psi.party = args.get('party') - psi.restrict_based_on = args.get('restrict_based_on') - psi.based_on_value = args.get('based_on_value') + psi.party_type = args.get("party_type") + psi.party = args.get("party") + psi.restrict_based_on = args.get("restrict_based_on") + psi.based_on_value = args.get("based_on_value") psi.insert() + class TestPartySpecificItem(FrappeTestCase): def setUp(self): self.customer = frappe.get_last_doc("Customer") @@ -23,15 +25,29 @@ class TestPartySpecificItem(FrappeTestCase): self.item = frappe.get_last_doc("Item") def test_item_query_for_customer(self): - create_party_specific_item(party_type='Customer', party=self.customer.name, restrict_based_on='Item', based_on_value=self.item.name) - filters = {'is_sales_item': 1, 'customer': self.customer.name} - items = item_query(doctype= 'Item', txt= '', searchfield= 'name', start= 0, page_len= 20,filters=filters, as_dict= False) + create_party_specific_item( + party_type="Customer", + party=self.customer.name, + restrict_based_on="Item", + based_on_value=self.item.name, + ) + filters = {"is_sales_item": 1, "customer": self.customer.name} + items = item_query( + doctype="Item", txt="", searchfield="name", start=0, page_len=20, filters=filters, as_dict=False + ) for item in items: self.assertEqual(item[0], self.item.name) def test_item_query_for_supplier(self): - create_party_specific_item(party_type='Supplier', party=self.supplier.name, restrict_based_on='Item Group', based_on_value=self.item.item_group) - filters = {'supplier': self.supplier.name, 'is_purchase_item': 1} - items = item_query(doctype= 'Item', txt= '', searchfield= 'name', start= 0, page_len= 20,filters=filters, as_dict= False) + create_party_specific_item( + party_type="Supplier", + party=self.supplier.name, + restrict_based_on="Item Group", + based_on_value=self.item.item_group, + ) + filters = {"supplier": self.supplier.name, "is_purchase_item": 1} + items = item_query( + doctype="Item", txt="", searchfield="name", start=0, page_len=20, filters=filters, as_dict=False + ) for item in items: self.assertEqual(item[2], self.item.item_group) diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py index 2bb876e6d0..575b956686 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/product_bundle.py @@ -16,11 +16,22 @@ class ProductBundle(Document): self.validate_main_item() self.validate_child_items() from erpnext.utilities.transaction_base import validate_uom_is_integer + validate_uom_is_integer(self, "uom", "qty") def on_trash(self): - linked_doctypes = ["Delivery Note", "Sales Invoice", "POS Invoice", "Purchase Receipt", "Purchase Invoice", - "Stock Entry", "Stock Reconciliation", "Sales Order", "Purchase Order", "Material Request"] + linked_doctypes = [ + "Delivery Note", + "Sales Invoice", + "POS Invoice", + "Purchase Receipt", + "Purchase Invoice", + "Stock Entry", + "Stock Reconciliation", + "Sales Order", + "Purchase Order", + "Material Request", + ] invoice_links = [] for doctype in linked_doctypes: @@ -29,15 +40,20 @@ class ProductBundle(Document): if doctype == "Stock Entry": item_doctype = doctype + " Detail" - invoices = frappe.db.get_all(item_doctype, {"item_code": self.new_item_code, "docstatus": 1}, ["parent"]) + invoices = frappe.db.get_all( + item_doctype, {"item_code": self.new_item_code, "docstatus": 1}, ["parent"] + ) for invoice in invoices: - invoice_links.append(get_link_to_form(doctype, invoice['parent'])) + invoice_links.append(get_link_to_form(doctype, invoice["parent"])) if len(invoice_links): frappe.throw( - "This Product Bundle is linked with {0}. You will have to cancel these documents in order to delete this Product Bundle" - .format(", ".join(invoice_links)), title=_("Not Allowed")) + "This Product Bundle is linked with {0}. You will have to cancel these documents in order to delete this Product Bundle".format( + ", ".join(invoice_links) + ), + title=_("Not Allowed"), + ) def validate_main_item(self): """Validates, main Item is not a stock item""" @@ -47,15 +63,22 @@ class ProductBundle(Document): def validate_child_items(self): for item in self.items: if frappe.db.exists("Product Bundle", item.item_code): - frappe.throw(_("Row #{0}: Child Item should not be a Product Bundle. Please remove Item {1} and Save").format(item.idx, frappe.bold(item.item_code))) + frappe.throw( + _( + "Row #{0}: Child Item should not be a Product Bundle. Please remove Item {1} and Save" + ).format(item.idx, frappe.bold(item.item_code)) + ) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_new_item_code(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond - return frappe.db.sql("""select name, item_name, description from tabItem + return frappe.db.sql( + """select name, item_name, description from tabItem where is_stock_item=0 and name not in (select name from `tabProduct Bundle`) - and %s like %s %s limit %s, %s""" % (searchfield, "%s", - get_match_cond(doctype),"%s", "%s"), - ("%%%s%%" % txt, start, page_len)) + and %s like %s %s limit %s, %s""" + % (searchfield, "%s", get_match_cond(doctype), "%s", "%s"), + ("%%%s%%" % txt, start, page_len), + ) diff --git a/erpnext/selling/doctype/product_bundle/test_product_bundle.py b/erpnext/selling/doctype/product_bundle/test_product_bundle.py index c1e2fdee8b..82fe892edf 100644 --- a/erpnext/selling/doctype/product_bundle/test_product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/test_product_bundle.py @@ -3,16 +3,14 @@ import frappe -test_records = frappe.get_test_records('Product Bundle') +test_records = frappe.get_test_records("Product Bundle") + def make_product_bundle(parent, items, qty=None): if frappe.db.exists("Product Bundle", parent): return frappe.get_doc("Product Bundle", parent) - product_bundle = frappe.get_doc({ - "doctype": "Product Bundle", - "new_item_code": parent - }) + product_bundle = frappe.get_doc({"doctype": "Product Bundle", "new_item_code": parent}) for item in items: product_bundle.append("items", {"item_code": item, "qty": qty or 1}) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index d602f0ca94..61c0b8af4e 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -10,18 +10,17 @@ from frappe.utils import flt, getdate, nowdate from erpnext.controllers.selling_controller import SellingController from erpnext.crm.utils import add_link_in_communication, copy_comments -form_grid_templates = { - "items": "templates/form_grid/item_grid.html" -} +form_grid_templates = {"items": "templates/form_grid/item_grid.html"} + class Quotation(SellingController): def set_indicator(self): - if self.docstatus==1: - self.indicator_color = 'blue' - self.indicator_title = 'Submitted' + if self.docstatus == 1: + self.indicator_color = "blue" + self.indicator_title = "Submitted" if self.valid_till and getdate(self.valid_till) < getdate(nowdate()): - self.indicator_color = 'gray' - self.indicator_title = 'Expired' + self.indicator_color = "gray" + self.indicator_title = "Expired" def validate(self): super(Quotation, self).validate() @@ -33,6 +32,7 @@ class Quotation(SellingController): self.with_items = 1 from erpnext.stock.doctype.packed_item.packed_item import make_packing_list + make_packing_list(self) def after_insert(self): @@ -57,10 +57,12 @@ class Quotation(SellingController): frappe.get_doc("Lead", self.party_name).set_status(update=True) def set_customer_name(self): - if self.party_name and self.quotation_to == 'Customer': + if self.party_name and self.quotation_to == "Customer": self.customer_name = frappe.db.get_value("Customer", self.party_name, "customer_name") - elif self.party_name and self.quotation_to == 'Lead': - lead_name, company_name = frappe.db.get_value("Lead", self.party_name, ["lead_name", "company_name"]) + elif self.party_name and self.quotation_to == "Lead": + 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, status): @@ -81,24 +83,27 @@ class Quotation(SellingController): @frappe.whitelist() def declare_enquiry_lost(self, lost_reasons_list, competitors, detailed_reason=None): if not self.has_sales_order(): - get_lost_reasons = frappe.get_list('Quotation Lost Reason', - fields = ["name"]) - lost_reasons_lst = [reason.get('name') for reason in get_lost_reasons] - frappe.db.set(self, 'status', 'Lost') + get_lost_reasons = frappe.get_list("Quotation Lost Reason", fields=["name"]) + lost_reasons_lst = [reason.get("name") for reason in get_lost_reasons] + frappe.db.set(self, "status", "Lost") if detailed_reason: - frappe.db.set(self, 'order_lost_reason', detailed_reason) + frappe.db.set(self, "order_lost_reason", detailed_reason) for reason in lost_reasons_list: - if reason.get('lost_reason') in lost_reasons_lst: - self.append('lost_reasons', reason) + if reason.get("lost_reason") in lost_reasons_lst: + self.append("lost_reasons", reason) else: - frappe.throw(_("Invalid lost reason {0}, please create a new lost reason").format(frappe.bold(reason.get('lost_reason')))) + frappe.throw( + _("Invalid lost reason {0}, please create a new lost reason").format( + frappe.bold(reason.get("lost_reason")) + ) + ) for competitor in competitors: - self.append('competitors', competitor) + self.append("competitors", competitor) - self.update_opportunity('Lost') + self.update_opportunity("Lost") self.update_lead() self.save() @@ -107,11 +112,12 @@ class Quotation(SellingController): def on_submit(self): # Check for Approving Authority - frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, - self.company, self.base_grand_total, self) + frappe.get_doc("Authorization Control").validate_approving_authority( + self.doctype, self.company, self.base_grand_total, self + ) - #update enquiry status - self.update_opportunity('Quotation') + # update enquiry status + self.update_opportunity("Quotation") self.update_lead() def on_cancel(self): @@ -119,14 +125,14 @@ class Quotation(SellingController): self.lost_reasons = [] super(Quotation, self).on_cancel() - #update enquiry status + # update enquiry status self.set_status(update=True) - self.update_opportunity('Open') + self.update_opportunity("Open") self.update_lead() - def print_other_charges(self,docname): + def print_other_charges(self, docname): print_lst = [] - for d in self.get('taxes'): + for d in self.get("taxes"): lst1 = [] lst1.append(d.description) lst1.append(d.total) @@ -136,25 +142,35 @@ class Quotation(SellingController): def on_recurring(self, reference_doc, auto_repeat_doc): self.valid_till = None + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context + list_context = get_list_context(context) - list_context.update({ - 'show_sidebar': True, - 'show_search': True, - 'no_breadcrumbs': True, - 'title': _('Quotations'), - }) + list_context.update( + { + "show_sidebar": True, + "show_search": True, + "no_breadcrumbs": True, + "title": _("Quotations"), + } + ) return list_context + @frappe.whitelist() def make_sales_order(source_name, target_doc=None): - quotation = frappe.db.get_value("Quotation", source_name, ["transaction_date", "valid_till"], as_dict = 1) - if quotation.valid_till and (quotation.valid_till < quotation.transaction_date or quotation.valid_till < getdate(nowdate())): + quotation = frappe.db.get_value( + "Quotation", source_name, ["transaction_date", "valid_till"], as_dict=1 + ) + if quotation.valid_till and ( + quotation.valid_till < quotation.transaction_date or quotation.valid_till < getdate(nowdate()) + ): frappe.throw(_("Validity period of this quotation has ended.")) return _make_sales_order(source_name, target_doc) + def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): customer = _make_customer(source_name, ignore_permissions) @@ -163,8 +179,10 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): target.customer = customer.name target.customer_name = customer.customer_name if source.referral_sales_partner: - target.sales_partner=source.referral_sales_partner - target.commission_rate=frappe.get_value('Sales Partner', source.referral_sales_partner, 'commission_rate') + target.sales_partner = source.referral_sales_partner + target.commission_rate = frappe.get_value( + "Sales Partner", source.referral_sales_partner, "commission_rate" + ) target.flags.ignore_permissions = ignore_permissions target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") @@ -177,39 +195,31 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): target.blanket_order = obj.blanket_order target.blanket_order_rate = obj.blanket_order_rate - doclist = get_mapped_doc("Quotation", source_name, { - "Quotation": { - "doctype": "Sales Order", - "validation": { - "docstatus": ["=", 1] - } - }, + doclist = get_mapped_doc( + "Quotation", + source_name, + { + "Quotation": {"doctype": "Sales Order", "validation": {"docstatus": ["=", 1]}}, "Quotation Item": { "doctype": "Sales Order Item", - "field_map": { - "parent": "prevdoc_docname" - }, - "postprocess": update_item + "field_map": {"parent": "prevdoc_docname"}, + "postprocess": update_item, }, - "Sales Taxes and Charges": { - "doctype": "Sales Taxes and Charges", - "add_if_empty": True - }, - "Sales Team": { - "doctype": "Sales Team", - "add_if_empty": True - }, - "Payment Schedule": { - "doctype": "Payment Schedule", - "add_if_empty": True - } - }, target_doc, set_missing_values, ignore_permissions=ignore_permissions) + "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, + "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, + "Payment Schedule": {"doctype": "Payment Schedule", "add_if_empty": True}, + }, + target_doc, + set_missing_values, + ignore_permissions=ignore_permissions, + ) # postprocess: fetch shipping address, set missing values - doclist.set_onload('ignore_price_list', True) + doclist.set_onload("ignore_price_list", True) return doclist + def set_expired_status(): # filter out submitted non expired quotations whose validity has been ended cond = "qo.docstatus = 1 and qo.status != 'Expired' and qo.valid_till < %s" @@ -224,15 +234,18 @@ def set_expired_status(): # if not exists any SO, set status as Expired frappe.db.sql( - """UPDATE `tabQuotation` qo SET qo.status = 'Expired' WHERE {cond} and not exists({so_against_quo})""" - .format(cond=cond, so_against_quo=so_against_quo), - (nowdate()) - ) + """UPDATE `tabQuotation` qo SET qo.status = 'Expired' WHERE {cond} and not exists({so_against_quo})""".format( + cond=cond, so_against_quo=so_against_quo + ), + (nowdate()), + ) + @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None): return _make_sales_invoice(source_name, target_doc) + def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): customer = _make_customer(source_name, ignore_permissions) @@ -249,54 +262,52 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): target.cost_center = None target.stock_qty = flt(obj.qty) * flt(obj.conversion_factor) - doclist = get_mapped_doc("Quotation", source_name, { - "Quotation": { - "doctype": "Sales Invoice", - "validation": { - "docstatus": ["=", 1] - } - }, - "Quotation Item": { - "doctype": "Sales Invoice Item", - "postprocess": update_item - }, - "Sales Taxes and Charges": { - "doctype": "Sales Taxes and Charges", - "add_if_empty": True - }, - "Sales Team": { - "doctype": "Sales Team", - "add_if_empty": True - } - }, target_doc, set_missing_values, ignore_permissions=ignore_permissions) + doclist = get_mapped_doc( + "Quotation", + source_name, + { + "Quotation": {"doctype": "Sales Invoice", "validation": {"docstatus": ["=", 1]}}, + "Quotation Item": {"doctype": "Sales Invoice Item", "postprocess": update_item}, + "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, + "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, + }, + target_doc, + set_missing_values, + ignore_permissions=ignore_permissions, + ) - doclist.set_onload('ignore_price_list', True) + doclist.set_onload("ignore_price_list", True) return doclist -def _make_customer(source_name, ignore_permissions=False): - quotation = frappe.db.get_value("Quotation", - source_name, ["order_type", "party_name", "customer_name"], as_dict=1) - if quotation and quotation.get('party_name'): +def _make_customer(source_name, ignore_permissions=False): + quotation = frappe.db.get_value( + "Quotation", source_name, ["order_type", "party_name", "customer_name"], as_dict=1 + ) + + if quotation and quotation.get("party_name"): if not frappe.db.exists("Customer", quotation.get("party_name")): lead_name = quotation.get("party_name") - customer_name = frappe.db.get_value("Customer", {"lead_name": lead_name}, - ["name", "customer_name"], as_dict=True) + customer_name = frappe.db.get_value( + "Customer", {"lead_name": lead_name}, ["name", "customer_name"], as_dict=True + ) if not customer_name: from erpnext.crm.doctype.lead.lead import _make_customer + customer_doclist = _make_customer(lead_name, ignore_permissions=ignore_permissions) customer = frappe.get_doc(customer_doclist) customer.flags.ignore_permissions = ignore_permissions if quotation.get("party_name") == "Shopping Cart": - customer.customer_group = frappe.db.get_value("E Commerce Settings", None, - "default_customer_group") + customer.customer_group = frappe.db.get_value( + "E Commerce Settings", None, "default_customer_group" + ) try: customer.insert() return customer except frappe.NameError: - if frappe.defaults.get_global_default('cust_master_name') == "Customer Name": + if frappe.defaults.get_global_default("cust_master_name") == "Customer Name": customer.run_method("autoname") customer.name += "-" + lead_name customer.insert() @@ -304,12 +315,14 @@ def _make_customer(source_name, ignore_permissions=False): else: raise except frappe.MandatoryError as e: - mandatory_fields = e.args[0].split(':')[1].split(',') + mandatory_fields = e.args[0].split(":")[1].split(",") mandatory_fields = [customer.meta.get_label(field.strip()) for field in mandatory_fields] frappe.local.message_log = [] lead_link = frappe.utils.get_link_to_form("Lead", lead_name) - message = _("Could not auto create Customer due to the following missing mandatory field(s):") + "
    " + message = ( + _("Could not auto create Customer due to the following missing mandatory field(s):") + "
    " + ) message += "
    • " + "
    • ".join(mandatory_fields) + "
    " message += _("Please create Customer from Lead {0}.").format(lead_link) diff --git a/erpnext/selling/doctype/quotation/quotation_dashboard.py b/erpnext/selling/doctype/quotation/quotation_dashboard.py index 0a1aad7bb6..7bfa034c53 100644 --- a/erpnext/selling/doctype/quotation/quotation_dashboard.py +++ b/erpnext/selling/doctype/quotation/quotation_dashboard.py @@ -3,18 +3,12 @@ from frappe import _ def get_data(): return { - 'fieldname': 'prevdoc_docname', - 'non_standard_fieldnames': { - 'Auto Repeat': 'reference_document', + "fieldname": "prevdoc_docname", + "non_standard_fieldnames": { + "Auto Repeat": "reference_document", }, - 'transactions': [ - { - 'label': _('Sales Order'), - 'items': ['Sales Order'] - }, - { - 'label': _('Subscription'), - 'items': ['Auto Repeat'] - }, - ] + "transactions": [ + {"label": _("Sales Order"), "items": ["Sales Order"]}, + {"label": _("Subscription"), "items": ["Auto Repeat"]}, + ], } diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index a749d9e1f1..b44fa5e551 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -11,7 +11,7 @@ test_dependencies = ["Product Bundle"] class TestQuotation(FrappeTestCase): def test_make_quotation_without_terms(self): quotation = make_quotation(do_not_save=1) - self.assertFalse(quotation.get('payment_schedule')) + self.assertFalse(quotation.get("payment_schedule")) quotation.insert() @@ -28,7 +28,7 @@ class TestQuotation(FrappeTestCase): sales_order = make_sales_order(quotation.name) - self.assertTrue(sales_order.get('payment_schedule')) + self.assertTrue(sales_order.get("payment_schedule")) def test_make_sales_order_with_different_currency(self): from erpnext.selling.doctype.quotation.quotation import make_sales_order @@ -80,9 +80,7 @@ class TestQuotation(FrappeTestCase): quotation = frappe.copy_doc(test_records[0]) quotation.transaction_date = nowdate() quotation.valid_till = add_months(quotation.transaction_date, 1) - quotation.update( - {"payment_terms_template": "_Test Payment Term Template"} - ) + quotation.update({"payment_terms_template": "_Test Payment Term Template"}) quotation.insert() self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name) @@ -92,7 +90,9 @@ class TestQuotation(FrappeTestCase): self.assertEqual(quotation.payment_schedule[0].payment_amount, 8906.00) self.assertEqual(quotation.payment_schedule[0].due_date, quotation.transaction_date) self.assertEqual(quotation.payment_schedule[1].payment_amount, 8906.00) - self.assertEqual(quotation.payment_schedule[1].due_date, add_days(quotation.transaction_date, 30)) + self.assertEqual( + quotation.payment_schedule[1].due_date, add_days(quotation.transaction_date, 30) + ) sales_order = make_sales_order(quotation.name) @@ -108,7 +108,7 @@ class TestQuotation(FrappeTestCase): sales_order.insert() # Remove any unknown taxes if applied - sales_order.set('taxes', []) + sales_order.set("taxes", []) sales_order.save() self.assertEqual(sales_order.payment_schedule[0].payment_amount, 8906.00) @@ -137,11 +137,11 @@ class TestQuotation(FrappeTestCase): make_sales_invoice, ) - rate_with_margin = flt((1500*18.75)/100 + 1500) + rate_with_margin = flt((1500 * 18.75) / 100 + 1500) - test_records[0]['items'][0]['price_list_rate'] = 1500 - test_records[0]['items'][0]['margin_type'] = 'Percentage' - test_records[0]['items'][0]['margin_rate_or_amount'] = 18.75 + test_records[0]["items"][0]["price_list_rate"] = 1500 + test_records[0]["items"][0]["margin_type"] = "Percentage" + test_records[0]["items"][0]["margin_rate_or_amount"] = 18.75 quotation = frappe.copy_doc(test_records[0]) quotation.transaction_date = nowdate() @@ -174,11 +174,9 @@ class TestQuotation(FrappeTestCase): def test_create_two_quotations(self): from erpnext.stock.doctype.item.test_item import make_item - first_item = make_item("_Test Laptop", - {"is_stock_item": 1}) + first_item = make_item("_Test Laptop", {"is_stock_item": 1}) - second_item = make_item("_Test CPU", - {"is_stock_item": 1}) + second_item = make_item("_Test CPU", {"is_stock_item": 1}) qo_item1 = [ { @@ -187,7 +185,7 @@ class TestQuotation(FrappeTestCase): "qty": 2, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", } ] @@ -197,7 +195,7 @@ class TestQuotation(FrappeTestCase): "warehouse": "_Test Warehouse - _TC", "qty": 2, "rate": 300, - "conversion_factor": 1.0 + "conversion_factor": 1.0, } ] @@ -209,17 +207,12 @@ class TestQuotation(FrappeTestCase): def test_quotation_expiry(self): from erpnext.selling.doctype.quotation.quotation import set_expired_status - quotation_item = [ - { - "item_code": "_Test Item", - "warehouse":"", - "qty": 1, - "rate": 500 - } - ] + quotation_item = [{"item_code": "_Test Item", "warehouse": "", "qty": 1, "rate": 500}] yesterday = add_days(nowdate(), -1) - expired_quotation = make_quotation(item_list=quotation_item, transaction_date=yesterday, do_not_submit=True) + expired_quotation = make_quotation( + item_list=quotation_item, transaction_date=yesterday, do_not_submit=True + ) expired_quotation.valid_till = yesterday expired_quotation.save() expired_quotation.submit() @@ -236,24 +229,49 @@ class TestQuotation(FrappeTestCase): make_item("_Test Bundle Item 1", {"is_stock_item": 1}) make_item("_Test Bundle Item 2", {"is_stock_item": 1}) - make_product_bundle("_Test Product Bundle", - ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle", ["_Test Bundle Item 1", "_Test Bundle Item 2"]) quotation = make_quotation(item_code="_Test Product Bundle", qty=1, rate=100) sales_order = make_sales_order(quotation.name) - quotation_item = [quotation.items[0].item_code, quotation.items[0].rate, quotation.items[0].qty, quotation.items[0].amount] - so_item = [sales_order.items[0].item_code, sales_order.items[0].rate, sales_order.items[0].qty, sales_order.items[0].amount] + quotation_item = [ + quotation.items[0].item_code, + quotation.items[0].rate, + quotation.items[0].qty, + quotation.items[0].amount, + ] + so_item = [ + sales_order.items[0].item_code, + sales_order.items[0].rate, + sales_order.items[0].qty, + sales_order.items[0].amount, + ] self.assertEqual(quotation_item, so_item) quotation_packed_items = [ - [quotation.packed_items[0].parent_item, quotation.packed_items[0].item_code, quotation.packed_items[0].qty], - [quotation.packed_items[1].parent_item, quotation.packed_items[1].item_code, quotation.packed_items[1].qty] + [ + quotation.packed_items[0].parent_item, + quotation.packed_items[0].item_code, + quotation.packed_items[0].qty, + ], + [ + quotation.packed_items[1].parent_item, + quotation.packed_items[1].item_code, + quotation.packed_items[1].qty, + ], ] so_packed_items = [ - [sales_order.packed_items[0].parent_item, sales_order.packed_items[0].item_code, sales_order.packed_items[0].qty], - [sales_order.packed_items[1].parent_item, sales_order.packed_items[1].item_code, sales_order.packed_items[1].qty] + [ + sales_order.packed_items[0].parent_item, + sales_order.packed_items[0].item_code, + sales_order.packed_items[0].qty, + ], + [ + sales_order.packed_items[1].parent_item, + sales_order.packed_items[1].item_code, + sales_order.packed_items[1].qty, + ], ] self.assertEqual(quotation_packed_items, so_packed_items) @@ -266,8 +284,7 @@ class TestQuotation(FrappeTestCase): bundle_item1 = make_item("_Test Bundle Item 1", {"is_stock_item": 1}) bundle_item2 = make_item("_Test Bundle Item 2", {"is_stock_item": 1}) - make_product_bundle("_Test Product Bundle", - ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle", ["_Test Bundle Item 1", "_Test Bundle Item 2"]) bundle_item1.valuation_rate = 100 bundle_item1.save() @@ -286,8 +303,7 @@ class TestQuotation(FrappeTestCase): make_item("_Test Bundle Item 1", {"is_stock_item": 1}) make_item("_Test Bundle Item 2", {"is_stock_item": 1}) - make_product_bundle("_Test Product Bundle", - ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle", ["_Test Bundle Item 1", "_Test Bundle Item 2"]) enable_calculate_bundle_price() @@ -301,7 +317,9 @@ class TestQuotation(FrappeTestCase): enable_calculate_bundle_price(enable=0) - def test_product_bundle_price_calculation_for_multiple_product_bundles_when_calculate_bundle_price_is_checked(self): + def test_product_bundle_price_calculation_for_multiple_product_bundles_when_calculate_bundle_price_is_checked( + self, + ): from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.stock.doctype.item.test_item import make_item @@ -311,10 +329,8 @@ class TestQuotation(FrappeTestCase): make_item("_Test Bundle Item 2", {"is_stock_item": 1}) make_item("_Test Bundle Item 3", {"is_stock_item": 1}) - make_product_bundle("_Test Product Bundle 1", - ["_Test Bundle Item 1", "_Test Bundle Item 2"]) - make_product_bundle("_Test Product Bundle 2", - ["_Test Bundle Item 2", "_Test Bundle Item 3"]) + make_product_bundle("_Test Product Bundle 1", ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle 2", ["_Test Bundle Item 2", "_Test Bundle Item 3"]) enable_calculate_bundle_price() @@ -325,7 +341,7 @@ class TestQuotation(FrappeTestCase): "qty": 1, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", }, { "item_code": "_Test Product Bundle 2", @@ -333,8 +349,8 @@ class TestQuotation(FrappeTestCase): "qty": 1, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' - } + "supplier": "_Test Supplier", + }, ] quotation = make_quotation(item_list=item_list, do_not_submit=1) @@ -347,7 +363,7 @@ class TestQuotation(FrappeTestCase): expected_values = [300, 500] for item in quotation.items: - self.assertEqual(item.amount, expected_values[item.idx-1]) + self.assertEqual(item.amount, expected_values[item.idx - 1]) enable_calculate_bundle_price(enable=0) @@ -362,12 +378,9 @@ class TestQuotation(FrappeTestCase): make_item("_Test Bundle Item 2", {"is_stock_item": 1}) make_item("_Test Bundle Item 3", {"is_stock_item": 1}) - make_product_bundle("_Test Product Bundle 1", - ["_Test Bundle Item 1", "_Test Bundle Item 2"]) - make_product_bundle("_Test Product Bundle 2", - ["_Test Bundle Item 2", "_Test Bundle Item 3"]) - make_product_bundle("_Test Product Bundle 3", - ["_Test Bundle Item 3", "_Test Bundle Item 1"]) + make_product_bundle("_Test Product Bundle 1", ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle 2", ["_Test Bundle Item 2", "_Test Bundle Item 3"]) + make_product_bundle("_Test Product Bundle 3", ["_Test Bundle Item 3", "_Test Bundle Item 1"]) item_list = [ { @@ -376,7 +389,7 @@ class TestQuotation(FrappeTestCase): "qty": 1, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", }, { "item_code": "_Test Product Bundle 2", @@ -384,7 +397,7 @@ class TestQuotation(FrappeTestCase): "qty": 1, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", }, { "item_code": "_Test Product Bundle 3", @@ -392,8 +405,8 @@ class TestQuotation(FrappeTestCase): "qty": 1, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' - } + "supplier": "_Test Supplier", + }, ] quotation = make_quotation(item_list=item_list, do_not_submit=1) @@ -404,29 +417,26 @@ class TestQuotation(FrappeTestCase): expected_index = id + 1 self.assertEqual(item.idx, expected_index) -test_records = frappe.get_test_records('Quotation') + +test_records = frappe.get_test_records("Quotation") + def enable_calculate_bundle_price(enable=1): selling_settings = frappe.get_doc("Selling Settings") selling_settings.editable_bundle_item_rates = enable selling_settings.save() + def get_quotation_dict(party_name=None, item_code=None): if not party_name: - party_name = '_Test Customer' + party_name = "_Test Customer" if not item_code: - item_code = '_Test Item' + item_code = "_Test Item" return { - 'doctype': 'Quotation', - 'party_name': party_name, - 'items': [ - { - 'item_code': item_code, - 'qty': 1, - 'rate': 100 - } - ] + "doctype": "Quotation", + "party_name": party_name, + "items": [{"item_code": item_code, "qty": 1, "rate": 100}], } @@ -450,13 +460,16 @@ def make_quotation(**args): qo.append("items", item) else: - qo.append("items", { - "item_code": args.item or args.item_code or "_Test Item", - "warehouse": args.warehouse, - "qty": args.qty or 10, - "uom": args.uom or None, - "rate": args.rate or 100 - }) + qo.append( + "items", + { + "item_code": args.item or args.item_code or "_Test Item", + "warehouse": args.warehouse, + "qty": args.qty or 10, + "uom": args.uom or None, + "rate": args.rate or 100, + }, + ) qo.delivery_date = add_days(qo.transaction_date, 10) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index b906ec0631..d3b4286be5 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -27,11 +27,12 @@ from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.stock.doctype.item.item import get_item_defaults from erpnext.stock.stock_balance import get_reserved_qty, update_bin_qty -form_grid_templates = { - "items": "templates/form_grid/item_grid.html" -} +form_grid_templates = {"items": "templates/form_grid/item_grid.html"} + + +class WarehouseRequired(frappe.ValidationError): + pass -class WarehouseRequired(frappe.ValidationError): pass class SalesOrder(SellingController): def __init__(self, *args, **kwargs): @@ -48,20 +49,26 @@ class SalesOrder(SellingController): self.validate_warehouse() self.validate_drop_ship() self.validate_serial_no_based_delivery() - validate_inter_company_party(self.doctype, self.customer, self.company, self.inter_company_order_reference) + validate_inter_company_party( + self.doctype, self.customer, self.company, self.inter_company_order_reference + ) if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code + validate_coupon_code(self.coupon_code) from erpnext.stock.doctype.packed_item.packed_item import make_packing_list + make_packing_list(self) self.validate_with_previous_doc() self.set_status() - if not self.billing_status: self.billing_status = 'Not Billed' - if not self.delivery_status: self.delivery_status = 'Not Delivered' + if not self.billing_status: + self.billing_status = "Not Billed" + if not self.delivery_status: + self.delivery_status = "Not Delivered" self.reset_default_field_value("set_warehouse", "items", "warehouse") @@ -70,55 +77,82 @@ class SalesOrder(SellingController): if self.po_date and not self.skip_delivery_note: for d in self.get("items"): if d.delivery_date and getdate(self.po_date) > getdate(d.delivery_date): - frappe.throw(_("Row #{0}: Expected Delivery Date cannot be before Purchase Order Date") - .format(d.idx)) + frappe.throw( + _("Row #{0}: Expected Delivery Date cannot be before Purchase Order Date").format(d.idx) + ) if self.po_no and self.customer and not self.skip_delivery_note: - so = frappe.db.sql("select name from `tabSales Order` \ + so = frappe.db.sql( + "select name from `tabSales Order` \ where ifnull(po_no, '') = %s and name != %s and docstatus < 2\ - and customer = %s", (self.po_no, self.name, self.customer)) - if so and so[0][0] and not cint(frappe.db.get_single_value("Selling Settings", - "allow_against_multiple_purchase_orders")): - frappe.msgprint(_("Warning: Sales Order {0} already exists against Customer's Purchase Order {1}").format(so[0][0], self.po_no)) + and customer = %s", + (self.po_no, self.name, self.customer), + ) + if ( + so + and so[0][0] + and not cint( + frappe.db.get_single_value("Selling Settings", "allow_against_multiple_purchase_orders") + ) + ): + frappe.msgprint( + _("Warning: Sales Order {0} already exists against Customer's Purchase Order {1}").format( + so[0][0], self.po_no + ) + ) def validate_for_items(self): - for d in self.get('items'): + for d in self.get("items"): # used for production plan d.transaction_date = self.transaction_date - tot_avail_qty = frappe.db.sql("select projected_qty from `tabBin` \ - where item_code = %s and warehouse = %s", (d.item_code, d.warehouse)) + tot_avail_qty = frappe.db.sql( + "select projected_qty from `tabBin` \ + where item_code = %s and warehouse = %s", + (d.item_code, d.warehouse), + ) d.projected_qty = tot_avail_qty and flt(tot_avail_qty[0][0]) or 0 def product_bundle_has_stock_item(self, product_bundle): """Returns true if product bundle has stock item""" - ret = len(frappe.db.sql("""select i.name from tabItem i, `tabProduct Bundle Item` pbi - where pbi.parent = %s and pbi.item_code = i.name and i.is_stock_item = 1""", product_bundle)) + ret = len( + frappe.db.sql( + """select i.name from tabItem i, `tabProduct Bundle Item` pbi + where pbi.parent = %s and pbi.item_code = i.name and i.is_stock_item = 1""", + product_bundle, + ) + ) return ret def validate_sales_mntc_quotation(self): - for d in self.get('items'): + for d in self.get("items"): if d.prevdoc_docname: - res = frappe.db.sql("select name from `tabQuotation` where name=%s and order_type = %s", - (d.prevdoc_docname, self.order_type)) + res = frappe.db.sql( + "select name from `tabQuotation` where name=%s and order_type = %s", + (d.prevdoc_docname, self.order_type), + ) if not res: - frappe.msgprint(_("Quotation {0} not of type {1}") - .format(d.prevdoc_docname, self.order_type)) + frappe.msgprint(_("Quotation {0} not of type {1}").format(d.prevdoc_docname, self.order_type)) def validate_delivery_date(self): - if self.order_type == 'Sales' and not self.skip_delivery_note: + if self.order_type == "Sales" and not self.skip_delivery_note: delivery_date_list = [d.delivery_date for d in self.get("items") if d.delivery_date] max_delivery_date = max(delivery_date_list) if delivery_date_list else None - if (max_delivery_date and not self.delivery_date) or (max_delivery_date and getdate(self.delivery_date) != getdate(max_delivery_date)): + if (max_delivery_date and not self.delivery_date) or ( + max_delivery_date and getdate(self.delivery_date) != getdate(max_delivery_date) + ): self.delivery_date = max_delivery_date if self.delivery_date: for d in self.get("items"): if not d.delivery_date: d.delivery_date = self.delivery_date if getdate(self.transaction_date) > getdate(d.delivery_date): - frappe.msgprint(_("Expected Delivery Date should be after Sales Order Date"), - indicator='orange', title=_('Warning')) + frappe.msgprint( + _("Expected Delivery Date should be after Sales Order Date"), + indicator="orange", + title=_("Warning"), + ) else: frappe.throw(_("Please enter Delivery Date")) @@ -126,47 +160,56 @@ class SalesOrder(SellingController): def validate_proj_cust(self): if self.project and self.customer_name: - res = frappe.db.sql("""select name from `tabProject` where name = %s + res = frappe.db.sql( + """select name from `tabProject` where name = %s and (customer = %s or ifnull(customer,'')='')""", - (self.project, self.customer)) + (self.project, self.customer), + ) if not res: - frappe.throw(_("Customer {0} does not belong to project {1}").format(self.customer, self.project)) + frappe.throw( + _("Customer {0} does not belong to project {1}").format(self.customer, self.project) + ) def validate_warehouse(self): super(SalesOrder, self).validate_warehouse() for d in self.get("items"): - if (frappe.get_cached_value("Item", d.item_code, "is_stock_item") == 1 or - (self.has_product_bundle(d.item_code) and self.product_bundle_has_stock_item(d.item_code))) \ - and not d.warehouse and not cint(d.delivered_by_supplier): - frappe.throw(_("Delivery warehouse required for stock item {0}").format(d.item_code), - WarehouseRequired) + if ( + ( + frappe.get_cached_value("Item", d.item_code, "is_stock_item") == 1 + or (self.has_product_bundle(d.item_code) and self.product_bundle_has_stock_item(d.item_code)) + ) + and not d.warehouse + and not cint(d.delivered_by_supplier) + ): + frappe.throw( + _("Delivery warehouse required for stock item {0}").format(d.item_code), WarehouseRequired + ) def validate_with_previous_doc(self): - super(SalesOrder, self).validate_with_previous_doc({ - "Quotation": { - "ref_dn_field": "prevdoc_docname", - "compare_fields": [["company", "="]] - } - }) - + super(SalesOrder, self).validate_with_previous_doc( + {"Quotation": {"ref_dn_field": "prevdoc_docname", "compare_fields": [["company", "="]]}} + ) def update_enquiry_status(self, prevdoc, flag): - enq = frappe.db.sql("select t2.prevdoc_docname from `tabQuotation` t1, `tabQuotation Item` t2 where t2.parent = t1.name and t1.name=%s", prevdoc) + enq = frappe.db.sql( + "select t2.prevdoc_docname from `tabQuotation` t1, `tabQuotation Item` t2 where t2.parent = t1.name and t1.name=%s", + prevdoc, + ) if enq: - frappe.db.sql("update `tabOpportunity` set status = %s where name=%s",(flag,enq[0][0])) + frappe.db.sql("update `tabOpportunity` set status = %s where name=%s", (flag, enq[0][0])) def update_prevdoc_status(self, flag=None): for quotation in set(d.prevdoc_docname for d in self.get("items")): if quotation: doc = frappe.get_doc("Quotation", quotation) - if doc.docstatus==2: + if doc.docstatus == 2: frappe.throw(_("Quotation {0} is cancelled").format(quotation)) doc.set_status(update=True) def validate_drop_ship(self): - for d in self.get('items'): + for d in self.get("items"): if d.delivered_by_supplier and not d.supplier: frappe.throw(_("Row #{0}: Set Supplier for item {1}").format(d.idx, d.item_code)) @@ -174,41 +217,47 @@ class SalesOrder(SellingController): self.check_credit_limit() self.update_reserved_qty() - frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, self.company, self.base_grand_total, self) + frappe.get_doc("Authorization Control").validate_approving_authority( + self.doctype, self.company, self.base_grand_total, self + ) self.update_project() - self.update_prevdoc_status('submit') + self.update_prevdoc_status("submit") self.update_blanket_order() update_linked_doc(self.doctype, self.name, self.inter_company_order_reference) if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count - update_coupon_code_count(self.coupon_code,'used') + + update_coupon_code_count(self.coupon_code, "used") def on_cancel(self): - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") super(SalesOrder, self).on_cancel() # Cannot cancel closed SO - if self.status == 'Closed': + if self.status == "Closed": frappe.throw(_("Closed order cannot be cancelled. Unclose to cancel.")) self.check_nextdoc_docstatus() self.update_reserved_qty() self.update_project() - self.update_prevdoc_status('cancel') + self.update_prevdoc_status("cancel") - frappe.db.set(self, 'status', 'Cancelled') + frappe.db.set(self, "status", "Cancelled") self.update_blanket_order() unlink_inter_company_doc(self.doctype, self.name, self.inter_company_order_reference) if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count - update_coupon_code_count(self.coupon_code,'cancelled') + + update_coupon_code_count(self.coupon_code, "cancelled") def update_project(self): - if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') != "Each Transaction": + if ( + frappe.db.get_single_value("Selling Settings", "sales_update_frequency") != "Each Transaction" + ): return if self.project: @@ -219,26 +268,34 @@ class SalesOrder(SellingController): def check_credit_limit(self): # if bypass credit limit check is set to true (1) at sales order level, # then we need not to check credit limit and vise versa - if not cint(frappe.db.get_value("Customer Credit Limit", - {'parent': self.customer, 'parenttype': 'Customer', 'company': self.company}, - "bypass_credit_limit_check")): + if not cint( + frappe.db.get_value( + "Customer Credit Limit", + {"parent": self.customer, "parenttype": "Customer", "company": self.company}, + "bypass_credit_limit_check", + ) + ): check_credit_limit(self.customer, self.company) def check_nextdoc_docstatus(self): - linked_invoices = frappe.db.sql_list("""select distinct t1.name + linked_invoices = frappe.db.sql_list( + """select distinct t1.name from `tabSales Invoice` t1,`tabSales Invoice Item` t2 where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus = 0""", - self.name) + self.name, + ) if linked_invoices: linked_invoices = [get_link_to_form("Sales Invoice", si) for si in linked_invoices] - frappe.throw(_("Sales Invoice {0} must be deleted before cancelling this Sales Order") - .format(", ".join(linked_invoices))) + frappe.throw( + _("Sales Invoice {0} must be deleted before cancelling this Sales Order").format( + ", ".join(linked_invoices) + ) + ) def check_modified_date(self): mod_db = frappe.db.get_value("Sales Order", self.name, "modified") - date_diff = frappe.db.sql("select TIMEDIFF('%s', '%s')" % - ( mod_db, cstr(self.modified))) + date_diff = frappe.db.sql("select TIMEDIFF('%s', '%s')" % (mod_db, cstr(self.modified))) if date_diff and date_diff[0][0]: frappe.throw(_("{0} {1} has been modified. Please refresh.").format(self.doctype, self.name)) @@ -252,10 +309,15 @@ class SalesOrder(SellingController): def update_reserved_qty(self, so_item_rows=None): """update requested qty (before ordered_qty is updated)""" item_wh_list = [] + def _valid_for_reserve(item_code, warehouse): - if item_code and warehouse and [item_code, warehouse] not in item_wh_list \ - and frappe.get_cached_value("Item", item_code, "is_stock_item"): - item_wh_list.append([item_code, warehouse]) + if ( + item_code + and warehouse + and [item_code, warehouse] not in item_wh_list + and frappe.get_cached_value("Item", item_code, "is_stock_item") + ): + item_wh_list.append([item_code, warehouse]) for d in self.get("items"): if (not so_item_rows or d.name in so_item_rows) and not d.delivered_by_supplier: @@ -267,9 +329,7 @@ class SalesOrder(SellingController): _valid_for_reserve(d.item_code, d.warehouse) for item_code, warehouse in item_wh_list: - update_bin_qty(item_code, warehouse, { - "reserved_qty": get_reserved_qty(item_code, warehouse) - }) + update_bin_qty(item_code, warehouse, {"reserved_qty": get_reserved_qty(item_code, warehouse)}) def on_update(self): pass @@ -286,13 +346,18 @@ class SalesOrder(SellingController): for item in self.items: if item.supplier: - supplier = frappe.db.get_value("Sales Order Item", {"parent": self.name, "item_code": item.item_code}, - "supplier") + supplier = frappe.db.get_value( + "Sales Order Item", {"parent": self.name, "item_code": item.item_code}, "supplier" + ) if item.ordered_qty > 0.0 and item.supplier != supplier: - exc_list.append(_("Row #{0}: Not allowed to change Supplier as Purchase Order already exists").format(item.idx)) + exc_list.append( + _("Row #{0}: Not allowed to change Supplier as Purchase Order already exists").format( + item.idx + ) + ) if exc_list: - frappe.throw('\n'.join(exc_list)) + frappe.throw("\n".join(exc_list)) def update_delivery_status(self): """Update delivery status from Purchase Order for drop shipping""" @@ -300,13 +365,16 @@ class SalesOrder(SellingController): for item in self.items: if item.delivered_by_supplier: - item_delivered_qty = frappe.db.sql("""select sum(qty) + item_delivered_qty = frappe.db.sql( + """select sum(qty) from `tabPurchase Order Item` poi, `tabPurchase Order` po where poi.sales_order_item = %s and poi.item_code = %s and poi.parent = po.name and po.docstatus = 1 - and po.status = 'Delivered'""", (item.name, item.item_code)) + and po.status = 'Delivered'""", + (item.name, item.item_code), + ) item_delivered_qty = item_delivered_qty[0][0] if item_delivered_qty else 0 item.db_set("delivered_qty", flt(item_delivered_qty), update_modified=False) @@ -315,9 +383,7 @@ class SalesOrder(SellingController): tot_qty += item.qty if tot_qty != 0: - self.db_set("per_delivered", flt(delivered_qty/tot_qty) * 100, - update_modified=False) - + self.db_set("per_delivered", flt(delivered_qty / tot_qty) * 100, update_modified=False) def set_indicator(self): """Set indicator for portal""" @@ -335,49 +401,62 @@ class SalesOrder(SellingController): @frappe.whitelist() def get_work_order_items(self, for_raw_material_request=0): - '''Returns items with BOM that already do not have a linked work order''' + """Returns items with BOM that already do not have a linked work order""" items = [] item_codes = [i.item_code for i in self.items] - product_bundle_parents = [pb.new_item_code for pb in frappe.get_all("Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"])] + product_bundle_parents = [ + pb.new_item_code + for pb in frappe.get_all( + "Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"] + ) + ] for table in [self.items, self.packed_items]: for i in table: bom = get_default_bom_item(i.item_code) - stock_qty = i.qty if i.doctype == 'Packed Item' else i.stock_qty + stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty if not for_raw_material_request: - total_work_order_qty = flt(frappe.db.sql('''select sum(qty) from `tabWork Order` - where production_item=%s and sales_order=%s and sales_order_item = %s and docstatus<2''', (i.item_code, self.name, i.name))[0][0]) + total_work_order_qty = flt( + frappe.db.sql( + """select sum(qty) from `tabWork Order` + where production_item=%s and sales_order=%s and sales_order_item = %s and docstatus<2""", + (i.item_code, self.name, i.name), + )[0][0] + ) pending_qty = stock_qty - total_work_order_qty else: pending_qty = stock_qty if pending_qty and i.item_code not in product_bundle_parents: if bom: - items.append(dict( - name= i.name, - item_code= i.item_code, - description= i.description, - bom = bom, - warehouse = i.warehouse, - pending_qty = pending_qty, - required_qty = pending_qty if for_raw_material_request else 0, - sales_order_item = i.name - )) + items.append( + dict( + name=i.name, + item_code=i.item_code, + description=i.description, + bom=bom, + warehouse=i.warehouse, + pending_qty=pending_qty, + required_qty=pending_qty if for_raw_material_request else 0, + sales_order_item=i.name, + ) + ) else: - items.append(dict( - name= i.name, - item_code= i.item_code, - description= i.description, - bom = '', - warehouse = i.warehouse, - pending_qty = pending_qty, - required_qty = pending_qty if for_raw_material_request else 0, - sales_order_item = i.name - )) + items.append( + dict( + name=i.name, + item_code=i.item_code, + description=i.description, + bom="", + warehouse=i.warehouse, + pending_qty=pending_qty, + required_qty=pending_qty if for_raw_material_request else 0, + sales_order_item=i.name, + ) + ) return items 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 = auto_repeat_doc.get_next_schedule_date(schedule_date=ref_doc_delivery_date) @@ -387,15 +466,26 @@ class SalesOrder(SellingController): return delivery_date - self.set("delivery_date", _get_delivery_date(reference_doc.delivery_date, - reference_doc.transaction_date, self.transaction_date )) + self.set( + "delivery_date", + _get_delivery_date( + reference_doc.delivery_date, reference_doc.transaction_date, self.transaction_date + ), + ) for d in self.get("items"): - reference_delivery_date = frappe.db.get_value("Sales Order Item", - {"parent": reference_doc.name, "item_code": d.item_code, "idx": d.idx}, "delivery_date") + reference_delivery_date = frappe.db.get_value( + "Sales Order Item", + {"parent": reference_doc.name, "item_code": d.item_code, "idx": d.idx}, + "delivery_date", + ) - d.set("delivery_date", _get_delivery_date(reference_delivery_date, - reference_doc.transaction_date, self.transaction_date)) + d.set( + "delivery_date", + _get_delivery_date( + reference_delivery_date, reference_doc.transaction_date, self.transaction_date + ), + ) def validate_serial_no_based_delivery(self): reserved_items = [] @@ -403,32 +493,52 @@ class SalesOrder(SellingController): for item in self.items: if item.ensure_delivery_based_on_produced_serial_no: if item.item_code in normal_items: - frappe.throw(_("Cannot ensure delivery by Serial No as Item {0} is added with and without Ensure Delivery by Serial No.").format(item.item_code)) + frappe.throw( + _( + "Cannot ensure delivery by Serial No as Item {0} is added with and without Ensure Delivery by Serial No." + ).format(item.item_code) + ) if item.item_code not in reserved_items: if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"): - frappe.throw(_("Item {0} has no Serial No. Only serilialized items can have delivery based on Serial No").format(item.item_code)) + frappe.throw( + _( + "Item {0} has no Serial No. Only serilialized items can have delivery based on Serial No" + ).format(item.item_code) + ) if not frappe.db.exists("BOM", {"item": item.item_code, "is_active": 1}): - frappe.throw(_("No active BOM found for item {0}. Delivery by Serial No cannot be ensured").format(item.item_code)) + frappe.throw( + _("No active BOM found for item {0}. Delivery by Serial No cannot be ensured").format( + item.item_code + ) + ) reserved_items.append(item.item_code) else: normal_items.append(item.item_code) - if not item.ensure_delivery_based_on_produced_serial_no and \ - item.item_code in reserved_items: - frappe.throw(_("Cannot ensure delivery by Serial No as Item {0} is added with and without Ensure Delivery by Serial No.").format(item.item_code)) + if not item.ensure_delivery_based_on_produced_serial_no and item.item_code in reserved_items: + frappe.throw( + _( + "Cannot ensure delivery by Serial No as Item {0} is added with and without Ensure Delivery by Serial No." + ).format(item.item_code) + ) + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context + list_context = get_list_context(context) - list_context.update({ - 'show_sidebar': True, - 'show_search': True, - 'no_breadcrumbs': True, - 'title': _('Orders'), - }) + list_context.update( + { + "show_sidebar": True, + "show_search": True, + "no_breadcrumbs": True, + "title": _("Orders"), + } + ) return list_context + @frappe.whitelist() def close_or_unclose_sales_orders(names, status): if not frappe.has_permission("Sales Order", "write"): @@ -439,23 +549,32 @@ def close_or_unclose_sales_orders(names, status): so = frappe.get_doc("Sales Order", name) if so.docstatus == 1: if status == "Closed": - if so.status not in ("Cancelled", "Closed") and (so.per_delivered < 100 or so.per_billed < 100): + if so.status not in ("Cancelled", "Closed") and ( + so.per_delivered < 100 or so.per_billed < 100 + ): so.update_status(status) else: if so.status == "Closed": - so.update_status('Draft') + so.update_status("Draft") so.update_blanket_order() frappe.local.message_log = [] + def get_requested_item_qty(sales_order): - return frappe._dict(frappe.db.sql(""" + return frappe._dict( + frappe.db.sql( + """ select sales_order_item, sum(qty) from `tabMaterial Request Item` where docstatus = 1 and sales_order = %s group by sales_order_item - """, sales_order)) + """, + sales_order, + ) + ) + @frappe.whitelist() def make_material_request(source_name, target_doc=None): @@ -468,55 +587,56 @@ def make_material_request(source_name, target_doc=None): target.qty = qty - requested_item_qty.get(source.name, 0) target.stock_qty = flt(target.qty) * flt(target.conversion_factor) - doc = get_mapped_doc("Sales Order", source_name, { - "Sales Order": { - "doctype": "Material Request", - "validation": { - "docstatus": ["=", 1] - } - }, - "Packed Item": { - "doctype": "Material Request Item", - "field_map": { - "parent": "sales_order", - "uom": "stock_uom" + doc = get_mapped_doc( + "Sales Order", + source_name, + { + "Sales Order": {"doctype": "Material Request", "validation": {"docstatus": ["=", 1]}}, + "Packed Item": { + "doctype": "Material Request Item", + "field_map": {"parent": "sales_order", "uom": "stock_uom"}, + "postprocess": update_item, }, - "postprocess": update_item - }, - "Sales Order Item": { - "doctype": "Material Request Item", - "field_map": { - "name": "sales_order_item", - "parent": "sales_order" + "Sales Order Item": { + "doctype": "Material Request Item", + "field_map": {"name": "sales_order_item", "parent": "sales_order"}, + "condition": lambda doc: not frappe.db.exists("Product Bundle", doc.item_code) + and doc.stock_qty > requested_item_qty.get(doc.name, 0), + "postprocess": update_item, }, - "condition": lambda doc: not frappe.db.exists('Product Bundle', doc.item_code) and doc.stock_qty > requested_item_qty.get(doc.name, 0), - "postprocess": update_item - } - }, target_doc) + }, + target_doc, + ) return doc + @frappe.whitelist() def make_project(source_name, target_doc=None): def postprocess(source, doc): doc.project_type = "External" doc.project_name = source.name - doc = get_mapped_doc("Sales Order", source_name, { - "Sales Order": { - "doctype": "Project", - "validation": { - "docstatus": ["=", 1] + doc = get_mapped_doc( + "Sales Order", + source_name, + { + "Sales Order": { + "doctype": "Project", + "validation": {"docstatus": ["=", 1]}, + "field_map": { + "name": "sales_order", + "base_grand_total": "estimated_costing", + }, }, - "field_map":{ - "name" : "sales_order", - "base_grand_total" : "estimated_costing", - } }, - }, target_doc, postprocess) + target_doc, + postprocess, + ) return doc + @frappe.whitelist() def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): def set_missing_values(source, target): @@ -525,13 +645,13 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): target.run_method("calculate_taxes_and_totals") if source.company_address: - target.update({'company_address': source.company_address}) + target.update({"company_address": source.company_address}) else: # set company address target.update(get_company_address(target.company)) if target.company_address: - target.update(get_fetch_values("Delivery Note", 'company_address', target.company_address)) + target.update(get_fetch_values("Delivery Note", "company_address", target.company_address)) def update_item(source, target, source_parent): target.base_amount = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.base_rate) @@ -542,34 +662,26 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): item_group = get_item_group_defaults(target.item_code, source_parent.company) if item: - target.cost_center = frappe.db.get_value("Project", source_parent.project, "cost_center") \ - or item.get("buying_cost_center") \ + target.cost_center = ( + frappe.db.get_value("Project", source_parent.project, "cost_center") + or item.get("buying_cost_center") or item_group.get("buying_cost_center") + ) mapper = { - "Sales Order": { - "doctype": "Delivery Note", - "validation": { - "docstatus": ["=", 1] - } - }, - "Sales Taxes and Charges": { - "doctype": "Sales Taxes and Charges", - "add_if_empty": True - }, - "Sales Team": { - "doctype": "Sales Team", - "add_if_empty": True - } + "Sales Order": {"doctype": "Delivery Note", "validation": {"docstatus": ["=", 1]}}, + "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, + "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, } if not skip_item_mapping: + def condition(doc): # make_mapped_doc sets js `args` into `frappe.flags.args` if frappe.flags.args and frappe.flags.args.delivery_dates: if cstr(doc.delivery_date) not in frappe.flags.args.delivery_dates: return False - return abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1 + return abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier != 1 mapper["Sales Order Item"] = { "doctype": "Delivery Note Item", @@ -579,20 +691,21 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): "parent": "against_sales_order", }, "postprocess": update_item, - "condition": condition + "condition": condition, } target_doc = get_mapped_doc("Sales Order", source_name, mapper, target_doc, set_missing_values) - target_doc.set_onload('ignore_price_list', True) + target_doc.set_onload("ignore_price_list", True) return target_doc + @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): def postprocess(source, target): set_missing_values(source, target) - #Get the advance paid Journal Entries in Sales Invoice Advance + # Get the advance paid Journal Entries in Sales Invoice Advance if target.get("allocate_advances_automatically"): target.set_advances() @@ -603,13 +716,13 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): target.run_method("calculate_taxes_and_totals") if source.company_address: - target.update({'company_address': source.company_address}) + target.update({"company_address": source.company_address}) else: # set company address target.update(get_company_address(target.company)) if target.company_address: - target.update(get_fetch_values("Sales Invoice", 'company_address', target.company_address)) + target.update(get_fetch_values("Sales Invoice", "company_address", target.company_address)) # set the redeem loyalty points if provided via shopping cart if source.loyalty_points and source.order_type == "Shopping Cart": @@ -618,108 +731,117 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): def update_item(source, target, source_parent): target.amount = flt(source.amount) - flt(source.billed_amt) target.base_amount = target.amount * flt(source_parent.conversion_rate) - target.qty = target.amount / flt(source.rate) if (source.rate and source.billed_amt) else source.qty - source.returned_qty + target.qty = ( + target.amount / flt(source.rate) + if (source.rate and source.billed_amt) + else source.qty - source.returned_qty + ) if source_parent.project: target.cost_center = frappe.db.get_value("Project", source_parent.project, "cost_center") if target.item_code: item = get_item_defaults(target.item_code, source_parent.company) item_group = get_item_group_defaults(target.item_code, source_parent.company) - cost_center = item.get("selling_cost_center") \ - or item_group.get("selling_cost_center") + cost_center = item.get("selling_cost_center") or item_group.get("selling_cost_center") if cost_center: target.cost_center = cost_center - doclist = get_mapped_doc("Sales Order", source_name, { - "Sales Order": { - "doctype": "Sales Invoice", - "field_map": { - "party_account_currency": "party_account_currency", - "payment_terms_template": "payment_terms_template" + doclist = get_mapped_doc( + "Sales Order", + source_name, + { + "Sales Order": { + "doctype": "Sales Invoice", + "field_map": { + "party_account_currency": "party_account_currency", + "payment_terms_template": "payment_terms_template", + }, + "field_no_map": ["payment_terms_template"], + "validation": {"docstatus": ["=", 1]}, }, - "field_no_map": ["payment_terms_template"], - "validation": { - "docstatus": ["=", 1] - } - }, - "Sales Order Item": { - "doctype": "Sales Invoice Item", - "field_map": { - "name": "so_detail", - "parent": "sales_order", + "Sales Order Item": { + "doctype": "Sales Invoice Item", + "field_map": { + "name": "so_detail", + "parent": "sales_order", + }, + "postprocess": update_item, + "condition": lambda doc: doc.qty + and (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)), }, - "postprocess": update_item, - "condition": lambda doc: doc.qty and (doc.base_amount==0 or abs(doc.billed_amt) < abs(doc.amount)) + "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, + "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, }, - "Sales Taxes and Charges": { - "doctype": "Sales Taxes and Charges", - "add_if_empty": True - }, - "Sales Team": { - "doctype": "Sales Team", - "add_if_empty": True - } - }, target_doc, postprocess, ignore_permissions=ignore_permissions) + target_doc, + postprocess, + ignore_permissions=ignore_permissions, + ) - automatically_fetch_payment_terms = cint(frappe.db.get_single_value('Accounts Settings', 'automatically_fetch_payment_terms')) + automatically_fetch_payment_terms = cint( + frappe.db.get_single_value("Accounts Settings", "automatically_fetch_payment_terms") + ) if automatically_fetch_payment_terms: doclist.set_payment_schedule() - doclist.set_onload('ignore_price_list', True) + doclist.set_onload("ignore_price_list", True) return doclist + @frappe.whitelist() def make_maintenance_schedule(source_name, target_doc=None): - maint_schedule = frappe.db.sql("""select t1.name + maint_schedule = frappe.db.sql( + """select t1.name from `tabMaintenance Schedule` t1, `tabMaintenance Schedule Item` t2 - where t2.parent=t1.name and t2.sales_order=%s and t1.docstatus=1""", source_name) + where t2.parent=t1.name and t2.sales_order=%s and t1.docstatus=1""", + source_name, + ) if not maint_schedule: - doclist = get_mapped_doc("Sales Order", source_name, { - "Sales Order": { - "doctype": "Maintenance Schedule", - "validation": { - "docstatus": ["=", 1] - } + doclist = get_mapped_doc( + "Sales Order", + source_name, + { + "Sales Order": {"doctype": "Maintenance Schedule", "validation": {"docstatus": ["=", 1]}}, + "Sales Order Item": { + "doctype": "Maintenance Schedule Item", + "field_map": {"parent": "sales_order"}, + }, }, - "Sales Order Item": { - "doctype": "Maintenance Schedule Item", - "field_map": { - "parent": "sales_order" - } - } - }, target_doc) + target_doc, + ) return doclist + @frappe.whitelist() def make_maintenance_visit(source_name, target_doc=None): - visit = frappe.db.sql("""select t1.name + visit = frappe.db.sql( + """select t1.name from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 where t2.parent=t1.name and t2.prevdoc_docname=%s - and t1.docstatus=1 and t1.completion_status='Fully Completed'""", source_name) + and t1.docstatus=1 and t1.completion_status='Fully Completed'""", + source_name, + ) if not visit: - doclist = get_mapped_doc("Sales Order", source_name, { - "Sales Order": { - "doctype": "Maintenance Visit", - "validation": { - "docstatus": ["=", 1] - } + doclist = get_mapped_doc( + "Sales Order", + source_name, + { + "Sales Order": {"doctype": "Maintenance Visit", "validation": {"docstatus": ["=", 1]}}, + "Sales Order Item": { + "doctype": "Maintenance Visit Purpose", + "field_map": {"parent": "prevdoc_docname", "parenttype": "prevdoc_doctype"}, + }, }, - "Sales Order Item": { - "doctype": "Maintenance Visit Purpose", - "field_map": { - "parent": "prevdoc_docname", - "parenttype": "prevdoc_doctype" - } - } - }, target_doc) + target_doc, + ) return doclist + @frappe.whitelist() def get_events(start, end, filters=None): """Returns events for Gantt / Calendar view rendering. @@ -729,9 +851,11 @@ def get_events(start, end, filters=None): :param filters: Filters (JSON). """ from frappe.desk.calendar import get_event_conditions + conditions = get_event_conditions("Sales Order", filters) - data = frappe.db.sql(""" + data = frappe.db.sql( + """ select distinct `tabSales Order`.name, `tabSales Order`.customer_name, `tabSales Order`.status, `tabSales Order`.delivery_status, `tabSales Order`.billing_status, @@ -744,16 +868,21 @@ def get_events(start, end, filters=None): and (`tabSales Order Item`.delivery_date between %(start)s and %(end)s) and `tabSales Order`.docstatus < 2 {conditions} - """.format(conditions=conditions), { - "start": start, - "end": end - }, as_dict=True, update={"allDay": 0}) + """.format( + conditions=conditions + ), + {"start": start, "end": end}, + as_dict=True, + update={"allDay": 0}, + ) return data + @frappe.whitelist() def make_purchase_order_for_default_supplier(source_name, selected_items=None, target_doc=None): """Creates Purchase Order for each Supplier. Returns a list of doc objects.""" - if not selected_items: return + if not selected_items: + return if isinstance(selected_items, str): selected_items = json.loads(selected_items) @@ -769,7 +898,7 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t if default_price_list: target.buying_price_list = default_price_list - if any( item.delivered_by_supplier==1 for item in source.items): + if any(item.delivered_by_supplier == 1 for item in source.items): if source.shipping_address_name: target.shipping_address = source.shipping_address_name target.shipping_address_display = source.shipping_address @@ -792,59 +921,67 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t def update_item(source, target, source_parent): target.schedule_date = source.delivery_date target.qty = flt(source.qty) - (flt(source.ordered_qty) / flt(source.conversion_factor)) - target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty)) + target.stock_qty = flt(source.stock_qty) - flt(source.ordered_qty) target.project = source_parent.project - suppliers = [item.get('supplier') for item in selected_items if item.get('supplier')] - suppliers = list(dict.fromkeys(suppliers)) # remove duplicates while preserving order + suppliers = [item.get("supplier") for item in selected_items if item.get("supplier")] + suppliers = list(dict.fromkeys(suppliers)) # remove duplicates while preserving order - items_to_map = [item.get('item_code') for item in selected_items if item.get('item_code')] + items_to_map = [item.get("item_code") for item in selected_items if item.get("item_code")] items_to_map = list(set(items_to_map)) if not suppliers: - frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order.")) + frappe.throw( + _("Please set a Supplier against the Items to be considered in the Purchase Order.") + ) purchase_orders = [] for supplier in suppliers: - 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", - "terms" - ], - "validation": { - "docstatus": ["=", 1] - } + 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", + "terms", + ], + "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", + "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, + }, }, - "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", - "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 - } - }, target_doc, set_missing_values) + target_doc, + set_missing_values, + ) doc.insert() frappe.db.commit() @@ -852,14 +989,20 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t return purchase_orders + @frappe.whitelist() def make_purchase_order(source_name, selected_items=None, target_doc=None): - if not selected_items: return + if not selected_items: + return if isinstance(selected_items, str): selected_items = json.loads(selected_items) - items_to_map = [item.get('item_code') for item in selected_items if item.get('item_code') and item.get('item_code')] + items_to_map = [ + item.get("item_code") + for item in selected_items + if item.get("item_code") and item.get("item_code") + ] items_to_map = list(set(items_to_map)) def set_missing_values(source, target): @@ -876,86 +1019,89 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): def update_item(source, target, source_parent): target.schedule_date = source.delivery_date target.qty = flt(source.qty) - (flt(source.ordered_qty) / flt(source.conversion_factor)) - target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty)) + target.stock_qty = flt(source.stock_qty) - flt(source.ordered_qty) target.project = source_parent.project def update_item_for_packed_item(source, target, source_parent): target.qty = flt(source.qty) - flt(source.ordered_qty) # po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")}) - 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", - "terms" - ], - "validation": { - "docstatus": ["=", 1] - } + 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", + "terms", + ], + "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", + "discount_percentage", + "discount_amount", + "supplier", + "pricing_rules", + ], + "postprocess": update_item, + "condition": lambda doc: doc.ordered_qty < doc.stock_qty + and doc.item_code in items_to_map + and not is_product_bundle(doc.item_code), + }, + "Packed Item": { + "doctype": "Purchase Order Item", + "field_map": [ + ["name", "sales_order_packed_item"], + ["parent", "sales_order"], + ["uom", "uom"], + ["conversion_factor", "conversion_factor"], + ["parent_item", "product_bundle"], + ["rate", "rate"], + ], + "field_no_map": [ + "price_list_rate", + "item_tax_template", + "discount_percentage", + "discount_amount", + "supplier", + "pricing_rules", + ], + "postprocess": update_item_for_packed_item, + "condition": lambda doc: doc.parent_item in items_to_map, + }, }, - "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", - "discount_percentage", - "discount_amount", - "supplier", - "pricing_rules" - ], - "postprocess": update_item, - "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.item_code in items_to_map and not is_product_bundle(doc.item_code) - }, - "Packed Item": { - "doctype": "Purchase Order Item", - "field_map": [ - ["name", "sales_order_packed_item"], - ["parent", "sales_order"], - ["uom", "uom"], - ["conversion_factor", "conversion_factor"], - ["parent_item", "product_bundle"], - ["rate", "rate"] - ], - "field_no_map": [ - "price_list_rate", - "item_tax_template", - "discount_percentage", - "discount_amount", - "supplier", - "pricing_rules" - ], - "postprocess": update_item_for_packed_item, - "condition": lambda doc: doc.parent_item in items_to_map - } - }, target_doc, set_missing_values) + target_doc, + set_missing_values, + ) set_delivery_date(doc.items, source_name) return doc + def set_delivery_date(items, sales_order): delivery_dates = frappe.get_all( - 'Sales Order Item', - filters = { - 'parent': sales_order - }, - fields = ['delivery_date', 'item_code'] + "Sales Order Item", filters={"parent": sales_order}, fields=["delivery_date", "item_code"] ) delivery_by_item = frappe._dict() @@ -966,13 +1112,15 @@ def set_delivery_date(items, sales_order): if item.product_bundle: item.schedule_date = delivery_by_item[item.product_bundle] + def is_product_bundle(item_code): - return frappe.db.exists('Product Bundle', item_code) + return frappe.db.exists("Product Bundle", item_code) + @frappe.whitelist() def make_work_orders(items, sales_order, company, project=None): - '''Make Work Orders against the given Sales Order for the given `items`''' - items = json.loads(items).get('items') + """Make Work Orders against the given Sales Order for the given `items`""" + items = json.loads(items).get("items") out = [] for i in items: @@ -981,18 +1129,20 @@ def make_work_orders(items, sales_order, company, project=None): if not i.get("pending_qty"): frappe.throw(_("Please select Qty against item {0}").format(i.get("item_code"))) - work_order = frappe.get_doc(dict( - doctype='Work Order', - production_item=i['item_code'], - bom_no=i.get('bom'), - qty=i['pending_qty'], - company=company, - sales_order=sales_order, - sales_order_item=i['sales_order_item'], - project=project, - fg_warehouse=i['warehouse'], - description=i['description'] - )).insert() + work_order = frappe.get_doc( + dict( + doctype="Work Order", + production_item=i["item_code"], + bom_no=i.get("bom"), + qty=i["pending_qty"], + company=company, + sales_order=sales_order, + sales_order_item=i["sales_order_item"], + project=project, + fg_warehouse=i["warehouse"], + description=i["description"], + ) + ).insert() work_order.set_work_order_operations() work_order.flags.ignore_mandatory = True work_order.save() @@ -1000,18 +1150,20 @@ def make_work_orders(items, sales_order, company, project=None): return [p.name for p in out] + @frappe.whitelist() def update_status(status, name): so = frappe.get_doc("Sales Order", name) so.update_status(status) + def get_default_bom_item(item_code): - bom = frappe.get_all('BOM', dict(item=item_code, is_active=True), - order_by='is_default desc') + bom = frappe.get_all("BOM", dict(item=item_code, is_active=True), order_by="is_default desc") bom = bom[0].name if bom else None return bom + @frappe.whitelist() def make_raw_material_request(items, company, sales_order, project=None): if not frappe.has_permission("Sales Order", "write"): @@ -1020,43 +1172,49 @@ def make_raw_material_request(items, company, sales_order, project=None): if isinstance(items, str): items = frappe._dict(json.loads(items)) - for item in items.get('items'): - item["include_exploded_items"] = items.get('include_exploded_items') - item["ignore_existing_ordered_qty"] = items.get('ignore_existing_ordered_qty') - item["include_raw_materials_from_sales_order"] = items.get('include_raw_materials_from_sales_order') + for item in items.get("items"): + item["include_exploded_items"] = items.get("include_exploded_items") + item["ignore_existing_ordered_qty"] = items.get("ignore_existing_ordered_qty") + item["include_raw_materials_from_sales_order"] = items.get( + "include_raw_materials_from_sales_order" + ) - items.update({ - 'company': company, - 'sales_order': sales_order - }) + items.update({"company": company, "sales_order": sales_order}) raw_materials = get_items_for_material_requests(items) if not raw_materials: - frappe.msgprint(_("Material Request not created, as quantity for Raw Materials already available.")) + frappe.msgprint( + _("Material Request not created, as quantity for Raw Materials already available.") + ) return - material_request = frappe.new_doc('Material Request') - material_request.update(dict( - doctype = 'Material Request', - transaction_date = nowdate(), - company = company, - material_request_type = 'Purchase' - )) + material_request = frappe.new_doc("Material Request") + material_request.update( + dict( + doctype="Material Request", + transaction_date=nowdate(), + company=company, + material_request_type="Purchase", + ) + ) for item in raw_materials: - item_doc = frappe.get_cached_doc('Item', item.get('item_code')) + item_doc = frappe.get_cached_doc("Item", item.get("item_code")) schedule_date = add_days(nowdate(), cint(item_doc.lead_time_days)) - row = material_request.append('items', { - 'item_code': item.get('item_code'), - 'qty': item.get('quantity'), - 'schedule_date': schedule_date, - 'warehouse': item.get('warehouse'), - 'sales_order': sales_order, - 'project': project - }) + row = material_request.append( + "items", + { + "item_code": item.get("item_code"), + "qty": item.get("quantity"), + "schedule_date": schedule_date, + "warehouse": item.get("warehouse"), + "sales_order": sales_order, + "project": project, + }, + ) if not (strip_html(item.get("description")) and strip_html(item_doc.description)): - row.description = item_doc.item_name or item.get('item_code') + row.description = item_doc.item_name or item.get("item_code") material_request.insert() material_request.flags.ignore_permissions = 1 @@ -1064,53 +1222,56 @@ def make_raw_material_request(items, company, sales_order, project=None): material_request.submit() return material_request + @frappe.whitelist() def make_inter_company_purchase_order(source_name, target_doc=None): from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction + return make_inter_company_transaction("Sales Order", source_name, target_doc) + @frappe.whitelist() def create_pick_list(source_name, target_doc=None): def update_item_quantity(source, target, source_parent): target.qty = flt(source.qty) - flt(source.delivered_qty) target.stock_qty = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.conversion_factor) - doc = get_mapped_doc('Sales Order', source_name, { - 'Sales Order': { - 'doctype': 'Pick List', - 'validation': { - 'docstatus': ['=', 1] - } - }, - 'Sales Order Item': { - 'doctype': 'Pick List Item', - 'field_map': { - 'parent': 'sales_order', - 'name': 'sales_order_item' + doc = get_mapped_doc( + "Sales Order", + source_name, + { + "Sales Order": {"doctype": "Pick List", "validation": {"docstatus": ["=", 1]}}, + "Sales Order Item": { + "doctype": "Pick List Item", + "field_map": {"parent": "sales_order", "name": "sales_order_item"}, + "postprocess": update_item_quantity, + "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) + and doc.delivered_by_supplier != 1, }, - 'postprocess': update_item_quantity, - 'condition': lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1 }, - }, target_doc) + target_doc, + ) - doc.purpose = 'Delivery' + doc.purpose = "Delivery" doc.set_item_locations() return doc + def update_produced_qty_in_so_item(sales_order, sales_order_item): - #for multiple work orders against same sales order item - linked_wo_with_so_item = frappe.db.get_all('Work Order', ['produced_qty'], { - 'sales_order_item': sales_order_item, - 'sales_order': sales_order, - 'docstatus': 1 - }) + # for multiple work orders against same sales order item + linked_wo_with_so_item = frappe.db.get_all( + "Work Order", + ["produced_qty"], + {"sales_order_item": sales_order_item, "sales_order": sales_order, "docstatus": 1}, + ) total_produced_qty = 0 for wo in linked_wo_with_so_item: - total_produced_qty += flt(wo.get('produced_qty')) + total_produced_qty += flt(wo.get("produced_qty")) - if not total_produced_qty and frappe.flags.in_patch: return + 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) + frappe.db.set_value("Sales Order Item", sales_order_item, "produced_qty", total_produced_qty) diff --git a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py index 1e616b87b2..ace2e29c2b 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py +++ b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py @@ -3,42 +3,25 @@ from frappe import _ def get_data(): return { - 'fieldname': 'sales_order', - 'non_standard_fieldnames': { - 'Delivery Note': 'against_sales_order', - 'Journal Entry': 'reference_name', - 'Payment Entry': 'reference_name', - 'Payment Request': 'reference_name', - 'Auto Repeat': 'reference_document', - 'Maintenance Visit': 'prevdoc_docname' + "fieldname": "sales_order", + "non_standard_fieldnames": { + "Delivery Note": "against_sales_order", + "Journal Entry": "reference_name", + "Payment Entry": "reference_name", + "Payment Request": "reference_name", + "Auto Repeat": "reference_document", + "Maintenance Visit": "prevdoc_docname", }, - 'internal_links': { - 'Quotation': ['items', 'prevdoc_docname'] - }, - 'transactions': [ + "internal_links": {"Quotation": ["items", "prevdoc_docname"]}, + "transactions": [ { - 'label': _('Fulfillment'), - 'items': ['Sales Invoice', 'Pick List', 'Delivery Note', 'Maintenance Visit'] + "label": _("Fulfillment"), + "items": ["Sales Invoice", "Pick List", "Delivery Note", "Maintenance Visit"], }, - { - 'label': _('Purchasing'), - 'items': ['Material Request', 'Purchase Order'] - }, - { - 'label': _('Projects'), - 'items': ['Project'] - }, - { - 'label': _('Manufacturing'), - 'items': ['Work Order'] - }, - { - 'label': _('Reference'), - 'items': ['Quotation', 'Auto Repeat'] - }, - { - 'label': _('Payment'), - 'items': ['Payment Entry', 'Payment Request', 'Journal Entry'] - }, - ] + {"label": _("Purchasing"), "items": ["Material Request", "Purchase Order"]}, + {"label": _("Projects"), "items": ["Project"]}, + {"label": _("Manufacturing"), "items": ["Work Order"]}, + {"label": _("Reference"), "items": ["Quotation", "Auto Repeat"]}, + {"label": _("Payment"), "items": ["Payment Entry", "Payment Request", "Journal Entry"]}, + ], } diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 86a08828b2..acae37f547 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -31,18 +31,24 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry class TestSalesOrder(FrappeTestCase): - @classmethod def setUpClass(cls): super().setUpClass() - cls.unlink_setting = int(frappe.db.get_value("Accounts Settings", "Accounts Settings", - "unlink_advance_payment_on_cancelation_of_order")) + cls.unlink_setting = int( + frappe.db.get_value( + "Accounts Settings", "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order" + ) + ) @classmethod def tearDownClass(cls) -> None: # reset config to previous state - frappe.db.set_value("Accounts Settings", "Accounts Settings", - "unlink_advance_payment_on_cancelation_of_order", cls.unlink_setting) + frappe.db.set_value( + "Accounts Settings", + "Accounts Settings", + "unlink_advance_payment_on_cancelation_of_order", + cls.unlink_setting, + ) super().tearDownClass() def tearDown(self): @@ -89,6 +95,7 @@ class TestSalesOrder(FrappeTestCase): def test_so_billed_amount_against_return_entry(self): from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return + so = make_sales_order(do_not_submit=True) so.submit() @@ -117,7 +124,7 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(len(si.get("items")), 1) si.insert() - si.set('taxes', []) + si.set("taxes", []) si.save() self.assertEqual(si.payment_schedule[0].payment_amount, 500.0) @@ -185,16 +192,16 @@ class TestSalesOrder(FrappeTestCase): dn1.items[0].so_detail = so.items[0].name dn1.submit() - si1 = create_sales_invoice(is_return=1, return_against=si2.name, qty=-1, update_stock=1, do_not_submit=True) + si1 = create_sales_invoice( + is_return=1, return_against=si2.name, qty=-1, update_stock=1, do_not_submit=True + ) si1.items[0].sales_order = so.name si1.items[0].so_detail = so.items[0].name si1.submit() - so.load_from_db() self.assertEqual(so.get("items")[0].delivered_qty, 5) - def test_reserved_qty_for_partial_delivery(self): make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100) existing_reserved_qty = get_reserved_qty() @@ -212,7 +219,7 @@ class TestSalesOrder(FrappeTestCase): # unclose so so.load_from_db() - so.update_status('Draft') + so.update_status("Draft") self.assertEqual(get_reserved_qty(), existing_reserved_qty + 5) dn.cancel() @@ -226,7 +233,7 @@ class TestSalesOrder(FrappeTestCase): def test_reserved_qty_for_over_delivery(self): make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100) # set over-delivery allowance - frappe.db.set_value('Item', "_Test Item", 'over_delivery_receipt_allowance', 50) + frappe.db.set_value("Item", "_Test Item", "over_delivery_receipt_allowance", 50) existing_reserved_qty = get_reserved_qty() @@ -243,8 +250,8 @@ class TestSalesOrder(FrappeTestCase): make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100) # set over-delivery allowance - frappe.db.set_value('Item', "_Test Item", 'over_delivery_receipt_allowance', 50) - frappe.db.set_value('Item', "_Test Item", 'over_billing_allowance', 20) + frappe.db.set_value("Item", "_Test Item", "over_delivery_receipt_allowance", 50) + frappe.db.set_value("Item", "_Test Item", "over_billing_allowance", 20) existing_reserved_qty = get_reserved_qty() @@ -272,7 +279,9 @@ class TestSalesOrder(FrappeTestCase): def test_reserved_qty_for_partial_delivery_with_packing_list(self): make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100) - make_stock_entry(item="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=10, rate=100) + make_stock_entry( + item="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=10, rate=100 + ) existing_reserved_qty_item1 = get_reserved_qty("_Test Item") existing_reserved_qty_item2 = get_reserved_qty("_Test Item Home Desktop 100") @@ -280,14 +289,16 @@ class TestSalesOrder(FrappeTestCase): so = make_sales_order(item_code="_Test Product Bundle Item") self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 50) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), - existing_reserved_qty_item2 + 20) + self.assertEqual( + get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20 + ) dn = create_dn_against_so(so.name) self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 25) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), - existing_reserved_qty_item2 + 10) + self.assertEqual( + get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 10 + ) # close so so.load_from_db() @@ -298,16 +309,18 @@ class TestSalesOrder(FrappeTestCase): # unclose so so.load_from_db() - so.update_status('Draft') + so.update_status("Draft") self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 25) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), - existing_reserved_qty_item2 + 10) + self.assertEqual( + get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 10 + ) dn.cancel() self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 50) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), - existing_reserved_qty_item2 + 20) + self.assertEqual( + get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20 + ) so.load_from_db() so.cancel() @@ -316,17 +329,19 @@ class TestSalesOrder(FrappeTestCase): def test_sales_order_on_hold(self): so = make_sales_order(item_code="_Test Product Bundle Item") - so.db_set('Status', "On Hold") + so.db_set("Status", "On Hold") si = make_sales_invoice(so.name) self.assertRaises(frappe.ValidationError, create_dn_against_so, so.name) self.assertRaises(frappe.ValidationError, si.submit) def test_reserved_qty_for_over_delivery_with_packing_list(self): make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100) - make_stock_entry(item="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=10, rate=100) + make_stock_entry( + item="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=10, rate=100 + ) # set over-delivery allowance - frappe.db.set_value('Item', "_Test Product Bundle Item", 'over_delivery_receipt_allowance', 50) + frappe.db.set_value("Item", "_Test Product Bundle Item", "over_delivery_receipt_allowance", 50) existing_reserved_qty_item1 = get_reserved_qty("_Test Item") existing_reserved_qty_item2 = get_reserved_qty("_Test Item Home Desktop 100") @@ -334,22 +349,23 @@ class TestSalesOrder(FrappeTestCase): so = make_sales_order(item_code="_Test Product Bundle Item") self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 50) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), - existing_reserved_qty_item2 + 20) + self.assertEqual( + get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20 + ) dn = create_dn_against_so(so.name, 15) self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), - existing_reserved_qty_item2) + self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2) dn.cancel() self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 50) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), - existing_reserved_qty_item2 + 20) + self.assertEqual( + get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20 + ) def test_update_child_adding_new_item(self): - so = make_sales_order(item_code= "_Test Item", qty=4) + so = make_sales_order(item_code="_Test Item", qty=4) create_dn_against_so(so.name, 4) make_sales_invoice(so.name) @@ -360,38 +376,38 @@ class TestSalesOrder(FrappeTestCase): reserved_qty_for_second_item = get_reserved_qty("_Test Item 2") first_item_of_so = so.get("items")[0] - trans_item = json.dumps([ - {'item_code' : first_item_of_so.item_code, 'rate' : first_item_of_so.rate, \ - 'qty' : first_item_of_so.qty, 'docname': first_item_of_so.name}, - {'item_code' : '_Test Item 2', 'rate' : 200, 'qty' : 7} - ]) - update_child_qty_rate('Sales Order', trans_item, so.name) + trans_item = json.dumps( + [ + { + "item_code": first_item_of_so.item_code, + "rate": first_item_of_so.rate, + "qty": first_item_of_so.qty, + "docname": first_item_of_so.name, + }, + {"item_code": "_Test Item 2", "rate": 200, "qty": 7}, + ] + ) + update_child_qty_rate("Sales Order", trans_item, so.name) so.reload() - self.assertEqual(so.get("items")[-1].item_code, '_Test Item 2') + self.assertEqual(so.get("items")[-1].item_code, "_Test Item 2") self.assertEqual(so.get("items")[-1].rate, 200) self.assertEqual(so.get("items")[-1].qty, 7) self.assertEqual(so.get("items")[-1].amount, 1400) # reserved qty should increase after adding row - self.assertEqual(get_reserved_qty('_Test Item 2'), reserved_qty_for_second_item + 7) + self.assertEqual(get_reserved_qty("_Test Item 2"), reserved_qty_for_second_item + 7) - self.assertEqual(so.status, 'To Deliver and Bill') + self.assertEqual(so.status, "To Deliver and Bill") updated_total = so.get("base_total") updated_total_in_words = so.get("base_in_words") - self.assertEqual(updated_total, prev_total+1400) + self.assertEqual(updated_total, prev_total + 1400) self.assertNotEqual(updated_total_in_words, prev_total_in_words) def test_update_child_removing_item(self): - so = make_sales_order(**{ - "item_list": [{ - "item_code": '_Test Item', - "qty": 5, - "rate":1000 - }] - }) + so = make_sales_order(**{"item_list": [{"item_code": "_Test Item", "qty": 5, "rate": 1000}]}) create_dn_against_so(so.name, 2) make_sales_invoice(so.name) @@ -399,64 +415,67 @@ class TestSalesOrder(FrappeTestCase): reserved_qty_for_second_item = get_reserved_qty("_Test Item 2") # add an item so as to try removing items - trans_item = json.dumps([ - {"item_code": '_Test Item', "qty": 5, "rate":1000, "docname": so.get("items")[0].name}, - {"item_code": '_Test Item 2', "qty": 2, "rate":500} - ]) - update_child_qty_rate('Sales Order', trans_item, so.name) + trans_item = json.dumps( + [ + {"item_code": "_Test Item", "qty": 5, "rate": 1000, "docname": so.get("items")[0].name}, + {"item_code": "_Test Item 2", "qty": 2, "rate": 500}, + ] + ) + update_child_qty_rate("Sales Order", trans_item, so.name) so.reload() self.assertEqual(len(so.get("items")), 2) # reserved qty should increase after adding row - self.assertEqual(get_reserved_qty('_Test Item 2'), reserved_qty_for_second_item + 2) + self.assertEqual(get_reserved_qty("_Test Item 2"), reserved_qty_for_second_item + 2) # check if delivered items can be removed - trans_item = json.dumps([{ - "item_code": '_Test Item 2', - "qty": 2, - "rate":500, - "docname": so.get("items")[1].name - }]) - self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Sales Order', trans_item, so.name) + trans_item = json.dumps( + [{"item_code": "_Test Item 2", "qty": 2, "rate": 500, "docname": so.get("items")[1].name}] + ) + self.assertRaises( + frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name + ) - #remove last added item - trans_item = json.dumps([{ - "item_code": '_Test Item', - "qty": 5, - "rate":1000, - "docname": so.get("items")[0].name - }]) - update_child_qty_rate('Sales Order', trans_item, so.name) + # remove last added item + trans_item = json.dumps( + [{"item_code": "_Test Item", "qty": 5, "rate": 1000, "docname": so.get("items")[0].name}] + ) + update_child_qty_rate("Sales Order", trans_item, so.name) so.reload() self.assertEqual(len(so.get("items")), 1) # reserved qty should decrease (back to initial) after deleting row - self.assertEqual(get_reserved_qty('_Test Item 2'), reserved_qty_for_second_item) - - self.assertEqual(so.status, 'To Deliver and Bill') + self.assertEqual(get_reserved_qty("_Test Item 2"), reserved_qty_for_second_item) + self.assertEqual(so.status, "To Deliver and Bill") def test_update_child(self): - so = make_sales_order(item_code= "_Test Item", qty=4) + so = make_sales_order(item_code="_Test Item", qty=4) create_dn_against_so(so.name, 4) make_sales_invoice(so.name) existing_reserved_qty = get_reserved_qty() - trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7, 'docname': so.items[0].name}]) - update_child_qty_rate('Sales Order', trans_item, so.name) + trans_item = json.dumps( + [{"item_code": "_Test Item", "rate": 200, "qty": 7, "docname": so.items[0].name}] + ) + update_child_qty_rate("Sales Order", trans_item, so.name) so.reload() self.assertEqual(so.get("items")[0].rate, 200) self.assertEqual(so.get("items")[0].qty, 7) self.assertEqual(so.get("items")[0].amount, 1400) - self.assertEqual(so.status, 'To Deliver and Bill') + self.assertEqual(so.status, "To Deliver and Bill") self.assertEqual(get_reserved_qty(), existing_reserved_qty + 3) - trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 2, 'docname': so.items[0].name}]) - self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) + trans_item = json.dumps( + [{"item_code": "_Test Item", "rate": 200, "qty": 2, "docname": so.items[0].name}] + ) + self.assertRaises( + frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name + ) def test_update_child_with_precision(self): from frappe.custom.doctype.property_setter.property_setter import make_property_setter @@ -465,48 +484,60 @@ class TestSalesOrder(FrappeTestCase): precision = get_field_precision(frappe.get_meta("Sales Order Item").get_field("rate")) make_property_setter("Sales Order Item", "rate", "precision", 7, "Currency") - so = make_sales_order(item_code= "_Test Item", qty=4, rate=200.34664) + so = make_sales_order(item_code="_Test Item", qty=4, rate=200.34664) - trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200.34669, 'qty' : 4, 'docname': so.items[0].name}]) - update_child_qty_rate('Sales Order', trans_item, so.name) + trans_item = json.dumps( + [{"item_code": "_Test Item", "rate": 200.34669, "qty": 4, "docname": so.items[0].name}] + ) + update_child_qty_rate("Sales Order", trans_item, so.name) so.reload() self.assertEqual(so.items[0].rate, 200.34669) make_property_setter("Sales Order Item", "rate", "precision", precision, "Currency") def test_update_child_perm(self): - so = make_sales_order(item_code= "_Test Item", qty=4) + so = make_sales_order(item_code="_Test Item", qty=4) test_user = create_user("test_so_child_perms@example.com", "Accounts User") frappe.set_user(test_user.name) # update qty - trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7, 'docname': so.items[0].name}]) - self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) + trans_item = json.dumps( + [{"item_code": "_Test Item", "rate": 200, "qty": 7, "docname": so.items[0].name}] + ) + self.assertRaises( + frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name + ) # add new item - trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 100, 'qty' : 2}]) - self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) + trans_item = json.dumps([{"item_code": "_Test Item", "rate": 100, "qty": 2}]) + self.assertRaises( + frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name + ) def test_update_child_qty_rate_with_workflow(self): from frappe.model.workflow import apply_workflow workflow = make_sales_order_workflow() - so = make_sales_order(item_code= "_Test Item", qty=1, rate=150, do_not_submit=1) - apply_workflow(so, 'Approve') + so = make_sales_order(item_code="_Test Item", qty=1, rate=150, do_not_submit=1) + apply_workflow(so, "Approve") - user = 'test@example.com' - test_user = frappe.get_doc('User', user) + user = "test@example.com" + test_user = frappe.get_doc("User", user) test_user.add_roles("Sales User", "Test Junior Approver") frappe.set_user(user) # user shouldn't be able to edit since grand_total will become > 200 if qty is doubled - trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 150, 'qty' : 2, 'docname': so.items[0].name}]) - self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Sales Order', trans_item, so.name) + trans_item = json.dumps( + [{"item_code": "_Test Item", "rate": 150, "qty": 2, "docname": so.items[0].name}] + ) + self.assertRaises( + frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name + ) frappe.set_user("Administrator") - user2 = 'test2@example.com' - test_user2 = frappe.get_doc('User', user2) + user2 = "test2@example.com" + test_user2 = frappe.get_doc("User", user2) test_user2.add_roles("Sales User", "Test Approver") frappe.set_user(user2) @@ -525,21 +556,21 @@ class TestSalesOrder(FrappeTestCase): # test Update Items with product bundle if not frappe.db.exists("Item", "_Product Bundle Item"): bundle_item = make_item("_Product Bundle Item", {"is_stock_item": 0}) - bundle_item.append("item_defaults", { - "company": "_Test Company", - "default_warehouse": "_Test Warehouse - _TC"}) + bundle_item.append( + "item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"} + ) bundle_item.save(ignore_permissions=True) make_item("_Packed Item", {"is_stock_item": 1}) make_product_bundle("_Product Bundle Item", ["_Packed Item"], 2) - so = make_sales_order(item_code = "_Test Item", warehouse=None) + so = make_sales_order(item_code="_Test Item", warehouse=None) # get reserved qty of packed item existing_reserved_qty = get_reserved_qty("_Packed Item") - added_item = json.dumps([{"item_code" : "_Product Bundle Item", "rate" : 200, 'qty' : 2}]) - update_child_qty_rate('Sales Order', added_item, so.name) + added_item = json.dumps([{"item_code": "_Product Bundle Item", "rate": 200, "qty": 2}]) + update_child_qty_rate("Sales Order", added_item, so.name) so.reload() self.assertEqual(so.packed_items[0].qty, 4) @@ -548,15 +579,19 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(get_reserved_qty("_Packed Item"), existing_reserved_qty + 4) # test uom and conversion factor change - update_uom_conv_factor = json.dumps([{ - 'item_code': so.get("items")[0].item_code, - 'rate': so.get("items")[0].rate, - 'qty': so.get("items")[0].qty, - 'uom': "_Test UOM 1", - 'conversion_factor': 2, - 'docname': so.get("items")[0].name - }]) - update_child_qty_rate('Sales Order', update_uom_conv_factor, so.name) + update_uom_conv_factor = json.dumps( + [ + { + "item_code": so.get("items")[0].item_code, + "rate": so.get("items")[0].rate, + "qty": so.get("items")[0].qty, + "uom": "_Test UOM 1", + "conversion_factor": 2, + "docname": so.get("items")[0].name, + } + ] + ) + update_child_qty_rate("Sales Order", update_uom_conv_factor, so.name) so.reload() self.assertEqual(so.packed_items[0].qty, 8) @@ -566,61 +601,67 @@ class TestSalesOrder(FrappeTestCase): def test_update_child_with_tax_template(self): """ - Test Action: Create a SO with one item having its tax account head already in the SO. - Add the same item + new item with tax template via Update Items. - Expected result: First Item's tax row is updated. New tax row is added for second Item. + Test Action: Create a SO with one item having its tax account head already in the SO. + Add the same item + new item with tax template via Update Items. + Expected result: First Item's tax row is updated. New tax row is added for second Item. """ if not frappe.db.exists("Item", "Test Item with Tax"): - make_item("Test Item with Tax", { - 'is_stock_item': 1, - }) + make_item( + "Test Item with Tax", + { + "is_stock_item": 1, + }, + ) - if not frappe.db.exists("Item Tax Template", {"title": 'Test Update Items Template'}): - frappe.get_doc({ - 'doctype': 'Item Tax Template', - 'title': 'Test Update Items Template', - 'company': '_Test Company', - 'taxes': [ - { - 'tax_type': "_Test Account Service Tax - _TC", - 'tax_rate': 10, - } - ] - }).insert() + if not frappe.db.exists("Item Tax Template", {"title": "Test Update Items Template"}): + frappe.get_doc( + { + "doctype": "Item Tax Template", + "title": "Test Update Items Template", + "company": "_Test Company", + "taxes": [ + { + "tax_type": "_Test Account Service Tax - _TC", + "tax_rate": 10, + } + ], + } + ).insert() new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax") - new_item_with_tax.append("taxes", { - "item_tax_template": "Test Update Items Template - _TC", - "valid_from": nowdate() - }) + new_item_with_tax.append( + "taxes", {"item_tax_template": "Test Update Items Template - _TC", "valid_from": nowdate()} + ) new_item_with_tax.save() tax_template = "_Test Account Excise Duty @ 10 - _TC" - item = "_Test Item Home Desktop 100" - if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}): + item = "_Test Item Home Desktop 100" + if not frappe.db.exists("Item Tax", {"parent": item, "item_tax_template": tax_template}): item_doc = frappe.get_doc("Item", item) - item_doc.append("taxes", { - "item_tax_template": tax_template, - "valid_from": nowdate() - }) + item_doc.append("taxes", {"item_tax_template": tax_template, "valid_from": nowdate()}) item_doc.save() else: # update valid from - frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = CURDATE() + frappe.db.sql( + """UPDATE `tabItem Tax` set valid_from = CURDATE() where parent = %(item)s and item_tax_template = %(tax)s""", - {"item": item, "tax": tax_template}) + {"item": item, "tax": tax_template}, + ) so = make_sales_order(item_code=item, qty=1, do_not_save=1) - so.append("taxes", { - "account_head": "_Test Account Excise Duty - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "Excise Duty", - "doctype": "Sales Taxes and Charges", - "rate": 10 - }) + so.append( + "taxes", + { + "account_head": "_Test Account Excise Duty - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "Excise Duty", + "doctype": "Sales Taxes and Charges", + "rate": 10, + }, + ) so.insert() so.submit() @@ -630,12 +671,22 @@ class TestSalesOrder(FrappeTestCase): old_stock_settings_value = frappe.db.get_single_value("Stock Settings", "default_warehouse") frappe.db.set_value("Stock Settings", None, "default_warehouse", "_Test Warehouse - _TC") - items = json.dumps([ - {'item_code' : item, 'rate' : 100, 'qty' : 1, 'docname': so.items[0].name}, - {'item_code' : item, 'rate' : 200, 'qty' : 1}, # added item whose tax account head already exists in PO - {'item_code' : new_item_with_tax.name, 'rate' : 100, 'qty' : 1} # added item whose tax account head is missing in PO - ]) - update_child_qty_rate('Sales Order', items, so.name) + items = json.dumps( + [ + {"item_code": item, "rate": 100, "qty": 1, "docname": so.items[0].name}, + { + "item_code": item, + "rate": 200, + "qty": 1, + }, # added item whose tax account head already exists in PO + { + "item_code": new_item_with_tax.name, + "rate": 100, + "qty": 1, + }, # added item whose tax account head is missing in PO + ] + ) + update_child_qty_rate("Sales Order", items, so.name) so.reload() self.assertEqual(so.taxes[0].tax_amount, 40) @@ -645,8 +696,11 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(so.taxes[1].total, 480) # teardown - frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = NULL - where parent = %(item)s and item_tax_template = %(tax)s""", {"item": item, "tax": tax_template}) + frappe.db.sql( + """UPDATE `tabItem Tax` set valid_from = NULL + where parent = %(item)s and item_tax_template = %(tax)s""", + {"item": item, "tax": tax_template}, + ) so.cancel() so.delete() new_item_with_tax.delete() @@ -666,8 +720,12 @@ class TestSalesOrder(FrappeTestCase): frappe.set_user(test_user.name) - so = make_sales_order(company="_Test Company 1", customer="_Test Customer 1", - warehouse="_Test Warehouse 2 - _TC1", do_not_save=True) + so = make_sales_order( + company="_Test Company 1", + customer="_Test Customer 1", + warehouse="_Test Warehouse 2 - _TC1", + do_not_save=True, + ) so.conversion_rate = 0.02 so.plc_conversion_rate = 0.02 self.assertRaises(frappe.PermissionError, so.insert) @@ -677,7 +735,9 @@ class TestSalesOrder(FrappeTestCase): frappe.set_user("Administrator") frappe.permissions.remove_user_permission("Warehouse", "_Test Warehouse 1 - _TC", test_user.name) - frappe.permissions.remove_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", test_user_2.name) + frappe.permissions.remove_user_permission( + "Warehouse", "_Test Warehouse 2 - _TC1", test_user_2.name + ) frappe.permissions.remove_user_permission("Company", "_Test Company 1", test_user_2.name) def test_block_delivery_note_against_cancelled_sales_order(self): @@ -697,10 +757,12 @@ class TestSalesOrder(FrappeTestCase): make_item("_Test Service Product Bundle Item 1", {"is_stock_item": 0}) make_item("_Test Service Product Bundle Item 2", {"is_stock_item": 0}) - make_product_bundle("_Test Service Product Bundle", - ["_Test Service Product Bundle Item 1", "_Test Service Product Bundle Item 2"]) + make_product_bundle( + "_Test Service Product Bundle", + ["_Test Service Product Bundle Item 1", "_Test Service Product Bundle Item 2"], + ) - so = make_sales_order(item_code = "_Test Service Product Bundle", warehouse=None) + so = make_sales_order(item_code="_Test Service Product Bundle", warehouse=None) self.assertTrue("_Test Service Product Bundle Item 1" in [d.item_code for d in so.packed_items]) self.assertTrue("_Test Service Product Bundle Item 2" in [d.item_code for d in so.packed_items]) @@ -710,38 +772,59 @@ class TestSalesOrder(FrappeTestCase): make_item("_Test Mix Product Bundle Item 1", {"is_stock_item": 1}) make_item("_Test Mix Product Bundle Item 2", {"is_stock_item": 0}) - make_product_bundle("_Test Mix Product Bundle", - ["_Test Mix Product Bundle Item 1", "_Test Mix Product Bundle Item 2"]) + make_product_bundle( + "_Test Mix Product Bundle", + ["_Test Mix Product Bundle Item 1", "_Test Mix Product Bundle Item 2"], + ) - self.assertRaises(WarehouseRequired, make_sales_order, item_code = "_Test Mix Product Bundle", warehouse="") + self.assertRaises( + WarehouseRequired, make_sales_order, item_code="_Test Mix Product Bundle", warehouse="" + ) def test_auto_insert_price(self): make_item("_Test Item for Auto Price List", {"is_stock_item": 0}) frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 1) - item_price = frappe.db.get_value("Item Price", {"price_list": "_Test Price List", - "item_code": "_Test Item for Auto Price List"}) + item_price = frappe.db.get_value( + "Item Price", {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"} + ) if item_price: frappe.delete_doc("Item Price", item_price) - make_sales_order(item_code = "_Test Item for Auto Price List", selling_price_list="_Test Price List", rate=100) - - self.assertEqual(frappe.db.get_value("Item Price", - {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"}, "price_list_rate"), 100) + make_sales_order( + item_code="_Test Item for Auto Price List", selling_price_list="_Test Price List", rate=100 + ) + self.assertEqual( + frappe.db.get_value( + "Item Price", + {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"}, + "price_list_rate", + ), + 100, + ) # do not update price list frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0) - item_price = frappe.db.get_value("Item Price", {"price_list": "_Test Price List", - "item_code": "_Test Item for Auto Price List"}) + item_price = frappe.db.get_value( + "Item Price", {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"} + ) if item_price: frappe.delete_doc("Item Price", item_price) - make_sales_order(item_code = "_Test Item for Auto Price List", selling_price_list="_Test Price List", rate=100) + make_sales_order( + item_code="_Test Item for Auto Price List", selling_price_list="_Test Price List", rate=100 + ) - self.assertEqual(frappe.db.get_value("Item Price", - {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"}, "price_list_rate"), None) + self.assertEqual( + frappe.db.get_value( + "Item Price", + {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"}, + "price_list_rate", + ), + None, + ) frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 1) @@ -753,7 +836,9 @@ class TestSalesOrder(FrappeTestCase): from erpnext.selling.doctype.sales_order.sales_order import update_status as so_update_status # make items - po_item = make_item("_Test Item for Drop Shipping", {"is_stock_item": 1, "delivered_by_supplier": 1}) + po_item = make_item( + "_Test Item for Drop Shipping", {"is_stock_item": 1, "delivered_by_supplier": 1} + ) dn_item = make_item("_Test Regular Item", {"is_stock_item": 1}) so_items = [ @@ -763,21 +848,21 @@ class TestSalesOrder(FrappeTestCase): "qty": 2, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", }, { "item_code": dn_item.item_code, "warehouse": "_Test Warehouse - _TC", "qty": 2, "rate": 300, - "conversion_factor": 1.0 - } + "conversion_factor": 1.0, + }, ] - if frappe.db.get_value("Item", "_Test Regular Item", "is_stock_item")==1: + if frappe.db.get_value("Item", "_Test Regular Item", "is_stock_item") == 1: make_stock_entry(item="_Test Regular Item", target="_Test Warehouse - _TC", qty=2, rate=100) - #create so, po and dn + # create so, po and dn so = make_sales_order(item_list=so_items, do_not_submit=True) so.submit() @@ -790,12 +875,15 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(po.items[0].sales_order, so.name) self.assertEqual(po.items[0].item_code, po_item.item_code) self.assertEqual(dn.items[0].item_code, dn_item.item_code) - #test po_item length + # test po_item length self.assertEqual(len(po.items), 1) # test ordered_qty and reserved_qty for drop ship item - bin_po_item = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"}, - fields=["ordered_qty", "reserved_qty"]) + bin_po_item = frappe.get_all( + "Bin", + filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"}, + fields=["ordered_qty", "reserved_qty"], + ) ordered_qty = bin_po_item[0].ordered_qty if bin_po_item else 0.0 reserved_qty = bin_po_item[0].reserved_qty if bin_po_item else 0.0 @@ -810,12 +898,15 @@ class TestSalesOrder(FrappeTestCase): po.load_from_db() # test after closing so - so.db_set('status', "Closed") + so.db_set("status", "Closed") so.update_reserved_qty() # test ordered_qty and reserved_qty for drop ship item after closing so - bin_po_item = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"}, - fields=["ordered_qty", "reserved_qty"]) + bin_po_item = frappe.get_all( + "Bin", + filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"}, + fields=["ordered_qty", "reserved_qty"], + ) ordered_qty = bin_po_item[0].ordered_qty if bin_po_item else 0.0 reserved_qty = bin_po_item[0].reserved_qty if bin_po_item else 0.0 @@ -838,8 +929,12 @@ class TestSalesOrder(FrappeTestCase): from erpnext.selling.doctype.sales_order.sales_order import 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}) + 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 = [ { @@ -848,7 +943,7 @@ class TestSalesOrder(FrappeTestCase): "qty": 2, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", }, { "item_code": po_item2.item_code, @@ -856,8 +951,8 @@ class TestSalesOrder(FrappeTestCase): "qty": 2, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' - } + "supplier": "_Test Supplier", + }, ] # create so and po @@ -871,7 +966,7 @@ class TestSalesOrder(FrappeTestCase): 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 + # test po item length self.assertEqual(len(po1.items), 1) # create po for remaining item @@ -905,7 +1000,7 @@ class TestSalesOrder(FrappeTestCase): "qty": 2, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", }, { "item_code": "_Test Item for Drop Shipping 2", @@ -913,8 +1008,8 @@ class TestSalesOrder(FrappeTestCase): "qty": 2, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier 1' - } + "supplier": "_Test Supplier 1", + }, ] # create so and po @@ -924,13 +1019,13 @@ class TestSalesOrder(FrappeTestCase): purchase_orders = make_purchase_order_for_default_supplier(so.name, selected_items=so_items) self.assertEqual(len(purchase_orders), 2) - self.assertEqual(purchase_orders[0].supplier, '_Test Supplier') - self.assertEqual(purchase_orders[1].supplier, '_Test Supplier 1') + self.assertEqual(purchase_orders[0].supplier, "_Test Supplier") + self.assertEqual(purchase_orders[1].supplier, "_Test Supplier 1") def test_product_bundles_in_so_are_replaced_with_bundle_items_in_po(self): """ - Tests if the the Product Bundles in the Items table of Sales Orders are replaced with - their child items(from the Packed Items table) on creating a Purchase Order from it. + Tests if the the Product Bundles in the Items table of Sales Orders are replaced with + their child items(from the Packed Items table) on creating a Purchase Order from it. """ from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order @@ -938,8 +1033,7 @@ class TestSalesOrder(FrappeTestCase): make_item("_Test Bundle Item 1", {"is_stock_item": 1}) make_item("_Test Bundle Item 2", {"is_stock_item": 1}) - make_product_bundle("_Test Product Bundle", - ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle", ["_Test Bundle Item 1", "_Test Bundle Item 2"]) so_items = [ { @@ -948,7 +1042,7 @@ class TestSalesOrder(FrappeTestCase): "qty": 2, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", } ] @@ -961,7 +1055,7 @@ class TestSalesOrder(FrappeTestCase): def test_purchase_order_updates_packed_item_ordered_qty(self): """ - Tests if the packed item's `ordered_qty` is updated with the quantity of the Purchase Order + Tests if the packed item's `ordered_qty` is updated with the quantity of the Purchase Order """ from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order @@ -969,8 +1063,7 @@ class TestSalesOrder(FrappeTestCase): make_item("_Test Bundle Item 1", {"is_stock_item": 1}) make_item("_Test Bundle Item 2", {"is_stock_item": 1}) - make_product_bundle("_Test Product Bundle", - ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle", ["_Test Bundle Item 1", "_Test Bundle Item 2"]) so_items = [ { @@ -979,7 +1072,7 @@ class TestSalesOrder(FrappeTestCase): "qty": 2, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", } ] @@ -996,145 +1089,163 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(so.packed_items[1].ordered_qty, 2) 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"]) + bin = frappe.get_all( + "Bin", + filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, + fields=["reserved_qty"], + ) existing_reserved_qty = bin[0].reserved_qty if bin else 0.0 so = make_sales_order(item_code="_Test Item", qty=1) - self.assertEqual(get_reserved_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"), existing_reserved_qty+1) + self.assertEqual( + get_reserved_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"), + existing_reserved_qty + 1, + ) so.update_status("Closed") - self.assertEqual(get_reserved_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"), existing_reserved_qty) + self.assertEqual( + get_reserved_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"), + existing_reserved_qty, + ) def test_create_so_with_margin(self): so = make_sales_order(item_code="_Test Item", qty=1, do_not_submit=True) so.items[0].price_list_rate = price_list_rate = 100 - so.items[0].margin_type = 'Percentage' + so.items[0].margin_type = "Percentage" so.items[0].margin_rate_or_amount = 25 so.save() new_so = frappe.copy_doc(so) new_so.save(ignore_permissions=True) - self.assertEqual(new_so.get("items")[0].rate, flt((price_list_rate*25)/100 + price_list_rate)) + self.assertEqual( + new_so.get("items")[0].rate, flt((price_list_rate * 25) / 100 + price_list_rate) + ) new_so.items[0].margin_rate_or_amount = 25 new_so.payment_schedule = [] new_so.save() new_so.submit() - self.assertEqual(new_so.get("items")[0].rate, flt((price_list_rate*25)/100 + price_list_rate)) + self.assertEqual( + new_so.get("items")[0].rate, flt((price_list_rate * 25) / 100 + price_list_rate) + ) def test_terms_auto_added(self): so = make_sales_order(do_not_save=1) - self.assertFalse(so.get('payment_schedule')) + self.assertFalse(so.get("payment_schedule")) so.insert() - self.assertTrue(so.get('payment_schedule')) + self.assertTrue(so.get("payment_schedule")) def test_terms_not_copied(self): so = make_sales_order() - self.assertTrue(so.get('payment_schedule')) + self.assertTrue(so.get("payment_schedule")) si = make_sales_invoice(so.name) - self.assertFalse(si.get('payment_schedule')) + self.assertFalse(si.get("payment_schedule")) def test_terms_copied(self): so = make_sales_order(do_not_copy=1, do_not_save=1) - so.payment_terms_template = '_Test Payment Term Template' + so.payment_terms_template = "_Test Payment Term Template" so.insert() so.submit() - self.assertTrue(so.get('payment_schedule')) + self.assertTrue(so.get("payment_schedule")) si = make_sales_invoice(so.name) si.insert() - self.assertTrue(si.get('payment_schedule')) + self.assertTrue(si.get("payment_schedule")) def test_make_work_order(self): # Make a new Sales Order - so = make_sales_order(**{ - "item_list": [{ - "item_code": "_Test FG Item", - "qty": 10, - "rate":100 - }, - { - "item_code": "_Test FG Item", - "qty": 20, - "rate":200 - }] - }) + so = make_sales_order( + **{ + "item_list": [ + {"item_code": "_Test FG Item", "qty": 10, "rate": 100}, + {"item_code": "_Test FG Item", "qty": 20, "rate": 200}, + ] + } + ) # Raise Work Orders - po_items= [] - so_item_name= {} + po_items = [] + so_item_name = {} for item in so.get_work_order_items(): - po_items.append({ - "warehouse": item.get("warehouse"), - "item_code": item.get("item_code"), - "pending_qty": item.get("pending_qty"), - "sales_order_item": item.get("sales_order_item"), - "bom": item.get("bom"), - "description": item.get("description") - }) - so_item_name[item.get("sales_order_item")]= item.get("pending_qty") - make_work_orders(json.dumps({"items":po_items}), so.name, so.company) + po_items.append( + { + "warehouse": item.get("warehouse"), + "item_code": item.get("item_code"), + "pending_qty": item.get("pending_qty"), + "sales_order_item": item.get("sales_order_item"), + "bom": item.get("bom"), + "description": item.get("description"), + } + ) + so_item_name[item.get("sales_order_item")] = item.get("pending_qty") + make_work_orders(json.dumps({"items": po_items}), so.name, so.company) # Check if Work Orders were raised for item in so_item_name: - wo_qty = frappe.db.sql("select sum(qty) from `tabWork Order` where sales_order=%s and sales_order_item=%s", (so.name, item)) + wo_qty = frappe.db.sql( + "select sum(qty) from `tabWork Order` where sales_order=%s and sales_order_item=%s", + (so.name, item), + ) self.assertEqual(wo_qty[0][0], so_item_name.get(item)) def test_serial_no_based_delivery(self): frappe.set_value("Stock Settings", None, "automatically_set_serial_nos_based_on_fifo", 1) - item = make_item("_Reserved_Serialized_Item", {"is_stock_item": 1, - "maintain_stock": 1, - "has_serial_no": 1, - "serial_no_series": "SI.####", - "valuation_rate": 500, - "item_defaults": [ - { - "default_warehouse": "_Test Warehouse - _TC", - "company": "_Test Company" - }] - }) + item = make_item( + "_Reserved_Serialized_Item", + { + "is_stock_item": 1, + "maintain_stock": 1, + "has_serial_no": 1, + "serial_no_series": "SI.####", + "valuation_rate": 500, + "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}], + }, + ) frappe.db.sql("""delete from `tabSerial No` where item_code=%s""", (item.item_code)) - make_item("_Test Item A", {"maintain_stock": 1, - "valuation_rate": 100, - "item_defaults": [ - { - "default_warehouse": "_Test Warehouse - _TC", - "company": "_Test Company" - }] - }) - make_item("_Test Item B", {"maintain_stock": 1, - "valuation_rate": 200, - "item_defaults": [ - { - "default_warehouse": "_Test Warehouse - _TC", - "company": "_Test Company" - }] - }) + make_item( + "_Test Item A", + { + "maintain_stock": 1, + "valuation_rate": 100, + "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}], + }, + ) + make_item( + "_Test Item B", + { + "maintain_stock": 1, + "valuation_rate": 200, + "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}], + }, + ) from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom - make_bom(item=item.item_code, rate=1000, - raw_materials = ['_Test Item A', '_Test Item B']) - so = make_sales_order(**{ - "item_list": [{ - "item_code": item.item_code, - "ensure_delivery_based_on_produced_serial_no": 1, - "qty": 1, - "rate":1000 - }] - }) + make_bom(item=item.item_code, rate=1000, raw_materials=["_Test Item A", "_Test Item B"]) + + so = make_sales_order( + **{ + "item_list": [ + { + "item_code": item.item_code, + "ensure_delivery_based_on_produced_serial_no": 1, + "qty": 1, + "rate": 1000, + } + ] + } + ) so.submit() from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record - work_order = make_wo_order_test_record(item=item.item_code, - qty=1, do_not_save=True) + + work_order = make_wo_order_test_record(item=item.item_code, qty=1, do_not_save=True) work_order.fg_warehouse = "_Test Warehouse - _TC" work_order.sales_order = so.name work_order.submit() @@ -1143,6 +1254,7 @@ class TestSalesOrder(FrappeTestCase): from erpnext.manufacturing.doctype.work_order.work_order import ( make_stock_entry as make_production_stock_entry, ) + se = frappe.get_doc(make_production_stock_entry(work_order.name, "Manufacture", 1)) se.submit() reserved_serial_no = se.get("items")[2].serial_no @@ -1154,7 +1266,7 @@ class TestSalesOrder(FrappeTestCase): item_line = dn.get("items")[0] item_line.serial_no = item_serial_no.name item_line = dn.get("items")[0] - item_line.serial_no = reserved_serial_no + item_line.serial_no = reserved_serial_no dn.submit() dn.load_from_db() dn.cancel() @@ -1177,6 +1289,7 @@ class TestSalesOrder(FrappeTestCase): from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( make_delivery_note as make_delivery_note_from_invoice, ) + dn = make_delivery_note_from_invoice(si.name) dn.save() dn.submit() @@ -1191,8 +1304,10 @@ class TestSalesOrder(FrappeTestCase): def test_advance_payment_entry_unlink_against_sales_order(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry - frappe.db.set_value("Accounts Settings", "Accounts Settings", - "unlink_advance_payment_on_cancelation_of_order", 0) + + frappe.db.set_value( + "Accounts Settings", "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order", 0 + ) so = make_sales_order() @@ -1207,7 +1322,7 @@ class TestSalesOrder(FrappeTestCase): pe.save(ignore_permissions=True) pe.submit() - so_doc = frappe.get_doc('Sales Order', so.name) + so_doc = frappe.get_doc("Sales Order", so.name) self.assertRaises(frappe.LinkExistsError, so_doc.cancel) @@ -1218,8 +1333,9 @@ class TestSalesOrder(FrappeTestCase): so = make_sales_order() # disable unlinking of payment entry - frappe.db.set_value("Accounts Settings", "Accounts Settings", - "unlink_advance_payment_on_cancelation_of_order", 0) + frappe.db.set_value( + "Accounts Settings", "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order", 0 + ) # create a payment entry against sales order pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Bank - _TC") @@ -1239,81 +1355,77 @@ class TestSalesOrder(FrappeTestCase): # Cancel sales order try: - so_doc = frappe.get_doc('Sales Order', so.name) + so_doc = frappe.get_doc("Sales Order", so.name) so_doc.cancel() except Exception: self.fail("Can not cancel sales order with linked cancelled payment entry") def test_request_for_raw_materials(self): - item = make_item("_Test Finished Item", {"is_stock_item": 1, - "maintain_stock": 1, - "valuation_rate": 500, - "item_defaults": [ - { - "default_warehouse": "_Test Warehouse - _TC", - "company": "_Test Company" - }] - }) - make_item("_Test Raw Item A", {"maintain_stock": 1, - "valuation_rate": 100, - "item_defaults": [ - { - "default_warehouse": "_Test Warehouse - _TC", - "company": "_Test Company" - }] - }) - make_item("_Test Raw Item B", {"maintain_stock": 1, - "valuation_rate": 200, - "item_defaults": [ - { - "default_warehouse": "_Test Warehouse - _TC", - "company": "_Test Company" - }] - }) + item = make_item( + "_Test Finished Item", + { + "is_stock_item": 1, + "maintain_stock": 1, + "valuation_rate": 500, + "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}], + }, + ) + make_item( + "_Test Raw Item A", + { + "maintain_stock": 1, + "valuation_rate": 100, + "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}], + }, + ) + make_item( + "_Test Raw Item B", + { + "maintain_stock": 1, + "valuation_rate": 200, + "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}], + }, + ) from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom - make_bom(item=item.item_code, rate=1000, - raw_materials = ['_Test Raw Item A', '_Test Raw Item B']) - so = make_sales_order(**{ - "item_list": [{ - "item_code": item.item_code, - "qty": 1, - "rate":1000 - }] - }) + make_bom(item=item.item_code, rate=1000, raw_materials=["_Test Raw Item A", "_Test Raw Item B"]) + + so = make_sales_order(**{"item_list": [{"item_code": item.item_code, "qty": 1, "rate": 1000}]}) so.submit() mr_dict = frappe._dict() items = so.get_work_order_items(1) - mr_dict['items'] = items - mr_dict['include_exploded_items'] = 0 - mr_dict['ignore_existing_ordered_qty'] = 1 + mr_dict["items"] = items + mr_dict["include_exploded_items"] = 0 + mr_dict["ignore_existing_ordered_qty"] = 1 make_raw_material_request(mr_dict, so.company, so.name) - mr = frappe.db.sql("""select name from `tabMaterial Request` ORDER BY creation DESC LIMIT 1""", as_dict=1)[0] - mr_doc = frappe.get_doc('Material Request',mr.get('name')) + mr = frappe.db.sql( + """select name from `tabMaterial Request` ORDER BY creation DESC LIMIT 1""", as_dict=1 + )[0] + mr_doc = frappe.get_doc("Material Request", mr.get("name")) self.assertEqual(mr_doc.items[0].sales_order, so.name) def test_so_optional_blanket_order(self): """ - Expected result: Blanket order Ordered Quantity should only be affected on Sales Order with against_blanket_order = 1. - Second Sales Order should not add on to Blanket Orders Ordered Quantity. + Expected result: Blanket order Ordered Quantity should only be affected on Sales Order with against_blanket_order = 1. + Second Sales Order should not add on to Blanket Orders Ordered Quantity. """ - bo = make_blanket_order(blanket_order_type = "Selling", quantity = 10, rate = 10) + bo = make_blanket_order(blanket_order_type="Selling", quantity=10, rate=10) - so = make_sales_order(item_code= "_Test Item", qty = 5, against_blanket_order = 1) - so_doc = frappe.get_doc('Sales Order', so.get('name')) + so = make_sales_order(item_code="_Test Item", qty=5, against_blanket_order=1) + so_doc = frappe.get_doc("Sales Order", so.get("name")) # To test if the SO has a Blanket Order self.assertTrue(so_doc.items[0].blanket_order) - so = make_sales_order(item_code= "_Test Item", qty = 5, against_blanket_order = 0) - so_doc = frappe.get_doc('Sales Order', so.get('name')) + so = make_sales_order(item_code="_Test Item", qty=5, against_blanket_order=0) + so_doc = frappe.get_doc("Sales Order", so.get("name")) # To test if the SO does NOT have a Blanket Order self.assertEqual(so_doc.items[0].blanket_order, None) def test_so_cancellation_when_si_drafted(self): """ - Test to check if Sales Order gets cancelled if Sales Invoice is in Draft state - Expected result: sales order should not get cancelled + Test to check if Sales Order gets cancelled if Sales Invoice is in Draft state + Expected result: sales order should not get cancelled """ so = make_sales_order() so.submit() @@ -1324,8 +1436,8 @@ class TestSalesOrder(FrappeTestCase): def test_so_cancellation_after_si_submission(self): """ - Test to check if Sales Order gets cancelled when linked Sales Invoice has been Submitted - Expected result: Sales Order should not get cancelled + Test to check if Sales Order gets cancelled when linked Sales Invoice has been Submitted + Expected result: Sales Order should not get cancelled """ so = make_sales_order() so.submit() @@ -1337,8 +1449,8 @@ class TestSalesOrder(FrappeTestCase): def test_so_cancellation_after_dn_submission(self): """ - Test to check if Sales Order gets cancelled when linked Delivery Note has been Submitted - Expected result: Sales Order should not get cancelled + Test to check if Sales Order gets cancelled when linked Delivery Note has been Submitted + Expected result: Sales Order should not get cancelled """ so = make_sales_order() so.submit() @@ -1350,7 +1462,7 @@ class TestSalesOrder(FrappeTestCase): def test_so_cancellation_after_maintenance_schedule_submission(self): """ - Expected result: Sales Order should not get cancelled + Expected result: Sales Order should not get cancelled """ so = make_sales_order() so.submit() @@ -1363,7 +1475,7 @@ class TestSalesOrder(FrappeTestCase): def test_so_cancellation_after_maintenance_visit_submission(self): """ - Expected result: Sales Order should not get cancelled + Expected result: Sales Order should not get cancelled """ so = make_sales_order() so.submit() @@ -1377,7 +1489,7 @@ class TestSalesOrder(FrappeTestCase): def test_so_cancellation_after_work_order_submission(self): """ - Expected result: Sales Order should not get cancelled + Expected result: Sales Order should not get cancelled """ from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record @@ -1398,7 +1510,7 @@ class TestSalesOrder(FrappeTestCase): so = make_sales_order(uom="Nos", do_not_save=1) create_payment_terms_template() - so.payment_terms_template = 'Test Receivable Template' + so.payment_terms_template = "Test Receivable Template" so.submit() si = create_sales_invoice(qty=10, do_not_save=1) @@ -1420,10 +1532,10 @@ class TestSalesOrder(FrappeTestCase): so.submit() self.assertEqual(so.net_total, 0) - self.assertEqual(so.billing_status, 'Not Billed') + self.assertEqual(so.billing_status, "Not Billed") si = create_sales_invoice(qty=10, do_not_save=1) - si.price_list = '_Test Price List' + si.price_list = "_Test Price List" si.items[0].rate = 0 si.items[0].price_list_rate = 0 si.items[0].sales_order = so.name @@ -1433,7 +1545,7 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(si.net_total, 0) so.load_from_db() - self.assertEqual(so.billing_status, 'Fully Billed') + self.assertEqual(so.billing_status, "Fully Billed") def test_so_back_updated_from_wo_via_mr(self): "SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO." @@ -1442,7 +1554,7 @@ class TestSalesOrder(FrappeTestCase): ) from erpnext.stock.doctype.material_request.material_request import raise_work_orders - so = make_sales_order(item_list=[{"item_code": "_Test FG Item","qty": 2, "rate":100}]) + so = make_sales_order(item_list=[{"item_code": "_Test FG Item", "qty": 2, "rate": 100}]) mr = make_material_request(so.name) mr.material_request_type = "Manufacture" @@ -1459,17 +1571,18 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(wo.sales_order_item, so.items[0].name) wo.submit() - make_stock_entry(item_code="_Test Item", # Stock RM - target="Work In Progress - _TC", - qty=4, basic_rate=100 + make_stock_entry( + item_code="_Test Item", target="Work In Progress - _TC", qty=4, basic_rate=100 # Stock RM ) - make_stock_entry(item_code="_Test Item Home Desktop 100", # Stock RM + make_stock_entry( + item_code="_Test Item Home Desktop 100", # Stock RM target="Work In Progress - _TC", - qty=4, basic_rate=100 + qty=4, + basic_rate=100, ) se = frappe.get_doc(make_se_from_wo(wo.name, "Manufacture", 2)) - se.submit() # Finish WO + se.submit() # Finish WO mr.reload() wo.reload() @@ -1479,7 +1592,10 @@ class TestSalesOrder(FrappeTestCase): def test_sales_order_with_shipping_rule(self): from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule - shipping_rule = create_shipping_rule(shipping_rule_type = "Selling", shipping_rule_name = "Shipping Rule - Sales Invoice Test") + + shipping_rule = create_shipping_rule( + shipping_rule_type="Selling", shipping_rule_name="Shipping Rule - Sales Invoice Test" + ) sales_order = make_sales_order(do_not_save=True) sales_order.shipping_rule = shipping_rule.name @@ -1499,29 +1615,32 @@ class TestSalesOrder(FrappeTestCase): sales_order.save() self.assertEqual(sales_order.taxes[0].tax_amount, 0) + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") accounts_settings.automatically_fetch_payment_terms = enable accounts_settings.save() + def compare_payment_schedules(doc, doc1, doc2): - for index, schedule in enumerate(doc1.get('payment_schedule')): + for index, schedule in enumerate(doc1.get("payment_schedule")): doc.assertEqual(schedule.payment_term, doc2.payment_schedule[index].payment_term) doc.assertEqual(getdate(schedule.due_date), doc2.payment_schedule[index].due_date) doc.assertEqual(schedule.invoice_portion, doc2.payment_schedule[index].invoice_portion) doc.assertEqual(schedule.payment_amount, doc2.payment_schedule[index].payment_amount) + def make_sales_order(**args): so = frappe.new_doc("Sales Order") args = frappe._dict(args) if args.transaction_date: so.transaction_date = args.transaction_date - so.set_warehouse = "" # no need to test set_warehouse permission since it only affects the client + so.set_warehouse = "" # no need to test set_warehouse permission since it only affects the client so.company = args.company or "_Test Company" so.customer = args.customer or "_Test Customer" so.currency = args.currency or "INR" - so.po_no = args.po_no or '12345' + so.po_no = args.po_no or "12345" if args.selling_price_list: so.selling_price_list = args.selling_price_list @@ -1533,14 +1652,17 @@ def make_sales_order(**args): so.append("items", item) else: - so.append("items", { - "item_code": args.item or args.item_code or "_Test Item", - "warehouse": args.warehouse, - "qty": args.qty or 10, - "uom": args.uom or None, - "rate": args.rate or 100, - "against_blanket_order": args.against_blanket_order - }) + so.append( + "items", + { + "item_code": args.item or args.item_code or "_Test Item", + "warehouse": args.warehouse, + "qty": args.qty or 10, + "uom": args.uom or None, + "rate": args.rate or 100, + "against_blanket_order": args.against_blanket_order, + }, + ) so.delivery_date = add_days(so.transaction_date, 10) @@ -1555,6 +1677,7 @@ def make_sales_order(**args): return so + def create_dn_against_so(so, delivered_qty=0): frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) @@ -1564,41 +1687,63 @@ def create_dn_against_so(so, delivered_qty=0): dn.submit() return dn + def get_reserved_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"): - return flt(frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, - "reserved_qty")) + return flt( + frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "reserved_qty") + ) + test_dependencies = ["Currency Exchange"] + def make_sales_order_workflow(): - if frappe.db.exists('Workflow', 'SO Test Workflow'): + if frappe.db.exists("Workflow", "SO Test Workflow"): doc = frappe.get_doc("Workflow", "SO Test Workflow") doc.set("is_active", 1) doc.save() return doc - frappe.get_doc(dict(doctype='Role', role_name='Test Junior Approver')).insert(ignore_if_duplicate=True) - frappe.get_doc(dict(doctype='Role', role_name='Test Approver')).insert(ignore_if_duplicate=True) - frappe.cache().hdel('roles', frappe.session.user) + frappe.get_doc(dict(doctype="Role", role_name="Test Junior Approver")).insert( + ignore_if_duplicate=True + ) + frappe.get_doc(dict(doctype="Role", role_name="Test Approver")).insert(ignore_if_duplicate=True) + frappe.cache().hdel("roles", frappe.session.user) - workflow = frappe.get_doc({ - "doctype": "Workflow", - "workflow_name": "SO Test Workflow", - "document_type": "Sales Order", - "workflow_state_field": "workflow_state", - "is_active": 1, - "send_email_alert": 0, - }) - workflow.append('states', dict( state = 'Pending', allow_edit = 'All' )) - workflow.append('states', dict( state = 'Approved', allow_edit = 'Test Approver', doc_status = 1 )) - workflow.append('transitions', dict( - state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Junior Approver', allow_self_approval = 1, - condition = 'doc.grand_total < 200' - )) - workflow.append('transitions', dict( - state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Approver', allow_self_approval = 1, - condition = 'doc.grand_total > 200' - )) + workflow = frappe.get_doc( + { + "doctype": "Workflow", + "workflow_name": "SO Test Workflow", + "document_type": "Sales Order", + "workflow_state_field": "workflow_state", + "is_active": 1, + "send_email_alert": 0, + } + ) + workflow.append("states", dict(state="Pending", allow_edit="All")) + workflow.append("states", dict(state="Approved", allow_edit="Test Approver", doc_status=1)) + workflow.append( + "transitions", + dict( + state="Pending", + action="Approve", + next_state="Approved", + allowed="Test Junior Approver", + allow_self_approval=1, + condition="doc.grand_total < 200", + ), + ) + workflow.append( + "transitions", + dict( + state="Pending", + action="Approve", + next_state="Approved", + allowed="Test Approver", + allow_self_approval=1, + condition="doc.grand_total > 200", + ), + ) workflow.insert(ignore_permissions=True) return workflow diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.py b/erpnext/selling/doctype/sales_order_item/sales_order_item.py index 441a6ac970..83d3f3bc07 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.py +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.py @@ -9,5 +9,6 @@ from frappe.model.document import Document class SalesOrderItem(Document): pass + def on_doctype_update(): frappe.db.add_index("Sales Order Item", ["item_code", "warehouse"]) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index e1ef63578e..29e4712be3 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/selling_settings.py @@ -16,23 +16,45 @@ class SellingSettings(Document): self.toggle_editable_rate_for_bundle_items() def validate(self): - for key in ["cust_master_name", "customer_group", "territory", - "maintain_same_sales_rate", "editable_price_list_rate", "selling_price_list"]: - frappe.db.set_default(key, self.get(key, "")) + for key in [ + "cust_master_name", + "customer_group", + "territory", + "maintain_same_sales_rate", + "editable_price_list_rate", + "selling_price_list", + ]: + frappe.db.set_default(key, self.get(key, "")) from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series - set_by_naming_series("Customer", "customer_name", - self.get("cust_master_name")=="Naming Series", hide_name_field=False) + + set_by_naming_series( + "Customer", + "customer_name", + self.get("cust_master_name") == "Naming Series", + hide_name_field=False, + ) def toggle_hide_tax_id(self): self.hide_tax_id = cint(self.hide_tax_id) # Make property setters to hide tax_id fields for doctype in ("Sales Order", "Sales Invoice", "Delivery Note"): - make_property_setter(doctype, "tax_id", "hidden", self.hide_tax_id, "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, "tax_id", "print_hide", self.hide_tax_id, "Check", validate_fields_for_doctype=False) + make_property_setter( + doctype, "tax_id", "hidden", self.hide_tax_id, "Check", validate_fields_for_doctype=False + ) + make_property_setter( + doctype, "tax_id", "print_hide", self.hide_tax_id, "Check", validate_fields_for_doctype=False + ) def toggle_editable_rate_for_bundle_items(self): editable_bundle_item_rates = cint(self.editable_bundle_item_rates) - make_property_setter("Packed Item", "rate", "read_only", not(editable_bundle_item_rates), "Check", validate_fields_for_doctype=False) + make_property_setter( + "Packed Item", + "rate", + "read_only", + not (editable_bundle_item_rates), + "Check", + validate_fields_for_doctype=False, + ) diff --git a/erpnext/selling/doctype/sms_center/sms_center.py b/erpnext/selling/doctype/sms_center/sms_center.py index d192457ee0..cdc7397e1e 100644 --- a/erpnext/selling/doctype/sms_center/sms_center.py +++ b/erpnext/selling/doctype/sms_center/sms_center.py @@ -12,59 +12,80 @@ from frappe.utils import cstr class SMSCenter(Document): @frappe.whitelist() def create_receiver_list(self): - rec, where_clause = '', '' - if self.send_to == 'All Customer Contact': + rec, where_clause = "", "" + if self.send_to == "All Customer Contact": where_clause = " and dl.link_doctype = 'Customer'" if self.customer: - where_clause += " and dl.link_name = '%s'" % \ - self.customer.replace("'", "\'") or " and ifnull(dl.link_name, '') != ''" - if self.send_to == 'All Supplier Contact': + where_clause += ( + " and dl.link_name = '%s'" % self.customer.replace("'", "'") + or " and ifnull(dl.link_name, '') != ''" + ) + if self.send_to == "All Supplier Contact": where_clause = " and dl.link_doctype = 'Supplier'" if self.supplier: - where_clause += " and dl.link_name = '%s'" % \ - self.supplier.replace("'", "\'") or " and ifnull(dl.link_name, '') != ''" - if self.send_to == 'All Sales Partner Contact': + where_clause += ( + " and dl.link_name = '%s'" % self.supplier.replace("'", "'") + or " and ifnull(dl.link_name, '') != ''" + ) + if self.send_to == "All Sales Partner Contact": where_clause = " and dl.link_doctype = 'Sales Partner'" if self.sales_partner: - where_clause += "and dl.link_name = '%s'" % \ - self.sales_partner.replace("'", "\'") or " and ifnull(dl.link_name, '') != ''" - if self.send_to in ['All Contact', 'All Customer Contact', 'All Supplier Contact', 'All Sales Partner Contact']: - rec = frappe.db.sql("""select CONCAT(ifnull(c.first_name,''), ' ', ifnull(c.last_name,'')), + where_clause += ( + "and dl.link_name = '%s'" % self.sales_partner.replace("'", "'") + or " and ifnull(dl.link_name, '') != ''" + ) + if self.send_to in [ + "All Contact", + "All Customer Contact", + "All Supplier Contact", + "All Sales Partner Contact", + ]: + rec = frappe.db.sql( + """select CONCAT(ifnull(c.first_name,''), ' ', ifnull(c.last_name,'')), c.mobile_no from `tabContact` c, `tabDynamic Link` dl where ifnull(c.mobile_no,'')!='' and - c.docstatus != 2 and dl.parent = c.name%s""" % where_clause) + c.docstatus != 2 and dl.parent = c.name%s""" + % where_clause + ) - elif self.send_to == 'All Lead (Open)': - rec = frappe.db.sql("""select lead_name, mobile_no from `tabLead` where - ifnull(mobile_no,'')!='' and docstatus != 2 and status='Open'""") + elif self.send_to == "All Lead (Open)": + rec = frappe.db.sql( + """select lead_name, mobile_no from `tabLead` where + ifnull(mobile_no,'')!='' and docstatus != 2 and status='Open'""" + ) - elif self.send_to == 'All Employee (Active)': - where_clause = self.department and " and department = '%s'" % \ - self.department.replace("'", "\'") or "" - where_clause += self.branch and " and branch = '%s'" % \ - self.branch.replace("'", "\'") or "" + elif self.send_to == "All Employee (Active)": + where_clause = ( + self.department and " and department = '%s'" % self.department.replace("'", "'") or "" + ) + where_clause += self.branch and " and branch = '%s'" % self.branch.replace("'", "'") or "" - rec = frappe.db.sql("""select employee_name, cell_number from + rec = frappe.db.sql( + """select employee_name, cell_number from `tabEmployee` where status = 'Active' and docstatus < 2 and - ifnull(cell_number,'')!='' %s""" % where_clause) + ifnull(cell_number,'')!='' %s""" + % where_clause + ) - elif self.send_to == 'All Sales Person': - rec = frappe.db.sql("""select sales_person_name, + elif self.send_to == "All Sales Person": + rec = frappe.db.sql( + """select sales_person_name, tabEmployee.cell_number from `tabSales Person` left join tabEmployee on `tabSales Person`.employee = tabEmployee.name - where ifnull(tabEmployee.cell_number,'')!=''""") + where ifnull(tabEmployee.cell_number,'')!=''""" + ) - rec_list = '' + rec_list = "" for d in rec: - rec_list += d[0] + ' - ' + d[1] + '\n' + rec_list += d[0] + " - " + d[1] + "\n" self.receiver_list = rec_list def get_receiver_nos(self): receiver_nos = [] if self.receiver_list: - for d in self.receiver_list.split('\n'): + for d in self.receiver_list.split("\n"): receiver_no = d - if '-' in d: - receiver_no = receiver_no.split('-')[1] + if "-" in d: + receiver_no = receiver_no.split("-")[1] if receiver_no.strip(): receiver_nos.append(cstr(receiver_no).strip()) else: diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 67948d779f..bf629824ad 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -20,31 +20,46 @@ def search_by_term(search_term, warehouse, price_list): barcode = result.get("barcode") or "" if result: - item_info = frappe.db.get_value("Item", item_code, - ["name as item_code", "item_name", "description", "stock_uom", "image as item_image", "is_stock_item"], - as_dict=1) + item_info = frappe.db.get_value( + "Item", + item_code, + [ + "name as item_code", + "item_name", + "description", + "stock_uom", + "image as item_image", + "is_stock_item", + ], + as_dict=1, + ) item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse) - price_list_rate, currency = frappe.db.get_value('Item Price', { - 'price_list': price_list, - 'item_code': item_code - }, ["price_list_rate", "currency"]) or [None, None] + price_list_rate, currency = frappe.db.get_value( + "Item Price", + {"price_list": price_list, "item_code": item_code}, + ["price_list_rate", "currency"], + ) or [None, None] - item_info.update({ - 'serial_no': serial_no, - 'batch_no': batch_no, - 'barcode': barcode, - 'price_list_rate': price_list_rate, - 'currency': currency, - 'actual_qty': item_stock_qty - }) + item_info.update( + { + "serial_no": serial_no, + "batch_no": batch_no, + "barcode": barcode, + "price_list_rate": price_list_rate, + "currency": currency, + "actual_qty": item_stock_qty, + } + ) + + return {"items": [item_info]} - return {'items': [item_info]} @frappe.whitelist() def get_items(start, page_length, price_list, item_group, pos_profile, search_term=""): warehouse, hide_unavailable_items = frappe.db.get_value( - 'POS Profile', pos_profile, ['warehouse', 'hide_unavailable_items']) + "POS Profile", pos_profile, ["warehouse", "hide_unavailable_items"] + ) result = [] @@ -53,20 +68,23 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te if result: return result - if not frappe.db.exists('Item Group', item_group): - item_group = get_root_of('Item Group') + if not frappe.db.exists("Item Group", item_group): + item_group = get_root_of("Item Group") condition = get_conditions(search_term) condition += get_item_group_condition(pos_profile) - lft, rgt = frappe.db.get_value('Item Group', item_group, ['lft', 'rgt']) + lft, rgt = frappe.db.get_value("Item Group", item_group, ["lft", "rgt"]) bin_join_selection, bin_join_condition = "", "" if hide_unavailable_items: bin_join_selection = ", `tabBin` bin" - bin_join_condition = "AND bin.warehouse = %(warehouse)s AND bin.item_code = item.name AND bin.actual_qty > 0" + bin_join_condition = ( + "AND bin.warehouse = %(warehouse)s AND bin.item_code = item.name AND bin.actual_qty > 0" + ) - items_data = frappe.db.sql(""" + items_data = frappe.db.sql( + """ SELECT item.name AS item_code, item.item_name, @@ -87,22 +105,26 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te ORDER BY item.name asc LIMIT - {start}, {page_length}""" - .format( + {start}, {page_length}""".format( start=start, page_length=page_length, lft=lft, rgt=rgt, condition=condition, bin_join_selection=bin_join_selection, - bin_join_condition=bin_join_condition - ), {'warehouse': warehouse}, as_dict=1) + bin_join_condition=bin_join_condition, + ), + {"warehouse": warehouse}, + as_dict=1, + ) if items_data: items = [d.item_code for d in items_data] - item_prices_data = frappe.get_all("Item Price", - fields = ["item_code", "price_list_rate", "currency"], - filters = {'price_list': price_list, 'item_code': ['in', items]}) + item_prices_data = frappe.get_all( + "Item Price", + fields=["item_code", "price_list_rate", "currency"], + filters={"price_list": price_list, "item_code": ["in", items]}, + ) item_prices = {} for d in item_prices_data: @@ -115,176 +137,204 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te row = {} row.update(item) - row.update({ - 'price_list_rate': item_price.get('price_list_rate'), - 'currency': item_price.get('currency'), - 'actual_qty': item_stock_qty, - }) + row.update( + { + "price_list_rate": item_price.get("price_list_rate"), + "currency": item_price.get("currency"), + "actual_qty": item_stock_qty, + } + ) result.append(row) - return {'items': result} + return {"items": result} + @frappe.whitelist() def search_for_serial_or_batch_or_barcode_number(search_value): # search barcode no - barcode_data = frappe.db.get_value('Item Barcode', {'barcode': search_value}, ['barcode', 'parent as item_code'], as_dict=True) + barcode_data = frappe.db.get_value( + "Item Barcode", {"barcode": search_value}, ["barcode", "parent as item_code"], as_dict=True + ) if barcode_data: return barcode_data # search serial no - serial_no_data = frappe.db.get_value('Serial No', search_value, ['name as serial_no', 'item_code'], as_dict=True) + serial_no_data = frappe.db.get_value( + "Serial No", search_value, ["name as serial_no", "item_code"], as_dict=True + ) if serial_no_data: return serial_no_data # search batch no - batch_no_data = frappe.db.get_value('Batch', search_value, ['name as batch_no', 'item as item_code'], as_dict=True) + batch_no_data = frappe.db.get_value( + "Batch", search_value, ["name as batch_no", "item as item_code"], as_dict=True + ) if batch_no_data: return batch_no_data return {} + def get_conditions(search_term): condition = "(" condition += """item.name like {search_term} - or item.item_name like {search_term}""".format(search_term=frappe.db.escape('%' + search_term + '%')) + or item.item_name like {search_term}""".format( + search_term=frappe.db.escape("%" + search_term + "%") + ) condition += add_search_fields_condition(search_term) condition += ")" return condition + def add_search_fields_condition(search_term): - condition = '' - search_fields = frappe.get_all('POS Search Fields', fields = ['fieldname']) + condition = "" + search_fields = frappe.get_all("POS Search Fields", fields=["fieldname"]) if search_fields: for field in search_fields: - condition += " or item.`{0}` like {1}".format(field['fieldname'], frappe.db.escape('%' + search_term + '%')) + condition += " or item.`{0}` like {1}".format( + field["fieldname"], frappe.db.escape("%" + search_term + "%") + ) return condition + def get_item_group_condition(pos_profile): cond = "and 1=1" item_groups = get_item_groups(pos_profile) if item_groups: - cond = "and item.item_group in (%s)"%(', '.join(['%s']*len(item_groups))) + cond = "and item.item_group in (%s)" % (", ".join(["%s"] * len(item_groups))) return cond % tuple(item_groups) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def item_group_query(doctype, txt, searchfield, start, page_len, filters): item_groups = [] cond = "1=1" - pos_profile= filters.get('pos_profile') + pos_profile = filters.get("pos_profile") if pos_profile: item_groups = get_item_groups(pos_profile) if item_groups: - cond = "name in (%s)"%(', '.join(['%s']*len(item_groups))) + cond = "name in (%s)" % (", ".join(["%s"] * len(item_groups))) cond = cond % tuple(item_groups) - return frappe.db.sql(""" select distinct name from `tabItem Group` - where {condition} and (name like %(txt)s) limit {start}, {page_len}""" - .format(condition = cond, start=start, page_len= page_len), - {'txt': '%%%s%%' % txt}) + return frappe.db.sql( + """ select distinct name from `tabItem Group` + where {condition} and (name like %(txt)s) limit {start}, {page_len}""".format( + condition=cond, start=start, page_len=page_len + ), + {"txt": "%%%s%%" % txt}, + ) + @frappe.whitelist() def check_opening_entry(user): - open_vouchers = frappe.db.get_all("POS Opening Entry", - filters = { - "user": user, - "pos_closing_entry": ["in", ["", None]], - "docstatus": 1 - }, - fields = ["name", "company", "pos_profile", "period_start_date"], - order_by = "period_start_date desc" + open_vouchers = frappe.db.get_all( + "POS Opening Entry", + filters={"user": user, "pos_closing_entry": ["in", ["", None]], "docstatus": 1}, + fields=["name", "company", "pos_profile", "period_start_date"], + order_by="period_start_date desc", ) return open_vouchers + @frappe.whitelist() def create_opening_voucher(pos_profile, company, balance_details): balance_details = json.loads(balance_details) - new_pos_opening = frappe.get_doc({ - 'doctype': 'POS Opening Entry', - "period_start_date": frappe.utils.get_datetime(), - "posting_date": frappe.utils.getdate(), - "user": frappe.session.user, - "pos_profile": pos_profile, - "company": company, - }) + new_pos_opening = frappe.get_doc( + { + "doctype": "POS Opening Entry", + "period_start_date": frappe.utils.get_datetime(), + "posting_date": frappe.utils.getdate(), + "user": frappe.session.user, + "pos_profile": pos_profile, + "company": company, + } + ) new_pos_opening.set("balance_details", balance_details) new_pos_opening.submit() return new_pos_opening.as_dict() + @frappe.whitelist() def get_past_order_list(search_term, status, limit=20): - fields = ['name', 'grand_total', 'currency', 'customer', 'posting_time', 'posting_date'] + fields = ["name", "grand_total", "currency", "customer", "posting_time", "posting_date"] invoice_list = [] if search_term and status: - invoices_by_customer = frappe.db.get_all('POS Invoice', filters={ - 'customer': ['like', '%{}%'.format(search_term)], - 'status': status - }, fields=fields) - invoices_by_name = frappe.db.get_all('POS Invoice', filters={ - 'name': ['like', '%{}%'.format(search_term)], - 'status': status - }, fields=fields) + invoices_by_customer = frappe.db.get_all( + "POS Invoice", + filters={"customer": ["like", "%{}%".format(search_term)], "status": status}, + fields=fields, + ) + invoices_by_name = frappe.db.get_all( + "POS Invoice", + filters={"name": ["like", "%{}%".format(search_term)], "status": status}, + fields=fields, + ) invoice_list = invoices_by_customer + invoices_by_name elif status: - invoice_list = frappe.db.get_all('POS Invoice', filters={ - 'status': status - }, fields=fields) + invoice_list = frappe.db.get_all("POS Invoice", filters={"status": status}, fields=fields) return invoice_list + @frappe.whitelist() def set_customer_info(fieldname, customer, value=""): - if fieldname == 'loyalty_program': - frappe.db.set_value('Customer', customer, 'loyalty_program', value) + if fieldname == "loyalty_program": + frappe.db.set_value("Customer", customer, "loyalty_program", value) - contact = frappe.get_cached_value('Customer', customer, 'customer_primary_contact') + contact = frappe.get_cached_value("Customer", customer, "customer_primary_contact") if not contact: - contact = frappe.db.sql(""" + contact = frappe.db.sql( + """ SELECT parent FROM `tabDynamic Link` WHERE parenttype = 'Contact' AND parentfield = 'links' AND link_doctype = 'Customer' AND link_name = %s - """, (customer), as_dict=1) - contact = contact[0].get('parent') if contact else None + """, + (customer), + as_dict=1, + ) + contact = contact[0].get("parent") if contact else None if not contact: - new_contact = frappe.new_doc('Contact') + new_contact = frappe.new_doc("Contact") new_contact.is_primary_contact = 1 new_contact.first_name = customer - new_contact.set('links', [{'link_doctype': 'Customer', 'link_name': customer}]) + new_contact.set("links", [{"link_doctype": "Customer", "link_name": customer}]) new_contact.save() contact = new_contact.name - frappe.db.set_value('Customer', customer, 'customer_primary_contact', contact) + frappe.db.set_value("Customer", customer, "customer_primary_contact", contact) - contact_doc = frappe.get_doc('Contact', contact) - if fieldname == 'email_id': - contact_doc.set('email_ids', [{ 'email_id': value, 'is_primary': 1}]) - frappe.db.set_value('Customer', customer, 'email_id', value) - elif fieldname == 'mobile_no': - contact_doc.set('phone_nos', [{ 'phone': value, 'is_primary_mobile_no': 1}]) - frappe.db.set_value('Customer', customer, 'mobile_no', value) + contact_doc = frappe.get_doc("Contact", contact) + if fieldname == "email_id": + contact_doc.set("email_ids", [{"email_id": value, "is_primary": 1}]) + frappe.db.set_value("Customer", customer, "email_id", value) + elif fieldname == "mobile_no": + contact_doc.set("phone_nos", [{"phone": value, "is_primary_mobile_no": 1}]) + frappe.db.set_value("Customer", customer, "mobile_no", value) contact_doc.save() + @frappe.whitelist() def get_pos_profile_data(pos_profile): - pos_profile = frappe.get_doc('POS Profile', pos_profile) + pos_profile = frappe.get_doc("POS Profile", pos_profile) pos_profile = pos_profile.as_dict() _customer_groups_with_children = [] for row in pos_profile.customer_groups: - children = get_child_nodes('Customer Group', row.customer_group) + children = get_child_nodes("Customer Group", row.customer_group) _customer_groups_with_children.extend(children) pos_profile.customer_groups = _customer_groups_with_children - return pos_profile \ No newline at end of file + return pos_profile diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.py b/erpnext/selling/page/sales_funnel/sales_funnel.py index a75108e403..c626f5b05f 100644 --- a/erpnext/selling/page/sales_funnel/sales_funnel.py +++ b/erpnext/selling/page/sales_funnel/sales_funnel.py @@ -16,86 +16,153 @@ def validate_filters(from_date, to_date, company): if not company: frappe.throw(_("Please Select a Company")) + @frappe.whitelist() def get_funnel_data(from_date, to_date, company): validate_filters(from_date, to_date, company) - active_leads = frappe.db.sql("""select count(*) from `tabLead` + active_leads = frappe.db.sql( + """select count(*) from `tabLead` where (date(`creation`) between %s and %s) - and company=%s""", (from_date, to_date, company))[0][0] + and company=%s""", + (from_date, to_date, company), + )[0][0] - opportunities = frappe.db.sql("""select count(*) from `tabOpportunity` + opportunities = frappe.db.sql( + """select count(*) from `tabOpportunity` where (date(`creation`) between %s and %s) - and opportunity_from='Lead' and company=%s""", (from_date, to_date, company))[0][0] + and opportunity_from='Lead' and company=%s""", + (from_date, to_date, company), + )[0][0] - quotations = frappe.db.sql("""select count(*) from `tabQuotation` + quotations = frappe.db.sql( + """select count(*) from `tabQuotation` where docstatus = 1 and (date(`creation`) between %s and %s) - and (opportunity!="" or quotation_to="Lead") and company=%s""", (from_date, to_date, company))[0][0] + and (opportunity!="" or quotation_to="Lead") and company=%s""", + (from_date, to_date, company), + )[0][0] - converted = frappe.db.sql("""select count(*) from `tabCustomer` + converted = frappe.db.sql( + """select count(*) from `tabCustomer` JOIN `tabLead` ON `tabLead`.name = `tabCustomer`.lead_name WHERE (date(`tabCustomer`.creation) between %s and %s) - and `tabLead`.company=%s""", (from_date, to_date, company))[0][0] - + and `tabLead`.company=%s""", + (from_date, to_date, company), + )[0][0] return [ - { "title": _("Active Leads"), "value": active_leads, "color": "#B03B46" }, - { "title": _("Opportunities"), "value": opportunities, "color": "#F09C00" }, - { "title": _("Quotations"), "value": quotations, "color": "#006685" }, - { "title": _("Converted"), "value": converted, "color": "#00AD65" } + {"title": _("Active Leads"), "value": active_leads, "color": "#B03B46"}, + {"title": _("Opportunities"), "value": opportunities, "color": "#F09C00"}, + {"title": _("Quotations"), "value": quotations, "color": "#006685"}, + {"title": _("Converted"), "value": converted, "color": "#00AD65"}, ] + @frappe.whitelist() def get_opp_by_lead_source(from_date, to_date, company): validate_filters(from_date, to_date, company) - opportunities = frappe.get_all("Opportunity", filters=[['status', 'in', ['Open', 'Quotation', 'Replied']], ['company', '=', company], ['transaction_date', 'Between', [from_date, to_date]]], fields=['currency', 'sales_stage', 'opportunity_amount', 'probability', 'source']) + opportunities = frappe.get_all( + "Opportunity", + filters=[ + ["status", "in", ["Open", "Quotation", "Replied"]], + ["company", "=", company], + ["transaction_date", "Between", [from_date, to_date]], + ], + fields=["currency", "sales_stage", "opportunity_amount", "probability", "source"], + ) if opportunities: - default_currency = frappe.get_cached_value('Global Defaults', 'None', 'default_currency') + default_currency = frappe.get_cached_value("Global Defaults", "None", "default_currency") - cp_opportunities = [dict(x, **{'compound_amount': (convert(x['opportunity_amount'], x['currency'], default_currency, to_date) * x['probability']/100)}) for x in opportunities] + cp_opportunities = [ + dict( + x, + **{ + "compound_amount": ( + convert(x["opportunity_amount"], x["currency"], default_currency, to_date) + * x["probability"] + / 100 + ) + } + ) + for x in opportunities + ] - df = pd.DataFrame(cp_opportunities).groupby(['source', 'sales_stage'], as_index=False).agg({'compound_amount': 'sum'}) + df = ( + pd.DataFrame(cp_opportunities) + .groupby(["source", "sales_stage"], as_index=False) + .agg({"compound_amount": "sum"}) + ) result = {} - result['labels'] = list(set(df.source.values)) - result['datasets'] = [] + result["labels"] = list(set(df.source.values)) + result["datasets"] = [] for s in set(df.sales_stage.values): - result['datasets'].append({'name': s, 'values': [0]*len(result['labels']), 'chartType': 'bar'}) + result["datasets"].append( + {"name": s, "values": [0] * len(result["labels"]), "chartType": "bar"} + ) for row in df.itertuples(): - source_index = result['labels'].index(row.source) + source_index = result["labels"].index(row.source) - for dataset in result['datasets']: - if dataset['name'] == row.sales_stage: - dataset['values'][source_index] = row.compound_amount + for dataset in result["datasets"]: + if dataset["name"] == row.sales_stage: + dataset["values"][source_index] = row.compound_amount return result else: - return 'empty' + return "empty" + @frappe.whitelist() def get_pipeline_data(from_date, to_date, company): validate_filters(from_date, to_date, company) - opportunities = frappe.get_all("Opportunity", filters=[['status', 'in', ['Open', 'Quotation', 'Replied']], ['company', '=', company], ['transaction_date', 'Between', [from_date, to_date]]], fields=['currency', 'sales_stage', 'opportunity_amount', 'probability']) + opportunities = frappe.get_all( + "Opportunity", + filters=[ + ["status", "in", ["Open", "Quotation", "Replied"]], + ["company", "=", company], + ["transaction_date", "Between", [from_date, to_date]], + ], + fields=["currency", "sales_stage", "opportunity_amount", "probability"], + ) if opportunities: - default_currency = frappe.get_cached_value('Global Defaults', 'None', 'default_currency') + default_currency = frappe.get_cached_value("Global Defaults", "None", "default_currency") - cp_opportunities = [dict(x, **{'compound_amount': (convert(x['opportunity_amount'], x['currency'], default_currency, to_date) * x['probability']/100)}) for x in opportunities] + cp_opportunities = [ + dict( + x, + **{ + "compound_amount": ( + convert(x["opportunity_amount"], x["currency"], default_currency, to_date) + * x["probability"] + / 100 + ) + } + ) + for x in opportunities + ] - df = pd.DataFrame(cp_opportunities).groupby(['sales_stage'], as_index=True).agg({'compound_amount': 'sum'}).to_dict() + df = ( + pd.DataFrame(cp_opportunities) + .groupby(["sales_stage"], as_index=True) + .agg({"compound_amount": "sum"}) + .to_dict() + ) result = {} - result['labels'] = df['compound_amount'].keys() - result['datasets'] = [] - result['datasets'].append({'name': _("Total Amount"), 'values': df['compound_amount'].values(), 'chartType': 'bar'}) + result["labels"] = df["compound_amount"].keys() + result["datasets"] = [] + result["datasets"].append( + {"name": _("Total Amount"), "values": df["compound_amount"].values(), "chartType": "bar"} + ) return result else: - return 'empty' + return "empty" diff --git a/erpnext/selling/report/address_and_contacts/address_and_contacts.py b/erpnext/selling/report/address_and_contacts/address_and_contacts.py index 915f8dc3cf..9a1cfda847 100644 --- a/erpnext/selling/report/address_and_contacts/address_and_contacts.py +++ b/erpnext/selling/report/address_and_contacts/address_and_contacts.py @@ -5,20 +5,30 @@ import frappe field_map = { - "Contact": [ "first_name", "last_name", "phone", "mobile_no", "email_id", "is_primary_contact" ], - "Address": [ "address_line1", "address_line2", "city", "state", "pincode", "country", "is_primary_address" ] + "Contact": ["first_name", "last_name", "phone", "mobile_no", "email_id", "is_primary_contact"], + "Address": [ + "address_line1", + "address_line2", + "city", + "state", + "pincode", + "country", + "is_primary_address", + ], } + def execute(filters=None): columns, data = get_columns(filters), get_data(filters) return columns, data + def get_columns(filters): party_type = filters.get("party_type") party_type_value = get_party_group(party_type) return [ "{party_type}:Link/{party_type}".format(party_type=party_type), - "{party_value_type}::150".format(party_value_type = frappe.unscrub(str(party_type_value))), + "{party_value_type}::150".format(party_value_type=frappe.unscrub(str(party_type_value))), "Address Line 1", "Address Line 2", "City", @@ -31,9 +41,10 @@ def get_columns(filters): "Phone", "Mobile No", "Email Id", - "Is Primary Contact:Check" + "Is Primary Contact:Check", ] + def get_data(filters): party_type = filters.get("party_type") party = filters.get("party_name") @@ -41,6 +52,7 @@ def get_data(filters): return get_party_addresses_and_contact(party_type, party, party_group) + def get_party_addresses_and_contact(party_type, party, party_group): data = [] filters = None @@ -50,9 +62,11 @@ def get_party_addresses_and_contact(party_type, party, party_group): return [] if party: - filters = { "name": party } + filters = {"name": party} - fetch_party_list = frappe.get_list(party_type, filters=filters, fields=["name", party_group], as_list=True) + fetch_party_list = frappe.get_list( + party_type, filters=filters, fields=["name", party_group], as_list=True + ) party_list = [d[0] for d in fetch_party_list] party_groups = {} for d in fetch_party_list: @@ -66,7 +80,7 @@ def get_party_addresses_and_contact(party_type, party, party_group): for party, details in party_details.items(): addresses = details.get("address", []) - contacts = details.get("contact", []) + contacts = details.get("contact", []) if not any([addresses, contacts]): result = [party] result.append(party_groups[party]) @@ -89,10 +103,11 @@ def get_party_addresses_and_contact(party_type, party, party_group): data.append(result) return data + def get_party_details(party_type, party_list, doctype, party_details): - filters = [ + filters = [ ["Dynamic Link", "link_doctype", "=", party_type], - ["Dynamic Link", "link_name", "in", party_list] + ["Dynamic Link", "link_name", "in", party_list], ] fields = ["`tabDynamic Link`.link_name"] + field_map.get(doctype, []) @@ -103,15 +118,18 @@ def get_party_details(party_type, party_list, doctype, party_details): return party_details + def add_blank_columns_for(doctype): return ["" for field in field_map.get(doctype, [])] + def get_party_group(party_type): - if not party_type: return + if not party_type: + return group = { "Customer": "customer_group", "Supplier": "supplier_group", - "Sales Partner": "partner_type" + "Sales Partner": "partner_type", } return group[party_type] diff --git a/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py b/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py index e702a51d0e..5e763bb436 100644 --- a/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py +++ b/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py @@ -7,7 +7,8 @@ from frappe.utils import flt def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns() iwq_map = get_item_warehouse_quantity_map() @@ -20,32 +21,49 @@ def execute(filters=None): for wh, item_qty in warehouse.items(): total += 1 if item_map.get(sbom): - row = [sbom, item_map.get(sbom).item_name, item_map.get(sbom).description, - item_map.get(sbom).stock_uom, wh] + row = [ + sbom, + item_map.get(sbom).item_name, + item_map.get(sbom).description, + item_map.get(sbom).stock_uom, + wh, + ] available_qty = item_qty total_qty += flt(available_qty) row += [available_qty] if available_qty: data.append(row) - if (total == len(warehouse)): + if total == len(warehouse): row = ["", "", "Total", "", "", total_qty] data.append(row) return columns, data + def get_columns(): - columns = ["Item Code:Link/Item:100", "Item Name::100", "Description::120", \ - "UOM:Link/UOM:80", "Warehouse:Link/Warehouse:100", "Quantity::100"] + columns = [ + "Item Code:Link/Item:100", + "Item Name::100", + "Description::120", + "UOM:Link/UOM:80", + "Warehouse:Link/Warehouse:100", + "Quantity::100", + ] return columns + def get_item_details(): item_map = {} - for item in frappe.db.sql("""SELECT name, item_name, description, stock_uom - from `tabItem`""", as_dict=1): + for item in frappe.db.sql( + """SELECT name, item_name, description, stock_uom + from `tabItem`""", + as_dict=1, + ): item_map.setdefault(item.name, item) return item_map + def get_item_warehouse_quantity_map(): query = """SELECT parent, warehouse, MIN(qty) AS qty FROM (SELECT b.parent, bi.item_code, bi.warehouse, diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py index 2426cbb0b5..33badc37f8 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py @@ -12,178 +12,177 @@ from frappe.utils import cint, cstr, getdate def execute(filters=None): common_columns = [ { - 'label': _('New Customers'), - 'fieldname': 'new_customers', - 'fieldtype': 'Int', - 'default': 0, - 'width': 125 + "label": _("New Customers"), + "fieldname": "new_customers", + "fieldtype": "Int", + "default": 0, + "width": 125, }, { - 'label': _('Repeat Customers'), - 'fieldname': 'repeat_customers', - 'fieldtype': 'Int', - 'default': 0, - 'width': 125 + "label": _("Repeat Customers"), + "fieldname": "repeat_customers", + "fieldtype": "Int", + "default": 0, + "width": 125, + }, + {"label": _("Total"), "fieldname": "total", "fieldtype": "Int", "default": 0, "width": 100}, + { + "label": _("New Customer Revenue"), + "fieldname": "new_customer_revenue", + "fieldtype": "Currency", + "default": 0.0, + "width": 175, }, { - 'label': _('Total'), - 'fieldname': 'total', - 'fieldtype': 'Int', - 'default': 0, - 'width': 100 + "label": _("Repeat Customer Revenue"), + "fieldname": "repeat_customer_revenue", + "fieldtype": "Currency", + "default": 0.0, + "width": 175, }, { - 'label': _('New Customer Revenue'), - 'fieldname': 'new_customer_revenue', - 'fieldtype': 'Currency', - 'default': 0.0, - 'width': 175 + "label": _("Total Revenue"), + "fieldname": "total_revenue", + "fieldtype": "Currency", + "default": 0.0, + "width": 175, }, - { - 'label': _('Repeat Customer Revenue'), - 'fieldname': 'repeat_customer_revenue', - 'fieldtype': 'Currency', - 'default': 0.0, - 'width': 175 - }, - { - 'label': _('Total Revenue'), - 'fieldname': 'total_revenue', - 'fieldtype': 'Currency', - 'default': 0.0, - 'width': 175 - } ] - if filters.get('view_type') == 'Monthly': + if filters.get("view_type") == "Monthly": return get_data_by_time(filters, common_columns) else: return get_data_by_territory(filters, common_columns) + def get_data_by_time(filters, common_columns): # key yyyy-mm columns = [ - { - 'label': _('Year'), - 'fieldname': 'year', - 'fieldtype': 'Data', - 'width': 100 - }, - { - 'label': _('Month'), - 'fieldname': 'month', - 'fieldtype': 'Data', - 'width': 100 - }, + {"label": _("Year"), "fieldname": "year", "fieldtype": "Data", "width": 100}, + {"label": _("Month"), "fieldname": "month", "fieldtype": "Data", "width": 100}, ] columns += common_columns customers_in = get_customer_stats(filters) # time series - from_year, from_month, temp = filters.get('from_date').split('-') - to_year, to_month, temp = filters.get('to_date').split('-') + from_year, from_month, temp = filters.get("from_date").split("-") + to_year, to_month, temp = filters.get("to_date").split("-") - from_year, from_month, to_year, to_month = \ - cint(from_year), cint(from_month), cint(to_year), cint(to_month) + from_year, from_month, to_year, to_month = ( + cint(from_year), + cint(from_month), + cint(to_year), + cint(to_month), + ) out = [] - for year in range(from_year, to_year+1): - for month in range(from_month if year==from_year else 1, (to_month+1) if year==to_year else 13): - key = '{year}-{month:02d}'.format(year=year, month=month) + for year in range(from_year, to_year + 1): + for month in range( + from_month if year == from_year else 1, (to_month + 1) if year == to_year else 13 + ): + key = "{year}-{month:02d}".format(year=year, month=month) data = customers_in.get(key) - new = data['new'] if data else [0, 0.0] - repeat = data['repeat'] if data else [0, 0.0] - out.append({ - 'year': cstr(year), - 'month': calendar.month_name[month], - 'new_customers': new[0], - 'repeat_customers': repeat[0], - 'total': new[0] + repeat[0], - 'new_customer_revenue': new[1], - 'repeat_customer_revenue': repeat[1], - 'total_revenue': new[1] + repeat[1] - }) + new = data["new"] if data else [0, 0.0] + repeat = data["repeat"] if data else [0, 0.0] + out.append( + { + "year": cstr(year), + "month": calendar.month_name[month], + "new_customers": new[0], + "repeat_customers": repeat[0], + "total": new[0] + repeat[0], + "new_customer_revenue": new[1], + "repeat_customer_revenue": repeat[1], + "total_revenue": new[1] + repeat[1], + } + ) return columns, out + def get_data_by_territory(filters, common_columns): - columns = [{ - 'label': 'Territory', - 'fieldname': 'territory', - 'fieldtype': 'Link', - 'options': 'Territory', - 'width': 150 - }] + columns = [ + { + "label": "Territory", + "fieldname": "territory", + "fieldtype": "Link", + "options": "Territory", + "width": 150, + } + ] columns += common_columns customers_in = get_customer_stats(filters, tree_view=True) territory_dict = {} - for t in frappe.db.sql('''SELECT name, lft, parent_territory, is_group FROM `tabTerritory` ORDER BY lft''', as_dict=1): - territory_dict.update({ - t.name: { - 'parent': t.parent_territory, - 'is_group': t.is_group - } - }) + for t in frappe.db.sql( + """SELECT name, lft, parent_territory, is_group FROM `tabTerritory` ORDER BY lft""", as_dict=1 + ): + territory_dict.update({t.name: {"parent": t.parent_territory, "is_group": t.is_group}}) depth_map = frappe._dict() for name, info in territory_dict.items(): - default = depth_map.get(info['parent']) + 1 if info['parent'] else 0 + default = depth_map.get(info["parent"]) + 1 if info["parent"] else 0 depth_map.setdefault(name, default) data = [] for name, indent in depth_map.items(): condition = customers_in.get(name) - new = customers_in[name]['new'] if condition else [0, 0.0] - repeat = customers_in[name]['repeat'] if condition else [0, 0.0] + new = customers_in[name]["new"] if condition else [0, 0.0] + repeat = customers_in[name]["repeat"] if condition else [0, 0.0] temp = { - 'territory': name, - 'parent_territory': territory_dict[name]['parent'], - 'indent': indent, - 'new_customers': new[0], - 'repeat_customers': repeat[0], - 'total': new[0] + repeat[0], - 'new_customer_revenue': new[1], - 'repeat_customer_revenue': repeat[1], - 'total_revenue': new[1] + repeat[1], - 'bold': 0 if indent else 1 + "territory": name, + "parent_territory": territory_dict[name]["parent"], + "indent": indent, + "new_customers": new[0], + "repeat_customers": repeat[0], + "total": new[0] + repeat[0], + "new_customer_revenue": new[1], + "repeat_customer_revenue": repeat[1], + "total_revenue": new[1] + repeat[1], + "bold": 0 if indent else 1, } data.append(temp) - loop_data = sorted(data, key=lambda k: k['indent'], reverse=True) + loop_data = sorted(data, key=lambda k: k["indent"], reverse=True) for ld in loop_data: - if ld['parent_territory']: - parent_data = [x for x in data if x['territory'] == ld['parent_territory']][0] + if ld["parent_territory"]: + parent_data = [x for x in data if x["territory"] == ld["parent_territory"]][0] for key in parent_data.keys(): - if key not in ['indent', 'territory', 'parent_territory', 'bold']: + if key not in ["indent", "territory", "parent_territory", "bold"]: parent_data[key] += ld[key] return columns, data, None, None, None, 1 + def get_customer_stats(filters, tree_view=False): - """ Calculates number of new and repeated customers and revenue. """ - company_condition = '' - if filters.get('company'): - company_condition = ' and company=%(company)s' + """Calculates number of new and repeated customers and revenue.""" + company_condition = "" + if filters.get("company"): + company_condition = " and company=%(company)s" customers = [] customers_in = {} - for si in frappe.db.sql('''select territory, posting_date, customer, base_grand_total from `tabSales Invoice` + for si in frappe.db.sql( + """select territory, posting_date, customer, base_grand_total from `tabSales Invoice` where docstatus=1 and posting_date <= %(to_date)s - {company_condition} order by posting_date'''.format(company_condition=company_condition), - filters, as_dict=1): + {company_condition} order by posting_date""".format( + company_condition=company_condition + ), + filters, + as_dict=1, + ): - key = si.territory if tree_view else si.posting_date.strftime('%Y-%m') - new_or_repeat = 'new' if si.customer not in customers else 'repeat' - customers_in.setdefault(key, {'new': [0, 0.0], 'repeat': [0, 0.0]}) + key = si.territory if tree_view else si.posting_date.strftime("%Y-%m") + new_or_repeat = "new" if si.customer not in customers else "repeat" + customers_in.setdefault(key, {"new": [0, 0.0], "repeat": [0, 0.0]}) # if filters.from_date <= si.posting_date.strftime('%Y-%m-%d'): if getdate(filters.from_date) <= getdate(si.posting_date): - customers_in[key][new_or_repeat][0] += 1 - customers_in[key][new_or_repeat][1] += si.base_grand_total - if new_or_repeat == 'new': + customers_in[key][new_or_repeat][0] += 1 + customers_in[key][new_or_repeat][1] += si.base_grand_total + if new_or_repeat == "new": customers.append(si.customer) return customers_in diff --git a/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py b/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py index dd49f1355d..1c10a374b6 100644 --- a/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py +++ b/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py @@ -10,8 +10,9 @@ from erpnext.selling.doctype.customer.customer import get_credit_limit, get_cust def execute(filters=None): - if not filters: filters = {} - #Check if customer id is according to naming series or customer name + if not filters: + filters = {} + # Check if customer id is according to naming series or customer name customer_naming_type = frappe.db.get_value("Selling Settings", None, "cust_master_name") columns = get_columns(customer_naming_type) @@ -22,8 +23,9 @@ def execute(filters=None): for d in customer_list: row = [] - outstanding_amt = get_customer_outstanding(d.name, filters.get("company"), - ignore_outstanding_sales_order=d.bypass_credit_limit_check) + outstanding_amt = get_customer_outstanding( + d.name, filters.get("company"), ignore_outstanding_sales_order=d.bypass_credit_limit_check + ) credit_limit = get_credit_limit(d.name, filters.get("company")) @@ -31,15 +33,24 @@ def execute(filters=None): if customer_naming_type == "Naming Series": row = [ - d.name, d.customer_name, credit_limit, - outstanding_amt, bal, d.bypass_credit_limit_check, - d.is_frozen, d.disabled + d.name, + d.customer_name, + credit_limit, + outstanding_amt, + bal, + d.bypass_credit_limit_check, + d.is_frozen, + d.disabled, ] else: row = [ - d.name, credit_limit, outstanding_amt, bal, - d.bypass_credit_limit_check, d.is_frozen, - d.disabled + d.name, + credit_limit, + outstanding_amt, + bal, + d.bypass_credit_limit_check, + d.is_frozen, + d.disabled, ] if credit_limit: @@ -47,6 +58,7 @@ def execute(filters=None): return columns, data + def get_columns(customer_naming_type): columns = [ _("Customer") + ":Link/Customer:120", @@ -63,6 +75,7 @@ def get_columns(customer_naming_type): return columns + def get_details(filters): sql_query = """SELECT diff --git a/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py b/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py index e5f9354320..a58f40362b 100644 --- a/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py +++ b/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py @@ -30,32 +30,23 @@ def get_columns(filters=None): "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 150 - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 200 - }, - { - "label": _("Selling Rate"), - "fieldname": "selling_rate", - "fieldtype": "Currency" + "width": 150, }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 200}, + {"label": _("Selling Rate"), "fieldname": "selling_rate", "fieldtype": "Currency"}, { "label": _("Available Stock"), "fieldname": "available_stock", "fieldtype": "Float", - "width": 150 + "width": 150, }, { "label": _("Price List"), "fieldname": "price_list", "fieldtype": "Link", "options": "Price List", - "width": 120 - } + "width": 120, + }, ] @@ -64,30 +55,33 @@ def get_data(filters=None): customer_details = get_customer_details(filters) items = get_selling_items(filters) - item_stock_map = frappe.get_all("Bin", fields=["item_code", "sum(actual_qty) AS available"], group_by="item_code") + item_stock_map = frappe.get_all( + "Bin", fields=["item_code", "sum(actual_qty) AS available"], group_by="item_code" + ) item_stock_map = {item.item_code: item.available for item in item_stock_map} for item in items: price_list_rate = get_price_list_rate_for(customer_details, item.item_code) or 0.0 available_stock = item_stock_map.get(item.item_code) - data.append({ - "item_code": item.item_code, - "item_name": item.item_name, - "selling_rate": price_list_rate, - "price_list": customer_details.get("price_list"), - "available_stock": available_stock, - }) + data.append( + { + "item_code": item.item_code, + "item_name": item.item_name, + "selling_rate": price_list_rate, + "price_list": customer_details.get("price_list"), + "available_stock": available_stock, + } + ) return data def get_customer_details(filters): customer_details = get_party_details(party=filters.get("customer"), party_type="Customer") - customer_details.update({ - "company": get_default_company(), - "price_list": customer_details.get("selling_price_list") - }) + customer_details.update( + {"company": get_default_company(), "price_list": customer_details.get("selling_price_list")} + ) return customer_details @@ -98,6 +92,8 @@ def get_selling_items(filters): else: item_filters = {"is_sales_item": 1, "disabled": 0} - items = frappe.get_all("Item", filters=item_filters, fields=["item_code", "item_name"], order_by="item_name") + items = frappe.get_all( + "Item", filters=item_filters, fields=["item_code", "item_name"], order_by="item_name" + ) return items diff --git a/erpnext/selling/report/inactive_customers/inactive_customers.py b/erpnext/selling/report/inactive_customers/inactive_customers.py index d97e1c6dcb..1b337fc495 100644 --- a/erpnext/selling/report/inactive_customers/inactive_customers.py +++ b/erpnext/selling/report/inactive_customers/inactive_customers.py @@ -8,7 +8,8 @@ from frappe.utils import cint def execute(filters=None): - if not filters: filters ={} + if not filters: + filters = {} days_since_last_order = filters.get("days_since_last_order") doctype = filters.get("doctype") @@ -22,10 +23,11 @@ def execute(filters=None): data = [] for cust in customers: if cint(cust[8]) >= cint(days_since_last_order): - cust.insert(7,get_last_sales_amt(cust[0], doctype)) + cust.insert(7, get_last_sales_amt(cust[0], doctype)) data.append(cust) return columns, data + def get_sales_details(doctype): cond = """sum(so.base_net_total) as 'total_order_considered', max(so.posting_date) as 'last_order_date', @@ -37,7 +39,8 @@ def get_sales_details(doctype): max(so.transaction_date) as 'last_order_date', DATEDIFF(CURDATE(), max(so.transaction_date)) as 'days_since_last_order'""" - return frappe.db.sql("""select + return frappe.db.sql( + """select cust.name, cust.customer_name, cust.territory, @@ -47,18 +50,29 @@ def get_sales_details(doctype): from `tabCustomer` cust, `tab{1}` so where cust.name = so.customer and so.docstatus = 1 group by cust.name - order by 'days_since_last_order' desc """.format(cond, doctype), as_list=1) + order by 'days_since_last_order' desc """.format( + cond, doctype + ), + as_list=1, + ) + def get_last_sales_amt(customer, doctype): cond = "posting_date" - if doctype =="Sales Order": + if doctype == "Sales Order": cond = "transaction_date" - res = frappe.db.sql("""select base_net_total from `tab{0}` + res = frappe.db.sql( + """select base_net_total from `tab{0}` where customer = %s and docstatus = 1 order by {1} desc - limit 1""".format(doctype, cond), customer) + limit 1""".format( + doctype, cond + ), + customer, + ) return res and res[0][0] or 0 + def get_columns(): return [ _("Customer") + ":Link/Customer:120", @@ -70,5 +84,5 @@ def get_columns(): _("Total Order Considered") + ":Currency:160", _("Last Order Amount") + ":Currency:160", _("Last Order Date") + ":Date:160", - _("Days Since Last Order") + "::160" + _("Days Since Last Order") + "::160", ] diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py index 56e1eb57b8..da0098409b 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py @@ -20,6 +20,7 @@ def execute(filters=None): return columns, data, None, chart_data + def get_columns(filters): return [ { @@ -27,120 +28,85 @@ def get_columns(filters): "fieldtype": "Link", "fieldname": "item_code", "options": "Item", - "width": 120 - }, - { - "label": _("Item Name"), - "fieldtype": "Data", - "fieldname": "item_name", - "width": 140 + "width": 120, }, + {"label": _("Item Name"), "fieldtype": "Data", "fieldname": "item_name", "width": 140}, { "label": _("Item Group"), "fieldtype": "Link", "fieldname": "item_group", "options": "Item Group", - "width": 120 - }, - { - "label": _("Description"), - "fieldtype": "Data", - "fieldname": "description", - "width": 150 - }, - { - "label": _("Quantity"), - "fieldtype": "Float", - "fieldname": "quantity", - "width": 150 - }, - { - "label": _("UOM"), - "fieldtype": "Link", - "fieldname": "uom", - "options": "UOM", - "width": 100 - }, - { - "label": _("Rate"), - "fieldname": "rate", - "options": "Currency", - "width": 120 - }, - { - "label": _("Amount"), - "fieldname": "amount", - "options": "Currency", - "width": 120 + "width": 120, }, + {"label": _("Description"), "fieldtype": "Data", "fieldname": "description", "width": 150}, + {"label": _("Quantity"), "fieldtype": "Float", "fieldname": "quantity", "width": 150}, + {"label": _("UOM"), "fieldtype": "Link", "fieldname": "uom", "options": "UOM", "width": 100}, + {"label": _("Rate"), "fieldname": "rate", "options": "Currency", "width": 120}, + {"label": _("Amount"), "fieldname": "amount", "options": "Currency", "width": 120}, { "label": _("Sales Order"), "fieldtype": "Link", "fieldname": "sales_order", "options": "Sales Order", - "width": 100 + "width": 100, }, { "label": _("Transaction Date"), "fieldtype": "Date", "fieldname": "transaction_date", - "width": 90 + "width": 90, }, { "label": _("Customer"), "fieldtype": "Link", "fieldname": "customer", "options": "Customer", - "width": 100 - }, - { - "label": _("Customer Name"), - "fieldtype": "Data", - "fieldname": "customer_name", - "width": 140 + "width": 100, }, + {"label": _("Customer Name"), "fieldtype": "Data", "fieldname": "customer_name", "width": 140}, { "label": _("Customer Group"), "fieldtype": "Link", "fieldname": "customer_group", "options": "Customer Group", - "width": 120 + "width": 120, }, { "label": _("Territory"), "fieldtype": "Link", "fieldname": "territory", "options": "Territory", - "width": 100 + "width": 100, }, { "label": _("Project"), "fieldtype": "Link", "fieldname": "project", "options": "Project", - "width": 100 + "width": 100, }, { "label": _("Delivered Quantity"), "fieldtype": "Float", "fieldname": "delivered_quantity", - "width": 150 + "width": 150, }, { "label": _("Billed Amount"), "fieldtype": "currency", "fieldname": "billed_amount", - "width": 120 + "width": 120, }, { "label": _("Company"), "fieldtype": "Link", "fieldname": "company", "options": "Company", - "width": 100 - } + "width": 100, + }, ] + def get_data(filters): data = [] @@ -156,74 +122,75 @@ def get_data(filters): customer_record = customer_details.get(record.customer) item_record = item_details.get(record.item_code) row = { - "item_code": record.get('item_code'), - "item_name": item_record.get('item_name'), - "item_group": item_record.get('item_group'), - "description": record.get('description'), - "quantity": record.get('qty'), - "uom": record.get('uom'), - "rate": record.get('base_rate'), - "amount": record.get('base_amount'), - "sales_order": record.get('name'), - "transaction_date": record.get('transaction_date'), - "customer": record.get('customer'), - "customer_name": customer_record.get('customer_name'), - "customer_group": customer_record.get('customer_group'), - "territory": record.get('territory'), - "project": record.get('project'), - "delivered_quantity": flt(record.get('delivered_qty')), - "billed_amount": flt(record.get('billed_amt')), - "company": record.get('company') + "item_code": record.get("item_code"), + "item_name": item_record.get("item_name"), + "item_group": item_record.get("item_group"), + "description": record.get("description"), + "quantity": record.get("qty"), + "uom": record.get("uom"), + "rate": record.get("base_rate"), + "amount": record.get("base_amount"), + "sales_order": record.get("name"), + "transaction_date": record.get("transaction_date"), + "customer": record.get("customer"), + "customer_name": customer_record.get("customer_name"), + "customer_group": customer_record.get("customer_group"), + "territory": record.get("territory"), + "project": record.get("project"), + "delivered_quantity": flt(record.get("delivered_qty")), + "billed_amount": flt(record.get("billed_amt")), + "company": record.get("company"), } data.append(row) return data + def get_conditions(filters): - conditions = '' - if filters.get('item_group'): - conditions += "AND so_item.item_group = %s" %frappe.db.escape(filters.item_group) + conditions = "" + if filters.get("item_group"): + conditions += "AND so_item.item_group = %s" % frappe.db.escape(filters.item_group) - if filters.get('from_date'): - conditions += "AND so.transaction_date >= '%s'" %filters.from_date + if filters.get("from_date"): + conditions += "AND so.transaction_date >= '%s'" % filters.from_date - if filters.get('to_date'): - conditions += "AND so.transaction_date <= '%s'" %filters.to_date + if filters.get("to_date"): + conditions += "AND so.transaction_date <= '%s'" % filters.to_date if filters.get("item_code"): - conditions += "AND so_item.item_code = %s" %frappe.db.escape(filters.item_code) + conditions += "AND so_item.item_code = %s" % frappe.db.escape(filters.item_code) if filters.get("customer"): - conditions += "AND so.customer = %s" %frappe.db.escape(filters.customer) + conditions += "AND so.customer = %s" % frappe.db.escape(filters.customer) return conditions + def get_customer_details(): - details = frappe.get_all("Customer", - fields=["name", "customer_name", "customer_group"]) + details = frappe.get_all("Customer", fields=["name", "customer_name", "customer_group"]) customer_details = {} for d in details: - customer_details.setdefault(d.name, frappe._dict({ - "customer_name": d.customer_name, - "customer_group": d.customer_group - })) + customer_details.setdefault( + d.name, frappe._dict({"customer_name": d.customer_name, "customer_group": d.customer_group}) + ) return customer_details + def get_item_details(): - details = frappe.db.get_all("Item", - fields=["item_code", "item_name", "item_group"]) + details = frappe.db.get_all("Item", fields=["item_code", "item_name", "item_group"]) item_details = {} for d in details: - item_details.setdefault(d.item_code, frappe._dict({ - "item_name": d.item_name, - "item_group": d.item_group - })) + item_details.setdefault( + d.item_code, frappe._dict({"item_name": d.item_name, "item_group": d.item_group}) + ) return item_details + def get_sales_order_details(company_list, filters): conditions = get_conditions(filters) - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT so_item.item_code, so_item.description, so_item.qty, so_item.uom, so_item.base_rate, so_item.base_amount, @@ -236,7 +203,13 @@ def get_sales_order_details(company_list, filters): so.name = so_item.parent AND so.company in ({0}) AND so.docstatus = 1 {1} - """.format(','.join(["%s"] * len(company_list)), conditions), tuple(company_list), as_dict=1) + """.format( + ",".join(["%s"] * len(company_list)), conditions + ), + tuple(company_list), + as_dict=1, + ) + def get_chart_data(data): item_wise_sales_map = {} @@ -250,21 +223,19 @@ def get_chart_data(data): item_wise_sales_map[item_key] = flt(item_wise_sales_map[item_key]) + flt(row.get("amount")) - item_wise_sales_map = { item: value for item, value in (sorted(item_wise_sales_map.items(), key = lambda i: i[1], reverse=True))} + item_wise_sales_map = { + item: value + for item, value in (sorted(item_wise_sales_map.items(), key=lambda i: i[1], reverse=True)) + } for key in item_wise_sales_map: labels.append(key) datapoints.append(item_wise_sales_map[key]) return { - "data" : { - "labels" : labels[:30], # show max of 30 items in chart - "datasets" : [ - { - "name" : _(" Total Sales Amount"), - "values" : datapoints[:30] - } - ] + "data": { + "labels": labels[:30], # show max of 30 items in chart + "datasets": [{"name": _(" Total Sales Amount"), "values": datapoints[:30]}], }, - "type" : "bar" + "type": "bar", } diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index e6a56eea31..7f797f67ee 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -62,12 +62,7 @@ def get_columns(): "fieldname": "status", "fieldtype": "Data", }, - { - "label": _("Currency"), - "fieldname": "currency", - "fieldtype": "Currency", - "hidden": 1 - } + {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "hidden": 1}, ] return columns @@ -156,7 +151,7 @@ def set_payment_terms_statuses(sales_orders, invoices, filters): """ for so in sales_orders: - so.currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency') + so.currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency") so.invoices = "" for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]: if so.base_payment_amount - so.paid_amount > 0: @@ -182,8 +177,14 @@ def prepare_chart(s_orders): "data": { "labels": [term.payment_term for term in s_orders], "datasets": [ - {"name": "Payment Amount", "values": [x.base_payment_amount for x in s_orders],}, - {"name": "Paid Amount", "values": [x.paid_amount for x in s_orders],}, + { + "name": "Payment Amount", + "values": [x.base_payment_amount for x in s_orders], + }, + { + "name": "Paid Amount", + "values": [x.paid_amount for x in s_orders], + }, ], }, "type": "bar", diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index f7f8a5dbce..89940a6e87 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -94,7 +94,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): "currency": "INR", "base_payment_amount": 500000.0, "paid_amount": 500000.0, - "invoices": ","+sinv.name, + "invoices": "," + sinv.name, }, { "name": so.name, @@ -107,25 +107,29 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): "currency": "INR", "base_payment_amount": 500000.0, "paid_amount": 100000.0, - "invoices": ","+sinv.name, + "invoices": "," + sinv.name, }, ] self.assertEqual(data, expected_value) def create_exchange_rate(self, date): # make an entry in Currency Exchange list. serves as a static exchange rate - if frappe.db.exists({'doctype': "Currency Exchange",'date': date,'from_currency': 'USD', 'to_currency':'INR'}): + if frappe.db.exists( + {"doctype": "Currency Exchange", "date": date, "from_currency": "USD", "to_currency": "INR"} + ): return else: - doc = frappe.get_doc({ - 'doctype': "Currency Exchange", - 'date': date, - 'from_currency': 'USD', - 'to_currency': frappe.get_cached_value("Company", '_Test Company','default_currency'), - 'exchange_rate': 70, - 'for_buying': True, - 'for_selling': True - }) + doc = frappe.get_doc( + { + "doctype": "Currency Exchange", + "date": date, + "from_currency": "USD", + "to_currency": frappe.get_cached_value("Company", "_Test Company", "default_currency"), + "exchange_rate": 70, + "for_buying": True, + "for_selling": True, + } + ) doc.insert() def test_alternate_currency(self): @@ -176,10 +180,10 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): "description": "_Test 50-50", "due_date": datetime.date(2021, 6, 30), "invoice_portion": 50.0, - "currency": frappe.get_cached_value("Company", '_Test Company','default_currency'), + "currency": frappe.get_cached_value("Company", "_Test Company", "default_currency"), "base_payment_amount": 3500000.0, "paid_amount": 3500000.0, - "invoices": ","+sinv.name, + "invoices": "," + sinv.name, }, { "name": so.name, @@ -189,10 +193,10 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): "description": "_Test 50-50", "due_date": datetime.date(2021, 7, 15), "invoice_portion": 50.0, - "currency": frappe.get_cached_value("Company", '_Test Company','default_currency'), + "currency": frappe.get_cached_value("Company", "_Test Company", "default_currency"), "base_payment_amount": 3500000.0, "paid_amount": 700000.0, - "invoices": ","+sinv.name, + "invoices": "," + sinv.name, }, ] self.assertEqual(data, expected_value) diff --git a/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py b/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py index 01421e8fd0..cc1055c787 100644 --- a/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py +++ b/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py @@ -12,6 +12,7 @@ def execute(filters=None): data = get_data() return columns, data + def get_columns(): columns = [ { @@ -19,80 +20,37 @@ def get_columns(): "options": "Item", "fieldname": "item_code", "fieldtype": "Link", - "width": 200 - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 200 - }, - { - "label": _("Description"), - "fieldname": "description", - "fieldtype": "Data", - "width": 140 + "width": 200, }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 200}, + {"label": _("Description"), "fieldname": "description", "fieldtype": "Data", "width": 140}, { "label": _("S.O. No."), "options": "Sales Order", "fieldname": "sales_order_no", "fieldtype": "Link", - "width": 140 - }, - { - "label": _("Date"), - "fieldname": "date", - "fieldtype": "Date", - "width": 140 + "width": 140, }, + {"label": _("Date"), "fieldname": "date", "fieldtype": "Date", "width": 140}, { "label": _("Material Request"), "fieldname": "material_request", "fieldtype": "Data", - "width": 140 + "width": 140, }, - { - "label": _("Customer"), - "fieldname": "customer", - "fieldtype": "Data", - "width": 140 - }, - { - "label": _("Territory"), - "fieldname": "territory", - "fieldtype": "Data", - "width": 140 - }, - { - "label": _("SO Qty"), - "fieldname": "so_qty", - "fieldtype": "Float", - "width": 140 - }, - { - "label": _("Requested Qty"), - "fieldname": "requested_qty", - "fieldtype": "Float", - "width": 140 - }, - { - "label": _("Pending Qty"), - "fieldname": "pending_qty", - "fieldtype": "Float", - "width": 140 - }, - { - "label": _("Company"), - "fieldname": "company", - "fieldtype": "Data", - "width": 140 - } + {"label": _("Customer"), "fieldname": "customer", "fieldtype": "Data", "width": 140}, + {"label": _("Territory"), "fieldname": "territory", "fieldtype": "Data", "width": 140}, + {"label": _("SO Qty"), "fieldname": "so_qty", "fieldtype": "Float", "width": 140}, + {"label": _("Requested Qty"), "fieldname": "requested_qty", "fieldtype": "Float", "width": 140}, + {"label": _("Pending Qty"), "fieldname": "pending_qty", "fieldtype": "Float", "width": 140}, + {"label": _("Company"), "fieldname": "company", "fieldtype": "Data", "width": 140}, ] return columns + def get_data(): - sales_order_entry = frappe.db.sql(""" + sales_order_entry = frappe.db.sql( + """ SELECT so_item.item_code, so_item.item_name, @@ -110,88 +68,94 @@ def get_data(): and so.status not in ("Closed","Completed","Cancelled") GROUP BY so.name,so_item.item_code - """, as_dict = 1) + """, + as_dict=1, + ) sales_orders = [row.name for row in sales_order_entry] - mr_records = frappe.get_all("Material Request Item", + mr_records = frappe.get_all( + "Material Request Item", {"sales_order": ("in", sales_orders), "docstatus": 1}, - ["parent", "qty", "sales_order", "item_code"]) + ["parent", "qty", "sales_order", "item_code"], + ) bundled_item_map = get_packed_items(sales_orders) - item_with_product_bundle = get_items_with_product_bundle([row.item_code for row in sales_order_entry]) + item_with_product_bundle = get_items_with_product_bundle( + [row.item_code for row in sales_order_entry] + ) materials_request_dict = {} for record in mr_records: key = (record.sales_order, record.item_code) if key not in materials_request_dict: - materials_request_dict.setdefault(key, { - 'qty': 0, - 'material_requests': [record.parent] - }) + materials_request_dict.setdefault(key, {"qty": 0, "material_requests": [record.parent]}) details = materials_request_dict.get(key) - details['qty'] += record.qty + details["qty"] += record.qty - if record.parent not in details.get('material_requests'): - details['material_requests'].append(record.parent) + if record.parent not in details.get("material_requests"): + details["material_requests"].append(record.parent) pending_so = [] for so in sales_order_entry: if so.item_code not in item_with_product_bundle: material_requests_against_so = materials_request_dict.get((so.name, so.item_code)) or {} # check for pending sales order - if flt(so.total_qty) > flt(material_requests_against_so.get('qty')): + if flt(so.total_qty) > flt(material_requests_against_so.get("qty")): so_record = { "item_code": so.item_code, "item_name": so.item_name, "description": so.description, "sales_order_no": so.name, "date": so.transaction_date, - "material_request": ','.join(material_requests_against_so.get('material_requests', [])), + "material_request": ",".join(material_requests_against_so.get("material_requests", [])), "customer": so.customer, "territory": so.territory, "so_qty": so.total_qty, - "requested_qty": material_requests_against_so.get('qty'), - "pending_qty": so.total_qty - flt(material_requests_against_so.get('qty')), - "company": so.company + "requested_qty": material_requests_against_so.get("qty"), + "pending_qty": so.total_qty - flt(material_requests_against_so.get("qty")), + "company": so.company, } pending_so.append(so_record) else: for item in bundled_item_map.get((so.name, so.item_code), []): material_requests_against_so = materials_request_dict.get((so.name, item.item_code)) or {} - if flt(item.qty) > flt(material_requests_against_so.get('qty')): + if flt(item.qty) > flt(material_requests_against_so.get("qty")): so_record = { "item_code": item.item_code, "item_name": item.item_name, "description": item.description, "sales_order_no": so.name, "date": so.transaction_date, - "material_request": ','.join(material_requests_against_so.get('material_requests', [])), + "material_request": ",".join(material_requests_against_so.get("material_requests", [])), "customer": so.customer, "territory": so.territory, "so_qty": item.qty, - "requested_qty": material_requests_against_so.get('qty', 0), - "pending_qty": item.qty - flt(material_requests_against_so.get('qty', 0)), - "company": so.company + "requested_qty": material_requests_against_so.get("qty", 0), + "pending_qty": item.qty - flt(material_requests_against_so.get("qty", 0)), + "company": so.company, } pending_so.append(so_record) - return pending_so + def get_items_with_product_bundle(item_list): - bundled_items = frappe.get_all("Product Bundle", filters = [ - ("new_item_code", "IN", item_list) - ], fields = ["new_item_code"]) + bundled_items = frappe.get_all( + "Product Bundle", filters=[("new_item_code", "IN", item_list)], fields=["new_item_code"] + ) return [d.new_item_code for d in bundled_items] + def get_packed_items(sales_order_list): - packed_items = frappe.get_all("Packed Item", filters = [ - ("parent", "IN", sales_order_list) - ], fields = ["parent_item", "item_code", "qty", "item_name", "description", "parent"]) + packed_items = frappe.get_all( + "Packed Item", + filters=[("parent", "IN", sales_order_list)], + fields=["parent_item", "item_code", "qty", "item_name", "description", "parent"], + ) bundled_item_map = frappe._dict() for d in packed_items: diff --git a/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py b/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py index 16162acc8f..e4ad5c622b 100644 --- a/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py +++ b/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py @@ -13,18 +13,18 @@ from erpnext.selling.report.pending_so_items_for_purchase_request.pending_so_ite class TestPendingSOItemsForPurchaseRequest(FrappeTestCase): - def test_result_for_partial_material_request(self): - so = make_sales_order() - mr=make_material_request(so.name) - mr.items[0].qty = 4 - mr.schedule_date = add_months(nowdate(),1) - mr.submit() - report = execute() - l = len(report[1]) - self.assertEqual((so.items[0].qty - mr.items[0].qty), report[1][l-1]['pending_qty']) + def test_result_for_partial_material_request(self): + so = make_sales_order() + mr = make_material_request(so.name) + mr.items[0].qty = 4 + mr.schedule_date = add_months(nowdate(), 1) + mr.submit() + report = execute() + l = len(report[1]) + self.assertEqual((so.items[0].qty - mr.items[0].qty), report[1][l - 1]["pending_qty"]) - def test_result_for_so_item(self): - so = make_sales_order() - report = execute() - l = len(report[1]) - self.assertEqual(so.items[0].qty, report[1][l-1]['pending_qty']) + def test_result_for_so_item(self): + so = make_sales_order() + report = execute() + l = len(report[1]) + self.assertEqual(so.items[0].qty, report[1][l - 1]["pending_qty"]) diff --git a/erpnext/selling/report/quotation_trends/quotation_trends.py b/erpnext/selling/report/quotation_trends/quotation_trends.py index 047b09081a..dfcec22cca 100644 --- a/erpnext/selling/report/quotation_trends/quotation_trends.py +++ b/erpnext/selling/report/quotation_trends/quotation_trends.py @@ -8,7 +8,8 @@ from erpnext.controllers.trends import get_columns, get_data def execute(filters=None): - if not filters: filters ={} + if not filters: + filters = {} data = [] conditions = get_columns(filters, "Quotation") data = get_data(filters, conditions) @@ -17,6 +18,7 @@ def execute(filters=None): return conditions["columns"], data, None, chart_data + def get_chart_data(data, conditions, filters): if not (data and conditions): return [] @@ -29,32 +31,27 @@ def get_chart_data(data, conditions, filters): # fetch only periodic columns as labels columns = conditions.get("columns")[start:-2][1::2] - labels = [column.split(':')[0] for column in columns] + labels = [column.split(":")[0] for column in columns] datapoints = [0] * len(labels) for row in data: # If group by filter, don't add first row of group (it's already summed) - if not row[start-1]: + if not row[start - 1]: continue # Remove None values and compute only periodic data row = [x if x else 0 for x in row[start:-2]] - row = row[1::2] + row = row[1::2] for i in range(len(row)): datapoints[i] += row[i] return { - "data" : { - "labels" : labels, - "datasets" : [ - { - "name" : _("{0}").format(filters.get("period")) + _(" Quoted Amount"), - "values" : datapoints - } - ] + "data": { + "labels": labels, + "datasets": [ + {"name": _("{0}").format(filters.get("period")) + _(" Quoted Amount"), "values": datapoints} + ], }, - "type" : "line", - "lineOptions": { - "regionFill": 1 - } + "type": "line", + "lineOptions": {"regionFill": 1}, } diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index 83588c3456..1a2476a9da 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -12,12 +12,29 @@ from erpnext.accounts.utils import get_fiscal_year def execute(filters=None): return Analytics(filters).run() + class Analytics(object): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) - self.date_field = 'transaction_date' \ - if self.filters.doc_type in ['Sales Order', 'Purchase Order'] else 'posting_date' - self.months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + self.date_field = ( + "transaction_date" + if self.filters.doc_type in ["Sales Order", "Purchase Order"] + else "posting_date" + ) + self.months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ] self.get_period_date_ranges() def run(self): @@ -34,52 +51,52 @@ class Analytics(object): return self.columns, self.data, None, self.chart, None, skip_total_row def get_columns(self): - self.columns = [{ + self.columns = [ + { "label": _(self.filters.tree_type), "options": self.filters.tree_type if self.filters.tree_type != "Order Type" else "", "fieldname": "entity", "fieldtype": "Link" if self.filters.tree_type != "Order Type" else "Data", - "width": 140 if self.filters.tree_type != "Order Type" else 200 - }] + "width": 140 if self.filters.tree_type != "Order Type" else 200, + } + ] if self.filters.tree_type in ["Customer", "Supplier", "Item"]: - self.columns.append({ - "label": _(self.filters.tree_type + " Name"), - "fieldname": "entity_name", - "fieldtype": "Data", - "width": 140 - }) + self.columns.append( + { + "label": _(self.filters.tree_type + " Name"), + "fieldname": "entity_name", + "fieldtype": "Data", + "width": 140, + } + ) if self.filters.tree_type == "Item": - self.columns.append({ - "label": _("UOM"), - "fieldname": 'stock_uom', - "fieldtype": "Link", - "options": "UOM", - "width": 100 - }) + self.columns.append( + { + "label": _("UOM"), + "fieldname": "stock_uom", + "fieldtype": "Link", + "options": "UOM", + "width": 100, + } + ) for end_date in self.periodic_daterange: period = self.get_period(end_date) - self.columns.append({ - "label": _(period), - "fieldname": scrub(period), - "fieldtype": "Float", - "width": 120 - }) + self.columns.append( + {"label": _(period), "fieldname": scrub(period), "fieldtype": "Float", "width": 120} + ) - self.columns.append({ - "label": _("Total"), - "fieldname": "total", - "fieldtype": "Float", - "width": 120 - }) + self.columns.append( + {"label": _("Total"), "fieldname": "total", "fieldtype": "Float", "width": 120} + ) def get_data(self): if self.filters.tree_type in ["Customer", "Supplier"]: self.get_sales_transactions_based_on_customers_or_suppliers() self.get_rows() - elif self.filters.tree_type == 'Item': + elif self.filters.tree_type == "Item": self.get_sales_transactions_based_on_items() self.get_rows() @@ -87,7 +104,7 @@ class Analytics(object): self.get_sales_transactions_based_on_customer_or_territory_group() self.get_rows_by_group() - elif self.filters.tree_type == 'Item Group': + elif self.filters.tree_type == "Item Group": self.get_sales_transactions_based_on_item_group() self.get_rows_by_group() @@ -103,40 +120,45 @@ class Analytics(object): self.get_rows() def get_sales_transactions_based_on_order_type(self): - if self.filters["value_quantity"] == 'Value': + if self.filters["value_quantity"] == "Value": value_field = "base_net_total" else: value_field = "total_qty" - self.entries = frappe.db.sql(""" select s.order_type as entity, s.{value_field} as value_field, s.{date_field} + self.entries = frappe.db.sql( + """ select s.order_type as entity, s.{value_field} as value_field, s.{date_field} from `tab{doctype}` s where s.docstatus = 1 and s.company = %s and s.{date_field} between %s and %s and ifnull(s.order_type, '') != '' order by s.order_type - """ - .format(date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type), - (self.filters.company, self.filters.from_date, self.filters.to_date), as_dict=1) + """.format( + date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type + ), + (self.filters.company, self.filters.from_date, self.filters.to_date), + as_dict=1, + ) self.get_teams() def get_sales_transactions_based_on_customers_or_suppliers(self): - if self.filters["value_quantity"] == 'Value': + if self.filters["value_quantity"] == "Value": value_field = "base_net_total as value_field" else: value_field = "total_qty as value_field" - if self.filters.tree_type == 'Customer': + if self.filters.tree_type == "Customer": entity = "customer as entity" entity_name = "customer_name as entity_name" else: entity = "supplier as entity" entity_name = "supplier_name as entity_name" - self.entries = frappe.get_all(self.filters.doc_type, + self.entries = frappe.get_all( + self.filters.doc_type, fields=[entity, entity_name, value_field, self.date_field], filters={ "docstatus": 1, "company": self.filters.company, - self.date_field: ('between', [self.filters.from_date, self.filters.to_date]) - } + self.date_field: ("between", [self.filters.from_date, self.filters.to_date]), + }, ) self.entity_names = {} @@ -145,80 +167,91 @@ class Analytics(object): def get_sales_transactions_based_on_items(self): - if self.filters["value_quantity"] == 'Value': - value_field = 'base_amount' + if self.filters["value_quantity"] == "Value": + value_field = "base_amount" else: - value_field = 'stock_qty' + value_field = "stock_qty" - self.entries = frappe.db.sql(""" + self.entries = frappe.db.sql( + """ select i.item_code as entity, i.item_name as entity_name, i.stock_uom, i.{value_field} as value_field, s.{date_field} from `tab{doctype} Item` i , `tab{doctype}` s where s.name = i.parent and i.docstatus = 1 and s.company = %s and s.{date_field} between %s and %s - """ - .format(date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type), - (self.filters.company, self.filters.from_date, self.filters.to_date), as_dict=1) + """.format( + date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type + ), + (self.filters.company, self.filters.from_date, self.filters.to_date), + as_dict=1, + ) self.entity_names = {} for d in self.entries: self.entity_names.setdefault(d.entity, d.entity_name) def get_sales_transactions_based_on_customer_or_territory_group(self): - if self.filters["value_quantity"] == 'Value': + if self.filters["value_quantity"] == "Value": value_field = "base_net_total as value_field" else: value_field = "total_qty as value_field" - if self.filters.tree_type == 'Customer Group': - entity_field = 'customer_group as entity' - elif self.filters.tree_type == 'Supplier Group': + if self.filters.tree_type == "Customer Group": + entity_field = "customer_group as entity" + elif self.filters.tree_type == "Supplier Group": entity_field = "supplier as entity" self.get_supplier_parent_child_map() else: entity_field = "territory as entity" - self.entries = frappe.get_all(self.filters.doc_type, + self.entries = frappe.get_all( + self.filters.doc_type, fields=[entity_field, value_field, self.date_field], filters={ "docstatus": 1, "company": self.filters.company, - self.date_field: ('between', [self.filters.from_date, self.filters.to_date]) - } + self.date_field: ("between", [self.filters.from_date, self.filters.to_date]), + }, ) self.get_groups() def get_sales_transactions_based_on_item_group(self): - if self.filters["value_quantity"] == 'Value': + if self.filters["value_quantity"] == "Value": value_field = "base_amount" else: value_field = "qty" - self.entries = frappe.db.sql(""" + self.entries = frappe.db.sql( + """ select i.item_group as entity, i.{value_field} as value_field, s.{date_field} from `tab{doctype} Item` i , `tab{doctype}` s where s.name = i.parent and i.docstatus = 1 and s.company = %s and s.{date_field} between %s and %s - """.format(date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type), - (self.filters.company, self.filters.from_date, self.filters.to_date), as_dict=1) + """.format( + date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type + ), + (self.filters.company, self.filters.from_date, self.filters.to_date), + as_dict=1, + ) self.get_groups() def get_sales_transactions_based_on_project(self): - if self.filters["value_quantity"] == 'Value': + if self.filters["value_quantity"] == "Value": value_field = "base_net_total as value_field" else: value_field = "total_qty as value_field" entity = "project as entity" - self.entries = frappe.get_all(self.filters.doc_type, + self.entries = frappe.get_all( + self.filters.doc_type, fields=[entity, value_field, self.date_field], filters={ "docstatus": 1, "company": self.filters.company, "project": ["!=", ""], - self.date_field: ('between', [self.filters.from_date, self.filters.to_date]) - } + self.date_field: ("between", [self.filters.from_date, self.filters.to_date]), + }, ) def get_rows(self): @@ -228,7 +261,7 @@ class Analytics(object): for entity, period_data in self.entity_periodic_data.items(): row = { "entity": entity, - "entity_name": self.entity_names.get(entity) if hasattr(self, 'entity_names') else None + "entity_name": self.entity_names.get(entity) if hasattr(self, "entity_names") else None, } total = 0 for end_date in self.periodic_daterange: @@ -249,10 +282,7 @@ class Analytics(object): out = [] for d in reversed(self.group_entries): - row = { - "entity": d.name, - "indent": self.depth_map.get(d.name) - } + row = {"entity": d.name, "indent": self.depth_map.get(d.name)} total = 0 for end_date in self.periodic_daterange: period = self.get_period(end_date) @@ -279,14 +309,14 @@ class Analytics(object): self.entity_periodic_data[d.entity][period] += flt(d.value_field) if self.filters.tree_type == "Item": - self.entity_periodic_data[d.entity]['stock_uom'] = d.stock_uom + self.entity_periodic_data[d.entity]["stock_uom"] = d.stock_uom def get_period(self, posting_date): - if self.filters.range == 'Weekly': + if self.filters.range == "Weekly": period = "Week " + str(posting_date.isocalendar()[1]) + " " + str(posting_date.year) - elif self.filters.range == 'Monthly': + elif self.filters.range == "Monthly": period = str(self.months[posting_date.month - 1]) + " " + str(posting_date.year) - elif self.filters.range == 'Quarterly': + elif self.filters.range == "Quarterly": period = "Quarter " + str(((posting_date.month - 1) // 3) + 1) + " " + str(posting_date.year) else: year = get_fiscal_year(posting_date, company=self.filters.company) @@ -295,16 +325,14 @@ class Analytics(object): def get_period_date_ranges(self): from dateutil.relativedelta import MO, relativedelta + 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) + increment = {"Monthly": 1, "Quarterly": 3, "Half-Yearly": 6, "Yearly": 12}.get( + self.filters.range, 1 + ) - if self.filters.range in ['Monthly', 'Quarterly']: + 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] @@ -329,19 +357,23 @@ class Analytics(object): def get_groups(self): if self.filters.tree_type == "Territory": - parent = 'parent_territory' + parent = "parent_territory" if self.filters.tree_type == "Customer Group": - parent = 'parent_customer_group' + parent = "parent_customer_group" if self.filters.tree_type == "Item Group": - parent = 'parent_item_group' + parent = "parent_item_group" if self.filters.tree_type == "Supplier Group": - parent = 'parent_supplier_group' + parent = "parent_supplier_group" self.depth_map = frappe._dict() - self.group_entries = frappe.db.sql("""select name, lft, rgt , {parent} as parent - from `tab{tree}` order by lft""" - .format(tree=self.filters.tree_type, parent=parent), as_dict=1) + self.group_entries = frappe.db.sql( + """select name, lft, rgt , {parent} as parent + from `tab{tree}` order by lft""".format( + tree=self.filters.tree_type, parent=parent + ), + as_dict=1, + ) for d in self.group_entries: if d.parent: @@ -352,11 +384,15 @@ class Analytics(object): def get_teams(self): self.depth_map = frappe._dict() - self.group_entries = frappe.db.sql(""" select * from (select "Order Types" as name, 0 as lft, + self.group_entries = frappe.db.sql( + """ select * from (select "Order Types" as name, 0 as lft, 2 as rgt, '' as parent union select distinct order_type as name, 1 as lft, 1 as rgt, "Order Types" as parent from `tab{doctype}` where ifnull(order_type, '') != '') as b order by lft, name - """ - .format(doctype=self.filters.doc_type), as_dict=1) + """.format( + doctype=self.filters.doc_type + ), + as_dict=1, + ) for d in self.group_entries: if d.parent: @@ -365,21 +401,17 @@ class Analytics(object): self.depth_map.setdefault(d.name, 0) def get_supplier_parent_child_map(self): - self.parent_child_map = frappe._dict(frappe.db.sql(""" select name, supplier_group from `tabSupplier`""")) + self.parent_child_map = frappe._dict( + frappe.db.sql(""" select name, supplier_group from `tabSupplier`""") + ) def get_chart_data(self): length = len(self.columns) if self.filters.tree_type in ["Customer", "Supplier"]: - labels = [d.get("label") for d in self.columns[2:length - 1]] + labels = [d.get("label") for d in self.columns[2 : length - 1]] elif self.filters.tree_type == "Item": - labels = [d.get("label") for d in self.columns[3:length - 1]] + labels = [d.get("label") for d in self.columns[3 : length - 1]] else: - labels = [d.get("label") for d in self.columns[1:length - 1]] - self.chart = { - "data": { - 'labels': labels, - 'datasets': [] - }, - "type": "line" - } + labels = [d.get("label") for d in self.columns[1 : length - 1]] + self.chart = {"data": {"labels": labels, "datasets": []}, "type": "line"} diff --git a/erpnext/selling/report/sales_analytics/test_analytics.py b/erpnext/selling/report/sales_analytics/test_analytics.py index 564f48fef3..15f06d9c9b 100644 --- a/erpnext/selling/report/sales_analytics/test_analytics.py +++ b/erpnext/selling/report/sales_analytics/test_analytics.py @@ -19,16 +19,15 @@ class TestAnalytics(FrappeTestCase): self.compare_result_for_customer_group() self.compare_result_for_customer_based_on_quantity() - def compare_result_for_customer(self): filters = { - 'doc_type': 'Sales Order', - 'range': 'Monthly', - 'to_date': '2018-03-31', - 'tree_type': 'Customer', - 'company': '_Test Company 2', - 'from_date': '2017-04-01', - 'value_quantity': 'Value' + "doc_type": "Sales Order", + "range": "Monthly", + "to_date": "2018-03-31", + "tree_type": "Customer", + "company": "_Test Company 2", + "from_date": "2017-04-01", + "value_quantity": "Value", } report = execute(filters) @@ -49,7 +48,7 @@ class TestAnalytics(FrappeTestCase): "jan_2018": 0.0, "feb_2018": 2000.0, "mar_2018": 0.0, - "total":2000.0 + "total": 2000.0, }, { "entity": "_Test Customer 2", @@ -66,7 +65,7 @@ class TestAnalytics(FrappeTestCase): "jan_2018": 0.0, "feb_2018": 0.0, "mar_2018": 0.0, - "total":2500.0 + "total": 2500.0, }, { "entity": "_Test Customer 3", @@ -83,21 +82,21 @@ class TestAnalytics(FrappeTestCase): "jan_2018": 0.0, "feb_2018": 0.0, "mar_2018": 0.0, - "total": 3000.0 - } + "total": 3000.0, + }, ] - result = sorted(report[1], key=lambda k: k['entity']) + result = sorted(report[1], key=lambda k: k["entity"]) self.assertEqual(expected_data, result) def compare_result_for_customer_group(self): filters = { - 'doc_type': 'Sales Order', - 'range': 'Monthly', - 'to_date': '2018-03-31', - 'tree_type': 'Customer Group', - 'company': '_Test Company 2', - 'from_date': '2017-04-01', - 'value_quantity': 'Value' + "doc_type": "Sales Order", + "range": "Monthly", + "to_date": "2018-03-31", + "tree_type": "Customer Group", + "company": "_Test Company 2", + "from_date": "2017-04-01", + "value_quantity": "Value", } report = execute(filters) @@ -117,19 +116,19 @@ class TestAnalytics(FrappeTestCase): "jan_2018": 0.0, "feb_2018": 2000.0, "mar_2018": 0.0, - "total":7500.0 + "total": 7500.0, } self.assertEqual(expected_first_row, report[1][0]) def compare_result_for_customer_based_on_quantity(self): filters = { - 'doc_type': 'Sales Order', - 'range': 'Monthly', - 'to_date': '2018-03-31', - 'tree_type': 'Customer', - 'company': '_Test Company 2', - 'from_date': '2017-04-01', - 'value_quantity': 'Quantity' + "doc_type": "Sales Order", + "range": "Monthly", + "to_date": "2018-03-31", + "tree_type": "Customer", + "company": "_Test Company 2", + "from_date": "2017-04-01", + "value_quantity": "Quantity", } report = execute(filters) @@ -150,7 +149,7 @@ class TestAnalytics(FrappeTestCase): "jan_2018": 0.0, "feb_2018": 20.0, "mar_2018": 0.0, - "total":20.0 + "total": 20.0, }, { "entity": "_Test Customer 2", @@ -167,7 +166,7 @@ class TestAnalytics(FrappeTestCase): "jan_2018": 0.0, "feb_2018": 0.0, "mar_2018": 0.0, - "total":25.0 + "total": 25.0, }, { "entity": "_Test Customer 3", @@ -184,47 +183,66 @@ class TestAnalytics(FrappeTestCase): "jan_2018": 0.0, "feb_2018": 0.0, "mar_2018": 0.0, - "total": 30.0 - } + "total": 30.0, + }, ] - result = sorted(report[1], key=lambda k: k['entity']) + result = sorted(report[1], key=lambda k: k["entity"]) self.assertEqual(expected_data, result) + def create_sales_orders(): frappe.set_user("Administrator") - make_sales_order(company="_Test Company 2", qty=10, - customer = "_Test Customer 1", - transaction_date = '2018-02-10', - warehouse = 'Finished Goods - _TC2', - currency = 'EUR') + make_sales_order( + company="_Test Company 2", + qty=10, + customer="_Test Customer 1", + transaction_date="2018-02-10", + warehouse="Finished Goods - _TC2", + currency="EUR", + ) - make_sales_order(company="_Test Company 2", - qty=10, customer = "_Test Customer 1", - transaction_date = '2018-02-15', - warehouse = 'Finished Goods - _TC2', - currency = 'EUR') + make_sales_order( + company="_Test Company 2", + qty=10, + customer="_Test Customer 1", + transaction_date="2018-02-15", + warehouse="Finished Goods - _TC2", + currency="EUR", + ) - make_sales_order(company = "_Test Company 2", - qty=10, customer = "_Test Customer 2", - transaction_date = '2017-10-10', - warehouse='Finished Goods - _TC2', - currency = 'EUR') + make_sales_order( + company="_Test Company 2", + qty=10, + customer="_Test Customer 2", + transaction_date="2017-10-10", + warehouse="Finished Goods - _TC2", + currency="EUR", + ) - make_sales_order(company="_Test Company 2", - qty=15, customer = "_Test Customer 2", - transaction_date='2017-09-23', - warehouse='Finished Goods - _TC2', - currency = 'EUR') + make_sales_order( + company="_Test Company 2", + qty=15, + customer="_Test Customer 2", + transaction_date="2017-09-23", + warehouse="Finished Goods - _TC2", + currency="EUR", + ) - make_sales_order(company="_Test Company 2", - qty=20, customer = "_Test Customer 3", - transaction_date='2017-06-15', - warehouse='Finished Goods - _TC2', - currency = 'EUR') + make_sales_order( + company="_Test Company 2", + qty=20, + customer="_Test Customer 3", + transaction_date="2017-06-15", + warehouse="Finished Goods - _TC2", + currency="EUR", + ) - make_sales_order(company="_Test Company 2", - qty=10, customer = "_Test Customer 3", - transaction_date='2017-07-10', - warehouse='Finished Goods - _TC2', - currency = 'EUR') + make_sales_order( + company="_Test Company 2", + qty=10, + customer="_Test Customer 3", + transaction_date="2017-07-10", + warehouse="Finished Goods - _TC2", + currency="EUR", + ) diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py index 3e22d0fa8c..a41051331f 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -26,6 +26,7 @@ def execute(filters=None): return columns, data, None, chart_data + def validate_filters(filters): from_date, to_date = filters.get("from_date"), filters.get("to_date") @@ -34,6 +35,7 @@ def validate_filters(filters): elif date_diff(to_date, from_date) < 0: frappe.throw(_("To Date cannot be before From Date.")) + def get_conditions(filters): conditions = "" if filters.get("from_date") and filters.get("to_date"): @@ -50,8 +52,10 @@ def get_conditions(filters): return conditions + def get_data(conditions, filters): - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT so.transaction_date as date, soi.delivery_date as delivery_date, @@ -86,10 +90,16 @@ def get_data(conditions, filters): {conditions} GROUP BY soi.name ORDER BY so.transaction_date ASC, soi.item_code ASC - """.format(conditions=conditions), filters, as_dict=1) + """.format( + conditions=conditions + ), + filters, + as_dict=1, + ) return data + def prepare_data(data, filters): completed, pending = 0, 0 @@ -119,8 +129,17 @@ def prepare_data(data, filters): so_row["delay"] = min(so_row["delay"], row["delay"]) # sum numeric columns - fields = ["qty", "delivered_qty", "pending_qty", "billed_qty", "qty_to_bill", "amount", - "delivered_qty_amount", "billed_amount", "pending_amount"] + fields = [ + "qty", + "delivered_qty", + "pending_qty", + "billed_qty", + "qty_to_bill", + "amount", + "delivered_qty_amount", + "billed_amount", + "pending_amount", + ] for field in fields: so_row[field] = flt(row[field]) + flt(so_row[field]) @@ -134,166 +153,148 @@ def prepare_data(data, filters): return data, chart_data + def prepare_chart_data(pending, completed): labels = ["Amount to Bill", "Billed Amount"] return { - "data" : { - "labels": labels, - "datasets": [ - {"values": [pending, completed]} - ] - }, - "type": 'donut', - "height": 300 + "data": {"labels": labels, "datasets": [{"values": [pending, completed]}]}, + "type": "donut", + "height": 300, } + def get_columns(filters): columns = [ - { - "label":_("Date"), - "fieldname": "date", - "fieldtype": "Date", - "width": 90 - }, + {"label": _("Date"), "fieldname": "date", "fieldtype": "Date", "width": 90}, { "label": _("Sales Order"), "fieldname": "sales_order", "fieldtype": "Link", "options": "Sales Order", - "width": 160 - }, - { - "label":_("Status"), - "fieldname": "status", - "fieldtype": "Data", - "width": 130 + "width": 160, }, + {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 130}, { "label": _("Customer"), "fieldname": "customer", "fieldtype": "Link", "options": "Customer", - "width": 130 - }] - - if not filters.get("group_by_so"): - columns.append({ - "label":_("Item Code"), - "fieldname": "item_code", - "fieldtype": "Link", - "options": "Item", - "width": 100 - }) - columns.append({ - "label":_("Description"), - "fieldname": "description", - "fieldtype": "Small Text", - "width": 100 - }) - - columns.extend([ - { - "label": _("Qty"), - "fieldname": "qty", - "fieldtype": "Float", - "width": 120, - "convertible": "qty" - }, - { - "label": _("Delivered Qty"), - "fieldname": "delivered_qty", - "fieldtype": "Float", - "width": 120, - "convertible": "qty" - }, - { - "label": _("Qty to Deliver"), - "fieldname": "pending_qty", - "fieldtype": "Float", - "width": 120, - "convertible": "qty" - }, - { - "label": _("Billed Qty"), - "fieldname": "billed_qty", - "fieldtype": "Float", - "width": 80, - "convertible": "qty" - }, - { - "label": _("Qty to Bill"), - "fieldname": "qty_to_bill", - "fieldtype": "Float", - "width": 80, - "convertible": "qty" - }, - { - "label": _("Amount"), - "fieldname": "amount", - "fieldtype": "Currency", - "width": 110, - "options": "Company:company:default_currency", - "convertible": "rate" - }, - { - "label": _("Billed Amount"), - "fieldname": "billed_amount", - "fieldtype": "Currency", - "width": 110, - "options": "Company:company:default_currency", - "convertible": "rate" - }, - { - "label": _("Pending Amount"), - "fieldname": "pending_amount", - "fieldtype": "Currency", "width": 130, - "options": "Company:company:default_currency", - "convertible": "rate" }, - { - "label": _("Amount Delivered"), - "fieldname": "delivered_qty_amount", - "fieldtype": "Currency", - "width": 100, - "options": "Company:company:default_currency", - "convertible": "rate" - }, - { - "label":_("Delivery Date"), - "fieldname": "delivery_date", - "fieldtype": "Date", - "width": 120 - }, - { - "label": _("Delay (in Days)"), - "fieldname": "delay", - "fieldtype": "Data", - "width": 100 - }, - { - "label": _("Time Taken to Deliver"), - "fieldname": "time_taken_to_deliver", - "fieldtype": "Duration", - "width": 100 - } - ]) + ] + if not filters.get("group_by_so"): - columns.append({ - "label": _("Warehouse"), - "fieldname": "warehouse", - "fieldtype": "Link", - "options": "Warehouse", - "width": 100 - }) - columns.append({ + columns.append( + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 100, + } + ) + columns.append( + {"label": _("Description"), "fieldname": "description", "fieldtype": "Small Text", "width": 100} + ) + + columns.extend( + [ + { + "label": _("Qty"), + "fieldname": "qty", + "fieldtype": "Float", + "width": 120, + "convertible": "qty", + }, + { + "label": _("Delivered Qty"), + "fieldname": "delivered_qty", + "fieldtype": "Float", + "width": 120, + "convertible": "qty", + }, + { + "label": _("Qty to Deliver"), + "fieldname": "pending_qty", + "fieldtype": "Float", + "width": 120, + "convertible": "qty", + }, + { + "label": _("Billed Qty"), + "fieldname": "billed_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Qty to Bill"), + "fieldname": "qty_to_bill", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Amount"), + "fieldname": "amount", + "fieldtype": "Currency", + "width": 110, + "options": "Company:company:default_currency", + "convertible": "rate", + }, + { + "label": _("Billed Amount"), + "fieldname": "billed_amount", + "fieldtype": "Currency", + "width": 110, + "options": "Company:company:default_currency", + "convertible": "rate", + }, + { + "label": _("Pending Amount"), + "fieldname": "pending_amount", + "fieldtype": "Currency", + "width": 130, + "options": "Company:company:default_currency", + "convertible": "rate", + }, + { + "label": _("Amount Delivered"), + "fieldname": "delivered_qty_amount", + "fieldtype": "Currency", + "width": 100, + "options": "Company:company:default_currency", + "convertible": "rate", + }, + {"label": _("Delivery Date"), "fieldname": "delivery_date", "fieldtype": "Date", "width": 120}, + {"label": _("Delay (in Days)"), "fieldname": "delay", "fieldtype": "Data", "width": 100}, + { + "label": _("Time Taken to Deliver"), + "fieldname": "time_taken_to_deliver", + "fieldtype": "Duration", + "width": 100, + }, + ] + ) + if not filters.get("group_by_so"): + columns.append( + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 100, + } + ) + columns.append( + { "label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", - "width": 100 - }) - + "width": 100, + } + ) return columns diff --git a/erpnext/selling/report/sales_order_trends/sales_order_trends.py b/erpnext/selling/report/sales_order_trends/sales_order_trends.py index 5a71171262..93707bd46d 100644 --- a/erpnext/selling/report/sales_order_trends/sales_order_trends.py +++ b/erpnext/selling/report/sales_order_trends/sales_order_trends.py @@ -8,7 +8,8 @@ from erpnext.controllers.trends import get_columns, get_data def execute(filters=None): - if not filters: filters ={} + if not filters: + filters = {} data = [] conditions = get_columns(filters, "Sales Order") data = get_data(filters, conditions) @@ -16,6 +17,7 @@ def execute(filters=None): return conditions["columns"], data, None, chart_data + def get_chart_data(data, conditions, filters): if not (data and conditions): return [] @@ -28,32 +30,27 @@ def get_chart_data(data, conditions, filters): # fetch only periodic columns as labels columns = conditions.get("columns")[start:-2][1::2] - labels = [column.split(':')[0] for column in columns] + labels = [column.split(":")[0] for column in columns] datapoints = [0] * len(labels) for row in data: # If group by filter, don't add first row of group (it's already summed) - if not row[start-1]: + if not row[start - 1]: continue # Remove None values and compute only periodic data row = [x if x else 0 for x in row[start:-2]] - row = row[1::2] + row = row[1::2] for i in range(len(row)): datapoints[i] += row[i] return { - "data" : { - "labels" : labels, - "datasets" : [ - { - "name" : _("{0}").format(filters.get("period")) + _(" Sales Value"), - "values" : datapoints - } - ] + "data": { + "labels": labels, + "datasets": [ + {"name": _("{0}").format(filters.get("period")) + _(" Sales Value"), "values": datapoints} + ], }, - "type" : "line", - "lineOptions": { - "regionFill": 1 - } + "type": "line", + "lineOptions": {"regionFill": 1}, } diff --git a/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py b/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py index b775907bd5..cf9ea219c1 100644 --- a/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py +++ b/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py @@ -7,80 +7,73 @@ from frappe import _, msgprint def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns(filters) data = get_entries(filters) return columns, data + def get_columns(filters): if not filters.get("doctype"): msgprint(_("Please select the document type first"), raise_exception=1) - columns =[ + columns = [ { "label": _(filters["doctype"]), "options": filters["doctype"], "fieldname": "name", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Customer"), "options": "Customer", "fieldname": "customer", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Territory"), "options": "Territory", "fieldname": "territory", "fieldtype": "Link", - "width": 100 - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 100 - }, - { - "label": _("Amount"), - "fieldname": "amount", - "fieldtype": "Currency", - "width": 120 + "width": 100, }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, + {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120}, { "label": _("Sales Partner"), "options": "Sales Partner", "fieldname": "sales_partner", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Commission Rate %"), "fieldname": "commission_rate", "fieldtype": "Data", - "width": 100 + "width": 100, }, { "label": _("Total Commission"), "fieldname": "total_commission", "fieldtype": "Currency", - "width": 120 - } + "width": 120, + }, ] return columns + def get_entries(filters): - date_field = ("transaction_date" if filters.get('doctype') == "Sales Order" - else "posting_date") + date_field = "transaction_date" if filters.get("doctype") == "Sales Order" else "posting_date" conditions = get_conditions(filters, date_field) - entries = frappe.db.sql(""" + entries = frappe.db.sql( + """ SELECT name, customer, territory, {0} as posting_date, base_net_total as amount, sales_partner, commission_rate, total_commission @@ -89,10 +82,16 @@ def get_entries(filters): WHERE {2} and docstatus = 1 and sales_partner is not null and sales_partner != '' order by name desc, sales_partner - """.format(date_field, filters.get('doctype'), conditions), filters, as_dict=1) + """.format( + date_field, filters.get("doctype"), conditions + ), + filters, + as_dict=1, + ) return entries + def get_conditions(filters, date_field): conditions = "1=1" diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py index a647eb4fea..f34f3e34e2 100644 --- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py @@ -14,8 +14,15 @@ from erpnext.accounts.utils import get_fiscal_year def get_data_column(filters, partner_doctype): data = [] - period_list = get_period_list(filters.fiscal_year, filters.fiscal_year, '', '', - 'Fiscal Year', filters.period, company=filters.company) + period_list = get_period_list( + filters.fiscal_year, + filters.fiscal_year, + "", + "", + "Fiscal Year", + filters.period, + company=filters.company, + ) rows = get_data(filters, period_list, partner_doctype) columns = get_columns(filters, period_list, partner_doctype) @@ -24,20 +31,19 @@ def get_data_column(filters, partner_doctype): return columns, data for key, value in rows.items(): - value.update({ - frappe.scrub(partner_doctype): key[0], - 'item_group': key[1] - }) + value.update({frappe.scrub(partner_doctype): key[0], "item_group": key[1]}) data.append(value) return columns, data + def get_data(filters, period_list, partner_doctype): sales_field = frappe.scrub(partner_doctype) sales_users_data = get_parents_data(filters, partner_doctype) - if not sales_users_data: return + if not sales_users_data: + return sales_users, item_groups = [], [] for d in sales_users_data: @@ -47,99 +53,110 @@ def get_data(filters, period_list, partner_doctype): if d.item_group not in item_groups: item_groups.append(d.item_group) - date_field = ("transaction_date" - if filters.get('doctype') == "Sales Order" else "posting_date") + date_field = "transaction_date" if filters.get("doctype") == "Sales Order" else "posting_date" actual_data = get_actual_data(filters, item_groups, sales_users, date_field, sales_field) - return prepare_data(filters, sales_users_data, - actual_data, date_field, period_list, sales_field) + return prepare_data(filters, sales_users_data, actual_data, date_field, period_list, sales_field) + def get_columns(filters, period_list, partner_doctype): fieldtype, options = "Currency", "currency" - if filters.get("target_on") == 'Quantity': + if filters.get("target_on") == "Quantity": fieldtype, options = "Float", "" - columns = [{ - "fieldname": frappe.scrub(partner_doctype), - "label": _(partner_doctype), - "fieldtype": "Link", - "options": partner_doctype, - "width": 150 - }, { - "fieldname": "item_group", - "label": _("Item Group"), - "fieldtype": "Link", - "options": "Item Group", - "width": 150 - }] + columns = [ + { + "fieldname": frappe.scrub(partner_doctype), + "label": _(partner_doctype), + "fieldtype": "Link", + "options": partner_doctype, + "width": 150, + }, + { + "fieldname": "item_group", + "label": _("Item Group"), + "fieldtype": "Link", + "options": "Item Group", + "width": 150, + }, + ] for period in period_list: - target_key = 'target_{}'.format(period.key) - variance_key = 'variance_{}'.format(period.key) + target_key = "target_{}".format(period.key) + variance_key = "variance_{}".format(period.key) - columns.extend([{ - "fieldname": target_key, - "label": _("Target ({})").format(period.label), - "fieldtype": fieldtype, - "options": options, - "width": 150 - }, { - "fieldname": period.key, - "label": _("Achieved ({})").format(period.label), - "fieldtype": fieldtype, - "options": options, - "width": 150 - }, { - "fieldname": variance_key, - "label": _("Variance ({})").format(period.label), - "fieldtype": fieldtype, - "options": options, - "width": 150 - }]) + columns.extend( + [ + { + "fieldname": target_key, + "label": _("Target ({})").format(period.label), + "fieldtype": fieldtype, + "options": options, + "width": 150, + }, + { + "fieldname": period.key, + "label": _("Achieved ({})").format(period.label), + "fieldtype": fieldtype, + "options": options, + "width": 150, + }, + { + "fieldname": variance_key, + "label": _("Variance ({})").format(period.label), + "fieldtype": fieldtype, + "options": options, + "width": 150, + }, + ] + ) - columns.extend([{ - "fieldname": "total_target", - "label": _("Total Target"), - "fieldtype": fieldtype, - "options": options, - "width": 150 - }, { - "fieldname": "total_achieved", - "label": _("Total Achieved"), - "fieldtype": fieldtype, - "options": options, - "width": 150 - }, { - "fieldname": "total_variance", - "label": _("Total Variance"), - "fieldtype": fieldtype, - "options": options, - "width": 150 - }]) + columns.extend( + [ + { + "fieldname": "total_target", + "label": _("Total Target"), + "fieldtype": fieldtype, + "options": options, + "width": 150, + }, + { + "fieldname": "total_achieved", + "label": _("Total Achieved"), + "fieldtype": fieldtype, + "options": options, + "width": 150, + }, + { + "fieldname": "total_variance", + "label": _("Total Variance"), + "fieldtype": fieldtype, + "options": options, + "width": 150, + }, + ] + ) return columns + def prepare_data(filters, sales_users_data, actual_data, date_field, period_list, sales_field): rows = {} - target_qty_amt_field = ("target_qty" - if filters.get("target_on") == 'Quantity' else "target_amount") + target_qty_amt_field = "target_qty" if filters.get("target_on") == "Quantity" else "target_amount" - qty_or_amount_field = ("stock_qty" - if filters.get("target_on") == 'Quantity' else "base_net_amount") + qty_or_amount_field = "stock_qty" if filters.get("target_on") == "Quantity" else "base_net_amount" for d in sales_users_data: key = (d.parent, d.item_group) - dist_data = get_periodwise_distribution_data(d.distribution_id, period_list, filters.get("period")) + dist_data = get_periodwise_distribution_data( + d.distribution_id, period_list, filters.get("period") + ) if key not in rows: - rows.setdefault(key,{ - 'total_target': 0, - 'total_achieved': 0, - 'total_variance': 0 - }) + rows.setdefault(key, {"total_target": 0, "total_achieved": 0, "total_variance": 0}) details = rows[key] for period in period_list: @@ -147,15 +164,19 @@ def prepare_data(filters, sales_users_data, actual_data, date_field, period_list if p_key not in details: details[p_key] = 0 - target_key = 'target_{}'.format(p_key) - variance_key = 'variance_{}'.format(p_key) + target_key = "target_{}".format(p_key) + variance_key = "variance_{}".format(p_key) details[target_key] = (d.get(target_qty_amt_field) * dist_data.get(p_key)) / 100 details[variance_key] = 0 details["total_target"] += details[target_key] for r in actual_data: - if (r.get(sales_field) == d.parent and r.item_group == d.item_group and - period.from_date <= r.get(date_field) and r.get(date_field) <= period.to_date): + if ( + r.get(sales_field) == d.parent + and r.item_group == d.item_group + and period.from_date <= r.get(date_field) + and r.get(date_field) <= period.to_date + ): details[p_key] += r.get(qty_or_amount_field, 0) details[variance_key] = details.get(p_key) - details.get(target_key) @@ -164,24 +185,28 @@ def prepare_data(filters, sales_users_data, actual_data, date_field, period_list return rows + def get_actual_data(filters, item_groups, sales_users_or_territory_data, date_field, sales_field): fiscal_year = get_fiscal_year(fiscal_year=filters.get("fiscal_year"), as_dict=1) dates = [fiscal_year.year_start_date, fiscal_year.year_end_date] select_field = "`tab{0}`.{1}".format(filters.get("doctype"), sales_field) - child_table = "`tab{0}`".format(filters.get("doctype") + ' Item') + child_table = "`tab{0}`".format(filters.get("doctype") + " Item") - if sales_field == 'sales_person': + if sales_field == "sales_person": select_field = "`tabSales Team`.sales_person" - child_table = "`tab{0}`, `tabSales Team`".format(filters.get("doctype") + ' Item') + child_table = "`tab{0}`, `tabSales Team`".format(filters.get("doctype") + " Item") cond = """`tabSales Team`.parent = `tab{0}`.name and - `tabSales Team`.sales_person in ({1}) """.format(filters.get("doctype"), - ','.join(['%s'] * len(sales_users_or_territory_data))) + `tabSales Team`.sales_person in ({1}) """.format( + filters.get("doctype"), ",".join(["%s"] * len(sales_users_or_territory_data)) + ) else: - cond = "`tab{0}`.{1} in ({2})".format(filters.get("doctype"), sales_field, - ','.join(['%s'] * len(sales_users_or_territory_data))) + cond = "`tab{0}`.{1} in ({2})".format( + filters.get("doctype"), sales_field, ",".join(["%s"] * len(sales_users_or_territory_data)) + ) - return frappe.db.sql(""" SELECT `tab{child_doc}`.item_group, + return frappe.db.sql( + """ SELECT `tab{child_doc}`.item_group, `tab{child_doc}`.stock_qty, `tab{child_doc}`.base_net_amount, {select_field}, `tab{parent_doc}`.{date_field} FROM `tab{parent_doc}`, {child_table} @@ -189,26 +214,30 @@ def get_actual_data(filters, item_groups, sales_users_or_territory_data, date_fi `tab{child_doc}`.parent = `tab{parent_doc}`.name and `tab{parent_doc}`.docstatus = 1 and {cond} and `tab{child_doc}`.item_group in ({item_groups}) - and `tab{parent_doc}`.{date_field} between %s and %s""" - .format( - cond = cond, - date_field = date_field, - select_field = select_field, - child_table = child_table, - parent_doc = filters.get("doctype"), - child_doc = filters.get("doctype") + ' Item', - item_groups = ','.join(['%s'] * len(item_groups)) - ), tuple(sales_users_or_territory_data + item_groups + dates), as_dict=1) + and `tab{parent_doc}`.{date_field} between %s and %s""".format( + cond=cond, + date_field=date_field, + select_field=select_field, + child_table=child_table, + parent_doc=filters.get("doctype"), + child_doc=filters.get("doctype") + " Item", + item_groups=",".join(["%s"] * len(item_groups)), + ), + tuple(sales_users_or_territory_data + item_groups + dates), + as_dict=1, + ) + def get_parents_data(filters, partner_doctype): - filters_dict = {'parenttype': partner_doctype} + filters_dict = {"parenttype": partner_doctype} - target_qty_amt_field = ("target_qty" - if filters.get("target_on") == 'Quantity' else "target_amount") + target_qty_amt_field = "target_qty" if filters.get("target_on") == "Quantity" else "target_amount" if filters.get("fiscal_year"): filters_dict["fiscal_year"] = filters.get("fiscal_year") - return frappe.get_all('Target Detail', - filters = filters_dict, - fields = ["parent", "item_group", target_qty_amt_field, "fiscal_year", "distribution_id"]) + return frappe.get_all( + "Target Detail", + filters=filters_dict, + fields=["parent", "item_group", target_qty_amt_field, "fiscal_year", "distribution_id"], + ) diff --git a/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.py b/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.py index c64555bf2d..2049520ead 100644 --- a/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.py +++ b/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.py @@ -7,120 +7,98 @@ from frappe import _, msgprint def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns(filters) data = get_entries(filters) return columns, data + def get_columns(filters): if not filters.get("doctype"): msgprint(_("Please select the document type first"), raise_exception=1) - columns =[ + columns = [ { "label": _(filters["doctype"]), "options": filters["doctype"], "fieldname": "name", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Customer"), "options": "Customer", "fieldname": "customer", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Territory"), "options": "Territory", "fieldname": "territory", "fieldtype": "Link", - "width": 100 - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 100 + "width": 100, }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, { "label": _("Item Code"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 100 + "width": 100, }, { "label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", - "width": 100 + "width": 100, }, { "label": _("Brand"), "fieldname": "brand", "fieldtype": "Link", "options": "Brand", - "width": 100 - }, - { - "label": _("Quantity"), - "fieldname": "qty", - "fieldtype": "Float", - "width": 120 - }, - { - "label": _("Rate"), - "fieldname": "rate", - "fieldtype": "Currency", - "width": 120 - }, - { - "label": _("Amount"), - "fieldname": "amount", - "fieldtype": "Currency", - "width": 120 + "width": 100, }, + {"label": _("Quantity"), "fieldname": "qty", "fieldtype": "Float", "width": 120}, + {"label": _("Rate"), "fieldname": "rate", "fieldtype": "Currency", "width": 120}, + {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120}, { "label": _("Sales Partner"), "options": "Sales Partner", "fieldname": "sales_partner", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Commission Rate %"), "fieldname": "commission_rate", "fieldtype": "Data", - "width": 100 - }, - { - "label": _("Commission"), - "fieldname": "commission", - "fieldtype": "Currency", - "width": 120 + "width": 100, }, + {"label": _("Commission"), "fieldname": "commission", "fieldtype": "Currency", "width": 120}, { "label": _("Currency"), "fieldname": "currency", "fieldtype": "Link", "options": "Currency", - "width": 120 - } + "width": 120, + }, ] return columns + def get_entries(filters): - date_field = ("transaction_date" if filters.get('doctype') == "Sales Order" - else "posting_date") + date_field = "transaction_date" if filters.get("doctype") == "Sales Order" else "posting_date" conditions = get_conditions(filters, date_field) - entries = frappe.db.sql(""" + entries = frappe.db.sql( + """ SELECT dt.name, dt.customer, dt.territory, dt.{date_field} as posting_date, dt.currency, dt_item.base_net_rate as rate, dt_item.qty, dt_item.base_net_amount as amount, @@ -132,11 +110,16 @@ def get_entries(filters): {cond} and dt.name = dt_item.parent and dt.docstatus = 1 and dt.sales_partner is not null and dt.sales_partner != '' order by dt.name desc, dt.sales_partner - """.format(date_field=date_field, doctype=filters.get('doctype'), - cond=conditions), filters, as_dict=1) + """.format( + date_field=date_field, doctype=filters.get("doctype"), cond=conditions + ), + filters, + as_dict=1, + ) return entries + def get_conditions(filters, date_field): conditions = "1=1" @@ -150,18 +133,19 @@ def get_conditions(filters, date_field): if filters.get("to_date"): conditions += " and dt.{0} <= %(to_date)s".format(date_field) - if not filters.get('show_return_entries'): + if not filters.get("show_return_entries"): conditions += " and dt_item.qty > 0.0" - if filters.get('brand'): + if filters.get("brand"): conditions += " and dt_item.brand = %(brand)s" - if filters.get('item_group'): - lft, rgt = frappe.get_cached_value('Item Group', - filters.get('item_group'), ['lft', 'rgt']) + if filters.get("item_group"): + lft, rgt = frappe.get_cached_value("Item Group", filters.get("item_group"), ["lft", "rgt"]) conditions += """ and dt_item.item_group in (select name from - `tabItem Group` where lft >= %s and rgt <= %s)""" % (lft, rgt) - + `tabItem Group` where lft >= %s and rgt <= %s)""" % ( + lft, + rgt, + ) return conditions diff --git a/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py b/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py index 1542e31fef..a8df530803 100644 --- a/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py +++ b/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py @@ -7,102 +7,101 @@ from frappe import _, msgprint def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns(filters) entries = get_entries(filters) data = [] for d in entries: - data.append([ - d.name, d.customer, d.territory, d.posting_date, - d.base_net_amount, d.sales_person, d.allocated_percentage, d.commission_rate, d.allocated_amount,d.incentives - ]) + data.append( + [ + d.name, + d.customer, + d.territory, + d.posting_date, + d.base_net_amount, + d.sales_person, + d.allocated_percentage, + d.commission_rate, + d.allocated_amount, + d.incentives, + ] + ) if data: - total_row = [""]*len(data[0]) + total_row = [""] * len(data[0]) data.append(total_row) return columns, data + def get_columns(filters): if not filters.get("doc_type"): msgprint(_("Please select the document type first"), raise_exception=1) - columns =[ + columns = [ { "label": _(filters["doc_type"]), "options": filters["doc_type"], - "fieldname": filters['doc_type'], + "fieldname": filters["doc_type"], "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Customer"), "options": "Customer", "fieldname": "customer", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Territory"), "options": "Territory", "fieldname": "territory", "fieldtype": "Link", - "width": 100 - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 100 - }, - { - "label": _("Amount"), - "fieldname": "amount", - "fieldtype": "Currency", - "width": 120 + "width": 100, }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, + {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120}, { "label": _("Sales Person"), "options": "Sales Person", "fieldname": "sales_person", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Contribution %"), "fieldname": "contribution_percentage", "fieldtype": "Data", - "width": 110 + "width": 110, }, { "label": _("Commission Rate %"), "fieldname": "commission_rate", "fieldtype": "Data", - "width": 100 + "width": 100, }, { "label": _("Contribution Amount"), "fieldname": "contribution_amount", "fieldtype": "Currency", - "width": 120 + "width": 120, }, - { - "label": _("Incentives"), - "fieldname": "incentives", - "fieldtype": "Currency", - "width": 120 - } + {"label": _("Incentives"), "fieldname": "incentives", "fieldtype": "Currency", "width": 120}, ] return columns + def get_entries(filters): date_field = filters["doc_type"] == "Sales Order" and "transaction_date" or "posting_date" conditions, values = get_conditions(filters, date_field) - entries = frappe.db.sql(""" + entries = frappe.db.sql( + """ select dt.name, dt.customer, dt.territory, dt.%s as posting_date,dt.base_net_total as base_net_amount, st.commission_rate,st.sales_person, st.allocated_percentage, st.allocated_amount, st.incentives @@ -111,11 +110,15 @@ def get_entries(filters): where st.parent = dt.name and st.parenttype = %s and dt.docstatus = 1 %s order by dt.name desc,st.sales_person - """ %(date_field, filters["doc_type"], '%s', conditions), - tuple([filters["doc_type"]] + values), as_dict=1) + """ + % (date_field, filters["doc_type"], "%s", conditions), + tuple([filters["doc_type"]] + values), + as_dict=1, + ) return entries + def get_conditions(filters, date_field): conditions = [""] values = [] diff --git a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py index c621be8829..cb6e8a1102 100644 --- a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py +++ b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py @@ -9,7 +9,8 @@ from erpnext import get_company_currency def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns(filters) entries = get_entries(filters) @@ -19,19 +20,33 @@ def execute(filters=None): company_currency = get_company_currency(filters.get("company")) for d in entries: - if d.stock_qty > 0 or filters.get('show_return_entries', 0): - data.append([ - d.name, d.customer, d.territory, d.warehouse, d.posting_date, d.item_code, - item_details.get(d.item_code, {}).get("item_group"), item_details.get(d.item_code, {}).get("brand"), - d.stock_qty, d.base_net_amount, d.sales_person, d.allocated_percentage, d.contribution_amt, company_currency - ]) + if d.stock_qty > 0 or filters.get("show_return_entries", 0): + data.append( + [ + d.name, + d.customer, + d.territory, + d.warehouse, + d.posting_date, + d.item_code, + item_details.get(d.item_code, {}).get("item_group"), + item_details.get(d.item_code, {}).get("brand"), + d.stock_qty, + d.base_net_amount, + d.sales_person, + d.allocated_percentage, + d.contribution_amt, + company_currency, + ] + ) if data: - total_row = [""]*len(data[0]) + total_row = [""] * len(data[0]) data.append(total_row) return columns, data + def get_columns(filters): if not filters.get("doc_type"): msgprint(_("Please select the document type first"), raise_exception=1) @@ -40,102 +55,88 @@ def get_columns(filters): { "label": _(filters["doc_type"]), "options": filters["doc_type"], - "fieldname": frappe.scrub(filters['doc_type']), + "fieldname": frappe.scrub(filters["doc_type"]), "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Customer"), "options": "Customer", "fieldname": "customer", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Territory"), "options": "Territory", "fieldname": "territory", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Warehouse"), "options": "Warehouse", "fieldname": "warehouse", "fieldtype": "Link", - "width": 140 - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 140 + "width": 140, }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 140}, { "label": _("Item Code"), "options": "Item", "fieldname": "item_code", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Item Group"), "options": "Item Group", "fieldname": "item_group", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Brand"), "options": "Brand", "fieldname": "brand", "fieldtype": "Link", - "width": 140 - }, - { - "label": _("Qty"), - "fieldname": "qty", - "fieldtype": "Float", - "width": 140 + "width": 140, }, + {"label": _("Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 140}, { "label": _("Amount"), "options": "currency", "fieldname": "amount", "fieldtype": "Currency", - "width": 140 + "width": 140, }, { "label": _("Sales Person"), "options": "Sales Person", "fieldname": "sales_person", "fieldtype": "Link", - "width": 140 - }, - { - "label": _("Contribution %"), - "fieldname": "contribution", - "fieldtype": "Float", - "width": 140 + "width": 140, }, + {"label": _("Contribution %"), "fieldname": "contribution", "fieldtype": "Float", "width": 140}, { "label": _("Contribution Amount"), "options": "currency", "fieldname": "contribution_amt", "fieldtype": "Currency", - "width": 140 + "width": 140, }, { - "label":_("Currency"), + "label": _("Currency"), "options": "Currency", - "fieldname":"currency", - "fieldtype":"Link", - "hidden" : 1 - } + "fieldname": "currency", + "fieldtype": "Link", + "hidden": 1, + }, ] return columns + def get_entries(filters): date_field = filters["doc_type"] == "Sales Order" and "transaction_date" or "posting_date" if filters["doc_type"] == "Sales Order": @@ -144,7 +145,8 @@ def get_entries(filters): qty_field = "qty" conditions, values = get_conditions(filters, date_field) - entries = frappe.db.sql(""" + entries = frappe.db.sql( + """ SELECT dt.name, dt.customer, dt.territory, dt.%s as posting_date, dt_item.item_code, st.sales_person, st.allocated_percentage, dt_item.warehouse, @@ -165,11 +167,24 @@ def get_entries(filters): WHERE st.parent = dt.name and dt.name = dt_item.parent and st.parenttype = %s and dt.docstatus = 1 %s order by st.sales_person, dt.name desc - """ %(date_field, qty_field, qty_field, qty_field, filters["doc_type"], filters["doc_type"], '%s', conditions), - tuple([filters["doc_type"]] + values), as_dict=1) + """ + % ( + date_field, + qty_field, + qty_field, + qty_field, + filters["doc_type"], + filters["doc_type"], + "%s", + conditions, + ), + tuple([filters["doc_type"]] + values), + as_dict=1, + ) return entries + def get_conditions(filters, date_field): conditions = [""] values = [] @@ -181,7 +196,11 @@ def get_conditions(filters, date_field): if filters.get("sales_person"): lft, rgt = frappe.get_value("Sales Person", filters.get("sales_person"), ["lft", "rgt"]) - conditions.append("exists(select name from `tabSales Person` where lft >= {0} and rgt <= {1} and name=st.sales_person)".format(lft, rgt)) + conditions.append( + "exists(select name from `tabSales Person` where lft >= {0} and rgt <= {1} and name=st.sales_person)".format( + lft, rgt + ) + ) if filters.get("from_date"): conditions.append("dt.{0}>=%s".format(date_field)) @@ -193,23 +212,29 @@ def get_conditions(filters, date_field): items = get_items(filters) if items: - conditions.append("dt_item.item_code in (%s)" % ', '.join(['%s']*len(items))) + conditions.append("dt_item.item_code in (%s)" % ", ".join(["%s"] * len(items))) values += items return " and ".join(conditions), values + def get_items(filters): - if filters.get("item_group"): key = "item_group" - elif filters.get("brand"): key = "brand" - else: key = "" + if filters.get("item_group"): + key = "item_group" + elif filters.get("brand"): + key = "brand" + else: + key = "" items = [] if key: - items = frappe.db.sql_list("""select name from tabItem where %s = %s""" % - (key, '%s'), (filters[key])) + items = frappe.db.sql_list( + """select name from tabItem where %s = %s""" % (key, "%s"), (filters[key]) + ) return items + def get_item_details(): item_details = {} for d in frappe.db.sql("""SELECT `name`, `item_group`, `brand` FROM `tabItem`""", as_dict=1): diff --git a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py index b7b4d3aa4c..5dfc1db097 100644 --- a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py +++ b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py @@ -23,38 +23,39 @@ def get_columns(): "fieldname": "territory", "fieldtype": "Link", "options": "Territory", - "width": 150 + "width": 150, }, { "label": _("Opportunity Amount"), "fieldname": "opportunity_amount", "fieldtype": "Currency", "options": currency, - "width": 150 + "width": 150, }, { "label": _("Quotation Amount"), "fieldname": "quotation_amount", "fieldtype": "Currency", "options": currency, - "width": 150 + "width": 150, }, { "label": _("Order Amount"), "fieldname": "order_amount", "fieldtype": "Currency", "options": currency, - "width": 150 + "width": 150, }, { "label": _("Billing Amount"), "fieldname": "billing_amount", "fieldtype": "Currency", "options": currency, - "width": 150 - } + "width": 150, + }, ] + def get_data(filters=None): data = [] @@ -84,26 +85,32 @@ def get_data(filters=None): if territory_orders: t_order_names = [t.name for t in territory_orders] - territory_invoices = list(filter(lambda x: x.sales_order in t_order_names, sales_invoices)) if t_order_names and sales_invoices else [] + territory_invoices = ( + list(filter(lambda x: x.sales_order in t_order_names, sales_invoices)) + if t_order_names and sales_invoices + else [] + ) territory_data = { "territory": territory.name, "opportunity_amount": _get_total(territory_opportunities, "opportunity_amount"), "quotation_amount": _get_total(territory_quotations), "order_amount": _get_total(territory_orders), - "billing_amount": _get_total(territory_invoices) + "billing_amount": _get_total(territory_invoices), } data.append(territory_data) return data + def get_opportunities(filters): conditions = "" - if filters.get('transaction_date'): + if filters.get("transaction_date"): conditions = " WHERE transaction_date between {0} and {1}".format( - frappe.db.escape(filters['transaction_date'][0]), - frappe.db.escape(filters['transaction_date'][1])) + frappe.db.escape(filters["transaction_date"][0]), + frappe.db.escape(filters["transaction_date"][1]), + ) if filters.company: if conditions: @@ -112,11 +119,17 @@ def get_opportunities(filters): conditions += " WHERE" conditions += " company = %(company)s" - - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT name, territory, opportunity_amount FROM `tabOpportunity` {0} - """.format(conditions), filters, as_dict=1) #nosec + """.format( + conditions + ), + filters, + as_dict=1, + ) # nosec + def get_quotations(opportunities): if not opportunities: @@ -124,11 +137,18 @@ def get_quotations(opportunities): opportunity_names = [o.name for o in opportunities] - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT `name`,`base_grand_total`, `opportunity` FROM `tabQuotation` WHERE docstatus=1 AND opportunity in ({0}) - """.format(', '.join(["%s"]*len(opportunity_names))), tuple(opportunity_names), as_dict=1) #nosec + """.format( + ", ".join(["%s"] * len(opportunity_names)) + ), + tuple(opportunity_names), + as_dict=1, + ) # nosec + def get_sales_orders(quotations): if not quotations: @@ -136,11 +156,18 @@ def get_sales_orders(quotations): quotation_names = [q.name for q in quotations] - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT so.`name`, so.`base_grand_total`, soi.prevdoc_docname as quotation FROM `tabSales Order` so, `tabSales Order Item` soi WHERE so.docstatus=1 AND so.name = soi.parent AND soi.prevdoc_docname in ({0}) - """.format(', '.join(["%s"]*len(quotation_names))), tuple(quotation_names), as_dict=1) #nosec + """.format( + ", ".join(["%s"] * len(quotation_names)) + ), + tuple(quotation_names), + as_dict=1, + ) # nosec + def get_sales_invoice(sales_orders): if not sales_orders: @@ -148,11 +175,18 @@ def get_sales_invoice(sales_orders): so_names = [so.name for so in sales_orders] - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT si.name, si.base_grand_total, sii.sales_order FROM `tabSales Invoice` si, `tabSales Invoice Item` sii WHERE si.docstatus=1 AND si.name = sii.parent AND sii.sales_order in ({0}) - """.format(', '.join(["%s"]*len(so_names))), tuple(so_names), as_dict=1) #nosec + """.format( + ", ".join(["%s"] * len(so_names)) + ), + tuple(so_names), + as_dict=1, + ) # nosec + def _get_total(doclist, amount_field="base_grand_total"): if not doclist: diff --git a/erpnext/setup/default_energy_point_rules.py b/erpnext/setup/default_energy_point_rules.py index 1ce06b8c2f..b7fe19758c 100644 --- a/erpnext/setup/default_energy_point_rules.py +++ b/erpnext/setup/default_energy_point_rules.py @@ -1,56 +1,48 @@ from frappe import _ doctype_rule_map = { - 'Item': { - 'points': 5, - 'for_doc_event': 'New' + "Item": {"points": 5, "for_doc_event": "New"}, + "Customer": {"points": 5, "for_doc_event": "New"}, + "Supplier": {"points": 5, "for_doc_event": "New"}, + "Lead": {"points": 2, "for_doc_event": "New"}, + "Opportunity": { + "points": 10, + "for_doc_event": "Custom", + "condition": 'doc.status=="Converted"', + "rule_name": _("On Converting Opportunity"), + "user_field": "converted_by", }, - 'Customer': { - 'points': 5, - 'for_doc_event': 'New' + "Sales Order": { + "points": 10, + "for_doc_event": "Submit", + "rule_name": _("On Sales Order Submission"), + "user_field": "modified_by", }, - 'Supplier': { - 'points': 5, - 'for_doc_event': 'New' + "Purchase Order": { + "points": 10, + "for_doc_event": "Submit", + "rule_name": _("On Purchase Order Submission"), + "user_field": "modified_by", }, - 'Lead': { - 'points': 2, - 'for_doc_event': 'New' + "Task": { + "points": 5, + "condition": 'doc.status == "Completed"', + "rule_name": _("On Task Completion"), + "user_field": "completed_by", }, - 'Opportunity': { - 'points': 10, - 'for_doc_event': 'Custom', - 'condition': 'doc.status=="Converted"', - 'rule_name': _('On Converting Opportunity'), - 'user_field': 'converted_by' - }, - 'Sales Order': { - 'points': 10, - 'for_doc_event': 'Submit', - 'rule_name': _('On Sales Order Submission'), - 'user_field': 'modified_by' - }, - 'Purchase Order': { - 'points': 10, - 'for_doc_event': 'Submit', - 'rule_name': _('On Purchase Order Submission'), - 'user_field': 'modified_by' - }, - 'Task': { - 'points': 5, - 'condition': 'doc.status == "Completed"', - 'rule_name': _('On Task Completion'), - 'user_field': 'completed_by' - } } + def get_default_energy_point_rules(): - return [{ - 'doctype': 'Energy Point Rule', - 'reference_doctype': doctype, - 'for_doc_event': rule.get('for_doc_event') or 'Custom', - 'condition': rule.get('condition'), - 'rule_name': rule.get('rule_name') or _('On {0} Creation').format(doctype), - 'points': rule.get('points'), - 'user_field': rule.get('user_field') or 'owner' - } for doctype, rule in doctype_rule_map.items()] + return [ + { + "doctype": "Energy Point Rule", + "reference_doctype": doctype, + "for_doc_event": rule.get("for_doc_event") or "Custom", + "condition": rule.get("condition"), + "rule_name": rule.get("rule_name") or _("On {0} Creation").format(doctype), + "points": rule.get("points"), + "user_field": rule.get("user_field") or "owner", + } + for doctype, rule in doctype_rule_map.items() + ] diff --git a/erpnext/setup/default_success_action.py b/erpnext/setup/default_success_action.py index a1f48df672..2b9e75c326 100644 --- a/erpnext/setup/default_success_action.py +++ b/erpnext/setup/default_success_action.py @@ -1,25 +1,31 @@ from frappe import _ doctype_list = [ - 'Purchase Receipt', - 'Purchase Invoice', - 'Quotation', - 'Sales Order', - 'Delivery Note', - 'Sales Invoice' + "Purchase Receipt", + "Purchase Invoice", + "Quotation", + "Sales Order", + "Delivery Note", + "Sales Invoice", ] + def get_message(doctype): return _("{0} has been submitted successfully").format(_(doctype)) + def get_first_success_message(doctype): return get_message(doctype) + def get_default_success_action(): - return [{ - 'doctype': 'Success Action', - 'ref_doctype': doctype, - 'message': get_message(doctype), - 'first_success_message': get_first_success_message(doctype), - 'next_actions': 'new\nprint\nemail' - } for doctype in doctype_list] + return [ + { + "doctype": "Success Action", + "ref_doctype": doctype, + "message": get_message(doctype), + "first_success_message": get_first_success_message(doctype), + "next_actions": "new\nprint\nemail", + } + for doctype in doctype_list + ] diff --git a/erpnext/setup/doctype/authorization_control/authorization_control.py b/erpnext/setup/doctype/authorization_control/authorization_control.py index 2a0d785520..309658d260 100644 --- a/erpnext/setup/doctype/authorization_control/authorization_control.py +++ b/erpnext/setup/doctype/authorization_control/authorization_control.py @@ -12,90 +12,121 @@ from erpnext.utilities.transaction_base import TransactionBase class AuthorizationControl(TransactionBase): def get_appr_user_role(self, det, doctype_name, total, based_on, condition, item, company): amt_list, appr_users, appr_roles = [], [], [] - users, roles = '','' + users, roles = "", "" if det: for x in det: amt_list.append(flt(x[0])) max_amount = max(amt_list) - app_dtl = frappe.db.sql("""select approving_user, approving_role from `tabAuthorization Rule` + app_dtl = frappe.db.sql( + """select approving_user, approving_role from `tabAuthorization Rule` where transaction = %s and (value = %s or value > %s) - and docstatus != 2 and based_on = %s and company = %s %s""" % - ('%s', '%s', '%s', '%s', '%s', condition), - (doctype_name, flt(max_amount), total, based_on, company)) + and docstatus != 2 and based_on = %s and company = %s %s""" + % ("%s", "%s", "%s", "%s", "%s", condition), + (doctype_name, flt(max_amount), total, based_on, company), + ) if not app_dtl: - app_dtl = frappe.db.sql("""select approving_user, approving_role from `tabAuthorization Rule` + app_dtl = frappe.db.sql( + """select approving_user, approving_role from `tabAuthorization Rule` where transaction = %s and (value = %s or value > %s) and docstatus != 2 - and based_on = %s and ifnull(company,'') = '' %s""" % - ('%s', '%s', '%s', '%s', condition), (doctype_name, flt(max_amount), total, based_on)) + and based_on = %s and ifnull(company,'') = '' %s""" + % ("%s", "%s", "%s", "%s", condition), + (doctype_name, flt(max_amount), total, based_on), + ) for d in app_dtl: - if(d[0]): appr_users.append(d[0]) - if(d[1]): appr_roles.append(d[1]) + if d[0]: + appr_users.append(d[0]) + if d[1]: + appr_roles.append(d[1]) - if not has_common(appr_roles, frappe.get_roles()) and not has_common(appr_users, [session['user']]): + if not has_common(appr_roles, frappe.get_roles()) and not has_common( + appr_users, [session["user"]] + ): frappe.msgprint(_("Not authroized since {0} exceeds limits").format(_(based_on))) frappe.throw(_("Can be approved by {0}").format(comma_or(appr_roles + appr_users))) - def validate_auth_rule(self, doctype_name, total, based_on, cond, company, item = ''): + def validate_auth_rule(self, doctype_name, total, based_on, cond, company, item=""): chk = 1 - add_cond1,add_cond2 = '','' - if based_on == 'Itemwise Discount': + add_cond1, add_cond2 = "", "" + if based_on == "Itemwise Discount": add_cond1 += " and master_name = " + frappe.db.escape(cstr(item)) - itemwise_exists = frappe.db.sql("""select value from `tabAuthorization Rule` + itemwise_exists = frappe.db.sql( + """select value from `tabAuthorization Rule` where transaction = %s and value <= %s - and based_on = %s and company = %s and docstatus != 2 %s %s""" % - ('%s', '%s', '%s', '%s', cond, add_cond1), (doctype_name, total, based_on, company)) + and based_on = %s and company = %s and docstatus != 2 %s %s""" + % ("%s", "%s", "%s", "%s", cond, add_cond1), + (doctype_name, total, based_on, company), + ) if not itemwise_exists: - itemwise_exists = frappe.db.sql("""select value from `tabAuthorization Rule` + itemwise_exists = frappe.db.sql( + """select value from `tabAuthorization Rule` where transaction = %s and value <= %s and based_on = %s - and ifnull(company,'') = '' and docstatus != 2 %s %s""" % - ('%s', '%s', '%s', cond, add_cond1), (doctype_name, total, based_on)) + and ifnull(company,'') = '' and docstatus != 2 %s %s""" + % ("%s", "%s", "%s", cond, add_cond1), + (doctype_name, total, based_on), + ) if itemwise_exists: - self.get_appr_user_role(itemwise_exists, doctype_name, total, based_on, cond+add_cond1, item,company) + self.get_appr_user_role( + itemwise_exists, doctype_name, total, based_on, cond + add_cond1, item, company + ) chk = 0 if chk == 1: - if based_on == 'Itemwise Discount': + if based_on == "Itemwise Discount": add_cond2 += " and ifnull(master_name,'') = ''" - appr = frappe.db.sql("""select value from `tabAuthorization Rule` + appr = frappe.db.sql( + """select value from `tabAuthorization Rule` where transaction = %s and value <= %s and based_on = %s - and company = %s and docstatus != 2 %s %s""" % - ('%s', '%s', '%s', '%s', cond, add_cond2), (doctype_name, total, based_on, company)) + and company = %s and docstatus != 2 %s %s""" + % ("%s", "%s", "%s", "%s", cond, add_cond2), + (doctype_name, total, based_on, company), + ) if not appr: - appr = frappe.db.sql("""select value from `tabAuthorization Rule` + appr = frappe.db.sql( + """select value from `tabAuthorization Rule` where transaction = %s and value <= %s and based_on = %s - and ifnull(company,'') = '' and docstatus != 2 %s %s""" % - ('%s', '%s', '%s', cond, add_cond2), (doctype_name, total, based_on)) + and ifnull(company,'') = '' and docstatus != 2 %s %s""" + % ("%s", "%s", "%s", cond, add_cond2), + (doctype_name, total, based_on), + ) - self.get_appr_user_role(appr, doctype_name, total, based_on, cond+add_cond2, item, company) + self.get_appr_user_role(appr, doctype_name, total, based_on, cond + add_cond2, item, company) def bifurcate_based_on_type(self, doctype_name, total, av_dis, based_on, doc_obj, val, company): - add_cond = '' + add_cond = "" auth_value = av_dis - if val == 1: add_cond += " and system_user = {}".format(frappe.db.escape(session['user'])) - elif val == 2: add_cond += " and system_role IN %s" % ("('"+"','".join(frappe.get_roles())+"')") - else: add_cond += " and ifnull(system_user,'') = '' and ifnull(system_role,'') = ''" + if val == 1: + add_cond += " and system_user = {}".format(frappe.db.escape(session["user"])) + elif val == 2: + add_cond += " and system_role IN %s" % ("('" + "','".join(frappe.get_roles()) + "')") + else: + add_cond += " and ifnull(system_user,'') = '' and ifnull(system_role,'') = ''" - if based_on == 'Grand Total': auth_value = total - elif based_on == 'Customerwise Discount': + if based_on == "Grand Total": + auth_value = total + elif based_on == "Customerwise Discount": if doc_obj: - if doc_obj.doctype == 'Sales Invoice': customer = doc_obj.customer - else: customer = doc_obj.customer_name + if doc_obj.doctype == "Sales Invoice": + customer = doc_obj.customer + else: + customer = doc_obj.customer_name add_cond = " and master_name = {}".format(frappe.db.escape(customer)) - if based_on == 'Itemwise Discount': + if based_on == "Itemwise Discount": if doc_obj: for t in doc_obj.get("items"): - self.validate_auth_rule(doctype_name, t.discount_percentage, based_on, add_cond, company,t.item_code ) + self.validate_auth_rule( + doctype_name, t.discount_percentage, based_on, add_cond, company, t.item_code + ) else: self.validate_auth_rule(doctype_name, auth_value, based_on, add_cond, company) - def validate_approving_authority(self, doctype_name,company, total, doc_obj = ''): + def validate_approving_authority(self, doctype_name, company, total, doc_obj=""): if not frappe.db.count("Authorization Rule"): return @@ -109,56 +140,85 @@ class AuthorizationControl(TransactionBase): if doc_obj.get("discount_amount"): base_rate -= flt(doc_obj.discount_amount) - if price_list_rate: av_dis = 100 - flt(base_rate * 100 / price_list_rate) + if price_list_rate: + av_dis = 100 - flt(base_rate * 100 / price_list_rate) - final_based_on = ['Grand Total','Average Discount','Customerwise Discount','Itemwise Discount'] + final_based_on = [ + "Grand Total", + "Average Discount", + "Customerwise Discount", + "Itemwise Discount", + ] # Check for authorization set for individual user - based_on = [x[0] for x in frappe.db.sql("""select distinct based_on from `tabAuthorization Rule` + based_on = [ + x[0] + for x in frappe.db.sql( + """select distinct based_on from `tabAuthorization Rule` where transaction = %s and system_user = %s and (company = %s or ifnull(company,'')='') and docstatus != 2""", - (doctype_name, session['user'], company))] + (doctype_name, session["user"], company), + ) + ] for d in based_on: self.bifurcate_based_on_type(doctype_name, total, av_dis, d, doc_obj, 1, company) # Remove user specific rules from global authorization rules for r in based_on: - if r in final_based_on and r != 'Itemwise Discount': final_based_on.remove(r) + if r in final_based_on and r != "Itemwise Discount": + final_based_on.remove(r) # Check for authorization set on particular roles - based_on = [x[0] for x in frappe.db.sql("""select based_on + based_on = [ + x[0] + for x in frappe.db.sql( + """select based_on from `tabAuthorization Rule` where transaction = %s and system_role IN (%s) and based_on IN (%s) and (company = %s or ifnull(company,'')='') and docstatus != 2 - """ % ('%s', "'"+"','".join(frappe.get_roles())+"'", "'"+"','".join(final_based_on)+"'", '%s'), (doctype_name, company))] + """ + % ( + "%s", + "'" + "','".join(frappe.get_roles()) + "'", + "'" + "','".join(final_based_on) + "'", + "%s", + ), + (doctype_name, company), + ) + ] for d in based_on: self.bifurcate_based_on_type(doctype_name, total, av_dis, d, doc_obj, 2, company) # Remove role specific rules from global authorization rules for r in based_on: - if r in final_based_on and r != 'Itemwise Discount': final_based_on.remove(r) + if r in final_based_on and r != "Itemwise Discount": + final_based_on.remove(r) # Check for global authorization for g in final_based_on: self.bifurcate_based_on_type(doctype_name, total, av_dis, g, doc_obj, 0, company) - def get_value_based_rule(self,doctype_name,employee,total_claimed_amount,company): - val_lst =[] - val = frappe.db.sql("""select value from `tabAuthorization Rule` + def get_value_based_rule(self, doctype_name, employee, total_claimed_amount, company): + val_lst = [] + val = frappe.db.sql( + """select value from `tabAuthorization Rule` where transaction=%s and (to_emp=%s or to_designation IN (select designation from `tabEmployee` where name=%s)) and ifnull(value,0)< %s and company = %s and docstatus!=2""", - (doctype_name,employee,employee,total_claimed_amount,company)) + (doctype_name, employee, employee, total_claimed_amount, company), + ) if not val: - val = frappe.db.sql("""select value from `tabAuthorization Rule` + val = frappe.db.sql( + """select value from `tabAuthorization Rule` where transaction=%s and (to_emp=%s or to_designation IN (select designation from `tabEmployee` where name=%s)) and ifnull(value,0)< %s and ifnull(company,'') = '' and docstatus!=2""", - (doctype_name, employee, employee, total_claimed_amount)) + (doctype_name, employee, employee, total_claimed_amount), + ) if val: val_lst = [y[0] for y in val] @@ -166,64 +226,83 @@ class AuthorizationControl(TransactionBase): val_lst.append(0) max_val = max(val_lst) - rule = frappe.db.sql("""select name, to_emp, to_designation, approving_role, approving_user + rule = frappe.db.sql( + """select name, to_emp, to_designation, approving_role, approving_user from `tabAuthorization Rule` where transaction=%s and company = %s and (to_emp=%s or to_designation IN (select designation from `tabEmployee` where name=%s)) and ifnull(value,0)= %s and docstatus!=2""", - (doctype_name,company,employee,employee,flt(max_val)), as_dict=1) + (doctype_name, company, employee, employee, flt(max_val)), + as_dict=1, + ) if not rule: - rule = frappe.db.sql("""select name, to_emp, to_designation, approving_role, approving_user + rule = frappe.db.sql( + """select name, to_emp, to_designation, approving_role, approving_user from `tabAuthorization Rule` where transaction=%s and ifnull(company,'') = '' and (to_emp=%s or to_designation IN (select designation from `tabEmployee` where name=%s)) and ifnull(value,0)= %s and docstatus!=2""", - (doctype_name,employee,employee,flt(max_val)), as_dict=1) + (doctype_name, employee, employee, flt(max_val)), + as_dict=1, + ) return rule # related to payroll module only - def get_approver_name(self, doctype_name, total, doc_obj=''): - app_user=[] - app_specific_user =[] - rule ={} + def get_approver_name(self, doctype_name, total, doc_obj=""): + app_user = [] + app_specific_user = [] + rule = {} if doc_obj: - if doctype_name == 'Expense Claim': - rule = self.get_value_based_rule(doctype_name, doc_obj.employee, - doc_obj.total_claimed_amount, doc_obj.company) - elif doctype_name == 'Appraisal': - rule = frappe.db.sql("""select name, to_emp, to_designation, approving_role, approving_user + if doctype_name == "Expense Claim": + rule = self.get_value_based_rule( + doctype_name, doc_obj.employee, doc_obj.total_claimed_amount, doc_obj.company + ) + elif doctype_name == "Appraisal": + rule = frappe.db.sql( + """select name, to_emp, to_designation, approving_role, approving_user from `tabAuthorization Rule` where transaction=%s and (to_emp=%s or to_designation IN (select designation from `tabEmployee` where name=%s)) and company = %s and docstatus!=2""", - (doctype_name,doc_obj.employee, doc_obj.employee, doc_obj.company),as_dict=1) + (doctype_name, doc_obj.employee, doc_obj.employee, doc_obj.company), + as_dict=1, + ) if not rule: - rule = frappe.db.sql("""select name, to_emp, to_designation, approving_role, approving_user + rule = frappe.db.sql( + """select name, to_emp, to_designation, approving_role, approving_user from `tabAuthorization Rule` where transaction=%s and (to_emp=%s or to_designation IN (select designation from `tabEmployee` where name=%s)) and ifnull(company,'') = '' and docstatus!=2""", - (doctype_name,doc_obj.employee, doc_obj.employee), as_dict=1) + (doctype_name, doc_obj.employee, doc_obj.employee), + as_dict=1, + ) if rule: for m in rule: - if m['to_emp'] or m['to_designation']: - if m['approving_user']: - app_specific_user.append(m['approving_user']) - elif m['approving_role']: - user_lst = [z[0] for z in frappe.db.sql("""select distinct t1.name + if m["to_emp"] or m["to_designation"]: + if m["approving_user"]: + app_specific_user.append(m["approving_user"]) + elif m["approving_role"]: + user_lst = [ + z[0] + for z in frappe.db.sql( + """select distinct t1.name from `tabUser` t1, `tabHas Role` t2 where t2.role=%s and t2.parent=t1.name and t1.name !='Administrator' - and t1.name != 'Guest' and t1.docstatus !=2""", m['approving_role'])] + and t1.name != 'Guest' and t1.docstatus !=2""", + m["approving_role"], + ) + ] for x in user_lst: if not x in app_user: app_user.append(x) - if len(app_specific_user) >0: + if len(app_specific_user) > 0: return app_specific_user else: return app_user diff --git a/erpnext/setup/doctype/authorization_rule/authorization_rule.py b/erpnext/setup/doctype/authorization_rule/authorization_rule.py index e07de3b293..faecd5ae06 100644 --- a/erpnext/setup/doctype/authorization_rule/authorization_rule.py +++ b/erpnext/setup/doctype/authorization_rule/authorization_rule.py @@ -10,40 +10,58 @@ from frappe.utils import cstr, flt class AuthorizationRule(Document): def check_duplicate_entry(self): - exists = frappe.db.sql("""select name, docstatus from `tabAuthorization Rule` + exists = frappe.db.sql( + """select name, docstatus from `tabAuthorization Rule` where transaction = %s and based_on = %s and system_user = %s and system_role = %s and approving_user = %s and approving_role = %s and to_emp =%s and to_designation=%s and name != %s""", - (self.transaction, self.based_on, cstr(self.system_user), - cstr(self.system_role), cstr(self.approving_user), - cstr(self.approving_role), cstr(self.to_emp), - cstr(self.to_designation), self.name)) - auth_exists = exists and exists[0][0] or '' + ( + self.transaction, + self.based_on, + cstr(self.system_user), + cstr(self.system_role), + cstr(self.approving_user), + cstr(self.approving_role), + cstr(self.to_emp), + cstr(self.to_designation), + self.name, + ), + ) + auth_exists = exists and exists[0][0] or "" if auth_exists: frappe.throw(_("Duplicate Entry. Please check Authorization Rule {0}").format(auth_exists)) - def validate_rule(self): - if self.transaction != 'Appraisal': + if self.transaction != "Appraisal": if not self.approving_role and not self.approving_user: frappe.throw(_("Please enter Approving Role or Approving User")) elif self.system_user and self.system_user == self.approving_user: frappe.throw(_("Approving User cannot be same as user the rule is Applicable To")) elif self.system_role and self.system_role == self.approving_role: frappe.throw(_("Approving Role cannot be same as role the rule is Applicable To")) - elif self.transaction in ['Purchase Order', 'Purchase Receipt', \ - 'Purchase Invoice', 'Stock Entry'] and self.based_on \ - in ['Average Discount', 'Customerwise Discount', 'Itemwise Discount']: - frappe.throw(_("Cannot set authorization on basis of Discount for {0}").format(self.transaction)) - elif self.based_on == 'Average Discount' and flt(self.value) > 100.00: + elif self.transaction in [ + "Purchase Order", + "Purchase Receipt", + "Purchase Invoice", + "Stock Entry", + ] and self.based_on in [ + "Average Discount", + "Customerwise Discount", + "Itemwise Discount", + ]: + frappe.throw( + _("Cannot set authorization on basis of Discount for {0}").format(self.transaction) + ) + elif self.based_on == "Average Discount" and flt(self.value) > 100.00: frappe.throw(_("Discount must be less than 100")) - elif self.based_on == 'Customerwise Discount' and not self.master_name: + elif self.based_on == "Customerwise Discount" and not self.master_name: frappe.throw(_("Customer required for 'Customerwise Discount'")) else: - if self.transaction == 'Appraisal': + if self.transaction == "Appraisal": self.based_on = "Not Applicable" def validate(self): self.check_duplicate_entry() self.validate_rule() - if not self.value: self.value = 0.0 + if not self.value: + self.value = 0.0 diff --git a/erpnext/setup/doctype/authorization_rule/test_authorization_rule.py b/erpnext/setup/doctype/authorization_rule/test_authorization_rule.py index 7d3d5d4c4d..55c1bbb79b 100644 --- a/erpnext/setup/doctype/authorization_rule/test_authorization_rule.py +++ b/erpnext/setup/doctype/authorization_rule/test_authorization_rule.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Authorization Rule') + class TestAuthorizationRule(unittest.TestCase): pass diff --git a/erpnext/setup/doctype/brand/brand.py b/erpnext/setup/doctype/brand/brand.py index 9b91b456c3..1bb6fc9f16 100644 --- a/erpnext/setup/doctype/brand/brand.py +++ b/erpnext/setup/doctype/brand/brand.py @@ -11,6 +11,7 @@ from frappe.model.document import Document class Brand(Document): pass + def get_brand_defaults(item, company): item = frappe.get_cached_doc("Item", item) if item.brand: diff --git a/erpnext/setup/doctype/brand/test_brand.py b/erpnext/setup/doctype/brand/test_brand.py index 1c71448cb8..2e030b09a3 100644 --- a/erpnext/setup/doctype/brand/test_brand.py +++ b/erpnext/setup/doctype/brand/test_brand.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Brand') +test_records = frappe.get_test_records("Brand") diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 36ad8fec9f..c11645822f 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -18,7 +18,7 @@ from erpnext.setup.setup_wizard.operations.taxes_setup import setup_taxes_and_ch class Company(NestedSet): - nsm_parent_field = 'parent_company' + nsm_parent_field = "parent_company" def onload(self): load_address_and_contact(self, "company") @@ -26,12 +26,24 @@ class Company(NestedSet): @frappe.whitelist() def check_if_transactions_exist(self): exists = False - for doctype in ["Sales Invoice", "Delivery Note", "Sales Order", "Quotation", - "Purchase Invoice", "Purchase Receipt", "Purchase Order", "Supplier Quotation"]: - if frappe.db.sql("""select name from `tab%s` where company=%s and docstatus=1 - limit 1""" % (doctype, "%s"), self.name): - exists = True - break + for doctype in [ + "Sales Invoice", + "Delivery Note", + "Sales Order", + "Quotation", + "Purchase Invoice", + "Purchase Receipt", + "Purchase Order", + "Supplier Quotation", + ]: + if frappe.db.sql( + """select name from `tab%s` where company=%s and docstatus=1 + limit 1""" + % (doctype, "%s"), + self.name, + ): + exists = True + break return exists @@ -53,7 +65,7 @@ class Company(NestedSet): def validate_abbr(self): if not self.abbr: - self.abbr = ''.join(c[0] for c in self.company_name.split()).upper() + self.abbr = "".join(c[0] for c in self.company_name.split()).upper() self.abbr = self.abbr.strip() @@ -63,7 +75,9 @@ class Company(NestedSet): if not self.abbr.strip(): frappe.throw(_("Abbreviation is mandatory")) - if frappe.db.sql("select abbr from tabCompany where name!=%s and abbr=%s", (self.name, self.abbr)): + if frappe.db.sql( + "select abbr from tabCompany where name!=%s and abbr=%s", (self.name, self.abbr) + ): frappe.throw(_("Abbreviation already used for another company")) @frappe.whitelist() @@ -72,37 +86,57 @@ class Company(NestedSet): def validate_default_accounts(self): accounts = [ - ["Default Bank Account", "default_bank_account"], ["Default Cash Account", "default_cash_account"], - ["Default Receivable Account", "default_receivable_account"], ["Default Payable Account", "default_payable_account"], - ["Default Expense Account", "default_expense_account"], ["Default Income Account", "default_income_account"], - ["Stock Received But Not Billed Account", "stock_received_but_not_billed"], ["Stock Adjustment Account", "stock_adjustment_account"], - ["Expense Included In Valuation Account", "expenses_included_in_valuation"], ["Default Payroll Payable Account", "default_payroll_payable_account"] + ["Default Bank Account", "default_bank_account"], + ["Default Cash Account", "default_cash_account"], + ["Default Receivable Account", "default_receivable_account"], + ["Default Payable Account", "default_payable_account"], + ["Default Expense Account", "default_expense_account"], + ["Default Income Account", "default_income_account"], + ["Stock Received But Not Billed Account", "stock_received_but_not_billed"], + ["Stock Adjustment Account", "stock_adjustment_account"], + ["Expense Included In Valuation Account", "expenses_included_in_valuation"], + ["Default Payroll Payable Account", "default_payroll_payable_account"], ] for account in accounts: if self.get(account[1]): for_company = frappe.db.get_value("Account", self.get(account[1]), "company") if for_company != self.name: - frappe.throw(_("Account {0} does not belong to company: {1}").format(self.get(account[1]), self.name)) + frappe.throw( + _("Account {0} does not belong to company: {1}").format(self.get(account[1]), self.name) + ) if get_account_currency(self.get(account[1])) != self.default_currency: - error_message = _("{0} currency must be same as company's default currency. Please select another account.") \ - .format(frappe.bold(account[0])) + error_message = _( + "{0} currency must be same as company's default currency. Please select another account." + ).format(frappe.bold(account[0])) frappe.throw(error_message) def validate_currency(self): if self.is_new(): return - self.previous_default_currency = frappe.get_cached_value('Company', self.name, "default_currency") - if self.default_currency and self.previous_default_currency and \ - self.default_currency != self.previous_default_currency and \ - self.check_if_transactions_exist(): - frappe.throw(_("Cannot change company's default currency, because there are existing transactions. Transactions must be cancelled to change the default currency.")) + self.previous_default_currency = frappe.get_cached_value( + "Company", self.name, "default_currency" + ) + if ( + self.default_currency + and self.previous_default_currency + and self.default_currency != self.previous_default_currency + and self.check_if_transactions_exist() + ): + frappe.throw( + _( + "Cannot change company's default currency, because there are existing transactions. Transactions must be cancelled to change the default currency." + ) + ) def on_update(self): NestedSet.on_update(self) - if not frappe.db.sql("""select name from tabAccount - where company=%s and docstatus<2 limit 1""", self.name): + if not frappe.db.sql( + """select name from tabAccount + where company=%s and docstatus<2 limit 1""", + self.name, + ): if not frappe.local.flags.ignore_chart_of_accounts: frappe.flags.country_change = True self.create_default_accounts() @@ -117,7 +151,8 @@ class Company(NestedSet): if not frappe.db.get_value("Department", {"company": self.name}): from erpnext.setup.setup_wizard.operations.install_fixtures import install_post_company_fixtures - install_post_company_fixtures(frappe._dict({'company_name': self.name})) + + install_post_company_fixtures(frappe._dict({"company_name": self.name})) if not frappe.local.flags.ignore_chart_of_accounts: self.set_default_accounts() @@ -127,12 +162,15 @@ class Company(NestedSet): if self.default_currency: frappe.db.set_value("Currency", self.default_currency, "enabled", 1) - if hasattr(frappe.local, 'enable_perpetual_inventory') and \ - self.name in frappe.local.enable_perpetual_inventory: + if ( + hasattr(frappe.local, "enable_perpetual_inventory") + and self.name in frappe.local.enable_perpetual_inventory + ): frappe.local.enable_perpetual_inventory[self.name] = self.enable_perpetual_inventory if frappe.flags.parent_company_changed: from frappe.utils.nestedset import rebuild_tree + rebuild_tree("Company", "parent_company") frappe.clear_cache() @@ -143,31 +181,48 @@ class Company(NestedSet): {"warehouse_name": _("Stores"), "is_group": 0}, {"warehouse_name": _("Work In Progress"), "is_group": 0}, {"warehouse_name": _("Finished Goods"), "is_group": 0}, - {"warehouse_name": _("Goods In Transit"), "is_group": 0, "warehouse_type": "Transit"}]: + {"warehouse_name": _("Goods In Transit"), "is_group": 0, "warehouse_type": "Transit"}, + ]: - if not frappe.db.exists("Warehouse", "{0} - {1}".format(wh_detail["warehouse_name"], self.abbr)): - warehouse = frappe.get_doc({ - "doctype":"Warehouse", - "warehouse_name": wh_detail["warehouse_name"], - "is_group": wh_detail["is_group"], - "company": self.name, - "parent_warehouse": "{0} - {1}".format(_("All Warehouses"), self.abbr) \ - if not wh_detail["is_group"] else "", - "warehouse_type" : wh_detail["warehouse_type"] if "warehouse_type" in wh_detail else None - }) + if not frappe.db.exists( + "Warehouse", "{0} - {1}".format(wh_detail["warehouse_name"], self.abbr) + ): + warehouse = frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": wh_detail["warehouse_name"], + "is_group": wh_detail["is_group"], + "company": self.name, + "parent_warehouse": "{0} - {1}".format(_("All Warehouses"), self.abbr) + if not wh_detail["is_group"] + else "", + "warehouse_type": wh_detail["warehouse_type"] if "warehouse_type" in wh_detail else None, + } + ) warehouse.flags.ignore_permissions = True warehouse.flags.ignore_mandatory = True warehouse.insert() def create_default_accounts(self): from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts + frappe.local.flags.ignore_root_company_validation = True create_charts(self.name, self.chart_of_accounts, self.existing_company) - frappe.db.set(self, "default_receivable_account", frappe.db.get_value("Account", - {"company": self.name, "account_type": "Receivable", "is_group": 0})) - frappe.db.set(self, "default_payable_account", frappe.db.get_value("Account", - {"company": self.name, "account_type": "Payable", "is_group": 0})) + frappe.db.set( + self, + "default_receivable_account", + frappe.db.get_value( + "Account", {"company": self.name, "account_type": "Receivable", "is_group": 0} + ), + ) + frappe.db.set( + self, + "default_payable_account", + frappe.db.get_value( + "Account", {"company": self.name, "account_type": "Payable", "is_group": 0} + ), + ) def validate_coa_input(self): if self.create_chart_of_accounts_based_on == "Existing Company": @@ -184,34 +239,46 @@ class Company(NestedSet): def validate_perpetual_inventory(self): if not self.get("__islocal"): if cint(self.enable_perpetual_inventory) == 1 and not self.default_inventory_account: - frappe.msgprint(_("Set default inventory account for perpetual inventory"), - alert=True, indicator='orange') + frappe.msgprint( + _("Set default inventory account for perpetual inventory"), alert=True, indicator="orange" + ) def validate_provisional_account_for_non_stock_items(self): if not self.get("__islocal"): - if cint(self.enable_provisional_accounting_for_non_stock_items) == 1 and not self.default_provisional_account: - frappe.throw(_("Set default {0} account for non stock items").format( - frappe.bold('Provisional Account'))) + if ( + cint(self.enable_provisional_accounting_for_non_stock_items) == 1 + and not self.default_provisional_account + ): + frappe.throw( + _("Set default {0} account for non stock items").format(frappe.bold("Provisional Account")) + ) - make_property_setter("Purchase Receipt", "provisional_expense_account", "hidden", - not self.enable_provisional_accounting_for_non_stock_items, "Check", validate_fields_for_doctype=False) + make_property_setter( + "Purchase Receipt", + "provisional_expense_account", + "hidden", + not self.enable_provisional_accounting_for_non_stock_items, + "Check", + validate_fields_for_doctype=False, + ) def check_country_change(self): frappe.flags.country_change = False - if not self.is_new() and \ - self.country != frappe.get_cached_value('Company', self.name, 'country'): + if not self.is_new() and self.country != frappe.get_cached_value( + "Company", self.name, "country" + ): frappe.flags.country_change = True def set_chart_of_accounts(self): - ''' If parent company is set, chart of accounts will be based on that company ''' + """If parent company is set, chart of accounts will be based on that company""" if self.parent_company: self.create_chart_of_accounts_based_on = "Existing Company" self.existing_company = self.parent_company def validate_parent_company(self): if self.parent_company: - is_group = frappe.get_value('Company', self.parent_company, 'is_group') + is_group = frappe.get_value("Company", self.parent_company, "is_group") if not is_group: frappe.throw(_("Parent Company must be a group company")) @@ -225,29 +292,33 @@ class Company(NestedSet): "depreciation_expense_account": "Depreciation", "capital_work_in_progress_account": "Capital Work in Progress", "asset_received_but_not_billed": "Asset Received But Not Billed", - "expenses_included_in_asset_valuation": "Expenses Included In Asset Valuation" + "expenses_included_in_asset_valuation": "Expenses Included In Asset Valuation", } if self.enable_perpetual_inventory: - default_accounts.update({ - "stock_received_but_not_billed": "Stock Received But Not Billed", - "default_inventory_account": "Stock", - "stock_adjustment_account": "Stock Adjustment", - "expenses_included_in_valuation": "Expenses Included In Valuation", - "default_expense_account": "Cost of Goods Sold" - }) + default_accounts.update( + { + "stock_received_but_not_billed": "Stock Received But Not Billed", + "default_inventory_account": "Stock", + "stock_adjustment_account": "Stock Adjustment", + "expenses_included_in_valuation": "Expenses Included In Valuation", + "default_expense_account": "Cost of Goods Sold", + } + ) if self.update_default_account: for default_account in default_accounts: self._set_default_account(default_account, default_accounts.get(default_account)) if not self.default_income_account: - income_account = frappe.db.get_value("Account", - {"account_name": _("Sales"), "company": self.name, "is_group": 0}) + income_account = frappe.db.get_value( + "Account", {"account_name": _("Sales"), "company": self.name, "is_group": 0} + ) if not income_account: - income_account = frappe.db.get_value("Account", - {"account_name": _("Sales Account"), "company": self.name}) + income_account = frappe.db.get_value( + "Account", {"account_name": _("Sales Account"), "company": self.name} + ) self.db_set("default_income_account", income_account) @@ -255,32 +326,38 @@ class Company(NestedSet): self.db_set("default_payable_account", self.default_payable_account) if not self.default_payroll_payable_account: - payroll_payable_account = frappe.db.get_value("Account", - {"account_name": _("Payroll Payable"), "company": self.name, "is_group": 0}) + payroll_payable_account = frappe.db.get_value( + "Account", {"account_name": _("Payroll Payable"), "company": self.name, "is_group": 0} + ) self.db_set("default_payroll_payable_account", payroll_payable_account) if not self.default_employee_advance_account: - employe_advance_account = frappe.db.get_value("Account", - {"account_name": _("Employee Advances"), "company": self.name, "is_group": 0}) + employe_advance_account = frappe.db.get_value( + "Account", {"account_name": _("Employee Advances"), "company": self.name, "is_group": 0} + ) self.db_set("default_employee_advance_account", employe_advance_account) if not self.write_off_account: - write_off_acct = frappe.db.get_value("Account", - {"account_name": _("Write Off"), "company": self.name, "is_group": 0}) + write_off_acct = frappe.db.get_value( + "Account", {"account_name": _("Write Off"), "company": self.name, "is_group": 0} + ) self.db_set("write_off_account", write_off_acct) if not self.exchange_gain_loss_account: - exchange_gain_loss_acct = frappe.db.get_value("Account", - {"account_name": _("Exchange Gain/Loss"), "company": self.name, "is_group": 0}) + exchange_gain_loss_acct = frappe.db.get_value( + "Account", {"account_name": _("Exchange Gain/Loss"), "company": self.name, "is_group": 0} + ) self.db_set("exchange_gain_loss_account", exchange_gain_loss_acct) if not self.disposal_account: - disposal_acct = frappe.db.get_value("Account", - {"account_name": _("Gain/Loss on Asset Disposal"), "company": self.name, "is_group": 0}) + disposal_acct = frappe.db.get_value( + "Account", + {"account_name": _("Gain/Loss on Asset Disposal"), "company": self.name, "is_group": 0}, + ) self.db_set("disposal_account", disposal_acct) @@ -288,35 +365,39 @@ class Company(NestedSet): if self.get(fieldname): return - account = frappe.db.get_value("Account", {"account_type": account_type, "is_group": 0, "company": self.name}) + account = frappe.db.get_value( + "Account", {"account_type": account_type, "is_group": 0, "company": self.name} + ) if account: self.db_set(fieldname, account) def set_mode_of_payment_account(self): - cash = frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name') - if cash and self.default_cash_account \ - and not frappe.db.get_value('Mode of Payment Account', {'company': self.name, 'parent': cash}): - mode_of_payment = frappe.get_doc('Mode of Payment', cash, for_update=True) - mode_of_payment.append('accounts', { - 'company': self.name, - 'default_account': self.default_cash_account - }) + cash = frappe.db.get_value("Mode of Payment", {"type": "Cash"}, "name") + if ( + cash + and self.default_cash_account + and not frappe.db.get_value("Mode of Payment Account", {"company": self.name, "parent": cash}) + ): + mode_of_payment = frappe.get_doc("Mode of Payment", cash, for_update=True) + mode_of_payment.append( + "accounts", {"company": self.name, "default_account": self.default_cash_account} + ) mode_of_payment.save(ignore_permissions=True) def create_default_cost_center(self): cc_list = [ { - 'cost_center_name': self.name, - 'company':self.name, - 'is_group': 1, - 'parent_cost_center':None + "cost_center_name": self.name, + "company": self.name, + "is_group": 1, + "parent_cost_center": None, }, { - 'cost_center_name':_('Main'), - 'company':self.name, - 'is_group':0, - 'parent_cost_center':self.name + ' - ' + self.abbr + "cost_center_name": _("Main"), + "company": self.name, + "is_group": 0, + "parent_cost_center": self.name + " - " + self.abbr, }, ] for cc in cc_list: @@ -335,26 +416,32 @@ class Company(NestedSet): def after_rename(self, olddn, newdn, merge=False): frappe.db.set(self, "company_name", newdn) - frappe.db.sql("""update `tabDefaultValue` set defvalue=%s - where defkey='Company' and defvalue=%s""", (newdn, olddn)) + frappe.db.sql( + """update `tabDefaultValue` set defvalue=%s + where defkey='Company' and defvalue=%s""", + (newdn, olddn), + ) clear_defaults_cache() def abbreviate(self): - self.abbr = ''.join(c[0].upper() for c in self.company_name.split()) + self.abbr = "".join(c[0].upper() for c in self.company_name.split()) def on_trash(self): """ - Trash accounts and cost centers for this company if no gl entry exists + Trash accounts and cost centers for this company if no gl entry exists """ NestedSet.validate_if_child_exists(self) frappe.utils.nestedset.update_nsm(self) rec = frappe.db.sql("SELECT name from `tabGL Entry` where company = %s", self.name) if not rec: - frappe.db.sql("""delete from `tabBudget Account` + frappe.db.sql( + """delete from `tabBudget Account` where exists(select name from tabBudget - where name=`tabBudget Account`.parent and company = %s)""", self.name) + where name=`tabBudget Account`.parent and company = %s)""", + self.name, + ) for doctype in ["Account", "Cost Center", "Budget", "Party Account"]: frappe.db.sql("delete from `tab{0}` where company = %s".format(doctype), self.name) @@ -369,26 +456,37 @@ class Company(NestedSet): # clear default accounts, warehouses from item warehouses = frappe.db.sql_list("select name from tabWarehouse where company=%s", self.name) if warehouses: - frappe.db.sql("""delete from `tabItem Reorder` where warehouse in (%s)""" - % ', '.join(['%s']*len(warehouses)), tuple(warehouses)) + frappe.db.sql( + """delete from `tabItem Reorder` where warehouse in (%s)""" + % ", ".join(["%s"] * len(warehouses)), + tuple(warehouses), + ) # reset default company - frappe.db.sql("""update `tabSingles` set value="" + frappe.db.sql( + """update `tabSingles` set value="" where doctype='Global Defaults' and field='default_company' - and value=%s""", self.name) + and value=%s""", + self.name, + ) # reset default company - frappe.db.sql("""update `tabSingles` set value="" + frappe.db.sql( + """update `tabSingles` set value="" where doctype='Chart of Accounts Importer' and field='company' - and value=%s""", self.name) + and value=%s""", + self.name, + ) # delete BOMs boms = frappe.db.sql_list("select name from tabBOM where company=%s", self.name) if boms: frappe.db.sql("delete from tabBOM where company=%s", self.name) for dt in ("BOM Operation", "BOM Item", "BOM Scrap Item", "BOM Explosion Item"): - frappe.db.sql("delete from `tab%s` where parent in (%s)""" - % (dt, ', '.join(['%s']*len(boms))), tuple(boms)) + frappe.db.sql( + "delete from `tab%s` where parent in (%s)" "" % (dt, ", ".join(["%s"] * len(boms))), + tuple(boms), + ) frappe.db.sql("delete from tabEmployee where company=%s", self.name) frappe.db.sql("delete from tabDepartment where company=%s", self.name) @@ -401,18 +499,20 @@ class Company(NestedSet): frappe.db.sql("delete from `tabItem Tax Template` where company=%s", self.name) # delete Process Deferred Accounts if no GL Entry found - if not frappe.db.get_value('GL Entry', {'company': self.name}): + if not frappe.db.get_value("GL Entry", {"company": self.name}): frappe.db.sql("delete from `tabProcess Deferred Accounting` where company=%s", self.name) def check_parent_changed(self): frappe.flags.parent_company_changed = False - if not self.is_new() and \ - self.parent_company != frappe.db.get_value("Company", self.name, "parent_company"): + if not self.is_new() and self.parent_company != frappe.db.get_value( + "Company", self.name, "parent_company" + ): frappe.flags.parent_company_changed = True + def get_name_with_abbr(name, company): - company_abbr = frappe.get_cached_value('Company', company, "abbr") + company_abbr = frappe.get_cached_value("Company", company, "abbr") parts = name.split(" - ") if parts[-1].lower() != company_abbr.lower(): @@ -420,6 +520,7 @@ def get_name_with_abbr(name, company): return " - ".join(parts) + def install_country_fixtures(company, country): try: module_name = f"erpnext.regional.{frappe.scrub(country)}.setup.setup" @@ -428,13 +529,18 @@ def install_country_fixtures(company, country): pass except Exception: frappe.log_error() - frappe.throw(_("Failed to setup defaults for country {0}. Please contact support.").format(frappe.bold(country))) + frappe.throw( + _("Failed to setup defaults for country {0}. Please contact support.").format( + frappe.bold(country) + ) + ) def update_company_current_month_sales(company): current_month_year = formatdate(today(), "MM-yyyy") - results = frappe.db.sql(''' + results = frappe.db.sql( + """ SELECT SUM(base_grand_total) AS total, DATE_FORMAT(`posting_date`, '%m-%Y') AS month_year @@ -446,44 +552,58 @@ def update_company_current_month_sales(company): AND company = {company} GROUP BY month_year - '''.format(current_month_year=current_month_year, company=frappe.db.escape(company)), - as_dict = True) + """.format( + current_month_year=current_month_year, company=frappe.db.escape(company) + ), + as_dict=True, + ) - monthly_total = results[0]['total'] if len(results) > 0 else 0 + monthly_total = results[0]["total"] if len(results) > 0 else 0 frappe.db.set_value("Company", company, "total_monthly_sales", monthly_total) + def update_company_monthly_sales(company): - '''Cache past year monthly sales of every company based on sales invoices''' + """Cache past year monthly sales of every company based on sales invoices""" import json from frappe.utils.goal import get_monthly_results - filter_str = "company = {0} and status != 'Draft' and docstatus=1".format(frappe.db.escape(company)) - month_to_value_dict = get_monthly_results("Sales Invoice", "base_grand_total", - "posting_date", filter_str, "sum") + + filter_str = "company = {0} and status != 'Draft' and docstatus=1".format( + frappe.db.escape(company) + ) + month_to_value_dict = get_monthly_results( + "Sales Invoice", "base_grand_total", "posting_date", filter_str, "sum" + ) frappe.db.set_value("Company", company, "sales_monthly_history", json.dumps(month_to_value_dict)) + def update_transactions_annual_history(company, commit=False): transactions_history = get_all_transactions_annual_history(company) - frappe.db.set_value("Company", company, "transactions_annual_history", json.dumps(transactions_history)) + frappe.db.set_value( + "Company", company, "transactions_annual_history", json.dumps(transactions_history) + ) if commit: frappe.db.commit() + def cache_companies_monthly_sales_history(): - companies = [d['name'] for d in frappe.get_list("Company")] + companies = [d["name"] for d in frappe.get_list("Company")] for company in companies: update_company_monthly_sales(company) update_transactions_annual_history(company) frappe.db.commit() + @frappe.whitelist() def get_children(doctype, parent=None, company=None, is_root=False): if parent == None or parent == "All Companies": parent = "" - return frappe.db.sql(""" + return frappe.db.sql( + """ select name as value, is_group as expandable @@ -492,25 +612,30 @@ def get_children(doctype, parent=None, company=None, is_root=False): where ifnull(parent_company, "")={parent} """.format( - doctype = doctype, - parent=frappe.db.escape(parent) - ), as_dict=1) + doctype=doctype, parent=frappe.db.escape(parent) + ), + as_dict=1, + ) + @frappe.whitelist() def add_node(): from frappe.desk.treeview import make_tree_args + args = frappe.form_dict args = make_tree_args(**args) - if args.parent_company == 'All Companies': + if args.parent_company == "All Companies": args.parent_company = None frappe.get_doc(args).insert() + def get_all_transactions_annual_history(company): out = {} - items = frappe.db.sql(''' + items = frappe.db.sql( + """ select transaction_date, count(*) as count from ( @@ -550,46 +675,55 @@ def get_all_transactions_annual_history(company): group by transaction_date - ''', (company), as_dict=True) + """, + (company), + as_dict=True, + ) for d in items: timestamp = get_timestamp(d["transaction_date"]) - out.update({ timestamp: d["count"] }) + out.update({timestamp: d["count"]}) return out + def get_timeline_data(doctype, name): - '''returns timeline data based on linked records in dashboard''' + """returns timeline data based on linked records in dashboard""" out = {} date_to_value_dict = {} - history = frappe.get_cached_value('Company', name, "transactions_annual_history") + history = frappe.get_cached_value("Company", name, "transactions_annual_history") try: - date_to_value_dict = json.loads(history) if history and '{' in history else None + date_to_value_dict = json.loads(history) if history and "{" in history else None except ValueError: date_to_value_dict = None if date_to_value_dict is None: update_transactions_annual_history(name, True) - history = frappe.get_cached_value('Company', name, "transactions_annual_history") - return json.loads(history) if history and '{' in history else {} + history = frappe.get_cached_value("Company", name, "transactions_annual_history") + return json.loads(history) if history and "{" in history else {} return date_to_value_dict + @frappe.whitelist() -def get_default_company_address(name, sort_key='is_primary_address', existing_address=None): - if sort_key not in ['is_shipping_address', 'is_primary_address']: +def get_default_company_address(name, sort_key="is_primary_address", existing_address=None): + if sort_key not in ["is_shipping_address", "is_primary_address"]: return None - out = frappe.db.sql(""" SELECT + out = frappe.db.sql( + """ SELECT addr.name, addr.%s FROM `tabAddress` addr, `tabDynamic Link` dl WHERE dl.parent = addr.name and dl.link_doctype = 'Company' and dl.link_name = %s and ifnull(addr.disabled, 0) = 0 - """ %(sort_key, '%s'), (name)) #nosec + """ + % (sort_key, "%s"), + (name), + ) # nosec if existing_address: if existing_address in [d[0] for d in out]: @@ -600,11 +734,9 @@ def get_default_company_address(name, sort_key='is_primary_address', existing_ad else: return None + @frappe.whitelist() def create_transaction_deletion_request(company): - tdr = frappe.get_doc({ - 'doctype': 'Transaction Deletion Record', - 'company': company - }) + tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company}) tdr.insert() tdr.submit() diff --git a/erpnext/setup/doctype/company/company_dashboard.py b/erpnext/setup/doctype/company/company_dashboard.py index 7cb0b1254f..ff1e7f1103 100644 --- a/erpnext/setup/doctype/company/company_dashboard.py +++ b/erpnext/setup/doctype/company/company_dashboard.py @@ -3,38 +3,25 @@ from frappe import _ def get_data(): return { - 'graph': True, - 'graph_method': "frappe.utils.goal.get_monthly_goal_graph_data", - 'graph_method_args': { - 'title': _('Sales'), - 'goal_value_field': 'monthly_sales_target', - 'goal_total_field': 'total_monthly_sales', - 'goal_history_field': 'sales_monthly_history', - 'goal_doctype': 'Sales Invoice', - 'goal_doctype_link': 'company', - 'goal_field': 'base_grand_total', - 'date_field': 'posting_date', - 'filter_str': "docstatus = 1 and is_opening != 'Yes'", - 'aggregation': 'sum' + "graph": True, + "graph_method": "frappe.utils.goal.get_monthly_goal_graph_data", + "graph_method_args": { + "title": _("Sales"), + "goal_value_field": "monthly_sales_target", + "goal_total_field": "total_monthly_sales", + "goal_history_field": "sales_monthly_history", + "goal_doctype": "Sales Invoice", + "goal_doctype_link": "company", + "goal_field": "base_grand_total", + "date_field": "posting_date", + "filter_str": "docstatus = 1 and is_opening != 'Yes'", + "aggregation": "sum", }, - - 'fieldname': 'company', - 'transactions': [ - { - 'label': _('Pre Sales'), - 'items': ['Quotation'] - }, - { - 'label': _('Orders'), - 'items': ['Sales Order', 'Delivery Note', 'Sales Invoice'] - }, - { - 'label': _('Support'), - 'items': ['Issue'] - }, - { - 'label': _('Projects'), - 'items': ['Project'] - } - ] + "fieldname": "company", + "transactions": [ + {"label": _("Pre Sales"), "items": ["Quotation"]}, + {"label": _("Orders"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]}, + {"label": _("Support"), "items": ["Issue"]}, + {"label": _("Projects"), "items": ["Project"]}, + ], } diff --git a/erpnext/setup/doctype/company/test_company.py b/erpnext/setup/doctype/company/test_company.py index e175c5435a..29e056e34f 100644 --- a/erpnext/setup/doctype/company/test_company.py +++ b/erpnext/setup/doctype/company/test_company.py @@ -14,7 +14,8 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import test_ignore = ["Account", "Cost Center", "Payment Terms Template", "Salary Component", "Warehouse"] test_dependencies = ["Fiscal Year"] -test_records = frappe.get_test_records('Company') +test_records = frappe.get_test_records("Company") + class TestCompany(unittest.TestCase): def test_coa_based_on_existing_company(self): @@ -37,8 +38,8 @@ class TestCompany(unittest.TestCase): "account_type": "Cash", "is_group": 0, "root_type": "Asset", - "parent_account": "Cash In Hand - CFEC" - } + "parent_account": "Cash In Hand - CFEC", + }, } for account, acc_property in expected_results.items(): @@ -69,15 +70,22 @@ class TestCompany(unittest.TestCase): company.chart_of_accounts = template company.save() - account_types = ["Cost of Goods Sold", "Depreciation", - "Expenses Included In Valuation", "Fixed Asset", "Payable", "Receivable", - "Stock Adjustment", "Stock Received But Not Billed", "Bank", "Cash", "Stock"] + account_types = [ + "Cost of Goods Sold", + "Depreciation", + "Expenses Included In Valuation", + "Fixed Asset", + "Payable", + "Receivable", + "Stock Adjustment", + "Stock Received But Not Billed", + "Bank", + "Cash", + "Stock", + ] for account_type in account_types: - filters = { - "company": template, - "account_type": account_type - } + filters = {"company": template, "account_type": account_type} if account_type in ["Bank", "Cash"]: filters["is_group"] = 1 @@ -90,8 +98,11 @@ class TestCompany(unittest.TestCase): frappe.delete_doc("Company", template) def delete_mode_of_payment(self, company): - frappe.db.sql(""" delete from `tabMode of Payment Account` - where company =%s """, (company)) + frappe.db.sql( + """ delete from `tabMode of Payment Account` + where company =%s """, + (company), + ) def test_basic_tree(self, records=None): min_lft = 1 @@ -101,12 +112,12 @@ class TestCompany(unittest.TestCase): records = test_records[2:] for company in records: - lft, rgt, parent_company = frappe.db.get_value("Company", company["company_name"], - ["lft", "rgt", "parent_company"]) + lft, rgt, parent_company = frappe.db.get_value( + "Company", company["company_name"], ["lft", "rgt", "parent_company"] + ) if parent_company: - parent_lft, parent_rgt = frappe.db.get_value("Company", parent_company, - ["lft", "rgt"]) + parent_lft, parent_rgt = frappe.db.get_value("Company", parent_company, ["lft", "rgt"]) else: # root parent_lft = min_lft - 1 @@ -125,8 +136,11 @@ class TestCompany(unittest.TestCase): def get_no_of_children(companies, no_of_children): children = [] for company in companies: - children += frappe.db.sql_list("""select name from `tabCompany` - where ifnull(parent_company, '')=%s""", company or '') + children += frappe.db.sql_list( + """select name from `tabCompany` + where ifnull(parent_company, '')=%s""", + company or "", + ) if len(children): return get_no_of_children(children, no_of_children + len(children)) @@ -148,40 +162,45 @@ class TestCompany(unittest.TestCase): child_company.save() self.test_basic_tree() + def create_company_communication(doctype, docname): - comm = frappe.get_doc({ + comm = frappe.get_doc( + { "doctype": "Communication", "communication_type": "Communication", "content": "Deduplication of Links", "communication_medium": "Email", - "reference_doctype":doctype, - "reference_name":docname - }) + "reference_doctype": doctype, + "reference_name": docname, + } + ) comm.insert() + def create_child_company(): child_company = frappe.db.exists("Company", "Test Company") if not child_company: - child_company = frappe.get_doc({ - "doctype":"Company", - "company_name":"Test Company", - "abbr":"test_company", - "default_currency":"INR" - }) + child_company = frappe.get_doc( + { + "doctype": "Company", + "company_name": "Test Company", + "abbr": "test_company", + "default_currency": "INR", + } + ) child_company.insert() else: child_company = frappe.get_doc("Company", child_company) return child_company.name + def create_test_lead_in_company(company): lead = frappe.db.exists("Lead", "Test Lead in new company") if not lead: - lead = frappe.get_doc({ - "doctype": "Lead", - "lead_name": "Test Lead in new company", - "scompany": company - }) + lead = frappe.get_doc( + {"doctype": "Lead", "lead_name": "Test Lead in new company", "scompany": company} + ) lead.insert() else: lead = frappe.get_doc("Lead", lead) diff --git a/erpnext/setup/doctype/currency_exchange/currency_exchange.py b/erpnext/setup/doctype/currency_exchange/currency_exchange.py index 4191935742..f9f3b3a7dc 100644 --- a/erpnext/setup/doctype/currency_exchange/currency_exchange.py +++ b/erpnext/setup/doctype/currency_exchange/currency_exchange.py @@ -17,13 +17,17 @@ class CurrencyExchange(Document): # If both selling and buying enabled purpose = "Selling-Buying" - if cint(self.for_buying)==0 and cint(self.for_selling)==1: + if cint(self.for_buying) == 0 and cint(self.for_selling) == 1: purpose = "Selling" - if cint(self.for_buying)==1 and cint(self.for_selling)==0: + if cint(self.for_buying) == 1 and cint(self.for_selling) == 0: purpose = "Buying" - self.name = '{0}-{1}-{2}{3}'.format(formatdate(get_datetime_str(self.date), "yyyy-MM-dd"), - self.from_currency, self.to_currency, ("-" + purpose) if purpose else "") + self.name = "{0}-{1}-{2}{3}".format( + formatdate(get_datetime_str(self.date), "yyyy-MM-dd"), + self.from_currency, + self.to_currency, + ("-" + purpose) if purpose else "", + ) def validate(self): self.validate_value("exchange_rate", ">", 0) diff --git a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py index 06a79b4102..e3d281a564 100644 --- a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py +++ b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py @@ -9,20 +9,27 @@ from frappe.utils import cint, flt from erpnext.setup.utils import get_exchange_rate -test_records = frappe.get_test_records('Currency Exchange') +test_records = frappe.get_test_records("Currency Exchange") + def save_new_records(test_records): for record in test_records: # If both selling and buying enabled purpose = "Selling-Buying" - if cint(record.get("for_buying"))==0 and cint(record.get("for_selling"))==1: + if cint(record.get("for_buying")) == 0 and cint(record.get("for_selling")) == 1: purpose = "Selling" - if cint(record.get("for_buying"))==1 and cint(record.get("for_selling"))==0: + if cint(record.get("for_buying")) == 1 and cint(record.get("for_selling")) == 0: purpose = "Buying" kwargs = dict( doctype=record.get("doctype"), - docname=record.get("date") + '-' + record.get("from_currency") + '-' + record.get("to_currency") + '-' + purpose, + docname=record.get("date") + + "-" + + record.get("from_currency") + + "-" + + record.get("to_currency") + + "-" + + purpose, fieldname="exchange_rate", value=record.get("exchange_rate"), ) @@ -39,10 +46,8 @@ def save_new_records(test_records): curr_exchange.for_selling = record["for_selling"] curr_exchange.insert() -test_exchange_values = { - '2015-12-15': '66.999', - '2016-01-15': '65.1' -} + +test_exchange_values = {"2015-12-15": "66.999", "2016-01-15": "65.1"} # Removing API call from get_exchange_rate def patched_requests_get(*args, **kwargs): @@ -58,19 +63,22 @@ def patched_requests_get(*args, **kwargs): def json(self): return self.json_data - if args[0] == "https://api.exchangerate.host/convert" and kwargs.get('params'): - if kwargs['params'].get('date') and kwargs['params'].get('from') and kwargs['params'].get('to'): - if test_exchange_values.get(kwargs['params']['date']): - return PatchResponse({'result': test_exchange_values[kwargs['params']['date']]}, 200) - elif args[0].startswith("https://frankfurter.app") and kwargs.get('params'): - if kwargs['params'].get('base') and kwargs['params'].get('symbols'): + if args[0] == "https://api.exchangerate.host/convert" and kwargs.get("params"): + if kwargs["params"].get("date") and kwargs["params"].get("from") and kwargs["params"].get("to"): + if test_exchange_values.get(kwargs["params"]["date"]): + return PatchResponse({"result": test_exchange_values[kwargs["params"]["date"]]}, 200) + elif args[0].startswith("https://frankfurter.app") and kwargs.get("params"): + if kwargs["params"].get("base") and kwargs["params"].get("symbols"): date = args[0].replace("https://frankfurter.app/", "") if test_exchange_values.get(date): - return PatchResponse({'rates': {kwargs['params'].get('symbols'): test_exchange_values.get(date)}}, 200) + return PatchResponse( + {"rates": {kwargs["params"].get("symbols"): test_exchange_values.get(date)}}, 200 + ) - return PatchResponse({'rates': None}, 404) + return PatchResponse({"rates": None}, 404) -@mock.patch('requests.get', side_effect=patched_requests_get) + +@mock.patch("requests.get", side_effect=patched_requests_get) class TestCurrencyExchange(unittest.TestCase): def clear_cache(self): cache = frappe.cache() @@ -112,7 +120,7 @@ class TestCurrencyExchange(unittest.TestCase): # Update Currency Exchange Rate settings = frappe.get_single("Currency Exchange Settings") - settings.service_provider = 'exchangerate.host' + settings.service_provider = "exchangerate.host" settings.save() # Update exchange @@ -139,7 +147,7 @@ class TestCurrencyExchange(unittest.TestCase): self.assertEqual(flt(exchange_rate, 3), 65.1) settings = frappe.get_single("Currency Exchange Settings") - settings.service_provider = 'frankfurter.app' + settings.service_provider = "frankfurter.app" settings.save() def test_exchange_rate_strict(self, mock_get): diff --git a/erpnext/setup/doctype/customer_group/customer_group.py b/erpnext/setup/doctype/customer_group/customer_group.py index 5b917265d9..246cc195e1 100644 --- a/erpnext/setup/doctype/customer_group/customer_group.py +++ b/erpnext/setup/doctype/customer_group/customer_group.py @@ -8,7 +8,8 @@ from frappe.utils.nestedset import NestedSet, get_root_of class CustomerGroup(NestedSet): - nsm_parent_field = 'parent_customer_group' + nsm_parent_field = "parent_customer_group" + def validate(self): if not self.parent_customer_group: self.parent_customer_group = get_root_of("Customer Group") @@ -22,12 +23,18 @@ class CustomerGroup(NestedSet): if frappe.db.exists("Customer", self.name): frappe.msgprint(_("A customer with the same name already exists"), raise_exception=1) -def get_parent_customer_groups(customer_group): - lft, rgt = frappe.db.get_value("Customer Group", customer_group, ['lft', 'rgt']) - return frappe.db.sql("""select name from `tabCustomer Group` +def get_parent_customer_groups(customer_group): + lft, rgt = frappe.db.get_value("Customer Group", customer_group, ["lft", "rgt"]) + + return frappe.db.sql( + """select name from `tabCustomer Group` where lft <= %s and rgt >= %s - order by lft asc""", (lft, rgt), as_dict=True) + order by lft asc""", + (lft, rgt), + as_dict=True, + ) + def on_doctype_update(): frappe.db.add_index("Customer Group", ["lft", "rgt"]) diff --git a/erpnext/setup/doctype/customer_group/test_customer_group.py b/erpnext/setup/doctype/customer_group/test_customer_group.py index f02ae09792..88762701f5 100644 --- a/erpnext/setup/doctype/customer_group/test_customer_group.py +++ b/erpnext/setup/doctype/customer_group/test_customer_group.py @@ -4,7 +4,6 @@ test_ignore = ["Price List"] - import frappe -test_records = frappe.get_test_records('Customer Group') +test_records = frappe.get_test_records("Customer Group") diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py index 02f9156e19..cdfea7764f 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.py +++ b/erpnext/setup/doctype/email_digest/email_digest.py @@ -36,16 +36,22 @@ class EmailDigest(Document): self.from_date, self.to_date = self.get_from_to_date() self.set_dates() self._accounts = {} - self.currency = frappe.db.get_value('Company', self.company, "default_currency") + self.currency = frappe.db.get_value("Company", self.company, "default_currency") @frappe.whitelist() def get_users(self): """get list of users""" - user_list = frappe.db.sql(""" + user_list = frappe.db.sql( + """ select name, enabled from tabUser where name not in ({}) and user_type != "Website User" - order by enabled desc, name asc""".format(", ".join(["%s"]*len(STANDARD_USERS))), STANDARD_USERS, as_dict=1) + order by enabled desc, name asc""".format( + ", ".join(["%s"] * len(STANDARD_USERS)) + ), + STANDARD_USERS, + as_dict=1, + ) if self.recipient_list: recipient_list = self.recipient_list.split("\n") @@ -54,13 +60,18 @@ class EmailDigest(Document): for p in user_list: p["checked"] = p["name"] in recipient_list and 1 or 0 - frappe.response['user_list'] = user_list + frappe.response["user_list"] = user_list @frappe.whitelist() def send(self): # send email only to enabled users - valid_users = [p[0] for p in frappe.db.sql("""select name from `tabUser` - where enabled=1""")] + valid_users = [ + p[0] + for p in frappe.db.sql( + """select name from `tabUser` + where enabled=1""" + ) + ] if self.recipients: for row in self.recipients: @@ -70,9 +81,10 @@ class EmailDigest(Document): recipients=row.recipient, subject=_("{0} Digest").format(self.frequency), message=msg_for_this_recipient, - reference_doctype = self.doctype, - reference_name = self.name, - unsubscribe_message = _("Unsubscribe from this Email Digest")) + reference_doctype=self.doctype, + reference_name=self.name, + unsubscribe_message=_("Unsubscribe from this Email Digest"), + ) def get_msg_html(self): """Build email digest content""" @@ -104,7 +116,10 @@ class EmailDigest(Document): context.quote = {"text": quote[0], "author": quote[1]} if self.get("purchase_orders_items_overdue"): - context.purchase_order_list, context.purchase_orders_items_overdue_list = self.get_purchase_orders_items_overdue_list() + ( + context.purchase_order_list, + context.purchase_orders_items_overdue_list, + ) = self.get_purchase_orders_items_overdue_list() if not context.purchase_order_list: frappe.throw(_("No items to be received are overdue")) @@ -114,49 +129,54 @@ class EmailDigest(Document): frappe.flags.ignore_account_permission = False # style - return frappe.render_template("erpnext/setup/doctype/email_digest/templates/default.html", - context, is_path=True) + return frappe.render_template( + "erpnext/setup/doctype/email_digest/templates/default.html", context, is_path=True + ) def set_title(self, context): """Set digest title""" - if self.frequency=="Daily": + if self.frequency == "Daily": context.title = _("Daily Reminders") context.subtitle = _("Pending activities for today") - elif self.frequency=="Weekly": + elif self.frequency == "Weekly": context.title = _("This Week's Summary") context.subtitle = _("Summary for this week and pending activities") - elif self.frequency=="Monthly": + elif self.frequency == "Monthly": context.title = _("This Month's Summary") context.subtitle = _("Summary for this month and pending activities") def set_style(self, context): """Set standard digest style""" - context.text_muted = '#8D99A6' - context.text_color = '#36414C' - context.h1 = 'margin-bottom: 30px; margin-top: 40px; font-weight: 400; font-size: 30px;' - context.h2 = 'margin-bottom: 30px; margin-top: -20px; font-weight: 400; font-size: 20px;' - context.label_css = '''display: inline-block; color: {text_muted}; - padding: 3px 7px; margin-right: 7px;'''.format(text_muted = context.text_muted) - context.section_head = 'margin-top: 60px; font-size: 16px;' - context.line_item = 'padding: 5px 0px; margin: 0; border-bottom: 1px solid #d1d8dd;' - context.link_css = 'color: {text_color}; text-decoration: none;'.format(text_color = context.text_color) - + context.text_muted = "#8D99A6" + context.text_color = "#36414C" + context.h1 = "margin-bottom: 30px; margin-top: 40px; font-weight: 400; font-size: 30px;" + context.h2 = "margin-bottom: 30px; margin-top: -20px; font-weight: 400; font-size: 20px;" + context.label_css = """display: inline-block; color: {text_muted}; + padding: 3px 7px; margin-right: 7px;""".format( + text_muted=context.text_muted + ) + context.section_head = "margin-top: 60px; font-size: 16px;" + context.line_item = "padding: 5px 0px; margin: 0; border-bottom: 1px solid #d1d8dd;" + context.link_css = "color: {text_color}; text-decoration: none;".format( + text_color=context.text_color + ) def get_notifications(self): """Get notifications for user""" notifications = frappe.desk.notifications.get_notifications() - notifications = sorted(notifications.get("open_count_doctype", {}).items(), - key=lambda a: a[1]) + notifications = sorted(notifications.get("open_count_doctype", {}).items(), key=lambda a: a[1]) - notifications = [{"key": n[0], "value": n[1], - "link": get_url_to_list(n[0])} for n in notifications if n[1]] + notifications = [ + {"key": n[0], "value": n[1], "link": get_url_to_list(n[0])} for n in notifications if n[1] + ] return notifications def get_calendar_events(self): """Get calendar events for given user""" from frappe.desk.doctype.event.event import get_events + from_date, to_date = get_future_date_for_calendaer_event(self.frequency) events = get_events(from_date, to_date) @@ -176,10 +196,13 @@ class EmailDigest(Document): if not user_id: user_id = frappe.session.user - todo_list = frappe.db.sql("""select * + todo_list = frappe.db.sql( + """select * from `tabToDo` where (owner=%s or assigned_by=%s) and status="Open" order by field(priority, 'High', 'Medium', 'Low') asc, date asc limit 20""", - (user_id, user_id), as_dict=True) + (user_id, user_id), + as_dict=True, + ) for t in todo_list: t.link = get_url_to_form("ToDo", t.name) @@ -191,9 +214,11 @@ class EmailDigest(Document): if not user_id: user_id = frappe.session.user - return frappe.db.sql("""select count(*) from `tabToDo` + return frappe.db.sql( + """select count(*) from `tabToDo` where status='Open' and (owner=%s or assigned_by=%s)""", - (user_id, user_id))[0][0] + (user_id, user_id), + )[0][0] def get_issue_list(self, user_id=None): """Get issue list""" @@ -205,9 +230,12 @@ class EmailDigest(Document): if not role_permissions.get("read"): return None - issue_list = frappe.db.sql("""select * + issue_list = frappe.db.sql( + """select * from `tabIssue` where status in ("Replied","Open") - order by modified asc limit 10""", as_dict=True) + order by modified asc limit 10""", + as_dict=True, + ) for t in issue_list: t.link = get_url_to_form("Issue", t.name) @@ -216,17 +244,22 @@ class EmailDigest(Document): def get_issue_count(self): """Get count of Issue""" - return frappe.db.sql("""select count(*) from `tabIssue` - where status in ('Open','Replied') """)[0][0] + return frappe.db.sql( + """select count(*) from `tabIssue` + where status in ('Open','Replied') """ + )[0][0] def get_project_list(self, user_id=None): """Get project list""" if not user_id: user_id = frappe.session.user - project_list = frappe.db.sql("""select * + project_list = frappe.db.sql( + """select * from `tabProject` where status='Open' and project_type='External' - order by modified asc limit 10""", as_dict=True) + order by modified asc limit 10""", + as_dict=True, + ) for t in project_list: t.link = get_url_to_form("Issue", t.name) @@ -235,22 +268,41 @@ class EmailDigest(Document): def get_project_count(self): """Get count of Project""" - return frappe.db.sql("""select count(*) from `tabProject` - where status='Open' and project_type='External'""")[0][0] + return frappe.db.sql( + """select count(*) from `tabProject` + where status='Open' and project_type='External'""" + )[0][0] def set_accounting_cards(self, context): """Create accounting cards if checked""" cache = frappe.cache() context.cards = [] - for key in ("income", "expenses_booked", "income_year_to_date", "expense_year_to_date", - "bank_balance", "credit_balance", "invoiced_amount", "payables", - "sales_orders_to_bill", "purchase_orders_to_bill", "sales_order", "purchase_order", - "sales_orders_to_deliver", "purchase_orders_to_receive", "sales_invoice", "purchase_invoice", - "new_quotations", "pending_quotations"): + for key in ( + "income", + "expenses_booked", + "income_year_to_date", + "expense_year_to_date", + "bank_balance", + "credit_balance", + "invoiced_amount", + "payables", + "sales_orders_to_bill", + "purchase_orders_to_bill", + "sales_order", + "purchase_order", + "sales_orders_to_deliver", + "purchase_orders_to_receive", + "sales_invoice", + "purchase_invoice", + "new_quotations", + "pending_quotations", + ): if self.get(key): - cache_key = "email_digest:card:{0}:{1}:{2}:{3}".format(self.company, self.frequency, key, self.from_date) + cache_key = "email_digest:card:{0}:{1}:{2}:{3}".format( + self.company, self.frequency, key, self.from_date + ) card = cache.get(cache_key) if card: @@ -271,8 +323,9 @@ class EmailDigest(Document): if key == "credit_balance": card.last_value = card.last_value * -1 - card.last_value = self.fmt_money(card.last_value,False if key in ("bank_balance", "credit_balance") else True) - + card.last_value = self.fmt_money( + card.last_value, False if key in ("bank_balance", "credit_balance") else True + ) if card.billed_value: card.billed = int(flt(card.billed_value) / card.value * 100) @@ -285,9 +338,11 @@ class EmailDigest(Document): else: card.delivered = "% Received " + str(card.delivered) - if key =="credit_balance": - card.value = card.value *-1 - card.value = self.fmt_money(card.value,False if key in ("bank_balance", "credit_balance") else True) + if key == "credit_balance": + card.value = card.value * -1 + card.value = self.fmt_money( + card.value, False if key in ("bank_balance", "credit_balance") else True + ) cache.set_value(cache_key, card, expires_in_sec=24 * 60 * 60) @@ -295,30 +350,25 @@ class EmailDigest(Document): def get_income(self): """Get income for given period""" - income, past_income, count = self.get_period_amounts(self.get_roots("income"),'income') + income, past_income, count = self.get_period_amounts(self.get_roots("income"), "income") - income_account = frappe.db.get_all('Account', + income_account = frappe.db.get_all( + "Account", fields=["name"], - filters={ - "root_type":"Income", - "parent_account":'', - "company": self.company - }) + filters={"root_type": "Income", "parent_account": "", "company": self.company}, + ) - label = get_link_to_report("General Ledger",self.meta.get_label("income"), + label = get_link_to_report( + "General Ledger", + self.meta.get_label("income"), filters={ "from_date": self.future_from_date, "to_date": self.future_to_date, "account": income_account[0].name, - "company": self.company - } + "company": self.company, + }, ) - return { - "label": label, - "value": income, - "last_value": past_income, - "count": count - } + return {"label": label, "value": income, "last_value": past_income, "count": count} def get_income_year_to_date(self): """Get income to date""" @@ -326,7 +376,7 @@ class EmailDigest(Document): def get_expense_year_to_date(self): """Get income to date""" - return self.get_year_to_date_balance("expense","expenses_booked") + return self.get_year_to_date_balance("expense", "expenses_booked") def get_year_to_date_balance(self, root_type, fieldname): """Get income to date""" @@ -334,67 +384,63 @@ class EmailDigest(Document): count = 0 for account in self.get_root_type_accounts(root_type): - balance += get_balance_on(account, date = self.future_to_date) - count += get_count_on(account, fieldname, date = self.future_to_date) + balance += get_balance_on(account, date=self.future_to_date) + count += get_count_on(account, fieldname, date=self.future_to_date) - if fieldname == 'income': - filters = { - "currency": self.currency - } - label = get_link_to_report('Profit and Loss Statement', label=self.meta.get_label(root_type + "_year_to_date"), filters=filters) + if fieldname == "income": + filters = {"currency": self.currency} + label = get_link_to_report( + "Profit and Loss Statement", + label=self.meta.get_label(root_type + "_year_to_date"), + filters=filters, + ) - elif fieldname == 'expenses_booked': - filters = { - "currency": self.currency - } - label = get_link_to_report('Profit and Loss Statement', label=self.meta.get_label(root_type + "_year_to_date"), filters=filters) + elif fieldname == "expenses_booked": + filters = {"currency": self.currency} + label = get_link_to_report( + "Profit and Loss Statement", + label=self.meta.get_label(root_type + "_year_to_date"), + filters=filters, + ) - return { - "label": label, - "value": balance, - "count": count - } + return {"label": label, "value": balance, "count": count} def get_bank_balance(self): # account is of type "Bank" and root_type is Asset - return self.get_type_balance('bank_balance', 'Bank', root_type='Asset') + return self.get_type_balance("bank_balance", "Bank", root_type="Asset") def get_credit_balance(self): # account is of type "Bank" and root_type is Liability - return self.get_type_balance('credit_balance', 'Bank', root_type='Liability') + return self.get_type_balance("credit_balance", "Bank", root_type="Liability") def get_payables(self): - return self.get_type_balance('payables', 'Payable') + return self.get_type_balance("payables", "Payable") def get_invoiced_amount(self): - return self.get_type_balance('invoiced_amount', 'Receivable') + return self.get_type_balance("invoiced_amount", "Receivable") def get_expenses_booked(self): - expenses, past_expenses, count = self.get_period_amounts(self.get_roots("expense"), 'expenses_booked') - - expense_account = frappe.db.get_all('Account', - fields=["name"], - filters={ - "root_type": "Expense", - "parent_account": '', - "company": self.company - } - ) - - label = get_link_to_report("General Ledger",self.meta.get_label("expenses_booked"), - filters={ - "company":self.company, - "from_date":self.future_from_date, - "to_date":self.future_to_date, - "account": expense_account[0].name - } + expenses, past_expenses, count = self.get_period_amounts( + self.get_roots("expense"), "expenses_booked" ) - return { - "label": label, - "value": expenses, - "last_value": past_expenses, - "count": count - } + + expense_account = frappe.db.get_all( + "Account", + fields=["name"], + filters={"root_type": "Expense", "parent_account": "", "company": self.company}, + ) + + label = get_link_to_report( + "General Ledger", + self.meta.get_label("expenses_booked"), + filters={ + "company": self.company, + "from_date": self.future_from_date, + "to_date": self.future_to_date, + "account": expense_account[0].name, + }, + ) + return {"label": label, "value": expenses, "last_value": past_expenses, "count": count} def get_period_amounts(self, accounts, fieldname): """Get amounts for current and past periods""" @@ -410,113 +456,129 @@ class EmailDigest(Document): def get_sales_orders_to_bill(self): """Get value not billed""" - value, count = frappe.db.sql("""select ifnull((sum(grand_total)) - (sum(grand_total*per_billed/100)),0), + value, count = frappe.db.sql( + """select ifnull((sum(grand_total)) - (sum(grand_total*per_billed/100)),0), count(*) from `tabSales Order` where (transaction_date <= %(to_date)s) and billing_status != "Fully Billed" and company = %(company)s - and status not in ('Closed','Cancelled', 'Completed') """, {"to_date": self.future_to_date, "company": self.company})[0] + and status not in ('Closed','Cancelled', 'Completed') """, + {"to_date": self.future_to_date, "company": self.company}, + )[0] - label = get_link_to_report('Sales Order', label=self.meta.get_label("sales_orders_to_bill"), + label = get_link_to_report( + "Sales Order", + label=self.meta.get_label("sales_orders_to_bill"), report_type="Report Builder", doctype="Sales Order", - filters = { - "status": [['!=', "Closed"], ['!=', "Cancelled"]], - "per_billed": [['<', 100]], - "transaction_date": [['<=', self.future_to_date]], - "company": self.company - } + filters={ + "status": [["!=", "Closed"], ["!=", "Cancelled"]], + "per_billed": [["<", 100]], + "transaction_date": [["<=", self.future_to_date]], + "company": self.company, + }, ) - return { - "label": label, - "value": value, - "count": count - } + return {"label": label, "value": value, "count": count} def get_sales_orders_to_deliver(self): """Get value not delivered""" - value, count = frappe.db.sql("""select ifnull((sum(grand_total)) - (sum(grand_total*per_delivered/100)),0), + value, count = frappe.db.sql( + """select ifnull((sum(grand_total)) - (sum(grand_total*per_delivered/100)),0), count(*) from `tabSales Order` where (transaction_date <= %(to_date)s) and delivery_status != "Fully Delivered" and company = %(company)s - and status not in ('Closed','Cancelled', 'Completed') """, {"to_date": self.future_to_date, "company": self.company})[0] + and status not in ('Closed','Cancelled', 'Completed') """, + {"to_date": self.future_to_date, "company": self.company}, + )[0] - label = get_link_to_report('Sales Order', label=self.meta.get_label("sales_orders_to_deliver"), + label = get_link_to_report( + "Sales Order", + label=self.meta.get_label("sales_orders_to_deliver"), report_type="Report Builder", doctype="Sales Order", - filters = { - "status": [['!=', "Closed"], ['!=', "Cancelled"], ['!=', "Completed"]], - "delivery_status": [['!=', "Fully Delivered"]], - "transaction_date": [['<=', self.future_to_date]], - "company": self.company - } + filters={ + "status": [["!=", "Closed"], ["!=", "Cancelled"], ["!=", "Completed"]], + "delivery_status": [["!=", "Fully Delivered"]], + "transaction_date": [["<=", self.future_to_date]], + "company": self.company, + }, ) - return { - "label": label, - "value": value, - "count": count - } + return {"label": label, "value": value, "count": count} def get_purchase_orders_to_receive(self): """Get value not received""" - value, count = frappe.db.sql("""select ifnull((sum(grand_total))-(sum(grand_total*per_received/100)),0), + value, count = frappe.db.sql( + """select ifnull((sum(grand_total))-(sum(grand_total*per_received/100)),0), count(*) from `tabPurchase Order` where (transaction_date <= %(to_date)s) and per_received < 100 and company = %(company)s - and status not in ('Closed','Cancelled', 'Completed') """, {"to_date": self.future_to_date, "company": self.company})[0] + and status not in ('Closed','Cancelled', 'Completed') """, + {"to_date": self.future_to_date, "company": self.company}, + )[0] - label = get_link_to_report('Purchase Order', label=self.meta.get_label("purchase_orders_to_receive"), + label = get_link_to_report( + "Purchase Order", + label=self.meta.get_label("purchase_orders_to_receive"), report_type="Report Builder", doctype="Purchase Order", - filters = { - "status": [['!=', "Closed"], ['!=', "Cancelled"], ['!=', "Completed"]], - "per_received": [['<', 100]], - "transaction_date": [['<=', self.future_to_date]], - "company": self.company - } + filters={ + "status": [["!=", "Closed"], ["!=", "Cancelled"], ["!=", "Completed"]], + "per_received": [["<", 100]], + "transaction_date": [["<=", self.future_to_date]], + "company": self.company, + }, ) - return { - "label": label, - "value": value, - "count": count - } + return {"label": label, "value": value, "count": count} def get_purchase_orders_to_bill(self): """Get purchase not billed""" - value, count = frappe.db.sql("""select ifnull((sum(grand_total)) - (sum(grand_total*per_billed/100)),0), + value, count = frappe.db.sql( + """select ifnull((sum(grand_total)) - (sum(grand_total*per_billed/100)),0), count(*) from `tabPurchase Order` where (transaction_date <= %(to_date)s) and per_billed < 100 and company = %(company)s - and status not in ('Closed','Cancelled', 'Completed') """, {"to_date": self.future_to_date, "company": self.company})[0] + and status not in ('Closed','Cancelled', 'Completed') """, + {"to_date": self.future_to_date, "company": self.company}, + )[0] - label = get_link_to_report('Purchase Order', label=self.meta.get_label("purchase_orders_to_bill"), + label = get_link_to_report( + "Purchase Order", + label=self.meta.get_label("purchase_orders_to_bill"), report_type="Report Builder", doctype="Purchase Order", - filters = { - "status": [['!=', "Closed"], ['!=', "Cancelled"], ['!=', "Completed"]], - "per_received": [['<', 100]], - "transaction_date": [['<=', self.future_to_date]], - "company": self.company - } + filters={ + "status": [["!=", "Closed"], ["!=", "Cancelled"], ["!=", "Completed"]], + "per_received": [["<", 100]], + "transaction_date": [["<=", self.future_to_date]], + "company": self.company, + }, ) - return { - "label": label, - "value": value, - "count": count - } + return {"label": label, "value": value, "count": count} def get_type_balance(self, fieldname, account_type, root_type=None): if root_type: - accounts = [d.name for d in \ - frappe.db.get_all("Account", filters={"account_type": account_type, - "company": self.company, "is_group": 0, "root_type": root_type})] + accounts = [ + d.name + for d in frappe.db.get_all( + "Account", + filters={ + "account_type": account_type, + "company": self.company, + "is_group": 0, + "root_type": root_type, + }, + ) + ] else: - accounts = [d.name for d in \ - frappe.db.get_all("Account", filters={"account_type": account_type, - "company": self.company, "is_group": 0})] + accounts = [ + d.name + for d in frappe.db.get_all( + "Account", filters={"account_type": account_type, "company": self.company, "is_group": 0} + ) + ] balance = prev_balance = 0.0 count = 0 @@ -525,92 +587,99 @@ class EmailDigest(Document): count += get_count_on(account, fieldname, date=self.future_to_date) prev_balance += get_balance_on(account, date=self.past_to_date, in_account_currency=False) - if fieldname in ("bank_balance","credit_balance"): + if fieldname in ("bank_balance", "credit_balance"): label = "" if fieldname == "bank_balance": filters = { "root_type": "Asset", "account_type": "Bank", "report_date": self.future_to_date, - "company": self.company + "company": self.company, } - label = get_link_to_report('Account Balance', label=self.meta.get_label(fieldname), filters=filters) + label = get_link_to_report( + "Account Balance", label=self.meta.get_label(fieldname), filters=filters + ) else: filters = { "root_type": "Liability", "account_type": "Bank", "report_date": self.future_to_date, - "company": self.company + "company": self.company, } - label = get_link_to_report('Account Balance', label=self.meta.get_label(fieldname), filters=filters) + label = get_link_to_report( + "Account Balance", label=self.meta.get_label(fieldname), filters=filters + ) - return { - 'label': label, - 'value': balance, - 'last_value': prev_balance - } + return {"label": label, "value": balance, "last_value": prev_balance} else: - if account_type == 'Payable': - label = get_link_to_report('Accounts Payable', label=self.meta.get_label(fieldname), - filters={ - "report_date": self.future_to_date, - "company": self.company - } ) - elif account_type == 'Receivable': - label = get_link_to_report('Accounts Receivable', label=self.meta.get_label(fieldname), - filters={ - "report_date": self.future_to_date, - "company": self.company - }) + if account_type == "Payable": + label = get_link_to_report( + "Accounts Payable", + label=self.meta.get_label(fieldname), + filters={"report_date": self.future_to_date, "company": self.company}, + ) + elif account_type == "Receivable": + label = get_link_to_report( + "Accounts Receivable", + label=self.meta.get_label(fieldname), + filters={"report_date": self.future_to_date, "company": self.company}, + ) else: label = self.meta.get_label(fieldname) - return { - 'label': label, - 'value': balance, - 'last_value': prev_balance, - 'count': count - } + return {"label": label, "value": balance, "last_value": prev_balance, "count": count} def get_roots(self, root_type): - return [d.name for d in frappe.db.get_all("Account", - filters={"root_type": root_type.title(), "company": self.company, - "is_group": 1, "parent_account": ["in", ("", None)]})] + return [ + d.name + for d in frappe.db.get_all( + "Account", + filters={ + "root_type": root_type.title(), + "company": self.company, + "is_group": 1, + "parent_account": ["in", ("", None)], + }, + ) + ] def get_root_type_accounts(self, root_type): if not root_type in self._accounts: - self._accounts[root_type] = [d.name for d in \ - frappe.db.get_all("Account", filters={"root_type": root_type.title(), - "company": self.company, "is_group": 0})] + self._accounts[root_type] = [ + d.name + for d in frappe.db.get_all( + "Account", filters={"root_type": root_type.title(), "company": self.company, "is_group": 0} + ) + ] return self._accounts[root_type] def get_purchase_order(self): - return self.get_summary_of_doc("Purchase Order","purchase_order") + return self.get_summary_of_doc("Purchase Order", "purchase_order") def get_sales_order(self): - return self.get_summary_of_doc("Sales Order","sales_order") + return self.get_summary_of_doc("Sales Order", "sales_order") def get_pending_purchase_orders(self): - return self.get_summary_of_pending("Purchase Order","pending_purchase_orders","per_received") + return self.get_summary_of_pending("Purchase Order", "pending_purchase_orders", "per_received") def get_pending_sales_orders(self): - return self.get_summary_of_pending("Sales Order","pending_sales_orders","per_delivered") + return self.get_summary_of_pending("Sales Order", "pending_sales_orders", "per_delivered") def get_sales_invoice(self): - return self.get_summary_of_doc("Sales Invoice","sales_invoice") + return self.get_summary_of_doc("Sales Invoice", "sales_invoice") def get_purchase_invoice(self): - return self.get_summary_of_doc("Purchase Invoice","purchase_invoice") + return self.get_summary_of_doc("Purchase Invoice", "purchase_invoice") def get_new_quotations(self): - return self.get_summary_of_doc("Quotation","new_quotations") + return self.get_summary_of_doc("Quotation", "new_quotations") def get_pending_quotations(self): @@ -618,89 +687,104 @@ class EmailDigest(Document): def get_summary_of_pending(self, doc_type, fieldname, getfield): - value, count, billed_value, delivered_value = frappe.db.sql("""select ifnull(sum(grand_total),0), count(*), + value, count, billed_value, delivered_value = frappe.db.sql( + """select ifnull(sum(grand_total),0), count(*), ifnull(sum(grand_total*per_billed/100),0), ifnull(sum(grand_total*{0}/100),0) from `tab{1}` where (transaction_date <= %(to_date)s) and status not in ('Closed','Cancelled', 'Completed') - and company = %(company)s """.format(getfield, doc_type), - {"to_date": self.future_to_date, "company": self.company})[0] + and company = %(company)s """.format( + getfield, doc_type + ), + {"to_date": self.future_to_date, "company": self.company}, + )[0] return { "label": self.meta.get_label(fieldname), "value": value, "billed_value": billed_value, "delivered_value": delivered_value, - "count": count + "count": count, } def get_summary_of_pending_quotations(self, fieldname): - value, count = frappe.db.sql("""select ifnull(sum(grand_total),0), count(*) from `tabQuotation` + value, count = frappe.db.sql( + """select ifnull(sum(grand_total),0), count(*) from `tabQuotation` where (transaction_date <= %(to_date)s) and company = %(company)s - and status not in ('Ordered','Cancelled', 'Lost') """,{"to_date": self.future_to_date, "company": self.company})[0] + and status not in ('Ordered','Cancelled', 'Lost') """, + {"to_date": self.future_to_date, "company": self.company}, + )[0] - last_value = frappe.db.sql("""select ifnull(sum(grand_total),0) from `tabQuotation` + last_value = frappe.db.sql( + """select ifnull(sum(grand_total),0) from `tabQuotation` where (transaction_date <= %(to_date)s) and company = %(company)s - and status not in ('Ordered','Cancelled', 'Lost') """,{"to_date": self.past_to_date, "company": self.company})[0][0] + and status not in ('Ordered','Cancelled', 'Lost') """, + {"to_date": self.past_to_date, "company": self.company}, + )[0][0] - label = get_link_to_report('Quotation', label=self.meta.get_label(fieldname), + label = get_link_to_report( + "Quotation", + label=self.meta.get_label(fieldname), report_type="Report Builder", doctype="Quotation", - filters = { - "status": [['!=', "Ordered"], ['!=', "Cancelled"], ['!=', "Lost"]], - "per_received": [['<', 100]], - "transaction_date": [['<=', self.future_to_date]], - "company": self.company - } + filters={ + "status": [["!=", "Ordered"], ["!=", "Cancelled"], ["!=", "Lost"]], + "per_received": [["<", 100]], + "transaction_date": [["<=", self.future_to_date]], + "company": self.company, + }, ) - return { - "label": label, - "value": value, - "last_value": last_value, - "count": count - } + return {"label": label, "value": value, "last_value": last_value, "count": count} def get_summary_of_doc(self, doc_type, fieldname): - date_field = 'posting_date' if doc_type in ['Sales Invoice', 'Purchase Invoice'] \ - else 'transaction_date' + date_field = ( + "posting_date" if doc_type in ["Sales Invoice", "Purchase Invoice"] else "transaction_date" + ) - value = flt(self.get_total_on(doc_type, self.future_from_date, self.future_to_date)[0].grand_total) + value = flt( + self.get_total_on(doc_type, self.future_from_date, self.future_to_date)[0].grand_total + ) count = self.get_total_on(doc_type, self.future_from_date, self.future_to_date)[0].count - last_value = flt(self.get_total_on(doc_type, self.past_from_date, self.past_to_date)[0].grand_total) + last_value = flt( + self.get_total_on(doc_type, self.past_from_date, self.past_to_date)[0].grand_total + ) filters = { - date_field: [['>=', self.future_from_date], ['<=', self.future_to_date]], - "status": [['!=','Cancelled']], - "company": self.company + date_field: [[">=", self.future_from_date], ["<=", self.future_to_date]], + "status": [["!=", "Cancelled"]], + "company": self.company, } - label = get_link_to_report(doc_type,label=self.meta.get_label(fieldname), - report_type="Report Builder", filters=filters, doctype=doc_type) + label = get_link_to_report( + doc_type, + label=self.meta.get_label(fieldname), + report_type="Report Builder", + filters=filters, + doctype=doc_type, + ) - return { - "label": label, - "value": value, - "last_value": last_value, - "count": count - } + return {"label": label, "value": value, "last_value": last_value, "count": count} def get_total_on(self, doc_type, from_date, to_date): - date_field = 'posting_date' if doc_type in ['Sales Invoice', 'Purchase Invoice'] \ - else 'transaction_date' + date_field = ( + "posting_date" if doc_type in ["Sales Invoice", "Purchase Invoice"] else "transaction_date" + ) - return frappe.get_all(doc_type, + return frappe.get_all( + doc_type, filters={ - date_field: ['between', (from_date, to_date)], - 'status': ['not in', ('Cancelled')], - 'company': self.company + date_field: ["between", (from_date, to_date)], + "status": ["not in", ("Cancelled")], + "company": self.company, }, - fields=['count(*) as count', 'sum(grand_total) as grand_total']) + fields=["count(*) as count", "sum(grand_total) as grand_total"], + ) def get_from_to_date(self): today = now_datetime().date() @@ -717,7 +801,7 @@ class EmailDigest(Document): to_date = from_date + timedelta(days=6) else: # from date is the 1st day of the previous month - from_date = today - relativedelta(days=today.day-1, months=1) + from_date = today - relativedelta(days=today.day - 1, months=1) # to date is the last day of the previous month to_date = today - relativedelta(days=today.day) @@ -728,7 +812,7 @@ class EmailDigest(Document): # decide from date based on email digest frequency if self.frequency == "Daily": - self.past_from_date = self.past_to_date = self.future_from_date - relativedelta(days = 1) + self.past_from_date = self.past_to_date = self.future_from_date - relativedelta(days=1) elif self.frequency == "Weekly": self.past_from_date = self.future_from_date - relativedelta(weeks=1) @@ -755,27 +839,33 @@ class EmailDigest(Document): def onload(self): self.get_next_sending() - def fmt_money(self, value,absol=True): + def fmt_money(self, value, absol=True): if absol: - return fmt_money(abs(value), currency = self.currency) + return fmt_money(abs(value), currency=self.currency) else: return fmt_money(value, currency=self.currency) def get_purchase_orders_items_overdue_list(self): fields_po = "distinct `tabPurchase Order Item`.parent as po" - fields_poi = "`tabPurchase Order Item`.parent, `tabPurchase Order Item`.schedule_date, item_code," \ - "received_qty, qty - received_qty as missing_qty, rate, amount" + fields_poi = ( + "`tabPurchase Order Item`.parent, `tabPurchase Order Item`.schedule_date, item_code," + "received_qty, qty - received_qty as missing_qty, rate, amount" + ) sql_po = """select {fields} from `tabPurchase Order Item` left join `tabPurchase Order` on `tabPurchase Order`.name = `tabPurchase Order Item`.parent where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and curdate() > `tabPurchase Order Item`.schedule_date and received_qty < qty order by `tabPurchase Order Item`.parent DESC, - `tabPurchase Order Item`.schedule_date DESC""".format(fields=fields_po) + `tabPurchase Order Item`.schedule_date DESC""".format( + fields=fields_po + ) sql_poi = """select {fields} from `tabPurchase Order Item` left join `tabPurchase Order` on `tabPurchase Order`.name = `tabPurchase Order Item`.parent where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and curdate() > `tabPurchase Order Item`.schedule_date - and received_qty < qty order by `tabPurchase Order Item`.idx""".format(fields=fields_poi) + and received_qty < qty order by `tabPurchase Order Item`.idx""".format( + fields=fields_poi + ) purchase_order_list = frappe.db.sql(sql_po, as_dict=True) purchase_order_items_overdue_list = frappe.db.sql(sql_poi, as_dict=True) @@ -785,37 +875,44 @@ class EmailDigest(Document): t.amount = fmt_money(t.amount, 2, t.currency) return purchase_order_list, purchase_order_items_overdue_list + def send(): now_date = now_datetime().date() - for ed in frappe.db.sql("""select name from `tabEmail Digest` - where enabled=1 and docstatus<2""", as_list=1): - ed_obj = frappe.get_doc('Email Digest', ed[0]) - if (now_date == ed_obj.get_next_sending()): + for ed in frappe.db.sql( + """select name from `tabEmail Digest` + where enabled=1 and docstatus<2""", + as_list=1, + ): + ed_obj = frappe.get_doc("Email Digest", ed[0]) + if now_date == ed_obj.get_next_sending(): ed_obj.send() + @frappe.whitelist() def get_digest_msg(name): return frappe.get_doc("Email Digest", name).get_msg_html() + def get_incomes_expenses_for_period(account, from_date, to_date): - """Get amounts for current and past periods""" + """Get amounts for current and past periods""" - val = 0.0 - balance_on_to_date = get_balance_on(account, date = to_date) - balance_before_from_date = get_balance_on(account, date = from_date - timedelta(days=1)) + val = 0.0 + balance_on_to_date = get_balance_on(account, date=to_date) + balance_before_from_date = get_balance_on(account, date=from_date - timedelta(days=1)) - fy_start_date = get_fiscal_year(to_date)[1] + fy_start_date = get_fiscal_year(to_date)[1] - if from_date == fy_start_date: - val = balance_on_to_date - elif from_date > fy_start_date: - val = balance_on_to_date - balance_before_from_date - else: - last_year_closing_balance = get_balance_on(account, date=fy_start_date - timedelta(days=1)) - val = balance_on_to_date + (last_year_closing_balance - balance_before_from_date) + if from_date == fy_start_date: + val = balance_on_to_date + elif from_date > fy_start_date: + val = balance_on_to_date - balance_before_from_date + else: + last_year_closing_balance = get_balance_on(account, date=fy_start_date - timedelta(days=1)) + val = balance_on_to_date + (last_year_closing_balance - balance_before_from_date) + + return val - return val def get_count_for_period(account, fieldname, from_date, to_date): count = 0.0 @@ -833,6 +930,7 @@ def get_count_for_period(account, fieldname, from_date, to_date): return count + def get_future_date_for_calendaer_event(frequency): from_date = to_date = today() diff --git a/erpnext/setup/doctype/email_digest/quotes.py b/erpnext/setup/doctype/email_digest/quotes.py index fbd2d94117..8c077a524c 100644 --- a/erpnext/setup/doctype/email_digest/quotes.py +++ b/erpnext/setup/doctype/email_digest/quotes.py @@ -3,31 +3,63 @@ import random def get_random_quote(): quotes = [ - ("Start by doing what's necessary; then do what's possible; and suddenly you are doing the impossible.", "Francis of Assisi"), - ("The best and most beautiful things in the world cannot be seen or even touched - they must be felt with the heart.", "Hellen Keller"), - ("I can't change the direction of the wind, but I can adjust my sails to always reach my destination.", "Jimmy Dean"), + ( + "Start by doing what's necessary; then do what's possible; and suddenly you are doing the impossible.", + "Francis of Assisi", + ), + ( + "The best and most beautiful things in the world cannot be seen or even touched - they must be felt with the heart.", + "Hellen Keller", + ), + ( + "I can't change the direction of the wind, but I can adjust my sails to always reach my destination.", + "Jimmy Dean", + ), ("We know what we are, but know not what we may be.", "William Shakespeare"), - ("There are only two mistakes one can make along the road to truth; not going all the way, and not starting.", "Buddha"), + ( + "There are only two mistakes one can make along the road to truth; not going all the way, and not starting.", + "Buddha", + ), ("Always remember that you are absolutely unique. Just like everyone else.", "Margaret Mead"), - ("You have to learn the rules of the game. And then you have to play better than anyone else.", "Albert Einstein"), + ( + "You have to learn the rules of the game. And then you have to play better than anyone else.", + "Albert Einstein", + ), ("Once we accept our limits, we go beyond them.", "Albert Einstein"), ("Quality is not an act, it is a habit.", "Aristotle"), - ("The more that you read, the more things you will know. The more that you learn, the more places you'll go.", "Dr. Seuss"), + ( + "The more that you read, the more things you will know. The more that you learn, the more places you'll go.", + "Dr. Seuss", + ), ("From there to here, and here to there, funny things are everywhere.", "Dr. Seuss"), ("The secret of getting ahead is getting started.", "Mark Twain"), ("All generalizations are false, including this one.", "Mark Twain"), ("Don't let schooling interfere with your education.", "Mark Twain"), ("Cauliflower is nothing but cabbage with a college education.", "Mark Twain"), - ("It's not the size of the dog in the fight, it's the size of the fight in the dog.", "Mark Twain"), + ( + "It's not the size of the dog in the fight, it's the size of the fight in the dog.", + "Mark Twain", + ), ("Climate is what we expect, weather is what we get.", "Mark Twain"), ("There are lies, damned lies and statistics.", "Mark Twain"), - ("Happiness is when what you think, what you say, and what you do are in harmony.", "Mahatma Gandhi"), - ("First they ignore you, then they laugh at you, then they fight you, then you win.", "Mahatma Gandhi"), + ( + "Happiness is when what you think, what you say, and what you do are in harmony.", + "Mahatma Gandhi", + ), + ( + "First they ignore you, then they laugh at you, then they fight you, then you win.", + "Mahatma Gandhi", + ), ("There is more to life than increasing its speed.", "Mahatma Gandhi"), - ("A small body of determined spirits fired by an unquenchable faith in their mission can alter the course of history.", "Mahatma Gandhi"), + ( + "A small body of determined spirits fired by an unquenchable faith in their mission can alter the course of history.", + "Mahatma Gandhi", + ), ("If two wrongs don't make a right, try three.", "Laurence J. Peter"), ("Inspiration exists, but it has to find you working.", "Pablo Picasso"), - ("The world’s first speeding ticket was given to a man going 4 times the speed limit! Walter Arnold was traveling at a breakneck 8 miles an hour in a 2mph zone, and was caught by a policeman on bicycle and fined one shilling!"), + ( + "The world’s first speeding ticket was given to a man going 4 times the speed limit! Walter Arnold was traveling at a breakneck 8 miles an hour in a 2mph zone, and was caught by a policeman on bicycle and fined one shilling!" + ), ] return random.choice(quotes) diff --git a/erpnext/setup/doctype/email_digest/test_email_digest.py b/erpnext/setup/doctype/email_digest/test_email_digest.py index 3fdf168a65..dae28b81b5 100644 --- a/erpnext/setup/doctype/email_digest/test_email_digest.py +++ b/erpnext/setup/doctype/email_digest/test_email_digest.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Email Digest') + class TestEmailDigest(unittest.TestCase): pass diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.py b/erpnext/setup/doctype/global_defaults/global_defaults.py index f0b720a42e..984bab4729 100644 --- a/erpnext/setup/doctype/global_defaults/global_defaults.py +++ b/erpnext/setup/doctype/global_defaults/global_defaults.py @@ -11,35 +11,37 @@ from frappe.utils import cint keydict = { # "key in defaults": "key in Global Defaults" "fiscal_year": "current_fiscal_year", - 'company': 'default_company', - 'currency': 'default_currency', + "company": "default_company", + "currency": "default_currency", "country": "country", - 'hide_currency_symbol':'hide_currency_symbol', - 'account_url':'account_url', - 'disable_rounded_total': 'disable_rounded_total', - 'disable_in_words': 'disable_in_words', + "hide_currency_symbol": "hide_currency_symbol", + "account_url": "account_url", + "disable_rounded_total": "disable_rounded_total", + "disable_in_words": "disable_in_words", } from frappe.model.document import Document class GlobalDefaults(Document): - def on_update(self): """update defaults""" for key in keydict: - frappe.db.set_default(key, self.get(keydict[key], '')) + frappe.db.set_default(key, self.get(keydict[key], "")) # update year start date and year end date from fiscal_year - year_start_end_date = frappe.db.sql("""select year_start_date, year_end_date - from `tabFiscal Year` where name=%s""", self.current_fiscal_year) + year_start_end_date = frappe.db.sql( + """select year_start_date, year_end_date + from `tabFiscal Year` where name=%s""", + self.current_fiscal_year, + ) if year_start_end_date: - ysd = year_start_end_date[0][0] or '' - yed = year_start_end_date[0][1] or '' + ysd = year_start_end_date[0][0] or "" + yed = year_start_end_date[0][1] or "" if ysd and yed: - frappe.db.set_default('year_start_date', ysd.strftime('%Y-%m-%d')) - frappe.db.set_default('year_end_date', yed.strftime('%Y-%m-%d')) + frappe.db.set_default("year_start_date", ysd.strftime("%Y-%m-%d")) + frappe.db.set_default("year_end_date", yed.strftime("%Y-%m-%d")) # enable default currency if self.default_currency: @@ -59,21 +61,81 @@ class GlobalDefaults(Document): self.disable_rounded_total = cint(self.disable_rounded_total) # Make property setters to hide rounded total fields - for doctype in ("Quotation", "Sales Order", "Sales Invoice", "Delivery Note", - "Supplier Quotation", "Purchase Order", "Purchase Invoice", "Purchase Receipt"): - make_property_setter(doctype, "base_rounded_total", "hidden", self.disable_rounded_total, "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, "base_rounded_total", "print_hide", 1, "Check", validate_fields_for_doctype=False) + for doctype in ( + "Quotation", + "Sales Order", + "Sales Invoice", + "Delivery Note", + "Supplier Quotation", + "Purchase Order", + "Purchase Invoice", + "Purchase Receipt", + ): + make_property_setter( + doctype, + "base_rounded_total", + "hidden", + self.disable_rounded_total, + "Check", + validate_fields_for_doctype=False, + ) + make_property_setter( + doctype, "base_rounded_total", "print_hide", 1, "Check", validate_fields_for_doctype=False + ) - make_property_setter(doctype, "rounded_total", "hidden", self.disable_rounded_total, "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, "rounded_total", "print_hide", self.disable_rounded_total, "Check", validate_fields_for_doctype=False) + make_property_setter( + doctype, + "rounded_total", + "hidden", + self.disable_rounded_total, + "Check", + validate_fields_for_doctype=False, + ) + make_property_setter( + doctype, + "rounded_total", + "print_hide", + self.disable_rounded_total, + "Check", + validate_fields_for_doctype=False, + ) - make_property_setter(doctype, "disable_rounded_total", "default", cint(self.disable_rounded_total), "Text", validate_fields_for_doctype=False) + make_property_setter( + doctype, + "disable_rounded_total", + "default", + cint(self.disable_rounded_total), + "Text", + validate_fields_for_doctype=False, + ) def toggle_in_words(self): self.disable_in_words = cint(self.disable_in_words) # Make property setters to hide in words fields - for doctype in ("Quotation", "Sales Order", "Sales Invoice", "Delivery Note", - "Supplier Quotation", "Purchase Order", "Purchase Invoice", "Purchase Receipt"): - make_property_setter(doctype, "in_words", "hidden", self.disable_in_words, "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, "in_words", "print_hide", self.disable_in_words, "Check", validate_fields_for_doctype=False) + for doctype in ( + "Quotation", + "Sales Order", + "Sales Invoice", + "Delivery Note", + "Supplier Quotation", + "Purchase Order", + "Purchase Invoice", + "Purchase Receipt", + ): + make_property_setter( + doctype, + "in_words", + "hidden", + self.disable_in_words, + "Check", + validate_fields_for_doctype=False, + ) + make_property_setter( + doctype, + "in_words", + "print_hide", + self.disable_in_words, + "Check", + validate_fields_for_doctype=False, + ) diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 2c53246c62..35557a5047 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -15,12 +15,12 @@ from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder class ItemGroup(NestedSet, WebsiteGenerator): - nsm_parent_field = 'parent_item_group' + nsm_parent_field = "parent_item_group" website = frappe._dict( - condition_field = "show_in_website", - template = "templates/generators/item_group.html", - no_cache = 1, - no_breadcrumbs = 1 + condition_field="show_in_website", + template="templates/generators/item_group.html", + no_cache=1, + no_breadcrumbs=1, ) def autoname(self): @@ -30,8 +30,8 @@ class ItemGroup(NestedSet, WebsiteGenerator): super(ItemGroup, self).validate() if not self.parent_item_group and not frappe.flags.in_test: - if frappe.db.exists("Item Group", _('All Item Groups')): - self.parent_item_group = _('All Item Groups') + if frappe.db.exists("Item Group", _("All Item Groups")): + self.parent_item_group = _("All Item Groups") self.make_route() self.validate_item_group_defaults() @@ -43,15 +43,15 @@ class ItemGroup(NestedSet, WebsiteGenerator): self.delete_child_item_groups_key() def make_route(self): - '''Make website route''' + """Make website route""" if not self.route: - self.route = '' + self.route = "" if self.parent_item_group: - parent_item_group = frappe.get_doc('Item Group', self.parent_item_group) + parent_item_group = frappe.get_doc("Item Group", self.parent_item_group) # make parent route only if not root if parent_item_group.parent_item_group and parent_item_group.route: - self.route = parent_item_group.route + '/' + self.route = parent_item_group.route + "/" self.route += self.scrub(self.item_group_name) @@ -65,28 +65,22 @@ class ItemGroup(NestedSet, WebsiteGenerator): def get_context(self, context): context.show_search = True context.body_class = "product-page" - context.page_length = cint(frappe.db.get_single_value('E Commerce Settings', 'products_per_page')) or 6 - context.search_link = '/product_search' + context.page_length = ( + cint(frappe.db.get_single_value("E Commerce Settings", "products_per_page")) or 6 + ) + context.search_link = "/product_search" filter_engine = ProductFiltersBuilder(self.name) context.field_filters = filter_engine.get_field_filters() context.attribute_filters = filter_engine.get_attribute_filters() - context.update({ - "parents": get_parent_item_groups(self.parent_item_group), - "title": self.name - }) + context.update({"parents": get_parent_item_groups(self.parent_item_group), "title": self.name}) if self.slideshow: - values = { - 'show_indicators': 1, - 'show_controls': 0, - 'rounded': 1, - 'slider_name': self.slideshow - } + values = {"show_indicators": 1, "show_controls": 0, "rounded": 1, "slider_name": self.slideshow} slideshow = frappe.get_doc("Website Slideshow", self.slideshow) - slides = slideshow.get({"doctype":"Website Slideshow Item"}) + slides = slideshow.get({"doctype": "Website Slideshow Item"}) for index, slide in enumerate(slides): values[f"slide_{index + 1}_image"] = slide.image values[f"slide_{index + 1}_title"] = slide.heading @@ -109,68 +103,64 @@ class ItemGroup(NestedSet, WebsiteGenerator): def validate_item_group_defaults(self): from erpnext.stock.doctype.item.item import validate_item_default_company_links + validate_item_default_company_links(self.item_group_defaults) + def get_child_groups_for_website(item_group_name, immediate=False, include_self=False): """Returns child item groups *excluding* passed group.""" item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1) - filters = { - "lft": [">", item_group.lft], - "rgt": ["<", item_group.rgt], - "show_in_website": 1 - } + filters = {"lft": [">", item_group.lft], "rgt": ["<", item_group.rgt], "show_in_website": 1} if immediate: filters["parent_item_group"] = item_group_name if include_self: - filters.update({ - "lft": [">=", item_group.lft], - "rgt": ["<=", item_group.rgt] - }) + filters.update({"lft": [">=", item_group.lft], "rgt": ["<=", item_group.rgt]}) + + return frappe.get_all("Item Group", filters=filters, fields=["name", "route"], order_by="name") - return frappe.get_all( - "Item Group", - filters=filters, - fields=["name", "route"], - order_by="name" - ) def get_child_item_groups(item_group_name): - item_group = frappe.get_cached_value("Item Group", - item_group_name, ["lft", "rgt"], as_dict=1) + item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1) - child_item_groups = [d.name for d in frappe.get_all('Item Group', - filters= {'lft': ('>=', item_group.lft),'rgt': ('<=', item_group.rgt)})] + child_item_groups = [ + d.name + for d in frappe.get_all( + "Item Group", filters={"lft": (">=", item_group.lft), "rgt": ("<=", item_group.rgt)} + ) + ] return child_item_groups or {} + def get_item_for_list_in_html(context): # add missing absolute link in files # user may forget it during upload if (context.get("website_image") or "").startswith("files/"): context["website_image"] = "/" + quote(context["website_image"]) - context["show_availability_status"] = cint(frappe.db.get_single_value('E Commerce Settings', - 'show_availability_status')) + context["show_availability_status"] = cint( + frappe.db.get_single_value("E Commerce Settings", "show_availability_status") + ) - products_template = 'templates/includes/products_as_list.html' + products_template = "templates/includes/products_as_list.html" return frappe.get_template(products_template).render(context) def get_parent_item_groups(item_group_name, from_item=False): - base_nav_page = {"name": _("Shop by Category"), "route":"/shop-by-category"} + base_nav_page = {"name": _("Shop by Category"), "route": "/shop-by-category"} if from_item and frappe.request.environ.get("HTTP_REFERER"): # base page after 'Home' will vary on Item page - last_page = frappe.request.environ["HTTP_REFERER"].split('/')[-1] + last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1] if last_page and last_page in ("shop-by-category", "all-products"): base_nav_page_title = " ".join(last_page.split("-")).title() - base_nav_page = {"name": _(base_nav_page_title), "route":"/"+last_page} + base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page} base_parents = [ - {"name": _("Home"), "route":"/"}, + {"name": _("Home"), "route": "/"}, base_nav_page, ] @@ -178,21 +168,27 @@ def get_parent_item_groups(item_group_name, from_item=False): return base_parents item_group = frappe.db.get_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1) - parent_groups = frappe.db.sql("""select name, route from `tabItem Group` + parent_groups = frappe.db.sql( + """select name, route from `tabItem Group` where lft <= %s and rgt >= %s and show_in_website=1 - order by lft asc""", (item_group.lft, item_group.rgt), as_dict=True) + order by lft asc""", + (item_group.lft, item_group.rgt), + as_dict=True, + ) return base_parents + parent_groups + def invalidate_cache_for(doc, item_group=None): if not item_group: item_group = doc.name for d in get_parent_item_groups(item_group): - item_group_name = frappe.db.get_value("Item Group", d.get('name')) + item_group_name = frappe.db.get_value("Item Group", d.get("name")) if item_group_name: - clear_cache(frappe.db.get_value('Item Group', item_group_name, 'route')) + clear_cache(frappe.db.get_value("Item Group", item_group_name, "route")) + def get_item_group_defaults(item, company): item = frappe.get_cached_doc("Item", item) diff --git a/erpnext/setup/doctype/item_group/test_item_group.py b/erpnext/setup/doctype/item_group/test_item_group.py index f6e9ed4ce5..11bc9b92c1 100644 --- a/erpnext/setup/doctype/item_group/test_item_group.py +++ b/erpnext/setup/doctype/item_group/test_item_group.py @@ -14,7 +14,8 @@ from frappe.utils.nestedset import ( rebuild_tree, ) -test_records = frappe.get_test_records('Item Group') +test_records = frappe.get_test_records("Item Group") + class TestItem(unittest.TestCase): def test_basic_tree(self, records=None): @@ -25,12 +26,12 @@ class TestItem(unittest.TestCase): records = test_records[2:] for item_group in records: - lft, rgt, parent_item_group = frappe.db.get_value("Item Group", item_group["item_group_name"], - ["lft", "rgt", "parent_item_group"]) + lft, rgt, parent_item_group = frappe.db.get_value( + "Item Group", item_group["item_group_name"], ["lft", "rgt", "parent_item_group"] + ) if parent_item_group: - parent_lft, parent_rgt = frappe.db.get_value("Item Group", parent_item_group, - ["lft", "rgt"]) + parent_lft, parent_rgt = frappe.db.get_value("Item Group", parent_item_group, ["lft", "rgt"]) else: # root parent_lft = min_lft - 1 @@ -55,8 +56,11 @@ class TestItem(unittest.TestCase): def get_no_of_children(item_groups, no_of_children): children = [] for ig in item_groups: - children += frappe.db.sql_list("""select name from `tabItem Group` - where ifnull(parent_item_group, '')=%s""", ig or '') + children += frappe.db.sql_list( + """select name from `tabItem Group` + where ifnull(parent_item_group, '')=%s""", + ig or "", + ) if len(children): return get_no_of_children(children, no_of_children + len(children)) @@ -119,7 +123,10 @@ class TestItem(unittest.TestCase): def print_tree(self): import json - print(json.dumps(frappe.db.sql("select name, lft, rgt from `tabItem Group` order by lft"), indent=1)) + + print( + json.dumps(frappe.db.sql("select name, lft, rgt from `tabItem Group` order by lft"), indent=1) + ) def test_move_leaf_into_another_group(self): # before move @@ -149,12 +156,20 @@ class TestItem(unittest.TestCase): def test_delete_leaf(self): # for checking later - parent_item_group = frappe.db.get_value("Item Group", "_Test Item Group B - 3", "parent_item_group") + parent_item_group = frappe.db.get_value( + "Item Group", "_Test Item Group B - 3", "parent_item_group" + ) rgt = frappe.db.get_value("Item Group", parent_item_group, "rgt") ancestors = get_ancestors_of("Item Group", "_Test Item Group B - 3") - ancestors = frappe.db.sql("""select name, rgt from `tabItem Group` - where name in ({})""".format(", ".join(["%s"]*len(ancestors))), tuple(ancestors), as_dict=True) + ancestors = frappe.db.sql( + """select name, rgt from `tabItem Group` + where name in ({})""".format( + ", ".join(["%s"] * len(ancestors)) + ), + tuple(ancestors), + as_dict=True, + ) frappe.delete_doc("Item Group", "_Test Item Group B - 3") records_to_test = test_records[2:] @@ -173,7 +188,9 @@ class TestItem(unittest.TestCase): def test_delete_group(self): # cannot delete group with child, but can delete leaf - self.assertRaises(NestedSetChildExistsError, frappe.delete_doc, "Item Group", "_Test Item Group B") + self.assertRaises( + NestedSetChildExistsError, frappe.delete_doc, "Item Group", "_Test Item Group B" + ) def test_merge_groups(self): frappe.rename_doc("Item Group", "_Test Item Group B", "_Test Item Group C", merge=True) @@ -186,8 +203,10 @@ class TestItem(unittest.TestCase): self.test_basic_tree() # move its children back - for name in frappe.db.sql_list("""select name from `tabItem Group` - where parent_item_group='_Test Item Group C'"""): + for name in frappe.db.sql_list( + """select name from `tabItem Group` + where parent_item_group='_Test Item Group C'""" + ): doc = frappe.get_doc("Item Group", name) doc.parent_item_group = "_Test Item Group B" @@ -206,9 +225,21 @@ class TestItem(unittest.TestCase): self.test_basic_tree() def test_merge_leaf_into_group(self): - self.assertRaises(NestedSetInvalidMergeError, frappe.rename_doc, "Item Group", "_Test Item Group B - 3", - "_Test Item Group B", merge=True) + self.assertRaises( + NestedSetInvalidMergeError, + frappe.rename_doc, + "Item Group", + "_Test Item Group B - 3", + "_Test Item Group B", + merge=True, + ) def test_merge_group_into_leaf(self): - self.assertRaises(NestedSetInvalidMergeError, frappe.rename_doc, "Item Group", "_Test Item Group B", - "_Test Item Group B - 3", merge=True) + self.assertRaises( + NestedSetInvalidMergeError, + frappe.rename_doc, + "Item Group", + "_Test Item Group B", + "_Test Item Group B - 3", + merge=True, + ) diff --git a/erpnext/setup/doctype/naming_series/naming_series.py b/erpnext/setup/doctype/naming_series/naming_series.py index 986b4e87ff..4fba776cb5 100644 --- a/erpnext/setup/doctype/naming_series/naming_series.py +++ b/erpnext/setup/doctype/naming_series/naming_series.py @@ -11,15 +11,25 @@ from frappe.permissions import get_doctypes_with_read from frappe.utils import cint, cstr -class NamingSeriesNotSetError(frappe.ValidationError): pass +class NamingSeriesNotSetError(frappe.ValidationError): + pass + class NamingSeries(Document): @frappe.whitelist() def get_transactions(self, arg=None): - doctypes = list(set(frappe.db.sql_list("""select parent - from `tabDocField` df where fieldname='naming_series'""") - + frappe.db.sql_list("""select dt from `tabCustom Field` - where fieldname='naming_series'"""))) + doctypes = list( + set( + frappe.db.sql_list( + """select parent + from `tabDocField` df where fieldname='naming_series'""" + ) + + frappe.db.sql_list( + """select dt from `tabCustom Field` + where fieldname='naming_series'""" + ) + ) + ) doctypes = list(set(get_doctypes_with_read()).intersection(set(doctypes))) prefixes = "" @@ -28,8 +38,8 @@ class NamingSeries(Document): try: options = self.get_options(d) except frappe.DoesNotExistError: - frappe.msgprint(_('Unable to find DocType {0}').format(d)) - #frappe.pass_does_not_exist_error() + frappe.msgprint(_("Unable to find DocType {0}").format(d)) + # frappe.pass_does_not_exist_error() continue if options: @@ -37,17 +47,21 @@ class NamingSeries(Document): prefixes.replace("\n\n", "\n") prefixes = prefixes.split("\n") - custom_prefixes = frappe.get_all('DocType', fields=["autoname"], - filters={"name": ('not in', doctypes), "autoname":('like', '%.#%'), 'module': ('not in', ['Core'])}) + custom_prefixes = frappe.get_all( + "DocType", + fields=["autoname"], + filters={ + "name": ("not in", doctypes), + "autoname": ("like", "%.#%"), + "module": ("not in", ["Core"]), + }, + ) if custom_prefixes: - prefixes = prefixes + [d.autoname.rsplit('.', 1)[0] for d in custom_prefixes] + prefixes = prefixes + [d.autoname.rsplit(".", 1)[0] for d in custom_prefixes] prefixes = "\n".join(sorted(prefixes)) - return { - "transactions": "\n".join([''] + sorted(doctypes)), - "prefixes": prefixes - } + return {"transactions": "\n".join([""] + sorted(doctypes)), "prefixes": prefixes} def scrub_options_list(self, ol): options = list(filter(lambda x: x, [cstr(n).strip() for n in ol])) @@ -64,7 +78,7 @@ class NamingSeries(Document): self.set_series_for(self.select_doc_for_series, series_list) # create series - map(self.insert_series, [d.split('.')[0] for d in series_list if d.strip()]) + map(self.insert_series, [d.split(".")[0] for d in series_list if d.strip()]) msgprint(_("Series Updated")) @@ -82,32 +96,35 @@ class NamingSeries(Document): self.validate_series_name(i) if options and self.user_must_always_select: - options = [''] + options + options = [""] + options - default = options[0] if options else '' + default = options[0] if options else "" # update in property setter - prop_dict = {'options': "\n".join(options), 'default': default} + prop_dict = {"options": "\n".join(options), "default": default} for prop in prop_dict: - ps_exists = frappe.db.get_value("Property Setter", - {"field_name": 'naming_series', 'doc_type': doctype, 'property': prop}) + ps_exists = frappe.db.get_value( + "Property Setter", {"field_name": "naming_series", "doc_type": doctype, "property": prop} + ) if ps_exists: - ps = frappe.get_doc('Property Setter', ps_exists) + ps = frappe.get_doc("Property Setter", ps_exists) ps.value = prop_dict[prop] ps.save() else: - ps = frappe.get_doc({ - 'doctype': 'Property Setter', - 'doctype_or_field': 'DocField', - 'doc_type': doctype, - 'field_name': 'naming_series', - 'property': prop, - 'value': prop_dict[prop], - 'property_type': 'Text', - '__islocal': 1 - }) + ps = frappe.get_doc( + { + "doctype": "Property Setter", + "doctype_or_field": "DocField", + "doc_type": doctype, + "field_name": "naming_series", + "property": prop, + "value": prop_dict[prop], + "property_type": "Text", + "__islocal": 1, + } + ) ps.save() self.set_options = "\n".join(options) @@ -115,16 +132,22 @@ class NamingSeries(Document): frappe.clear_cache(doctype=doctype) def check_duplicate(self): - parent = list(set( - frappe.db.sql_list("""select dt.name + parent = list( + set( + frappe.db.sql_list( + """select dt.name from `tabDocField` df, `tabDocType` dt where dt.name = df.parent and df.fieldname='naming_series' and dt.name != %s""", - self.select_doc_for_series) - + frappe.db.sql_list("""select dt.name + self.select_doc_for_series, + ) + + frappe.db.sql_list( + """select dt.name from `tabCustom Field` df, `tabDocType` dt where dt.name = df.dt and df.fieldname='naming_series' and dt.name != %s""", - self.select_doc_for_series) - )) + self.select_doc_for_series, + ) + ) + ) sr = [[frappe.get_meta(p).get_field("naming_series").options, p] for p in parent] dt = frappe.get_doc("DocType", self.select_doc_for_series) options = self.scrub_options_list(self.set_options.split("\n")) @@ -132,14 +155,17 @@ class NamingSeries(Document): validate_series(dt, series) for i in sr: if i[0]: - existing_series = [d.split('.')[0] for d in i[0].split("\n")] + existing_series = [d.split(".")[0] for d in i[0].split("\n")] if series.split(".")[0] in existing_series: - frappe.throw(_("Series {0} already used in {1}").format(series,i[1])) + frappe.throw(_("Series {0} already used in {1}").format(series, i[1])) def validate_series_name(self, n): import re + if not re.match(r"^[\w\- \/.#{}]+$", n, re.UNICODE): - throw(_('Special Characters except "-", "#", ".", "/", "{" and "}" not allowed in naming series')) + throw( + _('Special Characters except "-", "#", ".", "/", "{" and "}" not allowed in naming series') + ) @frappe.whitelist() def get_options(self, arg=None): @@ -151,12 +177,11 @@ class NamingSeries(Document): """get series current""" if self.prefix: prefix = self.parse_naming_series() - self.current_value = frappe.db.get_value("Series", - prefix, "current", order_by = "name") + self.current_value = frappe.db.get_value("Series", prefix, "current", order_by="name") def insert_series(self, series): """insert series if missing""" - if frappe.db.get_value('Series', series, 'name', order_by="name") == None: + if frappe.db.get_value("Series", series, "name", order_by="name") == None: frappe.db.sql("insert into tabSeries (name, current) values (%s, 0)", (series)) @frappe.whitelist() @@ -164,14 +189,15 @@ class NamingSeries(Document): if self.prefix: prefix = self.parse_naming_series() self.insert_series(prefix) - frappe.db.sql("update `tabSeries` set current = %s where name = %s", - (cint(self.current_value), prefix)) + frappe.db.sql( + "update `tabSeries` set current = %s where name = %s", (cint(self.current_value), prefix) + ) msgprint(_("Series Updated Successfully")) else: msgprint(_("Please select prefix first")) def parse_naming_series(self): - parts = self.prefix.split('.') + parts = self.prefix.split(".") # Remove ### from the end of series if parts[-1] == "#" * len(parts[-1]): @@ -180,34 +206,59 @@ class NamingSeries(Document): prefix = parse_naming_series(parts) return prefix -def set_by_naming_series(doctype, fieldname, naming_series, hide_name_field=True, make_mandatory=1): + +def set_by_naming_series( + doctype, fieldname, naming_series, hide_name_field=True, make_mandatory=1 +): from frappe.custom.doctype.property_setter.property_setter import make_property_setter + if naming_series: - make_property_setter(doctype, "naming_series", "hidden", 0, "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, "naming_series", "reqd", make_mandatory, "Check", validate_fields_for_doctype=False) + make_property_setter( + doctype, "naming_series", "hidden", 0, "Check", validate_fields_for_doctype=False + ) + make_property_setter( + doctype, "naming_series", "reqd", make_mandatory, "Check", validate_fields_for_doctype=False + ) # set values for mandatory try: - frappe.db.sql("""update `tab{doctype}` set naming_series={s} where - ifnull(naming_series, '')=''""".format(doctype=doctype, s="%s"), - get_default_naming_series(doctype)) + frappe.db.sql( + """update `tab{doctype}` set naming_series={s} where + ifnull(naming_series, '')=''""".format( + doctype=doctype, s="%s" + ), + get_default_naming_series(doctype), + ) except NamingSeriesNotSetError: pass if hide_name_field: make_property_setter(doctype, fieldname, "reqd", 0, "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, fieldname, "hidden", 1, "Check", validate_fields_for_doctype=False) + make_property_setter( + doctype, fieldname, "hidden", 1, "Check", validate_fields_for_doctype=False + ) else: - make_property_setter(doctype, "naming_series", "reqd", 0, "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, "naming_series", "hidden", 1, "Check", validate_fields_for_doctype=False) + make_property_setter( + doctype, "naming_series", "reqd", 0, "Check", validate_fields_for_doctype=False + ) + make_property_setter( + doctype, "naming_series", "hidden", 1, "Check", validate_fields_for_doctype=False + ) if hide_name_field: - make_property_setter(doctype, fieldname, "hidden", 0, "Check", validate_fields_for_doctype=False) + make_property_setter( + doctype, fieldname, "hidden", 0, "Check", validate_fields_for_doctype=False + ) make_property_setter(doctype, fieldname, "reqd", 1, "Check", validate_fields_for_doctype=False) # set values for mandatory - frappe.db.sql("""update `tab{doctype}` set `{fieldname}`=`name` where - ifnull({fieldname}, '')=''""".format(doctype=doctype, fieldname=fieldname)) + frappe.db.sql( + """update `tab{doctype}` set `{fieldname}`=`name` where + ifnull({fieldname}, '')=''""".format( + doctype=doctype, fieldname=fieldname + ) + ) + def get_default_naming_series(doctype): naming_series = frappe.get_meta(doctype).get_field("naming_series").options or "" @@ -215,7 +266,9 @@ def get_default_naming_series(doctype): out = naming_series[0] or (naming_series[1] if len(naming_series) > 1 else None) if not out: - frappe.throw(_("Please set Naming Series for {0} via Setup > Settings > Naming Series").format(doctype), - NamingSeriesNotSetError) + frappe.throw( + _("Please set Naming Series for {0} via Setup > Settings > Naming Series").format(doctype), + NamingSeriesNotSetError, + ) else: return out diff --git a/erpnext/setup/doctype/party_type/party_type.py b/erpnext/setup/doctype/party_type/party_type.py index d0d2946e94..d07ab08450 100644 --- a/erpnext/setup/doctype/party_type/party_type.py +++ b/erpnext/setup/doctype/party_type/party_type.py @@ -9,18 +9,20 @@ from frappe.model.document import Document class PartyType(Document): pass + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_party_type(doctype, txt, searchfield, start, page_len, filters): - cond = '' - if filters and filters.get('account'): - account_type = frappe.db.get_value('Account', filters.get('account'), 'account_type') + cond = "" + if filters and filters.get("account"): + account_type = frappe.db.get_value("Account", filters.get("account"), "account_type") cond = "and account_type = '%s'" % account_type - return frappe.db.sql("""select name from `tabParty Type` + return frappe.db.sql( + """select name from `tabParty Type` where `{key}` LIKE %(txt)s {cond} - order by name limit %(start)s, %(page_len)s""" - .format(key=searchfield, cond=cond), { - 'txt': '%' + txt + '%', - 'start': start, 'page_len': page_len - }) + order by name limit %(start)s, %(page_len)s""".format( + key=searchfield, cond=cond + ), + {"txt": "%" + txt + "%", "start": start, "page_len": page_len}, + ) diff --git a/erpnext/setup/doctype/party_type/test_party_type.py b/erpnext/setup/doctype/party_type/test_party_type.py index a9a3db8777..ab92ee15fc 100644 --- a/erpnext/setup/doctype/party_type/test_party_type.py +++ b/erpnext/setup/doctype/party_type/test_party_type.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Party Type') + class TestPartyType(unittest.TestCase): pass diff --git a/erpnext/setup/doctype/print_heading/test_print_heading.py b/erpnext/setup/doctype/print_heading/test_print_heading.py index 04de08d269..f0e4c763c4 100644 --- a/erpnext/setup/doctype/print_heading/test_print_heading.py +++ b/erpnext/setup/doctype/print_heading/test_print_heading.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Print Heading') +test_records = frappe.get_test_records("Print Heading") diff --git a/erpnext/setup/doctype/quotation_lost_reason/test_quotation_lost_reason.py b/erpnext/setup/doctype/quotation_lost_reason/test_quotation_lost_reason.py index 9330ba8587..891864a69e 100644 --- a/erpnext/setup/doctype/quotation_lost_reason/test_quotation_lost_reason.py +++ b/erpnext/setup/doctype/quotation_lost_reason/test_quotation_lost_reason.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Quotation Lost Reason') +test_records = frappe.get_test_records("Quotation Lost Reason") diff --git a/erpnext/setup/doctype/sales_partner/sales_partner.py b/erpnext/setup/doctype/sales_partner/sales_partner.py index d2ec49dd6c..c3136715fe 100644 --- a/erpnext/setup/doctype/sales_partner/sales_partner.py +++ b/erpnext/setup/doctype/sales_partner/sales_partner.py @@ -10,9 +10,9 @@ from frappe.website.website_generator import WebsiteGenerator class SalesPartner(WebsiteGenerator): website = frappe._dict( - page_title_field = "partner_name", - condition_field = "show_in_website", - template = "templates/generators/sales_partner.html" + page_title_field="partner_name", + condition_field="show_in_website", + template="templates/generators/sales_partner.html", ) def onload(self): @@ -30,18 +30,25 @@ class SalesPartner(WebsiteGenerator): self.partner_website = "http://" + self.partner_website def get_context(self, context): - address = frappe.db.get_value("Address", - {"sales_partner": self.name, "is_primary_address": 1}, - "*", as_dict=True) + address = frappe.db.get_value( + "Address", {"sales_partner": self.name, "is_primary_address": 1}, "*", as_dict=True + ) if address: city_state = ", ".join(filter(None, [address.city, address.state])) - address_rows = [address.address_line1, address.address_line2, - city_state, address.pincode, address.country] + address_rows = [ + address.address_line1, + address.address_line2, + city_state, + address.pincode, + address.country, + ] - context.update({ - "email": address.email_id, - "partner_address": filter_strip_join(address_rows, "\n
    "), - "phone": filter_strip_join(cstr(address.phone).split(","), "\n
    ") - }) + context.update( + { + "email": address.email_id, + "partner_address": filter_strip_join(address_rows, "\n
    "), + "phone": filter_strip_join(cstr(address.phone).split(","), "\n
    "), + } + ) return context diff --git a/erpnext/setup/doctype/sales_partner/test_sales_partner.py b/erpnext/setup/doctype/sales_partner/test_sales_partner.py index 80ef368014..933f68da5b 100644 --- a/erpnext/setup/doctype/sales_partner/test_sales_partner.py +++ b/erpnext/setup/doctype/sales_partner/test_sales_partner.py @@ -3,6 +3,6 @@ import frappe -test_records = frappe.get_test_records('Sales Partner') +test_records = frappe.get_test_records("Sales Partner") test_ignore = ["Item Group"] diff --git a/erpnext/setup/doctype/sales_person/sales_person.py b/erpnext/setup/doctype/sales_person/sales_person.py index 6af1b312bd..0082c70075 100644 --- a/erpnext/setup/doctype/sales_person/sales_person.py +++ b/erpnext/setup/doctype/sales_person/sales_person.py @@ -11,13 +11,13 @@ from erpnext import get_default_currency class SalesPerson(NestedSet): - nsm_parent_field = 'parent_sales_person' + nsm_parent_field = "parent_sales_person" def validate(self): if not self.parent_sales_person: self.parent_sales_person = get_root_of("Sales Person") - for d in self.get('targets') or []: + for d in self.get("targets") or []: if not flt(d.target_qty) and not flt(d.target_amount): frappe.throw(_("Either target qty or target amount is mandatory.")) self.validate_employee_id() @@ -28,20 +28,28 @@ class SalesPerson(NestedSet): def load_dashboard_info(self): company_default_currency = get_default_currency() - allocated_amount_against_order = flt(frappe.db.get_value('Sales Team', - {'docstatus': 1, 'parenttype': 'Sales Order', 'sales_person': self.sales_person_name}, - 'sum(allocated_amount)')) + allocated_amount_against_order = flt( + frappe.db.get_value( + "Sales Team", + {"docstatus": 1, "parenttype": "Sales Order", "sales_person": self.sales_person_name}, + "sum(allocated_amount)", + ) + ) - allocated_amount_against_invoice = flt(frappe.db.get_value('Sales Team', - {'docstatus': 1, 'parenttype': 'Sales Invoice', 'sales_person': self.sales_person_name}, - 'sum(allocated_amount)')) + allocated_amount_against_invoice = flt( + frappe.db.get_value( + "Sales Team", + {"docstatus": 1, "parenttype": "Sales Invoice", "sales_person": self.sales_person_name}, + "sum(allocated_amount)", + ) + ) info = {} info["allocated_amount_against_order"] = allocated_amount_against_order info["allocated_amount_against_invoice"] = allocated_amount_against_invoice info["currency"] = company_default_currency - self.set_onload('dashboard_info', info) + self.set_onload("dashboard_info", info) def on_update(self): super(SalesPerson, self).on_update() @@ -60,30 +68,46 @@ class SalesPerson(NestedSet): sales_person = frappe.db.get_value("Sales Person", {"employee": self.employee}) if sales_person and sales_person != self.name: - frappe.throw(_("Another Sales Person {0} exists with the same Employee id").format(sales_person)) + frappe.throw( + _("Another Sales Person {0} exists with the same Employee id").format(sales_person) + ) + def on_doctype_update(): frappe.db.add_index("Sales Person", ["lft", "rgt"]) + def get_timeline_data(doctype, name): out = {} - out.update(dict(frappe.db.sql('''select + out.update( + dict( + frappe.db.sql( + """select unix_timestamp(dt.transaction_date), count(st.parenttype) from `tabSales Order` dt, `tabSales Team` st where st.sales_person = %s and st.parent = dt.name and dt.transaction_date > date_sub(curdate(), interval 1 year) - group by dt.transaction_date ''', name))) + group by dt.transaction_date """, + name, + ) + ) + ) - sales_invoice = dict(frappe.db.sql('''select + sales_invoice = dict( + frappe.db.sql( + """select unix_timestamp(dt.posting_date), count(st.parenttype) from `tabSales Invoice` dt, `tabSales Team` st where st.sales_person = %s and st.parent = dt.name and dt.posting_date > date_sub(curdate(), interval 1 year) - group by dt.posting_date ''', name)) + group by dt.posting_date """, + name, + ) + ) for key in sales_invoice: if out.get(key): @@ -91,13 +115,18 @@ def get_timeline_data(doctype, name): else: out[key] = sales_invoice[key] - delivery_note = dict(frappe.db.sql('''select + delivery_note = dict( + frappe.db.sql( + """select unix_timestamp(dt.posting_date), count(st.parenttype) from `tabDelivery Note` dt, `tabSales Team` st where st.sales_person = %s and st.parent = dt.name and dt.posting_date > date_sub(curdate(), interval 1 year) - group by dt.posting_date ''', name)) + group by dt.posting_date """, + name, + ) + ) for key in delivery_note: if out.get(key): diff --git a/erpnext/setup/doctype/sales_person/sales_person_dashboard.py b/erpnext/setup/doctype/sales_person/sales_person_dashboard.py index e946406e63..2ec2002d3d 100644 --- a/erpnext/setup/doctype/sales_person/sales_person_dashboard.py +++ b/erpnext/setup/doctype/sales_person/sales_person_dashboard.py @@ -3,13 +3,12 @@ from frappe import _ def get_data(): return { - 'heatmap': True, - 'heatmap_message': _('This is based on transactions against this Sales Person. See timeline below for details'), - 'fieldname': 'sales_person', - 'transactions': [ - { - 'label': _('Sales'), - 'items': ['Sales Order', 'Delivery Note', 'Sales Invoice'] - }, - ] + "heatmap": True, + "heatmap_message": _( + "This is based on transactions against this Sales Person. See timeline below for details" + ), + "fieldname": "sales_person", + "transactions": [ + {"label": _("Sales"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]}, + ], } diff --git a/erpnext/setup/doctype/sales_person/test_sales_person.py b/erpnext/setup/doctype/sales_person/test_sales_person.py index 786d2cac4d..6ff1888230 100644 --- a/erpnext/setup/doctype/sales_person/test_sales_person.py +++ b/erpnext/setup/doctype/sales_person/test_sales_person.py @@ -5,6 +5,6 @@ test_dependencies = ["Employee"] import frappe -test_records = frappe.get_test_records('Sales Person') +test_records = frappe.get_test_records("Sales Person") test_ignore = ["Item Group"] diff --git a/erpnext/setup/doctype/supplier_group/supplier_group.py b/erpnext/setup/doctype/supplier_group/supplier_group.py index 381e1250c8..9d2b733b74 100644 --- a/erpnext/setup/doctype/supplier_group/supplier_group.py +++ b/erpnext/setup/doctype/supplier_group/supplier_group.py @@ -7,7 +7,7 @@ from frappe.utils.nestedset import NestedSet, get_root_of class SupplierGroup(NestedSet): - nsm_parent_field = 'parent_supplier_group' + nsm_parent_field = "parent_supplier_group" def validate(self): if not self.parent_supplier_group: diff --git a/erpnext/setup/doctype/supplier_group/test_supplier_group.py b/erpnext/setup/doctype/supplier_group/test_supplier_group.py index 283b3bfec3..97ba705a50 100644 --- a/erpnext/setup/doctype/supplier_group/test_supplier_group.py +++ b/erpnext/setup/doctype/supplier_group/test_supplier_group.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Supplier Group') +test_records = frappe.get_test_records("Supplier Group") diff --git a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.py b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.py index 658f286f7c..344f6c6a19 100644 --- a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.py +++ b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.py @@ -15,9 +15,15 @@ class TermsandConditions(Document): def validate(self): if self.terms: validate_template(self.terms) - if not cint(self.buying) and not cint(self.selling) and not cint(self.hr) and not cint(self.disabled): + if ( + not cint(self.buying) + and not cint(self.selling) + and not cint(self.hr) + and not cint(self.disabled) + ): throw(_("At least one of the Applicable Modules should be selected")) + @frappe.whitelist() def get_terms_and_conditions(template_name, doc): if isinstance(doc, str): diff --git a/erpnext/setup/doctype/terms_and_conditions/test_terms_and_conditions.py b/erpnext/setup/doctype/terms_and_conditions/test_terms_and_conditions.py index ca9e6c1aef..171840af98 100644 --- a/erpnext/setup/doctype/terms_and_conditions/test_terms_and_conditions.py +++ b/erpnext/setup/doctype/terms_and_conditions/test_terms_and_conditions.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Terms and Conditions') +test_records = frappe.get_test_records("Terms and Conditions") diff --git a/erpnext/setup/doctype/territory/territory.py b/erpnext/setup/doctype/territory/territory.py index 4c47d829e9..9bb5569de5 100644 --- a/erpnext/setup/doctype/territory/territory.py +++ b/erpnext/setup/doctype/territory/territory.py @@ -9,13 +9,13 @@ from frappe.utils.nestedset import NestedSet, get_root_of class Territory(NestedSet): - nsm_parent_field = 'parent_territory' + nsm_parent_field = "parent_territory" def validate(self): if not self.parent_territory: self.parent_territory = get_root_of("Territory") - for d in self.get('targets') or []: + for d in self.get("targets") or []: if not flt(d.target_qty) and not flt(d.target_amount): frappe.throw(_("Either target qty or target amount is mandatory")) @@ -23,5 +23,6 @@ class Territory(NestedSet): super(Territory, self).on_update() self.validate_one_root() + def on_doctype_update(): frappe.db.add_index("Territory", ["lft", "rgt"]) diff --git a/erpnext/setup/doctype/territory/test_territory.py b/erpnext/setup/doctype/territory/test_territory.py index a18b7bf70e..4ec695d385 100644 --- a/erpnext/setup/doctype/territory/test_territory.py +++ b/erpnext/setup/doctype/territory/test_territory.py @@ -3,6 +3,6 @@ import frappe -test_records = frappe.get_test_records('Territory') +test_records = frappe.get_test_records("Territory") test_ignore = ["Item Group"] diff --git a/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py index 095c3d0b6f..319d435ca6 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py @@ -8,61 +8,51 @@ import frappe class TestTransactionDeletionRecord(unittest.TestCase): def setUp(self): - create_company('Dunder Mifflin Paper Co') + create_company("Dunder Mifflin Paper Co") def tearDown(self): frappe.db.rollback() def test_doctypes_contain_company_field(self): - tdr = create_transaction_deletion_request('Dunder Mifflin Paper Co') + tdr = create_transaction_deletion_request("Dunder Mifflin Paper Co") for doctype in tdr.doctypes: contains_company = False - doctype_fields = frappe.get_meta(doctype.doctype_name).as_dict()['fields'] + doctype_fields = frappe.get_meta(doctype.doctype_name).as_dict()["fields"] for doctype_field in doctype_fields: - if doctype_field['fieldtype'] == 'Link' and doctype_field['options'] == 'Company': + if doctype_field["fieldtype"] == "Link" and doctype_field["options"] == "Company": contains_company = True break self.assertTrue(contains_company) def test_no_of_docs_is_correct(self): for i in range(5): - create_task('Dunder Mifflin Paper Co') - tdr = create_transaction_deletion_request('Dunder Mifflin Paper Co') + create_task("Dunder Mifflin Paper Co") + tdr = create_transaction_deletion_request("Dunder Mifflin Paper Co") for doctype in tdr.doctypes: - if doctype.doctype_name == 'Task': + if doctype.doctype_name == "Task": self.assertEqual(doctype.no_of_docs, 5) def test_deletion_is_successful(self): - create_task('Dunder Mifflin Paper Co') - create_transaction_deletion_request('Dunder Mifflin Paper Co') - tasks_containing_company = frappe.get_all('Task', - filters = { - 'company' : 'Dunder Mifflin Paper Co' - }) + create_task("Dunder Mifflin Paper Co") + create_transaction_deletion_request("Dunder Mifflin Paper Co") + tasks_containing_company = frappe.get_all("Task", filters={"company": "Dunder Mifflin Paper Co"}) self.assertEqual(tasks_containing_company, []) + def create_company(company_name): - company = frappe.get_doc({ - 'doctype': 'Company', - 'company_name': company_name, - 'default_currency': 'INR' - }) - company.insert(ignore_if_duplicate = True) + company = frappe.get_doc( + {"doctype": "Company", "company_name": company_name, "default_currency": "INR"} + ) + company.insert(ignore_if_duplicate=True) + def create_transaction_deletion_request(company): - tdr = frappe.get_doc({ - 'doctype': 'Transaction Deletion Record', - 'company': company - }) + tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company}) tdr.insert() tdr.submit() return tdr def create_task(company): - task = frappe.get_doc({ - 'doctype': 'Task', - 'company': company, - 'subject': 'Delete' - }) + task = frappe.get_doc({"doctype": "Task", "company": company, "subject": "Delete"}) task.insert() diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index 83ce042cde..78b3939012 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -11,15 +11,19 @@ from frappe.utils import cint class TransactionDeletionRecord(Document): def validate(self): - frappe.only_for('System Manager') + frappe.only_for("System Manager") self.validate_doctypes_to_be_ignored() def validate_doctypes_to_be_ignored(self): doctypes_to_be_ignored_list = get_doctypes_to_be_ignored() for doctype in self.doctypes_to_be_ignored: if doctype.doctype_name not in doctypes_to_be_ignored_list: - frappe.throw(_("DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it."), - title=_("Not Allowed")) + frappe.throw( + _( + "DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it." + ), + title=_("Not Allowed"), + ) def before_submit(self): if not self.doctypes_to_be_ignored: @@ -34,38 +38,55 @@ class TransactionDeletionRecord(Document): def populate_doctypes_to_be_ignored_table(self): doctypes_to_be_ignored_list = get_doctypes_to_be_ignored() for doctype in doctypes_to_be_ignored_list: - self.append('doctypes_to_be_ignored', { - 'doctype_name' : doctype - }) + self.append("doctypes_to_be_ignored", {"doctype_name": doctype}) def delete_bins(self): - frappe.db.sql("""delete from tabBin where warehouse in - (select name from tabWarehouse where company=%s)""", self.company) + frappe.db.sql( + """delete from tabBin where warehouse in + (select name from tabWarehouse where company=%s)""", + self.company, + ) def delete_lead_addresses(self): """Delete addresses to which leads are linked""" - leads = frappe.get_all('Lead', filters={'company': self.company}) + leads = frappe.get_all("Lead", filters={"company": self.company}) leads = ["'%s'" % row.get("name") for row in leads] addresses = [] if leads: - addresses = frappe.db.sql_list("""select parent from `tabDynamic Link` where link_name - in ({leads})""".format(leads=",".join(leads))) + addresses = frappe.db.sql_list( + """select parent from `tabDynamic Link` where link_name + in ({leads})""".format( + leads=",".join(leads) + ) + ) if addresses: addresses = ["%s" % frappe.db.escape(addr) for addr in addresses] - frappe.db.sql("""delete from tabAddress where name in ({addresses}) and + frappe.db.sql( + """delete from tabAddress where name in ({addresses}) and name not in (select distinct dl1.parent from `tabDynamic Link` dl1 inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent - and dl1.link_doctype<>dl2.link_doctype)""".format(addresses=",".join(addresses))) + and dl1.link_doctype<>dl2.link_doctype)""".format( + addresses=",".join(addresses) + ) + ) - frappe.db.sql("""delete from `tabDynamic Link` where link_doctype='Lead' - and parenttype='Address' and link_name in ({leads})""".format(leads=",".join(leads))) + frappe.db.sql( + """delete from `tabDynamic Link` where link_doctype='Lead' + and parenttype='Address' and link_name in ({leads})""".format( + leads=",".join(leads) + ) + ) - frappe.db.sql("""update tabCustomer set lead_name=NULL where lead_name in ({leads})""".format(leads=",".join(leads))) + frappe.db.sql( + """update tabCustomer set lead_name=NULL where lead_name in ({leads})""".format( + leads=",".join(leads) + ) + ) def reset_company_values(self): - company_obj = frappe.get_doc('Company', self.company) + company_obj = frappe.get_doc("Company", self.company) company_obj.total_monthly_sales = 0 company_obj.sales_monthly_history = None company_obj.save() @@ -76,24 +97,26 @@ class TransactionDeletionRecord(Document): tables = self.get_all_child_doctypes() for docfield in docfields: - if docfield['parent'] != self.doctype: - no_of_docs = self.get_number_of_docs_linked_with_specified_company(docfield['parent'], docfield['fieldname']) + if docfield["parent"] != self.doctype: + no_of_docs = self.get_number_of_docs_linked_with_specified_company( + docfield["parent"], docfield["fieldname"] + ) if no_of_docs > 0: - self.delete_version_log(docfield['parent'], docfield['fieldname']) - self.delete_communications(docfield['parent'], docfield['fieldname']) - self.populate_doctypes_table(tables, docfield['parent'], no_of_docs) + self.delete_version_log(docfield["parent"], docfield["fieldname"]) + self.delete_communications(docfield["parent"], docfield["fieldname"]) + self.populate_doctypes_table(tables, docfield["parent"], no_of_docs) - self.delete_child_tables(docfield['parent'], docfield['fieldname']) - self.delete_docs_linked_with_specified_company(docfield['parent'], docfield['fieldname']) + self.delete_child_tables(docfield["parent"], docfield["fieldname"]) + self.delete_docs_linked_with_specified_company(docfield["parent"], docfield["fieldname"]) - naming_series = frappe.db.get_value('DocType', docfield['parent'], 'autoname') + naming_series = frappe.db.get_value("DocType", docfield["parent"], "autoname") if naming_series: - if '#' in naming_series: - self.update_naming_series(naming_series, docfield['parent']) + if "#" in naming_series: + self.update_naming_series(naming_series, docfield["parent"]) def get_doctypes_to_be_ignored_list(self): - singles = frappe.get_all('DocType', filters = {'issingle': 1}, pluck = 'name') + singles = frappe.get_all("DocType", filters={"issingle": 1}, pluck="name") doctypes_to_be_ignored_list = singles for doctype in self.doctypes_to_be_ignored: doctypes_to_be_ignored_list.append(doctype.doctype_name) @@ -101,81 +124,104 @@ class TransactionDeletionRecord(Document): return doctypes_to_be_ignored_list def get_doctypes_with_company_field(self, doctypes_to_be_ignored_list): - docfields = frappe.get_all('DocField', - filters = { - 'fieldtype': 'Link', - 'options': 'Company', - 'parent': ['not in', doctypes_to_be_ignored_list]}, - fields=['parent', 'fieldname']) + docfields = frappe.get_all( + "DocField", + filters={ + "fieldtype": "Link", + "options": "Company", + "parent": ["not in", doctypes_to_be_ignored_list], + }, + fields=["parent", "fieldname"], + ) return docfields def get_all_child_doctypes(self): - return frappe.get_all('DocType', filters = {'istable': 1}, pluck = 'name') + return frappe.get_all("DocType", filters={"istable": 1}, pluck="name") def get_number_of_docs_linked_with_specified_company(self, doctype, company_fieldname): - return frappe.db.count(doctype, {company_fieldname : self.company}) + return frappe.db.count(doctype, {company_fieldname: self.company}) def populate_doctypes_table(self, tables, doctype, no_of_docs): if doctype not in tables: - self.append('doctypes', { - 'doctype_name' : doctype, - 'no_of_docs' : no_of_docs - }) + self.append("doctypes", {"doctype_name": doctype, "no_of_docs": no_of_docs}) def delete_child_tables(self, doctype, company_fieldname): - parent_docs_to_be_deleted = frappe.get_all(doctype, { - company_fieldname : self.company - }, pluck = 'name') + parent_docs_to_be_deleted = frappe.get_all( + doctype, {company_fieldname: self.company}, pluck="name" + ) - child_tables = frappe.get_all('DocField', filters = { - 'fieldtype': 'Table', - 'parent': doctype - }, pluck = 'options') + child_tables = frappe.get_all( + "DocField", filters={"fieldtype": "Table", "parent": doctype}, pluck="options" + ) for table in child_tables: - frappe.db.delete(table, { - 'parent': ['in', parent_docs_to_be_deleted] - }) + frappe.db.delete(table, {"parent": ["in", parent_docs_to_be_deleted]}) def delete_docs_linked_with_specified_company(self, doctype, company_fieldname): - frappe.db.delete(doctype, { - company_fieldname : self.company - }) + frappe.db.delete(doctype, {company_fieldname: self.company}) def update_naming_series(self, naming_series, doctype_name): - if '.' in naming_series: - prefix, hashes = naming_series.rsplit('.', 1) + if "." in naming_series: + prefix, hashes = naming_series.rsplit(".", 1) else: - prefix, hashes = naming_series.rsplit('{', 1) - last = frappe.db.sql("""select max(name) from `tab{0}` - where name like %s""".format(doctype_name), prefix + '%') + prefix, hashes = naming_series.rsplit("{", 1) + last = frappe.db.sql( + """select max(name) from `tab{0}` + where name like %s""".format( + doctype_name + ), + prefix + "%", + ) if last and last[0][0]: - last = cint(last[0][0].replace(prefix, '')) + last = cint(last[0][0].replace(prefix, "")) else: last = 0 frappe.db.sql("""update tabSeries set current = %s where name=%s""", (last, prefix)) def delete_version_log(self, doctype, company_fieldname): - frappe.db.sql("""delete from `tabVersion` where ref_doctype=%s and docname in - (select name from `tab{0}` where `{1}`=%s)""".format(doctype, - company_fieldname), (doctype, self.company)) + frappe.db.sql( + """delete from `tabVersion` where ref_doctype=%s and docname in + (select name from `tab{0}` where `{1}`=%s)""".format( + doctype, company_fieldname + ), + (doctype, self.company), + ) def delete_communications(self, doctype, company_fieldname): - reference_docs = frappe.get_all(doctype, filters={company_fieldname:self.company}) + reference_docs = frappe.get_all(doctype, filters={company_fieldname: self.company}) reference_doc_names = [r.name for r in reference_docs] - communications = frappe.get_all('Communication', filters={'reference_doctype':doctype,'reference_name':['in', reference_doc_names]}) + communications = frappe.get_all( + "Communication", + filters={"reference_doctype": doctype, "reference_name": ["in", reference_doc_names]}, + ) communication_names = [c.name for c in communications] - frappe.delete_doc('Communication', communication_names, ignore_permissions=True) + frappe.delete_doc("Communication", communication_names, ignore_permissions=True) + @frappe.whitelist() def get_doctypes_to_be_ignored(): - doctypes_to_be_ignored_list = ['Account', 'Cost Center', 'Warehouse', 'Budget', - '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', 'Customer', 'Supplier', 'GST Account'] + doctypes_to_be_ignored_list = [ + "Account", + "Cost Center", + "Warehouse", + "Budget", + "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", + "Customer", + "Supplier", + "GST Account", + ] return doctypes_to_be_ignored_list diff --git a/erpnext/setup/doctype/uom/test_uom.py b/erpnext/setup/doctype/uom/test_uom.py index feb4329307..3278d4eab8 100644 --- a/erpnext/setup/doctype/uom/test_uom.py +++ b/erpnext/setup/doctype/uom/test_uom.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('UOM') +test_records = frappe.get_test_records("UOM") diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 1d95ddb203..2b055d2dd3 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -19,7 +19,7 @@ default_mail_footer = """
    {1}".format(fileurl, args.get("company_name"))) + frappe.db.set_value( + "Website Settings", + "Website Settings", + "brand_html", + " {1}".format( + fileurl, args.get("company_name") + ), + ) + def create_website(args): website_maker(args) + def get_fy_details(fy_start_date, fy_end_date): start_year = getdate(fy_start_date).year if start_year == getdate(fy_end_date).year: fy = cstr(start_year) else: - fy = cstr(start_year) + '-' + cstr(start_year + 1) + fy = cstr(start_year) + "-" + cstr(start_year + 1) return fy diff --git a/erpnext/setup/setup_wizard/operations/default_website.py b/erpnext/setup/setup_wizard/operations/default_website.py index c11910b584..40b02b35df 100644 --- a/erpnext/setup/setup_wizard/operations/default_website.py +++ b/erpnext/setup/setup_wizard/operations/default_website.py @@ -12,14 +12,14 @@ class website_maker(object): self.args = args self.company = args.company_name self.tagline = args.company_tagline - self.user = args.get('email') + self.user = args.get("email") self.make_web_page() self.make_website_settings() self.make_blog() def make_web_page(self): # home page - homepage = frappe.get_doc('Homepage', 'Homepage') + homepage = frappe.get_doc("Homepage", "Homepage") homepage.company = self.company homepage.tag_line = self.tagline homepage.setup_items() @@ -28,34 +28,25 @@ class website_maker(object): def make_website_settings(self): # update in home page in settings website_settings = frappe.get_doc("Website Settings", "Website Settings") - website_settings.home_page = 'home' + website_settings.home_page = "home" website_settings.brand_html = self.company website_settings.copyright = self.company website_settings.top_bar_items = [] - website_settings.append("top_bar_items", { - "doctype": "Top Bar Item", - "label":"Contact", - "url": "/contact" - }) - website_settings.append("top_bar_items", { - "doctype": "Top Bar Item", - "label":"Blog", - "url": "/blog" - }) - website_settings.append("top_bar_items", { - "doctype": "Top Bar Item", - "label": _("Products"), - "url": "/all-products" - }) + website_settings.append( + "top_bar_items", {"doctype": "Top Bar Item", "label": "Contact", "url": "/contact"} + ) + website_settings.append( + "top_bar_items", {"doctype": "Top Bar Item", "label": "Blog", "url": "/blog"} + ) + website_settings.append( + "top_bar_items", {"doctype": "Top Bar Item", "label": _("Products"), "url": "/all-products"} + ) website_settings.save() def make_blog(self): - blog_category = frappe.get_doc({ - "doctype": "Blog Category", - "category_name": "general", - "published": 1, - "title": _("General") - }).insert() + blog_category = frappe.get_doc( + {"doctype": "Blog Category", "category_name": "general", "published": 1, "title": _("General")} + ).insert() if not self.user: # Admin setup @@ -69,21 +60,30 @@ class website_maker(object): blogger.avatar = user.user_image blogger.insert() - frappe.get_doc({ - "doctype": "Blog Post", - "title": "Welcome", - "published": 1, - "published_on": nowdate(), - "blogger": blogger.name, - "blog_category": blog_category.name, - "blog_intro": "My First Blog", - "content": frappe.get_template("setup/setup_wizard/data/sample_blog_post.html").render(), - }).insert() + frappe.get_doc( + { + "doctype": "Blog Post", + "title": "Welcome", + "published": 1, + "published_on": nowdate(), + "blogger": blogger.name, + "blog_category": blog_category.name, + "blog_intro": "My First Blog", + "content": frappe.get_template("setup/setup_wizard/data/sample_blog_post.html").render(), + } + ).insert() + def test(): frappe.delete_doc("Web Page", "test-company") frappe.delete_doc("Blog Post", "welcome") frappe.delete_doc("Blogger", "administrator") frappe.delete_doc("Blog Category", "general") - website_maker({'company':"Test Company", 'company_tagline': "Better Tools for Everyone", 'name': "Administrator"}) + website_maker( + { + "company": "Test Company", + "company_tagline": "Better Tools for Everyone", + "name": "Administrator", + } + ) frappe.db.commit() diff --git a/erpnext/setup/setup_wizard/operations/defaults_setup.py b/erpnext/setup/setup_wizard/operations/defaults_setup.py index ca1f57eb1d..07321ed7ac 100644 --- a/erpnext/setup/setup_wizard/operations/defaults_setup.py +++ b/erpnext/setup/setup_wizard/operations/defaults_setup.py @@ -5,17 +5,20 @@ import frappe from frappe import _ from frappe.utils import cstr, getdate + def set_default_settings(args): # enable default currency frappe.db.set_value("Currency", args.get("currency"), "enabled", 1) global_defaults = frappe.get_doc("Global Defaults", "Global Defaults") - global_defaults.update({ - 'current_fiscal_year': get_fy_details(args.get('fy_start_date'), args.get('fy_end_date')), - 'default_currency': args.get('currency'), - 'default_company':args.get('company_name') , - "country": args.get("country"), - }) + global_defaults.update( + { + "current_fiscal_year": get_fy_details(args.get("fy_start_date"), args.get("fy_end_date")), + "default_currency": args.get("currency"), + "default_company": args.get("company_name"), + "country": args.get("country"), + } + ) global_defaults.save() @@ -23,13 +26,15 @@ def set_default_settings(args): system_settings.email_footer_address = args.get("company_name") system_settings.save() - domain_settings = frappe.get_single('Domain Settings') - domain_settings.set_active_domains(args.get('domains')) + domain_settings = frappe.get_single("Domain Settings") + domain_settings.set_active_domains(args.get("domains")) stock_settings = frappe.get_doc("Stock Settings") stock_settings.item_naming_by = "Item Code" stock_settings.valuation_method = "FIFO" - stock_settings.default_warehouse = frappe.db.get_value('Warehouse', {'warehouse_name': _('Stores')}) + stock_settings.default_warehouse = frappe.db.get_value( + "Warehouse", {"warehouse_name": _("Stores")} + ) stock_settings.stock_uom = _("Nos") stock_settings.auto_indent = 1 stock_settings.auto_insert_price_list_rate_if_missing = 1 @@ -72,61 +77,74 @@ def set_default_settings(args): hr_settings.exit_questionnaire_notification_template = _("Exit Questionnaire Notification") hr_settings.save() + def set_no_copy_fields_in_variant_settings(): # set no copy fields of an item doctype to item variant settings - doc = frappe.get_doc('Item Variant Settings') + doc = frappe.get_doc("Item Variant Settings") doc.set_default_fields() doc.save() + def create_price_lists(args): for pl_type, pl_name in (("Selling", _("Standard Selling")), ("Buying", _("Standard Buying"))): - frappe.get_doc({ - "doctype": "Price List", - "price_list_name": pl_name, - "enabled": 1, - "buying": 1 if pl_type == "Buying" else 0, - "selling": 1 if pl_type == "Selling" else 0, - "currency": args["currency"] - }).insert() + frappe.get_doc( + { + "doctype": "Price List", + "price_list_name": pl_name, + "enabled": 1, + "buying": 1 if pl_type == "Buying" else 0, + "selling": 1 if pl_type == "Selling" else 0, + "currency": args["currency"], + } + ).insert() + def create_employee_for_self(args): - if frappe.session.user == 'Administrator': + if frappe.session.user == "Administrator": return # create employee for self - emp = frappe.get_doc({ - "doctype": "Employee", - "employee_name": " ".join(filter(None, [args.get("first_name"), args.get("last_name")])), - "user_id": frappe.session.user, - "status": "Active", - "company": args.get("company_name") - }) + emp = frappe.get_doc( + { + "doctype": "Employee", + "employee_name": " ".join(filter(None, [args.get("first_name"), args.get("last_name")])), + "user_id": frappe.session.user, + "status": "Active", + "company": args.get("company_name"), + } + ) emp.flags.ignore_mandatory = True - emp.insert(ignore_permissions = True) + emp.insert(ignore_permissions=True) + def create_territories(): """create two default territories, one for home country and one named Rest of the World""" from frappe.utils.nestedset import get_root_of + country = frappe.db.get_default("country") root_territory = get_root_of("Territory") for name in (country, _("Rest Of The World")): if name and not frappe.db.exists("Territory", name): - frappe.get_doc({ - "doctype": "Territory", - "territory_name": name.replace("'", ""), - "parent_territory": root_territory, - "is_group": "No" - }).insert() + frappe.get_doc( + { + "doctype": "Territory", + "territory_name": name.replace("'", ""), + "parent_territory": root_territory, + "is_group": "No", + } + ).insert() + def create_feed_and_todo(): """update Activity feed and create todo for creation of item, customer, vendor""" return + def get_fy_details(fy_start_date, fy_end_date): start_year = getdate(fy_start_date).year if start_year == getdate(fy_end_date).year: fy = cstr(start_year) else: - fy = cstr(start_year) + '-' + cstr(start_year + 1) + fy = cstr(start_year) + "-" + cstr(start_year + 1) return fy diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index cefa0f3887..a0056e2112 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -17,195 +17,372 @@ from frappe.utils.nestedset import rebuild_tree from erpnext.accounts.doctype.account.account import RootNotEditable from erpnext.regional.address_template.setup import set_up_address_templates -default_lead_sources = ["Existing Customer", "Reference", "Advertisement", - "Cold Calling", "Exhibition", "Supplier Reference", "Mass Mailing", - "Customer's Vendor", "Campaign", "Walk In"] +default_lead_sources = [ + "Existing Customer", + "Reference", + "Advertisement", + "Cold Calling", + "Exhibition", + "Supplier Reference", + "Mass Mailing", + "Customer's Vendor", + "Campaign", + "Walk In", +] + +default_sales_partner_type = [ + "Channel Partner", + "Distributor", + "Dealer", + "Agent", + "Retailer", + "Implementation Partner", + "Reseller", +] -default_sales_partner_type = ["Channel Partner", "Distributor", "Dealer", "Agent", - "Retailer", "Implementation Partner", "Reseller"] def install(country=None): records = [ # domains - { 'doctype': 'Domain', 'domain': 'Distribution'}, - { 'doctype': 'Domain', 'domain': 'Manufacturing'}, - { 'doctype': 'Domain', 'domain': 'Retail'}, - { 'doctype': 'Domain', 'domain': 'Services'}, - { 'doctype': 'Domain', 'domain': 'Education'}, - { 'doctype': 'Domain', 'domain': 'Healthcare'}, - { 'doctype': 'Domain', 'domain': 'Non Profit'}, - + {"doctype": "Domain", "domain": "Distribution"}, + {"doctype": "Domain", "domain": "Manufacturing"}, + {"doctype": "Domain", "domain": "Retail"}, + {"doctype": "Domain", "domain": "Services"}, + {"doctype": "Domain", "domain": "Education"}, + {"doctype": "Domain", "domain": "Healthcare"}, + {"doctype": "Domain", "domain": "Non Profit"}, # ensure at least an empty Address Template exists for this Country - {'doctype':"Address Template", "country": country}, - + {"doctype": "Address Template", "country": country}, # item group - {'doctype': 'Item Group', 'item_group_name': _('All Item Groups'), - 'is_group': 1, 'parent_item_group': ''}, - {'doctype': 'Item Group', 'item_group_name': _('Products'), - 'is_group': 0, 'parent_item_group': _('All Item Groups'), "show_in_website": 1 }, - {'doctype': 'Item Group', 'item_group_name': _('Raw Material'), - 'is_group': 0, 'parent_item_group': _('All Item Groups') }, - {'doctype': 'Item Group', 'item_group_name': _('Services'), - 'is_group': 0, 'parent_item_group': _('All Item Groups') }, - {'doctype': 'Item Group', 'item_group_name': _('Sub Assemblies'), - 'is_group': 0, 'parent_item_group': _('All Item Groups') }, - {'doctype': 'Item Group', 'item_group_name': _('Consumable'), - 'is_group': 0, 'parent_item_group': _('All Item Groups') }, - + { + "doctype": "Item Group", + "item_group_name": _("All Item Groups"), + "is_group": 1, + "parent_item_group": "", + }, + { + "doctype": "Item Group", + "item_group_name": _("Products"), + "is_group": 0, + "parent_item_group": _("All Item Groups"), + "show_in_website": 1, + }, + { + "doctype": "Item Group", + "item_group_name": _("Raw Material"), + "is_group": 0, + "parent_item_group": _("All Item Groups"), + }, + { + "doctype": "Item Group", + "item_group_name": _("Services"), + "is_group": 0, + "parent_item_group": _("All Item Groups"), + }, + { + "doctype": "Item Group", + "item_group_name": _("Sub Assemblies"), + "is_group": 0, + "parent_item_group": _("All Item Groups"), + }, + { + "doctype": "Item Group", + "item_group_name": _("Consumable"), + "is_group": 0, + "parent_item_group": _("All Item Groups"), + }, # salary component - {'doctype': 'Salary Component', 'salary_component': _('Income Tax'), 'description': _('Income Tax'), 'type': 'Deduction', 'is_income_tax_component': 1}, - {'doctype': 'Salary Component', 'salary_component': _('Basic'), 'description': _('Basic'), 'type': 'Earning'}, - {'doctype': 'Salary Component', 'salary_component': _('Arrear'), 'description': _('Arrear'), 'type': 'Earning'}, - {'doctype': 'Salary Component', 'salary_component': _('Leave Encashment'), 'description': _('Leave Encashment'), 'type': 'Earning'}, - - + { + "doctype": "Salary Component", + "salary_component": _("Income Tax"), + "description": _("Income Tax"), + "type": "Deduction", + "is_income_tax_component": 1, + }, + { + "doctype": "Salary Component", + "salary_component": _("Basic"), + "description": _("Basic"), + "type": "Earning", + }, + { + "doctype": "Salary Component", + "salary_component": _("Arrear"), + "description": _("Arrear"), + "type": "Earning", + }, + { + "doctype": "Salary Component", + "salary_component": _("Leave Encashment"), + "description": _("Leave Encashment"), + "type": "Earning", + }, # expense claim type - {'doctype': 'Expense Claim Type', 'name': _('Calls'), 'expense_type': _('Calls')}, - {'doctype': 'Expense Claim Type', 'name': _('Food'), 'expense_type': _('Food')}, - {'doctype': 'Expense Claim Type', 'name': _('Medical'), 'expense_type': _('Medical')}, - {'doctype': 'Expense Claim Type', 'name': _('Others'), 'expense_type': _('Others')}, - {'doctype': 'Expense Claim Type', 'name': _('Travel'), 'expense_type': _('Travel')}, - + {"doctype": "Expense Claim Type", "name": _("Calls"), "expense_type": _("Calls")}, + {"doctype": "Expense Claim Type", "name": _("Food"), "expense_type": _("Food")}, + {"doctype": "Expense Claim Type", "name": _("Medical"), "expense_type": _("Medical")}, + {"doctype": "Expense Claim Type", "name": _("Others"), "expense_type": _("Others")}, + {"doctype": "Expense Claim Type", "name": _("Travel"), "expense_type": _("Travel")}, # leave type - {'doctype': 'Leave Type', 'leave_type_name': _('Casual Leave'), 'name': _('Casual Leave'), - 'allow_encashment': 1, 'is_carry_forward': 1, 'max_continuous_days_allowed': '3', 'include_holiday': 1}, - {'doctype': 'Leave Type', 'leave_type_name': _('Compensatory Off'), 'name': _('Compensatory Off'), - 'allow_encashment': 0, 'is_carry_forward': 0, 'include_holiday': 1, 'is_compensatory':1 }, - {'doctype': 'Leave Type', 'leave_type_name': _('Sick Leave'), 'name': _('Sick Leave'), - 'allow_encashment': 0, 'is_carry_forward': 0, 'include_holiday': 1}, - {'doctype': 'Leave Type', 'leave_type_name': _('Privilege Leave'), 'name': _('Privilege Leave'), - 'allow_encashment': 0, 'is_carry_forward': 0, 'include_holiday': 1}, - {'doctype': 'Leave Type', 'leave_type_name': _('Leave Without Pay'), 'name': _('Leave Without Pay'), - 'allow_encashment': 0, 'is_carry_forward': 0, 'is_lwp':1, 'include_holiday': 1}, - + { + "doctype": "Leave Type", + "leave_type_name": _("Casual Leave"), + "name": _("Casual Leave"), + "allow_encashment": 1, + "is_carry_forward": 1, + "max_continuous_days_allowed": "3", + "include_holiday": 1, + }, + { + "doctype": "Leave Type", + "leave_type_name": _("Compensatory Off"), + "name": _("Compensatory Off"), + "allow_encashment": 0, + "is_carry_forward": 0, + "include_holiday": 1, + "is_compensatory": 1, + }, + { + "doctype": "Leave Type", + "leave_type_name": _("Sick Leave"), + "name": _("Sick Leave"), + "allow_encashment": 0, + "is_carry_forward": 0, + "include_holiday": 1, + }, + { + "doctype": "Leave Type", + "leave_type_name": _("Privilege Leave"), + "name": _("Privilege Leave"), + "allow_encashment": 0, + "is_carry_forward": 0, + "include_holiday": 1, + }, + { + "doctype": "Leave Type", + "leave_type_name": _("Leave Without Pay"), + "name": _("Leave Without Pay"), + "allow_encashment": 0, + "is_carry_forward": 0, + "is_lwp": 1, + "include_holiday": 1, + }, # Employment Type - {'doctype': 'Employment Type', 'employee_type_name': _('Full-time')}, - {'doctype': 'Employment Type', 'employee_type_name': _('Part-time')}, - {'doctype': 'Employment Type', 'employee_type_name': _('Probation')}, - {'doctype': 'Employment Type', 'employee_type_name': _('Contract')}, - {'doctype': 'Employment Type', 'employee_type_name': _('Commission')}, - {'doctype': 'Employment Type', 'employee_type_name': _('Piecework')}, - {'doctype': 'Employment Type', 'employee_type_name': _('Intern')}, - {'doctype': 'Employment Type', 'employee_type_name': _('Apprentice')}, - - + {"doctype": "Employment Type", "employee_type_name": _("Full-time")}, + {"doctype": "Employment Type", "employee_type_name": _("Part-time")}, + {"doctype": "Employment Type", "employee_type_name": _("Probation")}, + {"doctype": "Employment Type", "employee_type_name": _("Contract")}, + {"doctype": "Employment Type", "employee_type_name": _("Commission")}, + {"doctype": "Employment Type", "employee_type_name": _("Piecework")}, + {"doctype": "Employment Type", "employee_type_name": _("Intern")}, + {"doctype": "Employment Type", "employee_type_name": _("Apprentice")}, # Stock Entry Type - {'doctype': 'Stock Entry Type', 'name': 'Material Issue', 'purpose': 'Material Issue'}, - {'doctype': 'Stock Entry Type', 'name': 'Material Receipt', 'purpose': 'Material Receipt'}, - {'doctype': 'Stock Entry Type', 'name': 'Material Transfer', 'purpose': 'Material Transfer'}, - {'doctype': 'Stock Entry Type', 'name': 'Manufacture', 'purpose': 'Manufacture'}, - {'doctype': 'Stock Entry Type', 'name': 'Repack', 'purpose': 'Repack'}, - {'doctype': 'Stock Entry Type', 'name': 'Send to Subcontractor', 'purpose': 'Send to Subcontractor'}, - {'doctype': 'Stock Entry Type', 'name': 'Material Transfer for Manufacture', 'purpose': 'Material Transfer for Manufacture'}, - {'doctype': 'Stock Entry Type', 'name': 'Material Consumption for Manufacture', 'purpose': 'Material Consumption for Manufacture'}, - + {"doctype": "Stock Entry Type", "name": "Material Issue", "purpose": "Material Issue"}, + {"doctype": "Stock Entry Type", "name": "Material Receipt", "purpose": "Material Receipt"}, + {"doctype": "Stock Entry Type", "name": "Material Transfer", "purpose": "Material Transfer"}, + {"doctype": "Stock Entry Type", "name": "Manufacture", "purpose": "Manufacture"}, + {"doctype": "Stock Entry Type", "name": "Repack", "purpose": "Repack"}, + { + "doctype": "Stock Entry Type", + "name": "Send to Subcontractor", + "purpose": "Send to Subcontractor", + }, + { + "doctype": "Stock Entry Type", + "name": "Material Transfer for Manufacture", + "purpose": "Material Transfer for Manufacture", + }, + { + "doctype": "Stock Entry Type", + "name": "Material Consumption for Manufacture", + "purpose": "Material Consumption for Manufacture", + }, # Designation - {'doctype': 'Designation', 'designation_name': _('CEO')}, - {'doctype': 'Designation', 'designation_name': _('Manager')}, - {'doctype': 'Designation', 'designation_name': _('Analyst')}, - {'doctype': 'Designation', 'designation_name': _('Engineer')}, - {'doctype': 'Designation', 'designation_name': _('Accountant')}, - {'doctype': 'Designation', 'designation_name': _('Secretary')}, - {'doctype': 'Designation', 'designation_name': _('Associate')}, - {'doctype': 'Designation', 'designation_name': _('Administrative Officer')}, - {'doctype': 'Designation', 'designation_name': _('Business Development Manager')}, - {'doctype': 'Designation', 'designation_name': _('HR Manager')}, - {'doctype': 'Designation', 'designation_name': _('Project Manager')}, - {'doctype': 'Designation', 'designation_name': _('Head of Marketing and Sales')}, - {'doctype': 'Designation', 'designation_name': _('Software Developer')}, - {'doctype': 'Designation', 'designation_name': _('Designer')}, - {'doctype': 'Designation', 'designation_name': _('Researcher')}, - + {"doctype": "Designation", "designation_name": _("CEO")}, + {"doctype": "Designation", "designation_name": _("Manager")}, + {"doctype": "Designation", "designation_name": _("Analyst")}, + {"doctype": "Designation", "designation_name": _("Engineer")}, + {"doctype": "Designation", "designation_name": _("Accountant")}, + {"doctype": "Designation", "designation_name": _("Secretary")}, + {"doctype": "Designation", "designation_name": _("Associate")}, + {"doctype": "Designation", "designation_name": _("Administrative Officer")}, + {"doctype": "Designation", "designation_name": _("Business Development Manager")}, + {"doctype": "Designation", "designation_name": _("HR Manager")}, + {"doctype": "Designation", "designation_name": _("Project Manager")}, + {"doctype": "Designation", "designation_name": _("Head of Marketing and Sales")}, + {"doctype": "Designation", "designation_name": _("Software Developer")}, + {"doctype": "Designation", "designation_name": _("Designer")}, + {"doctype": "Designation", "designation_name": _("Researcher")}, # territory: with two default territories, one for home country and one named Rest of the World - {'doctype': 'Territory', 'territory_name': _('All Territories'), 'is_group': 1, 'name': _('All Territories'), 'parent_territory': ''}, - {'doctype': 'Territory', 'territory_name': country.replace("'", ""), 'is_group': 0, 'parent_territory': _('All Territories')}, - {'doctype': 'Territory', 'territory_name': _("Rest Of The World"), 'is_group': 0, 'parent_territory': _('All Territories')}, - + { + "doctype": "Territory", + "territory_name": _("All Territories"), + "is_group": 1, + "name": _("All Territories"), + "parent_territory": "", + }, + { + "doctype": "Territory", + "territory_name": country.replace("'", ""), + "is_group": 0, + "parent_territory": _("All Territories"), + }, + { + "doctype": "Territory", + "territory_name": _("Rest Of The World"), + "is_group": 0, + "parent_territory": _("All Territories"), + }, # customer group - {'doctype': 'Customer Group', 'customer_group_name': _('All Customer Groups'), 'is_group': 1, 'name': _('All Customer Groups'), 'parent_customer_group': ''}, - {'doctype': 'Customer Group', 'customer_group_name': _('Individual'), 'is_group': 0, 'parent_customer_group': _('All Customer Groups')}, - {'doctype': 'Customer Group', 'customer_group_name': _('Commercial'), 'is_group': 0, 'parent_customer_group': _('All Customer Groups')}, - {'doctype': 'Customer Group', 'customer_group_name': _('Non Profit'), 'is_group': 0, 'parent_customer_group': _('All Customer Groups')}, - {'doctype': 'Customer Group', 'customer_group_name': _('Government'), 'is_group': 0, 'parent_customer_group': _('All Customer Groups')}, - + { + "doctype": "Customer Group", + "customer_group_name": _("All Customer Groups"), + "is_group": 1, + "name": _("All Customer Groups"), + "parent_customer_group": "", + }, + { + "doctype": "Customer Group", + "customer_group_name": _("Individual"), + "is_group": 0, + "parent_customer_group": _("All Customer Groups"), + }, + { + "doctype": "Customer Group", + "customer_group_name": _("Commercial"), + "is_group": 0, + "parent_customer_group": _("All Customer Groups"), + }, + { + "doctype": "Customer Group", + "customer_group_name": _("Non Profit"), + "is_group": 0, + "parent_customer_group": _("All Customer Groups"), + }, + { + "doctype": "Customer Group", + "customer_group_name": _("Government"), + "is_group": 0, + "parent_customer_group": _("All Customer Groups"), + }, # supplier group - {'doctype': 'Supplier Group', 'supplier_group_name': _('All Supplier Groups'), 'is_group': 1, 'name': _('All Supplier Groups'), 'parent_supplier_group': ''}, - {'doctype': 'Supplier Group', 'supplier_group_name': _('Services'), 'is_group': 0, 'parent_supplier_group': _('All Supplier Groups')}, - {'doctype': 'Supplier Group', 'supplier_group_name': _('Local'), 'is_group': 0, 'parent_supplier_group': _('All Supplier Groups')}, - {'doctype': 'Supplier Group', 'supplier_group_name': _('Raw Material'), 'is_group': 0, 'parent_supplier_group': _('All Supplier Groups')}, - {'doctype': 'Supplier Group', 'supplier_group_name': _('Electrical'), 'is_group': 0, 'parent_supplier_group': _('All Supplier Groups')}, - {'doctype': 'Supplier Group', 'supplier_group_name': _('Hardware'), 'is_group': 0, 'parent_supplier_group': _('All Supplier Groups')}, - {'doctype': 'Supplier Group', 'supplier_group_name': _('Pharmaceutical'), 'is_group': 0, 'parent_supplier_group': _('All Supplier Groups')}, - {'doctype': 'Supplier Group', 'supplier_group_name': _('Distributor'), 'is_group': 0, 'parent_supplier_group': _('All Supplier Groups')}, - + { + "doctype": "Supplier Group", + "supplier_group_name": _("All Supplier Groups"), + "is_group": 1, + "name": _("All Supplier Groups"), + "parent_supplier_group": "", + }, + { + "doctype": "Supplier Group", + "supplier_group_name": _("Services"), + "is_group": 0, + "parent_supplier_group": _("All Supplier Groups"), + }, + { + "doctype": "Supplier Group", + "supplier_group_name": _("Local"), + "is_group": 0, + "parent_supplier_group": _("All Supplier Groups"), + }, + { + "doctype": "Supplier Group", + "supplier_group_name": _("Raw Material"), + "is_group": 0, + "parent_supplier_group": _("All Supplier Groups"), + }, + { + "doctype": "Supplier Group", + "supplier_group_name": _("Electrical"), + "is_group": 0, + "parent_supplier_group": _("All Supplier Groups"), + }, + { + "doctype": "Supplier Group", + "supplier_group_name": _("Hardware"), + "is_group": 0, + "parent_supplier_group": _("All Supplier Groups"), + }, + { + "doctype": "Supplier Group", + "supplier_group_name": _("Pharmaceutical"), + "is_group": 0, + "parent_supplier_group": _("All Supplier Groups"), + }, + { + "doctype": "Supplier Group", + "supplier_group_name": _("Distributor"), + "is_group": 0, + "parent_supplier_group": _("All Supplier Groups"), + }, # Sales Person - {'doctype': 'Sales Person', 'sales_person_name': _('Sales Team'), 'is_group': 1, "parent_sales_person": ""}, - + { + "doctype": "Sales Person", + "sales_person_name": _("Sales Team"), + "is_group": 1, + "parent_sales_person": "", + }, # Mode of Payment - {'doctype': 'Mode of Payment', - 'mode_of_payment': 'Check' if country=="United States" else _('Cheque'), - 'type': 'Bank'}, - {'doctype': 'Mode of Payment', 'mode_of_payment': _('Cash'), - 'type': 'Cash'}, - {'doctype': 'Mode of Payment', 'mode_of_payment': _('Credit Card'), - 'type': 'Bank'}, - {'doctype': 'Mode of Payment', 'mode_of_payment': _('Wire Transfer'), - 'type': 'Bank'}, - {'doctype': 'Mode of Payment', 'mode_of_payment': _('Bank Draft'), - 'type': 'Bank'}, - + { + "doctype": "Mode of Payment", + "mode_of_payment": "Check" if country == "United States" else _("Cheque"), + "type": "Bank", + }, + {"doctype": "Mode of Payment", "mode_of_payment": _("Cash"), "type": "Cash"}, + {"doctype": "Mode of Payment", "mode_of_payment": _("Credit Card"), "type": "Bank"}, + {"doctype": "Mode of Payment", "mode_of_payment": _("Wire Transfer"), "type": "Bank"}, + {"doctype": "Mode of Payment", "mode_of_payment": _("Bank Draft"), "type": "Bank"}, # Activity Type - {'doctype': 'Activity Type', 'activity_type': _('Planning')}, - {'doctype': 'Activity Type', 'activity_type': _('Research')}, - {'doctype': 'Activity Type', 'activity_type': _('Proposal Writing')}, - {'doctype': 'Activity Type', 'activity_type': _('Execution')}, - {'doctype': 'Activity Type', 'activity_type': _('Communication')}, - - {'doctype': "Item Attribute", "attribute_name": _("Size"), "item_attribute_values": [ - {"attribute_value": _("Extra Small"), "abbr": "XS"}, - {"attribute_value": _("Small"), "abbr": "S"}, - {"attribute_value": _("Medium"), "abbr": "M"}, - {"attribute_value": _("Large"), "abbr": "L"}, - {"attribute_value": _("Extra Large"), "abbr": "XL"} - ]}, - - {'doctype': "Item Attribute", "attribute_name": _("Colour"), "item_attribute_values": [ - {"attribute_value": _("Red"), "abbr": "RED"}, - {"attribute_value": _("Green"), "abbr": "GRE"}, - {"attribute_value": _("Blue"), "abbr": "BLU"}, - {"attribute_value": _("Black"), "abbr": "BLA"}, - {"attribute_value": _("White"), "abbr": "WHI"} - ]}, - + {"doctype": "Activity Type", "activity_type": _("Planning")}, + {"doctype": "Activity Type", "activity_type": _("Research")}, + {"doctype": "Activity Type", "activity_type": _("Proposal Writing")}, + {"doctype": "Activity Type", "activity_type": _("Execution")}, + {"doctype": "Activity Type", "activity_type": _("Communication")}, + { + "doctype": "Item Attribute", + "attribute_name": _("Size"), + "item_attribute_values": [ + {"attribute_value": _("Extra Small"), "abbr": "XS"}, + {"attribute_value": _("Small"), "abbr": "S"}, + {"attribute_value": _("Medium"), "abbr": "M"}, + {"attribute_value": _("Large"), "abbr": "L"}, + {"attribute_value": _("Extra Large"), "abbr": "XL"}, + ], + }, + { + "doctype": "Item Attribute", + "attribute_name": _("Colour"), + "item_attribute_values": [ + {"attribute_value": _("Red"), "abbr": "RED"}, + {"attribute_value": _("Green"), "abbr": "GRE"}, + {"attribute_value": _("Blue"), "abbr": "BLU"}, + {"attribute_value": _("Black"), "abbr": "BLA"}, + {"attribute_value": _("White"), "abbr": "WHI"}, + ], + }, # Issue Priority - {'doctype': 'Issue Priority', 'name': _('Low')}, - {'doctype': 'Issue Priority', 'name': _('Medium')}, - {'doctype': 'Issue Priority', 'name': _('High')}, - - #Job Applicant Source - {'doctype': 'Job Applicant Source', 'source_name': _('Website Listing')}, - {'doctype': 'Job Applicant Source', 'source_name': _('Walk In')}, - {'doctype': 'Job Applicant Source', 'source_name': _('Employee Referral')}, - {'doctype': 'Job Applicant Source', 'source_name': _('Campaign')}, - - {'doctype': "Email Account", "email_id": "sales@example.com", "append_to": "Opportunity"}, - {'doctype': "Email Account", "email_id": "support@example.com", "append_to": "Issue"}, - {'doctype': "Email Account", "email_id": "jobs@example.com", "append_to": "Job Applicant"}, - - {'doctype': "Party Type", "party_type": "Customer", "account_type": "Receivable"}, - {'doctype': "Party Type", "party_type": "Supplier", "account_type": "Payable"}, - {'doctype': "Party Type", "party_type": "Employee", "account_type": "Payable"}, - {'doctype': "Party Type", "party_type": "Shareholder", "account_type": "Payable"}, - {'doctype': "Party Type", "party_type": "Student", "account_type": "Receivable"}, - - {'doctype': "Opportunity Type", "name": _("Sales")}, - {'doctype': "Opportunity Type", "name": _("Support")}, - {'doctype': "Opportunity Type", "name": _("Maintenance")}, - - {'doctype': "Project Type", "project_type": "Internal"}, - {'doctype': "Project Type", "project_type": "External"}, - {'doctype': "Project Type", "project_type": "Other"}, - + {"doctype": "Issue Priority", "name": _("Low")}, + {"doctype": "Issue Priority", "name": _("Medium")}, + {"doctype": "Issue Priority", "name": _("High")}, + # Job Applicant Source + {"doctype": "Job Applicant Source", "source_name": _("Website Listing")}, + {"doctype": "Job Applicant Source", "source_name": _("Walk In")}, + {"doctype": "Job Applicant Source", "source_name": _("Employee Referral")}, + {"doctype": "Job Applicant Source", "source_name": _("Campaign")}, + {"doctype": "Email Account", "email_id": "sales@example.com", "append_to": "Opportunity"}, + {"doctype": "Email Account", "email_id": "support@example.com", "append_to": "Issue"}, + {"doctype": "Email Account", "email_id": "jobs@example.com", "append_to": "Job Applicant"}, + {"doctype": "Party Type", "party_type": "Customer", "account_type": "Receivable"}, + {"doctype": "Party Type", "party_type": "Supplier", "account_type": "Payable"}, + {"doctype": "Party Type", "party_type": "Employee", "account_type": "Payable"}, + {"doctype": "Party Type", "party_type": "Shareholder", "account_type": "Payable"}, + {"doctype": "Party Type", "party_type": "Student", "account_type": "Receivable"}, + {"doctype": "Opportunity Type", "name": _("Sales")}, + {"doctype": "Opportunity Type", "name": _("Support")}, + {"doctype": "Opportunity Type", "name": _("Maintenance")}, + {"doctype": "Project Type", "project_type": "Internal"}, + {"doctype": "Project Type", "project_type": "External"}, + {"doctype": "Project Type", "project_type": "Other"}, {"doctype": "Offer Term", "offer_term": _("Date of Joining")}, {"doctype": "Offer Term", "offer_term": _("Annual Salary")}, {"doctype": "Offer Term", "offer_term": _("Probationary Period")}, @@ -218,23 +395,22 @@ def install(country=None): {"doctype": "Offer Term", "offer_term": _("Leaves per Year")}, {"doctype": "Offer Term", "offer_term": _("Notice Period")}, {"doctype": "Offer Term", "offer_term": _("Incentives")}, - - {'doctype': "Print Heading", 'print_heading': _("Credit Note")}, - {'doctype': "Print Heading", 'print_heading': _("Debit Note")}, - + {"doctype": "Print Heading", "print_heading": _("Credit Note")}, + {"doctype": "Print Heading", "print_heading": _("Debit Note")}, # Assessment Group - {'doctype': 'Assessment Group', 'assessment_group_name': _('All Assessment Groups'), - 'is_group': 1, 'parent_assessment_group': ''}, - + { + "doctype": "Assessment Group", + "assessment_group_name": _("All Assessment Groups"), + "is_group": 1, + "parent_assessment_group": "", + }, # Share Management {"doctype": "Share Type", "title": _("Equity")}, {"doctype": "Share Type", "title": _("Preference")}, - # Market Segments {"doctype": "Market Segment", "market_segment": _("Lower Income")}, {"doctype": "Market Segment", "market_segment": _("Middle Income")}, {"doctype": "Market Segment", "market_segment": _("Upper Income")}, - # Sales Stages {"doctype": "Sales Stage", "stage_name": _("Prospecting")}, {"doctype": "Sales Stage", "stage_name": _("Qualification")}, @@ -244,47 +420,101 @@ def install(country=None): {"doctype": "Sales Stage", "stage_name": _("Perception Analysis")}, {"doctype": "Sales Stage", "stage_name": _("Proposal/Price Quote")}, {"doctype": "Sales Stage", "stage_name": _("Negotiation/Review")}, - # Warehouse Type - {'doctype': 'Warehouse Type', 'name': 'Transit'}, + {"doctype": "Warehouse Type", "name": "Transit"}, ] from erpnext.setup.setup_wizard.data.industry_type import get_industry_types - records += [{"doctype":"Industry Type", "industry": d} for d in get_industry_types()] - # records += [{"doctype":"Operation", "operation": d} for d in get_operations()] - records += [{'doctype': 'Lead Source', 'source_name': _(d)} for d in default_lead_sources] - records += [{'doctype': 'Sales Partner Type', 'sales_partner_type': _(d)} for d in default_sales_partner_type] + records += [{"doctype": "Industry Type", "industry": d} for d in get_industry_types()] + # records += [{"doctype":"Operation", "operation": d} for d in get_operations()] + records += [{"doctype": "Lead Source", "source_name": _(d)} for d in default_lead_sources] + + records += [ + {"doctype": "Sales Partner Type", "sales_partner_type": _(d)} for d in default_sales_partner_type + ] base_path = frappe.get_app_path("erpnext", "hr", "doctype") - response = frappe.read_file(os.path.join(base_path, "leave_application/leave_application_email_template.html")) + response = frappe.read_file( + os.path.join(base_path, "leave_application/leave_application_email_template.html") + ) - records += [{'doctype': 'Email Template', 'name': _("Leave Approval Notification"), 'response': response, - 'subject': _("Leave Approval Notification"), 'owner': frappe.session.user}] + records += [ + { + "doctype": "Email Template", + "name": _("Leave Approval Notification"), + "response": response, + "subject": _("Leave Approval Notification"), + "owner": frappe.session.user, + } + ] - records += [{'doctype': 'Email Template', 'name': _("Leave Status Notification"), 'response': response, - 'subject': _("Leave Status Notification"), 'owner': frappe.session.user}] + records += [ + { + "doctype": "Email Template", + "name": _("Leave Status Notification"), + "response": response, + "subject": _("Leave Status Notification"), + "owner": frappe.session.user, + } + ] - response = frappe.read_file(os.path.join(base_path, "interview/interview_reminder_notification_template.html")) + response = frappe.read_file( + os.path.join(base_path, "interview/interview_reminder_notification_template.html") + ) - records += [{'doctype': 'Email Template', 'name': _('Interview Reminder'), 'response': response, - 'subject': _('Interview Reminder'), 'owner': frappe.session.user}] + records += [ + { + "doctype": "Email Template", + "name": _("Interview Reminder"), + "response": response, + "subject": _("Interview Reminder"), + "owner": frappe.session.user, + } + ] - response = frappe.read_file(os.path.join(base_path, "interview/interview_feedback_reminder_template.html")) + response = frappe.read_file( + os.path.join(base_path, "interview/interview_feedback_reminder_template.html") + ) - records += [{'doctype': 'Email Template', 'name': _('Interview Feedback Reminder'), 'response': response, - 'subject': _('Interview Feedback Reminder'), 'owner': frappe.session.user}] + records += [ + { + "doctype": "Email Template", + "name": _("Interview Feedback Reminder"), + "response": response, + "subject": _("Interview Feedback Reminder"), + "owner": frappe.session.user, + } + ] - response = frappe.read_file(os.path.join(base_path, 'exit_interview/exit_questionnaire_notification_template.html')) + response = frappe.read_file( + os.path.join(base_path, "exit_interview/exit_questionnaire_notification_template.html") + ) - records += [{'doctype': 'Email Template', 'name': _('Exit Questionnaire Notification'), 'response': response, - 'subject': _('Exit Questionnaire Notification'), 'owner': frappe.session.user}] + records += [ + { + "doctype": "Email Template", + "name": _("Exit Questionnaire Notification"), + "response": response, + "subject": _("Exit Questionnaire Notification"), + "owner": frappe.session.user, + } + ] base_path = frappe.get_app_path("erpnext", "stock", "doctype") - response = frappe.read_file(os.path.join(base_path, "delivery_trip/dispatch_notification_template.html")) + response = frappe.read_file( + os.path.join(base_path, "delivery_trip/dispatch_notification_template.html") + ) - records += [{'doctype': 'Email Template', 'name': _("Dispatch Notification"), 'response': response, - 'subject': _("Your order is out for delivery!"), 'owner': frappe.session.user}] + records += [ + { + "doctype": "Email Template", + "name": _("Dispatch Notification"), + "response": response, + "subject": _("Your order is out for delivery!"), + "owner": frappe.session.user, + } + ] # Records for the Supplier Scorecard from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import make_default_records @@ -295,6 +525,7 @@ def install(country=None): set_more_defaults() update_global_search_doctypes() + def set_more_defaults(): # Do more setup stuff that can be done here with no dependencies update_selling_defaults() @@ -303,6 +534,7 @@ def set_more_defaults(): add_uom_data() update_item_variant_settings() + def update_selling_defaults(): selling_settings = frappe.get_doc("Selling Settings") selling_settings.cust_master_name = "Customer Name" @@ -312,6 +544,7 @@ def update_selling_defaults(): selling_settings.sales_update_frequency = "Each Transaction" selling_settings.save() + def update_buying_defaults(): buying_settings = frappe.get_doc("Buying Settings") buying_settings.supp_master_name = "Supplier Name" @@ -321,6 +554,7 @@ def update_buying_defaults(): buying_settings.allow_multiple_items = 1 buying_settings.save() + def update_hr_defaults(): hr_settings = frappe.get_doc("HR Settings") hr_settings.emp_created_by = "Naming Series" @@ -336,53 +570,66 @@ def update_hr_defaults(): hr_settings.save() + def update_item_variant_settings(): # set no copy fields of an item doctype to item variant settings - doc = frappe.get_doc('Item Variant Settings') + doc = frappe.get_doc("Item Variant Settings") doc.set_default_fields() doc.save() + def add_uom_data(): # add UOMs - uoms = json.loads(open(frappe.get_app_path("erpnext", "setup", "setup_wizard", "data", "uom_data.json")).read()) + uoms = json.loads( + open(frappe.get_app_path("erpnext", "setup", "setup_wizard", "data", "uom_data.json")).read() + ) for d in uoms: - if not frappe.db.exists('UOM', _(d.get("uom_name"))): - uom_doc = frappe.get_doc({ - "doctype": "UOM", - "uom_name": _(d.get("uom_name")), - "name": _(d.get("uom_name")), - "must_be_whole_number": d.get("must_be_whole_number"), - "enabled": 1, - }).db_insert() + if not frappe.db.exists("UOM", _(d.get("uom_name"))): + uom_doc = frappe.get_doc( + { + "doctype": "UOM", + "uom_name": _(d.get("uom_name")), + "name": _(d.get("uom_name")), + "must_be_whole_number": d.get("must_be_whole_number"), + "enabled": 1, + } + ).db_insert() # bootstrap uom conversion factors - uom_conversions = json.loads(open(frappe.get_app_path("erpnext", "setup", "setup_wizard", "data", "uom_conversion_data.json")).read()) + uom_conversions = json.loads( + open( + frappe.get_app_path("erpnext", "setup", "setup_wizard", "data", "uom_conversion_data.json") + ).read() + ) for d in uom_conversions: if not frappe.db.exists("UOM Category", _(d.get("category"))): - frappe.get_doc({ - "doctype": "UOM Category", - "category_name": _(d.get("category")) - }).db_insert() + frappe.get_doc({"doctype": "UOM Category", "category_name": _(d.get("category"))}).db_insert() + + if not frappe.db.exists( + "UOM Conversion Factor", {"from_uom": _(d.get("from_uom")), "to_uom": _(d.get("to_uom"))} + ): + uom_conversion = frappe.get_doc( + { + "doctype": "UOM Conversion Factor", + "category": _(d.get("category")), + "from_uom": _(d.get("from_uom")), + "to_uom": _(d.get("to_uom")), + "value": d.get("value"), + } + ).insert(ignore_permissions=True) - if not frappe.db.exists("UOM Conversion Factor", {"from_uom": _(d.get("from_uom")), "to_uom": _(d.get("to_uom"))}): - uom_conversion = frappe.get_doc({ - "doctype": "UOM Conversion Factor", - "category": _(d.get("category")), - "from_uom": _(d.get("from_uom")), - "to_uom": _(d.get("to_uom")), - "value": d.get("value") - }).insert(ignore_permissions=True) def add_market_segments(): records = [ # Market Segments {"doctype": "Market Segment", "market_segment": _("Lower Income")}, {"doctype": "Market Segment", "market_segment": _("Middle Income")}, - {"doctype": "Market Segment", "market_segment": _("Upper Income")} + {"doctype": "Market Segment", "market_segment": _("Upper Income")}, ] make_records(records) + def add_sale_stages(): # Sale Stages records = [ @@ -393,33 +640,33 @@ def add_sale_stages(): {"doctype": "Sales Stage", "stage_name": _("Identifying Decision Makers")}, {"doctype": "Sales Stage", "stage_name": _("Perception Analysis")}, {"doctype": "Sales Stage", "stage_name": _("Proposal/Price Quote")}, - {"doctype": "Sales Stage", "stage_name": _("Negotiation/Review")} + {"doctype": "Sales Stage", "stage_name": _("Negotiation/Review")}, ] for sales_stage in records: frappe.get_doc(sales_stage).db_insert() + def install_company(args): records = [ # Fiscal Year { - 'doctype': "Fiscal Year", - 'year': get_fy_details(args.fy_start_date, args.fy_end_date), - 'year_start_date': args.fy_start_date, - 'year_end_date': args.fy_end_date + "doctype": "Fiscal Year", + "year": get_fy_details(args.fy_start_date, args.fy_end_date), + "year_start_date": args.fy_start_date, + "year_end_date": args.fy_end_date, }, - # Company { - "doctype":"Company", - 'company_name': args.company_name, - 'enable_perpetual_inventory': 1, - 'abbr': args.company_abbr, - 'default_currency': args.currency, - 'country': args.country, - 'create_chart_of_accounts_based_on': 'Standard Template', - 'chart_of_accounts': args.chart_of_accounts, - 'domain': args.domain - } + "doctype": "Company", + "company_name": args.company_name, + "enable_perpetual_inventory": 1, + "abbr": args.company_abbr, + "default_currency": args.currency, + "country": args.country, + "create_chart_of_accounts_based_on": "Standard Template", + "chart_of_accounts": args.chart_of_accounts, + "domain": args.domain, + }, ] make_records(records) @@ -428,20 +675,90 @@ def install_company(args): def install_post_company_fixtures(args=None): records = [ # Department - {'doctype': 'Department', 'department_name': _('All Departments'), 'is_group': 1, 'parent_department': ''}, - {'doctype': 'Department', 'department_name': _('Accounts'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Marketing'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Sales'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Purchase'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Operations'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Production'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Dispatch'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Customer Service'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Human Resources'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Management'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Quality Management'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Research & Development'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Legal'), 'parent_department': _('All Departments'), 'company': args.company_name}, + { + "doctype": "Department", + "department_name": _("All Departments"), + "is_group": 1, + "parent_department": "", + }, + { + "doctype": "Department", + "department_name": _("Accounts"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Marketing"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Sales"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Purchase"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Operations"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Production"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Dispatch"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Customer Service"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Human Resources"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Management"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Quality Management"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Research & Development"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Legal"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, ] # Make root department with NSM updation @@ -456,8 +773,22 @@ def install_post_company_fixtures(args=None): def install_defaults(args=None): records = [ # Price Lists - { "doctype": "Price List", "price_list_name": _("Standard Buying"), "enabled": 1, "buying": 1, "selling": 0, "currency": args.currency }, - { "doctype": "Price List", "price_list_name": _("Standard Selling"), "enabled": 1, "buying": 0, "selling": 1, "currency": args.currency }, + { + "doctype": "Price List", + "price_list_name": _("Standard Buying"), + "enabled": 1, + "buying": 1, + "selling": 0, + "currency": args.currency, + }, + { + "doctype": "Price List", + "price_list_name": _("Standard Selling"), + "enabled": 1, + "buying": 0, + "selling": 1, + "currency": args.currency, + }, ] make_records(records) @@ -474,27 +805,34 @@ def install_defaults(args=None): args.update({"set_default": 1}) create_bank_account(args) + def set_global_defaults(args): global_defaults = frappe.get_doc("Global Defaults", "Global Defaults") current_fiscal_year = frappe.get_all("Fiscal Year")[0] - global_defaults.update({ - 'current_fiscal_year': current_fiscal_year.name, - 'default_currency': args.get('currency'), - 'default_company':args.get('company_name') , - "country": args.get("country"), - }) + global_defaults.update( + { + "current_fiscal_year": current_fiscal_year.name, + "default_currency": args.get("currency"), + "default_company": args.get("company_name"), + "country": args.get("country"), + } + ) global_defaults.save() + def set_active_domains(args): - frappe.get_single('Domain Settings').set_active_domains(args.get('domains')) + frappe.get_single("Domain Settings").set_active_domains(args.get("domains")) + def update_stock_settings(): stock_settings = frappe.get_doc("Stock Settings") stock_settings.item_naming_by = "Item Code" stock_settings.valuation_method = "FIFO" - stock_settings.default_warehouse = frappe.db.get_value('Warehouse', {'warehouse_name': _('Stores')}) + stock_settings.default_warehouse = frappe.db.get_value( + "Warehouse", {"warehouse_name": _("Stores")} + ) stock_settings.stock_uom = _("Nos") stock_settings.auto_indent = 1 stock_settings.auto_insert_price_list_rate_if_missing = 1 @@ -502,52 +840,65 @@ def update_stock_settings(): stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1 stock_settings.save() + def create_bank_account(args): - if not args.get('bank_account'): + if not args.get("bank_account"): return - company_name = args.get('company_name') - bank_account_group = frappe.db.get_value("Account", - {"account_type": "Bank", "is_group": 1, "root_type": "Asset", - "company": company_name}) + company_name = args.get("company_name") + bank_account_group = frappe.db.get_value( + "Account", {"account_type": "Bank", "is_group": 1, "root_type": "Asset", "company": company_name} + ) if bank_account_group: - bank_account = frappe.get_doc({ - "doctype": "Account", - 'account_name': args.get('bank_account'), - 'parent_account': bank_account_group, - 'is_group':0, - 'company': company_name, - "account_type": "Bank", - }) + bank_account = frappe.get_doc( + { + "doctype": "Account", + "account_name": args.get("bank_account"), + "parent_account": bank_account_group, + "is_group": 0, + "company": company_name, + "account_type": "Bank", + } + ) try: doc = bank_account.insert() - if args.get('set_default'): - frappe.db.set_value("Company", args.get('company_name'), "default_bank_account", bank_account.name, update_modified=False) + if args.get("set_default"): + frappe.db.set_value( + "Company", + args.get("company_name"), + "default_bank_account", + bank_account.name, + update_modified=False, + ) return doc except RootNotEditable: - frappe.throw(_("Bank account cannot be named as {0}").format(args.get('bank_account'))) + frappe.throw(_("Bank account cannot be named as {0}").format(args.get("bank_account"))) except frappe.DuplicateEntryError: # bank account same as a CoA entry pass -def update_shopping_cart_settings(args): # nosemgrep + +def update_shopping_cart_settings(args): # nosemgrep shopping_cart = frappe.get_doc("E Commerce Settings") - shopping_cart.update({ - "enabled": 1, - 'company': args.company_name, - 'price_list': frappe.db.get_value("Price List", {"selling": 1}), - 'default_customer_group': _("Individual"), - 'quotation_series': "QTN-", - }) + shopping_cart.update( + { + "enabled": 1, + "company": args.company_name, + "price_list": frappe.db.get_value("Price List", {"selling": 1}), + "default_customer_group": _("Individual"), + "quotation_series": "QTN-", + } + ) shopping_cart.update_single(shopping_cart.get_valid_dict()) + def get_fy_details(fy_start_date, fy_end_date): start_year = getdate(fy_start_date).year if start_year == getdate(fy_end_date).year: fy = cstr(start_year) else: - fy = cstr(start_year) + '-' + cstr(start_year + 1) + fy = cstr(start_year) + "-" + cstr(start_year + 1) return fy diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index 289ffa58b8..39dc7e3327 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -10,11 +10,11 @@ from frappe import _ def setup_taxes_and_charges(company_name: str, country: str): - if not frappe.db.exists('Company', company_name): - frappe.throw(_('Company {} does not exist yet. Taxes setup aborted.').format(company_name)) + if not frappe.db.exists("Company", company_name): + frappe.throw(_("Company {} does not exist yet. Taxes setup aborted.").format(company_name)) - file_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'country_wise_tax.json') - with open(file_path, 'r') as json_file: + file_path = os.path.join(os.path.dirname(__file__), "..", "data", "country_wise_tax.json") + with open(file_path, "r") as json_file: tax_data = json.load(json_file) country_wise_tax = tax_data.get(country) @@ -22,7 +22,7 @@ def setup_taxes_and_charges(company_name: str, country: str): if not country_wise_tax: return - if 'chart_of_accounts' not in country_wise_tax: + if "chart_of_accounts" not in country_wise_tax: country_wise_tax = simple_to_detailed(country_wise_tax) from_detailed_data(company_name, country_wise_tax) @@ -36,39 +36,44 @@ def simple_to_detailed(templates): Example input: { - "France VAT 20%": { - "account_name": "VAT 20%", - "tax_rate": 20, - "default": 1 - }, - "France VAT 10%": { - "account_name": "VAT 10%", - "tax_rate": 10 - } + "France VAT 20%": { + "account_name": "VAT 20%", + "tax_rate": 20, + "default": 1 + }, + "France VAT 10%": { + "account_name": "VAT 10%", + "tax_rate": 10 + } } """ return { - 'chart_of_accounts': { - '*': { - 'item_tax_templates': [{ - 'title': title, - 'taxes': [{ - 'tax_type': { - 'account_name': data.get('account_name'), - 'tax_rate': data.get('tax_rate') - } - }] - } for title, data in templates.items()], - '*': [{ - 'title': title, - 'is_default': data.get('default', 0), - 'taxes': [{ - 'account_head': { - 'account_name': data.get('account_name'), - 'tax_rate': data.get('tax_rate') - } - }] - } for title, data in templates.items()] + "chart_of_accounts": { + "*": { + "item_tax_templates": [ + { + "title": title, + "taxes": [ + {"tax_type": {"account_name": data.get("account_name"), "tax_rate": data.get("tax_rate")}} + ], + } + for title, data in templates.items() + ], + "*": [ + { + "title": title, + "is_default": data.get("default", 0), + "taxes": [ + { + "account_head": { + "account_name": data.get("account_name"), + "tax_rate": data.get("tax_rate"), + } + } + ], + } + for title, data in templates.items() + ], } } } @@ -76,13 +81,13 @@ def simple_to_detailed(templates): def from_detailed_data(company_name, data): """Create Taxes and Charges Templates from detailed data.""" - coa_name = frappe.db.get_value('Company', company_name, 'chart_of_accounts') - coa_data = data.get('chart_of_accounts', {}) - tax_templates = coa_data.get(coa_name) or coa_data.get('*', {}) - tax_categories = data.get('tax_categories') - sales_tax_templates = tax_templates.get('sales_tax_templates') or tax_templates.get('*', {}) - purchase_tax_templates = tax_templates.get('purchase_tax_templates') or tax_templates.get('*', {}) - item_tax_templates = tax_templates.get('item_tax_templates') or tax_templates.get('*', {}) + coa_name = frappe.db.get_value("Company", company_name, "chart_of_accounts") + coa_data = data.get("chart_of_accounts", {}) + tax_templates = coa_data.get(coa_name) or coa_data.get("*", {}) + tax_categories = data.get("tax_categories") + sales_tax_templates = tax_templates.get("sales_tax_templates") or tax_templates.get("*", {}) + purchase_tax_templates = tax_templates.get("purchase_tax_templates") or tax_templates.get("*", {}) + item_tax_templates = tax_templates.get("item_tax_templates") or tax_templates.get("*", {}) if tax_categories: for tax_category in tax_categories: @@ -90,11 +95,11 @@ def from_detailed_data(company_name, data): if sales_tax_templates: for template in sales_tax_templates: - make_taxes_and_charges_template(company_name, 'Sales Taxes and Charges Template', template) + make_taxes_and_charges_template(company_name, "Sales Taxes and Charges Template", template) if purchase_tax_templates: for template in purchase_tax_templates: - make_taxes_and_charges_template(company_name, 'Purchase Taxes and Charges Template', template) + make_taxes_and_charges_template(company_name, "Purchase Taxes and Charges Template", template) if item_tax_templates: for template in item_tax_templates: @@ -102,40 +107,45 @@ def from_detailed_data(company_name, data): def update_regional_tax_settings(country, company): - path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(country)) + path = frappe.get_app_path("erpnext", "regional", frappe.scrub(country)) if os.path.exists(path.encode("utf-8")): try: - module_name = "erpnext.regional.{0}.setup.update_regional_tax_settings".format(frappe.scrub(country)) + module_name = "erpnext.regional.{0}.setup.update_regional_tax_settings".format( + frappe.scrub(country) + ) frappe.get_attr(module_name)(country, company) except Exception as e: # Log error and ignore if failed to setup regional tax settings frappe.log_error() pass -def make_taxes_and_charges_template(company_name, doctype, template): - template['company'] = company_name - template['doctype'] = doctype - if frappe.db.exists(doctype, {'title': template.get('title'), 'company': company_name}): +def make_taxes_and_charges_template(company_name, doctype, template): + template["company"] = company_name + template["doctype"] = doctype + + if frappe.db.exists(doctype, {"title": template.get("title"), "company": company_name}): return - for tax_row in template.get('taxes'): - account_data = tax_row.get('account_head') + for tax_row in template.get("taxes"): + account_data = tax_row.get("account_head") tax_row_defaults = { - 'category': 'Total', - 'charge_type': 'On Net Total', - 'cost_center': frappe.db.get_value('Company', company_name, 'cost_center') + "category": "Total", + "charge_type": "On Net Total", + "cost_center": frappe.db.get_value("Company", company_name, "cost_center"), } - if doctype == 'Purchase Taxes and Charges Template': - tax_row_defaults['add_deduct_tax'] = 'Add' + if doctype == "Purchase Taxes and Charges Template": + tax_row_defaults["add_deduct_tax"] = "Add" # if account_head is a dict, search or create the account and get it's name if isinstance(account_data, dict): - tax_row_defaults['description'] = '{0} @ {1}'.format(account_data.get('account_name'), account_data.get('tax_rate')) - tax_row_defaults['rate'] = account_data.get('tax_rate') + tax_row_defaults["description"] = "{0} @ {1}".format( + account_data.get("account_name"), account_data.get("tax_rate") + ) + tax_row_defaults["rate"] = account_data.get("tax_rate") account = get_or_create_account(company_name, account_data) - tax_row['account_head'] = account.name + tax_row["account_head"] = account.name # use the default value if nothing other is specified for fieldname, default_value in tax_row_defaults.items(): @@ -151,28 +161,29 @@ def make_taxes_and_charges_template(company_name, doctype, template): doc.insert(ignore_permissions=True) return doc + def make_item_tax_template(company_name, template): """Create an Item Tax Template. This requires a separate method because Item Tax Template is structured differently from Sales and Purchase Tax Templates. """ - doctype = 'Item Tax Template' - template['company'] = company_name - template['doctype'] = doctype + doctype = "Item Tax Template" + template["company"] = company_name + template["doctype"] = doctype - if frappe.db.exists(doctype, {'title': template.get('title'), 'company': company_name}): + if frappe.db.exists(doctype, {"title": template.get("title"), "company": company_name}): return - for tax_row in template.get('taxes'): - account_data = tax_row.get('tax_type') + for tax_row in template.get("taxes"): + account_data = tax_row.get("tax_type") # if tax_type is a dict, search or create the account and get it's name if isinstance(account_data, dict): account = get_or_create_account(company_name, account_data) - tax_row['tax_type'] = account.name - if 'tax_rate' not in tax_row: - tax_row['tax_rate'] = account_data.get('tax_rate') + tax_row["tax_type"] = account.name + if "tax_rate" not in tax_row: + tax_row["tax_rate"] = account_data.get("tax_rate") doc = frappe.get_doc(template) @@ -183,36 +194,36 @@ def make_item_tax_template(company_name, template): doc.insert(ignore_permissions=True) return doc + def get_or_create_account(company_name, account): """ Check if account already exists. If not, create it. Return a tax account or None. """ - default_root_type = 'Liability' - root_type = account.get('root_type', default_root_type) + default_root_type = "Liability" + root_type = account.get("root_type", default_root_type) - existing_accounts = frappe.get_all('Account', - filters={ - 'company': company_name, - 'root_type': root_type - }, + existing_accounts = frappe.get_all( + "Account", + filters={"company": company_name, "root_type": root_type}, or_filters={ - 'account_name': account.get('account_name'), - 'account_number': account.get('account_number') - }) + "account_name": account.get("account_name"), + "account_number": account.get("account_number"), + }, + ) if existing_accounts: - return frappe.get_doc('Account', existing_accounts[0].name) + return frappe.get_doc("Account", existing_accounts[0].name) tax_group = get_or_create_tax_group(company_name, root_type) - account['doctype'] = 'Account' - account['company'] = company_name - account['parent_account'] = tax_group - account['report_type'] = 'Balance Sheet' - account['account_type'] = 'Tax' - account['root_type'] = root_type - account['is_group'] = 0 + account["doctype"] = "Account" + account["company"] = company_name + account["parent_account"] = tax_group + account["report_type"] = "Balance Sheet" + account["account_type"] = "Tax" + account["root_type"] = root_type + account["is_group"] = 0 doc = frappe.get_doc(account) doc.flags.ignore_links = True @@ -220,50 +231,53 @@ def get_or_create_account(company_name, account): doc.insert(ignore_permissions=True, ignore_mandatory=True) return doc + def get_or_create_tax_group(company_name, root_type): # Look for a group account of type 'Tax' - tax_group_name = frappe.db.get_value('Account', { - 'is_group': 1, - 'root_type': root_type, - 'account_type': 'Tax', - 'company': company_name - }) + tax_group_name = frappe.db.get_value( + "Account", + {"is_group": 1, "root_type": root_type, "account_type": "Tax", "company": company_name}, + ) if tax_group_name: return tax_group_name # Look for a group account named 'Duties and Taxes' or 'Tax Assets' - account_name = _('Duties and Taxes') if root_type == 'Liability' else _('Tax Assets') - tax_group_name = frappe.db.get_value('Account', { - 'is_group': 1, - 'root_type': root_type, - 'account_name': account_name, - 'company': company_name - }) + account_name = _("Duties and Taxes") if root_type == "Liability" else _("Tax Assets") + tax_group_name = frappe.db.get_value( + "Account", + {"is_group": 1, "root_type": root_type, "account_name": account_name, "company": company_name}, + ) if tax_group_name: return tax_group_name # Create a new group account named 'Duties and Taxes' or 'Tax Assets' just # below the root account - root_account = frappe.get_all('Account', { - 'is_group': 1, - 'root_type': root_type, - 'company': company_name, - 'report_type': 'Balance Sheet', - 'parent_account': ('is', 'not set') - }, limit=1)[0] + root_account = frappe.get_all( + "Account", + { + "is_group": 1, + "root_type": root_type, + "company": company_name, + "report_type": "Balance Sheet", + "parent_account": ("is", "not set"), + }, + limit=1, + )[0] - tax_group_account = frappe.get_doc({ - 'doctype': 'Account', - 'company': company_name, - 'is_group': 1, - 'report_type': 'Balance Sheet', - 'root_type': root_type, - 'account_type': 'Tax', - 'account_name': account_name, - 'parent_account': root_account.name - }) + tax_group_account = frappe.get_doc( + { + "doctype": "Account", + "company": company_name, + "is_group": 1, + "report_type": "Balance Sheet", + "root_type": root_type, + "account_type": "Tax", + "account_name": account_name, + "parent_account": root_account.name, + } + ) tax_group_account.flags.ignore_links = True tax_group_account.flags.ignore_validate = True @@ -275,11 +289,11 @@ def get_or_create_tax_group(company_name, root_type): def make_tax_category(tax_category): - doctype = 'Tax Category' + doctype = "Tax Category" if isinstance(tax_category, str): - tax_category = {'title': tax_category} + tax_category = {"title": tax_category} - tax_category['doctype'] = doctype - if not frappe.db.exists(doctype, tax_category['title']): + tax_category["doctype"] = doctype + if not frappe.db.exists(doctype, tax_category["title"]): doc = frappe.get_doc(tax_category) doc.insert(ignore_permissions=True) diff --git a/erpnext/setup/setup_wizard/setup_wizard.py b/erpnext/setup/setup_wizard/setup_wizard.py index 239e145352..4cae5191a6 100644 --- a/erpnext/setup/setup_wizard/setup_wizard.py +++ b/erpnext/setup/setup_wizard/setup_wizard.py @@ -13,91 +13,60 @@ def get_setup_stages(args=None): if frappe.db.sql("select name from tabCompany"): stages = [ { - 'status': _('Wrapping up'), - 'fail_msg': _('Failed to login'), - 'tasks': [ - { - 'fn': fin, - 'args': args, - 'fail_msg': _("Failed to login") - } - ] + "status": _("Wrapping up"), + "fail_msg": _("Failed to login"), + "tasks": [{"fn": fin, "args": args, "fail_msg": _("Failed to login")}], } ] else: stages = [ { - 'status': _('Installing presets'), - 'fail_msg': _('Failed to install presets'), - 'tasks': [ - { - 'fn': stage_fixtures, - 'args': args, - 'fail_msg': _("Failed to install presets") - } - ] + "status": _("Installing presets"), + "fail_msg": _("Failed to install presets"), + "tasks": [{"fn": stage_fixtures, "args": args, "fail_msg": _("Failed to install presets")}], }, { - 'status': _('Setting up company'), - 'fail_msg': _('Failed to setup company'), - 'tasks': [ - { - 'fn': setup_company, - 'args': args, - 'fail_msg': _("Failed to setup company") - } - ] + "status": _("Setting up company"), + "fail_msg": _("Failed to setup company"), + "tasks": [{"fn": setup_company, "args": args, "fail_msg": _("Failed to setup company")}], }, { - 'status': _('Setting defaults'), - 'fail_msg': 'Failed to set defaults', - 'tasks': [ - { - 'fn': setup_defaults, - 'args': args, - 'fail_msg': _("Failed to setup defaults") - }, - { - 'fn': stage_four, - 'args': args, - 'fail_msg': _("Failed to create website") - }, - { - 'fn': set_active_domains, - 'args': args, - 'fail_msg': _("Failed to add Domain") - }, - ] + "status": _("Setting defaults"), + "fail_msg": "Failed to set defaults", + "tasks": [ + {"fn": setup_defaults, "args": args, "fail_msg": _("Failed to setup defaults")}, + {"fn": stage_four, "args": args, "fail_msg": _("Failed to create website")}, + {"fn": set_active_domains, "args": args, "fail_msg": _("Failed to add Domain")}, + ], }, { - 'status': _('Wrapping up'), - 'fail_msg': _('Failed to login'), - 'tasks': [ - { - 'fn': fin, - 'args': args, - 'fail_msg': _("Failed to login") - } - ] - } + "status": _("Wrapping up"), + "fail_msg": _("Failed to login"), + "tasks": [{"fn": fin, "args": args, "fail_msg": _("Failed to login")}], + }, ] return stages + def stage_fixtures(args): - fixtures.install(args.get('country')) + fixtures.install(args.get("country")) + def setup_company(args): fixtures.install_company(args) + def setup_defaults(args): fixtures.install_defaults(frappe._dict(args)) + def stage_four(args): company_setup.create_website(args) company_setup.create_email_digest() company_setup.create_logo(args) + def fin(args): frappe.local.message_log = [] login_as_first_user(args) @@ -116,6 +85,7 @@ def setup_complete(args=None): stage_four(args) fin(args) + def set_active_domains(args): - domain_settings = frappe.get_single('Domain Settings') - domain_settings.set_active_domains(args.get('domains')) + domain_settings = frappe.get_single("Domain Settings") + domain_settings.set_active_domains(args.get("domains")) diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index 01884d9a25..2200e6cd1d 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -17,23 +17,25 @@ def before_tests(): if not frappe.db.a_row_exists("Company"): current_year = now_datetime().year - setup_complete({ - "currency" :"USD", - "full_name" :"Test User", - "company_name" :"Wind Power LLC", - "timezone" :"America/New_York", - "company_abbr" :"WP", - "industry" :"Manufacturing", - "country" :"United States", - "fy_start_date" :f"{current_year}-01-01", - "fy_end_date" :f"{current_year}-12-31", - "language" :"english", - "company_tagline" :"Testing", - "email" :"test@erpnext.com", - "password" :"test", - "chart_of_accounts" : "Standard", - "domains" : ["Manufacturing"], - }) + setup_complete( + { + "currency": "USD", + "full_name": "Test User", + "company_name": "Wind Power LLC", + "timezone": "America/New_York", + "company_abbr": "WP", + "industry": "Manufacturing", + "country": "United States", + "fy_start_date": f"{current_year}-01-01", + "fy_end_date": f"{current_year}-12-31", + "language": "english", + "company_tagline": "Testing", + "email": "test@erpnext.com", + "password": "test", + "chart_of_accounts": "Standard", + "domains": ["Manufacturing"], + } + ) _enable_all_domains() frappe.db.sql("delete from `tabLeave Allocation`") @@ -47,6 +49,7 @@ def before_tests(): frappe.db.commit() + @frappe.whitelist() def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=None): if not (from_currency and to_currency): @@ -63,7 +66,7 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No filters = [ ["date", "<=", get_datetime_str(transaction_date)], ["from_currency", "=", from_currency], - ["to_currency", "=", to_currency] + ["to_currency", "=", to_currency], ] if args == "for_buying": @@ -78,8 +81,8 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No # cksgb 19/09/2016: get last entry in Currency Exchange with from_currency and to_currency. entries = frappe.get_all( - "Currency Exchange", fields=["exchange_rate"], filters=filters, order_by="date desc", - limit=1) + "Currency Exchange", fields=["exchange_rate"], filters=filters, order_by="date desc", limit=1 + ) if entries: return flt(entries[0].exchange_rate) @@ -90,11 +93,12 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No if not value: import requests - settings = frappe.get_cached_doc('Currency Exchange Settings') + + settings = frappe.get_cached_doc("Currency Exchange Settings") req_params = { "transaction_date": transaction_date, "from_currency": from_currency, - "to_currency": to_currency + "to_currency": to_currency, } params = {} for row in settings.req_params: @@ -109,18 +113,24 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No return flt(value) except Exception: frappe.log_error(title="Get Exchange Rate") - frappe.msgprint(_("Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually").format(from_currency, to_currency, transaction_date)) + frappe.msgprint( + _( + "Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually" + ).format(from_currency, to_currency, transaction_date) + ) return 0.0 + def format_ces_api(data, param): return data.format( transaction_date=param.get("transaction_date"), to_currency=param.get("to_currency"), - from_currency=param.get("from_currency") + from_currency=param.get("from_currency"), ) + def enable_all_roles_and_domains(): - """ enable all roles and domain for testing """ + """enable all roles and domain for testing""" _enable_all_domains() _enable_all_roles_for_admin() @@ -129,18 +139,19 @@ def _enable_all_domains(): domains = frappe.get_all("Domain", pluck="name") if not domains: return - frappe.get_single('Domain Settings').set_active_domains(domains) + frappe.get_single("Domain Settings").set_active_domains(domains) def _enable_all_roles_for_admin(): from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to all_roles = set(frappe.db.get_values("Role", pluck="name")) - admin_roles = set(frappe.db.get_values("Has Role", - {"parent": "Administrator"}, fieldname="role", pluck="role")) + admin_roles = set( + frappe.db.get_values("Has Role", {"parent": "Administrator"}, fieldname="role", pluck="role") + ) if all_roles.difference(admin_roles): - add_all_roles_to('Administrator') + add_all_roles_to("Administrator") def set_defaults_for_tests(): @@ -150,7 +161,7 @@ def set_defaults_for_tests(): } frappe.db.set_single_value("Selling Settings", defaults) for key, value in defaults.items(): - frappe.db.set_default(key, value) + frappe.db.set_default(key, value) frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0) @@ -159,6 +170,7 @@ def insert_record(records): make_records(records) + def welcome_email(): site_name = get_default_company() or "ERPNext" title = _("Welcome to {0}").format(site_name) diff --git a/erpnext/startup/__init__.py b/erpnext/startup/__init__.py index dd1b40108c..489e24499c 100644 --- a/erpnext/startup/__init__.py +++ b/erpnext/startup/__init__.py @@ -17,7 +17,4 @@ # default settings that can be made for a user. product_name = "ERPNext" -user_defaults = { - "Company": "company", - "Territory": "territory" -} +user_defaults = {"Company": "company", "Territory": "territory"} diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py index 0da45a54d5..c3445ab644 100644 --- a/erpnext/startup/boot.py +++ b/erpnext/startup/boot.py @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt" - import frappe from frappe.utils import cint @@ -10,69 +9,72 @@ from frappe.utils import cint def boot_session(bootinfo): """boot session - send website info if guest""" - bootinfo.custom_css = frappe.db.get_value('Style Settings', None, 'custom_css') or '' + bootinfo.custom_css = frappe.db.get_value("Style Settings", None, "custom_css") or "" - if frappe.session['user']!='Guest': + if frappe.session["user"] != "Guest": update_page_info(bootinfo) load_country_and_currency(bootinfo) - bootinfo.sysdefaults.territory = frappe.db.get_single_value('Selling Settings', - 'territory') - bootinfo.sysdefaults.customer_group = frappe.db.get_single_value('Selling Settings', - 'customer_group') - bootinfo.sysdefaults.allow_stale = cint(frappe.db.get_single_value('Accounts Settings', - 'allow_stale')) - bootinfo.sysdefaults.quotation_valid_till = cint(frappe.db.get_single_value('CRM Settings', - 'default_valid_till')) + bootinfo.sysdefaults.territory = frappe.db.get_single_value("Selling Settings", "territory") + bootinfo.sysdefaults.customer_group = frappe.db.get_single_value( + "Selling Settings", "customer_group" + ) + bootinfo.sysdefaults.allow_stale = cint( + frappe.db.get_single_value("Accounts Settings", "allow_stale") + ) + bootinfo.sysdefaults.quotation_valid_till = cint( + frappe.db.get_single_value("CRM Settings", "default_valid_till") + ) # if no company, show a dialog box to create a new company bootinfo.customer_count = frappe.db.sql("""SELECT count(*) FROM `tabCustomer`""")[0][0] if not bootinfo.customer_count: - bootinfo.setup_complete = frappe.db.sql("""SELECT `name` + bootinfo.setup_complete = ( + frappe.db.sql( + """SELECT `name` FROM `tabCompany` - LIMIT 1""") and 'Yes' or 'No' + LIMIT 1""" + ) + and "Yes" + or "No" + ) - bootinfo.docs += frappe.db.sql("""select name, default_currency, cost_center, default_selling_terms, default_buying_terms, + bootinfo.docs += frappe.db.sql( + """select name, default_currency, cost_center, default_selling_terms, default_buying_terms, default_letter_head, default_bank_account, enable_perpetual_inventory, country from `tabCompany`""", - as_dict=1, update={"doctype":":Company"}) + as_dict=1, + update={"doctype": ":Company"}, + ) - party_account_types = frappe.db.sql(""" select name, ifnull(account_type, '') from `tabParty Type`""") + party_account_types = frappe.db.sql( + """ select name, ifnull(account_type, '') from `tabParty Type`""" + ) bootinfo.party_account_types = frappe._dict(party_account_types) + def load_country_and_currency(bootinfo): country = frappe.db.get_default("country") if country and frappe.db.exists("Country", country): bootinfo.docs += [frappe.get_doc("Country", country)] - bootinfo.docs += frappe.db.sql("""select name, fraction, fraction_units, + bootinfo.docs += frappe.db.sql( + """select name, fraction, fraction_units, number_format, smallest_currency_fraction_value, symbol from tabCurrency - where enabled=1""", as_dict=1, update={"doctype":":Currency"}) + where enabled=1""", + as_dict=1, + update={"doctype": ":Currency"}, + ) + def update_page_info(bootinfo): - bootinfo.page_info.update({ - "Chart of Accounts": { - "title": "Chart of Accounts", - "route": "Tree/Account" - }, - "Chart of Cost Centers": { - "title": "Chart of Cost Centers", - "route": "Tree/Cost Center" - }, - "Item Group Tree": { - "title": "Item Group Tree", - "route": "Tree/Item Group" - }, - "Customer Group Tree": { - "title": "Customer Group Tree", - "route": "Tree/Customer Group" - }, - "Territory Tree": { - "title": "Territory Tree", - "route": "Tree/Territory" - }, - "Sales Person Tree": { - "title": "Sales Person Tree", - "route": "Tree/Sales Person" + bootinfo.page_info.update( + { + "Chart of Accounts": {"title": "Chart of Accounts", "route": "Tree/Account"}, + "Chart of Cost Centers": {"title": "Chart of Cost Centers", "route": "Tree/Cost Center"}, + "Item Group Tree": {"title": "Item Group Tree", "route": "Tree/Item Group"}, + "Customer Group Tree": {"title": "Customer Group Tree", "route": "Tree/Customer Group"}, + "Territory Tree": {"title": "Territory Tree", "route": "Tree/Territory"}, + "Sales Person Tree": {"title": "Sales Person Tree", "route": "Tree/Sales Person"}, } - }) + ) diff --git a/erpnext/startup/leaderboard.py b/erpnext/startup/leaderboard.py index 8ae70d2a90..da7edbf814 100644 --- a/erpnext/startup/leaderboard.py +++ b/erpnext/startup/leaderboard.py @@ -6,66 +6,66 @@ def get_leaderboards(): leaderboards = { "Customer": { "fields": [ - {'fieldname': 'total_sales_amount', 'fieldtype': 'Currency'}, - 'total_qty_sold', - {'fieldname': 'outstanding_amount', 'fieldtype': 'Currency'} + {"fieldname": "total_sales_amount", "fieldtype": "Currency"}, + "total_qty_sold", + {"fieldname": "outstanding_amount", "fieldtype": "Currency"}, ], "method": "erpnext.startup.leaderboard.get_all_customers", - "icon": "customer" + "icon": "customer", }, "Item": { "fields": [ - {'fieldname': 'total_sales_amount', 'fieldtype': 'Currency'}, - 'total_qty_sold', - {'fieldname': 'total_purchase_amount', 'fieldtype': 'Currency'}, - 'total_qty_purchased', - 'available_stock_qty', - {'fieldname': 'available_stock_value', 'fieldtype': 'Currency'} + {"fieldname": "total_sales_amount", "fieldtype": "Currency"}, + "total_qty_sold", + {"fieldname": "total_purchase_amount", "fieldtype": "Currency"}, + "total_qty_purchased", + "available_stock_qty", + {"fieldname": "available_stock_value", "fieldtype": "Currency"}, ], "method": "erpnext.startup.leaderboard.get_all_items", - "icon": "stock" + "icon": "stock", }, "Supplier": { "fields": [ - {'fieldname': 'total_purchase_amount', 'fieldtype': 'Currency'}, - 'total_qty_purchased', - {'fieldname': 'outstanding_amount', 'fieldtype': 'Currency'} + {"fieldname": "total_purchase_amount", "fieldtype": "Currency"}, + "total_qty_purchased", + {"fieldname": "outstanding_amount", "fieldtype": "Currency"}, ], "method": "erpnext.startup.leaderboard.get_all_suppliers", - "icon": "buying" + "icon": "buying", }, "Sales Partner": { "fields": [ - {'fieldname': 'total_sales_amount', 'fieldtype': 'Currency'}, - {'fieldname': 'total_commission', 'fieldtype': 'Currency'} + {"fieldname": "total_sales_amount", "fieldtype": "Currency"}, + {"fieldname": "total_commission", "fieldtype": "Currency"}, ], "method": "erpnext.startup.leaderboard.get_all_sales_partner", - "icon": "hr" + "icon": "hr", }, "Sales Person": { - "fields": [ - {'fieldname': 'total_sales_amount', 'fieldtype': 'Currency'} - ], + "fields": [{"fieldname": "total_sales_amount", "fieldtype": "Currency"}], "method": "erpnext.startup.leaderboard.get_all_sales_person", - "icon": "customer" - } + "icon": "customer", + }, } return leaderboards + @frappe.whitelist() -def get_all_customers(date_range, company, field, limit = None): +def get_all_customers(date_range, company, field, limit=None): if field == "outstanding_amount": - filters = [['docstatus', '=', '1'], ['company', '=', company]] + filters = [["docstatus", "=", "1"], ["company", "=", company]] if date_range: date_range = frappe.parse_json(date_range) - filters.append(['posting_date', '>=', 'between', [date_range[0], date_range[1]]]) - return frappe.db.get_all('Sales Invoice', - fields = ['customer as name', 'sum(outstanding_amount) as value'], - filters = filters, - group_by = 'customer', - order_by = 'value desc', - limit = limit + filters.append(["posting_date", ">=", "between", [date_range[0], date_range[1]]]) + return frappe.db.get_all( + "Sales Invoice", + fields=["customer as name", "sum(outstanding_amount) as value"], + filters=filters, + group_by="customer", + order_by="value desc", + limit=limit, ) else: if field == "total_sales_amount": @@ -73,9 +73,10 @@ def get_all_customers(date_range, company, field, limit = None): elif field == "total_qty_sold": select_field = "sum(so_item.stock_qty)" - date_condition = get_date_condition(date_range, 'so.transaction_date') + date_condition = get_date_condition(date_range, "so.transaction_date") - return frappe.db.sql(""" + return frappe.db.sql( + """ select so.customer as name, {0} as value FROM `tabSales Order` as so JOIN `tabSales Order Item` as so_item ON so.name = so_item.parent @@ -83,17 +84,24 @@ def get_all_customers(date_range, company, field, limit = None): group by so.customer order by value DESC limit %s - """.format(select_field, date_condition), (company, cint(limit)), as_dict=1) + """.format( + select_field, date_condition + ), + (company, cint(limit)), + as_dict=1, + ) + @frappe.whitelist() -def get_all_items(date_range, company, field, limit = None): +def get_all_items(date_range, company, field, limit=None): if field in ("available_stock_qty", "available_stock_value"): - select_field = "sum(actual_qty)" if field=="available_stock_qty" else "sum(stock_value)" - return frappe.db.get_all('Bin', - fields = ['item_code as name', '{0} as value'.format(select_field)], - group_by = 'item_code', - order_by = 'value desc', - limit = limit + select_field = "sum(actual_qty)" if field == "available_stock_qty" else "sum(stock_value)" + return frappe.db.get_all( + "Bin", + fields=["item_code as name", "{0} as value".format(select_field)], + group_by="item_code", + order_by="value desc", + limit=limit, ) else: if field == "total_sales_amount": @@ -109,9 +117,10 @@ def get_all_items(date_range, company, field, limit = None): select_field = "sum(order_item.stock_qty)" select_doctype = "Purchase Order" - date_condition = get_date_condition(date_range, 'sales_order.transaction_date') + date_condition = get_date_condition(date_range, "sales_order.transaction_date") - return frappe.db.sql(""" + return frappe.db.sql( + """ select order_item.item_code as name, {0} as value from `tab{1}` sales_order join `tab{1} Item` as order_item on sales_order.name = order_item.parent @@ -120,21 +129,28 @@ def get_all_items(date_range, company, field, limit = None): group by order_item.item_code order by value desc limit %s - """.format(select_field, select_doctype, date_condition), (company, cint(limit)), as_dict=1) #nosec + """.format( + select_field, select_doctype, date_condition + ), + (company, cint(limit)), + as_dict=1, + ) # nosec + @frappe.whitelist() -def get_all_suppliers(date_range, company, field, limit = None): +def get_all_suppliers(date_range, company, field, limit=None): if field == "outstanding_amount": - filters = [['docstatus', '=', '1'], ['company', '=', company]] + filters = [["docstatus", "=", "1"], ["company", "=", company]] if date_range: date_range = frappe.parse_json(date_range) - filters.append(['posting_date', 'between', [date_range[0], date_range[1]]]) - return frappe.db.get_all('Purchase Invoice', - fields = ['supplier as name', 'sum(outstanding_amount) as value'], - filters = filters, - group_by = 'supplier', - order_by = 'value desc', - limit = limit + filters.append(["posting_date", "between", [date_range[0], date_range[1]]]) + return frappe.db.get_all( + "Purchase Invoice", + fields=["supplier as name", "sum(outstanding_amount) as value"], + filters=filters, + group_by="supplier", + order_by="value desc", + limit=limit, ) else: if field == "total_purchase_amount": @@ -142,9 +158,10 @@ def get_all_suppliers(date_range, company, field, limit = None): elif field == "total_qty_purchased": select_field = "sum(purchase_order_item.stock_qty)" - date_condition = get_date_condition(date_range, 'purchase_order.modified') + date_condition = get_date_condition(date_range, "purchase_order.modified") - return frappe.db.sql(""" + return frappe.db.sql( + """ select purchase_order.supplier as name, {0} as value FROM `tabPurchase Order` as purchase_order LEFT JOIN `tabPurchase Order Item` as purchase_order_item ON purchase_order.name = purchase_order_item.parent @@ -154,34 +171,45 @@ def get_all_suppliers(date_range, company, field, limit = None): and purchase_order.company = %s group by purchase_order.supplier order by value DESC - limit %s""".format(select_field, date_condition), (company, cint(limit)), as_dict=1) #nosec + limit %s""".format( + select_field, date_condition + ), + (company, cint(limit)), + as_dict=1, + ) # nosec + @frappe.whitelist() -def get_all_sales_partner(date_range, company, field, limit = None): +def get_all_sales_partner(date_range, company, field, limit=None): if field == "total_sales_amount": select_field = "sum(`base_net_total`)" elif field == "total_commission": select_field = "sum(`total_commission`)" - filters = { - 'sales_partner': ['!=', ''], - 'docstatus': 1, - 'company': company - } + filters = {"sales_partner": ["!=", ""], "docstatus": 1, "company": company} if date_range: date_range = frappe.parse_json(date_range) - filters['transaction_date'] = ['between', [date_range[0], date_range[1]]] + filters["transaction_date"] = ["between", [date_range[0], date_range[1]]] + + return frappe.get_list( + "Sales Order", + fields=[ + "`sales_partner` as name", + "{} as value".format(select_field), + ], + filters=filters, + group_by="sales_partner", + order_by="value DESC", + limit=limit, + ) - return frappe.get_list('Sales Order', fields=[ - '`sales_partner` as name', - '{} as value'.format(select_field), - ], filters=filters, group_by='sales_partner', order_by='value DESC', limit=limit) @frappe.whitelist() -def get_all_sales_person(date_range, company, field = None, limit = 0): - date_condition = get_date_condition(date_range, 'sales_order.transaction_date') +def get_all_sales_person(date_range, company, field=None, limit=0): + date_condition = get_date_condition(date_range, "sales_order.transaction_date") - return frappe.db.sql(""" + return frappe.db.sql( + """ select sales_team.sales_person as name, sum(sales_order.base_net_total) as value from `tabSales Order` as sales_order join `tabSales Team` as sales_team on sales_order.name = sales_team.parent and sales_team.parenttype = 'Sales Order' @@ -191,10 +219,16 @@ def get_all_sales_person(date_range, company, field = None, limit = 0): group by sales_team.sales_person order by value DESC limit %s - """.format(date_condition=date_condition), (company, cint(limit)), as_dict=1) + """.format( + date_condition=date_condition + ), + (company, cint(limit)), + as_dict=1, + ) + def get_date_condition(date_range, field): - date_condition = '' + date_condition = "" if date_range: date_range = frappe.parse_json(date_range) from_date, to_date = date_range diff --git a/erpnext/startup/notifications.py b/erpnext/startup/notifications.py index 0965ead57c..76cb91a463 100644 --- a/erpnext/startup/notifications.py +++ b/erpnext/startup/notifications.py @@ -6,8 +6,8 @@ import frappe def get_notification_config(): - notifications = { "for_doctype": - { + notifications = { + "for_doctype": { "Issue": {"status": "Open"}, "Warranty Claim": {"status": "Open"}, "Task": {"status": ("in", ("Open", "Overdue"))}, @@ -16,66 +16,46 @@ def get_notification_config(): "Contact": {"status": "Open"}, "Opportunity": {"status": "Open"}, "Quotation": {"docstatus": 0}, - "Sales Order": { - "status": ("not in", ("Completed", "Closed")), - "docstatus": ("<", 2) - }, + "Sales Order": {"status": ("not in", ("Completed", "Closed")), "docstatus": ("<", 2)}, "Journal Entry": {"docstatus": 0}, - "Sales Invoice": { - "outstanding_amount": (">", 0), - "docstatus": ("<", 2) - }, - "Purchase Invoice": { - "outstanding_amount": (">", 0), - "docstatus": ("<", 2) - }, + "Sales Invoice": {"outstanding_amount": (">", 0), "docstatus": ("<", 2)}, + "Purchase Invoice": {"outstanding_amount": (">", 0), "docstatus": ("<", 2)}, "Payment Entry": {"docstatus": 0}, "Leave Application": {"docstatus": 0}, "Expense Claim": {"docstatus": 0}, "Job Applicant": {"status": "Open"}, - "Delivery Note": { - "status": ("not in", ("Completed", "Closed")), - "docstatus": ("<", 2) - }, + "Delivery Note": {"status": ("not in", ("Completed", "Closed")), "docstatus": ("<", 2)}, "Stock Entry": {"docstatus": 0}, "Material Request": { "docstatus": ("<", 2), "status": ("not in", ("Stopped",)), - "per_ordered": ("<", 100) + "per_ordered": ("<", 100), }, - "Request for Quotation": { "docstatus": 0 }, + "Request for Quotation": {"docstatus": 0}, "Supplier Quotation": {"docstatus": 0}, - "Purchase Order": { - "status": ("not in", ("Completed", "Closed")), - "docstatus": ("<", 2) - }, - "Purchase Receipt": { - "status": ("not in", ("Completed", "Closed")), - "docstatus": ("<", 2) - }, - "Work Order": { "status": ("in", ("Draft", "Not Started", "In Process")) }, + "Purchase Order": {"status": ("not in", ("Completed", "Closed")), "docstatus": ("<", 2)}, + "Purchase Receipt": {"status": ("not in", ("Completed", "Closed")), "docstatus": ("<", 2)}, + "Work Order": {"status": ("in", ("Draft", "Not Started", "In Process"))}, "BOM": {"docstatus": 0}, - "Timesheet": {"status": "Draft"}, - "Lab Test": {"docstatus": 0}, "Sample Collection": {"docstatus": 0}, "Patient Appointment": {"status": "Open"}, - "Patient Encounter": {"docstatus": 0} + "Patient Encounter": {"docstatus": 0}, }, - "targets": { "Company": { - "filters" : { "monthly_sales_target": ( ">", 0 ) }, - "target_field" : "monthly_sales_target", - "value_field" : "total_monthly_sales" + "filters": {"monthly_sales_target": (">", 0)}, + "target_field": "monthly_sales_target", + "value_field": "total_monthly_sales", } - } + }, } - doctype = [d for d in notifications.get('for_doctype')] - for doc in frappe.get_all('DocType', - fields= ["name"], filters = {"name": ("not in", doctype), 'is_submittable': 1}): + doctype = [d for d in notifications.get("for_doctype")] + for doc in frappe.get_all( + "DocType", fields=["name"], filters={"name": ("not in", doctype), "is_submittable": 1} + ): notifications["for_doctype"][doc.name] = {"docstatus": 0} return notifications diff --git a/erpnext/startup/report_data_map.py b/erpnext/startup/report_data_map.py index 65b48bf043..f8c1b6cca0 100644 --- a/erpnext/startup/report_data_map.py +++ b/erpnext/startup/report_data_map.py @@ -6,90 +6,98 @@ # "remember to add indexes!" data_map = { - "Company": { - "columns": ["name"], - "conditions": ["docstatus < 2"] - }, + "Company": {"columns": ["name"], "conditions": ["docstatus < 2"]}, "Fiscal Year": { "columns": ["name", "year_start_date", "year_end_date"], "conditions": ["docstatus < 2"], }, - # Accounts "Account": { - "columns": ["name", "parent_account", "lft", "rgt", "report_type", - "company", "is_group"], + "columns": ["name", "parent_account", "lft", "rgt", "report_type", "company", "is_group"], "conditions": ["docstatus < 2"], "order_by": "lft", "links": { "company": ["Company", "name"], - } - + }, }, "Cost Center": { "columns": ["name", "lft", "rgt"], "conditions": ["docstatus < 2"], - "order_by": "lft" + "order_by": "lft", }, "GL Entry": { - "columns": ["name", "account", "posting_date", "cost_center", "debit", "credit", - "is_opening", "company", "voucher_type", "voucher_no", "remarks"], + "columns": [ + "name", + "account", + "posting_date", + "cost_center", + "debit", + "credit", + "is_opening", + "company", + "voucher_type", + "voucher_no", + "remarks", + ], "order_by": "posting_date, account", "links": { "account": ["Account", "name"], "company": ["Company", "name"], - "cost_center": ["Cost Center", "name"] - } + "cost_center": ["Cost Center", "name"], + }, }, - # Stock "Item": { - "columns": ["name", "if(item_name=name, '', item_name) as item_name", "description", - "item_group as parent_item_group", "stock_uom", "brand", "valuation_method"], + "columns": [ + "name", + "if(item_name=name, '', item_name) as item_name", + "description", + "item_group as parent_item_group", + "stock_uom", + "brand", + "valuation_method", + ], # "conditions": ["docstatus < 2"], "order_by": "name", - "links": { - "parent_item_group": ["Item Group", "name"], - "brand": ["Brand", "name"] - } + "links": {"parent_item_group": ["Item Group", "name"], "brand": ["Brand", "name"]}, }, "Item Group": { "columns": ["name", "parent_item_group"], # "conditions": ["docstatus < 2"], - "order_by": "lft" - }, - "Brand": { - "columns": ["name"], - "conditions": ["docstatus < 2"], - "order_by": "name" - }, - "Project": { - "columns": ["name"], - "conditions": ["docstatus < 2"], - "order_by": "name" - }, - "Warehouse": { - "columns": ["name"], - "conditions": ["docstatus < 2"], - "order_by": "name" + "order_by": "lft", }, + "Brand": {"columns": ["name"], "conditions": ["docstatus < 2"], "order_by": "name"}, + "Project": {"columns": ["name"], "conditions": ["docstatus < 2"], "order_by": "name"}, + "Warehouse": {"columns": ["name"], "conditions": ["docstatus < 2"], "order_by": "name"}, "Stock Ledger Entry": { - "columns": ["name", "posting_date", "posting_time", "item_code", "warehouse", - "actual_qty as qty", "voucher_type", "voucher_no", "project", - "incoming_rate as incoming_rate", "stock_uom", "serial_no", - "qty_after_transaction", "valuation_rate"], + "columns": [ + "name", + "posting_date", + "posting_time", + "item_code", + "warehouse", + "actual_qty as qty", + "voucher_type", + "voucher_no", + "project", + "incoming_rate as incoming_rate", + "stock_uom", + "serial_no", + "qty_after_transaction", + "valuation_rate", + ], "order_by": "posting_date, posting_time, creation", "links": { "item_code": ["Item", "name"], "warehouse": ["Warehouse", "name"], - "project": ["Project", "name"] + "project": ["Project", "name"], }, - "force_index": "posting_sort_index" + "force_index": "posting_sort_index", }, "Serial No": { "columns": ["name", "purchase_rate as incoming_rate"], "conditions": ["docstatus < 2"], - "order_by": "name" + "order_by": "name", }, "Stock Entry": { "columns": ["name", "purpose"], @@ -97,227 +105,223 @@ data_map = { "order_by": "posting_date, posting_time, name", }, "Material Request Item": { - "columns": ["item.name as name", "item_code", "warehouse", - "(qty - ordered_qty) as qty"], + "columns": ["item.name as name", "item_code", "warehouse", "(qty - ordered_qty) as qty"], "from": "`tabMaterial Request Item` item, `tabMaterial Request` main", - "conditions": ["item.parent = main.name", "main.docstatus=1", "main.status != 'Stopped'", - "ifnull(warehouse, '')!=''", "qty > ordered_qty"], - "links": { - "item_code": ["Item", "name"], - "warehouse": ["Warehouse", "name"] - }, + "conditions": [ + "item.parent = main.name", + "main.docstatus=1", + "main.status != 'Stopped'", + "ifnull(warehouse, '')!=''", + "qty > ordered_qty", + ], + "links": {"item_code": ["Item", "name"], "warehouse": ["Warehouse", "name"]}, }, "Purchase Order Item": { - "columns": ["item.name as name", "item_code", "warehouse", - "(qty - received_qty)*conversion_factor as qty"], + "columns": [ + "item.name as name", + "item_code", + "warehouse", + "(qty - received_qty)*conversion_factor as qty", + ], "from": "`tabPurchase Order Item` item, `tabPurchase Order` main", - "conditions": ["item.parent = main.name", "main.docstatus=1", "main.status != 'Stopped'", - "ifnull(warehouse, '')!=''", "qty > received_qty"], - "links": { - "item_code": ["Item", "name"], - "warehouse": ["Warehouse", "name"] - }, + "conditions": [ + "item.parent = main.name", + "main.docstatus=1", + "main.status != 'Stopped'", + "ifnull(warehouse, '')!=''", + "qty > received_qty", + ], + "links": {"item_code": ["Item", "name"], "warehouse": ["Warehouse", "name"]}, }, - "Sales Order Item": { - "columns": ["item.name as name", "item_code", "(qty - delivered_qty)*conversion_factor as qty", "warehouse"], + "columns": [ + "item.name as name", + "item_code", + "(qty - delivered_qty)*conversion_factor as qty", + "warehouse", + ], "from": "`tabSales Order Item` item, `tabSales Order` main", - "conditions": ["item.parent = main.name", "main.docstatus=1", "main.status != 'Stopped'", - "ifnull(warehouse, '')!=''", "qty > delivered_qty"], - "links": { - "item_code": ["Item", "name"], - "warehouse": ["Warehouse", "name"] - }, + "conditions": [ + "item.parent = main.name", + "main.docstatus=1", + "main.status != 'Stopped'", + "ifnull(warehouse, '')!=''", + "qty > delivered_qty", + ], + "links": {"item_code": ["Item", "name"], "warehouse": ["Warehouse", "name"]}, }, - # Sales "Customer": { - "columns": ["name", "if(customer_name=name, '', customer_name) as customer_name", - "customer_group as parent_customer_group", "territory as parent_territory"], + "columns": [ + "name", + "if(customer_name=name, '', customer_name) as customer_name", + "customer_group as parent_customer_group", + "territory as parent_territory", + ], "conditions": ["docstatus < 2"], "order_by": "name", "links": { "parent_customer_group": ["Customer Group", "name"], "parent_territory": ["Territory", "name"], - } + }, }, "Customer Group": { "columns": ["name", "parent_customer_group"], "conditions": ["docstatus < 2"], - "order_by": "lft" + "order_by": "lft", }, "Territory": { "columns": ["name", "parent_territory"], "conditions": ["docstatus < 2"], - "order_by": "lft" + "order_by": "lft", }, "Sales Invoice": { "columns": ["name", "customer", "posting_date", "company"], "conditions": ["docstatus=1"], "order_by": "posting_date", - "links": { - "customer": ["Customer", "name"], - "company":["Company", "name"] - } + "links": {"customer": ["Customer", "name"], "company": ["Company", "name"]}, }, "Sales Invoice Item": { "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], "order_by": "parent", - "links": { - "parent": ["Sales Invoice", "name"], - "item_code": ["Item", "name"] - } + "links": {"parent": ["Sales Invoice", "name"], "item_code": ["Item", "name"]}, }, "Sales Order": { "columns": ["name", "customer", "transaction_date as posting_date", "company"], "conditions": ["docstatus=1"], "order_by": "transaction_date", - "links": { - "customer": ["Customer", "name"], - "company":["Company", "name"] - } + "links": {"customer": ["Customer", "name"], "company": ["Company", "name"]}, }, "Sales Order Item[Sales Analytics]": { "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], "order_by": "parent", - "links": { - "parent": ["Sales Order", "name"], - "item_code": ["Item", "name"] - } + "links": {"parent": ["Sales Order", "name"], "item_code": ["Item", "name"]}, }, "Delivery Note": { "columns": ["name", "customer", "posting_date", "company"], "conditions": ["docstatus=1"], "order_by": "posting_date", - "links": { - "customer": ["Customer", "name"], - "company":["Company", "name"] - } + "links": {"customer": ["Customer", "name"], "company": ["Company", "name"]}, }, "Delivery Note Item[Sales Analytics]": { "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], "order_by": "parent", - "links": { - "parent": ["Delivery Note", "name"], - "item_code": ["Item", "name"] - } + "links": {"parent": ["Delivery Note", "name"], "item_code": ["Item", "name"]}, }, "Supplier": { - "columns": ["name", "if(supplier_name=name, '', supplier_name) as supplier_name", - "supplier_group as parent_supplier_group"], + "columns": [ + "name", + "if(supplier_name=name, '', supplier_name) as supplier_name", + "supplier_group as parent_supplier_group", + ], "conditions": ["docstatus < 2"], "order_by": "name", "links": { "parent_supplier_group": ["Supplier Group", "name"], - } + }, }, "Supplier Group": { "columns": ["name", "parent_supplier_group"], "conditions": ["docstatus < 2"], - "order_by": "name" + "order_by": "name", }, "Purchase Invoice": { "columns": ["name", "supplier", "posting_date", "company"], "conditions": ["docstatus=1"], "order_by": "posting_date", - "links": { - "supplier": ["Supplier", "name"], - "company":["Company", "name"] - } + "links": {"supplier": ["Supplier", "name"], "company": ["Company", "name"]}, }, "Purchase Invoice Item": { "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], "order_by": "parent", - "links": { - "parent": ["Purchase Invoice", "name"], - "item_code": ["Item", "name"] - } + "links": {"parent": ["Purchase Invoice", "name"], "item_code": ["Item", "name"]}, }, "Purchase Order": { "columns": ["name", "supplier", "transaction_date as posting_date", "company"], "conditions": ["docstatus=1"], "order_by": "posting_date", - "links": { - "supplier": ["Supplier", "name"], - "company":["Company", "name"] - } + "links": {"supplier": ["Supplier", "name"], "company": ["Company", "name"]}, }, "Purchase Order Item[Purchase Analytics]": { "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], "order_by": "parent", - "links": { - "parent": ["Purchase Order", "name"], - "item_code": ["Item", "name"] - } + "links": {"parent": ["Purchase Order", "name"], "item_code": ["Item", "name"]}, }, "Purchase Receipt": { "columns": ["name", "supplier", "posting_date", "company"], "conditions": ["docstatus=1"], "order_by": "posting_date", - "links": { - "supplier": ["Supplier", "name"], - "company":["Company", "name"] - } + "links": {"supplier": ["Supplier", "name"], "company": ["Company", "name"]}, }, "Purchase Receipt Item[Purchase Analytics]": { "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], "order_by": "parent", - "links": { - "parent": ["Purchase Receipt", "name"], - "item_code": ["Item", "name"] - } + "links": {"parent": ["Purchase Receipt", "name"], "item_code": ["Item", "name"]}, }, # Support "Issue": { - "columns": ["name","status","creation","resolution_date","first_responded_on"], + "columns": ["name", "status", "creation", "resolution_date", "first_responded_on"], "conditions": ["docstatus < 2"], - "order_by": "creation" + "order_by": "creation", }, - # Manufacturing "Work Order": { - "columns": ["name","status","creation","planned_start_date","planned_end_date","status","actual_start_date","actual_end_date", "modified"], + "columns": [ + "name", + "status", + "creation", + "planned_start_date", + "planned_end_date", + "status", + "actual_start_date", + "actual_end_date", + "modified", + ], "conditions": ["docstatus = 1"], - "order_by": "creation" + "order_by": "creation", }, - - #Medical + # Medical "Patient": { - "columns": ["name", "creation", "owner", "if(patient_name=name, '', patient_name) as patient_name"], + "columns": [ + "name", + "creation", + "owner", + "if(patient_name=name, '', patient_name) as patient_name", + ], "conditions": ["docstatus < 2"], "order_by": "name", - "links": { - "owner" : ["User", "name"] - } + "links": {"owner": ["User", "name"]}, }, "Patient Appointment": { - "columns": ["name", "appointment_type", "patient", "practitioner", "appointment_date", "department", "status", "company"], + "columns": [ + "name", + "appointment_type", + "patient", + "practitioner", + "appointment_date", + "department", + "status", + "company", + ], "order_by": "name", "links": { "practitioner": ["Healthcare Practitioner", "name"], - "appointment_type": ["Appointment Type", "name"] - } + "appointment_type": ["Appointment Type", "name"], + }, }, "Healthcare Practitioner": { "columns": ["name", "department"], "order_by": "name", "links": { "department": ["Department", "name"], - } - + }, }, - "Appointment Type": { - "columns": ["name"], - "order_by": "name" - }, - "Medical Department": { - "columns": ["name"], - "order_by": "name" - } + "Appointment Type": {"columns": ["name"], "order_by": "name"}, + "Medical Department": {"columns": ["name"], "order_by": "name"}, } diff --git a/erpnext/stock/__init__.py b/erpnext/stock/__init__.py index e8b2804b7d..45bf012be8 100644 --- a/erpnext/stock/__init__.py +++ b/erpnext/stock/__init__.py @@ -2,17 +2,24 @@ import frappe from frappe import _ install_docs = [ - {"doctype":"Role", "role_name":"Stock Manager", "name":"Stock Manager"}, - {"doctype":"Role", "role_name":"Item Manager", "name":"Item Manager"}, - {"doctype":"Role", "role_name":"Stock User", "name":"Stock User"}, - {"doctype":"Role", "role_name":"Quality Manager", "name":"Quality Manager"}, - {"doctype":"Item Group", "item_group_name":"All Item Groups", "is_group": 1}, - {"doctype":"Item Group", "item_group_name":"Default", - "parent_item_group":"All Item Groups", "is_group": 0}, + {"doctype": "Role", "role_name": "Stock Manager", "name": "Stock Manager"}, + {"doctype": "Role", "role_name": "Item Manager", "name": "Item Manager"}, + {"doctype": "Role", "role_name": "Stock User", "name": "Stock User"}, + {"doctype": "Role", "role_name": "Quality Manager", "name": "Quality Manager"}, + {"doctype": "Item Group", "item_group_name": "All Item Groups", "is_group": 1}, + { + "doctype": "Item Group", + "item_group_name": "Default", + "parent_item_group": "All Item Groups", + "is_group": 0, + }, ] + def get_warehouse_account_map(company=None): - company_warehouse_account_map = company and frappe.flags.setdefault('warehouse_account_map', {}).get(company) + company_warehouse_account_map = company and frappe.flags.setdefault( + "warehouse_account_map", {} + ).get(company) warehouse_account_map = frappe.flags.warehouse_account_map if not warehouse_account_map or not company_warehouse_account_map or frappe.flags.in_test: @@ -20,18 +27,20 @@ def get_warehouse_account_map(company=None): filters = {} if company: - filters['company'] = company - frappe.flags.setdefault('warehouse_account_map', {}).setdefault(company, {}) + filters["company"] = company + frappe.flags.setdefault("warehouse_account_map", {}).setdefault(company, {}) - for d in frappe.get_all('Warehouse', - fields = ["name", "account", "parent_warehouse", "company", "is_group"], - filters = filters, - order_by="lft, rgt"): + for d in frappe.get_all( + "Warehouse", + fields=["name", "account", "parent_warehouse", "company", "is_group"], + filters=filters, + order_by="lft, rgt", + ): if not d.account: d.account = get_warehouse_account(d, warehouse_account) if d.account: - d.account_currency = frappe.db.get_value('Account', d.account, 'account_currency', cache=True) + d.account_currency = frappe.db.get_value("Account", d.account, "account_currency", cache=True) warehouse_account.setdefault(d.name, d) if company: frappe.flags.warehouse_account_map[company] = warehouse_account @@ -40,6 +49,7 @@ def get_warehouse_account_map(company=None): return frappe.flags.warehouse_account_map.get(company) or frappe.flags.warehouse_account_map + def get_warehouse_account(warehouse, warehouse_account=None): account = warehouse.account if not account and warehouse.parent_warehouse: @@ -48,15 +58,20 @@ def get_warehouse_account(warehouse, warehouse_account=None): account = warehouse_account.get(warehouse.parent_warehouse).account else: from frappe.utils.nestedset import rebuild_tree + rebuild_tree("Warehouse", "parent_warehouse") else: - account = frappe.db.sql(""" + account = frappe.db.sql( + """ select account from `tabWarehouse` where lft <= %s and rgt >= %s and company = %s and account is not null and ifnull(account, '') !='' - order by lft desc limit 1""", (warehouse.lft, warehouse.rgt, warehouse.company), as_list=1) + order by lft desc limit 1""", + (warehouse.lft, warehouse.rgt, warehouse.company), + as_list=1, + ) account = account[0][0] if account else None @@ -64,13 +79,18 @@ def get_warehouse_account(warehouse, warehouse_account=None): account = get_company_default_inventory_account(warehouse.company) if not account and warehouse.company: - account = frappe.db.get_value('Account', - {'account_type': 'Stock', 'is_group': 0, 'company': warehouse.company}, 'name') + account = frappe.db.get_value( + "Account", {"account_type": "Stock", "is_group": 0, "company": warehouse.company}, "name" + ) if not account and warehouse.company and not warehouse.is_group: - frappe.throw(_("Please set Account in Warehouse {0} or Default Inventory Account in Company {1}") - .format(warehouse.name, warehouse.company)) + frappe.throw( + _("Please set Account in Warehouse {0} or Default Inventory Account in Company {1}").format( + warehouse.name, warehouse.company + ) + ) return account + def get_company_default_inventory_account(company): - return frappe.get_cached_value('Company', company, 'default_inventory_account') + return frappe.get_cached_value("Company", company, "default_inventory_account") diff --git a/erpnext/stock/dashboard/item_dashboard.py b/erpnext/stock/dashboard/item_dashboard.py index 75aa1d36c7..8fbb56ced0 100644 --- a/erpnext/stock/dashboard/item_dashboard.py +++ b/erpnext/stock/dashboard/item_dashboard.py @@ -4,55 +4,72 @@ from frappe.utils import cint, flt @frappe.whitelist() -def get_data(item_code=None, warehouse=None, item_group=None, - start=0, sort_by='actual_qty', sort_order='desc'): - '''Return data to render the item dashboard''' +def get_data( + item_code=None, warehouse=None, item_group=None, start=0, sort_by="actual_qty", sort_order="desc" +): + """Return data to render the item dashboard""" filters = [] if item_code: - filters.append(['item_code', '=', item_code]) + filters.append(["item_code", "=", item_code]) if warehouse: - filters.append(['warehouse', '=', warehouse]) + filters.append(["warehouse", "=", warehouse]) if item_group: lft, rgt = frappe.db.get_value("Item Group", item_group, ["lft", "rgt"]) - items = frappe.db.sql_list(""" + items = frappe.db.sql_list( + """ select i.name from `tabItem` i where exists(select name from `tabItem Group` where name=i.item_group and lft >=%s and rgt<=%s) - """, (lft, rgt)) - filters.append(['item_code', 'in', items]) + """, + (lft, rgt), + ) + filters.append(["item_code", "in", items]) try: # check if user has any restrictions based on user permissions on warehouse - if DatabaseQuery('Warehouse', user=frappe.session.user).build_match_conditions(): - filters.append(['warehouse', 'in', [w.name for w in frappe.get_list('Warehouse')]]) + if DatabaseQuery("Warehouse", user=frappe.session.user).build_match_conditions(): + filters.append(["warehouse", "in", [w.name for w in frappe.get_list("Warehouse")]]) except frappe.PermissionError: # user does not have access on warehouse return [] - items = frappe.db.get_all('Bin', fields=['item_code', 'warehouse', 'projected_qty', - 'reserved_qty', 'reserved_qty_for_production', 'reserved_qty_for_sub_contract', 'actual_qty', 'valuation_rate'], + items = frappe.db.get_all( + "Bin", + fields=[ + "item_code", + "warehouse", + "projected_qty", + "reserved_qty", + "reserved_qty_for_production", + "reserved_qty_for_sub_contract", + "actual_qty", + "valuation_rate", + ], or_filters={ - 'projected_qty': ['!=', 0], - 'reserved_qty': ['!=', 0], - 'reserved_qty_for_production': ['!=', 0], - 'reserved_qty_for_sub_contract': ['!=', 0], - 'actual_qty': ['!=', 0], + "projected_qty": ["!=", 0], + "reserved_qty": ["!=", 0], + "reserved_qty_for_production": ["!=", 0], + "reserved_qty_for_sub_contract": ["!=", 0], + "actual_qty": ["!=", 0], }, filters=filters, - order_by=sort_by + ' ' + sort_order, + order_by=sort_by + " " + sort_order, limit_start=start, - limit_page_length=21) + limit_page_length=21, + ) precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) for item in items: - item.update({ - 'item_name': frappe.get_cached_value("Item", item.item_code, 'item_name'), - 'disable_quick_entry': frappe.get_cached_value( "Item", item.item_code, 'has_batch_no') - or frappe.get_cached_value( "Item", item.item_code, 'has_serial_no'), - 'projected_qty': flt(item.projected_qty, precision), - 'reserved_qty': flt(item.reserved_qty, precision), - 'reserved_qty_for_production': flt(item.reserved_qty_for_production, precision), - 'reserved_qty_for_sub_contract': flt(item.reserved_qty_for_sub_contract, precision), - 'actual_qty': flt(item.actual_qty, precision), - }) + item.update( + { + "item_name": frappe.get_cached_value("Item", item.item_code, "item_name"), + "disable_quick_entry": frappe.get_cached_value("Item", item.item_code, "has_batch_no") + or frappe.get_cached_value("Item", item.item_code, "has_serial_no"), + "projected_qty": flt(item.projected_qty, precision), + "reserved_qty": flt(item.reserved_qty, precision), + "reserved_qty_for_production": flt(item.reserved_qty_for_production, precision), + "reserved_qty_for_sub_contract": flt(item.reserved_qty_for_sub_contract, precision), + "actual_qty": flt(item.actual_qty, precision), + } + ) return items diff --git a/erpnext/stock/dashboard/warehouse_capacity_dashboard.py b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py index c0666cffc7..24e0ef11ff 100644 --- a/erpnext/stock/dashboard/warehouse_capacity_dashboard.py +++ b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py @@ -6,8 +6,15 @@ from erpnext.stock.utils import get_stock_balance @frappe.whitelist() -def get_data(item_code=None, warehouse=None, parent_warehouse=None, - company=None, start=0, sort_by="stock_capacity", sort_order="desc"): +def get_data( + item_code=None, + warehouse=None, + parent_warehouse=None, + company=None, + start=0, + sort_by="stock_capacity", + sort_order="desc", +): """Return data to render the warehouse capacity dashboard.""" filters = get_filters(item_code, warehouse, parent_warehouse, company) @@ -18,51 +25,59 @@ def get_data(item_code=None, warehouse=None, parent_warehouse=None, capacity_data = get_warehouse_capacity_data(filters, start) asc_desc = -1 if sort_order == "desc" else 1 - capacity_data = sorted(capacity_data, key = lambda i: (i[sort_by] * asc_desc)) + capacity_data = sorted(capacity_data, key=lambda i: (i[sort_by] * asc_desc)) return capacity_data -def get_filters(item_code=None, warehouse=None, parent_warehouse=None, - company=None): - filters = [['disable', '=', 0]] + +def get_filters(item_code=None, warehouse=None, parent_warehouse=None, company=None): + filters = [["disable", "=", 0]] if item_code: - filters.append(['item_code', '=', item_code]) + filters.append(["item_code", "=", item_code]) if warehouse: - filters.append(['warehouse', '=', warehouse]) + filters.append(["warehouse", "=", warehouse]) if company: - filters.append(['company', '=', company]) + filters.append(["company", "=", company]) if parent_warehouse: lft, rgt = frappe.db.get_value("Warehouse", parent_warehouse, ["lft", "rgt"]) - warehouses = frappe.db.sql_list(""" + warehouses = frappe.db.sql_list( + """ select name from `tabWarehouse` where lft >=%s and rgt<=%s - """, (lft, rgt)) - filters.append(['warehouse', 'in', warehouses]) + """, + (lft, rgt), + ) + filters.append(["warehouse", "in", warehouses]) return filters + def get_warehouse_filter_based_on_permissions(filters): try: # check if user has any restrictions based on user permissions on warehouse - if DatabaseQuery('Warehouse', user=frappe.session.user).build_match_conditions(): - filters.append(['warehouse', 'in', [w.name for w in frappe.get_list('Warehouse')]]) + if DatabaseQuery("Warehouse", user=frappe.session.user).build_match_conditions(): + filters.append(["warehouse", "in", [w.name for w in frappe.get_list("Warehouse")]]) return False, filters except frappe.PermissionError: # user does not have access on warehouse return True, [] + def get_warehouse_capacity_data(filters, start): - capacity_data = frappe.db.get_all('Putaway Rule', - fields=['item_code', 'warehouse','stock_capacity', 'company'], + capacity_data = frappe.db.get_all( + "Putaway Rule", + fields=["item_code", "warehouse", "stock_capacity", "company"], filters=filters, limit_start=start, - limit_page_length='11' + limit_page_length="11", ) for entry in capacity_data: balance_qty = get_stock_balance(entry.item_code, entry.warehouse, nowdate()) or 0 - entry.update({ - 'actual_qty': balance_qty, - 'percent_occupied': flt((flt(balance_qty) / flt(entry.stock_capacity)) * 100, 0) - }) + entry.update( + { + "actual_qty": balance_qty, + "percent_occupied": flt((flt(balance_qty) / flt(entry.stock_capacity)) * 100, 0), + } + ) return capacity_data diff --git a/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py b/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py index d835420b9e..dbf6cf05e7 100644 --- a/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py +++ b/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py @@ -11,27 +11,38 @@ from erpnext.stock.utils import get_stock_value_from_bin @frappe.whitelist() @cache_source -def get(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None, - to_date = None, timespan = None, time_interval = None, heatmap_year = None): +def get( + chart_name=None, + chart=None, + no_cache=None, + filters=None, + from_date=None, + to_date=None, + timespan=None, + time_interval=None, + heatmap_year=None, +): labels, datapoints = [], [] filters = frappe.parse_json(filters) - warehouse_filters = [['is_group', '=', 0]] + warehouse_filters = [["is_group", "=", 0]] if filters and filters.get("company"): - warehouse_filters.append(['company', '=', filters.get("company")]) + warehouse_filters.append(["company", "=", filters.get("company")]) - warehouses = frappe.get_list("Warehouse", fields=['name'], filters=warehouse_filters, order_by='name') + warehouses = frappe.get_list( + "Warehouse", fields=["name"], filters=warehouse_filters, order_by="name" + ) for wh in warehouses: balance = get_stock_value_from_bin(warehouse=wh.name) wh["balance"] = balance[0][0] - warehouses = [x for x in warehouses if not (x.get('balance') == None)] + warehouses = [x for x in warehouses if not (x.get("balance") == None)] if not warehouses: return [] - sorted_warehouse_map = sorted(warehouses, key = lambda i: i['balance'], reverse=True) + sorted_warehouse_map = sorted(warehouses, key=lambda i: i["balance"], reverse=True) if len(sorted_warehouse_map) > 10: sorted_warehouse_map = sorted_warehouse_map[:10] @@ -40,11 +51,8 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d labels.append(_(warehouse.get("name"))) datapoints.append(warehouse.get("balance")) - return{ + return { "labels": labels, - "datasets": [{ - "name": _("Stock Value"), - "values": datapoints - }], - "type": "bar" + "datasets": [{"name": _("Stock Value"), "values": datapoints}], + "type": "bar", } diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index c9b4c147f1..aac6cd386c 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -23,7 +23,7 @@ def get_name_from_hash(): temp = None while not temp: temp = frappe.generate_hash()[:7].upper() - if frappe.db.exists('Batch', temp): + if frappe.db.exists("Batch", temp): temp = None return temp @@ -34,7 +34,7 @@ def batch_uses_naming_series(): Verify if the Batch is to be named using a naming series :return: bool """ - use_naming_series = cint(frappe.db.get_single_value('Stock Settings', 'use_naming_series')) + use_naming_series = cint(frappe.db.get_single_value("Stock Settings", "use_naming_series")) return bool(use_naming_series) @@ -46,9 +46,9 @@ def _get_batch_prefix(): is set to use naming series. :return: The naming series. """ - naming_series_prefix = frappe.db.get_single_value('Stock Settings', 'naming_series_prefix') + naming_series_prefix = frappe.db.get_single_value("Stock Settings", "naming_series_prefix") if not naming_series_prefix: - naming_series_prefix = 'BATCH-' + naming_series_prefix = "BATCH-" return naming_series_prefix @@ -62,9 +62,9 @@ def _make_naming_series_key(prefix): :return: The derived key. If no prefix is given, an empty string is returned """ if not str(prefix): - return '' + return "" else: - return prefix.upper() + '.#####' + return prefix.upper() + ".#####" def get_batch_naming_series(): @@ -74,7 +74,7 @@ def get_batch_naming_series(): Naming series key is in the format [prefix].[#####] :return: The naming series or empty string if not available """ - series = '' + series = "" if batch_uses_naming_series(): prefix = _get_batch_prefix() key = _make_naming_series_key(prefix) @@ -87,8 +87,9 @@ class Batch(Document): def autoname(self): """Generate random ID for batch if not specified""" if not self.batch_id: - create_new_batch, batch_number_series = frappe.db.get_value('Item', self.item, - ['create_new_batch', 'batch_number_series']) + create_new_batch, batch_number_series = frappe.db.get_value( + "Item", self.item, ["create_new_batch", "batch_number_series"] + ) if create_new_batch: if batch_number_series: @@ -98,12 +99,12 @@ class Batch(Document): else: self.batch_id = get_name_from_hash() else: - frappe.throw(_('Batch ID is mandatory'), frappe.MandatoryError) + frappe.throw(_("Batch ID is mandatory"), frappe.MandatoryError) self.name = self.batch_id def onload(self): - self.image = frappe.db.get_value('Item', self.item, 'image') + self.image = frappe.db.get_value("Item", self.item, "image") def after_delete(self): revert_series_if_last(get_batch_naming_series(), self.name) @@ -123,16 +124,21 @@ class Batch(Document): self.use_batchwise_valuation = 1 def before_save(self): - has_expiry_date, shelf_life_in_days = frappe.db.get_value('Item', self.item, ['has_expiry_date', 'shelf_life_in_days']) + has_expiry_date, shelf_life_in_days = frappe.db.get_value( + "Item", self.item, ["has_expiry_date", "shelf_life_in_days"] + ) if not self.expiry_date and has_expiry_date and shelf_life_in_days: self.expiry_date = add_days(self.manufacturing_date, shelf_life_in_days) if has_expiry_date and not self.expiry_date: - frappe.throw(msg=_("Please set {0} for Batched Item {1}, which is used to set {2} on Submit.") \ - .format(frappe.bold("Shelf Life in Days"), + frappe.throw( + msg=_("Please set {0} for Batched Item {1}, which is used to set {2} on Submit.").format( + frappe.bold("Shelf Life in Days"), get_link_to_form("Item", self.item), - frappe.bold("Batch Expiry Date")), - title=_("Expiry Date Mandatory")) + frappe.bold("Batch Expiry Date"), + ), + title=_("Expiry Date Mandatory"), + ) def get_name_from_naming_series(self): """ @@ -149,9 +155,11 @@ class Batch(Document): @frappe.whitelist() -def get_batch_qty(batch_no=None, warehouse=None, item_code=None, posting_date=None, posting_time=None): +def get_batch_qty( + batch_no=None, warehouse=None, item_code=None, posting_date=None, posting_time=None +): """Returns batch actual qty if warehouse is passed, - or returns dict of qty by warehouse if warehouse is None + or returns dict of qty by warehouse if warehouse is None The user must pass either batch_no or batch_no + warehouse or item_code + warehouse @@ -163,25 +171,41 @@ def get_batch_qty(batch_no=None, warehouse=None, item_code=None, posting_date=No if batch_no and warehouse: cond = "" if posting_date and posting_time: - cond = " and timestamp(posting_date, posting_time) <= timestamp('{0}', '{1}')".format(posting_date, - posting_time) + cond = " and timestamp(posting_date, posting_time) <= timestamp('{0}', '{1}')".format( + posting_date, posting_time + ) - out = float(frappe.db.sql("""select sum(actual_qty) + out = float( + frappe.db.sql( + """select sum(actual_qty) from `tabStock Ledger Entry` - where is_cancelled = 0 and warehouse=%s and batch_no=%s {0}""".format(cond), - (warehouse, batch_no))[0][0] or 0) + where is_cancelled = 0 and warehouse=%s and batch_no=%s {0}""".format( + cond + ), + (warehouse, batch_no), + )[0][0] + or 0 + ) if batch_no and not warehouse: - out = frappe.db.sql('''select warehouse, sum(actual_qty) as qty + out = frappe.db.sql( + """select warehouse, sum(actual_qty) as qty from `tabStock Ledger Entry` where is_cancelled = 0 and batch_no=%s - group by warehouse''', batch_no, as_dict=1) + group by warehouse""", + batch_no, + as_dict=1, + ) if not batch_no and item_code and warehouse: - out = frappe.db.sql('''select batch_no, sum(actual_qty) as qty + out = frappe.db.sql( + """select batch_no, sum(actual_qty) as qty from `tabStock Ledger Entry` where is_cancelled = 0 and item_code = %s and warehouse=%s - group by batch_no''', (item_code, warehouse), as_dict=1) + group by batch_no""", + (item_code, warehouse), + as_dict=1, + ) return out @@ -190,7 +214,9 @@ def get_batch_qty(batch_no=None, warehouse=None, item_code=None, posting_date=No def get_batches_by_oldest(item_code, warehouse): """Returns the oldest batch and qty for the given item_code and warehouse""" batches = get_batch_qty(item_code=item_code, warehouse=warehouse) - batches_dates = [[batch, frappe.get_value('Batch', batch.batch_no, 'expiry_date')] for batch in batches] + batches_dates = [ + [batch, frappe.get_value("Batch", batch.batch_no, "expiry_date")] for batch in batches + ] batches_dates.sort(key=lambda tup: tup[1]) return batches_dates @@ -198,33 +224,25 @@ def get_batches_by_oldest(item_code, warehouse): @frappe.whitelist() def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None): """Split the batch into a new batch""" - batch = frappe.get_doc(dict(doctype='Batch', item=item_code, batch_id=new_batch_id)).insert() + batch = frappe.get_doc(dict(doctype="Batch", item=item_code, batch_id=new_batch_id)).insert() - company = frappe.db.get_value('Stock Ledger Entry', dict( - item_code=item_code, - batch_no=batch_no, - warehouse=warehouse - ), ['company']) + company = frappe.db.get_value( + "Stock Ledger Entry", + dict(item_code=item_code, batch_no=batch_no, warehouse=warehouse), + ["company"], + ) - stock_entry = frappe.get_doc(dict( - doctype='Stock Entry', - purpose='Repack', - company=company, - items=[ - dict( - item_code=item_code, - qty=float(qty or 0), - s_warehouse=warehouse, - batch_no=batch_no - ), - dict( - item_code=item_code, - qty=float(qty or 0), - t_warehouse=warehouse, - batch_no=batch.name - ), - ] - )) + stock_entry = frappe.get_doc( + dict( + doctype="Stock Entry", + purpose="Repack", + company=company, + items=[ + dict(item_code=item_code, qty=float(qty or 0), s_warehouse=warehouse, batch_no=batch_no), + dict(item_code=item_code, qty=float(qty or 0), t_warehouse=warehouse, batch_no=batch.name), + ], + ) + ) stock_entry.set_stock_entry_type() stock_entry.insert() stock_entry.submit() @@ -235,15 +253,20 @@ def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None): def set_batch_nos(doc, warehouse_field, throw=False, child_table="items"): """Automatically select `batch_no` for outgoing items in item table""" for d in doc.get(child_table): - qty = d.get('stock_qty') or d.get('transfer_qty') or d.get('qty') or 0 + qty = d.get("stock_qty") or d.get("transfer_qty") or d.get("qty") or 0 warehouse = d.get(warehouse_field, None) - if warehouse and qty > 0 and frappe.db.get_value('Item', d.item_code, 'has_batch_no'): + if warehouse and qty > 0 and frappe.db.get_value("Item", d.item_code, "has_batch_no"): if not d.batch_no: d.batch_no = get_batch_no(d.item_code, warehouse, qty, throw, d.serial_no) else: batch_qty = get_batch_qty(batch_no=d.batch_no, warehouse=warehouse) if flt(batch_qty, d.precision("qty")) < flt(qty, d.precision("qty")): - frappe.throw(_("Row #{0}: The batch {1} has only {2} qty. Please select another batch which has {3} qty available or split the row into multiple rows, to deliver/issue from multiple batches").format(d.idx, d.batch_no, batch_qty, qty)) + frappe.throw( + _( + "Row #{0}: The batch {1} has only {2} qty. Please select another batch which has {3} qty available or split the row into multiple rows, to deliver/issue from multiple batches" + ).format(d.idx, d.batch_no, batch_qty, qty) + ) + @frappe.whitelist() def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None): @@ -264,7 +287,11 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None): break if not batch_no: - frappe.msgprint(_('Please select a Batch for Item {0}. Unable to find a single batch that fulfills this requirement').format(frappe.bold(item_code))) + frappe.msgprint( + _( + "Please select a Batch for Item {0}. Unable to find a single batch that fulfills this requirement" + ).format(frappe.bold(item_code)) + ) if throw: raise UnableToSelectBatchError @@ -273,16 +300,14 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None): def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos - cond = '' - if serial_no and frappe.get_cached_value('Item', item_code, 'has_batch_no'): + + cond = "" + if serial_no and frappe.get_cached_value("Item", item_code, "has_batch_no"): serial_nos = get_serial_nos(serial_no) - batch = frappe.get_all("Serial No", - fields = ["distinct batch_no"], - filters= { - "item_code": item_code, - "warehouse": warehouse, - "name": ("in", serial_nos) - } + batch = frappe.get_all( + "Serial No", + fields=["distinct batch_no"], + filters={"item_code": item_code, "warehouse": warehouse, "name": ("in", serial_nos)}, ) if not batch: @@ -291,9 +316,10 @@ def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None): if batch and len(batch) > 1: return [] - cond = " and `tabBatch`.name = %s" %(frappe.db.escape(batch[0].batch_no)) + cond = " and `tabBatch`.name = %s" % (frappe.db.escape(batch[0].batch_no)) - return frappe.db.sql(""" + return frappe.db.sql( + """ select batch_id, sum(`tabStock Ledger Entry`.actual_qty) as qty from `tabBatch` join `tabStock Ledger Entry` ignore index (item_code, warehouse) @@ -303,24 +329,34 @@ def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None): and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL) {0} group by batch_id order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC - """.format(cond), (item_code, warehouse), as_dict=True) + """.format( + cond + ), + (item_code, warehouse), + as_dict=True, + ) + def validate_serial_no_with_batch(serial_nos, item_code): if frappe.get_cached_value("Serial No", serial_nos[0], "item_code") != item_code: - frappe.throw(_("The serial no {0} does not belong to item {1}") - .format(get_link_to_form("Serial No", serial_nos[0]), get_link_to_form("Item", item_code))) + frappe.throw( + _("The serial no {0} does not belong to item {1}").format( + get_link_to_form("Serial No", serial_nos[0]), get_link_to_form("Item", item_code) + ) + ) - serial_no_link = ','.join(get_link_to_form("Serial No", sn) for sn in serial_nos) + serial_no_link = ",".join(get_link_to_form("Serial No", sn) for sn in serial_nos) message = "Serial Nos" if len(serial_nos) > 1 else "Serial No" - frappe.throw(_("There is no batch found against the {0}: {1}") - .format(message, serial_no_link)) + frappe.throw(_("There is no batch found against the {0}: {1}").format(message, serial_no_link)) + def make_batch(args): if frappe.db.get_value("Item", args.item, "has_batch_no"): args.doctype = "Batch" frappe.get_doc(args).insert().name + @frappe.whitelist() def get_pos_reserved_batch_qty(filters): import json @@ -332,16 +368,22 @@ def get_pos_reserved_batch_qty(filters): item = frappe.qb.DocType("POS Invoice Item").as_("item") sum_qty = frappe.query_builder.functions.Sum(item.qty).as_("qty") - reserved_batch_qty = frappe.qb.from_(p).from_(item).select(sum_qty).where( - (p.name == item.parent) & - (p.consolidated_invoice.isnull()) & - (p.status != "Consolidated") & - (p.docstatus == 1) & - (item.docstatus == 1) & - (item.item_code == filters.get('item_code')) & - (item.warehouse == filters.get('warehouse')) & - (item.batch_no == filters.get('batch_no')) - ).run() + reserved_batch_qty = ( + frappe.qb.from_(p) + .from_(item) + .select(sum_qty) + .where( + (p.name == item.parent) + & (p.consolidated_invoice.isnull()) + & (p.status != "Consolidated") + & (p.docstatus == 1) + & (item.docstatus == 1) + & (item.item_code == filters.get("item_code")) + & (item.warehouse == filters.get("warehouse")) + & (item.batch_no == filters.get("batch_no")) + ) + .run() + ) flt_reserved_batch_qty = flt(reserved_batch_qty[0][0]) return flt_reserved_batch_qty diff --git a/erpnext/stock/doctype/batch/batch_dashboard.py b/erpnext/stock/doctype/batch/batch_dashboard.py index 725365b2fc..84b64f36f4 100644 --- a/erpnext/stock/doctype/batch/batch_dashboard.py +++ b/erpnext/stock/doctype/batch/batch_dashboard.py @@ -3,23 +3,11 @@ from frappe import _ def get_data(): return { - 'fieldname': 'batch_no', - 'transactions': [ - { - 'label': _('Buy'), - 'items': ['Purchase Invoice', 'Purchase Receipt'] - }, - { - 'label': _('Sell'), - 'items': ['Sales Invoice', 'Delivery Note'] - }, - { - 'label': _('Move'), - 'items': ['Stock Entry'] - }, - { - 'label': _('Quality'), - 'items': ['Quality Inspection'] - } - ] + "fieldname": "batch_no", + "transactions": [ + {"label": _("Buy"), "items": ["Purchase Invoice", "Purchase Receipt"]}, + {"label": _("Sell"), "items": ["Sales Invoice", "Delivery Note"]}, + {"label": _("Move"), "items": ["Stock Entry"]}, + {"label": _("Quality"), "items": ["Quality Inspection"]}, + ], } diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index 5763753853..c76da626b5 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -21,134 +21,127 @@ from erpnext.stock.stock_ledger import get_valuation_rate class TestBatch(FrappeTestCase): def test_item_has_batch_enabled(self): - self.assertRaises(ValidationError, frappe.get_doc({ - "doctype": "Batch", - "name": "_test Batch", - "item": "_Test Item" - }).save) + self.assertRaises( + ValidationError, + frappe.get_doc({"doctype": "Batch", "name": "_test Batch", "item": "_Test Item"}).save, + ) @classmethod def make_batch_item(cls, item_name): from erpnext.stock.doctype.item.test_item import make_item + if not frappe.db.exists(item_name): - return make_item(item_name, dict(has_batch_no = 1, create_new_batch = 1, is_stock_item=1)) + return make_item(item_name, dict(has_batch_no=1, create_new_batch=1, is_stock_item=1)) - def test_purchase_receipt(self, batch_qty = 100): - '''Test automated batch creation from Purchase Receipt''' - self.make_batch_item('ITEM-BATCH-1') + def test_purchase_receipt(self, batch_qty=100): + """Test automated batch creation from Purchase Receipt""" + self.make_batch_item("ITEM-BATCH-1") - receipt = frappe.get_doc(dict( - doctype='Purchase Receipt', - supplier='_Test Supplier', - company='_Test Company', - items=[ - dict( - item_code='ITEM-BATCH-1', - qty=batch_qty, - rate=10, - warehouse= 'Stores - _TC' - ) - ] - )).insert() + receipt = frappe.get_doc( + dict( + doctype="Purchase Receipt", + supplier="_Test Supplier", + company="_Test Company", + items=[dict(item_code="ITEM-BATCH-1", qty=batch_qty, rate=10, warehouse="Stores - _TC")], + ) + ).insert() receipt.submit() self.assertTrue(receipt.items[0].batch_no) - self.assertEqual(get_batch_qty(receipt.items[0].batch_no, - receipt.items[0].warehouse), batch_qty) + self.assertEqual(get_batch_qty(receipt.items[0].batch_no, receipt.items[0].warehouse), batch_qty) return receipt def test_stock_entry_incoming(self): - '''Test batch creation via Stock Entry (Work Order)''' + """Test batch creation via Stock Entry (Work Order)""" - self.make_batch_item('ITEM-BATCH-1') + self.make_batch_item("ITEM-BATCH-1") - stock_entry = frappe.get_doc(dict( - doctype = 'Stock Entry', - purpose = 'Material Receipt', - company = '_Test Company', - items = [ - dict( - item_code = 'ITEM-BATCH-1', - qty = 90, - t_warehouse = '_Test Warehouse - _TC', - cost_center = 'Main - _TC', - rate = 10 - ) - ] - )) + stock_entry = frappe.get_doc( + dict( + doctype="Stock Entry", + purpose="Material Receipt", + company="_Test Company", + items=[ + dict( + item_code="ITEM-BATCH-1", + qty=90, + t_warehouse="_Test Warehouse - _TC", + cost_center="Main - _TC", + rate=10, + ) + ], + ) + ) stock_entry.set_stock_entry_type() stock_entry.insert() stock_entry.submit() self.assertTrue(stock_entry.items[0].batch_no) - self.assertEqual(get_batch_qty(stock_entry.items[0].batch_no, stock_entry.items[0].t_warehouse), 90) + self.assertEqual( + get_batch_qty(stock_entry.items[0].batch_no, stock_entry.items[0].t_warehouse), 90 + ) def test_delivery_note(self): - '''Test automatic batch selection for outgoing items''' + """Test automatic batch selection for outgoing items""" batch_qty = 15 receipt = self.test_purchase_receipt(batch_qty) - item_code = 'ITEM-BATCH-1' + item_code = "ITEM-BATCH-1" - delivery_note = frappe.get_doc(dict( - doctype='Delivery Note', - customer='_Test Customer', - company=receipt.company, - items=[ - dict( - item_code=item_code, - qty=batch_qty, - rate=10, - warehouse=receipt.items[0].warehouse - ) - ] - )).insert() + delivery_note = frappe.get_doc( + dict( + doctype="Delivery Note", + customer="_Test Customer", + company=receipt.company, + items=[ + dict(item_code=item_code, qty=batch_qty, rate=10, warehouse=receipt.items[0].warehouse) + ], + ) + ).insert() delivery_note.submit() # shipped from FEFO batch self.assertEqual( - delivery_note.items[0].batch_no, - get_batch_no(item_code, receipt.items[0].warehouse, batch_qty) + delivery_note.items[0].batch_no, get_batch_no(item_code, receipt.items[0].warehouse, batch_qty) ) def test_delivery_note_fail(self): - '''Test automatic batch selection for outgoing items''' + """Test automatic batch selection for outgoing items""" receipt = self.test_purchase_receipt(100) - delivery_note = frappe.get_doc(dict( - doctype = 'Delivery Note', - customer = '_Test Customer', - company = receipt.company, - items = [ - dict( - item_code = 'ITEM-BATCH-1', - qty = 5000, - rate = 10, - warehouse = receipt.items[0].warehouse - ) - ] - )) + delivery_note = frappe.get_doc( + dict( + doctype="Delivery Note", + customer="_Test Customer", + company=receipt.company, + items=[ + dict(item_code="ITEM-BATCH-1", qty=5000, rate=10, warehouse=receipt.items[0].warehouse) + ], + ) + ) self.assertRaises(UnableToSelectBatchError, delivery_note.insert) def test_stock_entry_outgoing(self): - '''Test automatic batch selection for outgoing stock entry''' + """Test automatic batch selection for outgoing stock entry""" batch_qty = 16 receipt = self.test_purchase_receipt(batch_qty) - item_code = 'ITEM-BATCH-1' + item_code = "ITEM-BATCH-1" - stock_entry = frappe.get_doc(dict( - doctype='Stock Entry', - purpose='Material Issue', - company=receipt.company, - items=[ - dict( - item_code=item_code, - qty=batch_qty, - s_warehouse=receipt.items[0].warehouse, - ) - ] - )) + stock_entry = frappe.get_doc( + dict( + doctype="Stock Entry", + purpose="Material Issue", + company=receipt.company, + items=[ + dict( + item_code=item_code, + qty=batch_qty, + s_warehouse=receipt.items[0].warehouse, + ) + ], + ) + ) stock_entry.set_stock_entry_type() stock_entry.insert() @@ -156,35 +149,38 @@ class TestBatch(FrappeTestCase): # assert same batch is selected self.assertEqual( - stock_entry.items[0].batch_no, - get_batch_no(item_code, receipt.items[0].warehouse, batch_qty) + stock_entry.items[0].batch_no, get_batch_no(item_code, receipt.items[0].warehouse, batch_qty) ) def test_batch_split(self): - '''Test batch splitting''' + """Test batch splitting""" receipt = self.test_purchase_receipt() from erpnext.stock.doctype.batch.batch import split_batch - new_batch = split_batch(receipt.items[0].batch_no, 'ITEM-BATCH-1', receipt.items[0].warehouse, 22) + new_batch = split_batch( + receipt.items[0].batch_no, "ITEM-BATCH-1", receipt.items[0].warehouse, 22 + ) self.assertEqual(get_batch_qty(receipt.items[0].batch_no, receipt.items[0].warehouse), 78) self.assertEqual(get_batch_qty(new_batch, receipt.items[0].warehouse), 22) def test_get_batch_qty(self): - '''Test getting batch quantities by batch_numbers, item_code or warehouse''' - self.make_batch_item('ITEM-BATCH-2') - self.make_new_batch_and_entry('ITEM-BATCH-2', 'batch a', '_Test Warehouse - _TC') - self.make_new_batch_and_entry('ITEM-BATCH-2', 'batch b', '_Test Warehouse - _TC') + """Test getting batch quantities by batch_numbers, item_code or warehouse""" + self.make_batch_item("ITEM-BATCH-2") + self.make_new_batch_and_entry("ITEM-BATCH-2", "batch a", "_Test Warehouse - _TC") + self.make_new_batch_and_entry("ITEM-BATCH-2", "batch b", "_Test Warehouse - _TC") - self.assertEqual(get_batch_qty(item_code = 'ITEM-BATCH-2', warehouse = '_Test Warehouse - _TC'), - [{'batch_no': u'batch a', 'qty': 90.0}, {'batch_no': u'batch b', 'qty': 90.0}]) + self.assertEqual( + get_batch_qty(item_code="ITEM-BATCH-2", warehouse="_Test Warehouse - _TC"), + [{"batch_no": "batch a", "qty": 90.0}, {"batch_no": "batch b", "qty": 90.0}], + ) - self.assertEqual(get_batch_qty('batch a', '_Test Warehouse - _TC'), 90) + self.assertEqual(get_batch_qty("batch a", "_Test Warehouse - _TC"), 90) def test_total_batch_qty(self): - self.make_batch_item('ITEM-BATCH-3') + self.make_batch_item("ITEM-BATCH-3") existing_batch_qty = flt(frappe.db.get_value("Batch", "B100", "batch_qty")) - stock_entry = self.make_new_batch_and_entry('ITEM-BATCH-3', 'B100', '_Test Warehouse - _TC') + stock_entry = self.make_new_batch_and_entry("ITEM-BATCH-3", "B100", "_Test Warehouse - _TC") current_batch_qty = flt(frappe.db.get_value("Batch", "B100", "batch_qty")) self.assertEqual(current_batch_qty, existing_batch_qty + 90) @@ -195,32 +191,32 @@ class TestBatch(FrappeTestCase): @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''' + """Make a new stock entry for given target warehouse and batch name of item""" if not frappe.db.exists("Batch", batch_name): - batch = frappe.get_doc(dict( - doctype = 'Batch', - item = item_name, - batch_id = batch_name - )).insert(ignore_permissions=True) + batch = frappe.get_doc(dict(doctype="Batch", item=item_name, batch_id=batch_name)).insert( + ignore_permissions=True + ) batch.save() - stock_entry = frappe.get_doc(dict( - doctype = 'Stock Entry', - purpose = 'Material Receipt', - company = '_Test Company', - items = [ - dict( - item_code = item_name, - qty = 90, - t_warehouse = warehouse, - cost_center = 'Main - _TC', - rate = 10, - batch_no = batch_name, - allow_zero_valuation_rate = 1 - ) - ] - )) + stock_entry = frappe.get_doc( + dict( + doctype="Stock Entry", + purpose="Material Receipt", + company="_Test Company", + items=[ + dict( + item_code=item_name, + qty=90, + t_warehouse=warehouse, + cost_center="Main - _TC", + rate=10, + batch_no=batch_name, + allow_zero_valuation_rate=1, + ) + ], + ) + ) stock_entry.set_stock_entry_type() stock_entry.insert() @@ -229,28 +225,28 @@ class TestBatch(FrappeTestCase): return stock_entry def test_batch_name_with_naming_series(self): - stock_settings = frappe.get_single('Stock Settings') + stock_settings = frappe.get_single("Stock Settings") use_naming_series = cint(stock_settings.use_naming_series) if not use_naming_series: - frappe.set_value('Stock Settings', 'Stock Settings', 'use_naming_series', 1) + frappe.set_value("Stock Settings", "Stock Settings", "use_naming_series", 1) - batch = self.make_new_batch('_Test Stock Item For Batch Test1') + batch = self.make_new_batch("_Test Stock Item For Batch Test1") batch_name = batch.name - self.assertTrue(batch_name.startswith('BATCH-')) + self.assertTrue(batch_name.startswith("BATCH-")) batch.delete() - batch = self.make_new_batch('_Test Stock Item For Batch Test2') + batch = self.make_new_batch("_Test Stock Item For Batch Test2") self.assertEqual(batch_name, batch.name) # reset Stock Settings if not use_naming_series: - frappe.set_value('Stock Settings', 'Stock Settings', 'use_naming_series', 0) + frappe.set_value("Stock Settings", "Stock Settings", "use_naming_series", 0) def make_new_batch(self, item_name, batch_id=None, do_not_insert=0): - batch = frappe.new_doc('Batch') + batch = frappe.new_doc("Batch") item = self.make_batch_item(item_name) batch.item = item.name @@ -263,53 +259,56 @@ class TestBatch(FrappeTestCase): 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) + 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) + 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) company = "_Test Company with perpetual inventory" - currency = frappe.get_cached_value("Company", company, "default_currency") + currency = frappe.get_cached_value("Company", company, "default_currency") - args = frappe._dict({ - "item_code": "_Test Batch Price Item", - "company": company, - "price_list": "_Test Price List", - "currency": currency, - "doctype": "Sales Invoice", - "conversion_rate": 1, - "price_list_currency": "_Test Currency", - "plc_conversion_rate": 1, - "customer": "_Test Customer", - "name": None - }) + args = frappe._dict( + { + "item_code": "_Test Batch Price Item", + "company": company, + "price_list": "_Test Price List", + "currency": 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}) + # test price for batch1 + args.update({"batch_no": batch1}) details = get_item_details(args) - self.assertEqual(details.get('price_list_rate'), 200) + self.assertEqual(details.get("price_list_rate"), 200) - #test price for batch2 - args.update({'batch_no': batch2}) + # test price for batch2 + args.update({"batch_no": batch2}) details = get_item_details(args) - self.assertEqual(details.get('price_list_rate'), 300) + self.assertEqual(details.get("price_list_rate"), 300) - #test price for batch3 - args.update({'batch_no': batch3}) + # test price for batch3 + args.update({"batch_no": batch3}) details = get_item_details(args) - self.assertEqual(details.get('price_list_rate'), 400) + self.assertEqual(details.get("price_list_rate"), 400) - - def test_basic_batch_wise_valuation(self, batch_qty = 100): + def test_basic_batch_wise_valuation(self, batch_qty=100): item_code = "_TestBatchWiseVal" warehouse = "_Test Warehouse - _TC" self.make_batch_item(item_code) @@ -358,7 +357,9 @@ class TestBatch(FrappeTestCase): self.make_batch_item(item_code) def assertValuation(expected): - actual = get_valuation_rate(item_code, warehouse, "voucher_type", "voucher_no", batch_no=batch_no) + actual = get_valuation_rate( + item_code, warehouse, "voucher_type", "voucher_no", batch_no=batch_no + ) self.assertAlmostEqual(actual, expected) se = make_stock_entry(item_code=item_code, qty=100, rate=10, target=warehouse) @@ -381,13 +382,14 @@ class TestBatch(FrappeTestCase): assertValuation(15) # reset rate with stock reconiliation - create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=10, rate=25, batch_no=batch_no) + create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=10, rate=25, batch_no=batch_no + ) assertValuation(25) make_stock_entry(item_code=item_code, qty=20, rate=20, target=warehouse, batch_no=batch_no) assertValuation((20 * 20 + 10 * 25) / (10 + 20)) - def test_update_batch_properties(self): item_code = "_TestBatchWiseVal" self.make_batch_item(item_code) @@ -406,13 +408,17 @@ class TestBatch(FrappeTestCase): self.assertEqual(getdate(batch.expiry_date), getdate(expiry_date)) - def create_batch(item_code, rate, create_item_price_for_batch): - pi = make_purchase_invoice(company="_Test Company", - warehouse= "Stores - _TC", cost_center = "Main - _TC", update_stock=1, - expense_account ="_Test Account Cost for Goods Sold - _TC", item_code=item_code) + pi = make_purchase_invoice( + company="_Test Company", + warehouse="Stores - _TC", + cost_center="Main - _TC", + update_stock=1, + expense_account="_Test Account Cost for Goods Sold - _TC", + item_code=item_code, + ) - batch = frappe.db.get_value('Batch', {'item': item_code, 'reference_name': pi.name}) + 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) @@ -421,14 +427,18 @@ def create_batch(item_code, rate, create_item_price_for_batch): 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() + 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) @@ -436,10 +446,12 @@ def make_new_batch(**args): if frappe.db.exists("Batch", args.batch_id): batch = frappe.get_doc("Batch", args.batch_id) else: - batch = frappe.get_doc({ - "doctype": "Batch", - "batch_id": args.batch_id, - "item": args.item_code, - }).insert() + batch = frappe.get_doc( + { + "doctype": "Batch", + "batch_id": args.batch_id, + "item": args.item_code, + } + ).insert() return batch diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 3bc15a8025..6cb9f7e479 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -12,93 +12,109 @@ from frappe.utils import flt class Bin(Document): def before_save(self): if self.get("__islocal") or not self.stock_uom: - self.stock_uom = frappe.get_cached_value('Item', self.item_code, 'stock_uom') + self.stock_uom = frappe.get_cached_value("Item", self.item_code, "stock_uom") self.set_projected_qty() def set_projected_qty(self): - self.projected_qty = (flt(self.actual_qty) + flt(self.ordered_qty) - + flt(self.indented_qty) + flt(self.planned_qty) - flt(self.reserved_qty) - - flt(self.reserved_qty_for_production) - flt(self.reserved_qty_for_sub_contract)) + self.projected_qty = ( + flt(self.actual_qty) + + flt(self.ordered_qty) + + flt(self.indented_qty) + + flt(self.planned_qty) + - flt(self.reserved_qty) + - flt(self.reserved_qty_for_production) + - flt(self.reserved_qty_for_sub_contract) + ) def update_reserved_qty_for_production(self): - '''Update qty reserved for production from Production Item tables - in open work orders''' + """Update qty reserved for production from Production Item tables + in open work orders""" from erpnext.manufacturing.doctype.work_order.work_order import get_reserved_qty_for_production - self.reserved_qty_for_production = get_reserved_qty_for_production(self.item_code, self.warehouse) + self.reserved_qty_for_production = get_reserved_qty_for_production( + self.item_code, self.warehouse + ) self.set_projected_qty() - self.db_set('reserved_qty_for_production', flt(self.reserved_qty_for_production)) - self.db_set('projected_qty', self.projected_qty) + self.db_set("reserved_qty_for_production", flt(self.reserved_qty_for_production)) + self.db_set("projected_qty", self.projected_qty) def update_reserved_qty_for_sub_contracting(self): - #reserved qty + # reserved qty po = frappe.qb.DocType("Purchase Order") supplied_item = frappe.qb.DocType("Purchase Order Item Supplied") reserved_qty_for_sub_contract = ( - frappe.qb - .from_(po) - .from_(supplied_item) - .select(Sum(Coalesce(supplied_item.required_qty, 0))) - .where( - (supplied_item.rm_item_code == self.item_code) - & (po.name == supplied_item.parent) - & (po.docstatus == 1) - & (po.is_subcontracted == "Yes") - & (po.status != "Closed") - & (po.per_received < 100) - & (supplied_item.reserve_warehouse == self.warehouse) - ) - ).run()[0][0] or 0.0 + frappe.qb.from_(po) + .from_(supplied_item) + .select(Sum(Coalesce(supplied_item.required_qty, 0))) + .where( + (supplied_item.rm_item_code == self.item_code) + & (po.name == supplied_item.parent) + & (po.docstatus == 1) + & (po.is_subcontracted == "Yes") + & (po.status != "Closed") + & (po.per_received < 100) + & (supplied_item.reserve_warehouse == self.warehouse) + ) + ).run()[0][0] or 0.0 se = frappe.qb.DocType("Stock Entry") se_item = frappe.qb.DocType("Stock Entry Detail") materials_transferred = ( - frappe.qb - .from_(se) - .from_(se_item) - .from_(po) - .select(Sum( - Case() - .when(se.is_return == 1, se_item.transfer_qty * -1) - .else_(se_item.transfer_qty) - )) - .where( - (se.docstatus == 1) - & (se.purpose == "Send to Subcontractor") - & (Coalesce(se.purchase_order, "") != "") - & ((se_item.item_code == self.item_code) - | (se_item.original_item == self.item_code)) - & (se.name == se_item.parent) - & (po.name == se.purchase_order) - & (po.docstatus == 1) - & (po.is_subcontracted == "Yes") - & (po.status != "Closed") - & (po.per_received < 100) - ) - ).run()[0][0] or 0.0 + frappe.qb.from_(se) + .from_(se_item) + .from_(po) + .select( + Sum(Case().when(se.is_return == 1, se_item.transfer_qty * -1).else_(se_item.transfer_qty)) + ) + .where( + (se.docstatus == 1) + & (se.purpose == "Send to Subcontractor") + & (Coalesce(se.purchase_order, "") != "") + & ((se_item.item_code == self.item_code) | (se_item.original_item == self.item_code)) + & (se.name == se_item.parent) + & (po.name == se.purchase_order) + & (po.docstatus == 1) + & (po.is_subcontracted == "Yes") + & (po.status != "Closed") + & (po.per_received < 100) + ) + ).run()[0][0] or 0.0 if reserved_qty_for_sub_contract > materials_transferred: reserved_qty_for_sub_contract = reserved_qty_for_sub_contract - materials_transferred else: reserved_qty_for_sub_contract = 0 - self.db_set('reserved_qty_for_sub_contract', reserved_qty_for_sub_contract) + self.db_set("reserved_qty_for_sub_contract", reserved_qty_for_sub_contract) self.set_projected_qty() - self.db_set('projected_qty', self.projected_qty) + self.db_set("projected_qty", self.projected_qty) + def on_doctype_update(): frappe.db.add_unique("Bin", ["item_code", "warehouse"], constraint_name="unique_item_warehouse") def get_bin_details(bin_name): - return frappe.db.get_value('Bin', bin_name, ['actual_qty', 'ordered_qty', - 'reserved_qty', 'indented_qty', 'planned_qty', 'reserved_qty_for_production', - 'reserved_qty_for_sub_contract'], as_dict=1) + return frappe.db.get_value( + "Bin", + bin_name, + [ + "actual_qty", + "ordered_qty", + "reserved_qty", + "indented_qty", + "planned_qty", + "reserved_qty_for_production", + "reserved_qty_for_sub_contract", + ], + as_dict=1, + ) + def update_qty(bin_name, args): from erpnext.controllers.stock_controller import future_sle_exists @@ -109,32 +125,45 @@ def update_qty(bin_name, args): # actual qty is not up to date in case of backdated transaction if future_sle_exists(args): - actual_qty = frappe.db.get_value("Stock Ledger Entry", + actual_qty = ( + frappe.db.get_value( + "Stock Ledger Entry", filters={ "item_code": args.get("item_code"), "warehouse": args.get("warehouse"), - "is_cancelled": 0 + "is_cancelled": 0, }, fieldname="qty_after_transaction", order_by="posting_date desc, posting_time desc, creation desc", - ) or 0.0 + ) + or 0.0 + ) ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty")) reserved_qty = flt(bin_details.reserved_qty) + flt(args.get("reserved_qty")) indented_qty = flt(bin_details.indented_qty) + flt(args.get("indented_qty")) planned_qty = flt(bin_details.planned_qty) + flt(args.get("planned_qty")) - # compute projected qty - projected_qty = (flt(actual_qty) + flt(ordered_qty) - + flt(indented_qty) + flt(planned_qty) - flt(reserved_qty) - - flt(bin_details.reserved_qty_for_production) - flt(bin_details.reserved_qty_for_sub_contract)) + projected_qty = ( + flt(actual_qty) + + flt(ordered_qty) + + flt(indented_qty) + + flt(planned_qty) + - flt(reserved_qty) + - flt(bin_details.reserved_qty_for_production) + - flt(bin_details.reserved_qty_for_sub_contract) + ) - frappe.db.set_value('Bin', bin_name, { - 'actual_qty': actual_qty, - 'ordered_qty': ordered_qty, - 'reserved_qty': reserved_qty, - 'indented_qty': indented_qty, - 'planned_qty': planned_qty, - 'projected_qty': projected_qty - }) + frappe.db.set_value( + "Bin", + bin_name, + { + "actual_qty": actual_qty, + "ordered_qty": ordered_qty, + "reserved_qty": reserved_qty, + "indented_qty": indented_qty, + "planned_qty": planned_qty, + "projected_qty": projected_qty, + }, + ) diff --git a/erpnext/stock/doctype/bin/test_bin.py b/erpnext/stock/doctype/bin/test_bin.py index ec0d8a88e3..b79dee81e2 100644 --- a/erpnext/stock/doctype/bin/test_bin.py +++ b/erpnext/stock/doctype/bin/test_bin.py @@ -9,10 +9,8 @@ from erpnext.stock.utils import _create_bin class TestBin(FrappeTestCase): - - def test_concurrent_inserts(self): - """ Ensure no duplicates are possible in case of concurrent inserts""" + """Ensure no duplicates are possible in case of concurrent inserts""" item_code = "_TestConcurrentBin" make_item(item_code) warehouse = "_Test Warehouse - _TC" diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 492f90b302..69e052bb81 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -15,72 +15,76 @@ from erpnext.controllers.selling_controller import SellingController from erpnext.stock.doctype.batch.batch import set_batch_nos from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no -form_grid_templates = { - "items": "templates/form_grid/item_grid.html" -} +form_grid_templates = {"items": "templates/form_grid/item_grid.html"} + class DeliveryNote(SellingController): def __init__(self, *args, **kwargs): super(DeliveryNote, self).__init__(*args, **kwargs) - self.status_updater = [{ - 'source_dt': 'Delivery Note Item', - 'target_dt': 'Sales Order Item', - 'join_field': 'so_detail', - 'target_field': 'delivered_qty', - 'target_parent_dt': 'Sales Order', - 'target_parent_field': 'per_delivered', - 'target_ref_field': 'qty', - 'source_field': 'qty', - 'percent_join_field': 'against_sales_order', - 'status_field': 'delivery_status', - 'keyword': 'Delivered', - 'second_source_dt': 'Sales Invoice Item', - 'second_source_field': 'qty', - 'second_join_field': 'so_detail', - 'overflow_type': 'delivery', - 'second_source_extra_cond': """ and exists(select name from `tabSales Invoice` - where name=`tabSales Invoice Item`.parent and update_stock = 1)""" - }, - { - 'source_dt': 'Delivery Note Item', - 'target_dt': 'Sales Invoice Item', - 'join_field': 'si_detail', - 'target_field': 'delivered_qty', - 'target_parent_dt': 'Sales Invoice', - 'target_ref_field': 'qty', - 'source_field': 'qty', - 'percent_join_field': 'against_sales_invoice', - 'overflow_type': 'delivery', - 'no_allowance': 1 - }] - if cint(self.is_return): - self.status_updater.extend([{ - 'source_dt': 'Delivery Note Item', - 'target_dt': 'Sales Order Item', - 'join_field': 'so_detail', - 'target_field': 'returned_qty', - 'target_parent_dt': 'Sales Order', - 'source_field': '-1 * qty', - 'second_source_dt': 'Sales Invoice Item', - 'second_source_field': '-1 * qty', - 'second_join_field': 'so_detail', - 'extra_cond': """ and exists (select name from `tabDelivery Note` - where name=`tabDelivery Note Item`.parent and is_return=1)""", - 'second_source_extra_cond': """ and exists (select name from `tabSales Invoice` - where name=`tabSales Invoice Item`.parent and is_return=1 and update_stock=1)""" + self.status_updater = [ + { + "source_dt": "Delivery Note Item", + "target_dt": "Sales Order Item", + "join_field": "so_detail", + "target_field": "delivered_qty", + "target_parent_dt": "Sales Order", + "target_parent_field": "per_delivered", + "target_ref_field": "qty", + "source_field": "qty", + "percent_join_field": "against_sales_order", + "status_field": "delivery_status", + "keyword": "Delivered", + "second_source_dt": "Sales Invoice Item", + "second_source_field": "qty", + "second_join_field": "so_detail", + "overflow_type": "delivery", + "second_source_extra_cond": """ and exists(select name from `tabSales Invoice` + where name=`tabSales Invoice Item`.parent and update_stock = 1)""", }, { - 'source_dt': 'Delivery Note Item', - 'target_dt': 'Delivery Note Item', - 'join_field': 'dn_detail', - 'target_field': 'returned_qty', - 'target_parent_dt': 'Delivery Note', - 'target_parent_field': 'per_returned', - 'target_ref_field': 'stock_qty', - 'source_field': '-1 * stock_qty', - 'percent_join_field_parent': 'return_against' - } - ]) + "source_dt": "Delivery Note Item", + "target_dt": "Sales Invoice Item", + "join_field": "si_detail", + "target_field": "delivered_qty", + "target_parent_dt": "Sales Invoice", + "target_ref_field": "qty", + "source_field": "qty", + "percent_join_field": "against_sales_invoice", + "overflow_type": "delivery", + "no_allowance": 1, + }, + ] + if cint(self.is_return): + self.status_updater.extend( + [ + { + "source_dt": "Delivery Note Item", + "target_dt": "Sales Order Item", + "join_field": "so_detail", + "target_field": "returned_qty", + "target_parent_dt": "Sales Order", + "source_field": "-1 * qty", + "second_source_dt": "Sales Invoice Item", + "second_source_field": "-1 * qty", + "second_join_field": "so_detail", + "extra_cond": """ and exists (select name from `tabDelivery Note` + where name=`tabDelivery Note Item`.parent and is_return=1)""", + "second_source_extra_cond": """ and exists (select name from `tabSales Invoice` + where name=`tabSales Invoice Item`.parent and is_return=1 and update_stock=1)""", + }, + { + "source_dt": "Delivery Note Item", + "target_dt": "Delivery Note Item", + "join_field": "dn_detail", + "target_field": "returned_qty", + "target_parent_dt": "Delivery Note", + "target_parent_field": "per_returned", + "target_ref_field": "stock_qty", + "source_field": "-1 * stock_qty", + "percent_join_field_parent": "return_against", + }, + ] + ) def before_print(self, settings=None): def toggle_print_hide(meta, fieldname): @@ -93,7 +97,7 @@ class DeliveryNote(SellingController): item_meta = frappe.get_meta("Delivery Note Item") print_hide_fields = { "parent": ["grand_total", "rounded_total", "in_words", "currency", "total", "taxes"], - "items": ["rate", "amount", "discount_amount", "price_list_rate", "discount_percentage"] + "items": ["rate", "amount", "discount_amount", "price_list_rate", "discount_percentage"], } for key, fieldname in print_hide_fields.items(): @@ -103,16 +107,19 @@ class DeliveryNote(SellingController): super(DeliveryNote, self).before_print(settings) def set_actual_qty(self): - for d in self.get('items'): + for d in self.get("items"): if d.item_code and d.warehouse: - actual_qty = frappe.db.sql("""select actual_qty from `tabBin` - where item_code = %s and warehouse = %s""", (d.item_code, d.warehouse)) + actual_qty = frappe.db.sql( + """select actual_qty from `tabBin` + where item_code = %s and warehouse = %s""", + (d.item_code, d.warehouse), + ) d.actual_qty = actual_qty and flt(actual_qty[0][0]) or 0 def so_required(self): """check in manage account if sales order required or not""" - if frappe.db.get_value("Selling Settings", None, 'so_required') == 'Yes': - for d in self.get('items'): + if frappe.db.get_value("Selling Settings", None, "so_required") == "Yes": + for d in self.get("items"): if not d.against_sales_order: frappe.throw(_("Sales Order required for Item {0}").format(d.item_code)) @@ -129,71 +136,91 @@ class DeliveryNote(SellingController): self.validate_with_previous_doc() from erpnext.stock.doctype.packed_item.packed_item import make_packing_list + make_packing_list(self) - if self._action != 'submit' and not self.is_return: - set_batch_nos(self, 'warehouse', throw=True) - set_batch_nos(self, 'warehouse', throw=True, child_table="packed_items") + if self._action != "submit" and not self.is_return: + set_batch_nos(self, "warehouse", throw=True) + set_batch_nos(self, "warehouse", throw=True, child_table="packed_items") self.update_current_stock() - if not self.installation_status: self.installation_status = 'Not Installed' + if not self.installation_status: + self.installation_status = "Not Installed" self.reset_default_field_value("set_warehouse", "items", "warehouse") def validate_with_previous_doc(self): - super(DeliveryNote, self).validate_with_previous_doc({ - "Sales Order": { - "ref_dn_field": "against_sales_order", - "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]] - }, - "Sales Order Item": { - "ref_dn_field": "so_detail", - "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]], - "is_child_table": True, - "allow_duplicate_prev_row_id": True - }, - "Sales Invoice": { - "ref_dn_field": "against_sales_invoice", - "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]] - }, - "Sales Invoice Item": { - "ref_dn_field": "si_detail", - "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]], - "is_child_table": True, - "allow_duplicate_prev_row_id": True - }, - }) + super(DeliveryNote, self).validate_with_previous_doc( + { + "Sales Order": { + "ref_dn_field": "against_sales_order", + "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]], + }, + "Sales Order Item": { + "ref_dn_field": "so_detail", + "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]], + "is_child_table": True, + "allow_duplicate_prev_row_id": True, + }, + "Sales Invoice": { + "ref_dn_field": "against_sales_invoice", + "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]], + }, + "Sales Invoice Item": { + "ref_dn_field": "si_detail", + "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]], + "is_child_table": True, + "allow_duplicate_prev_row_id": True, + }, + } + ) - if cint(frappe.db.get_single_value('Selling Settings', 'maintain_same_sales_rate')) \ - and not self.is_return: - self.validate_rate_with_reference_doc([["Sales Order", "against_sales_order", "so_detail"], - ["Sales Invoice", "against_sales_invoice", "si_detail"]]) + if ( + cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")) + and not self.is_return + ): + self.validate_rate_with_reference_doc( + [ + ["Sales Order", "against_sales_order", "so_detail"], + ["Sales Invoice", "against_sales_invoice", "si_detail"], + ] + ) def validate_proj_cust(self): """check for does customer belong to same project as entered..""" if self.project and self.customer: - res = frappe.db.sql("""select name from `tabProject` + res = frappe.db.sql( + """select name from `tabProject` where name = %s and (customer = %s or - ifnull(customer,'')='')""", (self.project, self.customer)) + ifnull(customer,'')='')""", + (self.project, self.customer), + ) if not res: - frappe.throw(_("Customer {0} does not belong to project {1}").format(self.customer, self.project)) + frappe.throw( + _("Customer {0} does not belong to project {1}").format(self.customer, self.project) + ) def validate_warehouse(self): super(DeliveryNote, self).validate_warehouse() for d in self.get_item_list(): - if not d['warehouse'] and frappe.db.get_value("Item", d['item_code'], "is_stock_item") == 1: + if not d["warehouse"] and frappe.db.get_value("Item", d["item_code"], "is_stock_item") == 1: frappe.throw(_("Warehouse required for stock Item {0}").format(d["item_code"])) def update_current_stock(self): if self.get("_action") and self._action != "update_after_submit": - for d in self.get('items'): - d.actual_qty = frappe.db.get_value("Bin", {"item_code": d.item_code, - "warehouse": d.warehouse}, "actual_qty") + for d in self.get("items"): + d.actual_qty = frappe.db.get_value( + "Bin", {"item_code": d.item_code, "warehouse": d.warehouse}, "actual_qty" + ) - for d in self.get('packed_items'): - bin_qty = frappe.db.get_value("Bin", {"item_code": d.item_code, - "warehouse": d.warehouse}, ["actual_qty", "projected_qty"], as_dict=True) + for d in self.get("packed_items"): + bin_qty = frappe.db.get_value( + "Bin", + {"item_code": d.item_code, "warehouse": d.warehouse}, + ["actual_qty", "projected_qty"], + as_dict=True, + ) if bin_qty: d.actual_qty = flt(bin_qty.actual_qty) d.projected_qty = flt(bin_qty.projected_qty) @@ -202,7 +229,9 @@ class DeliveryNote(SellingController): self.validate_packed_qty() # Check for Approving Authority - frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, self.company, self.base_grand_total, self) + frappe.get_doc("Authorization Control").validate_approving_authority( + self.doctype, self.company, self.base_grand_total, self + ) # update delivered qty in sales order self.update_prevdoc_status() @@ -235,16 +264,20 @@ class DeliveryNote(SellingController): self.make_gl_entries_on_cancel() self.repost_future_sle_and_gle() - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') + 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 extra_amount = 0 validate_against_credit_limit = False - bypass_credit_limit_check_at_sales_order = cint(frappe.db.get_value("Customer Credit Limit", - filters={'parent': self.customer, 'parenttype': 'Customer', 'company': self.company}, - fieldname="bypass_credit_limit_check")) + bypass_credit_limit_check_at_sales_order = cint( + frappe.db.get_value( + "Customer Credit Limit", + filters={"parent": self.customer, "parenttype": "Customer", "company": self.company}, + fieldname="bypass_credit_limit_check", + ) + ) if bypass_credit_limit_check_at_sales_order: validate_against_credit_limit = True @@ -256,48 +289,58 @@ class DeliveryNote(SellingController): break if validate_against_credit_limit: - check_credit_limit(self.customer, self.company, - bypass_credit_limit_check_at_sales_order, extra_amount) + check_credit_limit( + self.customer, self.company, bypass_credit_limit_check_at_sales_order, extra_amount + ) def validate_packed_qty(self): """ - Validate that if packed qty exists, it should be equal to qty + Validate that if packed qty exists, it should be equal to qty """ - if not any(flt(d.get('packed_qty')) for d in self.get("items")): + if not any(flt(d.get("packed_qty")) for d in self.get("items")): return has_error = False for d in self.get("items"): - if flt(d.get('qty')) != flt(d.get('packed_qty')): - frappe.msgprint(_("Packed quantity must equal quantity for Item {0} in row {1}").format(d.item_code, d.idx)) + if flt(d.get("qty")) != flt(d.get("packed_qty")): + frappe.msgprint( + _("Packed quantity must equal quantity for Item {0} in row {1}").format(d.item_code, d.idx) + ) has_error = True if has_error: raise frappe.ValidationError def check_next_docstatus(self): - submit_rv = frappe.db.sql("""select t1.name + submit_rv = frappe.db.sql( + """select t1.name from `tabSales Invoice` t1,`tabSales Invoice Item` t2 where t1.name = t2.parent and t2.delivery_note = %s and t1.docstatus = 1""", - (self.name)) + (self.name), + ) if submit_rv: frappe.throw(_("Sales Invoice {0} has already been submitted").format(submit_rv[0][0])) - submit_in = frappe.db.sql("""select t1.name + submit_in = frappe.db.sql( + """select t1.name from `tabInstallation Note` t1, `tabInstallation Note Item` t2 where t1.name = t2.parent and t2.prevdoc_docname = %s and t1.docstatus = 1""", - (self.name)) + (self.name), + ) if submit_in: frappe.throw(_("Installation Note {0} has already been submitted").format(submit_in[0][0])) def cancel_packing_slips(self): """ - Cancel submitted packing slips related to this delivery note + Cancel submitted packing slips related to this delivery note """ - res = frappe.db.sql("""SELECT name FROM `tabPacking Slip` WHERE delivery_note = %s - AND docstatus = 1""", self.name) + res = frappe.db.sql( + """SELECT name FROM `tabPacking Slip` WHERE delivery_note = %s + AND docstatus = 1""", + self.name, + ) if res: for r in res: - ps = frappe.get_doc('Packing Slip', r[0]) + ps = frappe.get_doc("Packing Slip", r[0]) ps.cancel() frappe.msgprint(_("Packing Slip(s) cancelled")) @@ -310,7 +353,7 @@ class DeliveryNote(SellingController): updated_delivery_notes = [self.name] for d in self.get("items"): if d.si_detail and not d.so_detail: - d.db_set('billed_amt', d.amount, update_modified=update_modified) + d.db_set("billed_amt", d.amount, update_modified=update_modified) elif d.so_detail: updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified) @@ -327,11 +370,16 @@ class DeliveryNote(SellingController): return_invoice.save() return_invoice.submit() - credit_note_link = frappe.utils.get_link_to_form('Sales Invoice', return_invoice.name) + credit_note_link = frappe.utils.get_link_to_form("Sales Invoice", return_invoice.name) frappe.msgprint(_("Credit Note {0} has been created automatically").format(credit_note_link)) except Exception: - frappe.throw(_("Could not create Credit Note automatically, please uncheck 'Issue Credit Note' and submit again")) + frappe.throw( + _( + "Could not create Credit Note automatically, please uncheck 'Issue Credit Note' and submit again" + ) + ) + def update_billed_amount_based_on_so(so_detail, update_modified=True): from frappe.query_builder.functions import Sum @@ -340,25 +388,35 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True): si_item = frappe.qb.DocType("Sales Invoice Item").as_("si_item") sum_amount = Sum(si_item.amount).as_("amount") - billed_against_so = frappe.qb.from_(si_item).select(sum_amount).where( - (si_item.so_detail == so_detail) & - ((si_item.dn_detail.isnull()) | (si_item.dn_detail == '')) & - (si_item.docstatus == 1) - ).run() + billed_against_so = ( + frappe.qb.from_(si_item) + .select(sum_amount) + .where( + (si_item.so_detail == so_detail) + & ((si_item.dn_detail.isnull()) | (si_item.dn_detail == "")) + & (si_item.docstatus == 1) + ) + .run() + ) billed_against_so = billed_against_so and billed_against_so[0][0] or 0 # Get all Delivery Note Item rows against the Sales Order Item row dn = frappe.qb.DocType("Delivery Note").as_("dn") dn_item = frappe.qb.DocType("Delivery Note Item").as_("dn_item") - dn_details = frappe.qb.from_(dn).from_(dn_item).select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent).where( - (dn.name == dn_item.parent) & - (dn_item.so_detail == so_detail) & - (dn.docstatus == 1) & - (dn.is_return == 0) - ).orderby( - dn.posting_date, dn.posting_time, dn.name - ).run(as_dict=True) + dn_details = ( + frappe.qb.from_(dn) + .from_(dn_item) + .select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent) + .where( + (dn.name == dn_item.parent) + & (dn_item.so_detail == so_detail) + & (dn.docstatus == 1) + & (dn.is_return == 0) + ) + .orderby(dn.posting_date, dn.posting_time, dn.name) + .run(as_dict=True) + ) updated_dn = [] for dnd in dn_details: @@ -370,8 +428,11 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True): billed_against_so -= billed_amt_agianst_dn else: # Get billed amount directly against Delivery Note - billed_amt_agianst_dn = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item` - where dn_detail=%s and docstatus=1""", dnd.name) + billed_amt_agianst_dn = frappe.db.sql( + """select sum(amount) from `tabSales Invoice Item` + where dn_detail=%s and docstatus=1""", + dnd.name, + ) billed_amt_agianst_dn = billed_amt_agianst_dn and billed_amt_agianst_dn[0][0] or 0 # Distribute billed amount directly against SO between DNs based on FIFO @@ -384,50 +445,71 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True): billed_amt_agianst_dn += billed_against_so billed_against_so = 0 - frappe.db.set_value("Delivery Note Item", dnd.name, "billed_amt", billed_amt_agianst_dn, update_modified=update_modified) + frappe.db.set_value( + "Delivery Note Item", + dnd.name, + "billed_amt", + billed_amt_agianst_dn, + update_modified=update_modified, + ) updated_dn.append(dnd.parent) return updated_dn + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context + list_context = get_list_context(context) - list_context.update({ - 'show_sidebar': True, - 'show_search': True, - 'no_breadcrumbs': True, - 'title': _('Shipments'), - }) + list_context.update( + { + "show_sidebar": True, + "show_search": True, + "no_breadcrumbs": True, + "title": _("Shipments"), + } + ) return list_context + def get_invoiced_qty_map(delivery_note): """returns a map: {dn_detail: invoiced_qty}""" invoiced_qty_map = {} - for dn_detail, qty in frappe.db.sql("""select dn_detail, qty from `tabSales Invoice Item` - where delivery_note=%s and docstatus=1""", delivery_note): - if not invoiced_qty_map.get(dn_detail): - invoiced_qty_map[dn_detail] = 0 - invoiced_qty_map[dn_detail] += qty + for dn_detail, qty in frappe.db.sql( + """select dn_detail, qty from `tabSales Invoice Item` + where delivery_note=%s and docstatus=1""", + delivery_note, + ): + if not invoiced_qty_map.get(dn_detail): + invoiced_qty_map[dn_detail] = 0 + invoiced_qty_map[dn_detail] += qty return invoiced_qty_map + def get_returned_qty_map(delivery_note): """returns a map: {so_detail: returned_qty}""" - returned_qty_map = frappe._dict(frappe.db.sql("""select dn_item.dn_detail, abs(dn_item.qty) as qty + returned_qty_map = frappe._dict( + frappe.db.sql( + """select dn_item.dn_detail, abs(dn_item.qty) as qty from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn where dn.name = dn_item.parent and dn.docstatus = 1 and dn.is_return = 1 and dn.return_against = %s - """, delivery_note)) + """, + delivery_note, + ) + ) return returned_qty_map + @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None): - doc = frappe.get_doc('Delivery Note', source_name) + doc = frappe.get_doc("Delivery Note", source_name) to_make_invoice_qty_map = {} returned_qty_map = get_returned_qty_map(source_name) @@ -444,20 +526,21 @@ def make_sales_invoice(source_name, target_doc=None): # set company address if source.company_address: - target.update({'company_address': source.company_address}) + target.update({"company_address": source.company_address}) else: # set company address target.update(get_company_address(target.company)) if target.company_address: - target.update(get_fetch_values("Sales Invoice", 'company_address', target.company_address)) + target.update(get_fetch_values("Sales Invoice", "company_address", target.company_address)) def update_item(source_doc, target_doc, source_parent): target_doc.qty = to_make_invoice_qty_map[source_doc.name] if source_doc.serial_no and source_parent.per_billed > 0 and not source_parent.is_return: - target_doc.serial_no = get_delivery_note_serial_no(source_doc.item_code, - target_doc.qty, source_parent.name) + target_doc.serial_no = get_delivery_note_serial_no( + source_doc.item_code, target_doc.qty, source_parent.name + ) def get_pending_qty(item_row): pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0) @@ -479,50 +562,52 @@ def make_sales_invoice(source_name, target_doc=None): return pending_qty - doc = get_mapped_doc("Delivery Note", source_name, { - "Delivery Note": { - "doctype": "Sales Invoice", - "field_map": { - "is_return": "is_return" + doc = get_mapped_doc( + "Delivery Note", + source_name, + { + "Delivery Note": { + "doctype": "Sales Invoice", + "field_map": {"is_return": "is_return"}, + "validation": {"docstatus": ["=", 1]}, }, - "validation": { - "docstatus": ["=", 1] - } - }, - "Delivery Note Item": { - "doctype": "Sales Invoice Item", - "field_map": { - "name": "dn_detail", - "parent": "delivery_note", - "so_detail": "so_detail", - "against_sales_order": "sales_order", - "serial_no": "serial_no", - "cost_center": "cost_center" + "Delivery Note Item": { + "doctype": "Sales Invoice Item", + "field_map": { + "name": "dn_detail", + "parent": "delivery_note", + "so_detail": "so_detail", + "against_sales_order": "sales_order", + "serial_no": "serial_no", + "cost_center": "cost_center", + }, + "postprocess": update_item, + "filter": lambda d: get_pending_qty(d) <= 0 + if not doc.get("is_return") + else get_pending_qty(d) > 0, }, - "postprocess": update_item, - "filter": lambda d: get_pending_qty(d) <= 0 if not doc.get("is_return") else get_pending_qty(d) > 0 - }, - "Sales Taxes and Charges": { - "doctype": "Sales Taxes and Charges", - "add_if_empty": True - }, - "Sales Team": { - "doctype": "Sales Team", - "field_map": { - "incentives": "incentives" + "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, + "Sales Team": { + "doctype": "Sales Team", + "field_map": {"incentives": "incentives"}, + "add_if_empty": True, }, - "add_if_empty": True - } - }, target_doc, set_missing_values) + }, + target_doc, + set_missing_values, + ) - automatically_fetch_payment_terms = cint(frappe.db.get_single_value('Accounts Settings', 'automatically_fetch_payment_terms')) + automatically_fetch_payment_terms = cint( + frappe.db.get_single_value("Accounts Settings", "automatically_fetch_payment_terms") + ) if automatically_fetch_payment_terms: doc.set_payment_schedule() - doc.set_onload('ignore_price_list', True) + doc.set_onload("ignore_price_list", True) return doc + @frappe.whitelist() def make_delivery_trip(source_name, target_doc=None): def update_stop_details(source_doc, target_doc, source_parent): @@ -538,107 +623,110 @@ def make_delivery_trip(source_name, target_doc=None): delivery_notes = [] - doclist = get_mapped_doc("Delivery Note", source_name, { - "Delivery Note": { - "doctype": "Delivery Trip", - "validation": { - "docstatus": ["=", 1] - } - }, - "Delivery Note Item": { - "doctype": "Delivery Stop", - "field_map": { - "parent": "delivery_note" + doclist = get_mapped_doc( + "Delivery Note", + source_name, + { + "Delivery Note": {"doctype": "Delivery Trip", "validation": {"docstatus": ["=", 1]}}, + "Delivery Note Item": { + "doctype": "Delivery Stop", + "field_map": {"parent": "delivery_note"}, + "condition": lambda item: item.parent not in delivery_notes, + "postprocess": update_stop_details, }, - "condition": lambda item: item.parent not in delivery_notes, - "postprocess": update_stop_details - } - }, target_doc) + }, + target_doc, + ) return doclist + @frappe.whitelist() def make_installation_note(source_name, target_doc=None): def update_item(obj, target, source_parent): target.qty = flt(obj.qty) - flt(obj.installed_qty) target.serial_no = obj.serial_no - doclist = get_mapped_doc("Delivery Note", source_name, { - "Delivery Note": { - "doctype": "Installation Note", - "validation": { - "docstatus": ["=", 1] - } - }, - "Delivery Note Item": { - "doctype": "Installation Note Item", - "field_map": { - "name": "prevdoc_detail_docname", - "parent": "prevdoc_docname", - "parenttype": "prevdoc_doctype", + doclist = get_mapped_doc( + "Delivery Note", + source_name, + { + "Delivery Note": {"doctype": "Installation Note", "validation": {"docstatus": ["=", 1]}}, + "Delivery Note Item": { + "doctype": "Installation Note Item", + "field_map": { + "name": "prevdoc_detail_docname", + "parent": "prevdoc_docname", + "parenttype": "prevdoc_doctype", + }, + "postprocess": update_item, + "condition": lambda doc: doc.installed_qty < doc.qty, }, - "postprocess": update_item, - "condition": lambda doc: doc.installed_qty < doc.qty - } - }, target_doc) + }, + target_doc, + ) return doclist + @frappe.whitelist() def make_packing_slip(source_name, target_doc=None): - doclist = get_mapped_doc("Delivery Note", source_name, { - "Delivery Note": { - "doctype": "Packing Slip", - "field_map": { - "name": "delivery_note", - "letter_head": "letter_head" + doclist = get_mapped_doc( + "Delivery Note", + source_name, + { + "Delivery Note": { + "doctype": "Packing Slip", + "field_map": {"name": "delivery_note", "letter_head": "letter_head"}, + "validation": {"docstatus": ["=", 0]}, + }, + "Delivery Note Item": { + "doctype": "Packing Slip Item", + "field_map": { + "item_code": "item_code", + "item_name": "item_name", + "description": "description", + "qty": "qty", + }, }, - "validation": { - "docstatus": ["=", 0] - } }, - - "Delivery Note Item": { - "doctype": "Packing Slip Item", - "field_map": { - "item_code": "item_code", - "item_name": "item_name", - "description": "description", - "qty": "qty", - } - } - - }, target_doc) + target_doc, + ) return doclist + @frappe.whitelist() def make_shipment(source_name, target_doc=None): def postprocess(source, target): - user = frappe.db.get_value("User", frappe.session.user, ['email', 'full_name', 'phone', 'mobile_no'], as_dict=1) + user = frappe.db.get_value( + "User", frappe.session.user, ["email", "full_name", "phone", "mobile_no"], as_dict=1 + ) target.pickup_contact_email = user.email - pickup_contact_display = '{}'.format(user.full_name) + pickup_contact_display = "{}".format(user.full_name) if user: if user.email: - pickup_contact_display += '
    ' + user.email + pickup_contact_display += "
    " + user.email if user.phone: - pickup_contact_display += '
    ' + user.phone + pickup_contact_display += "
    " + user.phone if user.mobile_no and not user.phone: - pickup_contact_display += '
    ' + user.mobile_no + 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) + 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: if contact.email_id: - delivery_contact_display += '
    ' + contact.email_id + delivery_contact_display += "
    " + contact.email_id if contact.phone: - delivery_contact_display += '
    ' + contact.phone + delivery_contact_display += "
    " + contact.phone if contact.mobile_no and not contact.phone: - delivery_contact_display += '
    ' + contact.mobile_no + delivery_contact_display += "
    " + contact.mobile_no target.delivery_contact = delivery_contact_display if source.shipping_address_name: @@ -648,38 +736,44 @@ def make_shipment(source_name, target_doc=None): 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", - "field_map": { - "grand_total": "value_of_goods", - "company": "pickup_company", - "company_address": "pickup_address_name", - "company_address_display": "pickup_address", - "customer": "delivery_customer", - "contact_person": "delivery_contact_name", - "contact_email": "delivery_contact_email" + doclist = get_mapped_doc( + "Delivery Note", + source_name, + { + "Delivery Note": { + "doctype": "Shipment", + "field_map": { + "grand_total": "value_of_goods", + "company": "pickup_company", + "company_address": "pickup_address_name", + "company_address_display": "pickup_address", + "customer": "delivery_customer", + "contact_person": "delivery_contact_name", + "contact_email": "delivery_contact_email", + }, + "validation": {"docstatus": ["=", 1]}, + }, + "Delivery Note Item": { + "doctype": "Shipment Delivery Note", + "field_map": { + "name": "prevdoc_detail_docname", + "parent": "prevdoc_docname", + "parenttype": "prevdoc_doctype", + "base_amount": "grand_total", + }, }, - "validation": { - "docstatus": ["=", 1] - } }, - "Delivery Note Item": { - "doctype": "Shipment Delivery Note", - "field_map": { - "name": "prevdoc_detail_docname", - "parent": "prevdoc_docname", - "parenttype": "prevdoc_doctype", - "base_amount": "grand_total" - } - } - }, target_doc, postprocess) + target_doc, + postprocess, + ) return doclist + @frappe.whitelist() def make_sales_return(source_name, target_doc=None): from erpnext.controllers.sales_and_purchase_return import make_return_doc + return make_return_doc("Delivery Note", source_name, target_doc) @@ -688,10 +782,12 @@ def update_delivery_note_status(docname, status): dn = frappe.get_doc("Delivery Note", docname) dn.update_status(status) + @frappe.whitelist() def make_inter_company_purchase_receipt(source_name, target_doc=None): return make_inter_company_transaction("Delivery Note", source_name, target_doc) + def make_inter_company_transaction(doctype, source_name, target_doc=None): from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( get_inter_company_details, @@ -701,16 +797,16 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): validate_inter_company_transaction, ) - if doctype == 'Delivery Note': + if doctype == "Delivery Note": source_doc = frappe.get_doc(doctype, source_name) target_doctype = "Purchase Receipt" - source_document_warehouse_field = 'target_warehouse' - target_document_warehouse_field = 'from_warehouse' + source_document_warehouse_field = "target_warehouse" + target_document_warehouse_field = "from_warehouse" else: source_doc = frappe.get_doc(doctype, source_name) - target_doctype = 'Delivery Note' - source_document_warehouse_field = 'from_warehouse' - target_document_warehouse_field = 'target_warehouse' + target_doctype = "Delivery Note" + source_document_warehouse_field = "from_warehouse" + target_document_warehouse_field = "target_warehouse" validate_inter_company_transaction(source_doc, doctype) details = get_inter_company_details(source_doc, doctype) @@ -719,18 +815,18 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): target.run_method("set_missing_values") set_purchase_references(target) - if target.doctype == 'Purchase Receipt': - master_doctype = 'Purchase Taxes and Charges Template' + if target.doctype == "Purchase Receipt": + master_doctype = "Purchase Taxes and Charges Template" else: - master_doctype = 'Sales Taxes and Charges Template' + master_doctype = "Sales Taxes and Charges Template" - if not target.get('taxes') and target.get('taxes_and_charges'): - for tax in get_taxes_and_charges(master_doctype, target.get('taxes_and_charges')): - target.append('taxes', tax) + if not target.get("taxes") and target.get("taxes_and_charges"): + for tax in get_taxes_and_charges(master_doctype, target.get("taxes_and_charges")): + target.append("taxes", tax) def update_details(source_doc, target_doc, source_parent): target_doc.inter_company_invoice_reference = source_doc.name - if target_doc.doctype == 'Purchase Receipt': + if target_doc.doctype == "Purchase Receipt": target_doc.company = details.get("company") target_doc.supplier = details.get("party") target_doc.buying_price_list = source_doc.selling_price_list @@ -738,12 +834,20 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): target_doc.inter_company_reference = source_doc.name # Invert the address on target doc creation - update_address(target_doc, 'supplier_address', 'address_display', source_doc.company_address) - update_address(target_doc, 'shipping_address', 'shipping_address_display', source_doc.customer_address) + update_address(target_doc, "supplier_address", "address_display", source_doc.company_address) + update_address( + target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address + ) - update_taxes(target_doc, party=target_doc.supplier, party_type='Supplier', company=target_doc.company, - doctype=target_doc.doctype, party_address=target_doc.supplier_address, - company_address=target_doc.shipping_address) + update_taxes( + target_doc, + party=target_doc.supplier, + party_type="Supplier", + company=target_doc.company, + doctype=target_doc.doctype, + party_address=target_doc.supplier_address, + company_address=target_doc.shipping_address, + ) else: target_doc.company = details.get("company") target_doc.customer = details.get("party") @@ -753,39 +857,52 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): target_doc.inter_company_reference = source_doc.name # Invert the address on target doc creation - update_address(target_doc, 'company_address', 'company_address_display', source_doc.supplier_address) - update_address(target_doc, 'shipping_address_name', 'shipping_address', source_doc.shipping_address) - update_address(target_doc, 'customer_address', 'address_display', source_doc.shipping_address) + update_address( + target_doc, "company_address", "company_address_display", source_doc.supplier_address + ) + update_address( + target_doc, "shipping_address_name", "shipping_address", source_doc.shipping_address + ) + update_address(target_doc, "customer_address", "address_display", source_doc.shipping_address) - update_taxes(target_doc, party=target_doc.customer, party_type='Customer', company=target_doc.company, - doctype=target_doc.doctype, party_address=target_doc.customer_address, - company_address=target_doc.company_address, shipping_address_name=target_doc.shipping_address_name) + update_taxes( + target_doc, + party=target_doc.customer, + party_type="Customer", + company=target_doc.company, + doctype=target_doc.doctype, + party_address=target_doc.customer_address, + company_address=target_doc.company_address, + shipping_address_name=target_doc.shipping_address_name, + ) - doclist = get_mapped_doc(doctype, source_name, { - doctype: { - "doctype": target_doctype, - "postprocess": update_details, - "field_no_map": [ - "taxes_and_charges", - "set_warehouse" - ] - }, - doctype +" Item": { - "doctype": target_doctype + " Item", - "field_map": { - source_document_warehouse_field: target_document_warehouse_field, - 'name': 'delivery_note_item', - 'batch_no': 'batch_no', - 'serial_no': 'serial_no' + doclist = get_mapped_doc( + doctype, + source_name, + { + doctype: { + "doctype": target_doctype, + "postprocess": update_details, + "field_no_map": ["taxes_and_charges", "set_warehouse"], }, - "field_no_map": [ - "warehouse" - ] - } - - }, target_doc, set_missing_values) + doctype + + " Item": { + "doctype": target_doctype + " Item", + "field_map": { + source_document_warehouse_field: target_document_warehouse_field, + "name": "delivery_note_item", + "batch_no": "batch_no", + "serial_no": "serial_no", + }, + "field_no_map": ["warehouse"], + }, + }, + target_doc, + set_missing_values, + ) return doclist + def on_doctype_update(): frappe.db.add_index("Delivery Note", ["customer", "is_return", "return_against"]) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py index ca61a36928..fd44e9cee5 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py @@ -3,31 +3,19 @@ from frappe import _ def get_data(): return { - 'fieldname': 'delivery_note', - 'non_standard_fieldnames': { - 'Stock Entry': 'delivery_note_no', - 'Quality Inspection': 'reference_name', - 'Auto Repeat': 'reference_document', + "fieldname": "delivery_note", + "non_standard_fieldnames": { + "Stock Entry": "delivery_note_no", + "Quality Inspection": "reference_name", + "Auto Repeat": "reference_document", }, - 'internal_links': { - 'Sales Order': ['items', 'against_sales_order'], + "internal_links": { + "Sales Order": ["items", "against_sales_order"], }, - 'transactions': [ - { - 'label': _('Related'), - 'items': ['Sales Invoice', 'Packing Slip', 'Delivery Trip'] - }, - { - 'label': _('Reference'), - 'items': ['Sales Order', 'Shipment', 'Quality Inspection'] - }, - { - 'label': _('Returns'), - 'items': ['Stock Entry'] - }, - { - 'label': _('Subscription'), - 'items': ['Auto Repeat'] - }, - ] + "transactions": [ + {"label": _("Related"), "items": ["Sales Invoice", "Packing Slip", "Delivery Trip"]}, + {"label": _("Reference"), "items": ["Sales Order", "Shipment", "Quality Inspection"]}, + {"label": _("Returns"), "items": ["Stock Entry"]}, + {"label": _("Subscription"), "items": ["Auto Repeat"]}, + ], } diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 82f4e7dd29..b5a45578c6 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt - import json import frappe @@ -54,21 +53,28 @@ class TestDeliveryNote(FrappeTestCase): self.assertRaises(frappe.ValidationError, frappe.get_doc(si).insert) def test_delivery_note_no_gl_entry(self): - company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company') + company = frappe.db.get_value("Warehouse", "_Test Warehouse - _TC", "company") make_stock_entry(target="_Test Warehouse - _TC", qty=5, basic_rate=100) - stock_queue = json.loads(get_previous_sle({ - "item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC", - "posting_date": nowdate(), - "posting_time": nowtime() - }).stock_queue or "[]") + stock_queue = json.loads( + get_previous_sle( + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "posting_date": nowdate(), + "posting_time": nowtime(), + } + ).stock_queue + or "[]" + ) dn = create_delivery_note() - sle = frappe.get_doc("Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn.name}) + sle = frappe.get_doc( + "Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn.name} + ) - self.assertEqual(sle.stock_value_difference, flt(-1*stock_queue[0][1], 2)) + self.assertEqual(sle.stock_value_difference, flt(-1 * stock_queue[0][1], 2)) self.assertFalse(get_gl_entries("Delivery Note", dn.name)) @@ -123,24 +129,43 @@ class TestDeliveryNote(FrappeTestCase): # set_perpetual_inventory(0, company) def test_delivery_note_gl_entry_packing_item(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") make_stock_entry(item_code="_Test Item", target="Stores - TCP1", qty=10, basic_rate=100) - make_stock_entry(item_code="_Test Item Home Desktop 100", - target="Stores - TCP1", qty=10, basic_rate=100) + make_stock_entry( + item_code="_Test Item Home Desktop 100", target="Stores - TCP1", qty=10, basic_rate=100 + ) - stock_in_hand_account = get_inventory_account('_Test Company with perpetual inventory') + stock_in_hand_account = get_inventory_account("_Test Company with perpetual inventory") prev_bal = get_balance_on(stock_in_hand_account) - dn = create_delivery_note(item_code="_Test Product Bundle Item", company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1") + dn = create_delivery_note( + item_code="_Test Product Bundle Item", + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + ) - stock_value_diff_rm1 = abs(frappe.db.get_value("Stock Ledger Entry", - {"voucher_type": "Delivery Note", "voucher_no": dn.name, "item_code": "_Test Item"}, - "stock_value_difference")) + stock_value_diff_rm1 = abs( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn.name, "item_code": "_Test Item"}, + "stock_value_difference", + ) + ) - stock_value_diff_rm2 = abs(frappe.db.get_value("Stock Ledger Entry", - {"voucher_type": "Delivery Note", "voucher_no": dn.name, - "item_code": "_Test Item Home Desktop 100"}, "stock_value_difference")) + stock_value_diff_rm2 = abs( + frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Delivery Note", + "voucher_no": dn.name, + "item_code": "_Test Item Home Desktop 100", + }, + "stock_value_difference", + ) + ) stock_value_diff = stock_value_diff_rm1 + stock_value_diff_rm2 @@ -149,7 +174,7 @@ class TestDeliveryNote(FrappeTestCase): expected_values = { stock_in_hand_account: [0.0, stock_value_diff], - "Cost of Goods Sold - TCP1": [stock_value_diff, 0.0] + "Cost of Goods Sold - TCP1": [stock_value_diff, 0.0], } for i, gle in enumerate(gl_entries): self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account)) @@ -166,10 +191,7 @@ class TestDeliveryNote(FrappeTestCase): dn = create_delivery_note(item_code="_Test Serialized Item With Series", serial_no=serial_no) - self.check_serial_no_values(serial_no, { - "warehouse": "", - "delivery_document_no": dn.name - }) + self.check_serial_no_values(serial_no, {"warehouse": "", "delivery_document_no": dn.name}) si = make_sales_invoice(dn.name) si.insert(ignore_permissions=True) @@ -177,17 +199,18 @@ class TestDeliveryNote(FrappeTestCase): dn.cancel() - self.check_serial_no_values(serial_no, { - "warehouse": "_Test Warehouse - _TC", - "delivery_document_no": "" - }) + self.check_serial_no_values( + serial_no, {"warehouse": "_Test Warehouse - _TC", "delivery_document_no": ""} + ) def test_serialized_partial_sales_invoice(self): se = make_serialized_item() serial_no = get_serial_nos(se.get("items")[0].serial_no) - serial_no = '\n'.join(serial_no) + serial_no = "\n".join(serial_no) - dn = create_delivery_note(item_code="_Test Serialized Item With Series", qty=2, serial_no=serial_no) + dn = create_delivery_note( + item_code="_Test Serialized Item With Series", qty=2, serial_no=serial_no + ) si = make_sales_invoice(dn.name) si.items[0].qty = 1 @@ -200,15 +223,19 @@ class TestDeliveryNote(FrappeTestCase): def test_serialize_status(self): from frappe.model.naming import make_autoname - serial_no = frappe.get_doc({ - "doctype": "Serial No", - "item_code": "_Test Serialized Item With Series", - "serial_no": make_autoname("SR", "Serial No") - }) + + serial_no = frappe.get_doc( + { + "doctype": "Serial No", + "item_code": "_Test Serialized Item With Series", + "serial_no": make_autoname("SR", "Serial No"), + } + ) serial_no.save() - dn = create_delivery_note(item_code="_Test Serialized Item With Series", - serial_no=serial_no.name, do_not_submit=True) + dn = create_delivery_note( + item_code="_Test Serialized Item With Series", serial_no=serial_no.name, do_not_submit=True + ) self.assertRaises(SerialNoWarehouseError, dn.submit) @@ -218,26 +245,46 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(cstr(serial_no.get(field)), value) def test_sales_return_for_non_bundled_items_partial(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") make_stock_entry(item_code="_Test Item", target="Stores - TCP1", qty=50, basic_rate=100) actual_qty_0 = get_qty_after_transaction(warehouse="Stores - TCP1") - dn = create_delivery_note(qty=5, rate=500, warehouse="Stores - TCP1", company=company, - expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1") + dn = create_delivery_note( + qty=5, + rate=500, + warehouse="Stores - TCP1", + company=company, + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) actual_qty_1 = get_qty_after_transaction(warehouse="Stores - TCP1") self.assertEqual(actual_qty_0 - 5, actual_qty_1) # outgoing_rate - outgoing_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note", - "voucher_no": dn.name}, "stock_value_difference") / 5 + outgoing_rate = ( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn.name}, + "stock_value_difference", + ) + / 5 + ) # return entry - dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-2, rate=500, - company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", - cost_center="Main - TCP1", do_not_submit=1) + dn1 = create_delivery_note( + is_return=1, + return_against=dn.name, + qty=-2, + rate=500, + company=company, + warehouse="Stores - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + do_not_submit=1, + ) dn1.items[0].dn_detail = dn.items[0].name dn1.submit() @@ -245,15 +292,20 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(actual_qty_1 + 2, actual_qty_2) - incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + incoming_rate, stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn1.name}, - ["incoming_rate", "stock_value_difference"]) + ["incoming_rate", "stock_value_difference"], + ) self.assertEqual(flt(incoming_rate, 3), abs(flt(outgoing_rate, 3))) stock_in_hand_account = get_inventory_account(company, dn1.items[0].warehouse) - gle_warehouse_amount = frappe.db.get_value("GL Entry", {"voucher_type": "Delivery Note", - "voucher_no": dn1.name, "account": stock_in_hand_account}, "debit") + gle_warehouse_amount = frappe.db.get_value( + "GL Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn1.name, "account": stock_in_hand_account}, + "debit", + ) self.assertEqual(gle_warehouse_amount, stock_value_difference) @@ -267,6 +319,7 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(dn.per_returned, 40) from erpnext.controllers.sales_and_purchase_return import make_return_doc + return_dn_2 = make_return_doc("Delivery Note", dn.name) # Check if unreturned amount is mapped in 2nd return @@ -281,7 +334,7 @@ class TestDeliveryNote(FrappeTestCase): # DN should be completed on billing all unreturned amount self.assertEqual(dn.items[0].billed_amt, 1500) self.assertEqual(dn.per_billed, 100) - self.assertEqual(dn.status, 'Completed') + self.assertEqual(dn.status, "Completed") si.load_from_db() si.cancel() @@ -293,19 +346,35 @@ class TestDeliveryNote(FrappeTestCase): dn.cancel() def test_sales_return_for_non_bundled_items_full(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") - make_item("Box", {'is_stock_item': 1}) + make_item("Box", {"is_stock_item": 1}) make_stock_entry(item_code="Box", target="Stores - TCP1", qty=10, basic_rate=100) - dn = create_delivery_note(item_code="Box", qty=5, rate=500, warehouse="Stores - TCP1", company=company, - expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1") + dn = create_delivery_note( + item_code="Box", + qty=5, + rate=500, + warehouse="Stores - TCP1", + company=company, + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) - #return entry - dn1 = create_delivery_note(item_code="Box", is_return=1, return_against=dn.name, qty=-5, rate=500, - company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", - cost_center="Main - TCP1", do_not_submit=1) + # return entry + dn1 = create_delivery_note( + item_code="Box", + is_return=1, + return_against=dn.name, + qty=-5, + rate=500, + company=company, + warehouse="Stores - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + do_not_submit=1, + ) dn1.items[0].dn_detail = dn.items[0].name dn1.submit() @@ -317,93 +386,157 @@ class TestDeliveryNote(FrappeTestCase): # Check if Original DN updated self.assertEqual(dn.items[0].returned_qty, 5) self.assertEqual(dn.per_returned, 100) - self.assertEqual(dn.status, 'Return Issued') + self.assertEqual(dn.status, "Return Issued") def test_return_single_item_from_bundled_items(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") - create_stock_reconciliation(item_code="_Test Item", - warehouse="Stores - TCP1", qty=50, rate=100, - company=company, expense_account = "Stock Adjustment - TCP1") - create_stock_reconciliation(item_code="_Test Item Home Desktop 100", - warehouse="Stores - TCP1", qty=50, rate=100, - company=company, expense_account = "Stock Adjustment - TCP1") + create_stock_reconciliation( + item_code="_Test Item", + warehouse="Stores - TCP1", + qty=50, + rate=100, + company=company, + expense_account="Stock Adjustment - TCP1", + ) + create_stock_reconciliation( + item_code="_Test Item Home Desktop 100", + warehouse="Stores - TCP1", + qty=50, + rate=100, + company=company, + expense_account="Stock Adjustment - TCP1", + ) - dn = create_delivery_note(item_code="_Test Product Bundle Item", qty=5, rate=500, - company=company, warehouse="Stores - TCP1", - expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1") + dn = create_delivery_note( + item_code="_Test Product Bundle Item", + qty=5, + rate=500, + company=company, + warehouse="Stores - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) # Qty after delivery actual_qty_1 = get_qty_after_transaction(warehouse="Stores - TCP1") - self.assertEqual(actual_qty_1, 25) + self.assertEqual(actual_qty_1, 25) # outgoing_rate - outgoing_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note", - "voucher_no": dn.name, "item_code": "_Test Item"}, "stock_value_difference") / 25 + outgoing_rate = ( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn.name, "item_code": "_Test Item"}, + "stock_value_difference", + ) + / 25 + ) # return 'test item' from packed items - dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-10, rate=500, - company=company, warehouse="Stores - TCP1", - expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1") + dn1 = create_delivery_note( + is_return=1, + return_against=dn.name, + qty=-10, + rate=500, + company=company, + warehouse="Stores - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) # qty after return actual_qty_2 = get_qty_after_transaction(warehouse="Stores - TCP1") self.assertEqual(actual_qty_2, 35) # Check incoming rate for return entry - incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + incoming_rate, stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn1.name}, - ["incoming_rate", "stock_value_difference"]) + ["incoming_rate", "stock_value_difference"], + ) self.assertEqual(flt(incoming_rate, 3), abs(flt(outgoing_rate, 3))) stock_in_hand_account = get_inventory_account(company, dn1.items[0].warehouse) # Check gl entry for warehouse - gle_warehouse_amount = frappe.db.get_value("GL Entry", {"voucher_type": "Delivery Note", - "voucher_no": dn1.name, "account": stock_in_hand_account}, "debit") + gle_warehouse_amount = frappe.db.get_value( + "GL Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn1.name, "account": stock_in_hand_account}, + "debit", + ) self.assertEqual(gle_warehouse_amount, stock_value_difference) - def test_return_entire_bundled_items(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") - create_stock_reconciliation(item_code="_Test Item", - warehouse="Stores - TCP1", qty=50, rate=100, - company=company, expense_account = "Stock Adjustment - TCP1") - create_stock_reconciliation(item_code="_Test Item Home Desktop 100", - warehouse="Stores - TCP1", qty=50, rate=100, - company=company, expense_account = "Stock Adjustment - TCP1") + create_stock_reconciliation( + item_code="_Test Item", + warehouse="Stores - TCP1", + qty=50, + rate=100, + company=company, + expense_account="Stock Adjustment - TCP1", + ) + create_stock_reconciliation( + item_code="_Test Item Home Desktop 100", + warehouse="Stores - TCP1", + qty=50, + rate=100, + company=company, + expense_account="Stock Adjustment - TCP1", + ) actual_qty = get_qty_after_transaction(warehouse="Stores - TCP1") self.assertEqual(actual_qty, 50) - dn = create_delivery_note(item_code="_Test Product Bundle Item", - qty=5, rate=500, company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1") + dn = create_delivery_note( + item_code="_Test Product Bundle Item", + qty=5, + rate=500, + company=company, + warehouse="Stores - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) # qty after return actual_qty = get_qty_after_transaction(warehouse="Stores - TCP1") self.assertEqual(actual_qty, 25) # return bundled item - dn1 = create_delivery_note(item_code='_Test Product Bundle Item', is_return=1, - return_against=dn.name, qty=-2, rate=500, company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1") + dn1 = create_delivery_note( + item_code="_Test Product Bundle Item", + is_return=1, + return_against=dn.name, + qty=-2, + rate=500, + company=company, + warehouse="Stores - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) # qty after return actual_qty = get_qty_after_transaction(warehouse="Stores - TCP1") self.assertEqual(actual_qty, 35) # Check incoming rate for return entry - incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + incoming_rate, stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn1.name}, - ["incoming_rate", "stock_value_difference"]) + ["incoming_rate", "stock_value_difference"], + ) self.assertEqual(incoming_rate, 100) - stock_in_hand_account = get_inventory_account('_Test Company', dn1.items[0].warehouse) + stock_in_hand_account = get_inventory_account("_Test Company", dn1.items[0].warehouse) # Check gl entry for warehouse - gle_warehouse_amount = frappe.db.get_value("GL Entry", {"voucher_type": "Delivery Note", - "voucher_no": dn1.name, "account": stock_in_hand_account}, "debit") + gle_warehouse_amount = frappe.db.get_value( + "GL Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn1.name, "account": stock_in_hand_account}, + "debit", + ) self.assertEqual(gle_warehouse_amount, 1400) @@ -411,69 +544,88 @@ class TestDeliveryNote(FrappeTestCase): se = make_serialized_item() serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] - dn = create_delivery_note(item_code="_Test Serialized Item With Series", rate=500, serial_no=serial_no) + dn = create_delivery_note( + item_code="_Test Serialized Item With Series", rate=500, serial_no=serial_no + ) - self.check_serial_no_values(serial_no, { - "warehouse": "", - "delivery_document_no": dn.name - }) + self.check_serial_no_values(serial_no, {"warehouse": "", "delivery_document_no": dn.name}) # return entry - dn1 = create_delivery_note(item_code="_Test Serialized Item With Series", - is_return=1, return_against=dn.name, qty=-1, rate=500, serial_no=serial_no) + dn1 = create_delivery_note( + item_code="_Test Serialized Item With Series", + is_return=1, + return_against=dn.name, + qty=-1, + rate=500, + serial_no=serial_no, + ) - self.check_serial_no_values(serial_no, { - "warehouse": "_Test Warehouse - _TC", - "delivery_document_no": "" - }) + self.check_serial_no_values( + serial_no, {"warehouse": "_Test Warehouse - _TC", "delivery_document_no": ""} + ) dn1.cancel() - self.check_serial_no_values(serial_no, { - "warehouse": "", - "delivery_document_no": dn.name - }) + self.check_serial_no_values(serial_no, {"warehouse": "", "delivery_document_no": dn.name}) dn.cancel() - self.check_serial_no_values(serial_no, { - "warehouse": "_Test Warehouse - _TC", - "delivery_document_no": "", - "purchase_document_no": se.name - }) + self.check_serial_no_values( + serial_no, + { + "warehouse": "_Test Warehouse - _TC", + "delivery_document_no": "", + "purchase_document_no": se.name, + }, + ) def test_delivery_of_bundled_items_to_target_warehouse(self): from erpnext.selling.doctype.customer.test_customer import create_internal_customer - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") customer_name = create_internal_customer( customer_name="_Test Internal Customer 2", represents_company="_Test Company with perpetual inventory", - allowed_to_interact_with="_Test Company with perpetual inventory" + allowed_to_interact_with="_Test Company with perpetual inventory", ) set_valuation_method("_Test Item", "FIFO") set_valuation_method("_Test Item Home Desktop 100", "FIFO") - target_warehouse = get_warehouse(company=company, abbr="TCP1", - warehouse_name="_Test Customer Warehouse").name + target_warehouse = get_warehouse( + company=company, abbr="TCP1", warehouse_name="_Test Customer Warehouse" + ).name for warehouse in ("Stores - TCP1", target_warehouse): - create_stock_reconciliation(item_code="_Test Item", warehouse=warehouse, company = company, - expense_account = "Stock Adjustment - TCP1", qty=500, rate=100) - create_stock_reconciliation(item_code="_Test Item Home Desktop 100", company = company, - expense_account = "Stock Adjustment - TCP1", warehouse=warehouse, qty=500, rate=100) + create_stock_reconciliation( + item_code="_Test Item", + warehouse=warehouse, + company=company, + expense_account="Stock Adjustment - TCP1", + qty=500, + rate=100, + ) + create_stock_reconciliation( + item_code="_Test Item Home Desktop 100", + company=company, + expense_account="Stock Adjustment - TCP1", + warehouse=warehouse, + qty=500, + rate=100, + ) dn = create_delivery_note( item_code="_Test Product Bundle Item", company="_Test Company with perpetual inventory", customer=customer_name, - cost_center = 'Main - TCP1', - expense_account = "Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", do_not_submit=True, - qty=5, rate=500, + qty=5, + rate=500, warehouse="Stores - TCP1", - target_warehouse=target_warehouse) + target_warehouse=target_warehouse, + ) dn.submit() @@ -485,16 +637,28 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(actual_qty_at_target, 525) # stock value diff for source warehouse for "_Test Item" - stock_value_difference = frappe.db.get_value("Stock Ledger Entry", - {"voucher_type": "Delivery Note", "voucher_no": dn.name, - "item_code": "_Test Item", "warehouse": "Stores - TCP1"}, - "stock_value_difference") + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Delivery Note", + "voucher_no": dn.name, + "item_code": "_Test Item", + "warehouse": "Stores - TCP1", + }, + "stock_value_difference", + ) # stock value diff for target warehouse - stock_value_difference1 = frappe.db.get_value("Stock Ledger Entry", - {"voucher_type": "Delivery Note", "voucher_no": dn.name, - "item_code": "_Test Item", "warehouse": target_warehouse}, - "stock_value_difference") + stock_value_difference1 = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Delivery Note", + "voucher_no": dn.name, + "item_code": "_Test Item", + "warehouse": target_warehouse, + }, + "stock_value_difference", + ) self.assertEqual(abs(stock_value_difference), stock_value_difference1) @@ -502,13 +666,18 @@ class TestDeliveryNote(FrappeTestCase): gl_entries = get_gl_entries("Delivery Note", dn.name) self.assertTrue(gl_entries) - stock_value_difference = abs(frappe.db.sql("""select sum(stock_value_difference) + stock_value_difference = abs( + frappe.db.sql( + """select sum(stock_value_difference) from `tabStock Ledger Entry` where voucher_type='Delivery Note' and voucher_no=%s - and warehouse='Stores - TCP1'""", dn.name)[0][0]) + and warehouse='Stores - TCP1'""", + dn.name, + )[0][0] + ) expected_values = { "Stock In Hand - TCP1": [0.0, stock_value_difference], - target_warehouse: [stock_value_difference, 0.0] + target_warehouse: [stock_value_difference, 0.0], } for i, gle in enumerate(gl_entries): self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account)) @@ -521,8 +690,13 @@ class TestDeliveryNote(FrappeTestCase): make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100) - dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', - cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1", do_not_submit=True) + dn = create_delivery_note( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + do_not_submit=True, + ) dn.submit() @@ -599,6 +773,7 @@ class TestDeliveryNote(FrappeTestCase): from erpnext.selling.doctype.sales_order.sales_order import ( make_sales_invoice as make_sales_invoice_from_so, ) + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) so = make_sales_order() @@ -672,28 +847,33 @@ class TestDeliveryNote(FrappeTestCase): def test_delivery_note_with_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" - create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company with perpetual inventory") - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + cost_center = "_Test Cost Center for BS Account - TCP1" + create_cost_center( + cost_center_name="_Test Cost Center for BS Account", + company="_Test Company with perpetual inventory", + ) + + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") set_valuation_method("_Test Item", "FIFO") make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100) - stock_in_hand_account = get_inventory_account('_Test Company with perpetual inventory') - dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', expense_account = "Cost of Goods Sold - TCP1", cost_center=cost_center) + stock_in_hand_account = get_inventory_account("_Test Company with perpetual inventory") + dn = create_delivery_note( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center=cost_center, + ) gl_entries = get_gl_entries("Delivery Note", dn.name) self.assertTrue(gl_entries) expected_values = { - "Cost of Goods Sold - TCP1": { - "cost_center": cost_center - }, - stock_in_hand_account: { - "cost_center": cost_center - } + "Cost of Goods Sold - TCP1": {"cost_center": cost_center}, + stock_in_hand_account: {"cost_center": cost_center}, } for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) @@ -701,29 +881,30 @@ class TestDeliveryNote(FrappeTestCase): def test_delivery_note_cost_center_with_balance_sheet_account(self): cost_center = "Main - TCP1" - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") set_valuation_method("_Test Item", "FIFO") make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100) - stock_in_hand_account = get_inventory_account('_Test Company with perpetual inventory') - dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1", - do_not_submit=1) + stock_in_hand_account = get_inventory_account("_Test Company with perpetual inventory") + dn = create_delivery_note( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + do_not_submit=1, + ) - dn.get('items')[0].cost_center = None + dn.get("items")[0].cost_center = None dn.submit() gl_entries = get_gl_entries("Delivery Note", dn.name) self.assertTrue(gl_entries) expected_values = { - "Cost of Goods Sold - TCP1": { - "cost_center": cost_center - }, - stock_in_hand_account: { - "cost_center": cost_center - } + "Cost of Goods Sold - TCP1": {"cost_center": cost_center}, + stock_in_hand_account: {"cost_center": cost_center}, } for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) @@ -751,15 +932,18 @@ class TestDeliveryNote(FrappeTestCase): from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice dn = create_delivery_note(qty=8, do_not_submit=True) - dn.append("items", { - "item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC", - "qty": 1, - "rate": 100, - "conversion_factor": 1.0, - "expense_account": "Cost of Goods Sold - _TC", - "cost_center": "_Test Cost Center - _TC" - }) + dn.append( + "items", + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "qty": 1, + "rate": 100, + "conversion_factor": 1.0, + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ) dn.submit() si1 = make_sales_invoice(dn.name) @@ -776,14 +960,21 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(si2.items[0].qty, 2) self.assertEqual(si2.items[1].qty, 1) - def test_delivery_note_bundle_with_batched_item(self): batched_bundle = make_item("_Test Batched bundle", {"is_stock_item": 0}) - batched_item = make_item("_Test Batched Item", - {"is_stock_item": 1, "has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "TESTBATCH.#####"} - ) + batched_item = make_item( + "_Test Batched Item", + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TESTBATCH.#####", + }, + ) make_product_bundle(parent=batched_bundle.name, items=[batched_item.name]) - make_stock_entry(item_code=batched_item.name, target="_Test Warehouse - _TC", qty=10, basic_rate=42) + make_stock_entry( + item_code=batched_item.name, target="_Test Warehouse - _TC", qty=10, basic_rate=42 + ) try: dn = create_delivery_note(item_code=batched_bundle.name, qty=1) @@ -792,7 +983,9 @@ class TestDeliveryNote(FrappeTestCase): self.fail("Batch numbers not getting added to bundled items in DN.") raise e - self.assertTrue("TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item") + self.assertTrue( + "TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item" + ) def test_payment_terms_are_fetched_when_creating_sales_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( @@ -804,13 +997,13 @@ class TestDeliveryNote(FrappeTestCase): so = make_sales_order(uom="Nos", do_not_save=1) create_payment_terms_template() - so.payment_terms_template = 'Test Receivable Template' + so.payment_terms_template = "Test Receivable Template" so.submit() dn = create_dn_against_so(so.name, delivered_qty=10) si = create_sales_invoice(qty=10, do_not_save=1) - si.items[0].delivery_note= dn.name + si.items[0].delivery_note = dn.name si.items[0].dn_detail = dn.items[0].name si.items[0].sales_order = so.name si.items[0].so_detail = so.items[0].name @@ -823,6 +1016,7 @@ class TestDeliveryNote(FrappeTestCase): automatically_fetch_payment_terms(enable=0) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") args = frappe._dict(args) @@ -836,18 +1030,21 @@ def create_delivery_note(**args): dn.is_return = args.is_return dn.return_against = args.return_against - dn.append("items", { - "item_code": args.item or args.item_code or "_Test Item", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty or 1, - "rate": args.rate if args.get("rate") is not None else 100, - "conversion_factor": 1.0, - "allow_zero_valuation_rate": args.allow_zero_valuation_rate or 1, - "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, - "target_warehouse": args.target_warehouse - }) + dn.append( + "items", + { + "item_code": args.item or args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": args.qty or 1, + "rate": args.rate if args.get("rate") is not None else 100, + "conversion_factor": 1.0, + "allow_zero_valuation_rate": args.allow_zero_valuation_rate or 1, + "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, + "target_warehouse": args.target_warehouse, + }, + ) if not args.do_not_save: dn.insert() @@ -855,4 +1052,5 @@ def create_delivery_note(**args): dn.submit() return dn + test_dependencies = ["Product Bundle"] diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py index c749b2e670..73b250db54 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py @@ -17,9 +17,12 @@ class DeliveryTrip(Document): super(DeliveryTrip, self).__init__(*args, **kwargs) # Google Maps returns distances in meters by default - self.default_distance_uom = frappe.db.get_single_value("Global Defaults", "default_distance_unit") or "Meter" - self.uom_conversion_factor = frappe.db.get_value("UOM Conversion Factor", - {"from_uom": "Meter", "to_uom": self.default_distance_uom}, "value") + self.default_distance_uom = ( + frappe.db.get_single_value("Global Defaults", "default_distance_unit") or "Meter" + ) + self.uom_conversion_factor = frappe.db.get_value( + "UOM Conversion Factor", {"from_uom": "Meter", "to_uom": self.default_distance_uom}, "value" + ) def validate(self): self.validate_stop_addresses() @@ -41,11 +44,7 @@ class DeliveryTrip(Document): stop.customer_address = get_address_display(frappe.get_doc("Address", stop.address).as_dict()) def update_status(self): - status = { - 0: "Draft", - 1: "Scheduled", - 2: "Cancelled" - }[self.docstatus] + status = {0: "Draft", 1: "Scheduled", 2: "Cancelled"}[self.docstatus] if self.docstatus == 1: visited_stops = [stop.visited for stop in self.delivery_stops] @@ -63,17 +62,19 @@ class DeliveryTrip(Document): are removed. Args: - delete (bool, optional): Defaults to `False`. `True` if driver details need to be emptied, else `False`. + delete (bool, optional): Defaults to `False`. `True` if driver details need to be emptied, else `False`. """ - delivery_notes = list(set(stop.delivery_note for stop in self.delivery_stops if stop.delivery_note)) + delivery_notes = list( + set(stop.delivery_note for stop in self.delivery_stops if stop.delivery_note) + ) update_fields = { "driver": self.driver, "driver_name": self.driver_name, "vehicle_no": self.vehicle, "lr_no": self.name, - "lr_date": self.departure_time + "lr_date": self.departure_time, } for delivery_note in delivery_notes: @@ -97,7 +98,7 @@ class DeliveryTrip(Document): on the optimized order, before estimating the arrival times. Args: - optimize (bool): True if route needs to be optimized, else False + optimize (bool): True if route needs to be optimized, else False """ departure_datetime = get_datetime(self.departure_time) @@ -134,8 +135,9 @@ class DeliveryTrip(Document): # Include last leg in the final distance calculation self.uom = self.default_distance_uom - total_distance = sum(leg.get("distance", {}).get("value", 0.0) - for leg in directions.get("legs")) # in meters + total_distance = sum( + leg.get("distance", {}).get("value", 0.0) for leg in directions.get("legs") + ) # in meters self.total_distance = total_distance * self.uom_conversion_factor else: idx += len(route) - 1 @@ -149,10 +151,10 @@ class DeliveryTrip(Document): split into sublists at the specified lock position(s). Args: - optimize (bool): `True` if route needs to be optimized, else `False` + optimize (bool): `True` if route needs to be optimized, else `False` Returns: - (list of list of str): List of address routes split at locks, if optimize is `True` + (list of list of str): List of address routes split at locks, if optimize is `True` """ if not self.driver_address: frappe.throw(_("Cannot Calculate Arrival Time as Driver Address is Missing.")) @@ -186,8 +188,8 @@ class DeliveryTrip(Document): for vehicle routing problems. Args: - optimized_order (list of int): The index-based optimized order of the route - start (int): The index at which to start the rearrangement + optimized_order (list of int): The index-based optimized order of the route + start (int): The index at which to start the rearrangement """ stops_order = [] @@ -200,7 +202,7 @@ class DeliveryTrip(Document): self.delivery_stops[old_idx].idx = new_idx stops_order.append(self.delivery_stops[old_idx]) - self.delivery_stops[start:start + len(stops_order)] = stops_order + self.delivery_stops[start : start + len(stops_order)] = stops_order def get_directions(self, route, optimize): """ @@ -212,11 +214,11 @@ class DeliveryTrip(Document): but it only works for routes without any waypoints. Args: - route (list of str): Route addresses (origin -> waypoint(s), if any -> destination) - optimize (bool): `True` if route needs to be optimized, else `False` + route (list of str): Route addresses (origin -> waypoint(s), if any -> destination) + optimize (bool): `True` if route needs to be optimized, else `False` Returns: - (dict): Route legs and, if `optimize` is `True`, optimized waypoint order + (dict): Route legs and, if `optimize` is `True`, optimized waypoint order """ if not frappe.db.get_single_value("Google Settings", "api_key"): frappe.throw(_("Enter API key in Google Settings.")) @@ -231,8 +233,8 @@ class DeliveryTrip(Document): directions_data = { "origin": route[0], "destination": route[-1], - "waypoints": route[1: -1], - "optimize_waypoints": optimize + "waypoints": route[1:-1], + "optimize_waypoints": optimize, } try: @@ -243,7 +245,6 @@ class DeliveryTrip(Document): return directions[0] if directions else False - @frappe.whitelist() def get_contact_and_address(name): out = frappe._dict() @@ -265,7 +266,10 @@ def get_default_contact(out, name): dl.link_doctype="Customer" AND dl.link_name=%s AND dl.parenttype = "Contact" - """, (name), as_dict=1) + """, + (name), + as_dict=1, + ) if contact_persons: for out.contact_person in contact_persons: @@ -288,7 +292,10 @@ def get_default_address(out, name): dl.link_doctype="Customer" AND dl.link_name=%s AND dl.parenttype = "Address" - """, (name), as_dict=1) + """, + (name), + as_dict=1, + ) if shipping_addresses: for out.shipping_address in shipping_addresses: @@ -303,16 +310,18 @@ def get_default_address(out, name): @frappe.whitelist() def get_contact_display(contact): contact_info = frappe.db.get_value( - "Contact", contact, - ["first_name", "last_name", "phone", "mobile_no"], - as_dict=1) + "Contact", contact, ["first_name", "last_name", "phone", "mobile_no"], as_dict=1 + ) - contact_info.html = """ %(first_name)s %(last_name)s
    %(phone)s
    %(mobile_no)s""" % { - "first_name": contact_info.first_name, - "last_name": contact_info.last_name or "", - "phone": contact_info.phone or "", - "mobile_no": contact_info.mobile_no or "" - } + contact_info.html = ( + """ %(first_name)s %(last_name)s
    %(phone)s
    %(mobile_no)s""" + % { + "first_name": contact_info.first_name, + "last_name": contact_info.last_name or "", + "phone": contact_info.phone or "", + "mobile_no": contact_info.mobile_no or "", + } + ) return contact_info.html @@ -322,19 +331,19 @@ def sanitize_address(address): Remove HTML breaks in a given address Args: - address (str): Address to be sanitized + address (str): Address to be sanitized Returns: - (str): Sanitized address + (str): Sanitized address """ if not address: return - address = address.split('
    ') + address = address.split("
    ") # Only get the first 3 blocks of the address - return ', '.join(address[:3]) + return ", ".join(address[:3]) @frappe.whitelist() @@ -349,11 +358,15 @@ def notify_customers(delivery_trip): email_recipients = [] for stop in delivery_trip.delivery_stops: - contact_info = frappe.db.get_value("Contact", stop.contact, ["first_name", "last_name", "email_id"], as_dict=1) + contact_info = frappe.db.get_value( + "Contact", stop.contact, ["first_name", "last_name", "email_id"], as_dict=1 + ) context.update({"items": []}) if stop.delivery_note: - items = frappe.get_all("Delivery Note Item", filters={"parent": stop.delivery_note, "docstatus": 1}, fields=["*"]) + items = frappe.get_all( + "Delivery Note Item", filters={"parent": stop.delivery_note, "docstatus": 1}, fields=["*"] + ) context.update({"items": items}) if contact_info and contact_info.email_id: @@ -363,10 +376,12 @@ def notify_customers(delivery_trip): dispatch_template_name = frappe.db.get_single_value("Delivery Settings", "dispatch_template") dispatch_template = frappe.get_doc("Email Template", dispatch_template_name) - frappe.sendmail(recipients=contact_info.email_id, + frappe.sendmail( + recipients=contact_info.email_id, subject=dispatch_template.subject, message=frappe.render_template(dispatch_template.response, context), - attachments=get_attachments(stop)) + attachments=get_attachments(stop), + ) stop.db_set("email_sent_to", contact_info.email_id) email_recipients.append(contact_info.email_id) @@ -379,29 +394,37 @@ def notify_customers(delivery_trip): def get_attachments(delivery_stop): - if not (frappe.db.get_single_value("Delivery Settings", "send_with_attachment") and delivery_stop.delivery_note): + if not ( + frappe.db.get_single_value("Delivery Settings", "send_with_attachment") + and delivery_stop.delivery_note + ): return [] dispatch_attachment = frappe.db.get_single_value("Delivery Settings", "dispatch_attachment") - attachments = frappe.attach_print("Delivery Note", delivery_stop.delivery_note, - file_name="Delivery Note", print_format=dispatch_attachment) + attachments = frappe.attach_print( + "Delivery Note", + delivery_stop.delivery_note, + file_name="Delivery Note", + print_format=dispatch_attachment, + ) return [attachments] + @frappe.whitelist() def get_driver_email(driver): employee = frappe.db.get_value("Driver", driver, "employee") email = frappe.db.get_value("Employee", employee, "prefered_email") return {"email": email} + @frappe.whitelist() def make_expense_claim(source_name, target_doc=None): - doc = get_mapped_doc("Delivery Trip", source_name, - {"Delivery Trip": { - "doctype": "Expense Claim", - "field_map": { - "name" : "delivery_trip" - } - }}, target_doc) + doc = get_mapped_doc( + "Delivery Trip", + source_name, + {"Delivery Trip": {"doctype": "Expense Claim", "field_map": {"name": "delivery_trip"}}}, + target_doc, + ) return doc diff --git a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py index dcdff4a0f1..555361afbc 100644 --- a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py @@ -106,23 +106,21 @@ class TestDeliveryTrip(FrappeTestCase): self.delivery_trip.save() self.assertEqual(self.delivery_trip.status, "Completed") + def create_address(driver): if not frappe.db.exists("Address", {"address_title": "_Test Address for Driver"}): - address = frappe.get_doc({ - "doctype": "Address", - "address_title": "_Test Address for Driver", - "address_type": "Office", - "address_line1": "Station Road", - "city": "_Test City", - "state": "Test State", - "country": "India", - "links":[ - { - "link_doctype": "Driver", - "link_name": driver.name - } - ] - }).insert(ignore_permissions=True) + address = frappe.get_doc( + { + "doctype": "Address", + "address_title": "_Test Address for Driver", + "address_type": "Office", + "address_line1": "Station Road", + "city": "_Test City", + "state": "Test State", + "country": "India", + "links": [{"link_doctype": "Driver", "link_name": driver.name}], + } + ).insert(ignore_permissions=True) frappe.db.set_value("Driver", driver.name, "address", address.name) @@ -130,49 +128,57 @@ def create_address(driver): return frappe.get_doc("Address", {"address_title": "_Test Address for Driver"}) + def create_driver(): if not frappe.db.exists("Driver", {"full_name": "Newton Scmander"}): - driver = frappe.get_doc({ - "doctype": "Driver", - "full_name": "Newton Scmander", - "cell_number": "98343424242", - "license_number": "B809", - }).insert(ignore_permissions=True) + driver = frappe.get_doc( + { + "doctype": "Driver", + "full_name": "Newton Scmander", + "cell_number": "98343424242", + "license_number": "B809", + } + ).insert(ignore_permissions=True) return driver return frappe.get_doc("Driver", {"full_name": "Newton Scmander"}) + def create_delivery_notification(): if not frappe.db.exists("Email Template", "Delivery Notification"): - dispatch_template = frappe.get_doc({ - 'doctype': 'Email Template', - 'name': 'Delivery Notification', - 'response': 'Test Delivery Trip', - 'subject': 'Test Subject', - 'owner': frappe.session.user - }) + dispatch_template = frappe.get_doc( + { + "doctype": "Email Template", + "name": "Delivery Notification", + "response": "Test Delivery Trip", + "subject": "Test Subject", + "owner": frappe.session.user, + } + ) dispatch_template.insert() delivery_settings = frappe.get_single("Delivery Settings") - delivery_settings.dispatch_template = 'Delivery Notification' + delivery_settings.dispatch_template = "Delivery Notification" delivery_settings.save() def create_vehicle(): if not frappe.db.exists("Vehicle", "JB 007"): - vehicle = frappe.get_doc({ - "doctype": "Vehicle", - "license_plate": "JB 007", - "make": "Maruti", - "model": "PCM", - "last_odometer": 5000, - "acquisition_date": nowdate(), - "location": "Mumbai", - "chassis_no": "1234ABCD", - "uom": "Litre", - "vehicle_value": flt(500000) - }) + vehicle = frappe.get_doc( + { + "doctype": "Vehicle", + "license_plate": "JB 007", + "make": "Maruti", + "model": "PCM", + "last_odometer": 5000, + "acquisition_date": nowdate(), + "location": "Mumbai", + "chassis_no": "1234ABCD", + "uom": "Litre", + "vehicle_value": flt(500000), + } + ) vehicle.insert() @@ -180,23 +186,27 @@ def create_delivery_trip(driver, address, contact=None): if not contact: contact = get_contact_and_address("_Test Customer") - delivery_trip = frappe.get_doc({ - "doctype": "Delivery Trip", - "company": erpnext.get_default_company(), - "departure_time": add_days(now_datetime(), 5), - "driver": driver.name, - "driver_address": address.name, - "vehicle": "JB 007", - "delivery_stops": [{ - "customer": "_Test Customer", - "address": contact.shipping_address.parent, - "contact": contact.contact_person.parent - }, + delivery_trip = frappe.get_doc( { - "customer": "_Test Customer", - "address": contact.shipping_address.parent, - "contact": contact.contact_person.parent - }] - }).insert(ignore_permissions=True) + "doctype": "Delivery Trip", + "company": erpnext.get_default_company(), + "departure_time": add_days(now_datetime(), 5), + "driver": driver.name, + "driver_address": address.name, + "vehicle": "JB 007", + "delivery_stops": [ + { + "customer": "_Test Customer", + "address": contact.shipping_address.parent, + "contact": contact.contact_person.parent, + }, + { + "customer": "_Test Customer", + "address": contact.shipping_address.parent, + "contact": contact.contact_person.parent, + }, + ], + } + ).insert(ignore_permissions=True) return delivery_trip diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 3abeecd742..9d7c22fc8e 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -44,13 +44,15 @@ class StockExistsForTemplate(frappe.ValidationError): class InvalidBarcode(frappe.ValidationError): pass + class DataValidationError(frappe.ValidationError): pass + class Item(Document): def onload(self): - self.set_onload('stock_exists', self.stock_ledger_created()) - self.set_onload('asset_naming_series', get_asset_naming_series()) + self.set_onload("stock_exists", self.stock_ledger_created()) + self.set_onload("asset_naming_series", get_asset_naming_series()) def autoname(self): if frappe.db.get_default("item_naming_by") == "Naming Series": @@ -60,6 +62,7 @@ class Item(Document): make_variant_item_code(self.variant_of, template_item_name, self) else: from frappe.model.naming import set_name_by_naming_series + set_name_by_naming_series(self) self.item_code = self.name @@ -70,9 +73,8 @@ class Item(Document): if not self.description: self.description = self.item_name - def after_insert(self): - '''set opening stock and item price''' + """set opening stock and item price""" if self.standard_rate: for default in self.item_defaults or [frappe._dict()]: self.add_price(default.default_price_list) @@ -128,8 +130,8 @@ class Item(Document): self.update_website_item() def validate_description(self): - '''Clean HTML description if set''' - if cint(frappe.db.get_single_value('Stock Settings', 'clean_description_html')): + """Clean HTML description if set""" + if cint(frappe.db.get_single_value("Stock Settings", "clean_description_html")): self.description = clean_html(self.description) def validate_customer_provided_part(self): @@ -141,24 +143,27 @@ class Item(Document): self.default_material_request_type = "Customer Provided" def add_price(self, price_list=None): - '''Add a new price''' + """Add a new price""" if not price_list: - price_list = (frappe.db.get_single_value('Selling Settings', 'selling_price_list') - or frappe.db.get_value('Price List', _('Standard Selling'))) + price_list = frappe.db.get_single_value( + "Selling Settings", "selling_price_list" + ) or frappe.db.get_value("Price List", _("Standard Selling")) if price_list: - item_price = frappe.get_doc({ - "doctype": "Item Price", - "price_list": price_list, - "item_code": self.name, - "uom": self.stock_uom, - "brand": self.brand, - "currency": erpnext.get_default_currency(), - "price_list_rate": self.standard_rate - }) + item_price = frappe.get_doc( + { + "doctype": "Item Price", + "price_list": price_list, + "item_code": self.name, + "uom": self.stock_uom, + "brand": self.brand, + "currency": erpnext.get_default_currency(), + "price_list_rate": self.standard_rate, + } + ) item_price.insert() def set_opening_stock(self): - '''set opening stock''' + """set opening stock""" if not self.is_stock_item or self.has_serial_no or self.has_batch_no: return @@ -171,19 +176,30 @@ class Item(Document): from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry # default warehouse, or Stores - for default in self.item_defaults or [frappe._dict({'company': frappe.defaults.get_defaults().company})]: - default_warehouse = (default.default_warehouse - or frappe.db.get_single_value('Stock Settings', 'default_warehouse')) + for default in self.item_defaults or [ + frappe._dict({"company": frappe.defaults.get_defaults().company}) + ]: + default_warehouse = default.default_warehouse or frappe.db.get_single_value( + "Stock Settings", "default_warehouse" + ) if default_warehouse: warehouse_company = frappe.db.get_value("Warehouse", default_warehouse, "company") if not default_warehouse or warehouse_company != default.company: - default_warehouse = frappe.db.get_value('Warehouse', - {'warehouse_name': _('Stores'), 'company': default.company}) + default_warehouse = frappe.db.get_value( + "Warehouse", {"warehouse_name": _("Stores"), "company": default.company} + ) if default_warehouse: - stock_entry = make_stock_entry(item_code=self.name, target=default_warehouse, qty=self.opening_stock, - rate=self.valuation_rate, company=default.company, posting_date=getdate(), posting_time=nowtime()) + stock_entry = make_stock_entry( + item_code=self.name, + target=default_warehouse, + qty=self.opening_stock, + rate=self.valuation_rate, + company=default.company, + posting_date=getdate(), + posting_time=nowtime(), + ) stock_entry.add_comment("Comment", _("Opening Stock")) @@ -201,14 +217,21 @@ class Item(Document): if not self.is_fixed_asset: asset = frappe.db.get_all("Asset", filters={"item_code": self.name, "docstatus": 1}, limit=1) if asset: - frappe.throw(_('"Is Fixed Asset" cannot be unchecked, as Asset record exists against the item')) + frappe.throw( + _('"Is Fixed Asset" cannot be unchecked, as Asset record exists against the item') + ) def validate_retain_sample(self): - if self.retain_sample and not frappe.db.get_single_value('Stock Settings', 'sample_retention_warehouse'): + if self.retain_sample and not frappe.db.get_single_value( + "Stock Settings", "sample_retention_warehouse" + ): frappe.throw(_("Please select Sample Retention Warehouse in Stock Settings first")) if self.retain_sample and not self.has_batch_no: - frappe.throw(_("{0} Retain Sample is based on batch, please check Has Batch No to retain sample of item").format( - self.item_code)) + frappe.throw( + _( + "{0} Retain Sample is based on batch, please check Has Batch No to retain sample of item" + ).format(self.item_code) + ) def clear_retain_sample(self): if not self.has_batch_no: @@ -228,10 +251,7 @@ class Item(Document): uoms_list = [d.uom for d in self.get("uoms")] if self.stock_uom not in uoms_list: - self.append("uoms", { - "uom": self.stock_uom, - "conversion_factor": 1 - }) + self.append("uoms", {"uom": self.stock_uom, "conversion_factor": 1}) def update_website_item(self): """Update Website Item if change in Item impacts it.""" @@ -239,8 +259,7 @@ class Item(Document): if web_item: changed = {} - editable_fields = ["item_name", "item_group", "stock_uom", "brand", "description", - "disabled"] + editable_fields = ["item_name", "item_group", "stock_uom", "brand", "description", "disabled"] doc_before_save = self.get_doc_before_save() for field in editable_fields: @@ -258,7 +277,7 @@ class Item(Document): web_item_doc.save() def validate_item_tax_net_rate_range(self): - for tax in self.get('taxes'): + for tax in self.get("taxes"): if flt(tax.maximum_net_rate) < flt(tax.minimum_net_rate): frappe.throw(_("Row #{0}: Maximum Net Rate cannot be greater than Minimum Net Rate")) @@ -273,23 +292,31 @@ class Item(Document): if not self.get("reorder_levels"): for d in template.get("reorder_levels"): n = {} - for k in ("warehouse", "warehouse_reorder_level", - "warehouse_reorder_qty", "material_request_type"): + for k in ( + "warehouse", + "warehouse_reorder_level", + "warehouse_reorder_qty", + "material_request_type", + ): n[k] = d.get(k) self.append("reorder_levels", n) def validate_conversion_factor(self): check_list = [] - for d in self.get('uoms'): + for d in self.get("uoms"): if cstr(d.uom) in check_list: frappe.throw( - _("Unit of Measure {0} has been entered more than once in Conversion Factor Table").format(d.uom)) + _("Unit of Measure {0} has been entered more than once in Conversion Factor Table").format( + d.uom + ) + ) else: check_list.append(cstr(d.uom)) if d.uom and cstr(d.uom) == cstr(self.stock_uom) and flt(d.conversion_factor) != 1: frappe.throw( - _("Conversion factor for default Unit of Measure must be 1 in row {0}").format(d.idx)) + _("Conversion factor for default Unit of Measure must be 1 in row {0}").format(d.idx) + ) def validate_item_type(self): if self.has_serial_no == 1 and self.is_stock_item == 0 and not self.is_fixed_asset: @@ -302,28 +329,32 @@ class Item(Document): for field in ["serial_no_series", "batch_number_series"]: series = self.get(field) if series and "#" in series and "." not in series: - frappe.throw(_("Invalid naming series (. missing) for {0}") - .format(frappe.bold(self.meta.get_field(field).label))) + frappe.throw( + _("Invalid naming series (. missing) for {0}").format( + frappe.bold(self.meta.get_field(field).label) + ) + ) def check_for_active_boms(self): if self.default_bom: bom_item = frappe.db.get_value("BOM", self.default_bom, "item") if bom_item not in (self.name, self.variant_of): frappe.throw( - _("Default BOM ({0}) must be active for this item or its template").format(bom_item)) + _("Default BOM ({0}) must be active for this item or its template").format(bom_item) + ) def fill_customer_code(self): """ - Append all the customer codes and insert into "customer_code" field of item table. - Used to search Item by customer code. + Append all the customer codes and insert into "customer_code" field of item table. + Used to search Item by customer code. """ customer_codes = set(d.ref_code for d in self.get("customer_items", [])) - self.customer_code = ','.join(customer_codes) + self.customer_code = ",".join(customer_codes) def check_item_tax(self): """Check whether Tax Rate is not entered twice for same Tax Type""" check_list = [] - for d in self.get('taxes'): + for d in self.get("taxes"): if d.item_tax_template: if d.item_tax_template in check_list: frappe.throw(_("{0} entered twice in Item Tax").format(d.item_tax_template)) @@ -332,24 +363,39 @@ class Item(Document): def validate_barcode(self): from stdnum import ean + if len(self.barcodes) > 0: for item_barcode in self.barcodes: - options = frappe.get_meta("Item Barcode").get_options("barcode_type").split('\n') + options = frappe.get_meta("Item Barcode").get_options("barcode_type").split("\n") if item_barcode.barcode: duplicate = frappe.db.sql( - """select parent from `tabItem Barcode` where barcode = %s and parent != %s""", (item_barcode.barcode, self.name)) + """select parent from `tabItem Barcode` where barcode = %s and parent != %s""", + (item_barcode.barcode, self.name), + ) if duplicate: - frappe.throw(_("Barcode {0} already used in Item {1}").format( - item_barcode.barcode, duplicate[0][0])) + frappe.throw( + _("Barcode {0} already used in Item {1}").format(item_barcode.barcode, duplicate[0][0]) + ) - item_barcode.barcode_type = "" if item_barcode.barcode_type not in options else item_barcode.barcode_type - if item_barcode.barcode_type and item_barcode.barcode_type.upper() in ('EAN', 'UPC-A', 'EAN-13', 'EAN-8'): + item_barcode.barcode_type = ( + "" if item_barcode.barcode_type not in options else item_barcode.barcode_type + ) + if item_barcode.barcode_type and item_barcode.barcode_type.upper() in ( + "EAN", + "UPC-A", + "EAN-13", + "EAN-8", + ): if not ean.is_valid(item_barcode.barcode): - frappe.throw(_("Barcode {0} is not a valid {1} code").format( - item_barcode.barcode, item_barcode.barcode_type), InvalidBarcode) + frappe.throw( + _("Barcode {0} is not a valid {1} code").format( + item_barcode.barcode, item_barcode.barcode_type + ), + InvalidBarcode, + ) def validate_warehouse_for_reorder(self): - '''Validate Reorder level table for duplicate and conditional mandatory''' + """Validate Reorder level table for duplicate and conditional mandatory""" warehouse = [] for d in self.get("reorder_levels"): if not d.warehouse_group: @@ -357,20 +403,30 @@ class Item(Document): if d.get("warehouse") and d.get("warehouse") not in warehouse: warehouse += [d.get("warehouse")] else: - frappe.throw(_("Row {0}: An Reorder entry already exists for this warehouse {1}") - .format(d.idx, d.warehouse), DuplicateReorderRows) + frappe.throw( + _("Row {0}: An Reorder entry already exists for this warehouse {1}").format( + d.idx, d.warehouse + ), + DuplicateReorderRows, + ) if d.warehouse_reorder_level and not d.warehouse_reorder_qty: frappe.throw(_("Row #{0}: Please set reorder quantity").format(d.idx)) def stock_ledger_created(self): - if not hasattr(self, '_stock_ledger_created'): - self._stock_ledger_created = len(frappe.db.sql("""select name from `tabStock Ledger Entry` - where item_code = %s and is_cancelled = 0 limit 1""", self.name)) + if not hasattr(self, "_stock_ledger_created"): + self._stock_ledger_created = len( + frappe.db.sql( + """select name from `tabStock Ledger Entry` + where item_code = %s and is_cancelled = 0 limit 1""", + self.name, + ) + ) return self._stock_ledger_created def update_item_price(self): - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabItem Price` SET item_name=%(item_name)s, @@ -382,8 +438,8 @@ class Item(Document): item_name=self.item_name, item_description=self.description, brand=self.brand, - item_code=self.name - ) + item_code=self.name, + ), ) def on_trash(self): @@ -405,8 +461,11 @@ class Item(Document): def after_rename(self, old_name, new_name, merge): if merge: self.validate_duplicate_item_in_stock_reconciliation(old_name, new_name) - frappe.msgprint(_("It can take upto few hours for accurate stock values to be visible after merging items."), - indicator="orange", title="Note") + frappe.msgprint( + _("It can take upto few hours for accurate stock values to be visible after merging items."), + indicator="orange", + title="Note", + ) if self.published_in_website: invalidate_cache_for_item(self) @@ -418,39 +477,54 @@ class Item(Document): self.recalculate_bin_qty(new_name) for dt in ("Sales Taxes and Charges", "Purchase Taxes and Charges"): - for d in frappe.db.sql("""select name, item_wise_tax_detail from `tab{0}` - where ifnull(item_wise_tax_detail, '') != ''""".format(dt), as_dict=1): + for d in frappe.db.sql( + """select name, item_wise_tax_detail from `tab{0}` + where ifnull(item_wise_tax_detail, '') != ''""".format( + dt + ), + as_dict=1, + ): item_wise_tax_detail = json.loads(d.item_wise_tax_detail) if isinstance(item_wise_tax_detail, dict) and old_name in item_wise_tax_detail: item_wise_tax_detail[new_name] = item_wise_tax_detail[old_name] item_wise_tax_detail.pop(old_name) - frappe.db.set_value(dt, d.name, "item_wise_tax_detail", - json.dumps(item_wise_tax_detail), update_modified=False) + frappe.db.set_value( + dt, d.name, "item_wise_tax_detail", json.dumps(item_wise_tax_detail), update_modified=False + ) def delete_old_bins(self, old_name): frappe.db.delete("Bin", {"item_code": old_name}) def validate_duplicate_item_in_stock_reconciliation(self, old_name, new_name): - records = frappe.db.sql(""" SELECT parent, COUNT(*) as records + records = frappe.db.sql( + """ SELECT parent, COUNT(*) as records FROM `tabStock Reconciliation Item` WHERE item_code = %s and docstatus = 1 GROUP By item_code, warehouse, parent HAVING records > 1 - """, new_name, as_dict=1) + """, + new_name, + as_dict=1, + ) - if not records: return + if not records: + return document = _("Stock Reconciliation") if len(records) == 1 else _("Stock Reconciliations") msg = _("The items {0} and {1} are present in the following {2} :").format( - frappe.bold(old_name), frappe.bold(new_name), document) + frappe.bold(old_name), frappe.bold(new_name), document + ) - msg += '
    ' - msg += ', '.join([get_link_to_form("Stock Reconciliation", d.parent) for d in records]) + "

    " + msg += "
    " + msg += ( + ", ".join([get_link_to_form("Stock Reconciliation", d.parent) for d in records]) + "

    " + ) - msg += _("Note: To merge the items, create a separate Stock Reconciliation for the old item {0}").format( - frappe.bold(old_name)) + msg += _( + "Note: To merge the items, create a separate Stock Reconciliation for the old item {0}" + ).format(frappe.bold(old_name)) frappe.throw(_(msg), title=_("Cannot Merge"), exc=DataValidationError) @@ -469,8 +543,8 @@ class Item(Document): def validate_duplicate_product_bundles_before_merge(self, old_name, new_name): "Block merge if both old and new items have product bundles." - old_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": old_name}) - new_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": new_name}) + old_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": old_name}) + new_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": new_name}) if old_bundle and new_bundle: bundle_link = get_link_to_form("Product Bundle", old_bundle) @@ -483,15 +557,14 @@ class Item(Document): def validate_duplicate_website_item_before_merge(self, old_name, new_name): """ - Block merge if both old and new items have website items against them. - This is to avoid duplicate website items after merging. + Block merge if both old and new items have website items against them. + This is to avoid duplicate website items after merging. """ web_items = frappe.get_all( "Website Item", - filters={ - "item_code": ["in", [old_name, new_name]] - }, - fields=["item_code", "name"]) + filters={"item_code": ["in", [old_name, new_name]]}, + fields=["item_code", "name"], + ) if len(web_items) <= 1: return @@ -509,11 +582,19 @@ class Item(Document): def recalculate_bin_qty(self, new_name): from erpnext.stock.stock_balance import repost_stock - existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") + + existing_allow_negative_stock = frappe.db.get_value( + "Stock Settings", None, "allow_negative_stock" + ) frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) - repost_stock_for_warehouses = frappe.get_all("Stock Ledger Entry", - "warehouse", filters={"item_code": new_name}, pluck="warehouse", distinct=True) + repost_stock_for_warehouses = frappe.get_all( + "Stock Ledger Entry", + "warehouse", + filters={"item_code": new_name}, + pluck="warehouse", + distinct=True, + ) # Delete all existing bins to avoid duplicate bins for the same item and warehouse frappe.db.delete("Bin", {"item_code": new_name}) @@ -521,30 +602,41 @@ class Item(Document): for warehouse in repost_stock_for_warehouses: repost_stock(new_name, warehouse) - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) + frappe.db.set_value( + "Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock + ) def update_bom_item_desc(self): if self.is_new(): return - if self.db_get('description') != self.description: - frappe.db.sql(""" + if self.db_get("description") != self.description: + frappe.db.sql( + """ update `tabBOM` set description = %s where item = %s and docstatus < 2 - """, (self.description, self.name)) + """, + (self.description, self.name), + ) - frappe.db.sql(""" + frappe.db.sql( + """ update `tabBOM Item` set description = %s where item_code = %s and docstatus < 2 - """, (self.description, self.name)) + """, + (self.description, self.name), + ) - frappe.db.sql(""" + frappe.db.sql( + """ update `tabBOM Explosion Item` set description = %s where item_code = %s and docstatus < 2 - """, (self.description, self.name)) + """, + (self.description, self.name), + ) def validate_item_defaults(self): companies = {row.company for row in self.item_defaults} @@ -554,41 +646,61 @@ class Item(Document): validate_item_default_company_links(self.item_defaults) - def update_defaults_from_item_group(self): """Get defaults from Item Group""" if self.item_defaults or not self.item_group: return - item_defaults = frappe.db.get_values("Item Default", {"parent": self.item_group}, - ['company', 'default_warehouse','default_price_list','buying_cost_center','default_supplier', - 'expense_account','selling_cost_center','income_account'], as_dict = 1) + item_defaults = frappe.db.get_values( + "Item Default", + {"parent": self.item_group}, + [ + "company", + "default_warehouse", + "default_price_list", + "buying_cost_center", + "default_supplier", + "expense_account", + "selling_cost_center", + "income_account", + ], + as_dict=1, + ) if item_defaults: for item in item_defaults: - self.append('item_defaults', { - 'company': item.company, - 'default_warehouse': item.default_warehouse, - 'default_price_list': item.default_price_list, - 'buying_cost_center': item.buying_cost_center, - 'default_supplier': item.default_supplier, - 'expense_account': item.expense_account, - 'selling_cost_center': item.selling_cost_center, - 'income_account': item.income_account - }) + self.append( + "item_defaults", + { + "company": item.company, + "default_warehouse": item.default_warehouse, + "default_price_list": item.default_price_list, + "buying_cost_center": item.buying_cost_center, + "default_supplier": item.default_supplier, + "expense_account": item.expense_account, + "selling_cost_center": item.selling_cost_center, + "income_account": item.income_account, + }, + ) else: defaults = frappe.defaults.get_defaults() or {} # To check default warehouse is belong to the default company - if defaults.get("default_warehouse") and defaults.company and frappe.db.exists("Warehouse", - {'name': defaults.default_warehouse, 'company': defaults.company}): - self.append("item_defaults", { - "company": defaults.get("company"), - "default_warehouse": defaults.default_warehouse - }) + if ( + defaults.get("default_warehouse") + and defaults.company + and frappe.db.exists( + "Warehouse", {"name": defaults.default_warehouse, "company": defaults.company} + ) + ): + self.append( + "item_defaults", + {"company": defaults.get("company"), "default_warehouse": defaults.default_warehouse}, + ) def update_variants(self): - if self.flags.dont_update_variants or \ - frappe.db.get_single_value('Item Variant Settings', 'do_not_update_variants'): + if self.flags.dont_update_variants or frappe.db.get_single_value( + "Item Variant Settings", "do_not_update_variants" + ): return if self.has_variants: variants = frappe.db.get_all("Item", fields=["item_code"], filters={"variant_of": self.name}) @@ -597,8 +709,13 @@ class Item(Document): update_variants(variants, self, publish_progress=False) frappe.msgprint(_("Item Variants updated")) else: - frappe.enqueue("erpnext.stock.doctype.item.item.update_variants", - variants=variants, template=self, now=frappe.flags.in_test, timeout=600) + frappe.enqueue( + "erpnext.stock.doctype.item.item.update_variants", + variants=variants, + template=self, + now=frappe.flags.in_test, + timeout=600, + ) def validate_has_variants(self): if not self.has_variants and frappe.db.get_value("Item", self.name, "has_variants"): @@ -630,11 +747,8 @@ class Item(Document): # fetch all attributes of these items item_attributes = frappe.get_all( "Item Variant Attribute", - filters={ - "parent": ["in", items], - "attribute": ["in", deleted_attribute] - }, - fields=["attribute", "parent"] + filters={"parent": ["in", items], "attribute": ["in", deleted_attribute]}, + fields=["attribute", "parent"], ) not_included = defaultdict(list) @@ -653,14 +767,18 @@ class Item(Document): return """ {0} {1} - """.format(title, body) + """.format( + title, body + ) - rows = '' + rows = "" for docname, attr_list in not_included.items(): link = "{0}".format(frappe.bold(_(docname))) rows += table_row(link, body(attr_list)) - error_description = _('The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template.') + error_description = _( + "The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template." + ) message = """
    {0}

    @@ -671,25 +789,37 @@ class Item(Document): {3} - """.format(error_description, _('Variant Items'), _('Attributes'), rows) + """.format( + error_description, _("Variant Items"), _("Attributes"), rows + ) frappe.throw(message, title=_("Variant Attribute Error"), is_minimizable=True, wide=True) - def validate_stock_exists_for_template_item(self): if self.stock_ledger_created() and self._doc_before_save: - if (cint(self._doc_before_save.has_variants) != cint(self.has_variants) - or self._doc_before_save.variant_of != self.variant_of): - frappe.throw(_("Cannot change Variant properties after stock transaction. You will have to make a new Item to do this.").format(self.name), - StockExistsForTemplate) + if ( + cint(self._doc_before_save.has_variants) != cint(self.has_variants) + or self._doc_before_save.variant_of != self.variant_of + ): + frappe.throw( + _( + "Cannot change Variant properties after stock transaction. You will have to make a new Item to do this." + ).format(self.name), + StockExistsForTemplate, + ) if self.has_variants or self.variant_of: - if not self.is_child_table_same('attributes'): + if not self.is_child_table_same("attributes"): frappe.throw( - _('Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item')) + _( + "Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item" + ) + ) def validate_variant_based_on_change(self): - if not self.is_new() and (self.variant_of or (self.has_variants and frappe.get_all("Item", {"variant_of": self.name}))): + if not self.is_new() and ( + self.variant_of or (self.has_variants and frappe.get_all("Item", {"variant_of": self.name})) + ): if self.variant_based_on != frappe.db.get_value("Item", self.name, "variant_based_on"): frappe.throw(_("Variant Based On cannot be changed")) @@ -702,8 +832,11 @@ class Item(Document): if self.variant_of: template_uom = frappe.db.get_value("Item", self.variant_of, "stock_uom") if template_uom != self.stock_uom: - frappe.throw(_("Default Unit of Measure for Variant '{0}' must be same as in Template '{1}'") - .format(self.stock_uom, template_uom)) + frappe.throw( + _("Default Unit of Measure for Variant '{0}' must be same as in Template '{1}'").format( + self.stock_uom, template_uom + ) + ) def validate_uom_conversion_factor(self): if self.uoms: @@ -717,21 +850,22 @@ class Item(Document): return if not self.variant_based_on: - self.variant_based_on = 'Item Attribute' + self.variant_based_on = "Item Attribute" - if self.variant_based_on == 'Item Attribute': + if self.variant_based_on == "Item Attribute": attributes = [] if not self.attributes: frappe.throw(_("Attribute table is mandatory")) for d in self.attributes: if d.attribute in attributes: frappe.throw( - _("Attribute {0} selected multiple times in Attributes Table").format(d.attribute)) + _("Attribute {0} selected multiple times in Attributes Table").format(d.attribute) + ) else: attributes.append(d.attribute) def validate_variant_attributes(self): - if self.is_new() and self.variant_of and self.variant_based_on == 'Item Attribute': + if self.is_new() and self.variant_of and self.variant_based_on == "Item Attribute": # remove attributes with no attribute_value set self.attributes = [d for d in self.attributes if cstr(d.attribute_value).strip()] @@ -742,8 +876,9 @@ class Item(Document): variant = get_variant(self.variant_of, args, self.name) if variant: - frappe.throw(_("Item variant {0} exists with same attributes") - .format(variant), ItemVariantExistsError) + frappe.throw( + _("Item variant {0} exists with same attributes").format(variant), ItemVariantExistsError + ) validate_item_variant_attributes(self, args) @@ -758,31 +893,52 @@ class Item(Document): fields = ("has_serial_no", "is_stock_item", "valuation_method", "has_batch_no") values = frappe.db.get_value("Item", self.name, fields, as_dict=True) - if not values.get('valuation_method') and self.get('valuation_method'): - values['valuation_method'] = frappe.db.get_single_value("Stock Settings", "valuation_method") or "FIFO" + if not values.get("valuation_method") and self.get("valuation_method"): + values["valuation_method"] = ( + frappe.db.get_single_value("Stock Settings", "valuation_method") or "FIFO" + ) if values: for field in fields: if cstr(self.get(field)) != cstr(values.get(field)): if self.check_if_linked_document_exists(field): - frappe.throw(_("As there are existing transactions against item {0}, you can not change the value of {1}").format(self.name, frappe.bold(self.meta.get_label(field)))) + frappe.throw( + _( + "As there are existing transactions against item {0}, you can not change the value of {1}" + ).format(self.name, frappe.bold(self.meta.get_label(field))) + ) def check_if_linked_document_exists(self, field): - linked_doctypes = ["Delivery Note Item", "Sales Invoice Item", "POS Invoice Item", "Purchase Receipt Item", - "Purchase Invoice Item", "Stock Entry Detail", "Stock Reconciliation Item"] + linked_doctypes = [ + "Delivery Note Item", + "Sales Invoice Item", + "POS Invoice Item", + "Purchase Receipt Item", + "Purchase Invoice Item", + "Stock Entry Detail", + "Stock Reconciliation Item", + ] # For "Is Stock Item", following doctypes is important # because reserved_qty, ordered_qty and requested_qty updated from these doctypes if field == "is_stock_item": - linked_doctypes += ["Sales Order Item", "Purchase Order Item", "Material Request Item", "Product Bundle"] + linked_doctypes += [ + "Sales Order Item", + "Purchase Order Item", + "Material Request Item", + "Product Bundle", + ] for doctype in linked_doctypes: - filters={"item_code": self.name, "docstatus": 1} + filters = {"item_code": self.name, "docstatus": 1} if doctype == "Product Bundle": - filters={"new_item_code": self.name} + filters = {"new_item_code": self.name} - if doctype in ("Purchase Invoice Item", "Sales Invoice Item",): + if doctype in ( + "Purchase Invoice Item", + "Sales Invoice Item", + ): # If Invoice has Stock impact, only then consider it. if self.stock_ledger_created(): return True @@ -792,37 +948,48 @@ class Item(Document): def validate_auto_reorder_enabled_in_stock_settings(self): if self.reorder_levels: - enabled = frappe.db.get_single_value('Stock Settings', 'auto_indent') + enabled = frappe.db.get_single_value("Stock Settings", "auto_indent") if not enabled: - frappe.msgprint(msg=_("You have to enable auto re-order in Stock Settings to maintain re-order levels."), title=_("Enable Auto Re-Order"), indicator="orange") + frappe.msgprint( + msg=_("You have to enable auto re-order in Stock Settings to maintain re-order levels."), + title=_("Enable Auto Re-Order"), + indicator="orange", + ) def make_item_price(item, price_list_name, item_price): - frappe.get_doc({ - 'doctype': 'Item Price', - 'price_list': price_list_name, - 'item_code': item, - 'price_list_rate': item_price - }).insert() + frappe.get_doc( + { + "doctype": "Item Price", + "price_list": price_list_name, + "item_code": item, + "price_list_rate": item_price, + } + ).insert() + def get_timeline_data(doctype, name): """get timeline data based on Stock Ledger Entry. This is displayed as heatmap on the item page.""" - items = frappe.db.sql("""select unix_timestamp(posting_date), count(*) + items = frappe.db.sql( + """select unix_timestamp(posting_date), count(*) from `tabStock Ledger Entry` where item_code=%s and posting_date > date_sub(curdate(), interval 1 year) - group by posting_date""", name) + group by posting_date""", + name, + ) return dict(items) - def validate_end_of_life(item_code, end_of_life=None, disabled=None): if (not end_of_life) or (disabled is None): end_of_life, disabled = frappe.db.get_value("Item", item_code, ["end_of_life", "disabled"]) if end_of_life and end_of_life != "0000-00-00" and getdate(end_of_life) <= now_datetime().date(): - frappe.throw(_("Item {0} has reached its end of life on {1}").format(item_code, formatdate(end_of_life))) + frappe.throw( + _("Item {0} has reached its end of life on {1}").format(item_code, formatdate(end_of_life)) + ) if disabled: frappe.throw(_("Item {0} is disabled").format(item_code)) @@ -843,11 +1010,13 @@ def validate_cancelled_item(item_code, docstatus=None): if docstatus == 2: frappe.throw(_("Item {0} is cancelled").format(item_code)) + def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): """returns last purchase details in stock uom""" # get last purchase order item details - last_purchase_order = frappe.db.sql("""\ + last_purchase_order = frappe.db.sql( + """\ select po.name, po.transaction_date, po.conversion_rate, po_item.conversion_factor, po_item.base_price_list_rate, po_item.discount_percentage, po_item.base_rate, po_item.base_net_rate @@ -855,11 +1024,14 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): where po.docstatus = 1 and po_item.item_code = %s and po.name != %s and po.name = po_item.parent order by po.transaction_date desc, po.name desc - limit 1""", (item_code, cstr(doc_name)), as_dict=1) - + limit 1""", + (item_code, cstr(doc_name)), + as_dict=1, + ) # get last purchase receipt item details - last_purchase_receipt = frappe.db.sql("""\ + last_purchase_receipt = frappe.db.sql( + """\ select pr.name, pr.posting_date, pr.posting_time, pr.conversion_rate, pr_item.conversion_factor, pr_item.base_price_list_rate, pr_item.discount_percentage, pr_item.base_rate, pr_item.base_net_rate @@ -867,20 +1039,29 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): where pr.docstatus = 1 and pr_item.item_code = %s and pr.name != %s and pr.name = pr_item.parent order by pr.posting_date desc, pr.posting_time desc, pr.name desc - limit 1""", (item_code, cstr(doc_name)), as_dict=1) + limit 1""", + (item_code, cstr(doc_name)), + as_dict=1, + ) - purchase_order_date = getdate(last_purchase_order and last_purchase_order[0].transaction_date - or "1900-01-01") - purchase_receipt_date = getdate(last_purchase_receipt and - last_purchase_receipt[0].posting_date or "1900-01-01") + purchase_order_date = getdate( + last_purchase_order and last_purchase_order[0].transaction_date or "1900-01-01" + ) + purchase_receipt_date = getdate( + last_purchase_receipt and last_purchase_receipt[0].posting_date or "1900-01-01" + ) - if last_purchase_order and (purchase_order_date >= purchase_receipt_date or not last_purchase_receipt): + if last_purchase_order and ( + purchase_order_date >= purchase_receipt_date or not last_purchase_receipt + ): # use purchase order last_purchase = last_purchase_order[0] purchase_date = purchase_order_date - elif last_purchase_receipt and (purchase_receipt_date > purchase_order_date or not last_purchase_order): + elif last_purchase_receipt and ( + purchase_receipt_date > purchase_order_date or not last_purchase_order + ): # use purchase receipt last_purchase = last_purchase_receipt[0] purchase_date = purchase_receipt_date @@ -889,22 +1070,25 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): return frappe._dict() conversion_factor = flt(last_purchase.conversion_factor) - out = frappe._dict({ - "base_price_list_rate": flt(last_purchase.base_price_list_rate) / conversion_factor, - "base_rate": flt(last_purchase.base_rate) / conversion_factor, - "base_net_rate": flt(last_purchase.base_net_rate) / conversion_factor, - "discount_percentage": flt(last_purchase.discount_percentage), - "purchase_date": purchase_date - }) - + out = frappe._dict( + { + "base_price_list_rate": flt(last_purchase.base_price_list_rate) / conversion_factor, + "base_rate": flt(last_purchase.base_rate) / conversion_factor, + "base_net_rate": flt(last_purchase.base_net_rate) / conversion_factor, + "discount_percentage": flt(last_purchase.discount_percentage), + "purchase_date": purchase_date, + } + ) conversion_rate = flt(conversion_rate) or 1.0 - out.update({ - "price_list_rate": out.base_price_list_rate / conversion_rate, - "rate": out.base_rate / conversion_rate, - "base_rate": out.base_rate, - "base_net_rate": out.base_net_rate - }) + out.update( + { + "price_list_rate": out.base_price_list_rate / conversion_rate, + "rate": out.base_rate / conversion_rate, + "base_rate": out.base_rate, + "base_net_rate": out.base_net_rate, + } + ) return out @@ -927,39 +1111,51 @@ def invalidate_item_variants_cache_for_website(doc): is_web_item = doc.get("published_in_website") or doc.get("published") if doc.has_variants and is_web_item: item_code = doc.item_code - elif doc.variant_of and frappe.db.get_value('Item', doc.variant_of, 'published_in_website'): + elif doc.variant_of and frappe.db.get_value("Item", doc.variant_of, "published_in_website"): item_code = doc.variant_of if item_code: item_cache = ItemVariantsCacheManager(item_code) item_cache.rebuild_cache() + def check_stock_uom_with_bin(item, stock_uom): if stock_uom == frappe.db.get_value("Item", item, "stock_uom"): return - ref_uom = frappe.db.get_value("Stock Ledger Entry", - {"item_code": item}, "stock_uom") + ref_uom = frappe.db.get_value("Stock Ledger Entry", {"item_code": item}, "stock_uom") if ref_uom: if cstr(ref_uom) != cstr(stock_uom): - frappe.throw(_("Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.").format(item)) + frappe.throw( + _( + "Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM." + ).format(item) + ) - bin_list = frappe.db.sql(""" + bin_list = frappe.db.sql( + """ select * from tabBin where item_code = %s and (reserved_qty > 0 or ordered_qty > 0 or indented_qty > 0 or planned_qty > 0) and stock_uom != %s - """, (item, stock_uom), as_dict=1) + """, + (item, stock_uom), + as_dict=1, + ) if bin_list: - frappe.throw(_("Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You need to either cancel the linked documents or create a new Item.").format(item)) + frappe.throw( + _( + "Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You need to either cancel the linked documents or create a new Item." + ).format(item) + ) # No SLE or documents against item. Bin UOM can be changed safely. frappe.db.sql("""update tabBin set stock_uom=%s where item_code=%s""", (stock_uom, item)) def get_item_defaults(item_code, company): - item = frappe.get_cached_doc('Item', item_code) + item = frappe.get_cached_doc("Item", item_code) out = item.as_dict() @@ -970,8 +1166,9 @@ def get_item_defaults(item_code, company): out.update(row) return out + def set_item_default(item_code, company, fieldname, value): - item = frappe.get_cached_doc('Item', item_code) + item = frappe.get_cached_doc("Item", item_code) for d in item.item_defaults: if d.company == company: @@ -980,10 +1177,11 @@ def set_item_default(item_code, company, fieldname, value): return # no row found, add a new row for the company - d = item.append('item_defaults', {fieldname: value, "company": company}) + d = item.append("item_defaults", {fieldname: value, "company": company}) d.db_insert() item.clear_cache() + @frappe.whitelist() def get_item_details(item_code, company=None): out = frappe._dict() @@ -995,30 +1193,36 @@ def get_item_details(item_code, company=None): return out + @frappe.whitelist() def get_uom_conv_factor(uom, stock_uom): - """ Get UOM conversion factor from uom to stock_uom - e.g. uom = "Kg", stock_uom = "Gram" then returns 1000.0 + """Get UOM conversion factor from uom to stock_uom + e.g. uom = "Kg", stock_uom = "Gram" then returns 1000.0 """ if uom == stock_uom: return 1.0 - from_uom, to_uom = uom, stock_uom # renaming for readability + from_uom, to_uom = uom, stock_uom # renaming for readability - exact_match = frappe.db.get_value("UOM Conversion Factor", {"to_uom": to_uom, "from_uom": from_uom}, ["value"], as_dict=1) + exact_match = frappe.db.get_value( + "UOM Conversion Factor", {"to_uom": to_uom, "from_uom": from_uom}, ["value"], as_dict=1 + ) if exact_match: return exact_match.value - inverse_match = frappe.db.get_value("UOM Conversion Factor", {"to_uom": from_uom, "from_uom": to_uom}, ["value"], as_dict=1) + inverse_match = frappe.db.get_value( + "UOM Conversion Factor", {"to_uom": from_uom, "from_uom": to_uom}, ["value"], as_dict=1 + ) if inverse_match: return 1 / inverse_match.value # This attempts to try and get conversion from intermediate UOM. # case: - # g -> mg = 1000 - # g -> kg = 0.001 + # g -> mg = 1000 + # g -> kg = 0.001 # therefore kg -> mg = 1000 / 0.001 = 1,000,000 - intermediate_match = frappe.db.sql(""" + intermediate_match = frappe.db.sql( + """ select (first.value / second.value) as value from `tabUOM Conversion Factor` first join `tabUOM Conversion Factor` second @@ -1027,7 +1231,10 @@ def get_uom_conv_factor(uom, stock_uom): first.to_uom = %(to_uom)s and second.to_uom = %(from_uom)s limit 1 - """, {"to_uom": to_uom, "from_uom": from_uom}, as_dict=1) + """, + {"to_uom": to_uom, "from_uom": from_uom}, + as_dict=1, + ) if intermediate_match: return intermediate_match[0].value @@ -1039,8 +1246,12 @@ def get_item_attribute(parent, attribute_value=""): if not frappe.has_permission("Item"): frappe.throw(_("No Permission")) - return frappe.get_all("Item Attribute Value", fields = ["attribute_value"], - filters = {'parent': parent, 'attribute_value': ("like", f"%{attribute_value}%")}) + return frappe.get_all( + "Item Attribute Value", + fields=["attribute_value"], + filters={"parent": parent, "attribute_value": ("like", f"%{attribute_value}%")}, + ) + def update_variants(variants, template, publish_progress=True): total = len(variants) @@ -1051,6 +1262,7 @@ def update_variants(variants, template, publish_progress=True): if publish_progress: frappe.publish_progress(count / total * 100, title=_("Updating Variants...")) + @erpnext.allow_regional def set_item_tax_from_hsn_code(item): pass @@ -1059,23 +1271,25 @@ def set_item_tax_from_hsn_code(item): def validate_item_default_company_links(item_defaults: List[ItemDefault]) -> None: for item_default in item_defaults: for doctype, field in [ - ['Warehouse', 'default_warehouse'], - ['Cost Center', 'buying_cost_center'], - ['Cost Center', 'selling_cost_center'], - ['Account', 'expense_account'], - ['Account', 'income_account'] + ["Warehouse", "default_warehouse"], + ["Cost Center", "buying_cost_center"], + ["Cost Center", "selling_cost_center"], + ["Account", "expense_account"], + ["Account", "income_account"], ]: if item_default.get(field): - company = frappe.db.get_value(doctype, item_default.get(field), 'company', cache=True) + company = frappe.db.get_value(doctype, item_default.get(field), "company", cache=True) if company and company != item_default.company: - frappe.throw(_("Row #{}: {} {} doesn't belong to Company {}. Please select valid {}.") - .format( + frappe.throw( + _("Row #{}: {} {} doesn't belong to Company {}. Please select valid {}.").format( item_default.idx, doctype, frappe.bold(item_default.get(field)), frappe.bold(item_default.company), - frappe.bold(frappe.unscrub(field)) - ), title=_("Invalid Item Defaults")) + frappe.bold(frappe.unscrub(field)), + ), + title=_("Invalid Item Defaults"), + ) @frappe.whitelist() @@ -1083,4 +1297,3 @@ def get_asset_naming_series(): from erpnext.assets.doctype.asset.asset import get_asset_naming_series return get_asset_naming_series() - diff --git a/erpnext/stock/doctype/item/item_dashboard.py b/erpnext/stock/doctype/item/item_dashboard.py index e16f5bbfeb..33acf4bfd8 100644 --- a/erpnext/stock/doctype/item/item_dashboard.py +++ b/erpnext/stock/doctype/item/item_dashboard.py @@ -3,45 +3,34 @@ from frappe import _ def get_data(): return { - 'heatmap': True, - 'heatmap_message': _('This is based on stock movement. See {0} for details')\ - .format('' + _('Stock Ledger') + ''), - 'fieldname': 'item_code', - 'non_standard_fieldnames': { - 'Work Order': 'production_item', - 'Product Bundle': 'new_item_code', - 'BOM': 'item', - 'Batch': 'item' + "heatmap": True, + "heatmap_message": _("This is based on stock movement. See {0} for details").format( + '' + _("Stock Ledger") + "" + ), + "fieldname": "item_code", + "non_standard_fieldnames": { + "Work Order": "production_item", + "Product Bundle": "new_item_code", + "BOM": "item", + "Batch": "item", }, - 'transactions': [ + "transactions": [ + {"label": _("Groups"), "items": ["BOM", "Product Bundle", "Item Alternative"]}, + {"label": _("Pricing"), "items": ["Item Price", "Pricing Rule"]}, + {"label": _("Sell"), "items": ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]}, { - 'label': _('Groups'), - 'items': ['BOM', 'Product Bundle', 'Item Alternative'] + "label": _("Buy"), + "items": [ + "Material Request", + "Supplier Quotation", + "Request for Quotation", + "Purchase Order", + "Purchase Receipt", + "Purchase Invoice", + ], }, - { - 'label': _('Pricing'), - 'items': ['Item Price', 'Pricing Rule'] - }, - { - 'label': _('Sell'), - 'items': ['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice'] - }, - { - 'label': _('Buy'), - 'items': ['Material Request', 'Supplier Quotation', 'Request for Quotation', - 'Purchase Order', 'Purchase Receipt', 'Purchase Invoice'] - }, - { - 'label': _('Manufacture'), - 'items': ['Production Plan', 'Work Order', 'Item Manufacturer'] - }, - { - 'label': _('Traceability'), - 'items': ['Serial No', 'Batch'] - }, - { - 'label': _('Move'), - 'items': ['Stock Entry'] - } - ] + {"label": _("Manufacture"), "items": ["Production Plan", "Work Order", "Item Manufacturer"]}, + {"label": _("Traceability"), "items": ["Serial No", "Batch"]}, + {"label": _("Move"), "items": ["Stock Entry"]}, + ], } diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 05e6e76e21..328d937f31 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -30,6 +30,7 @@ from erpnext.stock.get_item_details import get_item_details test_ignore = ["BOM"] test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"] + def make_item(item_code=None, properties=None): if not item_code: item_code = frappe.generate_hash(length=16) @@ -37,13 +38,15 @@ def make_item(item_code=None, properties=None): if frappe.db.exists("Item", item_code): return frappe.get_doc("Item", item_code) - item = frappe.get_doc({ - "doctype": "Item", - "item_code": item_code, - "item_name": item_code, - "description": item_code, - "item_group": "Products" - }) + item = frappe.get_doc( + { + "doctype": "Item", + "item_code": item_code, + "item_name": item_code, + "description": item_code, + "item_group": "Products", + } + ) if properties: item.update(properties) @@ -56,6 +59,7 @@ def make_item(item_code=None, properties=None): return item + class TestItem(FrappeTestCase): def setUp(self): super().setUp() @@ -98,56 +102,91 @@ class TestItem(FrappeTestCase): make_test_objects("Item Price") company = "_Test Company" - currency = frappe.get_cached_value("Company", company, "default_currency") + currency = frappe.get_cached_value("Company", company, "default_currency") - details = get_item_details({ - "item_code": "_Test Item", - "company": company, - "price_list": "_Test Price List", - "currency": currency, - "doctype": "Sales Order", - "conversion_rate": 1, - "price_list_currency": currency, - "plc_conversion_rate": 1, - "order_type": "Sales", - "customer": "_Test Customer", - "conversion_factor": 1, - "price_list_uom_dependant": 1, - "ignore_pricing_rule": 1 - }) + details = get_item_details( + { + "item_code": "_Test Item", + "company": company, + "price_list": "_Test Price List", + "currency": currency, + "doctype": "Sales Order", + "conversion_rate": 1, + "price_list_currency": currency, + "plc_conversion_rate": 1, + "order_type": "Sales", + "customer": "_Test Customer", + "conversion_factor": 1, + "price_list_uom_dependant": 1, + "ignore_pricing_rule": 1, + } + ) for key, value in to_check.items(): self.assertEqual(value, details.get(key)) def test_item_tax_template(self): expected_item_tax_template = [ - {"item_code": "_Test Item With Item Tax Template", "tax_category": "", - "item_tax_template": "_Test Account Excise Duty @ 10 - _TC"}, - {"item_code": "_Test Item With Item Tax Template", "tax_category": "_Test Tax Category 1", - "item_tax_template": "_Test Account Excise Duty @ 12 - _TC"}, - {"item_code": "_Test Item With Item Tax Template", "tax_category": "_Test Tax Category 2", - "item_tax_template": None}, - - {"item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "", - "item_tax_template": "_Test Account Excise Duty @ 10 - _TC"}, - {"item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "_Test Tax Category 1", - "item_tax_template": "_Test Account Excise Duty @ 12 - _TC"}, - {"item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "_Test Tax Category 2", - "item_tax_template": None}, - - {"item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "", - "item_tax_template": "_Test Account Excise Duty @ 15 - _TC"}, - {"item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "_Test Tax Category 1", - "item_tax_template": "_Test Account Excise Duty @ 12 - _TC"}, - {"item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "_Test Tax Category 2", - "item_tax_template": None}, - - {"item_code": "_Test Item Override Group Item Tax Template", "tax_category": "", - "item_tax_template": "_Test Account Excise Duty @ 20 - _TC"}, - {"item_code": "_Test Item Override Group Item Tax Template", "tax_category": "_Test Tax Category 1", - "item_tax_template": "_Test Item Tax Template 1 - _TC"}, - {"item_code": "_Test Item Override Group Item Tax Template", "tax_category": "_Test Tax Category 2", - "item_tax_template": None}, + { + "item_code": "_Test Item With Item Tax Template", + "tax_category": "", + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC", + }, + { + "item_code": "_Test Item With Item Tax Template", + "tax_category": "_Test Tax Category 1", + "item_tax_template": "_Test Account Excise Duty @ 12 - _TC", + }, + { + "item_code": "_Test Item With Item Tax Template", + "tax_category": "_Test Tax Category 2", + "item_tax_template": None, + }, + { + "item_code": "_Test Item Inherit Group Item Tax Template 1", + "tax_category": "", + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC", + }, + { + "item_code": "_Test Item Inherit Group Item Tax Template 1", + "tax_category": "_Test Tax Category 1", + "item_tax_template": "_Test Account Excise Duty @ 12 - _TC", + }, + { + "item_code": "_Test Item Inherit Group Item Tax Template 1", + "tax_category": "_Test Tax Category 2", + "item_tax_template": None, + }, + { + "item_code": "_Test Item Inherit Group Item Tax Template 2", + "tax_category": "", + "item_tax_template": "_Test Account Excise Duty @ 15 - _TC", + }, + { + "item_code": "_Test Item Inherit Group Item Tax Template 2", + "tax_category": "_Test Tax Category 1", + "item_tax_template": "_Test Account Excise Duty @ 12 - _TC", + }, + { + "item_code": "_Test Item Inherit Group Item Tax Template 2", + "tax_category": "_Test Tax Category 2", + "item_tax_template": None, + }, + { + "item_code": "_Test Item Override Group Item Tax Template", + "tax_category": "", + "item_tax_template": "_Test Account Excise Duty @ 20 - _TC", + }, + { + "item_code": "_Test Item Override Group Item Tax Template", + "tax_category": "_Test Tax Category 1", + "item_tax_template": "_Test Item Tax Template 1 - _TC", + }, + { + "item_code": "_Test Item Override Group Item Tax Template", + "tax_category": "_Test Tax Category 2", + "item_tax_template": None, + }, ] expected_item_tax_map = { @@ -156,43 +195,55 @@ class TestItem(FrappeTestCase): "_Test Account Excise Duty @ 12 - _TC": {"_Test Account Excise Duty - _TC": 12}, "_Test Account Excise Duty @ 15 - _TC": {"_Test Account Excise Duty - _TC": 15}, "_Test Account Excise Duty @ 20 - _TC": {"_Test Account Excise Duty - _TC": 20}, - "_Test Item Tax Template 1 - _TC": {"_Test Account Excise Duty - _TC": 5, "_Test Account Education Cess - _TC": 10, - "_Test Account S&H Education Cess - _TC": 15} + "_Test Item Tax Template 1 - _TC": { + "_Test Account Excise Duty - _TC": 5, + "_Test Account Education Cess - _TC": 10, + "_Test Account S&H Education Cess - _TC": 15, + }, } for data in expected_item_tax_template: - details = get_item_details({ - "item_code": data['item_code'], - "tax_category": data['tax_category'], - "company": "_Test Company", - "price_list": "_Test Price List", - "currency": "_Test Currency", - "doctype": "Sales Order", - "conversion_rate": 1, - "price_list_currency": "_Test Currency", - "plc_conversion_rate": 1, - "order_type": "Sales", - "customer": "_Test Customer", - "conversion_factor": 1, - "price_list_uom_dependant": 1, - "ignore_pricing_rule": 1 - }) + details = get_item_details( + { + "item_code": data["item_code"], + "tax_category": data["tax_category"], + "company": "_Test Company", + "price_list": "_Test Price List", + "currency": "_Test Currency", + "doctype": "Sales Order", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "order_type": "Sales", + "customer": "_Test Customer", + "conversion_factor": 1, + "price_list_uom_dependant": 1, + "ignore_pricing_rule": 1, + } + ) - self.assertEqual(details.item_tax_template, data['item_tax_template']) - self.assertEqual(json.loads(details.item_tax_rate), expected_item_tax_map[details.item_tax_template]) + self.assertEqual(details.item_tax_template, data["item_tax_template"]) + self.assertEqual( + json.loads(details.item_tax_rate), expected_item_tax_map[details.item_tax_template] + ) def test_item_defaults(self): frappe.delete_doc_if_exists("Item", "Test Item With Defaults", force=1) - make_item("Test Item With Defaults", { - "item_group": "_Test Item Group", - "brand": "_Test Brand With Item Defaults", - "item_defaults": [{ - "company": "_Test Company", - "default_warehouse": "_Test Warehouse 2 - _TC", # no override - "expense_account": "_Test Account Stock Expenses - _TC", # override brand default - "buying_cost_center": "_Test Write Off Cost Center - _TC", # override item group default - }] - }) + make_item( + "Test Item With Defaults", + { + "item_group": "_Test Item Group", + "brand": "_Test Brand With Item Defaults", + "item_defaults": [ + { + "company": "_Test Company", + "default_warehouse": "_Test Warehouse 2 - _TC", # no override + "expense_account": "_Test Account Stock Expenses - _TC", # override brand default + "buying_cost_center": "_Test Write Off Cost Center - _TC", # override item group default + } + ], + }, + ) sales_item_check = { "item_code": "Test Item With Defaults", @@ -201,17 +252,19 @@ class TestItem(FrappeTestCase): "expense_account": "_Test Account Stock Expenses - _TC", # from item "cost_center": "_Test Cost Center 2 - _TC", # from item group } - sales_item_details = get_item_details({ - "item_code": "Test Item With Defaults", - "company": "_Test Company", - "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", - }) + sales_item_details = get_item_details( + { + "item_code": "Test Item With Defaults", + "company": "_Test Company", + "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", + } + ) for key, value in sales_item_check.items(): self.assertEqual(value, sales_item_details.get(key)) @@ -220,38 +273,47 @@ class TestItem(FrappeTestCase): "warehouse": "_Test Warehouse 2 - _TC", # from item "expense_account": "_Test Account Stock Expenses - _TC", # from item "income_account": "_Test Account Sales - _TC", # from brand - "cost_center": "_Test Write Off Cost Center - _TC" # from item + "cost_center": "_Test Write Off Cost Center - _TC", # from item } - purchase_item_details = get_item_details({ - "item_code": "Test Item With Defaults", - "company": "_Test Company", - "price_list": "_Test Price List", - "currency": "_Test Currency", - "doctype": "Purchase Invoice", - "conversion_rate": 1, - "price_list_currency": "_Test Currency", - "plc_conversion_rate": 1, - "supplier": "_Test Supplier", - }) + purchase_item_details = get_item_details( + { + "item_code": "Test Item With Defaults", + "company": "_Test Company", + "price_list": "_Test Price List", + "currency": "_Test Currency", + "doctype": "Purchase Invoice", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "supplier": "_Test Supplier", + } + ) for key, value in purchase_item_check.items(): self.assertEqual(value, purchase_item_details.get(key)) def test_item_default_validations(self): with self.assertRaises(frappe.ValidationError) as ve: - make_item("Bad Item defaults", { - "item_group": "_Test Item Group", - "item_defaults": [{ - "company": "_Test Company 1", - "default_warehouse": "_Test Warehouse - _TC", - "expense_account": "Stock In Hand - _TC", - "buying_cost_center": "_Test Cost Center - _TC", - "selling_cost_center": "_Test Cost Center - _TC", - }] - }) + make_item( + "Bad Item defaults", + { + "item_group": "_Test Item Group", + "item_defaults": [ + { + "company": "_Test Company 1", + "default_warehouse": "_Test Warehouse - _TC", + "expense_account": "Stock In Hand - _TC", + "buying_cost_center": "_Test Cost Center - _TC", + "selling_cost_center": "_Test Cost Center - _TC", + } + ], + }, + ) - self.assertTrue("belong to company" in str(ve.exception).lower(), - msg="Mismatching company entities in item defaults should not be allowed.") + self.assertTrue( + "belong to company" in str(ve.exception).lower(), + msg="Mismatching company entities in item defaults should not be allowed.", + ) def test_item_attribute_change_after_variant(self): frappe.delete_doc_if_exists("Item", "_Test Variant Item-L", force=1) @@ -259,7 +321,7 @@ class TestItem(FrappeTestCase): variant = create_variant("_Test Variant Item", {"Test Size": "Large"}) variant.save() - attribute = frappe.get_doc('Item Attribute', 'Test Size') + attribute = frappe.get_doc("Item Attribute", "Test Size") attribute.item_attribute_values = [] # reset flags @@ -282,20 +344,18 @@ class TestItem(FrappeTestCase): def test_copy_fields_from_template_to_variants(self): frappe.delete_doc_if_exists("Item", "_Test Variant Item-XL", force=1) - fields = [{'field_name': 'item_group'}, {'field_name': 'is_stock_item'}] - allow_fields = [d.get('field_name') for d in fields] + fields = [{"field_name": "item_group"}, {"field_name": "is_stock_item"}] + allow_fields = [d.get("field_name") for d in fields] set_item_variant_settings(fields) - if not frappe.db.get_value('Item Attribute Value', - {'parent': 'Test Size', 'attribute_value': 'Extra Large'}, 'name'): - item_attribute = frappe.get_doc('Item Attribute', 'Test Size') - item_attribute.append('item_attribute_values', { - 'attribute_value' : 'Extra Large', - 'abbr': 'XL' - }) + if not frappe.db.get_value( + "Item Attribute Value", {"parent": "Test Size", "attribute_value": "Extra Large"}, "name" + ): + item_attribute = frappe.get_doc("Item Attribute", "Test Size") + item_attribute.append("item_attribute_values", {"attribute_value": "Extra Large", "abbr": "XL"}) item_attribute.save() - template = frappe.get_doc('Item', '_Test Variant Item') + template = frappe.get_doc("Item", "_Test Variant Item") template.item_group = "_Test Item Group D" template.save() @@ -304,70 +364,71 @@ class TestItem(FrappeTestCase): variant.item_name = "_Test Variant Item-XL" variant.save() - variant = frappe.get_doc('Item', '_Test Variant Item-XL') + variant = frappe.get_doc("Item", "_Test Variant Item-XL") for fieldname in allow_fields: self.assertEqual(template.get(fieldname), variant.get(fieldname)) - template = frappe.get_doc('Item', '_Test Variant Item') + template = frappe.get_doc("Item", "_Test Variant Item") template.item_group = "_Test Item Group Desktops" template.save() def test_make_item_variant_with_numeric_values(self): # cleanup - for d in frappe.db.get_all('Item', filters={'variant_of': - '_Test Numeric Template Item'}): + for d in frappe.db.get_all("Item", filters={"variant_of": "_Test Numeric Template Item"}): frappe.delete_doc_if_exists("Item", d.name) frappe.delete_doc_if_exists("Item", "_Test Numeric Template Item") frappe.delete_doc_if_exists("Item Attribute", "Test Item Length") - frappe.db.sql('''delete from `tabItem Variant Attribute` - where attribute="Test Item Length"''') + frappe.db.sql( + '''delete from `tabItem Variant Attribute` + where attribute="Test Item Length"''' + ) frappe.flags.attribute_values = None # make item attribute - frappe.get_doc({ - "doctype": "Item Attribute", - "attribute_name": "Test Item Length", - "numeric_values": 1, - "from_range": 0.0, - "to_range": 100.0, - "increment": 0.5 - }).insert() + frappe.get_doc( + { + "doctype": "Item Attribute", + "attribute_name": "Test Item Length", + "numeric_values": 1, + "from_range": 0.0, + "to_range": 100.0, + "increment": 0.5, + } + ).insert() # make template item - make_item("_Test Numeric Template Item", { - "attributes": [ - { - "attribute": "Test Size" - }, - { - "attribute": "Test Item Length", - "numeric_values": 1, - "from_range": 0.0, - "to_range": 100.0, - "increment": 0.5 - } - ], - "item_defaults": [ - { - "default_warehouse": "_Test Warehouse - _TC", - "company": "_Test Company" - } - ], - "has_variants": 1 - }) + make_item( + "_Test Numeric Template Item", + { + "attributes": [ + {"attribute": "Test Size"}, + { + "attribute": "Test Item Length", + "numeric_values": 1, + "from_range": 0.0, + "to_range": 100.0, + "increment": 0.5, + }, + ], + "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}], + "has_variants": 1, + }, + ) - variant = create_variant("_Test Numeric Template Item", - {"Test Size": "Large", "Test Item Length": 1.1}) + variant = create_variant( + "_Test Numeric Template Item", {"Test Size": "Large", "Test Item Length": 1.1} + ) self.assertEqual(variant.item_code, "_Test Numeric Template Item-L-1.1") variant.item_code = "_Test Numeric Variant-L-1.1" variant.item_name = "_Test Numeric Variant Large 1.1m" self.assertRaises(InvalidItemAttributeValueError, variant.save) - variant = create_variant("_Test Numeric Template Item", - {"Test Size": "Large", "Test Item Length": 1.5}) + variant = create_variant( + "_Test Numeric Template Item", {"Test Size": "Large", "Test Item Length": 1.5} + ) self.assertEqual(variant.item_code, "_Test Numeric Template Item-L-1.5") variant.item_code = "_Test Numeric Variant-L-1.5" variant.item_name = "_Test Numeric Variant Large 1.5m" @@ -377,21 +438,20 @@ class TestItem(FrappeTestCase): old = create_item(frappe.generate_hash(length=20)).name new = create_item(frappe.generate_hash(length=20)).name - make_stock_entry(item_code=old, target="_Test Warehouse - _TC", - qty=1, rate=100) - make_stock_entry(item_code=old, target="_Test Warehouse 1 - _TC", - qty=1, rate=100) - make_stock_entry(item_code=new, target="_Test Warehouse 1 - _TC", - qty=1, rate=100) + make_stock_entry(item_code=old, target="_Test Warehouse - _TC", qty=1, rate=100) + make_stock_entry(item_code=old, target="_Test Warehouse 1 - _TC", qty=1, rate=100) + make_stock_entry(item_code=new, target="_Test Warehouse 1 - _TC", qty=1, rate=100) frappe.rename_doc("Item", old, new, merge=True) self.assertFalse(frappe.db.exists("Item", old)) - self.assertTrue(frappe.db.get_value("Bin", - {"item_code": new, "warehouse": "_Test Warehouse - _TC"})) - self.assertTrue(frappe.db.get_value("Bin", - {"item_code": new, "warehouse": "_Test Warehouse 1 - _TC"})) + self.assertTrue( + frappe.db.get_value("Bin", {"item_code": new, "warehouse": "_Test Warehouse - _TC"}) + ) + self.assertTrue( + frappe.db.get_value("Bin", {"item_code": new, "warehouse": "_Test Warehouse 1 - _TC"}) + ) def test_item_merging_with_product_bundle(self): from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle @@ -414,13 +474,12 @@ class TestItem(FrappeTestCase): self.assertFalse(frappe.db.exists("Item", "Test Item Bundle Item 1")) def test_uom_conversion_factor(self): - if frappe.db.exists('Item', 'Test Item UOM'): - frappe.delete_doc('Item', 'Test Item UOM') + if frappe.db.exists("Item", "Test Item UOM"): + frappe.delete_doc("Item", "Test Item UOM") - item_doc = make_item("Test Item UOM", { - "stock_uom": "Gram", - "uoms": [dict(uom='Carat'), dict(uom='Kg')] - }) + item_doc = make_item( + "Test Item UOM", {"stock_uom": "Gram", "uoms": [dict(uom="Carat"), dict(uom="Kg")]} + ) for d in item_doc.uoms: value = get_uom_conv_factor(d.uom, item_doc.stock_uom) @@ -440,48 +499,46 @@ class TestItem(FrappeTestCase): self.assertEqual(factor, 1.0) def test_item_variant_by_manufacturer(self): - fields = [{'field_name': 'description'}, {'field_name': 'variant_based_on'}] + fields = [{"field_name": "description"}, {"field_name": "variant_based_on"}] set_item_variant_settings(fields) - if frappe.db.exists('Item', '_Test Variant Mfg'): - frappe.delete_doc('Item', '_Test Variant Mfg') - if frappe.db.exists('Item', '_Test Variant Mfg-1'): - frappe.delete_doc('Item', '_Test Variant Mfg-1') - if frappe.db.exists('Manufacturer', 'MSG1'): - frappe.delete_doc('Manufacturer', 'MSG1') + if frappe.db.exists("Item", "_Test Variant Mfg"): + frappe.delete_doc("Item", "_Test Variant Mfg") + if frappe.db.exists("Item", "_Test Variant Mfg-1"): + frappe.delete_doc("Item", "_Test Variant Mfg-1") + if frappe.db.exists("Manufacturer", "MSG1"): + frappe.delete_doc("Manufacturer", "MSG1") - template = frappe.get_doc(dict( - doctype='Item', - item_code='_Test Variant Mfg', - has_variant=1, - item_group='Products', - variant_based_on='Manufacturer' - )).insert() + template = frappe.get_doc( + dict( + doctype="Item", + item_code="_Test Variant Mfg", + has_variant=1, + item_group="Products", + variant_based_on="Manufacturer", + ) + ).insert() - manufacturer = frappe.get_doc(dict( - doctype='Manufacturer', - short_name='MSG1' - )).insert() + manufacturer = frappe.get_doc(dict(doctype="Manufacturer", short_name="MSG1")).insert() variant = get_variant(template.name, manufacturer=manufacturer.name) - self.assertEqual(variant.item_code, '_Test Variant Mfg-1') - self.assertEqual(variant.description, '_Test Variant Mfg') - self.assertEqual(variant.manufacturer, 'MSG1') + self.assertEqual(variant.item_code, "_Test Variant Mfg-1") + self.assertEqual(variant.description, "_Test Variant Mfg") + self.assertEqual(variant.manufacturer, "MSG1") variant.insert() - variant = get_variant(template.name, manufacturer=manufacturer.name, - manufacturer_part_no='007') - self.assertEqual(variant.item_code, '_Test Variant Mfg-2') - self.assertEqual(variant.description, '_Test Variant Mfg') - self.assertEqual(variant.manufacturer, 'MSG1') - self.assertEqual(variant.manufacturer_part_no, '007') + variant = get_variant(template.name, manufacturer=manufacturer.name, manufacturer_part_no="007") + self.assertEqual(variant.item_code, "_Test Variant Mfg-2") + self.assertEqual(variant.description, "_Test Variant Mfg") + self.assertEqual(variant.manufacturer, "MSG1") + self.assertEqual(variant.manufacturer_part_no, "007") def test_stock_exists_against_template_item(self): - stock_item = frappe.get_all('Stock Ledger Entry', fields = ["item_code"], limit=1) + stock_item = frappe.get_all("Stock Ledger Entry", fields=["item_code"], limit=1) if stock_item: item_code = stock_item[0].item_code - item_doc = frappe.get_doc('Item', item_code) + item_doc = frappe.get_doc("Item", item_code) item_doc.has_variants = 1 self.assertRaises(StockExistsForTemplate, item_doc.save) @@ -494,37 +551,27 @@ class TestItem(FrappeTestCase): # Create new item and add barcodes barcode_properties_list = [ - { - "barcode": "0012345678905", - "barcode_type": "EAN" - }, - { - "barcode": "012345678905", - "barcode_type": "UAN" - }, + {"barcode": "0012345678905", "barcode_type": "EAN"}, + {"barcode": "012345678905", "barcode_type": "UAN"}, { "barcode": "ARBITRARY_TEXT", - } + }, ] create_item(item_code) for barcode_properties in barcode_properties_list: - item_doc = frappe.get_doc('Item', item_code) - new_barcode = item_doc.append('barcodes') + item_doc = frappe.get_doc("Item", item_code) + new_barcode = item_doc.append("barcodes") new_barcode.update(barcode_properties) item_doc.save() # Check values saved correctly barcodes = frappe.get_all( - 'Item Barcode', - fields=['barcode', 'barcode_type'], - filters={'parent': item_code}) + "Item Barcode", fields=["barcode", "barcode_type"], filters={"parent": item_code} + ) for barcode_properties in barcode_properties_list: - barcode_to_find = barcode_properties['barcode'] - matching_barcodes = [ - x for x in barcodes - if x['barcode'] == barcode_to_find - ] + barcode_to_find = barcode_properties["barcode"] + matching_barcodes = [x for x in barcodes if x["barcode"] == barcode_to_find] self.assertEqual(len(matching_barcodes), 1) details = matching_barcodes[0] @@ -532,20 +579,21 @@ class TestItem(FrappeTestCase): self.assertEqual(value, details.get(key)) # Add barcode again - should cause DuplicateEntryError - item_doc = frappe.get_doc('Item', item_code) - new_barcode = item_doc.append('barcodes') + item_doc = frappe.get_doc("Item", item_code) + new_barcode = item_doc.append("barcodes") new_barcode.update(barcode_properties_list[0]) self.assertRaises(frappe.UniqueValidationError, item_doc.save) # Add invalid barcode - should cause InvalidBarcode - item_doc = frappe.get_doc('Item', item_code) - new_barcode = item_doc.append('barcodes') - new_barcode.barcode = '9999999999999' - new_barcode.barcode_type = 'EAN' + item_doc = frappe.get_doc("Item", item_code) + new_barcode = item_doc.append("barcodes") + new_barcode.barcode = "9999999999999" + new_barcode.barcode_type = "EAN" self.assertRaises(InvalidBarcode, item_doc.save) def test_heatmap_data(self): import time + data = get_timeline_data("Item", "_Test Item") self.assertTrue(isinstance(data, dict)) @@ -588,20 +636,17 @@ class TestItem(FrappeTestCase): def test_check_stock_uom_with_bin_no_sle(self): from erpnext.stock.stock_balance import update_bin_qty + item = create_item("_Item with bin qty") item.stock_uom = "Gram" item.save() - update_bin_qty(item.item_code, "_Test Warehouse - _TC", { - "reserved_qty": 10 - }) + update_bin_qty(item.item_code, "_Test Warehouse - _TC", {"reserved_qty": 10}) item.stock_uom = "Kilometer" self.assertRaises(frappe.ValidationError, item.save) - update_bin_qty(item.item_code, "_Test Warehouse - _TC", { - "reserved_qty": 0 - }) + update_bin_qty(item.item_code, "_Test Warehouse - _TC", {"reserved_qty": 0}) item.load_from_db() item.stock_uom = "Kilometer" @@ -636,7 +681,7 @@ class TestItem(FrappeTestCase): @change_settings("Stock Settings", {"allow_negative_stock": 0}) def test_item_wise_negative_stock(self): - """ When global settings are disabled check that item that allows + """When global settings are disabled check that item that allows negative stock can still consume material in all known stock transactions that consume inventory.""" from erpnext.stock.stock_ledger import is_negative_stock_allowed @@ -648,17 +693,22 @@ class TestItem(FrappeTestCase): @change_settings("Stock Settings", {"allow_negative_stock": 0}) def test_backdated_negative_stock(self): - """ same as test above but backdated entries """ + """same as test above but backdated entries""" from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + item = make_item("_TestNegativeItemSetting", {"allow_negative_stock": 1, "valuation_rate": 100}) # create a future entry so all new entries are backdated - make_stock_entry(qty=1, item_code=item.name, target="_Test Warehouse - _TC", posting_date = add_days(today(), 5)) + make_stock_entry( + qty=1, item_code=item.name, target="_Test Warehouse - _TC", posting_date=add_days(today(), 5) + ) self.consume_item_code_with_differet_stock_transactions(item_code=item.name) @change_settings("Stock Settings", {"sample_retention_warehouse": "_Test Warehouse - _TC"}) def test_retain_sample(self): - item = make_item("_TestRetainSample", {'has_batch_no': 1, 'retain_sample': 1, 'sample_quantity': 1}) + item = make_item( + "_TestRetainSample", {"has_batch_no": 1, "retain_sample": 1, "sample_quantity": 1} + ) self.assertEqual(item.has_batch_no, 1) self.assertEqual(item.retain_sample, 1) @@ -670,7 +720,9 @@ class TestItem(FrappeTestCase): self.assertEqual(item.sample_quantity, None) item.delete() - def consume_item_code_with_differet_stock_transactions(self, item_code, warehouse="_Test Warehouse - _TC"): + def consume_item_code_with_differet_stock_transactions( + self, item_code, warehouse="_Test Warehouse - _TC" + ): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt @@ -694,10 +746,11 @@ class TestItem(FrappeTestCase): def set_item_variant_settings(fields): - doc = frappe.get_doc('Item Variant Settings') - doc.set('fields', fields) + doc = frappe.get_doc("Item Variant Settings") + doc.set("fields", fields) doc.save() + def make_item_variant(): if not frappe.db.exists("Item", "_Test Variant Item-S"): variant = create_variant("_Test Variant Item", """{"Test Size": "Small"}""") @@ -705,11 +758,23 @@ def make_item_variant(): variant.item_name = "_Test Variant Item-S" variant.save() -test_records = frappe.get_test_records('Item') -def create_item(item_code, is_stock_item=1, valuation_rate=0, warehouse="_Test Warehouse - _TC", - is_customer_provided_item=None, customer=None, is_purchase_item=None, opening_stock=0, is_fixed_asset=0, - asset_category=None, company="_Test Company"): +test_records = frappe.get_test_records("Item") + + +def create_item( + item_code, + is_stock_item=1, + valuation_rate=0, + warehouse="_Test Warehouse - _TC", + is_customer_provided_item=None, + customer=None, + is_purchase_item=None, + opening_stock=0, + is_fixed_asset=0, + asset_category=None, + company="_Test Company", +): if not frappe.db.exists("Item", item_code): item = frappe.new_doc("Item") item.item_code = item_code @@ -723,11 +788,8 @@ def create_item(item_code, is_stock_item=1, valuation_rate=0, warehouse="_Test W item.valuation_rate = valuation_rate item.is_purchase_item = is_purchase_item item.is_customer_provided_item = is_customer_provided_item - item.customer = customer or '' - item.append("item_defaults", { - "default_warehouse": warehouse, - "company": company - }) + item.customer = customer or "" + item.append("item_defaults", {"default_warehouse": warehouse, "company": company}) item.save() else: item = frappe.get_doc("Item", item_code) diff --git a/erpnext/stock/doctype/item_alternative/item_alternative.py b/erpnext/stock/doctype/item_alternative/item_alternative.py index 766647b32e..0f93bb9e95 100644 --- a/erpnext/stock/doctype/item_alternative/item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/item_alternative.py @@ -14,8 +14,7 @@ class ItemAlternative(Document): self.validate_duplicate() def has_alternative_item(self): - if (self.item_code and - not frappe.db.get_value('Item', self.item_code, 'allow_alternative_item')): + if self.item_code and not frappe.db.get_value("Item", self.item_code, "allow_alternative_item"): frappe.throw(_("Not allow to set alternative item for the item {0}").format(self.item_code)) def validate_alternative_item(self): @@ -23,19 +22,32 @@ class ItemAlternative(Document): frappe.throw(_("Alternative item must not be same as item code")) item_meta = frappe.get_meta("Item") - fields = ["is_stock_item", "include_item_in_manufacturing","has_serial_no", "has_batch_no", "allow_alternative_item"] + fields = [ + "is_stock_item", + "include_item_in_manufacturing", + "has_serial_no", + "has_batch_no", + "allow_alternative_item", + ] item_data = frappe.db.get_value("Item", self.item_code, fields, as_dict=1) - alternative_item_data = frappe.db.get_value("Item", self.alternative_item_code, fields, as_dict=1) + alternative_item_data = frappe.db.get_value( + "Item", self.alternative_item_code, fields, as_dict=1 + ) for field in fields: - if item_data.get(field) != alternative_item_data.get(field): + if item_data.get(field) != alternative_item_data.get(field): raise_exception, alert = [1, False] if field == "is_stock_item" else [0, True] - frappe.msgprint(_("The value of {0} differs between Items {1} and {2}") \ - .format(frappe.bold(item_meta.get_label(field)), - frappe.bold(self.alternative_item_code), - frappe.bold(self.item_code)), - alert=alert, raise_exception=raise_exception, indicator="Orange") + frappe.msgprint( + _("The value of {0} differs between Items {1} and {2}").format( + frappe.bold(item_meta.get_label(field)), + frappe.bold(self.alternative_item_code), + frappe.bold(self.item_code), + ), + alert=alert, + raise_exception=raise_exception, + indicator="Orange", + ) alternate_item_check_msg = _("Allow Alternative Item must be checked on Item {}") @@ -44,24 +56,30 @@ class ItemAlternative(Document): if self.two_way and not alternative_item_data.allow_alternative_item: frappe.throw(alternate_item_check_msg.format(self.item_code)) - - - def validate_duplicate(self): - if frappe.db.get_value("Item Alternative", {'item_code': self.item_code, - 'alternative_item_code': self.alternative_item_code, 'name': ('!=', self.name)}): + if frappe.db.get_value( + "Item Alternative", + { + "item_code": self.item_code, + "alternative_item_code": self.alternative_item_code, + "name": ("!=", self.name), + }, + ): frappe.throw(_("Already record exists for the item {0}").format(self.item_code)) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_alternative_items(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql(""" (select alternative_item_code from `tabItem Alternative` + return frappe.db.sql( + """ (select alternative_item_code from `tabItem Alternative` where item_code = %(item_code)s and alternative_item_code like %(txt)s) union (select item_code from `tabItem Alternative` where alternative_item_code = %(item_code)s and item_code like %(txt)s and two_way = 1) limit {0}, {1} - """.format(start, page_len), { - "item_code": filters.get('item_code'), - "txt": '%' + txt + '%' - }) + """.format( + start, page_len + ), + {"item_code": filters.get("item_code"), "txt": "%" + txt + "%"}, + ) diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py index 501c1c1ad3..d829b2cbf3 100644 --- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py @@ -27,121 +27,181 @@ class TestItemAlternative(FrappeTestCase): make_items() def test_alternative_item_for_subcontract_rm(self): - frappe.db.set_value('Buying Settings', None, - 'backflush_raw_materials_of_subcontract_based_on', 'BOM') + frappe.db.set_value( + "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM" + ) - create_stock_reconciliation(item_code='Alternate Item For A RW 1', warehouse='_Test Warehouse - _TC', - qty=5, rate=2000) - create_stock_reconciliation(item_code='Test FG A RW 2', warehouse='_Test Warehouse - _TC', - qty=5, rate=2000) + create_stock_reconciliation( + item_code="Alternate Item For A RW 1", warehouse="_Test Warehouse - _TC", qty=5, rate=2000 + ) + create_stock_reconciliation( + item_code="Test FG A RW 2", warehouse="_Test Warehouse - _TC", qty=5, rate=2000 + ) supplier_warehouse = "Test Supplier Warehouse - _TC" - po = create_purchase_order(item = "Test Finished Goods - A", - is_subcontracted='Yes', qty=5, rate=3000, supplier_warehouse=supplier_warehouse) + po = create_purchase_order( + item="Test Finished Goods - A", + is_subcontracted="Yes", + qty=5, + rate=3000, + supplier_warehouse=supplier_warehouse, + ) - rm_item = [{"item_code": "Test Finished Goods - A", "rm_item_code": "Test FG A RW 1", "item_name":"Test FG A RW 1", - "qty":5, "warehouse":"_Test Warehouse - _TC", "rate":2000, "amount":10000, "stock_uom":"Nos"}, - {"item_code": "Test Finished Goods - A", "rm_item_code": "Test FG A RW 2", "item_name":"Test FG A RW 2", - "qty":5, "warehouse":"_Test Warehouse - _TC", "rate":2000, "amount":10000, "stock_uom":"Nos"}] + rm_item = [ + { + "item_code": "Test Finished Goods - A", + "rm_item_code": "Test FG A RW 1", + "item_name": "Test FG A RW 1", + "qty": 5, + "warehouse": "_Test Warehouse - _TC", + "rate": 2000, + "amount": 10000, + "stock_uom": "Nos", + }, + { + "item_code": "Test Finished Goods - A", + "rm_item_code": "Test FG A RW 2", + "item_name": "Test FG A RW 2", + "qty": 5, + "warehouse": "_Test Warehouse - _TC", + "rate": 2000, + "amount": 10000, + "stock_uom": "Nos", + }, + ] rm_item_string = json.dumps(rm_item) - reserved_qty_for_sub_contract = frappe.db.get_value('Bin', - {'item_code': 'Test FG A RW 1', 'warehouse': '_Test Warehouse - _TC'}, 'reserved_qty_for_sub_contract') + reserved_qty_for_sub_contract = frappe.db.get_value( + "Bin", + {"item_code": "Test FG A RW 1", "warehouse": "_Test Warehouse - _TC"}, + "reserved_qty_for_sub_contract", + ) se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string)) se.to_warehouse = supplier_warehouse se.insert() - doc = frappe.get_doc('Stock Entry', se.name) + doc = frappe.get_doc("Stock Entry", se.name) for item in doc.items: - if item.item_code == 'Test FG A RW 1': - item.item_code = 'Alternate Item For A RW 1' - item.item_name = 'Alternate Item For A RW 1' - item.description = 'Alternate Item For A RW 1' - item.original_item = 'Test FG A RW 1' + if item.item_code == "Test FG A RW 1": + item.item_code = "Alternate Item For A RW 1" + item.item_name = "Alternate Item For A RW 1" + item.description = "Alternate Item For A RW 1" + item.original_item = "Test FG A RW 1" doc.save() doc.submit() - after_transfer_reserved_qty_for_sub_contract = frappe.db.get_value('Bin', - {'item_code': 'Test FG A RW 1', 'warehouse': '_Test Warehouse - _TC'}, 'reserved_qty_for_sub_contract') + after_transfer_reserved_qty_for_sub_contract = frappe.db.get_value( + "Bin", + {"item_code": "Test FG A RW 1", "warehouse": "_Test Warehouse - _TC"}, + "reserved_qty_for_sub_contract", + ) - self.assertEqual(after_transfer_reserved_qty_for_sub_contract, flt(reserved_qty_for_sub_contract - 5)) + self.assertEqual( + after_transfer_reserved_qty_for_sub_contract, flt(reserved_qty_for_sub_contract - 5) + ) pr = make_purchase_receipt(po.name) pr.save() - pr = frappe.get_doc('Purchase Receipt', pr.name) + pr = frappe.get_doc("Purchase Receipt", pr.name) status = False for d in pr.supplied_items: - if d.rm_item_code == 'Alternate Item For A RW 1': + if d.rm_item_code == "Alternate Item For A RW 1": status = True self.assertEqual(status, True) - frappe.db.set_value('Buying Settings', None, - 'backflush_raw_materials_of_subcontract_based_on', 'Material Transferred for Subcontract') + frappe.db.set_value( + "Buying Settings", + None, + "backflush_raw_materials_of_subcontract_based_on", + "Material Transferred for Subcontract", + ) def test_alternative_item_for_production_rm(self): - create_stock_reconciliation(item_code='Alternate Item For A RW 1', - warehouse='_Test Warehouse - _TC',qty=5, rate=2000) - create_stock_reconciliation(item_code='Test FG A RW 2', warehouse='_Test Warehouse - _TC', - qty=5, rate=2000) - pro_order = make_wo_order_test_record(production_item='Test Finished Goods - A', - qty=5, source_warehouse='_Test Warehouse - _TC', wip_warehouse='Test Supplier Warehouse - _TC') + create_stock_reconciliation( + item_code="Alternate Item For A RW 1", warehouse="_Test Warehouse - _TC", qty=5, rate=2000 + ) + create_stock_reconciliation( + item_code="Test FG A RW 2", warehouse="_Test Warehouse - _TC", qty=5, rate=2000 + ) + pro_order = make_wo_order_test_record( + production_item="Test Finished Goods - A", + qty=5, + source_warehouse="_Test Warehouse - _TC", + wip_warehouse="Test Supplier Warehouse - _TC", + ) - reserved_qty_for_production = frappe.db.get_value('Bin', - {'item_code': 'Test FG A RW 1', 'warehouse': '_Test Warehouse - _TC'}, 'reserved_qty_for_production') + reserved_qty_for_production = frappe.db.get_value( + "Bin", + {"item_code": "Test FG A RW 1", "warehouse": "_Test Warehouse - _TC"}, + "reserved_qty_for_production", + ) ste = frappe.get_doc(make_stock_entry(pro_order.name, "Material Transfer for Manufacture", 5)) ste.insert() for item in ste.items: - if item.item_code == 'Test FG A RW 1': - item.item_code = 'Alternate Item For A RW 1' - item.item_name = 'Alternate Item For A RW 1' - item.description = 'Alternate Item For A RW 1' - item.original_item = 'Test FG A RW 1' + if item.item_code == "Test FG A RW 1": + item.item_code = "Alternate Item For A RW 1" + item.item_name = "Alternate Item For A RW 1" + item.description = "Alternate Item For A RW 1" + item.original_item = "Test FG A RW 1" ste.submit() - reserved_qty_for_production_after_transfer = frappe.db.get_value('Bin', - {'item_code': 'Test FG A RW 1', 'warehouse': '_Test Warehouse - _TC'}, 'reserved_qty_for_production') + reserved_qty_for_production_after_transfer = frappe.db.get_value( + "Bin", + {"item_code": "Test FG A RW 1", "warehouse": "_Test Warehouse - _TC"}, + "reserved_qty_for_production", + ) - self.assertEqual(reserved_qty_for_production_after_transfer, flt(reserved_qty_for_production - 5)) + self.assertEqual( + reserved_qty_for_production_after_transfer, flt(reserved_qty_for_production - 5) + ) ste1 = frappe.get_doc(make_stock_entry(pro_order.name, "Manufacture", 5)) status = False for d in ste1.items: - if d.item_code == 'Alternate Item For A RW 1': + if d.item_code == "Alternate Item For A RW 1": status = True self.assertEqual(status, True) ste1.submit() + def make_items(): - items = ['Test Finished Goods - A', 'Test FG A RW 1', 'Test FG A RW 2', 'Alternate Item For A RW 1'] + items = [ + "Test Finished Goods - A", + "Test FG A RW 1", + "Test FG A RW 2", + "Alternate Item For A RW 1", + ] for item_code in items: - if not frappe.db.exists('Item', item_code): + if not frappe.db.exists("Item", item_code): create_item(item_code) - create_stock_reconciliation(item_code="Test FG A RW 1", - warehouse='_Test Warehouse - _TC', qty=10, rate=2000) + create_stock_reconciliation( + item_code="Test FG A RW 1", warehouse="_Test Warehouse - _TC", qty=10, rate=2000 + ) - if frappe.db.exists('Item', 'Test FG A RW 1'): - doc = frappe.get_doc('Item', 'Test FG A RW 1') + if frappe.db.exists("Item", "Test FG A RW 1"): + doc = frappe.get_doc("Item", "Test FG A RW 1") doc.allow_alternative_item = 1 doc.save() - if frappe.db.exists('Item', 'Test Finished Goods - A'): - doc = frappe.get_doc('Item', 'Test Finished Goods - A') + if frappe.db.exists("Item", "Test Finished Goods - A"): + doc = frappe.get_doc("Item", "Test Finished Goods - A") doc.is_sub_contracted_item = 1 doc.save() - if not frappe.db.get_value('BOM', - {'item': 'Test Finished Goods - A', 'docstatus': 1}): - make_bom(item = 'Test Finished Goods - A', raw_materials = ['Test FG A RW 1', 'Test FG A RW 2']) + if not frappe.db.get_value("BOM", {"item": "Test Finished Goods - A", "docstatus": 1}): + make_bom(item="Test Finished Goods - A", raw_materials=["Test FG A RW 1", "Test FG A RW 2"]) - if not frappe.db.get_value('Warehouse', {'warehouse_name': 'Test Supplier Warehouse'}): - frappe.get_doc({ - 'doctype': 'Warehouse', - 'warehouse_name': 'Test Supplier Warehouse', - 'company': '_Test Company' - }).insert(ignore_permissions=True) + if not frappe.db.get_value("Warehouse", {"warehouse_name": "Test Supplier Warehouse"}): + frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": "Test Supplier Warehouse", + "company": "_Test Company", + } + ).insert(ignore_permissions=True) diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.py b/erpnext/stock/doctype/item_attribute/item_attribute.py index 5a28a9e231..391ff06918 100644 --- a/erpnext/stock/doctype/item_attribute/item_attribute.py +++ b/erpnext/stock/doctype/item_attribute/item_attribute.py @@ -14,7 +14,9 @@ from erpnext.controllers.item_variant import ( ) -class ItemAttributeIncrementError(frappe.ValidationError): pass +class ItemAttributeIncrementError(frappe.ValidationError): + pass + class ItemAttribute(Document): def __setup__(self): @@ -29,11 +31,12 @@ class ItemAttribute(Document): self.validate_exising_items() def validate_exising_items(self): - '''Validate that if there are existing items with attributes, they are valid''' + """Validate that if there are existing items with attributes, they are valid""" attributes_list = [d.attribute_value for d in self.item_attribute_values] # Get Item Variant Attribute details of variant items - items = frappe.db.sql(""" + items = frappe.db.sql( + """ select i.name, iva.attribute_value as value from @@ -41,13 +44,18 @@ class ItemAttribute(Document): where iva.attribute = %(attribute)s and iva.parent = i.name and - i.variant_of is not null and i.variant_of != ''""", {"attribute" : self.name}, as_dict=1) + i.variant_of is not null and i.variant_of != ''""", + {"attribute": self.name}, + as_dict=1, + ) for item in items: if self.numeric_values: validate_is_incremental(self, self.name, item.value, item.name) else: - validate_item_attribute_value(attributes_list, self.name, item.value, item.name, from_variant=False) + validate_item_attribute_value( + attributes_list, self.name, item.value, item.name, from_variant=False + ) def validate_numeric(self): if self.numeric_values: diff --git a/erpnext/stock/doctype/item_attribute/test_item_attribute.py b/erpnext/stock/doctype/item_attribute/test_item_attribute.py index 055c22e0c5..a30f0e999f 100644 --- a/erpnext/stock/doctype/item_attribute/test_item_attribute.py +++ b/erpnext/stock/doctype/item_attribute/test_item_attribute.py @@ -4,7 +4,7 @@ import frappe -test_records = frappe.get_test_records('Item Attribute') +test_records = frappe.get_test_records("Item Attribute") from frappe.tests.utils import FrappeTestCase @@ -18,14 +18,16 @@ class TestItemAttribute(FrappeTestCase): frappe.delete_doc("Item Attribute", "_Test_Length") def test_numeric_item_attribute(self): - item_attribute = frappe.get_doc({ - "doctype": "Item Attribute", - "attribute_name": "_Test_Length", - "numeric_values": 1, - "from_range": 0.0, - "to_range": 100.0, - "increment": 0 - }) + item_attribute = frappe.get_doc( + { + "doctype": "Item Attribute", + "attribute_name": "_Test_Length", + "numeric_values": 1, + "from_range": 0.0, + "to_range": 100.0, + "increment": 0, + } + ) self.assertRaises(ItemAttributeIncrementError, item_attribute.save) diff --git a/erpnext/stock/doctype/item_barcode/item_barcode.py b/erpnext/stock/doctype/item_barcode/item_barcode.py index 64c39dabde..c2c042143e 100644 --- a/erpnext/stock/doctype/item_barcode/item_barcode.py +++ b/erpnext/stock/doctype/item_barcode/item_barcode.py @@ -6,4 +6,4 @@ from frappe.model.document import Document class ItemBarcode(Document): - pass + pass diff --git a/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py b/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py index 469ccd8f2d..b65ba98a8b 100644 --- a/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py +++ b/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py @@ -18,14 +18,17 @@ class ItemManufacturer(Document): def validate_duplicate_entry(self): if self.is_new(): filters = { - 'item_code': self.item_code, - 'manufacturer': self.manufacturer, - 'manufacturer_part_no': self.manufacturer_part_no + "item_code": self.item_code, + "manufacturer": self.manufacturer, + "manufacturer_part_no": self.manufacturer_part_no, } if frappe.db.exists("Item Manufacturer", filters): - frappe.throw(_("Duplicate entry against the item code {0} and manufacturer {1}") - .format(self.item_code, self.manufacturer)) + frappe.throw( + _("Duplicate entry against the item code {0} and manufacturer {1}").format( + self.item_code, self.manufacturer + ) + ) def manage_default_item_manufacturer(self, delete=False): from frappe.model.utils import set_default @@ -37,11 +40,9 @@ class ItemManufacturer(Document): if not self.is_default: # if unchecked and default in Item master, clear it. if default_manufacturer == self.manufacturer and default_part_no == self.manufacturer_part_no: - frappe.db.set_value("Item", item.name, - { - "default_item_manufacturer": None, - "default_manufacturer_part_no": None - }) + frappe.db.set_value( + "Item", item.name, {"default_item_manufacturer": None, "default_manufacturer_part_no": None} + ) elif self.is_default: set_default(self, "item_code") @@ -50,18 +51,26 @@ class ItemManufacturer(Document): if delete: manufacturer, manufacturer_part_no = None, None - elif (default_manufacturer != self.manufacturer) or \ - (default_manufacturer == self.manufacturer and default_part_no != self.manufacturer_part_no): + elif (default_manufacturer != self.manufacturer) or ( + default_manufacturer == self.manufacturer and default_part_no != self.manufacturer_part_no + ): manufacturer = self.manufacturer manufacturer_part_no = self.manufacturer_part_no - frappe.db.set_value("Item", item.name, - { - "default_item_manufacturer": manufacturer, - "default_manufacturer_part_no": manufacturer_part_no - }) + frappe.db.set_value( + "Item", + item.name, + { + "default_item_manufacturer": manufacturer, + "default_manufacturer_part_no": manufacturer_part_no, + }, + ) + @frappe.whitelist() def get_item_manufacturer_part_no(item_code, manufacturer): - return frappe.db.get_value("Item Manufacturer", - {'item_code': item_code, 'manufacturer': manufacturer}, 'manufacturer_part_no') + return frappe.db.get_value( + "Item Manufacturer", + {"item_code": item_code, "manufacturer": manufacturer}, + "manufacturer_part_no", + ) diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index 010e01a78b..562f7b9e12 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -13,7 +13,6 @@ class ItemPriceDuplicateItem(frappe.ValidationError): class ItemPrice(Document): - def validate(self): self.validate_item() self.validate_dates() @@ -32,22 +31,26 @@ class ItemPrice(Document): def update_price_list_details(self): if self.price_list: - price_list_details = frappe.db.get_value("Price List", - {"name": self.price_list, "enabled": 1}, - ["buying", "selling", "currency"]) + price_list_details = frappe.db.get_value( + "Price List", {"name": self.price_list, "enabled": 1}, ["buying", "selling", "currency"] + ) if not price_list_details: - link = frappe.utils.get_link_to_form('Price List', self.price_list) + link = frappe.utils.get_link_to_form("Price List", self.price_list) frappe.throw("The price list {0} does not exist or is disabled".format(link)) self.buying, self.selling, self.currency = price_list_details def update_item_details(self): if self.item_code: - self.item_name, self.item_description = frappe.db.get_value("Item", self.item_code,["item_name", "description"]) + self.item_name, self.item_description = frappe.db.get_value( + "Item", self.item_code, ["item_name", "description"] + ) def check_duplicates(self): - conditions = """where item_code = %(item_code)s and price_list = %(price_list)s and name != %(name)s""" + conditions = ( + """where item_code = %(item_code)s and price_list = %(price_list)s and name != %(name)s""" + ) for field in [ "uom", @@ -56,21 +59,31 @@ class ItemPrice(Document): "packing_unit", "customer", "supplier", - "batch_no"]: + "batch_no", + ]: if self.get(field): conditions += " and {0} = %({0})s ".format(field) else: conditions += "and (isnull({0}) or {0} = '')".format(field) - price_list_rate = frappe.db.sql(""" + price_list_rate = frappe.db.sql( + """ select price_list_rate from `tabItem Price` {conditions} - """.format(conditions=conditions), - self.as_dict(),) + """.format( + conditions=conditions + ), + 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, Batch, UOM, Qty, and Dates." + ), + ItemPriceDuplicateItem, + ) def before_save(self): if self.selling: diff --git a/erpnext/stock/doctype/item_price/test_item_price.py b/erpnext/stock/doctype/item_price/test_item_price.py index 6ceba3f8d3..30d933e247 100644 --- a/erpnext/stock/doctype/item_price/test_item_price.py +++ b/erpnext/stock/doctype/item_price/test_item_price.py @@ -23,8 +23,14 @@ class TestItemPrice(FrappeTestCase): def test_addition_of_new_fields(self): # Based on https://github.com/frappe/erpnext/issues/8456 test_fields_existance = [ - 'supplier', 'customer', 'uom', 'lead_time_days', - 'packing_unit', 'valid_from', 'valid_upto', 'note' + "supplier", + "customer", + "uom", + "lead_time_days", + "packing_unit", + "valid_from", + "valid_upto", + "note", ] doc_fields = frappe.copy_doc(test_records[1]).__dict__.keys() @@ -45,10 +51,10 @@ class TestItemPrice(FrappeTestCase): args = { "price_list": doc.price_list, - "customer": doc.customer, - "uom": "_Test UOM", - "transaction_date": '2017-04-18', - "qty": 10 + "customer": doc.customer, + "uom": "_Test UOM", + "transaction_date": "2017-04-18", + "qty": 10, } price = get_price_list_rate_for(process_args(args), doc.item_code) @@ -61,13 +67,12 @@ class TestItemPrice(FrappeTestCase): "price_list": doc.price_list, "customer": doc.customer, "uom": "_Test UOM", - "transaction_date": '2017-04-18', + "transaction_date": "2017-04-18", } price = get_price_list_rate_for(args, doc.item_code) self.assertEqual(price, None) - def test_prices_at_date(self): # Check correct price at first date doc = frappe.copy_doc(test_records[2]) @@ -76,35 +81,35 @@ class TestItemPrice(FrappeTestCase): "price_list": doc.price_list, "customer": "_Test Customer", "uom": "_Test UOM", - "transaction_date": '2017-04-18', - "qty": 7 + "transaction_date": "2017-04-18", + "qty": 7, } price = get_price_list_rate_for(args, doc.item_code) self.assertEqual(price, 20) def test_prices_at_invalid_date(self): - #Check correct price at invalid date + # Check correct price at invalid date doc = frappe.copy_doc(test_records[3]) args = { "price_list": doc.price_list, "qty": 7, "uom": "_Test UOM", - "transaction_date": "01-15-2019" + "transaction_date": "01-15-2019", } price = get_price_list_rate_for(args, doc.item_code) self.assertEqual(price, None) def test_prices_outside_of_date(self): - #Check correct price when outside of the date + # Check correct price when outside of the date doc = frappe.copy_doc(test_records[4]) args = { "price_list": doc.price_list, - "customer": "_Test Customer", - "uom": "_Test UOM", + "customer": "_Test Customer", + "uom": "_Test UOM", "transaction_date": "2017-04-25", "qty": 7, } @@ -113,7 +118,7 @@ class TestItemPrice(FrappeTestCase): self.assertEqual(price, None) def test_lowest_price_when_no_date_provided(self): - #Check lowest price when no date provided + # Check lowest price when no date provided doc = frappe.copy_doc(test_records[1]) args = { @@ -125,7 +130,6 @@ class TestItemPrice(FrappeTestCase): price = get_price_list_rate_for(args, doc.item_code) self.assertEqual(price, 10) - def test_invalid_item(self): doc = frappe.copy_doc(test_records[1]) # Enter invalid item code @@ -150,8 +154,8 @@ class TestItemPrice(FrappeTestCase): args = { "price_list": doc.price_list, "uom": "_Test UOM", - "transaction_date": '2017-04-18', - "qty": 7 + "transaction_date": "2017-04-18", + "qty": 7, } price = get_price_list_rate_for(args, doc.item_code) @@ -159,4 +163,5 @@ class TestItemPrice(FrappeTestCase): self.assertEqual(price, 21) -test_records = frappe.get_test_records('Item Price') + +test_records = frappe.get_test_records("Item Price") diff --git a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py index be1517eb58..cec5e218cc 100644 --- a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py +++ b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py @@ -8,29 +8,46 @@ from frappe.model.document import Document class ItemVariantSettings(Document): - invalid_fields_for_copy_fields_in_variants = ['barcodes'] + invalid_fields_for_copy_fields_in_variants = ["barcodes"] def set_default_fields(self): self.fields = [] - fields = frappe.get_meta('Item').fields - exclude_fields = {"naming_series", "item_code", "item_name", "published_in_website", - "standard_rate", "opening_stock", "image", "description", - "variant_of", "valuation_rate", "description", "barcodes", - "has_variants", "attributes"} + fields = frappe.get_meta("Item").fields + exclude_fields = { + "naming_series", + "item_code", + "item_name", + "published_in_website", + "standard_rate", + "opening_stock", + "image", + "description", + "variant_of", + "valuation_rate", + "description", + "barcodes", + "has_variants", + "attributes", + } for d in fields: - if not d.no_copy and d.fieldname not in exclude_fields and \ - d.fieldtype not in ['HTML', 'Section Break', 'Column Break', 'Button', 'Read Only']: - self.append('fields', { - 'field_name': d.fieldname - }) + if ( + not d.no_copy + and d.fieldname not in exclude_fields + and d.fieldtype not in ["HTML", "Section Break", "Column Break", "Button", "Read Only"] + ): + self.append("fields", {"field_name": d.fieldname}) def remove_invalid_fields_for_copy_fields_in_variants(self): - fields = [row for row in self.fields if row.field_name not in self.invalid_fields_for_copy_fields_in_variants] + fields = [ + row + for row in self.fields + if row.field_name not in self.invalid_fields_for_copy_fields_in_variants + ] self.fields = fields self.save() def validate(self): for d in self.fields: if d.field_name in self.invalid_fields_for_copy_fields_in_variants: - frappe.throw(_('Cannot set the field {0} for copying in variants').format(d.field_name)) + frappe.throw(_("Cannot set the field {0} for copying in variants").format(d.field_name)) 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 7aff95d1e8..b3af309359 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -19,13 +19,19 @@ class LandedCostVoucher(Document): self.set("items", []) for pr in self.get("purchase_receipts"): if pr.receipt_document_type and pr.receipt_document: - pr_items = frappe.db.sql("""select pr_item.item_code, pr_item.description, + pr_items = frappe.db.sql( + """select pr_item.item_code, pr_item.description, pr_item.qty, pr_item.base_rate, pr_item.base_amount, pr_item.name, pr_item.cost_center, pr_item.is_fixed_asset from `tab{doctype} Item` pr_item where parent = %s and exists(select name from tabItem where name = pr_item.item_code and (is_stock_item = 1 or is_fixed_asset=1)) - """.format(doctype=pr.receipt_document_type), pr.receipt_document, as_dict=True) + """.format( + doctype=pr.receipt_document_type + ), + pr.receipt_document, + as_dict=True, + ) for d in pr_items: item = self.append("items") @@ -33,8 +39,7 @@ class LandedCostVoucher(Document): item.description = d.description item.qty = d.qty item.rate = d.base_rate - item.cost_center = d.cost_center or \ - erpnext.get_default_cost_center(self.company) + item.cost_center = d.cost_center or erpnext.get_default_cost_center(self.company) item.amount = d.base_amount item.receipt_document_type = pr.receipt_document_type item.receipt_document = pr.receipt_document @@ -52,26 +57,30 @@ class LandedCostVoucher(Document): self.set_applicable_charges_on_item() self.validate_applicable_charges_for_item() - def check_mandatory(self): if not self.get("purchase_receipts"): frappe.throw(_("Please enter Receipt Document")) - def validate_receipt_documents(self): receipt_documents = [] for d in self.get("purchase_receipts"): docstatus = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "docstatus") if docstatus != 1: - msg = f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted" + msg = ( + f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted" + ) frappe.throw(_(msg), title=_("Invalid Document")) if d.receipt_document_type == "Purchase Invoice": update_stock = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "update_stock") if not update_stock: - msg = _("Row {0}: Purchase Invoice {1} has no stock impact.").format(d.idx, frappe.bold(d.receipt_document)) - msg += "
    " + _("Please create Landed Cost Vouchers against Invoices that have 'Update Stock' enabled.") + msg = _("Row {0}: Purchase Invoice {1} has no stock impact.").format( + d.idx, frappe.bold(d.receipt_document) + ) + msg += "
    " + _( + "Please create Landed Cost Vouchers against Invoices that have 'Update Stock' enabled." + ) frappe.throw(msg, title=_("Incorrect Invoice")) receipt_documents.append(d.receipt_document) @@ -81,52 +90,64 @@ class LandedCostVoucher(Document): frappe.throw(_("Item must be added using 'Get Items from Purchase Receipts' button")) elif item.receipt_document not in receipt_documents: - frappe.throw(_("Item Row {0}: {1} {2} does not exist in above '{1}' table") - .format(item.idx, item.receipt_document_type, item.receipt_document)) + frappe.throw( + _("Item Row {0}: {1} {2} does not exist in above '{1}' table").format( + item.idx, item.receipt_document_type, item.receipt_document + ) + ) if not item.cost_center: - frappe.throw(_("Row {0}: Cost center is required for an item {1}") - .format(item.idx, item.item_code)) + frappe.throw( + _("Row {0}: Cost center is required for an item {1}").format(item.idx, item.item_code) + ) def set_total_taxes_and_charges(self): self.total_taxes_and_charges = sum(flt(d.base_amount) for d in self.get("taxes")) def set_applicable_charges_on_item(self): - if self.get('taxes') and self.distribute_charges_based_on != 'Distribute Manually': + if self.get("taxes") and self.distribute_charges_based_on != "Distribute Manually": total_item_cost = 0.0 total_charges = 0.0 item_count = 0 based_on_field = frappe.scrub(self.distribute_charges_based_on) - for item in self.get('items'): + for item in self.get("items"): total_item_cost += item.get(based_on_field) - for item in self.get('items'): - item.applicable_charges = flt(flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)), - item.precision('applicable_charges')) + for item in self.get("items"): + item.applicable_charges = flt( + flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)), + item.precision("applicable_charges"), + ) total_charges += item.applicable_charges item_count += 1 if total_charges != self.total_taxes_and_charges: diff = self.total_taxes_and_charges - total_charges - self.get('items')[item_count - 1].applicable_charges += diff + self.get("items")[item_count - 1].applicable_charges += diff def validate_applicable_charges_for_item(self): based_on = self.distribute_charges_based_on.lower() - if based_on != 'distribute manually': + if based_on != "distribute manually": total = sum(flt(d.get(based_on)) for d in self.get("items")) else: # consider for proportion while distributing manually - total = sum(flt(d.get('applicable_charges')) for d in self.get("items")) + total = sum(flt(d.get("applicable_charges")) for d in self.get("items")) if not total: - frappe.throw(_("Total {0} for all items is zero, may be you should change 'Distribute Charges Based On'").format(based_on)) + frappe.throw( + _( + "Total {0} for all items is zero, may be you should change 'Distribute Charges Based On'" + ).format(based_on) + ) total_applicable_charges = sum(flt(d.applicable_charges) for d in self.get("items")) - precision = get_field_precision(frappe.get_meta("Landed Cost Item").get_field("applicable_charges"), - currency=frappe.get_cached_value('Company', self.company, "default_currency")) + precision = get_field_precision( + frappe.get_meta("Landed Cost Item").get_field("applicable_charges"), + currency=frappe.get_cached_value("Company", self.company, "default_currency"), + ) diff = flt(self.total_taxes_and_charges) - flt(total_applicable_charges) diff = flt(diff, precision) @@ -134,7 +155,11 @@ class LandedCostVoucher(Document): if abs(diff) < (2.0 / (10**precision)): self.items[-1].applicable_charges += diff else: - frappe.throw(_("Total Applicable Charges in Purchase Receipt Items table must be same as Total Taxes and Charges")) + frappe.throw( + _( + "Total Applicable Charges in Purchase Receipt Items table must be same as Total Taxes and Charges" + ) + ) def on_submit(self): self.update_landed_cost() @@ -177,25 +202,41 @@ class LandedCostVoucher(Document): doc.repost_future_sle_and_gle() def validate_asset_qty_and_status(self, receipt_document_type, receipt_document): - for item in self.get('items'): + for item in self.get("items"): if item.is_fixed_asset: - receipt_document_type = 'purchase_invoice' if item.receipt_document_type == 'Purchase Invoice' \ - else 'purchase_receipt' - docs = frappe.db.get_all('Asset', filters={ receipt_document_type: item.receipt_document, - 'item_code': item.item_code }, fields=['name', 'docstatus']) + receipt_document_type = ( + "purchase_invoice" if item.receipt_document_type == "Purchase Invoice" else "purchase_receipt" + ) + 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}. Please create or link {1} Assets with respective document." + ).format(item.receipt_document, item.qty) + ) if docs: for d in docs: if d.docstatus == 1: - frappe.throw(_('{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( + _( + "{2} {0} has submitted Assets. Remove Item {1} from table to continue." + ).format( + item.receipt_document, item.item_code, item.receipt_document_type + ) + ) def update_rate_in_serial_no_for_non_asset_items(self, receipt_document): for item in receipt_document.get("items"): if not item.is_fixed_asset and item.serial_no: serial_nos = get_serial_nos(item.serial_no) if serial_nos: - frappe.db.sql("update `tabSerial No` set purchase_rate=%s where name in ({0})" - .format(", ".join(["%s"]*len(serial_nos))), tuple([item.valuation_rate] + serial_nos)) + frappe.db.sql( + "update `tabSerial No` set purchase_rate=%s where name in ({0})".format( + ", ".join(["%s"] * len(serial_nos)) + ), + tuple([item.valuation_rate] + serial_nos), + ) 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 6dc4fee569..1af9953451 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 @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt - import frappe from frappe.tests.utils import FrappeTestCase from frappe.utils import add_to_date, flt, now @@ -22,34 +21,50 @@ class TestLandedCostVoucher(FrappeTestCase): def test_landed_cost_voucher(self): frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", - get_multiple_items = True, get_taxes_and_charges = True) + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", + get_multiple_items=True, + get_taxes_and_charges=True, + ) - last_sle = frappe.db.get_value("Stock Ledger Entry", { + last_sle = frappe.db.get_value( + "Stock Ledger Entry", + { "voucher_type": pr.doctype, "voucher_no": pr.name, "item_code": "_Test Item", "warehouse": "Stores - TCP1", "is_cancelled": 0, }, - fieldname=["qty_after_transaction", "stock_value"], as_dict=1) + fieldname=["qty_after_transaction", "stock_value"], + as_dict=1, + ) 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") + pr_lc_value = frappe.db.get_value( + "Purchase Receipt Item", {"parent": pr.name}, "landed_cost_voucher_amount" + ) self.assertEqual(pr_lc_value, 25.0) - last_sle_after_landed_cost = frappe.db.get_value("Stock Ledger Entry", { + last_sle_after_landed_cost = frappe.db.get_value( + "Stock Ledger Entry", + { "voucher_type": pr.doctype, "voucher_no": pr.name, "item_code": "_Test Item", "warehouse": "Stores - TCP1", "is_cancelled": 0, }, - fieldname=["qty_after_transaction", "stock_value"], as_dict=1) + fieldname=["qty_after_transaction", "stock_value"], + as_dict=1, + ) - self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction) + self.assertEqual( + last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction + ) self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 25.0) # assert after submit @@ -57,24 +72,20 @@ class TestLandedCostVoucher(FrappeTestCase): # Mess up cancelled SLE modified timestamp to check # if they aren't effective in any business logic. - frappe.db.set_value("Stock Ledger Entry", - { - "is_cancelled": 1, - "voucher_type": pr.doctype, - "voucher_no": pr.name - }, - "is_cancelled", 1, - modified=add_to_date(now(), hours=1, as_datetime=True, as_string=True) + frappe.db.set_value( + "Stock Ledger Entry", + {"is_cancelled": 1, "voucher_type": pr.doctype, "voucher_no": pr.name}, + "is_cancelled", + 1, + modified=add_to_date(now(), hours=1, as_datetime=True, as_string=True), ) items, warehouses = pr.get_items_and_warehouses() - update_gl_entries_after(pr.posting_date, pr.posting_time, - warehouses, items, company=pr.company) + update_gl_entries_after(pr.posting_date, pr.posting_time, warehouses, items, company=pr.company) # reassert after reposting self.assertPurchaseReceiptLCVGLEntries(pr) - def assertPurchaseReceiptLCVGLEntries(self, pr): gl_entries = get_gl_entries("Purchase Receipt", pr.name) @@ -90,54 +101,74 @@ class TestLandedCostVoucher(FrappeTestCase): "Stock Received But Not Billed - TCP1": [0.0, 500.0], "Expenses Included In Valuation - TCP1": [0.0, 50.0], "_Test Account Customs Duty - TCP1": [0.0, 150], - "_Test Account Shipping Charges - TCP1": [0.0, 100.00] + "_Test Account Shipping Charges - TCP1": [0.0, 100.00], } else: expected_values = { stock_in_hand_account: [400.0, 0.0], fixed_asset_account: [400.0, 0.0], "Stock Received But Not Billed - TCP1": [0.0, 500.0], - "Expenses Included In Valuation - TCP1": [0.0, 300.0] + "Expenses Included In Valuation - TCP1": [0.0, 300.0], } for gle in gl_entries: - if not gle.get('is_cancelled'): - self.assertEqual(expected_values[gle.account][0], gle.debit, msg=f"incorrect debit for {gle.account}") - self.assertEqual(expected_values[gle.account][1], gle.credit, msg=f"incorrect credit for {gle.account}") - + if not gle.get("is_cancelled"): + self.assertEqual( + expected_values[gle.account][0], gle.debit, msg=f"incorrect debit for {gle.account}" + ) + self.assertEqual( + expected_values[gle.account][1], gle.credit, msg=f"incorrect credit for {gle.account}" + ) def test_landed_cost_voucher_against_purchase_invoice(self): - pi = make_purchase_invoice(update_stock=1, posting_date=frappe.utils.nowdate(), - posting_time=frappe.utils.nowtime(), cash_bank_account="Cash - TCP1", - 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") + pi = make_purchase_invoice( + update_stock=1, + posting_date=frappe.utils.nowdate(), + posting_time=frappe.utils.nowtime(), + cash_bank_account="Cash - TCP1", + 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", + ) - last_sle = frappe.db.get_value("Stock Ledger Entry", { + last_sle = frappe.db.get_value( + "Stock Ledger Entry", + { "voucher_type": pi.doctype, "voucher_no": pi.name, "item_code": "_Test Item", - "warehouse": "Stores - TCP1" + "warehouse": "Stores - TCP1", }, - fieldname=["qty_after_transaction", "stock_value"], as_dict=1) + fieldname=["qty_after_transaction", "stock_value"], + as_dict=1, + ) 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") + pi_lc_value = frappe.db.get_value( + "Purchase Invoice Item", {"parent": pi.name}, "landed_cost_voucher_amount" + ) self.assertEqual(pi_lc_value, 50.0) - last_sle_after_landed_cost = frappe.db.get_value("Stock Ledger Entry", { + last_sle_after_landed_cost = frappe.db.get_value( + "Stock Ledger Entry", + { "voucher_type": pi.doctype, "voucher_no": pi.name, "item_code": "_Test Item", - "warehouse": "Stores - TCP1" + "warehouse": "Stores - TCP1", }, - fieldname=["qty_after_transaction", "stock_value"], as_dict=1) + fieldname=["qty_after_transaction", "stock_value"], + as_dict=1, + ) - self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction) + self.assertEqual( + last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction + ) self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0) @@ -149,20 +180,26 @@ class TestLandedCostVoucher(FrappeTestCase): expected_values = { stock_in_hand_account: [300.0, 0.0], "Creditors - TCP1": [0.0, 250.0], - "Expenses Included In Valuation - TCP1": [0.0, 50.0] + "Expenses Included In Valuation - TCP1": [0.0, 50.0], } for gle in gl_entries: - if not gle.get('is_cancelled'): + if not gle.get("is_cancelled"): self.assertEqual(expected_values[gle.account][0], gle.debit) self.assertEqual(expected_values[gle.account][1], gle.credit) - def test_landed_cost_voucher_for_serialized_item(self): - frappe.db.sql("delete from `tabSerial No` where name in ('SN001', 'SN002', 'SN003', 'SN004', 'SN005')") - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1", get_multiple_items = True, - get_taxes_and_charges = True, do_not_submit = True) + frappe.db.sql( + "delete from `tabSerial No` where name in ('SN001', 'SN002', 'SN003', 'SN004', 'SN005')" + ) + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", + get_multiple_items=True, + get_taxes_and_charges=True, + do_not_submit=True, + ) pr.items[0].item_code = "_Test Serialized Item" pr.items[0].serial_no = "SN001\nSN002\nSN003\nSN004\nSN005" @@ -172,8 +209,7 @@ class TestLandedCostVoucher(FrappeTestCase): 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) + serial_no = frappe.db.get_value("Serial No", "SN001", ["warehouse", "purchase_rate"], as_dict=1) self.assertEqual(serial_no.purchase_rate - serial_no_rate, 5.0) self.assertEqual(serial_no.warehouse, "Stores - TCP1") @@ -183,60 +219,82 @@ class TestLandedCostVoucher(FrappeTestCase): landed costs, this should be allowed for serial nos too. Case: - - receipt a serial no @ X rate - - delivery the serial no @ X rate - - add LCV to receipt X + Y - - LCV should be successful - - delivery should reflect X+Y valuation. + - receipt a serial no @ X rate + - delivery the serial no @ X rate + - add LCV to receipt X + Y + - LCV should be successful + - delivery should reflect X+Y valuation. """ serial_no = "LCV_TEST_SR_NO" item_code = "_Test Serialized Item" warehouse = "Stores - TCP1" - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", - warehouse=warehouse, qty=1, rate=200, - item_code=item_code, serial_no=serial_no) + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse=warehouse, + qty=1, + rate=200, + item_code=item_code, + serial_no=serial_no, + ) serial_no_rate = frappe.db.get_value("Serial No", serial_no, "purchase_rate") # deliver it before creating LCV - dn = create_delivery_note(item_code=item_code, - company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', - serial_no=serial_no, qty=1, rate=500, - cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1") + dn = create_delivery_note( + item_code=item_code, + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + serial_no=serial_no, + qty=1, + rate=500, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + ) charges = 10 create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges) new_purchase_rate = serial_no_rate + charges - serial_no = frappe.db.get_value("Serial No", serial_no, - ["warehouse", "purchase_rate"], as_dict=1) + serial_no = frappe.db.get_value( + "Serial No", serial_no, ["warehouse", "purchase_rate"], as_dict=1 + ) self.assertEqual(serial_no.purchase_rate, new_purchase_rate) - stock_value_difference = frappe.db.get_value("Stock Ledger Entry", - filters={ - "voucher_no": dn.name, - "voucher_type": dn.doctype, - "is_cancelled": 0 # LCV cancels with same name. - }, - fieldname="stock_value_difference") + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + filters={ + "voucher_no": dn.name, + "voucher_type": dn.doctype, + "is_cancelled": 0, # LCV cancels with same name. + }, + fieldname="stock_value_difference", + ) # reposting should update the purchase rate in future delivery self.assertEqual(stock_value_difference, -new_purchase_rate) - def test_landed_cost_voucher_for_odd_numbers (self): - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", do_not_save=True) + def test_landed_cost_voucher_for_odd_numbers(self): + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", + do_not_save=True, + ) pr.items[0].cost_center = "Main - TCP1" for x in range(2): - pr.append("items", { - "item_code": "_Test Item", - "warehouse": "Stores - TCP1", - "cost_center": "Main - TCP1", - "qty": 5, - "rate": 50 - }) + pr.append( + "items", + { + "item_code": "_Test Item", + "warehouse": "Stores - TCP1", + "cost_center": "Main - TCP1", + "qty": 5, + "rate": 50, + }, + ) pr.submit() lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, 123.22) @@ -245,37 +303,50 @@ class TestLandedCostVoucher(FrappeTestCase): self.assertEqual(flt(lcv.items[2].applicable_charges, 2), 41.08) def test_multiple_landed_cost_voucher_against_pr(self): - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", - supplier_warehouse = "Stores - TCP1", do_not_save=True) + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Stores - TCP1", + do_not_save=True, + ) - pr.append("items", { - "item_code": "_Test Item", - "warehouse": "Stores - TCP1", - "cost_center": "Main - TCP1", - "qty": 5, - "rate": 100 - }) + pr.append( + "items", + { + "item_code": "_Test Item", + "warehouse": "Stores - TCP1", + "cost_center": "Main - TCP1", + "qty": 5, + "rate": 100, + }, + ) pr.submit() - lcv1 = make_landed_cost_voucher(company = pr.company, receipt_document_type = 'Purchase Receipt', - receipt_document=pr.name, charges=100, do_not_save=True) + lcv1 = make_landed_cost_voucher( + company=pr.company, + receipt_document_type="Purchase Receipt", + receipt_document=pr.name, + charges=100, + do_not_save=True, + ) lcv1.insert() - lcv1.set('items', [ - lcv1.get('items')[0] - ]) + lcv1.set("items", [lcv1.get("items")[0]]) distribute_landed_cost_on_items(lcv1) lcv1.submit() - lcv2 = make_landed_cost_voucher(company = pr.company, receipt_document_type = 'Purchase Receipt', - receipt_document=pr.name, charges=100, do_not_save=True) + lcv2 = make_landed_cost_voucher( + company=pr.company, + receipt_document_type="Purchase Receipt", + receipt_document=pr.name, + charges=100, + do_not_save=True, + ) lcv2.insert() - lcv2.set('items', [ - lcv2.get('items')[1] - ]) + lcv2.set("items", [lcv2.get("items")[1]]) distribute_landed_cost_on_items(lcv2) lcv2.submit() @@ -294,22 +365,31 @@ class TestLandedCostVoucher(FrappeTestCase): save_new_records(test_records) ## Create USD Shipping charges_account - usd_shipping = create_account(account_name="Shipping Charges USD", - parent_account="Duties and Taxes - TCP1", company="_Test Company with perpetual inventory", - account_currency="USD") + usd_shipping = create_account( + account_name="Shipping Charges USD", + parent_account="Duties and Taxes - TCP1", + company="_Test Company with perpetual inventory", + account_currency="USD", + ) - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", - supplier_warehouse = "Stores - TCP1") + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Stores - TCP1", + ) pr.submit() - lcv = make_landed_cost_voucher(company = pr.company, receipt_document_type = "Purchase Receipt", - receipt_document=pr.name, charges=100, do_not_save=True) + lcv = make_landed_cost_voucher( + company=pr.company, + receipt_document_type="Purchase Receipt", + receipt_document=pr.name, + charges=100, + do_not_save=True, + ) - lcv.append("taxes", { - "description": "Shipping Charges", - "expense_account": usd_shipping, - "amount": 10 - }) + lcv.append( + "taxes", {"description": "Shipping Charges", "expense_account": usd_shipping, "amount": 10} + ) lcv.save() lcv.submit() @@ -319,12 +399,18 @@ class TestLandedCostVoucher(FrappeTestCase): self.assertEqual(lcv.total_taxes_and_charges, 729) self.assertEqual(pr.items[0].landed_cost_voucher_amount, 729) - gl_entries = frappe.get_all("GL Entry", fields=["account", "credit", "credit_in_account_currency"], - filters={"voucher_no": pr.name, "account": ("in", ["Shipping Charges USD - TCP1", "Expenses Included In Valuation - TCP1"])}) + gl_entries = frappe.get_all( + "GL Entry", + fields=["account", "credit", "credit_in_account_currency"], + filters={ + "voucher_no": pr.name, + "account": ("in", ["Shipping Charges USD - TCP1", "Expenses Included In Valuation - TCP1"]), + }, + ) expected_gl_entries = { "Shipping Charges USD - TCP1": [629, 10], - "Expenses Included In Valuation - TCP1": [100, 100] + "Expenses Included In Valuation - TCP1": [100, 100], } for entry in gl_entries: @@ -334,7 +420,9 @@ class TestLandedCostVoucher(FrappeTestCase): def test_asset_lcv(self): "Check if LCV for an Asset updates the Assets Gross Purchase Amount correctly." - frappe.db.set_value("Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC") + frappe.db.set_value( + "Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC" + ) if not frappe.db.exists("Asset Category", "Computers"): create_asset_category() @@ -345,15 +433,16 @@ class TestLandedCostVoucher(FrappeTestCase): pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=50000) # check if draft asset was created - assets = frappe.db.get_all('Asset', filters={'purchase_receipt': pr.name}) + assets = frappe.db.get_all("Asset", filters={"purchase_receipt": pr.name}) self.assertEqual(len(assets), 1) lcv = make_landed_cost_voucher( - company = pr.company, - receipt_document_type = "Purchase Receipt", + company=pr.company, + receipt_document_type="Purchase Receipt", receipt_document=pr.name, charges=80, - expense_account="Expenses Included In Valuation - _TC") + expense_account="Expenses Included In Valuation - _TC", + ) lcv.save() lcv.submit() @@ -365,27 +454,38 @@ class TestLandedCostVoucher(FrappeTestCase): lcv.cancel() pr.cancel() -def make_landed_cost_voucher(** args): + +def make_landed_cost_voucher(**args): args = frappe._dict(args) ref_doc = frappe.get_doc(args.receipt_document_type, args.receipt_document) - lcv = frappe.new_doc('Landed Cost Voucher') - lcv.company = args.company or '_Test Company' - lcv.distribute_charges_based_on = 'Amount' + lcv = frappe.new_doc("Landed Cost Voucher") + lcv.company = args.company or "_Test Company" + lcv.distribute_charges_based_on = "Amount" - lcv.set('purchase_receipts', [{ - "receipt_document_type": args.receipt_document_type, - "receipt_document": args.receipt_document, - "supplier": ref_doc.supplier, - "posting_date": ref_doc.posting_date, - "grand_total": ref_doc.grand_total - }]) + lcv.set( + "purchase_receipts", + [ + { + "receipt_document_type": args.receipt_document_type, + "receipt_document": args.receipt_document, + "supplier": ref_doc.supplier, + "posting_date": ref_doc.posting_date, + "grand_total": ref_doc.grand_total, + } + ], + ) - lcv.set("taxes", [{ - "description": "Shipping Charges", - "expense_account": args.expense_account or "Expenses Included In Valuation - TCP1", - "amount": args.charges - }]) + lcv.set( + "taxes", + [ + { + "description": "Shipping Charges", + "expense_account": args.expense_account or "Expenses Included In Valuation - TCP1", + "amount": args.charges, + } + ], + ) if not args.do_not_save: lcv.insert() @@ -400,21 +500,31 @@ def create_landed_cost_voucher(receipt_document_type, receipt_document, company, lcv = frappe.new_doc("Landed Cost Voucher") lcv.company = company - lcv.distribute_charges_based_on = 'Amount' + lcv.distribute_charges_based_on = "Amount" - lcv.set("purchase_receipts", [{ - "receipt_document_type": receipt_document_type, - "receipt_document": receipt_document, - "supplier": ref_doc.supplier, - "posting_date": ref_doc.posting_date, - "grand_total": ref_doc.base_grand_total - }]) + lcv.set( + "purchase_receipts", + [ + { + "receipt_document_type": receipt_document_type, + "receipt_document": receipt_document, + "supplier": ref_doc.supplier, + "posting_date": ref_doc.posting_date, + "grand_total": ref_doc.base_grand_total, + } + ], + ) - lcv.set("taxes", [{ - "description": "Insurance Charges", - "expense_account": "Expenses Included In Valuation - TCP1", - "amount": charges - }]) + lcv.set( + "taxes", + [ + { + "description": "Insurance Charges", + "expense_account": "Expenses Included In Valuation - TCP1", + "amount": charges, + } + ], + ) lcv.insert() @@ -424,6 +534,7 @@ def create_landed_cost_voucher(receipt_document_type, receipt_document, company, return lcv + def distribute_landed_cost_on_items(lcv): based_on = lcv.distribute_charges_based_on.lower() total = sum(flt(d.get(based_on)) for d in lcv.get("items")) @@ -432,4 +543,5 @@ def distribute_landed_cost_on_items(lcv): item.applicable_charges = flt(item.get(based_on)) * flt(lcv.total_taxes_and_charges) / flt(total) item.applicable_charges = flt(item.applicable_charges, lcv.precision("applicable_charges", item)) -test_records = frappe.get_test_records('Landed Cost Voucher') + +test_records = frappe.get_test_records("Landed Cost Voucher") diff --git a/erpnext/stock/doctype/manufacturer/test_manufacturer.py b/erpnext/stock/doctype/manufacturer/test_manufacturer.py index 66323478c8..e176b28b85 100644 --- a/erpnext/stock/doctype/manufacturer/test_manufacturer.py +++ b/erpnext/stock/doctype/manufacturer/test_manufacturer.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Manufacturer') + class TestManufacturer(unittest.TestCase): pass diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 49fefae550..4524914f5c 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -18,9 +18,8 @@ from erpnext.manufacturing.doctype.work_order.work_order import get_item_details from erpnext.stock.doctype.item.item import get_item_defaults from erpnext.stock.stock_balance import get_indented_qty, update_bin_qty -form_grid_templates = { - "items": "templates/form_grid/material_request_grid.html" -} +form_grid_templates = {"items": "templates/form_grid/material_request_grid.html"} + class MaterialRequest(BuyingController): def get_feed(self): @@ -30,8 +29,8 @@ class MaterialRequest(BuyingController): pass def validate_qty_against_so(self): - so_items = {} # Format --> {'SO/00001': {'Item/001': 120, 'Item/002': 24}} - for d in self.get('items'): + so_items = {} # Format --> {'SO/00001': {'Item/001': 120, 'Item/002': 24}} + for d in self.get("items"): if d.sales_order: if not d.sales_order in so_items: so_items[d.sales_order] = {d.item_code: flt(d.qty)} @@ -43,24 +42,34 @@ class MaterialRequest(BuyingController): for so_no in so_items.keys(): for item in so_items[so_no].keys(): - already_indented = frappe.db.sql("""select sum(qty) + already_indented = frappe.db.sql( + """select sum(qty) from `tabMaterial Request Item` where item_code = %s and sales_order = %s and - docstatus = 1 and parent != %s""", (item, so_no, self.name)) + docstatus = 1 and parent != %s""", + (item, so_no, self.name), + ) already_indented = already_indented and flt(already_indented[0][0]) or 0 - actual_so_qty = frappe.db.sql("""select sum(stock_qty) from `tabSales Order Item` - where parent = %s and item_code = %s and docstatus = 1""", (so_no, item)) + actual_so_qty = frappe.db.sql( + """select sum(stock_qty) from `tabSales Order Item` + where parent = %s and item_code = %s and docstatus = 1""", + (so_no, item), + ) actual_so_qty = actual_so_qty and flt(actual_so_qty[0][0]) or 0 if actual_so_qty and (flt(so_items[so_no][item]) + already_indented > actual_so_qty): - frappe.throw(_("Material Request of maximum {0} can be made for Item {1} against Sales Order {2}").format(actual_so_qty - already_indented, item, so_no)) + frappe.throw( + _("Material Request of maximum {0} can be made for Item {1} against Sales Order {2}").format( + actual_so_qty - already_indented, item, so_no + ) + ) def validate(self): super(MaterialRequest, self).validate() self.validate_schedule_date() - self.check_for_on_hold_or_closed_status('Sales Order', 'sales_order') + self.check_for_on_hold_or_closed_status("Sales Order", "sales_order") self.validate_uom_is_integer("uom", "qty") self.validate_material_request_type() @@ -68,9 +77,22 @@ class MaterialRequest(BuyingController): self.status = "Draft" from erpnext.controllers.status_updater import validate_status - validate_status(self.status, - ["Draft", "Submitted", "Stopped", "Cancelled", "Pending", - "Partially Ordered", "Ordered", "Issued", "Transferred", "Received"]) + + validate_status( + self.status, + [ + "Draft", + "Submitted", + "Stopped", + "Cancelled", + "Pending", + "Partially Ordered", + "Ordered", + "Issued", + "Transferred", + "Received", + ], + ) validate_for_items(self) @@ -86,22 +108,22 @@ class MaterialRequest(BuyingController): self.validate_schedule_date() def validate_material_request_type(self): - """ Validate fields in accordance with selected type """ + """Validate fields in accordance with selected type""" if self.material_request_type != "Customer Provided": self.customer = None def set_title(self): - '''Set title as comma separated list of items''' + """Set title as comma separated list of items""" if not self.title: - items = ', '.join([d.item_name for d in self.items][:3]) - self.title = _('{0} Request for {1}').format(self.material_request_type, items)[:100] + items = ", ".join([d.item_name for d in self.items][:3]) + self.title = _("{0} Request for {1}").format(self.material_request_type, items)[:100] def on_submit(self): # frappe.db.set(self, 'status', 'Submitted') self.update_requested_qty() self.update_requested_qty_in_production_plan() - if self.material_request_type == 'Purchase': + if self.material_request_type == "Purchase": self.validate_budget() def before_save(self): @@ -114,13 +136,15 @@ class MaterialRequest(BuyingController): # if MRQ is already closed, no point saving the document check_on_hold_or_closed_status(self.doctype, self.name) - self.set_status(update=True, status='Cancelled') + self.set_status(update=True, status="Cancelled") def check_modified_date(self): - mod_db = frappe.db.sql("""select modified from `tabMaterial Request` where name = %s""", - self.name) - date_diff = frappe.db.sql("""select TIMEDIFF('%s', '%s')""" - % (mod_db[0][0], cstr(self.modified))) + mod_db = frappe.db.sql( + """select modified from `tabMaterial Request` where name = %s""", self.name + ) + date_diff = frappe.db.sql( + """select TIMEDIFF('%s', '%s')""" % (mod_db[0][0], cstr(self.modified)) + ) if date_diff and date_diff[0][0]: frappe.throw(_("{0} {1} has been modified. Please refresh.").format(_(self.doctype), self.name)) @@ -136,22 +160,24 @@ class MaterialRequest(BuyingController): validates that `status` is acceptable for the present controller status and throws an Exception if otherwise. """ - if self.status and self.status == 'Cancelled': + if self.status and self.status == "Cancelled": # cancelled documents cannot change if status != self.status: frappe.throw( - _("{0} {1} is cancelled so the action cannot be completed"). - format(_(self.doctype), self.name), - frappe.InvalidStatusError + _("{0} {1} is cancelled so the action cannot be completed").format( + _(self.doctype), self.name + ), + frappe.InvalidStatusError, ) - elif self.status and self.status == 'Draft': + elif self.status and self.status == "Draft": # draft document to pending only - if status != 'Pending': + if status != "Pending": frappe.throw( - _("{0} {1} has not been submitted so the action cannot be completed"). - format(_(self.doctype), self.name), - frappe.InvalidStatusError + _("{0} {1} has not been submitted so the action cannot be completed").format( + _(self.doctype), self.name + ), + frappe.InvalidStatusError, ) def on_cancel(self): @@ -168,67 +194,90 @@ class MaterialRequest(BuyingController): for d in self.get("items"): if d.name in mr_items: if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"): - d.ordered_qty = flt(frappe.db.sql("""select sum(transfer_qty) + d.ordered_qty = flt( + frappe.db.sql( + """select sum(transfer_qty) from `tabStock Entry Detail` where material_request = %s and material_request_item = %s and docstatus = 1""", - (self.name, d.name))[0][0]) - mr_qty_allowance = frappe.db.get_single_value('Stock Settings', 'mr_qty_allowance') + (self.name, d.name), + )[0][0] + ) + mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance") if mr_qty_allowance: - allowed_qty = d.qty + (d.qty * (mr_qty_allowance/100)) + allowed_qty = d.qty + (d.qty * (mr_qty_allowance / 100)) if d.ordered_qty and d.ordered_qty > allowed_qty: - frappe.throw(_("The total Issue / Transfer quantity {0} in Material Request {1} \ - cannot be greater than allowed requested quantity {2} for Item {3}").format(d.ordered_qty, d.parent, allowed_qty, d.item_code)) + frappe.throw( + _( + "The total Issue / Transfer quantity {0} in Material Request {1} \ + cannot be greater than allowed requested quantity {2} for Item {3}" + ).format(d.ordered_qty, d.parent, allowed_qty, d.item_code) + ) elif d.ordered_qty and d.ordered_qty > d.stock_qty: - frappe.throw(_("The total Issue / Transfer quantity {0} in Material Request {1} \ - cannot be greater than requested quantity {2} for Item {3}").format(d.ordered_qty, d.parent, d.qty, d.item_code)) + frappe.throw( + _( + "The total Issue / Transfer quantity {0} in Material Request {1} \ + cannot be greater than requested quantity {2} for Item {3}" + ).format(d.ordered_qty, d.parent, d.qty, d.item_code) + ) elif self.material_request_type == "Manufacture": - d.ordered_qty = flt(frappe.db.sql("""select sum(qty) + d.ordered_qty = flt( + frappe.db.sql( + """select sum(qty) from `tabWork Order` where material_request = %s and material_request_item = %s and docstatus = 1""", - (self.name, d.name))[0][0]) + (self.name, d.name), + )[0][0] + ) frappe.db.set_value(d.doctype, d.name, "ordered_qty", d.ordered_qty) - self._update_percent_field({ - "target_dt": "Material Request Item", - "target_parent_dt": self.doctype, - "target_parent_field": "per_ordered", - "target_ref_field": "stock_qty", - "target_field": "ordered_qty", - "name": self.name, - }, update_modified) + self._update_percent_field( + { + "target_dt": "Material Request Item", + "target_parent_dt": self.doctype, + "target_parent_field": "per_ordered", + "target_ref_field": "stock_qty", + "target_field": "ordered_qty", + "name": self.name, + }, + update_modified, + ) def update_requested_qty(self, mr_item_rows=None): """update requested qty (before ordered_qty is updated)""" item_wh_list = [] for d in self.get("items"): - if (not mr_item_rows or d.name in mr_item_rows) and [d.item_code, d.warehouse] not in item_wh_list \ - and d.warehouse and frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1 : + if ( + (not mr_item_rows or d.name in mr_item_rows) + and [d.item_code, d.warehouse] not in item_wh_list + and d.warehouse + and frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1 + ): item_wh_list.append([d.item_code, d.warehouse]) for item_code, warehouse in item_wh_list: - update_bin_qty(item_code, warehouse, { - "indented_qty": get_indented_qty(item_code, warehouse) - }) + update_bin_qty(item_code, warehouse, {"indented_qty": get_indented_qty(item_code, warehouse)}) def update_requested_qty_in_production_plan(self): production_plans = [] - for d in self.get('items'): + for d in self.get("items"): if d.production_plan and d.material_request_plan_item: qty = d.qty if self.docstatus == 1 else 0 - frappe.db.set_value('Material Request Plan Item', - d.material_request_plan_item, 'requested_qty', qty) + frappe.db.set_value( + "Material Request Plan Item", d.material_request_plan_item, "requested_qty", qty + ) if d.production_plan not in production_plans: production_plans.append(d.production_plan) for production_plan in production_plans: - doc = frappe.get_doc('Production Plan', production_plan) + doc = frappe.get_doc("Production Plan", production_plan) doc.set_status() - doc.db_set('status', doc.status) + doc.db_set("status", doc.status) + def update_completed_and_requested_qty(stock_entry, method): if stock_entry.doctype == "Stock Entry": @@ -243,43 +292,55 @@ def update_completed_and_requested_qty(stock_entry, method): mr_obj = frappe.get_doc("Material Request", mr) if mr_obj.status in ["Stopped", "Cancelled"]: - frappe.throw(_("{0} {1} is cancelled or stopped").format(_("Material Request"), mr), - frappe.InvalidStatusError) + frappe.throw( + _("{0} {1} is cancelled or stopped").format(_("Material Request"), mr), + frappe.InvalidStatusError, + ) mr_obj.update_completed_qty(mr_item_rows) mr_obj.update_requested_qty(mr_item_rows) + def set_missing_values(source, target_doc): - if target_doc.doctype == "Purchase Order" and getdate(target_doc.schedule_date) < getdate(nowdate()): + if target_doc.doctype == "Purchase Order" and getdate(target_doc.schedule_date) < getdate( + nowdate() + ): target_doc.schedule_date = None target_doc.run_method("set_missing_values") target_doc.run_method("calculate_taxes_and_totals") + def update_item(obj, target, source_parent): target.conversion_factor = obj.conversion_factor - target.qty = flt(flt(obj.stock_qty) - flt(obj.ordered_qty))/ target.conversion_factor - target.stock_qty = (target.qty * target.conversion_factor) + target.qty = flt(flt(obj.stock_qty) - flt(obj.ordered_qty)) / target.conversion_factor + target.stock_qty = target.qty * target.conversion_factor if getdate(target.schedule_date) < getdate(nowdate()): target.schedule_date = None + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context + list_context = get_list_context(context) - list_context.update({ - 'show_sidebar': True, - 'show_search': True, - 'no_breadcrumbs': True, - 'title': _('Material Request'), - }) + list_context.update( + { + "show_sidebar": True, + "show_search": True, + "no_breadcrumbs": True, + "title": _("Material Request"), + } + ) return list_context + @frappe.whitelist() def update_status(name, status): - material_request = frappe.get_doc('Material Request', name) - material_request.check_permission('write') + material_request = frappe.get_doc("Material Request", name) + material_request.check_permission("write") material_request.update_status(status) + @frappe.whitelist() def make_purchase_order(source_name, target_doc=None, args=None): if args is None: @@ -292,7 +353,7 @@ def make_purchase_order(source_name, target_doc=None, args=None): # items only for given default supplier supplier_items = [] for d in target_doc.items: - default_supplier = get_item_defaults(d.item_code, target_doc.company).get('default_supplier') + default_supplier = get_item_defaults(d.item_code, target_doc.company).get("default_supplier") if frappe.flags.args.default_supplier == default_supplier: supplier_items.append(d) target_doc.items = supplier_items @@ -300,58 +361,65 @@ def make_purchase_order(source_name, target_doc=None, args=None): set_missing_values(source, target_doc) def select_item(d): - filtered_items = args.get('filtered_children', []) + filtered_items = args.get("filtered_children", []) child_filter = d.name in filtered_items if filtered_items else True return d.ordered_qty < d.stock_qty and child_filter - doclist = get_mapped_doc("Material Request", source_name, { - "Material Request": { - "doctype": "Purchase Order", - "validation": { - "docstatus": ["=", 1], - "material_request_type": ["=", "Purchase"] - } + doclist = get_mapped_doc( + "Material Request", + source_name, + { + "Material Request": { + "doctype": "Purchase Order", + "validation": {"docstatus": ["=", 1], "material_request_type": ["=", "Purchase"]}, + }, + "Material Request Item": { + "doctype": "Purchase Order Item", + "field_map": [ + ["name", "material_request_item"], + ["parent", "material_request"], + ["uom", "stock_uom"], + ["uom", "uom"], + ["sales_order", "sales_order"], + ["sales_order_item", "sales_order_item"], + ], + "postprocess": update_item, + "condition": select_item, + }, }, - "Material Request Item": { - "doctype": "Purchase Order Item", - "field_map": [ - ["name", "material_request_item"], - ["parent", "material_request"], - ["uom", "stock_uom"], - ["uom", "uom"], - ["sales_order", "sales_order"], - ["sales_order_item", "sales_order_item"] - ], - "postprocess": update_item, - "condition": select_item - } - }, target_doc, postprocess) + target_doc, + postprocess, + ) return doclist + @frappe.whitelist() def make_request_for_quotation(source_name, target_doc=None): - doclist = get_mapped_doc("Material Request", source_name, { - "Material Request": { - "doctype": "Request for Quotation", - "validation": { - "docstatus": ["=", 1], - "material_request_type": ["=", "Purchase"] - } + doclist = get_mapped_doc( + "Material Request", + source_name, + { + "Material Request": { + "doctype": "Request for Quotation", + "validation": {"docstatus": ["=", 1], "material_request_type": ["=", "Purchase"]}, + }, + "Material Request Item": { + "doctype": "Request for Quotation Item", + "field_map": [ + ["name", "material_request_item"], + ["parent", "material_request"], + ["uom", "uom"], + ], + }, }, - "Material Request Item": { - "doctype": "Request for Quotation Item", - "field_map": [ - ["name", "material_request_item"], - ["parent", "material_request"], - ["uom", "uom"] - ] - } - }, target_doc) + target_doc, + ) return doclist + @frappe.whitelist() def make_purchase_order_based_on_supplier(source_name, target_doc=None, args=None): mr = source_name @@ -362,43 +430,59 @@ def make_purchase_order_based_on_supplier(source_name, target_doc=None, args=Non target_doc.supplier = args.get("supplier") if getdate(target_doc.schedule_date) < getdate(nowdate()): target_doc.schedule_date = None - target_doc.set("items", [d for d in target_doc.get("items") - if d.get("item_code") in supplier_items and d.get("qty") > 0]) + target_doc.set( + "items", + [ + d for d in target_doc.get("items") if d.get("item_code") in supplier_items and d.get("qty") > 0 + ], + ) set_missing_values(source, target_doc) - target_doc = get_mapped_doc("Material Request", mr, { - "Material Request": { - "doctype": "Purchase Order", + target_doc = get_mapped_doc( + "Material Request", + mr, + { + "Material Request": { + "doctype": "Purchase Order", + }, + "Material Request Item": { + "doctype": "Purchase Order Item", + "field_map": [ + ["name", "material_request_item"], + ["parent", "material_request"], + ["uom", "stock_uom"], + ["uom", "uom"], + ], + "postprocess": update_item, + "condition": lambda doc: doc.ordered_qty < doc.qty, + }, }, - "Material Request Item": { - "doctype": "Purchase Order Item", - "field_map": [ - ["name", "material_request_item"], - ["parent", "material_request"], - ["uom", "stock_uom"], - ["uom", "uom"] - ], - "postprocess": update_item, - "condition": lambda doc: doc.ordered_qty < doc.qty - } - }, target_doc, postprocess) + target_doc, + postprocess, + ) return target_doc + @frappe.whitelist() def get_items_based_on_default_supplier(supplier): - supplier_items = [d.parent for d in frappe.db.get_all("Item Default", - {"default_supplier": supplier, "parenttype": "Item"}, 'parent')] + supplier_items = [ + d.parent + for d in frappe.db.get_all( + "Item Default", {"default_supplier": supplier, "parenttype": "Item"}, "parent" + ) + ] return supplier_items + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, page_len, filters): conditions = "" if txt: - conditions += "and mr.name like '%%"+txt+"%%' " + conditions += "and mr.name like '%%" + txt + "%%' " if filters.get("transaction_date"): date = filters.get("transaction_date")[1] @@ -410,7 +494,8 @@ def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, pa if not supplier_items: frappe.throw(_("{0} is not the default supplier for any items.").format(supplier)) - material_requests = frappe.db.sql("""select distinct mr.name, transaction_date,company + material_requests = frappe.db.sql( + """select distinct mr.name, transaction_date,company from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item where mr.name = mr_item.parent and mr_item.item_code in ({0}) @@ -421,12 +506,16 @@ def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, pa and mr.company = '{1}' {2} order by mr_item.item_code ASC - limit {3} offset {4} """ \ - .format(', '.join(['%s']*len(supplier_items)), filters.get("company"), conditions, page_len, start), - tuple(supplier_items), as_dict=1) + limit {3} offset {4} """.format( + ", ".join(["%s"] * len(supplier_items)), filters.get("company"), conditions, page_len, start + ), + tuple(supplier_items), + as_dict=1, + ) return material_requests + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_default_supplier_query(doctype, txt, searchfield, start, page_len, filters): @@ -435,47 +524,63 @@ def get_default_supplier_query(doctype, txt, searchfield, start, page_len, filte for d in doc.items: item_list.append(d.item_code) - return frappe.db.sql("""select default_supplier + return frappe.db.sql( + """select default_supplier from `tabItem Default` where parent in ({0}) and default_supplier IS NOT NULL - """.format(', '.join(['%s']*len(item_list))),tuple(item_list)) + """.format( + ", ".join(["%s"] * len(item_list)) + ), + tuple(item_list), + ) + @frappe.whitelist() def make_supplier_quotation(source_name, target_doc=None): def postprocess(source, target_doc): set_missing_values(source, target_doc) - doclist = get_mapped_doc("Material Request", source_name, { - "Material Request": { - "doctype": "Supplier Quotation", - "validation": { - "docstatus": ["=", 1], - "material_request_type": ["=", "Purchase"] - } + doclist = get_mapped_doc( + "Material Request", + source_name, + { + "Material Request": { + "doctype": "Supplier Quotation", + "validation": {"docstatus": ["=", 1], "material_request_type": ["=", "Purchase"]}, + }, + "Material Request Item": { + "doctype": "Supplier Quotation Item", + "field_map": { + "name": "material_request_item", + "parent": "material_request", + "sales_order": "sales_order", + }, + }, }, - "Material Request Item": { - "doctype": "Supplier Quotation Item", - "field_map": { - "name": "material_request_item", - "parent": "material_request", - "sales_order": "sales_order" - } - } - }, target_doc, postprocess) + target_doc, + postprocess, + ) return doclist + @frappe.whitelist() def make_stock_entry(source_name, target_doc=None): def update_item(obj, target, source_parent): - qty = flt(flt(obj.stock_qty) - flt(obj.ordered_qty))/ target.conversion_factor \ - if flt(obj.stock_qty) > flt(obj.ordered_qty) else 0 + qty = ( + flt(flt(obj.stock_qty) - flt(obj.ordered_qty)) / target.conversion_factor + if flt(obj.stock_qty) > flt(obj.ordered_qty) + else 0 + ) target.qty = qty target.transfer_qty = qty * obj.conversion_factor target.conversion_factor = obj.conversion_factor - if source_parent.material_request_type == "Material Transfer" or source_parent.material_request_type == "Customer Provided": + if ( + source_parent.material_request_type == "Material Transfer" + or source_parent.material_request_type == "Customer Provided" + ): target.t_warehouse = obj.warehouse else: target.s_warehouse = obj.warehouse @@ -489,7 +594,7 @@ def make_stock_entry(source_name, target_doc=None): def set_missing_values(source, target): target.purpose = source.material_request_type if source.job_card: - target.purpose = 'Material Transfer for Manufacture' + target.purpose = "Material Transfer for Manufacture" if source.material_request_type == "Customer Provided": target.purpose = "Material Receipt" @@ -498,101 +603,119 @@ def make_stock_entry(source_name, target_doc=None): target.set_stock_entry_type() target.set_job_card_data() - doclist = get_mapped_doc("Material Request", source_name, { - "Material Request": { - "doctype": "Stock Entry", - "validation": { - "docstatus": ["=", 1], - "material_request_type": ["in", ["Material Transfer", "Material Issue", "Customer Provided"]] - } - }, - "Material Request Item": { - "doctype": "Stock Entry Detail", - "field_map": { - "name": "material_request_item", - "parent": "material_request", - "uom": "stock_uom", - "job_card_item": "job_card_item" + doclist = get_mapped_doc( + "Material Request", + source_name, + { + "Material Request": { + "doctype": "Stock Entry", + "validation": { + "docstatus": ["=", 1], + "material_request_type": ["in", ["Material Transfer", "Material Issue", "Customer Provided"]], + }, }, - "postprocess": update_item, - "condition": lambda doc: doc.ordered_qty < doc.stock_qty - } - }, target_doc, set_missing_values) + "Material Request Item": { + "doctype": "Stock Entry Detail", + "field_map": { + "name": "material_request_item", + "parent": "material_request", + "uom": "stock_uom", + "job_card_item": "job_card_item", + }, + "postprocess": update_item, + "condition": lambda doc: doc.ordered_qty < doc.stock_qty, + }, + }, + target_doc, + set_missing_values, + ) return doclist + @frappe.whitelist() def raise_work_orders(material_request): - mr= frappe.get_doc("Material Request", material_request) - errors =[] + mr = frappe.get_doc("Material Request", material_request) + errors = [] work_orders = [] - default_wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_wip_warehouse") + default_wip_warehouse = frappe.db.get_single_value( + "Manufacturing Settings", "default_wip_warehouse" + ) for d in mr.items: if (d.stock_qty - d.ordered_qty) > 0: if frappe.db.exists("BOM", {"item": d.item_code, "is_default": 1}): wo_order = frappe.new_doc("Work Order") - wo_order.update({ - "production_item": d.item_code, - "qty": d.stock_qty - d.ordered_qty, - "fg_warehouse": d.warehouse, - "wip_warehouse": default_wip_warehouse, - "description": d.description, - "stock_uom": d.stock_uom, - "expected_delivery_date": d.schedule_date, - "sales_order": d.sales_order, - "sales_order_item": d.get("sales_order_item"), - "bom_no": get_item_details(d.item_code).bom_no, - "material_request": mr.name, - "material_request_item": d.name, - "planned_start_date": mr.transaction_date, - "company": mr.company - }) + wo_order.update( + { + "production_item": d.item_code, + "qty": d.stock_qty - d.ordered_qty, + "fg_warehouse": d.warehouse, + "wip_warehouse": default_wip_warehouse, + "description": d.description, + "stock_uom": d.stock_uom, + "expected_delivery_date": d.schedule_date, + "sales_order": d.sales_order, + "sales_order_item": d.get("sales_order_item"), + "bom_no": get_item_details(d.item_code).bom_no, + "material_request": mr.name, + "material_request_item": d.name, + "planned_start_date": mr.transaction_date, + "company": mr.company, + } + ) wo_order.set_work_order_operations() wo_order.save() work_orders.append(wo_order.name) else: - errors.append(_("Row {0}: Bill of Materials not found for the Item {1}") - .format(d.idx, get_link_to_form("Item", d.item_code))) + errors.append( + _("Row {0}: Bill of Materials not found for the Item {1}").format( + d.idx, get_link_to_form("Item", d.item_code) + ) + ) if work_orders: work_orders_list = [get_link_to_form("Work Order", d) for d in work_orders] if len(work_orders) > 1: - msgprint(_("The following {0} were created: {1}") - .format(frappe.bold(_("Work Orders")), '
    ' + ', '.join(work_orders_list))) + msgprint( + _("The following {0} were created: {1}").format( + frappe.bold(_("Work Orders")), "
    " + ", ".join(work_orders_list) + ) + ) else: - msgprint(_("The {0} {1} created sucessfully") - .format(frappe.bold(_("Work Order")), work_orders_list[0])) + msgprint( + _("The {0} {1} created sucessfully").format(frappe.bold(_("Work Order")), work_orders_list[0]) + ) if errors: - frappe.throw(_("Work Order cannot be created for following reason:
    {0}") - .format(new_line_sep(errors))) + frappe.throw( + _("Work Order cannot be created for following reason:
    {0}").format(new_line_sep(errors)) + ) return work_orders + @frappe.whitelist() def create_pick_list(source_name, target_doc=None): - doc = get_mapped_doc('Material Request', source_name, { - 'Material Request': { - 'doctype': 'Pick List', - 'field_map': { - 'material_request_type': 'purpose' + doc = get_mapped_doc( + "Material Request", + source_name, + { + "Material Request": { + "doctype": "Pick List", + "field_map": {"material_request_type": "purpose"}, + "validation": {"docstatus": ["=", 1]}, }, - 'validation': { - 'docstatus': ['=', 1] - } - }, - 'Material Request Item': { - 'doctype': 'Pick List Item', - 'field_map': { - 'name': 'material_request_item', - 'qty': 'stock_qty' + "Material Request Item": { + "doctype": "Pick List Item", + "field_map": {"name": "material_request_item", "qty": "stock_qty"}, }, }, - }, target_doc) + target_doc, + ) doc.set_item_locations() diff --git a/erpnext/stock/doctype/material_request/material_request_dashboard.py b/erpnext/stock/doctype/material_request/material_request_dashboard.py index c1ce0a93e2..b073e6a22e 100644 --- a/erpnext/stock/doctype/material_request/material_request_dashboard.py +++ b/erpnext/stock/doctype/material_request/material_request_dashboard.py @@ -3,20 +3,13 @@ from frappe import _ def get_data(): return { - 'fieldname': 'material_request', - 'transactions': [ + "fieldname": "material_request", + "transactions": [ { - 'label': _('Reference'), - 'items': ['Request for Quotation', 'Supplier Quotation', 'Purchase Order'] + "label": _("Reference"), + "items": ["Request for Quotation", "Supplier Quotation", "Purchase Order"], }, - { - 'label': _('Stock'), - 'items': ['Stock Entry', 'Purchase Receipt', 'Pick List'] - - }, - { - 'label': _('Manufacturing'), - 'items': ['Work Order'] - } - ] + {"label": _("Stock"), "items": ["Stock Entry", "Purchase Receipt", "Pick List"]}, + {"label": _("Manufacturing"), "items": ["Work Order"]}, + ], } diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 866f3ab2d5..78af1532ea 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -22,8 +22,7 @@ class TestMaterialRequest(FrappeTestCase): def test_make_purchase_order(self): mr = frappe.copy_doc(test_records[0]).insert() - self.assertRaises(frappe.ValidationError, make_purchase_order, - mr.name) + self.assertRaises(frappe.ValidationError, make_purchase_order, mr.name) mr = frappe.get_doc("Material Request", mr.name) mr.submit() @@ -44,7 +43,6 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(sq.doctype, "Supplier Quotation") self.assertEqual(len(sq.get("items")), len(mr.get("items"))) - def test_make_stock_entry(self): mr = frappe.copy_doc(test_records[0]).insert() @@ -58,42 +56,44 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(se.doctype, "Stock Entry") self.assertEqual(len(se.get("items")), len(mr.get("items"))) - def _insert_stock_entry(self, qty1, qty2, warehouse = None ): - se = frappe.get_doc({ - "company": "_Test Company", - "doctype": "Stock Entry", - "posting_date": "2013-03-01", - "posting_time": "00:00:00", - "purpose": "Material Receipt", - "items": [ - { - "conversion_factor": 1.0, - "doctype": "Stock Entry Detail", - "item_code": "_Test Item Home Desktop 100", - "parentfield": "items", - "basic_rate": 100, - "qty": qty1, - "stock_uom": "_Test UOM 1", - "transfer_qty": qty1, - "uom": "_Test UOM 1", - "t_warehouse": warehouse or "_Test Warehouse 1 - _TC", - "cost_center": "_Test Cost Center - _TC" - }, - { - "conversion_factor": 1.0, - "doctype": "Stock Entry Detail", - "item_code": "_Test Item Home Desktop 200", - "parentfield": "items", - "basic_rate": 100, - "qty": qty2, - "stock_uom": "_Test UOM 1", - "transfer_qty": qty2, - "uom": "_Test UOM 1", - "t_warehouse": warehouse or "_Test Warehouse 1 - _TC", - "cost_center": "_Test Cost Center - _TC" - } - ] - }) + def _insert_stock_entry(self, qty1, qty2, warehouse=None): + se = frappe.get_doc( + { + "company": "_Test Company", + "doctype": "Stock Entry", + "posting_date": "2013-03-01", + "posting_time": "00:00:00", + "purpose": "Material Receipt", + "items": [ + { + "conversion_factor": 1.0, + "doctype": "Stock Entry Detail", + "item_code": "_Test Item Home Desktop 100", + "parentfield": "items", + "basic_rate": 100, + "qty": qty1, + "stock_uom": "_Test UOM 1", + "transfer_qty": qty1, + "uom": "_Test UOM 1", + "t_warehouse": warehouse or "_Test Warehouse 1 - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + { + "conversion_factor": 1.0, + "doctype": "Stock Entry Detail", + "item_code": "_Test Item Home Desktop 200", + "parentfield": "items", + "basic_rate": 100, + "qty": qty2, + "stock_uom": "_Test UOM 1", + "transfer_qty": qty2, + "uom": "_Test UOM 1", + "t_warehouse": warehouse or "_Test Warehouse 1 - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ], + } + ) se.set_stock_entry_type() se.insert() @@ -106,19 +106,19 @@ class TestMaterialRequest(FrappeTestCase): mr.load_from_db() mr.cancel() - self.assertRaises(frappe.ValidationError, mr.update_status, 'Stopped') + self.assertRaises(frappe.ValidationError, mr.update_status, "Stopped") def test_mr_changes_from_stopped_to_pending_after_reopen(self): mr = frappe.copy_doc(test_records[0]) mr.insert() mr.submit() - self.assertEqual('Pending', mr.status) + self.assertEqual("Pending", mr.status) - mr.update_status('Stopped') - self.assertEqual('Stopped', mr.status) + mr.update_status("Stopped") + self.assertEqual("Stopped", mr.status) - mr.update_status('Submitted') - self.assertEqual('Pending', mr.status) + mr.update_status("Submitted") + self.assertEqual("Pending", mr.status) def test_cannot_submit_cancelled_mr(self): mr = frappe.copy_doc(test_records[0]) @@ -133,7 +133,7 @@ class TestMaterialRequest(FrappeTestCase): mr.insert() mr.submit() mr.cancel() - self.assertEqual('Cancelled', mr.status) + self.assertEqual("Cancelled", mr.status) def test_cannot_change_cancelled_mr(self): mr = frappe.copy_doc(test_records[0]) @@ -142,12 +142,12 @@ class TestMaterialRequest(FrappeTestCase): mr.load_from_db() mr.cancel() - self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Draft') - self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Stopped') - self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Ordered') - self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Issued') - self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Transferred') - self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Pending') + self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Draft") + self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Stopped") + self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Ordered") + self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Issued") + self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Transferred") + self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Pending") def test_cannot_submit_deleted_material_request(self): mr = frappe.copy_doc(test_records[0]) @@ -169,9 +169,9 @@ class TestMaterialRequest(FrappeTestCase): mr.submit() mr.load_from_db() - mr.update_status('Stopped') - mr.update_status('Submitted') - self.assertEqual(mr.status, 'Pending') + mr.update_status("Stopped") + mr.update_status("Submitted") + self.assertEqual(mr.status, "Pending") def test_pending_mr_changes_to_stopped_after_stop(self): mr = frappe.copy_doc(test_records[0]) @@ -179,17 +179,21 @@ class TestMaterialRequest(FrappeTestCase): mr.submit() mr.load_from_db() - mr.update_status('Stopped') - self.assertEqual(mr.status, 'Stopped') + mr.update_status("Stopped") + self.assertEqual(mr.status, "Stopped") def test_cannot_stop_unsubmitted_mr(self): mr = frappe.copy_doc(test_records[0]) mr.insert() - self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Stopped') + self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Stopped") def test_completed_qty_for_purchase(self): - existing_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - existing_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + existing_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + existing_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) # submit material request of type Purchase mr = frappe.copy_doc(test_records[0]) @@ -206,19 +210,18 @@ class TestMaterialRequest(FrappeTestCase): po_doc.get("items")[0].schedule_date = "2013-07-09" po_doc.get("items")[1].schedule_date = "2013-07-09" - # check for stopped status of Material Request po = frappe.copy_doc(po_doc) po.insert() po.load_from_db() - mr.update_status('Stopped') + mr.update_status("Stopped") self.assertRaises(frappe.InvalidStatusError, po.submit) frappe.db.set(po, "docstatus", 1) self.assertRaises(frappe.InvalidStatusError, po.cancel) # resubmit and check for per complete mr.load_from_db() - mr.update_status('Submitted') + mr.update_status("Submitted") po = frappe.copy_doc(po_doc) po.insert() po.submit() @@ -229,8 +232,12 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(mr.get("items")[0].ordered_qty, 27.0) self.assertEqual(mr.get("items")[1].ordered_qty, 1.5) - current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + current_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + current_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 27.0) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 1.5) @@ -242,15 +249,23 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(mr.get("items")[0].ordered_qty, 0) self.assertEqual(mr.get("items")[1].ordered_qty, 0) - current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + current_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + current_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0) def test_completed_qty_for_transfer(self): - existing_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - existing_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + existing_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + existing_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) # submit material request of type Purchase mr = frappe.copy_doc(test_records[0]) @@ -264,31 +279,31 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(mr.get("items")[0].ordered_qty, 0) self.assertEqual(mr.get("items")[1].ordered_qty, 0) - current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + current_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + current_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0) # map a stock entry se_doc = make_stock_entry(mr.name) - se_doc.update({ - "posting_date": "2013-03-01", - "posting_time": "01:00", - "fiscal_year": "_Test Fiscal Year 2013", - }) - se_doc.get("items")[0].update({ - "qty": 27.0, - "transfer_qty": 27.0, - "s_warehouse": "_Test Warehouse 1 - _TC", - "basic_rate": 1.0 - }) - se_doc.get("items")[1].update({ - "qty": 1.5, - "transfer_qty": 1.5, - "s_warehouse": "_Test Warehouse 1 - _TC", - "basic_rate": 1.0 - }) + se_doc.update( + { + "posting_date": "2013-03-01", + "posting_time": "01:00", + "fiscal_year": "_Test Fiscal Year 2013", + } + ) + se_doc.get("items")[0].update( + {"qty": 27.0, "transfer_qty": 27.0, "s_warehouse": "_Test Warehouse 1 - _TC", "basic_rate": 1.0} + ) + se_doc.get("items")[1].update( + {"qty": 1.5, "transfer_qty": 1.5, "s_warehouse": "_Test Warehouse 1 - _TC", "basic_rate": 1.0} + ) # make available the qty in _Test Warehouse 1 before transfer self._insert_stock_entry(27.0, 1.5) @@ -296,17 +311,17 @@ class TestMaterialRequest(FrappeTestCase): # check for stopped status of Material Request se = frappe.copy_doc(se_doc) se.insert() - mr.update_status('Stopped') + mr.update_status("Stopped") self.assertRaises(frappe.InvalidStatusError, se.submit) - mr.update_status('Submitted') + mr.update_status("Submitted") se.flags.ignore_validate_update_after_submit = True se.submit() - mr.update_status('Stopped') + mr.update_status("Stopped") self.assertRaises(frappe.InvalidStatusError, se.cancel) - mr.update_status('Submitted') + mr.update_status("Submitted") se = frappe.copy_doc(se_doc) se.insert() se.submit() @@ -317,8 +332,12 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(mr.get("items")[0].ordered_qty, 27.0) self.assertEqual(mr.get("items")[1].ordered_qty, 1.5) - current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + current_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + current_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 27.0) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 1.5) @@ -330,56 +349,70 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(mr.get("items")[0].ordered_qty, 0) self.assertEqual(mr.get("items")[1].ordered_qty, 0) - current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + current_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + current_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0) def test_over_transfer_qty_allowance(self): - mr = frappe.new_doc('Material Request') + mr = frappe.new_doc("Material Request") mr.company = "_Test Company" mr.scheduled_date = today() - mr.append('items',{ - "item_code": "_Test FG Item", - "item_name": "_Test FG Item", - "qty": 10, - "schedule_date": today(), - "uom": "_Test UOM 1", - "warehouse": "_Test Warehouse - _TC" - }) + mr.append( + "items", + { + "item_code": "_Test FG Item", + "item_name": "_Test FG Item", + "qty": 10, + "schedule_date": today(), + "uom": "_Test UOM 1", + "warehouse": "_Test Warehouse - _TC", + }, + ) mr.material_request_type = "Material Transfer" mr.insert() mr.submit() - frappe.db.set_value('Stock Settings', None, 'mr_qty_allowance', 20) + frappe.db.set_value("Stock Settings", None, "mr_qty_allowance", 20) # map a stock entry se_doc = make_stock_entry(mr.name) - se_doc.update({ - "posting_date": today(), - "posting_time": "00:00", - }) - se_doc.get("items")[0].update({ - "qty": 13, - "transfer_qty": 12.0, - "s_warehouse": "_Test Warehouse - _TC", - "t_warehouse": "_Test Warehouse 1 - _TC", - "basic_rate": 1.0 - }) + se_doc.update( + { + "posting_date": today(), + "posting_time": "00:00", + } + ) + se_doc.get("items")[0].update( + { + "qty": 13, + "transfer_qty": 12.0, + "s_warehouse": "_Test Warehouse - _TC", + "t_warehouse": "_Test Warehouse 1 - _TC", + "basic_rate": 1.0, + } + ) # make available the qty in _Test Warehouse 1 before transfer sr = frappe.new_doc("Stock Reconciliation") sr.company = "_Test Company" sr.purpose = "Opening Stock" - sr.append('items', { - "item_code": "_Test FG Item", - "warehouse": "_Test Warehouse - _TC", - "qty": 20, - "valuation_rate": 0.01 - }) + sr.append( + "items", + { + "item_code": "_Test FG Item", + "warehouse": "_Test Warehouse - _TC", + "qty": 20, + "valuation_rate": 0.01, + }, + ) sr.insert() sr.submit() se = frappe.copy_doc(se_doc) @@ -389,8 +422,12 @@ class TestMaterialRequest(FrappeTestCase): se.submit() def test_completed_qty_for_over_transfer(self): - existing_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - existing_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + existing_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + existing_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) # submit material request of type Purchase mr = frappe.copy_doc(test_records[0]) @@ -401,23 +438,19 @@ class TestMaterialRequest(FrappeTestCase): # map a stock entry se_doc = make_stock_entry(mr.name) - se_doc.update({ - "posting_date": "2013-03-01", - "posting_time": "00:00", - "fiscal_year": "_Test Fiscal Year 2013", - }) - se_doc.get("items")[0].update({ - "qty": 54.0, - "transfer_qty": 54.0, - "s_warehouse": "_Test Warehouse 1 - _TC", - "basic_rate": 1.0 - }) - se_doc.get("items")[1].update({ - "qty": 3.0, - "transfer_qty": 3.0, - "s_warehouse": "_Test Warehouse 1 - _TC", - "basic_rate": 1.0 - }) + se_doc.update( + { + "posting_date": "2013-03-01", + "posting_time": "00:00", + "fiscal_year": "_Test Fiscal Year 2013", + } + ) + se_doc.get("items")[0].update( + {"qty": 54.0, "transfer_qty": 54.0, "s_warehouse": "_Test Warehouse 1 - _TC", "basic_rate": 1.0} + ) + se_doc.get("items")[1].update( + {"qty": 3.0, "transfer_qty": 3.0, "s_warehouse": "_Test Warehouse 1 - _TC", "basic_rate": 1.0} + ) # make available the qty in _Test Warehouse 1 before transfer self._insert_stock_entry(60.0, 3.0) @@ -426,11 +459,11 @@ class TestMaterialRequest(FrappeTestCase): se = frappe.copy_doc(se_doc) se.set_stock_entry_type() se.insert() - mr.update_status('Stopped') + mr.update_status("Stopped") self.assertRaises(frappe.InvalidStatusError, se.submit) self.assertRaises(frappe.InvalidStatusError, se.cancel) - mr.update_status('Submitted') + mr.update_status("Submitted") se = frappe.copy_doc(se_doc) se.set_stock_entry_type() se.insert() @@ -443,8 +476,12 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(mr.get("items")[0].ordered_qty, 54.0) self.assertEqual(mr.get("items")[1].ordered_qty, 3.0) - current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + current_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + current_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2) @@ -456,8 +493,12 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(mr.get("items")[0].ordered_qty, 0) self.assertEqual(mr.get("items")[1].ordered_qty, 0) - current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + current_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + current_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0) @@ -470,25 +511,31 @@ class TestMaterialRequest(FrappeTestCase): mr.submit() se_doc = make_stock_entry(mr.name) - se_doc.update({ - "posting_date": "2013-03-01", - "posting_time": "00:00", - "fiscal_year": "_Test Fiscal Year 2013", - }) - se_doc.get("items")[0].update({ - "qty": 60.0, - "transfer_qty": 60.0, - "s_warehouse": "_Test Warehouse - _TC", - "t_warehouse": "_Test Warehouse 1 - _TC", - "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", - "basic_rate": 1.0 - }) + se_doc.update( + { + "posting_date": "2013-03-01", + "posting_time": "00:00", + "fiscal_year": "_Test Fiscal Year 2013", + } + ) + se_doc.get("items")[0].update( + { + "qty": 60.0, + "transfer_qty": 60.0, + "s_warehouse": "_Test Warehouse - _TC", + "t_warehouse": "_Test Warehouse 1 - _TC", + "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", + "basic_rate": 1.0, + } + ) # check for stopped status of Material Request se = frappe.copy_doc(se_doc) @@ -505,18 +552,20 @@ class TestMaterialRequest(FrappeTestCase): def test_warehouse_company_validation(self): from erpnext.stock.utils import InvalidWarehouseCompany + mr = frappe.copy_doc(test_records[0]) mr.company = "_Test Company 1" self.assertRaises(InvalidWarehouseCompany, mr.insert) def _get_requested_qty(self, item_code, warehouse): - return flt(frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "indented_qty")) + return flt( + frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "indented_qty") + ) def test_make_stock_entry_for_material_issue(self): mr = frappe.copy_doc(test_records[0]).insert() - self.assertRaises(frappe.ValidationError, make_stock_entry, - mr.name) + self.assertRaises(frappe.ValidationError, make_stock_entry, mr.name) mr = frappe.get_doc("Material Request", mr.name) mr.material_request_type = "Material Issue" @@ -528,8 +577,13 @@ class TestMaterialRequest(FrappeTestCase): def test_completed_qty_for_issue(self): def _get_requested_qty(): - return flt(frappe.db.get_value("Bin", {"item_code": "_Test Item Home Desktop 100", - "warehouse": "_Test Warehouse - _TC"}, "indented_qty")) + return flt( + frappe.db.get_value( + "Bin", + {"item_code": "_Test Item Home Desktop 100", "warehouse": "_Test Warehouse - _TC"}, + "indented_qty", + ) + ) existing_requested_qty = _get_requested_qty() @@ -537,7 +591,7 @@ class TestMaterialRequest(FrappeTestCase): mr.material_request_type = "Material Issue" mr.submit() - #testing bin value after material request is submitted + # testing bin value after material request is submitted self.assertEqual(_get_requested_qty(), existing_requested_qty - 54.0) # receive items to allow issue @@ -556,7 +610,7 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(mr.get("items")[0].ordered_qty, 54.0) self.assertEqual(mr.get("items")[1].ordered_qty, 3.0) - #testing bin requested qty after issuing stock against material request + # testing bin requested qty after issuing stock against material request self.assertEqual(_get_requested_qty(), existing_requested_qty) def test_material_request_type_manufacture(self): @@ -564,8 +618,11 @@ class TestMaterialRequest(FrappeTestCase): mr = frappe.get_doc("Material Request", mr.name) mr.submit() completed_qty = mr.items[0].ordered_qty - requested_qty = frappe.db.sql("""select indented_qty from `tabBin` where \ - item_code= %s and warehouse= %s """, (mr.items[0].item_code, mr.items[0].warehouse))[0][0] + requested_qty = frappe.db.sql( + """select indented_qty from `tabBin` where \ + item_code= %s and warehouse= %s """, + (mr.items[0].item_code, mr.items[0].warehouse), + )[0][0] prod_order = raise_work_orders(mr.name) po = frappe.get_doc("Work Order", prod_order[0]) @@ -575,8 +632,11 @@ class TestMaterialRequest(FrappeTestCase): mr = frappe.get_doc("Material Request", mr.name) self.assertEqual(completed_qty + po.qty, mr.items[0].ordered_qty) - new_requested_qty = frappe.db.sql("""select indented_qty from `tabBin` where \ - item_code= %s and warehouse= %s """, (mr.items[0].item_code, mr.items[0].warehouse))[0][0] + new_requested_qty = frappe.db.sql( + """select indented_qty from `tabBin` where \ + item_code= %s and warehouse= %s """, + (mr.items[0].item_code, mr.items[0].warehouse), + )[0][0] self.assertEqual(requested_qty - po.qty, new_requested_qty) @@ -585,17 +645,24 @@ class TestMaterialRequest(FrappeTestCase): mr = frappe.get_doc("Material Request", mr.name) self.assertEqual(completed_qty, mr.items[0].ordered_qty) - new_requested_qty = frappe.db.sql("""select indented_qty from `tabBin` where \ - item_code= %s and warehouse= %s """, (mr.items[0].item_code, mr.items[0].warehouse))[0][0] + new_requested_qty = frappe.db.sql( + """select indented_qty from `tabBin` where \ + item_code= %s and warehouse= %s """, + (mr.items[0].item_code, mr.items[0].warehouse), + )[0][0] self.assertEqual(requested_qty, new_requested_qty) def test_requested_qty_multi_uom(self): - existing_requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC') + existing_requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC") - mr = make_material_request(item_code='_Test FG Item', material_request_type='Manufacture', - uom="_Test UOM 1", conversion_factor=12) + 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') + requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC") self.assertEqual(requested_qty, existing_requested_qty + 120) @@ -605,42 +672,36 @@ class TestMaterialRequest(FrappeTestCase): wo.wip_warehouse = "_Test Warehouse 1 - _TC" wo.submit() - requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC') + requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC") self.assertEqual(requested_qty, existing_requested_qty + 70) wo.cancel() - requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC') + requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC") self.assertEqual(requested_qty, existing_requested_qty + 120) mr.reload() mr.cancel() - requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC') + requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC") self.assertEqual(requested_qty, existing_requested_qty) - def test_multi_uom_for_purchase(self): mr = frappe.copy_doc(test_records[0]) - mr.material_request_type = 'Purchase' + mr.material_request_type = "Purchase" item = mr.items[0] mr.schedule_date = today() - if not frappe.db.get_value('UOM Conversion Detail', - {'parent': item.item_code, 'uom': 'Kg'}): - item_doc = frappe.get_doc('Item', item.item_code) - item_doc.append('uoms', { - 'uom': 'Kg', - 'conversion_factor': 5 - }) + if not frappe.db.get_value("UOM Conversion Detail", {"parent": item.item_code, "uom": "Kg"}): + item_doc = frappe.get_doc("Item", item.item_code) + item_doc.append("uoms", {"uom": "Kg", "conversion_factor": 5}) item_doc.save(ignore_permissions=True) - item.uom = 'Kg' + item.uom = "Kg" for item in mr.items: item.schedule_date = mr.schedule_date mr.insert() - self.assertRaises(frappe.ValidationError, make_purchase_order, - mr.name) + self.assertRaises(frappe.ValidationError, make_purchase_order, mr.name) mr = frappe.get_doc("Material Request", mr.name) mr.submit() @@ -654,17 +715,19 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(po.doctype, "Purchase Order") self.assertEqual(len(po.get("items")), len(mr.get("items"))) - po.supplier = '_Test Supplier' + po.supplier = "_Test Supplier" po.insert() po.submit() mr = frappe.get_doc("Material Request", mr.name) self.assertEqual(mr.per_ordered, 100) def test_customer_provided_parts_mr(self): - create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0) + create_item( + "CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0 + ) existing_requested_qty = self._get_requested_qty("_Test Customer", "_Test Warehouse - _TC") - mr = make_material_request(item_code='CUST-0987', material_request_type='Customer Provided') + mr = make_material_request(item_code="CUST-0987", material_request_type="Customer Provided") se = make_stock_entry(mr.name) se.insert() se.submit() @@ -677,25 +740,30 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(mr.per_ordered, 100) self.assertEqual(existing_requested_qty, current_requested_qty) + def make_material_request(**args): args = frappe._dict(args) mr = frappe.new_doc("Material Request") mr.material_request_type = args.material_request_type or "Purchase" mr.company = args.company or "_Test Company" - mr.customer = args.customer or '_Test Customer' - mr.append("items", { - "item_code": args.item_code or "_Test Item", - "qty": args.qty or 10, - "uom": args.uom or "_Test UOM", - "conversion_factor": args.conversion_factor or 1, - "schedule_date": args.schedule_date or today(), - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "cost_center": args.cost_center or "_Test Cost Center - _TC" - }) + mr.customer = args.customer or "_Test Customer" + mr.append( + "items", + { + "item_code": args.item_code or "_Test Item", + "qty": args.qty or 10, + "uom": args.uom or "_Test UOM", + "conversion_factor": args.conversion_factor or 1, + "schedule_date": args.schedule_date or today(), + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "cost_center": args.cost_center or "_Test Cost Center - _TC", + }, + ) mr.insert() if not args.do_not_submit: mr.submit() return mr + test_dependencies = ["Currency Exchange", "BOM"] -test_records = frappe.get_test_records('Material Request') +test_records = frappe.get_test_records("Material Request") diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.py b/erpnext/stock/doctype/material_request_item/material_request_item.py index 32407d0fb0..08c9ed2742 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.py +++ b/erpnext/stock/doctype/material_request_item/material_request_item.py @@ -11,5 +11,6 @@ from frappe.model.document import Document class MaterialRequestItem(Document): pass + def on_doctype_update(): frappe.db.add_index("Material Request Item", ["item_code", "warehouse"]) diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index f9c00c59ba..026dd4e122 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -23,7 +23,9 @@ def make_packing_list(doc): return parent_items_price, reset = {}, False - set_price_from_children = frappe.db.get_single_value("Selling Settings", "editable_bundle_item_rates") + set_price_from_children = frappe.db.get_single_value( + "Selling Settings", "editable_bundle_item_rates" + ) stale_packed_items_table = get_indexed_packed_items_table(doc) @@ -33,9 +35,11 @@ def make_packing_list(doc): if frappe.db.exists("Product Bundle", {"new_item_code": item_row.item_code}): for bundle_item in get_product_bundle_items(item_row.item_code): pi_row = add_packed_item_row( - doc=doc, packing_item=bundle_item, - main_item_row=item_row, packed_items_table=stale_packed_items_table, - reset=reset + doc=doc, + packing_item=bundle_item, + main_item_row=item_row, + packed_items_table=stale_packed_items_table, + reset=reset, ) item_data = get_packed_item_details(bundle_item.item_code, doc.company) update_packed_item_basic_data(item_row, pi_row, bundle_item, item_data) @@ -43,18 +47,19 @@ def make_packing_list(doc): update_packed_item_price_data(pi_row, item_data, doc) update_packed_item_from_cancelled_doc(item_row, bundle_item, pi_row, doc) - if set_price_from_children: # create/update bundle item wise price dict + if set_price_from_children: # create/update bundle item wise price dict update_product_bundle_rate(parent_items_price, pi_row) if parent_items_price: - set_product_bundle_rate_amount(doc, parent_items_price) # set price in bundle item + set_product_bundle_rate_amount(doc, parent_items_price) # set price in bundle item + def get_indexed_packed_items_table(doc): """ - Create dict from stale packed items table like: - {(Parent Item 1, Bundle Item 1, ae4b5678): {...}, (key): {value}} + Create dict from stale packed items table like: + {(Parent Item 1, Bundle Item 1, ae4b5678): {...}, (key): {value}} - Use: to quickly retrieve/check if row existed in table instead of looping n times + Use: to quickly retrieve/check if row existed in table instead of looping n times """ indexed_table = {} for packed_item in doc.get("packed_items"): @@ -63,6 +68,7 @@ def get_indexed_packed_items_table(doc): return indexed_table + def reset_packing_list(doc): "Conditionally reset the table and return if it was reset or not." reset_table = False @@ -86,33 +92,34 @@ def reset_packing_list(doc): doc.set("packed_items", []) return reset_table + def get_product_bundle_items(item_code): product_bundle = frappe.qb.DocType("Product Bundle") product_bundle_item = frappe.qb.DocType("Product Bundle Item") query = ( frappe.qb.from_(product_bundle_item) - .join(product_bundle).on(product_bundle_item.parent == product_bundle.name) + .join(product_bundle) + .on(product_bundle_item.parent == product_bundle.name) .select( product_bundle_item.item_code, product_bundle_item.qty, product_bundle_item.uom, - product_bundle_item.description - ).where( - product_bundle.new_item_code == item_code - ).orderby( - product_bundle_item.idx + product_bundle_item.description, ) + .where(product_bundle.new_item_code == item_code) + .orderby(product_bundle_item.idx) ) return query.run(as_dict=True) + def add_packed_item_row(doc, packing_item, main_item_row, packed_items_table, reset): """Add and return packed item row. - doc: Transaction document - packing_item (dict): Packed Item details - main_item_row (dict): Items table row corresponding to packed item - packed_items_table (dict): Packed Items table before save (indexed) - reset (bool): State if table is reset or preserved as is + doc: Transaction document + packing_item (dict): Packed Item details + main_item_row (dict): Items table row corresponding to packed item + packed_items_table (dict): Packed Items table before save (indexed) + reset (bool): State if table is reset or preserved as is """ exists, pi_row = False, {} @@ -122,33 +129,34 @@ def add_packed_item_row(doc, packing_item, main_item_row, packed_items_table, re pi_row, exists = packed_items_table.get(key), True if not exists: - pi_row = doc.append('packed_items', {}) - elif reset: # add row if row exists but table is reset + pi_row = doc.append("packed_items", {}) + elif reset: # add row if row exists but table is reset pi_row.idx, pi_row.name = None, None - pi_row = doc.append('packed_items', pi_row) + pi_row = doc.append("packed_items", pi_row) return pi_row + def get_packed_item_details(item_code, company): item = frappe.qb.DocType("Item") item_default = frappe.qb.DocType("Item Default") query = ( frappe.qb.from_(item) .left_join(item_default) - .on( - (item_default.parent == item.name) - & (item_default.company == company) - ).select( - item.item_name, item.is_stock_item, - item.description, item.stock_uom, + .on((item_default.parent == item.name) & (item_default.company == company)) + .select( + item.item_name, + item.is_stock_item, + item.description, + item.stock_uom, item.valuation_rate, - item_default.default_warehouse - ).where( - item.name == item_code + item_default.default_warehouse, ) + .where(item.name == item_code) ) return query.run(as_dict=True)[0] + def update_packed_item_basic_data(main_item_row, pi_row, packing_item, item_data): pi_row.parent_item = main_item_row.item_code pi_row.parent_detail_docname = main_item_row.name @@ -161,12 +169,16 @@ def update_packed_item_basic_data(main_item_row, pi_row, packing_item, item_data if not pi_row.description: pi_row.description = packing_item.get("description") + def update_packed_item_stock_data(main_item_row, pi_row, packing_item, item_data, doc): # TODO batch_no, actual_batch_qty, incoming_rate if not pi_row.warehouse and not doc.amended_from: - fetch_warehouse = (doc.get('is_pos') or item_data.is_stock_item or not item_data.default_warehouse) - pi_row.warehouse = (main_item_row.warehouse if (fetch_warehouse and main_item_row.warehouse) - else item_data.default_warehouse) + fetch_warehouse = doc.get("is_pos") or item_data.is_stock_item or not item_data.default_warehouse + pi_row.warehouse = ( + main_item_row.warehouse + if (fetch_warehouse and main_item_row.warehouse) + else item_data.default_warehouse + ) if not pi_row.target_warehouse: pi_row.target_warehouse = main_item_row.get("target_warehouse") @@ -175,6 +187,7 @@ def update_packed_item_stock_data(main_item_row, pi_row, packing_item, item_data pi_row.actual_qty = flt(bin.get("actual_qty")) pi_row.projected_qty = flt(bin.get("projected_qty")) + def update_packed_item_price_data(pi_row, item_data, doc): "Set price as per price list or from the Item master." if pi_row.rate: @@ -182,50 +195,60 @@ def update_packed_item_price_data(pi_row, item_data, doc): item_doc = frappe.get_cached_doc("Item", pi_row.item_code) row_data = pi_row.as_dict().copy() - row_data.update({ - "company": doc.get("company"), - "price_list": doc.get("selling_price_list"), - "currency": doc.get("currency"), - "conversion_rate": doc.get("conversion_rate"), - }) + row_data.update( + { + "company": doc.get("company"), + "price_list": doc.get("selling_price_list"), + "currency": doc.get("currency"), + "conversion_rate": doc.get("conversion_rate"), + } + ) rate = get_price_list_rate(row_data, item_doc).get("price_list_rate") pi_row.rate = rate or item_data.get("valuation_rate") or 0.0 + def update_packed_item_from_cancelled_doc(main_item_row, packing_item, pi_row, doc): "Update packed item row details from cancelled doc into amended doc." prev_doc_packed_items_map = None if doc.amended_from: prev_doc_packed_items_map = get_cancelled_doc_packed_item_details(doc.packed_items) - if prev_doc_packed_items_map and prev_doc_packed_items_map.get((packing_item.item_code, main_item_row.item_code)): + if prev_doc_packed_items_map and prev_doc_packed_items_map.get( + (packing_item.item_code, main_item_row.item_code) + ): prev_doc_row = prev_doc_packed_items_map.get((packing_item.item_code, main_item_row.item_code)) pi_row.batch_no = prev_doc_row[0].batch_no pi_row.serial_no = prev_doc_row[0].serial_no pi_row.warehouse = prev_doc_row[0].warehouse + def get_packed_item_bin_qty(item, warehouse): bin_data = frappe.db.get_values( "Bin", fieldname=["actual_qty", "projected_qty"], filters={"item_code": item, "warehouse": warehouse}, - as_dict=True + as_dict=True, ) return bin_data[0] if bin_data else {} + def get_cancelled_doc_packed_item_details(old_packed_items): prev_doc_packed_items_map = {} for items in old_packed_items: - prev_doc_packed_items_map.setdefault((items.item_code ,items.parent_item), []).append(items.as_dict()) + prev_doc_packed_items_map.setdefault((items.item_code, items.parent_item), []).append( + items.as_dict() + ) return prev_doc_packed_items_map + def update_product_bundle_rate(parent_items_price, pi_row): """ - Update the price dict of Product Bundles based on the rates of the Items in the bundle. + Update the price dict of Product Bundles based on the rates of the Items in the bundle. - Stucture: - {(Bundle Item 1, ae56fgji): 150.0, (Bundle Item 2, bc78fkjo): 200.0} + Stucture: + {(Bundle Item 1, ae56fgji): 150.0, (Bundle Item 2, bc78fkjo): 200.0} """ key = (pi_row.parent_item, pi_row.parent_detail_docname) rate = parent_items_price.get(key) @@ -234,6 +257,7 @@ def update_product_bundle_rate(parent_items_price, pi_row): parent_items_price[key] += flt(pi_row.rate) + def set_product_bundle_rate_amount(doc, parent_items_price): "Set cumulative rate and amount in bundle item." for item in doc.get("items"): @@ -242,6 +266,7 @@ def set_product_bundle_rate_amount(doc, parent_items_price): item.rate = bundle_rate item.amount = flt(bundle_rate * item.qty) + def on_doctype_update(): frappe.db.add_index("Packed Item", ["item_code", "warehouse"]) @@ -252,10 +277,7 @@ def get_items_from_product_bundle(row): bundled_items = get_product_bundle_items(row["item_code"]) for item in bundled_items: - row.update({ - "item_code": item.item_code, - "qty": flt(row["quantity"]) * flt(item.qty) - }) + row.update({"item_code": item.item_code, "qty": flt(row["quantity"]) * flt(item.qty)}) items.append(get_item_details(row)) return items diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py index 5f1b9542d6..fe1b0d9f79 100644 --- a/erpnext/stock/doctype/packed_item/test_packed_item.py +++ b/erpnext/stock/doctype/packed_item/test_packed_item.py @@ -14,6 +14,7 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry class TestPackedItem(FrappeTestCase): "Test impact on Packed Items table in various scenarios." + @classmethod def setUpClass(cls) -> None: super().setUpClass() @@ -39,8 +40,7 @@ class TestPackedItem(FrappeTestCase): def test_adding_bundle_item(self): "Test impact on packed items if bundle item row is added." - so = make_sales_order(item_code = self.bundle, qty=1, - do_not_submit=True) + so = make_sales_order(item_code=self.bundle, qty=1, do_not_submit=True) self.assertEqual(so.items[0].qty, 1) self.assertEqual(len(so.packed_items), 2) @@ -51,7 +51,7 @@ class TestPackedItem(FrappeTestCase): "Test impact on packed items if bundle item row is updated." so = make_sales_order(item_code=self.bundle, qty=1, do_not_submit=True) - so.items[0].qty = 2 # change qty + so.items[0].qty = 2 # change qty so.save() self.assertEqual(so.packed_items[0].qty, 4) @@ -67,12 +67,9 @@ class TestPackedItem(FrappeTestCase): "Test impact on packed items if same bundle item is added and removed." so_items = [] for qty in [2, 4, 6, 8]: - so_items.append({ - "item_code": self.bundle, - "qty": qty, - "rate": 400, - "warehouse": "_Test Warehouse - _TC" - }) + so_items.append( + {"item_code": self.bundle, "qty": qty, "rate": 400, "warehouse": "_Test Warehouse - _TC"} + ) # create SO with recurring bundle item so = make_sales_order(item_list=so_items, do_not_submit=True) @@ -120,18 +117,15 @@ class TestPackedItem(FrappeTestCase): "Test impact on packed items in newly mapped DN from SO." so_items = [] for qty in [2, 4]: - so_items.append({ - "item_code": self.bundle, - "qty": qty, - "rate": 400, - "warehouse": "_Test Warehouse - _TC" - }) + so_items.append( + {"item_code": self.bundle, "qty": qty, "rate": 400, "warehouse": "_Test Warehouse - _TC"} + ) # create SO with recurring bundle item so = make_sales_order(item_list=so_items) dn = make_delivery_note(so.name) - dn.items[1].qty = 3 # change second row qty for inserting doc + dn.items[1].qty = 3 # change second row qty for inserting doc dn.save() self.assertEqual(len(dn.packed_items), 4) @@ -148,7 +142,7 @@ class TestPackedItem(FrappeTestCase): for item in self.bundle_items: make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=100, posting_date=today) - so = make_sales_order(item_code = self.bundle, qty=1, company=company, warehouse=warehouse) + so = make_sales_order(item_code=self.bundle, qty=1, company=company, warehouse=warehouse) dn = make_delivery_note(so.name) dn.save() @@ -159,7 +153,9 @@ class TestPackedItem(FrappeTestCase): # backdated stock entry for item in self.bundle_items: - make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=200, posting_date=yesterday) + make_stock_entry( + item_code=item, to_warehouse=warehouse, qty=10, rate=200, posting_date=yesterday + ) # assert correct reposting gles = get_gl_entries(dn.doctype, dn.name) @@ -173,8 +169,7 @@ class TestPackedItem(FrappeTestCase): sort_function = lambda p: (p.parent_item, p.item_code, p.qty) for sent, returned in zip( - sorted(original, key=sort_function), - sorted(returned, key=sort_function) + sorted(original, key=sort_function), sorted(returned, key=sort_function) ): self.assertEqual(sent.item_code, returned.item_code) self.assertEqual(sent.parent_item, returned.parent_item) @@ -195,7 +190,7 @@ class TestPackedItem(FrappeTestCase): "warehouse": self.warehouse, "qty": 1, "rate": 100, - } + }, ] so = make_sales_order(item_list=item_list, warehouse=self.warehouse) @@ -224,7 +219,7 @@ class TestPackedItem(FrappeTestCase): "warehouse": self.warehouse, "qty": 1, "rate": 100, - } + }, ] so = make_sales_order(item_list=item_list, warehouse=self.warehouse) @@ -246,11 +241,10 @@ class TestPackedItem(FrappeTestCase): expected_returns = [d for d in dn.packed_items if d.parent_item == self.bundle] self.assertReturns(expected_returns, dn_ret.packed_items) - def test_returning_partial_bundle_qty(self): from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return - so = make_sales_order(item_code=self.bundle, warehouse=self.warehouse, qty = 2) + so = make_sales_order(item_code=self.bundle, warehouse=self.warehouse, qty=2) dn = make_delivery_note(so.name) dn.save() diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py index b092862415..e9ccf5fc77 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/packing_slip.py @@ -10,14 +10,13 @@ from frappe.utils import cint, flt class PackingSlip(Document): - def validate(self): """ - * Validate existence of submitted Delivery Note - * Case nos do not overlap - * Check if packed qty doesn't exceed actual qty of delivery note + * Validate existence of submitted Delivery Note + * Case nos do not overlap + * Check if packed qty doesn't exceed actual qty of delivery note - It is necessary to validate case nos before checking quantity + It is necessary to validate case nos before checking quantity """ self.validate_delivery_note() self.validate_items_mandatory() @@ -25,12 +24,13 @@ class PackingSlip(Document): self.validate_qty() from erpnext.utilities.transaction_base import validate_uom_is_integer + validate_uom_is_integer(self, "stock_uom", "qty") validate_uom_is_integer(self, "weight_uom", "net_weight") def validate_delivery_note(self): """ - Validates if delivery note has status as draft + Validates if delivery note has status as draft """ if cint(frappe.db.get_value("Delivery Note", self.delivery_note, "docstatus")) != 0: frappe.throw(_("Delivery Note {0} must not be submitted").format(self.delivery_note)) @@ -42,27 +42,33 @@ class PackingSlip(Document): def validate_case_nos(self): """ - Validate if case nos overlap. If they do, recommend next case no. + Validate if case nos overlap. If they do, recommend next case no. """ if not cint(self.from_case_no): frappe.msgprint(_("Please specify a valid 'From Case No.'"), raise_exception=1) elif not self.to_case_no: self.to_case_no = self.from_case_no elif cint(self.from_case_no) > cint(self.to_case_no): - frappe.msgprint(_("'To Case No.' cannot be less than 'From Case No.'"), - raise_exception=1) + frappe.msgprint(_("'To Case No.' cannot be less than 'From Case No.'"), raise_exception=1) - res = frappe.db.sql("""SELECT name FROM `tabPacking Slip` + res = frappe.db.sql( + """SELECT name FROM `tabPacking Slip` WHERE delivery_note = %(delivery_note)s AND docstatus = 1 AND ((from_case_no BETWEEN %(from_case_no)s AND %(to_case_no)s) OR (to_case_no BETWEEN %(from_case_no)s AND %(to_case_no)s) OR (%(from_case_no)s BETWEEN from_case_no AND to_case_no)) - """, {"delivery_note":self.delivery_note, - "from_case_no":self.from_case_no, - "to_case_no":self.to_case_no}) + """, + { + "delivery_note": self.delivery_note, + "from_case_no": self.from_case_no, + "to_case_no": self.to_case_no, + }, + ) if res: - frappe.throw(_("""Case No(s) already in use. Try from Case No {0}""").format(self.get_recommended_case_no())) + frappe.throw( + _("""Case No(s) already in use. Try from Case No {0}""").format(self.get_recommended_case_no()) + ) def validate_qty(self): """Check packed qty across packing slips and delivery note""" @@ -70,36 +76,37 @@ class PackingSlip(Document): dn_details, ps_item_qty, no_of_cases = self.get_details_for_packing() for item in dn_details: - new_packed_qty = (flt(ps_item_qty[item['item_code']]) * no_of_cases) + \ - flt(item['packed_qty']) - if new_packed_qty > flt(item['qty']) and no_of_cases: + new_packed_qty = (flt(ps_item_qty[item["item_code"]]) * no_of_cases) + flt(item["packed_qty"]) + if new_packed_qty > flt(item["qty"]) and no_of_cases: self.recommend_new_qty(item, ps_item_qty, no_of_cases) - def get_details_for_packing(self): """ - Returns - * 'Delivery Note Items' query result as a list of dict - * Item Quantity dict of current packing slip doc - * No. of Cases of this packing slip + Returns + * 'Delivery Note Items' query result as a list of dict + * Item Quantity dict of current packing slip doc + * No. of Cases of this packing slip """ rows = [d.item_code for d in self.get("items")] # also pick custom fields from delivery note - custom_fields = ', '.join('dni.`{0}`'.format(d.fieldname) + custom_fields = ", ".join( + "dni.`{0}`".format(d.fieldname) for d in frappe.get_meta("Delivery Note Item").get_custom_fields() - if d.fieldtype not in no_value_fields) + if d.fieldtype not in no_value_fields + ) if custom_fields: - custom_fields = ', ' + custom_fields + custom_fields = ", " + custom_fields condition = "" if rows: - condition = " and item_code in (%s)" % (", ".join(["%s"]*len(rows))) + condition = " and item_code in (%s)" % (", ".join(["%s"] * len(rows))) # gets item code, qty per item code, latest packed qty per item code and stock uom - res = frappe.db.sql("""select item_code, sum(qty) as qty, + res = frappe.db.sql( + """select item_code, sum(qty) as qty, (select sum(psi.qty * (abs(ps.to_case_no - ps.from_case_no) + 1)) from `tabPacking Slip` ps, `tabPacking Slip Item` psi where ps.name = psi.parent and ps.docstatus = 1 @@ -107,47 +114,57 @@ class PackingSlip(Document): stock_uom, item_name, description, dni.batch_no {custom_fields} from `tabDelivery Note Item` dni where parent=%s {condition} - group by item_code""".format(condition=condition, custom_fields=custom_fields), - tuple([self.delivery_note] + rows), as_dict=1) + group by item_code""".format( + condition=condition, custom_fields=custom_fields + ), + tuple([self.delivery_note] + rows), + as_dict=1, + ) ps_item_qty = dict([[d.item_code, d.qty] for d in self.get("items")]) no_of_cases = cint(self.to_case_no) - cint(self.from_case_no) + 1 return res, ps_item_qty, no_of_cases - def recommend_new_qty(self, item, ps_item_qty, no_of_cases): """ - Recommend a new quantity and raise a validation exception + Recommend a new quantity and raise a validation exception """ - item['recommended_qty'] = (flt(item['qty']) - flt(item['packed_qty'])) / no_of_cases - item['specified_qty'] = flt(ps_item_qty[item['item_code']]) - if not item['packed_qty']: item['packed_qty'] = 0 + item["recommended_qty"] = (flt(item["qty"]) - flt(item["packed_qty"])) / no_of_cases + item["specified_qty"] = flt(ps_item_qty[item["item_code"]]) + if not item["packed_qty"]: + item["packed_qty"] = 0 - frappe.throw(_("Quantity for Item {0} must be less than {1}").format(item.get("item_code"), item.get("recommended_qty"))) + frappe.throw( + _("Quantity for Item {0} must be less than {1}").format( + item.get("item_code"), item.get("recommended_qty") + ) + ) def update_item_details(self): """ - Fill empty columns in Packing Slip Item + Fill empty columns in Packing Slip Item """ if not self.from_case_no: self.from_case_no = self.get_recommended_case_no() for d in self.get("items"): - res = frappe.db.get_value("Item", d.item_code, - ["weight_per_unit", "weight_uom"], as_dict=True) + res = frappe.db.get_value("Item", d.item_code, ["weight_per_unit", "weight_uom"], as_dict=True) - if res and len(res)>0: + if res and len(res) > 0: d.net_weight = res["weight_per_unit"] d.weight_uom = res["weight_uom"] def get_recommended_case_no(self): """ - Returns the next case no. for a new packing slip for a delivery - note + Returns the next case no. for a new packing slip for a delivery + note """ - recommended_case_no = frappe.db.sql("""SELECT MAX(to_case_no) FROM `tabPacking Slip` - WHERE delivery_note = %s AND docstatus=1""", self.delivery_note) + recommended_case_no = frappe.db.sql( + """SELECT MAX(to_case_no) FROM `tabPacking Slip` + WHERE delivery_note = %s AND docstatus=1""", + self.delivery_note, + ) return cint(recommended_case_no[0][0]) + 1 @@ -160,7 +177,7 @@ class PackingSlip(Document): dn_details = self.get_details_for_packing()[0] for item in dn_details: if flt(item.qty) > flt(item.packed_qty): - ch = self.append('items', {}) + ch = self.append("items", {}) ch.item_code = item.item_code ch.item_name = item.item_name ch.stock_uom = item.stock_uom @@ -175,14 +192,18 @@ class PackingSlip(Document): self.update_item_details() + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def item_details(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond - return frappe.db.sql("""select name, item_name, description from `tabItem` + + return frappe.db.sql( + """select name, item_name, description from `tabItem` where name in ( select item_code FROM `tabDelivery Note Item` where parent= %s) and %s like "%s" %s - limit %s, %s """ % ("%s", searchfield, "%s", - get_match_cond(doctype), "%s", "%s"), - ((filters or {}).get("delivery_note"), "%%%s%%" % txt, start, page_len)) + limit %s, %s """ + % ("%s", searchfield, "%s", get_match_cond(doctype), "%s", "%s"), + ((filters or {}).get("delivery_note"), "%%%s%%" % txt, start, page_len), + ) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 35cbc2fd85..7061ee1eea 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -19,6 +19,7 @@ from erpnext.stock.get_item_details import get_conversion_factor # TODO: Prioritize SO or WO group warehouse + class PickList(Document): def validate(self): self.validate_for_qty() @@ -27,8 +28,11 @@ class PickList(Document): self.set_item_locations() # set percentage picked in SO - for location in self.get('locations'): - if location.sales_order and frappe.db.get_value("Sales Order",location.sales_order,"per_picked") == 100: + for location in self.get("locations"): + if ( + location.sales_order + and frappe.db.get_value("Sales Order", location.sales_order, "per_picked") == 100 + ): frappe.throw("Row " + str(location.idx) + " has been picked already!") def before_submit(self): @@ -39,44 +43,62 @@ class PickList(Document): if item.sales_order_item: # update the picked_qty in SO Item - self.update_so(item.sales_order_item,item.picked_qty,item.item_code) + self.update_so(item.sales_order_item, item.picked_qty, item.item_code) - if not frappe.get_cached_value('Item', item.item_code, 'has_serial_no'): + if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"): continue if not item.serial_no: - frappe.throw(_("Row #{0}: {1} does not have any available serial numbers in {2}").format( - frappe.bold(item.idx), frappe.bold(item.item_code), frappe.bold(item.warehouse)), - title=_("Serial Nos Required")) - if len(item.serial_no.split('\n')) == item.picked_qty: + frappe.throw( + _("Row #{0}: {1} does not have any available serial numbers in {2}").format( + frappe.bold(item.idx), frappe.bold(item.item_code), frappe.bold(item.warehouse) + ), + title=_("Serial Nos Required"), + ) + if len(item.serial_no.split("\n")) == item.picked_qty: continue - frappe.throw(_('For item {0} at row {1}, count of serial numbers does not match with the picked quantity') - .format(frappe.bold(item.item_code), frappe.bold(item.idx)), title=_("Quantity Mismatch")) + frappe.throw( + _( + "For item {0} at row {1}, count of serial numbers does not match with the picked quantity" + ).format(frappe.bold(item.item_code), frappe.bold(item.idx)), + title=_("Quantity Mismatch"), + ) def before_cancel(self): - #update picked_qty in SO Item on cancel of PL - for item in self.get('locations'): + # update picked_qty in SO Item on cancel of PL + for item in self.get("locations"): if item.sales_order_item: self.update_so(item.sales_order_item, -1 * item.picked_qty, item.item_code) - def update_so(self,so_item,picked_qty,item_code): - so_doc = frappe.get_doc("Sales Order",frappe.db.get_value("Sales Order Item",so_item,"parent")) - already_picked,actual_qty = frappe.db.get_value("Sales Order Item",so_item,["picked_qty","qty"]) + def update_so(self, so_item, picked_qty, item_code): + so_doc = frappe.get_doc( + "Sales Order", frappe.db.get_value("Sales Order Item", so_item, "parent") + ) + already_picked, actual_qty = frappe.db.get_value( + "Sales Order Item", so_item, ["picked_qty", "qty"] + ) if self.docstatus == 1: - if (((already_picked + picked_qty)/ actual_qty)*100) > (100 + flt(frappe.db.get_single_value('Stock Settings', 'over_delivery_receipt_allowance'))): - frappe.throw('You are picking more than required quantity for ' + item_code + '. Check if there is any other pick list created for '+so_doc.name) + if (((already_picked + picked_qty) / actual_qty) * 100) > ( + 100 + flt(frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance")) + ): + frappe.throw( + "You are picking more than required quantity for " + + item_code + + ". Check if there is any other pick list created for " + + so_doc.name + ) - frappe.db.set_value("Sales Order Item",so_item,"picked_qty",already_picked+picked_qty) + frappe.db.set_value("Sales Order Item", so_item, "picked_qty", already_picked + picked_qty) total_picked_qty = 0 total_so_qty = 0 - for item in so_doc.get('items'): + for item in so_doc.get("items"): total_picked_qty += flt(item.picked_qty) total_so_qty += flt(item.stock_qty) - total_picked_qty=total_picked_qty + picked_qty - per_picked = total_picked_qty/total_so_qty * 100 + total_picked_qty = total_picked_qty + picked_qty + per_picked = total_picked_qty / total_so_qty * 100 - so_doc.db_set("per_picked", flt(per_picked) ,update_modified=False) + so_doc.db_set("per_picked", flt(per_picked), update_modified=False) @frappe.whitelist() def set_item_locations(self, save=False): @@ -86,20 +108,26 @@ class PickList(Document): from_warehouses = None if self.parent_warehouse: - from_warehouses = frappe.db.get_descendants('Warehouse', self.parent_warehouse) + from_warehouses = frappe.db.get_descendants("Warehouse", self.parent_warehouse) # Create replica before resetting, to handle empty table on update after submit. - locations_replica = self.get('locations') + locations_replica = self.get("locations") # reset - self.delete_key('locations') + self.delete_key("locations") for item_doc in items: item_code = item_doc.item_code - self.item_location_map.setdefault(item_code, - get_available_item_locations(item_code, from_warehouses, self.item_count_map.get(item_code), self.company)) + self.item_location_map.setdefault( + item_code, + get_available_item_locations( + item_code, from_warehouses, self.item_count_map.get(item_code), self.company + ), + ) - locations = get_items_with_location_and_quantity(item_doc, self.item_location_map, self.docstatus) + locations = get_items_with_location_and_quantity( + item_doc, self.item_location_map, self.docstatus + ) item_doc.idx = None item_doc.name = None @@ -107,23 +135,28 @@ class PickList(Document): for row in locations: location = item_doc.as_dict() location.update(row) - self.append('locations', location) + self.append("locations", location) # If table is empty on update after submit, set stock_qty, picked_qty to 0 so that indicator is red # and give feedback to the user. This is to avoid empty Pick Lists. - if not self.get('locations') and self.docstatus == 1: + if not self.get("locations") and self.docstatus == 1: for location in locations_replica: location.stock_qty = 0 location.picked_qty = 0 - self.append('locations', location) - frappe.msgprint(_("Please Restock Items and Update the Pick List to continue. To discontinue, cancel the Pick List."), - title=_("Out of Stock"), indicator="red") + self.append("locations", location) + frappe.msgprint( + _( + "Please Restock Items and Update the Pick List to continue. To discontinue, cancel the Pick List." + ), + title=_("Out of Stock"), + indicator="red", + ) if save: self.save() def aggregate_item_qty(self): - locations = self.get('locations') + locations = self.get("locations") self.item_count_map = {} # aggregate qty for same item item_map = OrderedDict() @@ -150,8 +183,9 @@ class PickList(Document): return item_map.values() def validate_for_qty(self): - if self.purpose == "Material Transfer for Manufacture" \ - and (self.for_qty is None or self.for_qty == 0): + if self.purpose == "Material Transfer for Manufacture" and ( + self.for_qty is None or self.for_qty == 0 + ): frappe.throw(_("Qty of Finished Goods Item should be greater than 0.")) def before_print(self, settings=None): @@ -163,7 +197,7 @@ class PickList(Document): group_picked_qty = defaultdict(float) for item in self.locations: - group_item_qty[(item.item_code, item.warehouse)] += item.qty + group_item_qty[(item.item_code, item.warehouse)] += item.qty group_picked_qty[(item.item_code, item.warehouse)] += item.picked_qty duplicate_list = [] @@ -187,37 +221,47 @@ def validate_item_locations(pick_list): if not pick_list.locations: frappe.throw(_("Add items in the Item Locations table")) + def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus): available_locations = item_location_map.get(item_doc.item_code) locations = [] # if stock qty is zero on submitted entry, show positive remaining qty to recalculate in case of restock. - remaining_stock_qty = item_doc.qty if (docstatus == 1 and item_doc.stock_qty == 0) else item_doc.stock_qty + remaining_stock_qty = ( + item_doc.qty if (docstatus == 1 and item_doc.stock_qty == 0) else item_doc.stock_qty + ) while remaining_stock_qty > 0 and available_locations: item_location = available_locations.pop(0) item_location = frappe._dict(item_location) - stock_qty = remaining_stock_qty if item_location.qty >= remaining_stock_qty else item_location.qty + stock_qty = ( + remaining_stock_qty if item_location.qty >= remaining_stock_qty else item_location.qty + ) qty = stock_qty / (item_doc.conversion_factor or 1) - uom_must_be_whole_number = frappe.db.get_value('UOM', item_doc.uom, 'must_be_whole_number') + uom_must_be_whole_number = frappe.db.get_value("UOM", item_doc.uom, "must_be_whole_number") if uom_must_be_whole_number: qty = floor(qty) stock_qty = qty * item_doc.conversion_factor - if not stock_qty: break + if not stock_qty: + break serial_nos = None if item_location.serial_no: - serial_nos = '\n'.join(item_location.serial_no[0: cint(stock_qty)]) + serial_nos = "\n".join(item_location.serial_no[0 : cint(stock_qty)]) - locations.append(frappe._dict({ - 'qty': qty, - 'stock_qty': stock_qty, - 'warehouse': item_location.warehouse, - 'serial_no': serial_nos, - 'batch_no': item_location.batch_no - })) + locations.append( + frappe._dict( + { + "qty": qty, + "stock_qty": stock_qty, + "warehouse": item_location.warehouse, + "serial_no": serial_nos, + "batch_no": item_location.batch_no, + } + ) + ) remaining_stock_qty -= stock_qty @@ -227,55 +271,69 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus) item_location.qty = qty_diff if item_location.serial_no: # set remaining serial numbers - item_location.serial_no = item_location.serial_no[-int(qty_diff):] + item_location.serial_no = item_location.serial_no[-int(qty_diff) :] available_locations = [item_location] + available_locations # update available locations for the item item_location_map[item_doc.item_code] = available_locations return locations -def get_available_item_locations(item_code, from_warehouses, required_qty, company, ignore_validation=False): + +def get_available_item_locations( + item_code, from_warehouses, required_qty, company, ignore_validation=False +): locations = [] - has_serial_no = frappe.get_cached_value('Item', item_code, 'has_serial_no') - has_batch_no = frappe.get_cached_value('Item', item_code, 'has_batch_no') + has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no") + has_batch_no = frappe.get_cached_value("Item", item_code, "has_batch_no") if has_batch_no and has_serial_no: - locations = get_available_item_locations_for_serial_and_batched_item(item_code, from_warehouses, required_qty, company) + locations = get_available_item_locations_for_serial_and_batched_item( + item_code, from_warehouses, required_qty, company + ) elif has_serial_no: - locations = get_available_item_locations_for_serialized_item(item_code, from_warehouses, required_qty, company) + locations = get_available_item_locations_for_serialized_item( + item_code, from_warehouses, required_qty, company + ) elif has_batch_no: - locations = get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty, company) + locations = get_available_item_locations_for_batched_item( + item_code, from_warehouses, required_qty, company + ) else: - locations = get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty, company) + locations = get_available_item_locations_for_other_item( + item_code, from_warehouses, required_qty, company + ) - total_qty_available = sum(location.get('qty') for location in locations) + total_qty_available = sum(location.get("qty") for location in locations) remaining_qty = required_qty - total_qty_available if remaining_qty > 0 and not ignore_validation: - frappe.msgprint(_('{0} units of Item {1} is not available.') - .format(remaining_qty, frappe.get_desk_link('Item', item_code)), - title=_("Insufficient Stock")) + frappe.msgprint( + _("{0} units of Item {1} is not available.").format( + remaining_qty, frappe.get_desk_link("Item", item_code) + ), + title=_("Insufficient Stock"), + ) return locations -def get_available_item_locations_for_serialized_item(item_code, from_warehouses, required_qty, company): - filters = frappe._dict({ - 'item_code': item_code, - 'company': company, - 'warehouse': ['!=', ''] - }) +def get_available_item_locations_for_serialized_item( + item_code, from_warehouses, required_qty, company +): + filters = frappe._dict({"item_code": item_code, "company": company, "warehouse": ["!=", ""]}) if from_warehouses: - filters.warehouse = ['in', from_warehouses] + filters.warehouse = ["in", from_warehouses] - serial_nos = frappe.get_all('Serial No', - fields=['name', 'warehouse'], + serial_nos = frappe.get_all( + "Serial No", + fields=["name", "warehouse"], filters=filters, limit=required_qty, - order_by='purchase_date', - as_list=1) + order_by="purchase_date", + as_list=1, + ) warehouse_serial_nos_map = frappe._dict() for serial_no, warehouse in serial_nos: @@ -283,17 +341,17 @@ def get_available_item_locations_for_serialized_item(item_code, from_warehouses, locations = [] for warehouse, serial_nos in warehouse_serial_nos_map.items(): - locations.append({ - 'qty': len(serial_nos), - 'warehouse': warehouse, - 'serial_no': serial_nos - }) + locations.append({"qty": len(serial_nos), "warehouse": warehouse, "serial_no": serial_nos}) return locations -def get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty, company): - warehouse_condition = 'and warehouse in %(warehouses)s' if from_warehouses else '' - batch_locations = frappe.db.sql(""" + +def get_available_item_locations_for_batched_item( + item_code, from_warehouses, required_qty, company +): + warehouse_condition = "and warehouse in %(warehouses)s" if from_warehouses else "" + batch_locations = frappe.db.sql( + """ SELECT sle.`warehouse`, sle.`batch_no`, @@ -314,84 +372,94 @@ def get_available_item_locations_for_batched_item(item_code, from_warehouses, re sle.`item_code` HAVING `qty` > 0 ORDER BY IFNULL(batch.`expiry_date`, '2200-01-01'), batch.`creation` - """.format(warehouse_condition=warehouse_condition), { #nosec - 'item_code': item_code, - 'company': company, - 'today': today(), - 'warehouses': from_warehouses - }, as_dict=1) + """.format( + warehouse_condition=warehouse_condition + ), + { # nosec + "item_code": item_code, + "company": company, + "today": today(), + "warehouses": from_warehouses, + }, + as_dict=1, + ) return batch_locations -def get_available_item_locations_for_serial_and_batched_item(item_code, from_warehouses, required_qty, company): - # Get batch nos by FIFO - locations = get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty, company) - filters = frappe._dict({ - 'item_code': item_code, - 'company': company, - 'warehouse': ['!=', ''], - 'batch_no': '' - }) +def get_available_item_locations_for_serial_and_batched_item( + item_code, from_warehouses, required_qty, company +): + # Get batch nos by FIFO + locations = get_available_item_locations_for_batched_item( + item_code, from_warehouses, required_qty, company + ) + + filters = frappe._dict( + {"item_code": item_code, "company": company, "warehouse": ["!=", ""], "batch_no": ""} + ) # Get Serial Nos by FIFO for Batch No for location in locations: filters.batch_no = location.batch_no filters.warehouse = location.warehouse - location.qty = required_qty if location.qty > required_qty else location.qty # if extra qty in batch + location.qty = ( + required_qty if location.qty > required_qty else location.qty + ) # if extra qty in batch - serial_nos = frappe.get_list('Serial No', - fields=['name'], - filters=filters, - limit=location.qty, - order_by='purchase_date') + serial_nos = frappe.get_list( + "Serial No", fields=["name"], filters=filters, limit=location.qty, order_by="purchase_date" + ) serial_nos = [sn.name for sn in serial_nos] location.serial_no = serial_nos return locations + def get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty, company): # gets all items available in different warehouses - warehouses = [x.get('name') for x in frappe.get_list("Warehouse", {'company': company}, "name")] + warehouses = [x.get("name") for x in frappe.get_list("Warehouse", {"company": company}, "name")] - filters = frappe._dict({ - 'item_code': item_code, - 'warehouse': ['in', warehouses], - 'actual_qty': ['>', 0] - }) + filters = frappe._dict( + {"item_code": item_code, "warehouse": ["in", warehouses], "actual_qty": [">", 0]} + ) if from_warehouses: - filters.warehouse = ['in', from_warehouses] + filters.warehouse = ["in", from_warehouses] - item_locations = frappe.get_all('Bin', - fields=['warehouse', 'actual_qty as qty'], + item_locations = frappe.get_all( + "Bin", + fields=["warehouse", "actual_qty as qty"], filters=filters, limit=required_qty, - order_by='creation') + order_by="creation", + ) return item_locations @frappe.whitelist() def create_delivery_note(source_name, target_doc=None): - pick_list = frappe.get_doc('Pick List', source_name) + pick_list = frappe.get_doc("Pick List", source_name) validate_item_locations(pick_list) sales_dict = dict() sales_orders = [] delivery_note = None for location in pick_list.locations: if location.sales_order: - sales_orders.append([frappe.db.get_value("Sales Order",location.sales_order,'customer'),location.sales_order]) + sales_orders.append( + [frappe.db.get_value("Sales Order", location.sales_order, "customer"), location.sales_order] + ) # Group sales orders by customer - for key,keydata in groupby(sales_orders,key=itemgetter(0)): + for key, keydata in groupby(sales_orders, key=itemgetter(0)): sales_dict[key] = set([d[1] for d in keydata]) if sales_dict: - delivery_note = create_dn_with_so(sales_dict,pick_list) + delivery_note = create_dn_with_so(sales_dict, pick_list) is_item_wo_so = 0 - for location in pick_list.locations : + for location in pick_list.locations: if not location.sales_order: is_item_wo_so = 1 break @@ -399,64 +467,69 @@ def create_delivery_note(source_name, target_doc=None): # Create a DN for items without sales orders as well delivery_note = create_dn_wo_so(pick_list) - frappe.msgprint(_('Delivery Note(s) created for the Pick List')) + frappe.msgprint(_("Delivery Note(s) created for the Pick List")) return delivery_note + def create_dn_wo_so(pick_list): - delivery_note = frappe.new_doc("Delivery Note") + delivery_note = frappe.new_doc("Delivery Note") - item_table_mapper_without_so = { - 'doctype': 'Delivery Note Item', - 'field_map': { - 'rate': 'rate', - 'name': 'name', - 'parent': '', - } - } - map_pl_locations(pick_list,item_table_mapper_without_so,delivery_note) - delivery_note.insert(ignore_mandatory = True) + item_table_mapper_without_so = { + "doctype": "Delivery Note Item", + "field_map": { + "rate": "rate", + "name": "name", + "parent": "", + }, + } + map_pl_locations(pick_list, item_table_mapper_without_so, delivery_note) + delivery_note.insert(ignore_mandatory=True) - return delivery_note + return delivery_note -def create_dn_with_so(sales_dict,pick_list): +def create_dn_with_so(sales_dict, pick_list): delivery_note = None for customer in sales_dict: for so in sales_dict[customer]: delivery_note = None - delivery_note = create_delivery_note_from_sales_order(so, - delivery_note, skip_item_mapping=True) + delivery_note = create_delivery_note_from_sales_order(so, delivery_note, skip_item_mapping=True) item_table_mapper = { - 'doctype': 'Delivery Note Item', - 'field_map': { - 'rate': 'rate', - 'name': 'so_detail', - 'parent': 'against_sales_order', + "doctype": "Delivery Note Item", + "field_map": { + "rate": "rate", + "name": "so_detail", + "parent": "against_sales_order", }, - 'condition': lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1 + "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) + and doc.delivered_by_supplier != 1, } break if delivery_note: # map all items of all sales orders of that customer for so in sales_dict[customer]: - map_pl_locations(pick_list,item_table_mapper,delivery_note,so) - delivery_note.insert(ignore_mandatory = True) + map_pl_locations(pick_list, item_table_mapper, delivery_note, so) + delivery_note.insert(ignore_mandatory=True) return delivery_note -def map_pl_locations(pick_list,item_mapper,delivery_note,sales_order = None): + +def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None): for location in pick_list.locations: if location.sales_order == sales_order: if location.sales_order_item: - sales_order_item = frappe.get_cached_doc('Sales Order Item', {'name':location.sales_order_item}) + sales_order_item = frappe.get_cached_doc( + "Sales Order Item", {"name": location.sales_order_item} + ) else: sales_order_item = None - source_doc, table_mapper = [sales_order_item, item_mapper] if sales_order_item \ - else [location, item_mapper] + source_doc, table_mapper = ( + [sales_order_item, item_mapper] if sales_order_item else [location, item_mapper] + ) dn_item = map_child_doc(source_doc, delivery_note, table_mapper) @@ -471,7 +544,7 @@ def map_pl_locations(pick_list,item_mapper,delivery_note,sales_order = None): delivery_note.pick_list = pick_list.name delivery_note.company = pick_list.company - delivery_note.customer = frappe.get_value("Sales Order",sales_order,"customer") + delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer") @frappe.whitelist() @@ -479,17 +552,17 @@ def create_stock_entry(pick_list): pick_list = frappe.get_doc(json.loads(pick_list)) validate_item_locations(pick_list) - if stock_entry_exists(pick_list.get('name')): - return frappe.msgprint(_('Stock Entry has been already created against this Pick List')) + if stock_entry_exists(pick_list.get("name")): + return frappe.msgprint(_("Stock Entry has been already created against this Pick List")) - stock_entry = frappe.new_doc('Stock Entry') - stock_entry.pick_list = pick_list.get('name') - stock_entry.purpose = pick_list.get('purpose') + stock_entry = frappe.new_doc("Stock Entry") + stock_entry.pick_list = pick_list.get("name") + stock_entry.purpose = pick_list.get("purpose") stock_entry.set_stock_entry_type() - if pick_list.get('work_order'): + if pick_list.get("work_order"): stock_entry = update_stock_entry_based_on_work_order(pick_list, stock_entry) - elif pick_list.get('material_request'): + elif pick_list.get("material_request"): stock_entry = update_stock_entry_based_on_material_request(pick_list, stock_entry) else: stock_entry = update_stock_entry_items_with_no_reference(pick_list, stock_entry) @@ -499,9 +572,11 @@ def create_stock_entry(pick_list): return stock_entry.as_dict() + @frappe.whitelist() def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filters, as_dict): - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT `name`, `company`, `planned_start_date` FROM @@ -517,25 +592,27 @@ def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filte LIMIT %(start)s, %(page_length)s""", { - 'txt': "%%%s%%" % txt, - '_txt': txt.replace('%', ''), - 'start': start, - 'page_length': frappe.utils.cint(page_length), - 'company': filters.get('company') - }, as_dict=as_dict) + "txt": "%%%s%%" % txt, + "_txt": txt.replace("%", ""), + "start": start, + "page_length": frappe.utils.cint(page_length), + "company": filters.get("company"), + }, + as_dict=as_dict, + ) + @frappe.whitelist() def target_document_exists(pick_list_name, purpose): - if purpose == 'Delivery': - return frappe.db.exists('Delivery Note', { - 'pick_list': pick_list_name - }) + if purpose == "Delivery": + return frappe.db.exists("Delivery Note", {"pick_list": pick_list_name}) return stock_entry_exists(pick_list_name) + @frappe.whitelist() def get_item_details(item_code, uom=None): - details = frappe.db.get_value('Item', item_code, ['stock_uom', 'name'], as_dict=1) + details = frappe.db.get_value("Item", item_code, ["stock_uom", "name"], as_dict=1) details.uom = uom or details.stock_uom if uom: details.update(get_conversion_factor(item_code, uom)) @@ -544,37 +621,37 @@ def get_item_details(item_code, uom=None): def update_delivery_note_item(source, target, delivery_note): - cost_center = frappe.db.get_value('Project', delivery_note.project, 'cost_center') + cost_center = frappe.db.get_value("Project", delivery_note.project, "cost_center") if not cost_center: - cost_center = get_cost_center(source.item_code, 'Item', delivery_note.company) + cost_center = get_cost_center(source.item_code, "Item", delivery_note.company) if not cost_center: - cost_center = get_cost_center(source.item_group, 'Item Group', delivery_note.company) + cost_center = get_cost_center(source.item_group, "Item Group", delivery_note.company) target.cost_center = cost_center + def get_cost_center(for_item, from_doctype, company): - '''Returns Cost Center for Item or Item Group''' - return frappe.db.get_value('Item Default', - fieldname=['buying_cost_center'], - filters={ - 'parent': for_item, - 'parenttype': from_doctype, - 'company': company - }) + """Returns Cost Center for Item or Item Group""" + return frappe.db.get_value( + "Item Default", + fieldname=["buying_cost_center"], + filters={"parent": for_item, "parenttype": from_doctype, "company": company}, + ) + def set_delivery_note_missing_values(target): - target.run_method('set_missing_values') - target.run_method('set_po_nos') - target.run_method('calculate_taxes_and_totals') + target.run_method("set_missing_values") + target.run_method("set_po_nos") + target.run_method("calculate_taxes_and_totals") + def stock_entry_exists(pick_list_name): - return frappe.db.exists('Stock Entry', { - 'pick_list': pick_list_name - }) + return frappe.db.exists("Stock Entry", {"pick_list": pick_list_name}) + def update_stock_entry_based_on_work_order(pick_list, stock_entry): - work_order = frappe.get_doc("Work Order", pick_list.get('work_order')) + work_order = frappe.get_doc("Work Order", pick_list.get("work_order")) stock_entry.work_order = work_order.name stock_entry.company = work_order.company @@ -583,10 +660,11 @@ def update_stock_entry_based_on_work_order(pick_list, stock_entry): stock_entry.use_multi_level_bom = work_order.use_multi_level_bom stock_entry.fg_completed_qty = pick_list.for_qty if work_order.bom_no: - stock_entry.inspection_required = frappe.db.get_value('BOM', - work_order.bom_no, 'inspection_required') + stock_entry.inspection_required = frappe.db.get_value( + "BOM", work_order.bom_no, "inspection_required" + ) - is_wip_warehouse_group = frappe.db.get_value('Warehouse', work_order.wip_warehouse, 'is_group') + is_wip_warehouse_group = frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group") if not (is_wip_warehouse_group and work_order.skip_transfer): wip_warehouse = work_order.wip_warehouse else: @@ -600,32 +678,36 @@ def update_stock_entry_based_on_work_order(pick_list, stock_entry): update_common_item_properties(item, location) item.t_warehouse = wip_warehouse - stock_entry.append('items', item) + stock_entry.append("items", item) return stock_entry + def update_stock_entry_based_on_material_request(pick_list, stock_entry): for location in pick_list.locations: target_warehouse = None if location.material_request_item: - target_warehouse = frappe.get_value('Material Request Item', - location.material_request_item, 'warehouse') + target_warehouse = frappe.get_value( + "Material Request Item", location.material_request_item, "warehouse" + ) item = frappe._dict() update_common_item_properties(item, location) item.t_warehouse = target_warehouse - stock_entry.append('items', item) + stock_entry.append("items", item) return stock_entry + def update_stock_entry_items_with_no_reference(pick_list, stock_entry): for location in pick_list.locations: item = frappe._dict() update_common_item_properties(item, location) - stock_entry.append('items', item) + stock_entry.append("items", item) return stock_entry + def update_common_item_properties(item, location): item.item_code = location.item_code item.s_warehouse = location.warehouse diff --git a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py index ec3047e98f..92e57bed22 100644 --- a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py +++ b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py @@ -1,9 +1,7 @@ def get_data(): return { - 'fieldname': 'pick_list', - 'transactions': [ - { - 'items': ['Stock Entry', 'Delivery Note'] - }, - ] + "fieldname": "pick_list", + "transactions": [ + {"items": ["Stock Entry", "Delivery Note"]}, + ], } diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index f60104c09a..7496b6b179 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -4,7 +4,7 @@ import frappe from frappe import _dict -test_dependencies = ['Item', 'Sales Invoice', 'Stock Entry', 'Batch'] +test_dependencies = ["Item", "Sales Invoice", "Stock Entry", "Batch"] from frappe.tests.utils import FrappeTestCase @@ -19,146 +19,174 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( class TestPickList(FrappeTestCase): def test_pick_list_picks_warehouse_for_each_item(self): try: - frappe.get_doc({ - 'doctype': 'Stock Reconciliation', - 'company': '_Test Company', - 'purpose': 'Opening Stock', - 'expense_account': 'Temporary Opening - _TC', - 'items': [{ - 'item_code': '_Test Item', - 'warehouse': '_Test Warehouse - _TC', - 'valuation_rate': 100, - 'qty': 5 - }] - }).submit() + frappe.get_doc( + { + "doctype": "Stock Reconciliation", + "company": "_Test Company", + "purpose": "Opening Stock", + "expense_account": "Temporary Opening - _TC", + "items": [ + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "valuation_rate": 100, + "qty": 5, + } + ], + } + ).submit() except EmptyStockReconciliationItemsError: pass - pick_list = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'customer': '_Test Customer', - 'items_based_on': 'Sales Order', - 'purpose': 'Delivery', - 'locations': [{ - 'item_code': '_Test Item', - 'qty': 5, - 'stock_qty': 5, - 'conversion_factor': 1, - 'sales_order': '_T-Sales Order-1', - 'sales_order_item': '_T-Sales Order-1_item', - }] - }) + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "customer": "_Test Customer", + "items_based_on": "Sales Order", + "purpose": "Delivery", + "locations": [ + { + "item_code": "_Test Item", + "qty": 5, + "stock_qty": 5, + "conversion_factor": 1, + "sales_order": "_T-Sales Order-1", + "sales_order_item": "_T-Sales Order-1_item", + } + ], + } + ) pick_list.set_item_locations() - self.assertEqual(pick_list.locations[0].item_code, '_Test Item') - self.assertEqual(pick_list.locations[0].warehouse, '_Test Warehouse - _TC') + self.assertEqual(pick_list.locations[0].item_code, "_Test Item") + self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse - _TC") self.assertEqual(pick_list.locations[0].qty, 5) def test_pick_list_splits_row_according_to_warehouse_availability(self): try: - frappe.get_doc({ - 'doctype': 'Stock Reconciliation', - 'company': '_Test Company', - 'purpose': 'Opening Stock', - 'expense_account': 'Temporary Opening - _TC', - 'items': [{ - 'item_code': '_Test Item Warehouse Group Wise Reorder', - 'warehouse': '_Test Warehouse Group-C1 - _TC', - 'valuation_rate': 100, - 'qty': 5 - }] - }).submit() + frappe.get_doc( + { + "doctype": "Stock Reconciliation", + "company": "_Test Company", + "purpose": "Opening Stock", + "expense_account": "Temporary Opening - _TC", + "items": [ + { + "item_code": "_Test Item Warehouse Group Wise Reorder", + "warehouse": "_Test Warehouse Group-C1 - _TC", + "valuation_rate": 100, + "qty": 5, + } + ], + } + ).submit() except EmptyStockReconciliationItemsError: pass try: - frappe.get_doc({ - 'doctype': 'Stock Reconciliation', - 'company': '_Test Company', - 'purpose': 'Opening Stock', - 'expense_account': 'Temporary Opening - _TC', - 'items': [{ - 'item_code': '_Test Item Warehouse Group Wise Reorder', - 'warehouse': '_Test Warehouse 2 - _TC', - 'valuation_rate': 400, - 'qty': 10 - }] - }).submit() + frappe.get_doc( + { + "doctype": "Stock Reconciliation", + "company": "_Test Company", + "purpose": "Opening Stock", + "expense_account": "Temporary Opening - _TC", + "items": [ + { + "item_code": "_Test Item Warehouse Group Wise Reorder", + "warehouse": "_Test Warehouse 2 - _TC", + "valuation_rate": 400, + "qty": 10, + } + ], + } + ).submit() except EmptyStockReconciliationItemsError: pass - pick_list = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'customer': '_Test Customer', - 'items_based_on': 'Sales Order', - 'purpose': 'Delivery', - 'locations': [{ - 'item_code': '_Test Item Warehouse Group Wise Reorder', - 'qty': 1000, - 'stock_qty': 1000, - 'conversion_factor': 1, - 'sales_order': '_T-Sales Order-1', - 'sales_order_item': '_T-Sales Order-1_item', - }] - }) + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "customer": "_Test Customer", + "items_based_on": "Sales Order", + "purpose": "Delivery", + "locations": [ + { + "item_code": "_Test Item Warehouse Group Wise Reorder", + "qty": 1000, + "stock_qty": 1000, + "conversion_factor": 1, + "sales_order": "_T-Sales Order-1", + "sales_order_item": "_T-Sales Order-1_item", + } + ], + } + ) pick_list.set_item_locations() - self.assertEqual(pick_list.locations[0].item_code, '_Test Item Warehouse Group Wise Reorder') - self.assertEqual(pick_list.locations[0].warehouse, '_Test Warehouse Group-C1 - _TC') + self.assertEqual(pick_list.locations[0].item_code, "_Test Item Warehouse Group Wise Reorder") + self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse Group-C1 - _TC") self.assertEqual(pick_list.locations[0].qty, 5) - self.assertEqual(pick_list.locations[1].item_code, '_Test Item Warehouse Group Wise Reorder') - self.assertEqual(pick_list.locations[1].warehouse, '_Test Warehouse 2 - _TC') + self.assertEqual(pick_list.locations[1].item_code, "_Test Item Warehouse Group Wise Reorder") + self.assertEqual(pick_list.locations[1].warehouse, "_Test Warehouse 2 - _TC") self.assertEqual(pick_list.locations[1].qty, 10) def test_pick_list_shows_serial_no_for_serialized_item(self): - stock_reconciliation = frappe.get_doc({ - 'doctype': 'Stock Reconciliation', - 'purpose': 'Stock Reconciliation', - 'company': '_Test Company', - 'items': [{ - 'item_code': '_Test Serialized Item', - 'warehouse': '_Test Warehouse - _TC', - 'valuation_rate': 100, - 'qty': 5, - 'serial_no': '123450\n123451\n123452\n123453\n123454' - }] - }) + stock_reconciliation = frappe.get_doc( + { + "doctype": "Stock Reconciliation", + "purpose": "Stock Reconciliation", + "company": "_Test Company", + "items": [ + { + "item_code": "_Test Serialized Item", + "warehouse": "_Test Warehouse - _TC", + "valuation_rate": 100, + "qty": 5, + "serial_no": "123450\n123451\n123452\n123453\n123454", + } + ], + } + ) try: stock_reconciliation.submit() except EmptyStockReconciliationItemsError: pass - pick_list = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'customer': '_Test Customer', - 'items_based_on': 'Sales Order', - 'purpose': 'Delivery', - 'locations': [{ - 'item_code': '_Test Serialized Item', - 'qty': 1000, - 'stock_qty': 1000, - 'conversion_factor': 1, - 'sales_order': '_T-Sales Order-1', - 'sales_order_item': '_T-Sales Order-1_item', - }] - }) + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "customer": "_Test Customer", + "items_based_on": "Sales Order", + "purpose": "Delivery", + "locations": [ + { + "item_code": "_Test Serialized Item", + "qty": 1000, + "stock_qty": 1000, + "conversion_factor": 1, + "sales_order": "_T-Sales Order-1", + "sales_order_item": "_T-Sales Order-1_item", + } + ], + } + ) pick_list.set_item_locations() - self.assertEqual(pick_list.locations[0].item_code, '_Test Serialized Item') - self.assertEqual(pick_list.locations[0].warehouse, '_Test Warehouse - _TC') + self.assertEqual(pick_list.locations[0].item_code, "_Test Serialized Item") + self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse - _TC") self.assertEqual(pick_list.locations[0].qty, 5) - self.assertEqual(pick_list.locations[0].serial_no, '123450\n123451\n123452\n123453\n123454') + self.assertEqual(pick_list.locations[0].serial_no, "123450\n123451\n123452\n123453\n123454") def test_pick_list_shows_batch_no_for_batched_item(self): # check if oldest batch no is picked - item = frappe.db.exists("Item", {'item_name': 'Batched Item'}) + item = frappe.db.exists("Item", {"item_name": "Batched Item"}) if not item: item = create_item("Batched Item") item.has_batch_no = 1 @@ -166,7 +194,7 @@ class TestPickList(FrappeTestCase): item.batch_number_series = "B-BATCH-.##" item.save() else: - item = frappe.get_doc("Item", {'item_name': 'Batched Item'}) + item = frappe.get_doc("Item", {"item_name": "Batched Item"}) pr1 = make_purchase_receipt(item_code="Batched Item", qty=1, rate=100.0) @@ -175,27 +203,30 @@ class TestPickList(FrappeTestCase): pr2 = make_purchase_receipt(item_code="Batched Item", qty=2, rate=100.0) - pick_list = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'purpose': 'Material Transfer', - 'locations': [{ - 'item_code': 'Batched Item', - 'qty': 1, - 'stock_qty': 1, - 'conversion_factor': 1, - }] - }) + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "purpose": "Material Transfer", + "locations": [ + { + "item_code": "Batched Item", + "qty": 1, + "stock_qty": 1, + "conversion_factor": 1, + } + ], + } + ) pick_list.set_item_locations() self.assertEqual(pick_list.locations[0].batch_no, oldest_batch_no) pr1.cancel() pr2.cancel() - def test_pick_list_for_batched_and_serialised_item(self): # check if oldest batch no and serial nos are picked - item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'}) + item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"}) if not item: item = create_item("Batched and Serialised Item") item.has_batch_no = 1 @@ -205,7 +236,7 @@ class TestPickList(FrappeTestCase): item.serial_no_series = "S-.####" item.save() else: - item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'}) + item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"}) pr1 = make_purchase_receipt(item_code="Batched and Serialised Item", qty=2, rate=100.0) @@ -215,17 +246,21 @@ class TestPickList(FrappeTestCase): pr2 = make_purchase_receipt(item_code="Batched and Serialised Item", qty=2, rate=100.0) - pick_list = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'purpose': 'Material Transfer', - 'locations': [{ - 'item_code': 'Batched and Serialised Item', - 'qty': 2, - 'stock_qty': 2, - 'conversion_factor': 1, - }] - }) + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "purpose": "Material Transfer", + "locations": [ + { + "item_code": "Batched and Serialised Item", + "qty": 2, + "stock_qty": 2, + "conversion_factor": 1, + } + ], + } + ) pick_list.set_item_locations() self.assertEqual(pick_list.locations[0].batch_no, oldest_batch_no) @@ -236,64 +271,71 @@ class TestPickList(FrappeTestCase): def test_pick_list_for_items_from_multiple_sales_orders(self): try: - frappe.get_doc({ - 'doctype': 'Stock Reconciliation', - 'company': '_Test Company', - 'purpose': 'Opening Stock', - 'expense_account': 'Temporary Opening - _TC', - 'items': [{ - 'item_code': '_Test Item', - 'warehouse': '_Test Warehouse - _TC', - 'valuation_rate': 100, - 'qty': 10 - }] - }).submit() + frappe.get_doc( + { + "doctype": "Stock Reconciliation", + "company": "_Test Company", + "purpose": "Opening Stock", + "expense_account": "Temporary Opening - _TC", + "items": [ + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "valuation_rate": 100, + "qty": 10, + } + ], + } + ).submit() except EmptyStockReconciliationItemsError: pass - sales_order = frappe.get_doc({ - 'doctype': "Sales Order", - 'customer': '_Test Customer', - 'company': '_Test Company', - 'items': [{ - 'item_code': '_Test Item', - 'qty': 10, - 'delivery_date': frappe.utils.today() - }], - }) + sales_order = frappe.get_doc( + { + "doctype": "Sales Order", + "customer": "_Test Customer", + "company": "_Test Company", + "items": [{"item_code": "_Test Item", "qty": 10, "delivery_date": frappe.utils.today()}], + } + ) sales_order.submit() - pick_list = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'customer': '_Test Customer', - 'items_based_on': 'Sales Order', - 'purpose': 'Delivery', - 'locations': [{ - 'item_code': '_Test Item', - 'qty': 5, - 'stock_qty': 5, - 'conversion_factor': 1, - 'sales_order': '_T-Sales Order-1', - 'sales_order_item': '_T-Sales Order-1_item', - }, { - 'item_code': '_Test Item', - 'qty': 5, - 'stock_qty': 5, - 'conversion_factor': 1, - 'sales_order': sales_order.name, - 'sales_order_item': sales_order.items[0].name, - }] - }) + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "customer": "_Test Customer", + "items_based_on": "Sales Order", + "purpose": "Delivery", + "locations": [ + { + "item_code": "_Test Item", + "qty": 5, + "stock_qty": 5, + "conversion_factor": 1, + "sales_order": "_T-Sales Order-1", + "sales_order_item": "_T-Sales Order-1_item", + }, + { + "item_code": "_Test Item", + "qty": 5, + "stock_qty": 5, + "conversion_factor": 1, + "sales_order": sales_order.name, + "sales_order_item": sales_order.items[0].name, + }, + ], + } + ) pick_list.set_item_locations() - self.assertEqual(pick_list.locations[0].item_code, '_Test Item') - self.assertEqual(pick_list.locations[0].warehouse, '_Test Warehouse - _TC') + self.assertEqual(pick_list.locations[0].item_code, "_Test Item") + self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse - _TC") self.assertEqual(pick_list.locations[0].qty, 5) - self.assertEqual(pick_list.locations[0].sales_order_item, '_T-Sales Order-1_item') + self.assertEqual(pick_list.locations[0].sales_order_item, "_T-Sales Order-1_item") - self.assertEqual(pick_list.locations[1].item_code, '_Test Item') - self.assertEqual(pick_list.locations[1].warehouse, '_Test Warehouse - _TC') + self.assertEqual(pick_list.locations[1].item_code, "_Test Item") + self.assertEqual(pick_list.locations[1].warehouse, "_Test Warehouse - _TC") self.assertEqual(pick_list.locations[1].qty, 5) self.assertEqual(pick_list.locations[1].sales_order_item, sales_order.items[0].name) @@ -301,47 +343,57 @@ class TestPickList(FrappeTestCase): purchase_receipt = make_purchase_receipt(item_code="_Test Item", qty=10) purchase_receipt.submit() - sales_order = frappe.get_doc({ - 'doctype': 'Sales Order', - 'customer': '_Test Customer', - 'company': '_Test Company', - 'items': [{ - 'item_code': '_Test Item', - 'qty': 1, - 'conversion_factor': 5, - 'stock_qty':5, - 'delivery_date': frappe.utils.today() - }, { - 'item_code': '_Test Item', - 'qty': 1, - 'conversion_factor': 1, - 'delivery_date': frappe.utils.today() - }], - }).insert() + sales_order = frappe.get_doc( + { + "doctype": "Sales Order", + "customer": "_Test Customer", + "company": "_Test Company", + "items": [ + { + "item_code": "_Test Item", + "qty": 1, + "conversion_factor": 5, + "stock_qty": 5, + "delivery_date": frappe.utils.today(), + }, + { + "item_code": "_Test Item", + "qty": 1, + "conversion_factor": 1, + "delivery_date": frappe.utils.today(), + }, + ], + } + ).insert() sales_order.submit() - pick_list = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'customer': '_Test Customer', - 'items_based_on': 'Sales Order', - 'purpose': 'Delivery', - 'locations': [{ - 'item_code': '_Test Item', - 'qty': 2, - 'stock_qty': 1, - 'conversion_factor': 0.5, - 'sales_order': sales_order.name, - 'sales_order_item': sales_order.items[0].name , - }, { - 'item_code': '_Test Item', - 'qty': 1, - 'stock_qty': 1, - 'conversion_factor': 1, - 'sales_order': sales_order.name, - 'sales_order_item': sales_order.items[1].name , - }] - }) + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "customer": "_Test Customer", + "items_based_on": "Sales Order", + "purpose": "Delivery", + "locations": [ + { + "item_code": "_Test Item", + "qty": 2, + "stock_qty": 1, + "conversion_factor": 0.5, + "sales_order": sales_order.name, + "sales_order_item": sales_order.items[0].name, + }, + { + "item_code": "_Test Item", + "qty": 1, + "stock_qty": 1, + "conversion_factor": 1, + "sales_order": sales_order.name, + "sales_order_item": sales_order.items[1].name, + }, + ], + } + ) pick_list.set_item_locations() pick_list.submit() @@ -349,7 +401,9 @@ class TestPickList(FrappeTestCase): self.assertEqual(pick_list.locations[0].qty, delivery_note.items[0].qty) self.assertEqual(pick_list.locations[1].qty, delivery_note.items[1].qty) - self.assertEqual(sales_order.items[0].conversion_factor, delivery_note.items[0].conversion_factor) + self.assertEqual( + sales_order.items[0].conversion_factor, delivery_note.items[0].conversion_factor + ) pick_list.cancel() sales_order.cancel() @@ -362,22 +416,30 @@ class TestPickList(FrappeTestCase): self.assertEqual(b.get(key), value, msg=f"{key} doesn't match") # nothing should be grouped - pl = frappe.get_doc(doctype="Pick List", group_same_items=True, locations=[ - _dict(item_code="A", warehouse="X", qty=1, picked_qty=2), - _dict(item_code="B", warehouse="X", qty=1, picked_qty=2), - _dict(item_code="A", warehouse="Y", qty=1, picked_qty=2), - _dict(item_code="B", warehouse="Y", qty=1, picked_qty=2), - ]) + pl = frappe.get_doc( + doctype="Pick List", + group_same_items=True, + locations=[ + _dict(item_code="A", warehouse="X", qty=1, picked_qty=2), + _dict(item_code="B", warehouse="X", qty=1, picked_qty=2), + _dict(item_code="A", warehouse="Y", qty=1, picked_qty=2), + _dict(item_code="B", warehouse="Y", qty=1, picked_qty=2), + ], + ) pl.before_print() self.assertEqual(len(pl.locations), 4) # grouping should halve the number of items - pl = frappe.get_doc(doctype="Pick List", group_same_items=True, locations=[ - _dict(item_code="A", warehouse="X", qty=5, picked_qty=1), - _dict(item_code="B", warehouse="Y", qty=4, picked_qty=2), - _dict(item_code="A", warehouse="X", qty=3, picked_qty=2), - _dict(item_code="B", warehouse="Y", qty=2, picked_qty=2), - ]) + pl = frappe.get_doc( + doctype="Pick List", + group_same_items=True, + locations=[ + _dict(item_code="A", warehouse="X", qty=5, picked_qty=1), + _dict(item_code="B", warehouse="Y", qty=4, picked_qty=2), + _dict(item_code="A", warehouse="X", qty=3, picked_qty=2), + _dict(item_code="B", warehouse="Y", qty=2, picked_qty=2), + ], + ) pl.before_print() self.assertEqual(len(pl.locations), 2) @@ -389,93 +451,118 @@ class TestPickList(FrappeTestCase): _compare_dicts(expected_item, created_item) def test_multiple_dn_creation(self): - sales_order_1 = frappe.get_doc({ - 'doctype': 'Sales Order', - 'customer': '_Test Customer', - 'company': '_Test Company', - 'items': [{ - 'item_code': '_Test Item', - 'qty': 1, - 'conversion_factor': 1, - 'delivery_date': frappe.utils.today() - }], - }).insert() - sales_order_1.submit() - sales_order_2 = frappe.get_doc({ - 'doctype': 'Sales Order', - 'customer': '_Test Customer 1', - 'company': '_Test Company', - 'items': [{ - 'item_code': '_Test Item 2', - 'qty': 1, - 'conversion_factor': 1, - 'delivery_date': frappe.utils.today() - }, + sales_order_1 = frappe.get_doc( + { + "doctype": "Sales Order", + "customer": "_Test Customer", + "company": "_Test Company", + "items": [ + { + "item_code": "_Test Item", + "qty": 1, + "conversion_factor": 1, + "delivery_date": frappe.utils.today(), + } ], - }).insert() - sales_order_2.submit() - pick_list = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'items_based_on': 'Sales Order', - 'purpose': 'Delivery', - 'picker':'P001', - 'locations': [{ - 'item_code': '_Test Item ', - 'qty': 1, - 'stock_qty': 1, - 'conversion_factor': 1, - 'sales_order': sales_order_1.name, - 'sales_order_item': sales_order_1.items[0].name , - }, { - 'item_code': '_Test Item 2', - 'qty': 1, - 'stock_qty': 1, - 'conversion_factor': 1, - 'sales_order': sales_order_2.name, - 'sales_order_item': sales_order_2.items[0].name , } - ] - }) + ).insert() + sales_order_1.submit() + sales_order_2 = frappe.get_doc( + { + "doctype": "Sales Order", + "customer": "_Test Customer 1", + "company": "_Test Company", + "items": [ + { + "item_code": "_Test Item 2", + "qty": 1, + "conversion_factor": 1, + "delivery_date": frappe.utils.today(), + }, + ], + } + ).insert() + sales_order_2.submit() + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "items_based_on": "Sales Order", + "purpose": "Delivery", + "picker": "P001", + "locations": [ + { + "item_code": "_Test Item ", + "qty": 1, + "stock_qty": 1, + "conversion_factor": 1, + "sales_order": sales_order_1.name, + "sales_order_item": sales_order_1.items[0].name, + }, + { + "item_code": "_Test Item 2", + "qty": 1, + "stock_qty": 1, + "conversion_factor": 1, + "sales_order": sales_order_2.name, + "sales_order_item": sales_order_2.items[0].name, + }, + ], + } + ) pick_list.set_item_locations() pick_list.submit() create_delivery_note(pick_list.name) - for dn in frappe.get_all("Delivery Note",filters={"pick_list":pick_list.name,"customer":"_Test Customer"},fields={"name"}): - for dn_item in frappe.get_doc("Delivery Note",dn.name).get("items"): - self.assertEqual(dn_item.item_code, '_Test Item') - self.assertEqual(dn_item.against_sales_order,sales_order_1.name) - for dn in frappe.get_all("Delivery Note",filters={"pick_list":pick_list.name,"customer":"_Test Customer 1"},fields={"name"}): - for dn_item in frappe.get_doc("Delivery Note",dn.name).get("items"): - self.assertEqual(dn_item.item_code, '_Test Item 2') - self.assertEqual(dn_item.against_sales_order,sales_order_2.name) - #test DN creation without so - pick_list_1 = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'purpose': 'Delivery', - 'picker':'P001', - 'locations': [{ - 'item_code': '_Test Item ', - 'qty': 1, - 'stock_qty': 1, - 'conversion_factor': 1, - }, { - 'item_code': '_Test Item 2', - 'qty': 2, - 'stock_qty': 2, - 'conversion_factor': 1, + for dn in frappe.get_all( + "Delivery Note", + filters={"pick_list": pick_list.name, "customer": "_Test Customer"}, + fields={"name"}, + ): + for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"): + self.assertEqual(dn_item.item_code, "_Test Item") + self.assertEqual(dn_item.against_sales_order, sales_order_1.name) + for dn in frappe.get_all( + "Delivery Note", + filters={"pick_list": pick_list.name, "customer": "_Test Customer 1"}, + fields={"name"}, + ): + for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"): + self.assertEqual(dn_item.item_code, "_Test Item 2") + self.assertEqual(dn_item.against_sales_order, sales_order_2.name) + # test DN creation without so + pick_list_1 = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "purpose": "Delivery", + "picker": "P001", + "locations": [ + { + "item_code": "_Test Item ", + "qty": 1, + "stock_qty": 1, + "conversion_factor": 1, + }, + { + "item_code": "_Test Item 2", + "qty": 2, + "stock_qty": 2, + "conversion_factor": 1, + }, + ], } - ] - }) + ) pick_list_1.set_item_locations() pick_list_1.submit() create_delivery_note(pick_list_1.name) - for dn in frappe.get_all("Delivery Note",filters={"pick_list":pick_list_1.name},fields={"name"}): - for dn_item in frappe.get_doc("Delivery Note",dn.name).get("items"): - if dn_item.item_code == '_Test Item': - self.assertEqual(dn_item.qty,1) - if dn_item.item_code == '_Test Item 2': - self.assertEqual(dn_item.qty,2) + for dn in frappe.get_all( + "Delivery Note", filters={"pick_list": pick_list_1.name}, fields={"name"} + ): + for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"): + if dn_item.item_code == "_Test Item": + self.assertEqual(dn_item.qty, 1) + if dn_item.item_code == "_Test Item 2": + self.assertEqual(dn_item.qty, 2) # def test_pick_list_skips_items_in_expired_batch(self): # pass diff --git a/erpnext/stock/doctype/price_list/price_list.py b/erpnext/stock/doctype/price_list/price_list.py index 8a3172e9e2..554055fd83 100644 --- a/erpnext/stock/doctype/price_list/price_list.py +++ b/erpnext/stock/doctype/price_list/price_list.py @@ -31,9 +31,11 @@ class PriceList(Document): frappe.set_value("Buying Settings", "Buying Settings", "buying_price_list", self.name) def update_item_price(self): - frappe.db.sql("""update `tabItem Price` set currency=%s, + frappe.db.sql( + """update `tabItem Price` set currency=%s, buying=%s, selling=%s, modified=NOW() where price_list=%s""", - (self.currency, cint(self.buying), cint(self.selling), self.name)) + (self.currency, cint(self.buying), cint(self.selling), self.name), + ) def check_impact_on_shopping_cart(self): "Check if Price List currency change impacts E Commerce Cart." @@ -66,12 +68,14 @@ class PriceList(Document): def delete_price_list_details_key(self): frappe.cache().hdel("price_list_details", self.name) + def get_price_list_details(price_list): price_list_details = frappe.cache().hget("price_list_details", price_list) if not price_list_details: - price_list_details = frappe.get_cached_value("Price List", price_list, - ["currency", "price_not_uom_dependent", "enabled"], as_dict=1) + price_list_details = frappe.get_cached_value( + "Price List", price_list, ["currency", "price_not_uom_dependent", "enabled"], as_dict=1 + ) if not price_list_details or not price_list_details.get("enabled"): throw(_("Price List {0} is disabled or does not exist").format(price_list)) diff --git a/erpnext/stock/doctype/price_list/test_price_list.py b/erpnext/stock/doctype/price_list/test_price_list.py index b8218b942e..93660930c7 100644 --- a/erpnext/stock/doctype/price_list/test_price_list.py +++ b/erpnext/stock/doctype/price_list/test_price_list.py @@ -6,4 +6,4 @@ import frappe # test_ignore = ["Item"] -test_records = frappe.get_test_records('Price List') +test_records = frappe.get_test_records("Price List") diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 4bf37fee2c..1e1c0b9f7c 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -16,82 +16,85 @@ from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.controllers.buying_controller import BuyingController from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction -form_grid_templates = { - "items": "templates/form_grid/item_grid.html" -} +form_grid_templates = {"items": "templates/form_grid/item_grid.html"} + class PurchaseReceipt(BuyingController): def __init__(self, *args, **kwargs): super(PurchaseReceipt, self).__init__(*args, **kwargs) - self.status_updater = [{ - 'target_dt': 'Purchase Order Item', - 'join_field': 'purchase_order_item', - 'target_field': 'received_qty', - 'target_parent_dt': 'Purchase Order', - 'target_parent_field': 'per_received', - 'target_ref_field': 'qty', - 'source_dt': 'Purchase Receipt Item', - 'source_field': 'received_qty', - 'second_source_dt': 'Purchase Invoice Item', - 'second_source_field': 'received_qty', - 'second_join_field': 'po_detail', - 'percent_join_field': 'purchase_order', - 'overflow_type': 'receipt', - 'second_source_extra_cond': """ and exists(select name from `tabPurchase Invoice` - where name=`tabPurchase Invoice Item`.parent and update_stock = 1)""" - }, - { - 'source_dt': 'Purchase Receipt Item', - 'target_dt': 'Material Request Item', - 'join_field': 'material_request_item', - 'target_field': 'received_qty', - 'target_parent_dt': 'Material Request', - 'target_parent_field': 'per_received', - 'target_ref_field': 'stock_qty', - 'source_field': 'stock_qty', - 'percent_join_field': 'material_request' - }, - { - 'source_dt': 'Purchase Receipt Item', - 'target_dt': 'Purchase Invoice Item', - 'join_field': 'purchase_invoice_item', - 'target_field': 'received_qty', - 'target_parent_dt': 'Purchase Invoice', - 'target_parent_field': 'per_received', - 'target_ref_field': 'qty', - 'source_field': 'received_qty', - 'percent_join_field': 'purchase_invoice', - 'overflow_type': 'receipt' - }] + self.status_updater = [ + { + "target_dt": "Purchase Order Item", + "join_field": "purchase_order_item", + "target_field": "received_qty", + "target_parent_dt": "Purchase Order", + "target_parent_field": "per_received", + "target_ref_field": "qty", + "source_dt": "Purchase Receipt Item", + "source_field": "received_qty", + "second_source_dt": "Purchase Invoice Item", + "second_source_field": "received_qty", + "second_join_field": "po_detail", + "percent_join_field": "purchase_order", + "overflow_type": "receipt", + "second_source_extra_cond": """ and exists(select name from `tabPurchase Invoice` + where name=`tabPurchase Invoice Item`.parent and update_stock = 1)""", + }, + { + "source_dt": "Purchase Receipt Item", + "target_dt": "Material Request Item", + "join_field": "material_request_item", + "target_field": "received_qty", + "target_parent_dt": "Material Request", + "target_parent_field": "per_received", + "target_ref_field": "stock_qty", + "source_field": "stock_qty", + "percent_join_field": "material_request", + }, + { + "source_dt": "Purchase Receipt Item", + "target_dt": "Purchase Invoice Item", + "join_field": "purchase_invoice_item", + "target_field": "received_qty", + "target_parent_dt": "Purchase Invoice", + "target_parent_field": "per_received", + "target_ref_field": "qty", + "source_field": "received_qty", + "percent_join_field": "purchase_invoice", + "overflow_type": "receipt", + }, + ] if cint(self.is_return): - self.status_updater.extend([ - { - 'source_dt': 'Purchase Receipt Item', - 'target_dt': 'Purchase Order Item', - 'join_field': 'purchase_order_item', - 'target_field': 'returned_qty', - 'source_field': '-1 * qty', - 'second_source_dt': 'Purchase Invoice Item', - 'second_source_field': '-1 * qty', - 'second_join_field': 'po_detail', - 'extra_cond': """ and exists (select name from `tabPurchase Receipt` + self.status_updater.extend( + [ + { + "source_dt": "Purchase Receipt Item", + "target_dt": "Purchase Order Item", + "join_field": "purchase_order_item", + "target_field": "returned_qty", + "source_field": "-1 * qty", + "second_source_dt": "Purchase Invoice Item", + "second_source_field": "-1 * qty", + "second_join_field": "po_detail", + "extra_cond": """ and exists (select name from `tabPurchase Receipt` where name=`tabPurchase Receipt Item`.parent and is_return=1)""", - 'second_source_extra_cond': """ and exists (select name from `tabPurchase Invoice` - where name=`tabPurchase Invoice Item`.parent and is_return=1 and update_stock=1)""" - }, - { - 'source_dt': 'Purchase Receipt Item', - 'target_dt': 'Purchase Receipt Item', - 'join_field': 'purchase_receipt_item', - 'target_field': 'returned_qty', - 'target_parent_dt': 'Purchase Receipt', - 'target_parent_field': 'per_returned', - 'target_ref_field': 'received_stock_qty', - 'source_field': '-1 * received_stock_qty', - 'percent_join_field_parent': 'return_against' - } - ]) + "second_source_extra_cond": """ and exists (select name from `tabPurchase Invoice` + where name=`tabPurchase Invoice Item`.parent and is_return=1 and update_stock=1)""", + }, + { + "source_dt": "Purchase Receipt Item", + "target_dt": "Purchase Receipt Item", + "join_field": "purchase_receipt_item", + "target_field": "returned_qty", + "target_parent_dt": "Purchase Receipt", + "target_parent_field": "per_returned", + "target_ref_field": "received_stock_qty", + "source_field": "-1 * received_stock_qty", + "percent_join_field_parent": "return_against", + }, + ] + ) def before_validate(self): from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule @@ -103,8 +106,8 @@ class PurchaseReceipt(BuyingController): self.validate_posting_time() super(PurchaseReceipt, self).validate() - if self._action=="submit": - self.make_batches('warehouse') + if self._action == "submit": + self.make_batches("warehouse") else: self.set_status() @@ -124,20 +127,23 @@ class PurchaseReceipt(BuyingController): self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") - def validate_cwip_accounts(self): - for item in self.get('items'): + for item in self.get("items"): if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category): # check cwip accounts before making auto assets # Improves UX by not giving messages of "Assets Created" before throwing error of not finding arbnb account arbnb_account = self.get_company_default("asset_received_but_not_billed") - cwip_account = get_asset_account("capital_work_in_progress_account", asset_category = item.asset_category, \ - company = self.company) + cwip_account = get_asset_account( + "capital_work_in_progress_account", asset_category=item.asset_category, company=self.company + ) break def validate_provisional_expense_account(self): - provisional_accounting_for_non_stock_items = \ - cint(frappe.db.get_value('Company', self.company, 'enable_provisional_accounting_for_non_stock_items')) + provisional_accounting_for_non_stock_items = cint( + frappe.db.get_value( + "Company", self.company, "enable_provisional_accounting_for_non_stock_items" + ) + ) if provisional_accounting_for_non_stock_items: default_provisional_account = self.get_company_default("default_provisional_account") @@ -145,56 +151,68 @@ class PurchaseReceipt(BuyingController): self.provisional_expense_account = default_provisional_account def validate_with_previous_doc(self): - super(PurchaseReceipt, self).validate_with_previous_doc({ - "Purchase Order": { - "ref_dn_field": "purchase_order", - "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]], - }, - "Purchase Order Item": { - "ref_dn_field": "purchase_order_item", - "compare_fields": [["project", "="], ["uom", "="], ["item_code", "="]], - "is_child_table": True, - "allow_duplicate_prev_row_id": True + super(PurchaseReceipt, self).validate_with_previous_doc( + { + "Purchase Order": { + "ref_dn_field": "purchase_order", + "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]], + }, + "Purchase Order Item": { + "ref_dn_field": "purchase_order_item", + "compare_fields": [["project", "="], ["uom", "="], ["item_code", "="]], + "is_child_table": True, + "allow_duplicate_prev_row_id": True, + }, } - }) + ) - if cint(frappe.db.get_single_value('Buying Settings', 'maintain_same_rate')) and not self.is_return: - self.validate_rate_with_reference_doc([["Purchase Order", "purchase_order", "purchase_order_item"]]) + if ( + cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate")) and not self.is_return + ): + self.validate_rate_with_reference_doc( + [["Purchase Order", "purchase_order", "purchase_order_item"]] + ) def po_required(self): - if frappe.db.get_value("Buying Settings", None, "po_required") == 'Yes': - for d in self.get('items'): + if frappe.db.get_value("Buying Settings", None, "po_required") == "Yes": + for d in self.get("items"): if not d.purchase_order: frappe.throw(_("Purchase Order number required for Item {0}").format(d.item_code)) def get_already_received_qty(self, po, po_detail): - qty = frappe.db.sql("""select sum(qty) from `tabPurchase Receipt Item` + qty = frappe.db.sql( + """select sum(qty) from `tabPurchase Receipt Item` where purchase_order_item = %s and docstatus = 1 and purchase_order=%s - and parent != %s""", (po_detail, po, self.name)) + and parent != %s""", + (po_detail, po, self.name), + ) return qty and flt(qty[0][0]) or 0.0 def get_po_qty_and_warehouse(self, po_detail): - po_qty, po_warehouse = frappe.db.get_value("Purchase Order Item", po_detail, - ["qty", "warehouse"]) + po_qty, po_warehouse = frappe.db.get_value( + "Purchase Order Item", po_detail, ["qty", "warehouse"] + ) return po_qty, po_warehouse # Check for Closed status def check_on_hold_or_closed_status(self): - check_list =[] - for d in self.get('items'): - if (d.meta.get_field('purchase_order') and d.purchase_order - and d.purchase_order not in check_list): + check_list = [] + for d in self.get("items"): + if ( + d.meta.get_field("purchase_order") and d.purchase_order and d.purchase_order not in check_list + ): check_list.append(d.purchase_order) - check_on_hold_or_closed_status('Purchase Order', d.purchase_order) + check_on_hold_or_closed_status("Purchase Order", d.purchase_order) # on submit def on_submit(self): super(PurchaseReceipt, self).on_submit() # Check for Approving Authority - frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, - self.company, self.base_grand_total) + frappe.get_doc("Authorization Control").validate_approving_authority( + self.doctype, self.company, self.base_grand_total + ) self.update_prevdoc_status() if flt(self.per_billed) < 100: @@ -202,13 +220,13 @@ class PurchaseReceipt(BuyingController): else: self.db_set("status", "Completed") - # Updating stock ledger should always be called after updating prevdoc status, # because updating ordered qty, reserved_qty_for_subcontract in bin # depends upon updated ordered qty in PO self.update_stock_ledger() from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit + update_serial_nos_after_submit(self, "items") self.make_gl_entries() @@ -216,10 +234,12 @@ class PurchaseReceipt(BuyingController): self.set_consumed_qty_in_po() def check_next_docstatus(self): - submit_rv = frappe.db.sql("""select t1.name + submit_rv = frappe.db.sql( + """select t1.name from `tabPurchase Invoice` t1,`tabPurchase Invoice Item` t2 where t1.name = t2.parent and t2.purchase_receipt = %s and t1.docstatus = 1""", - (self.name)) + (self.name), + ) if submit_rv: frappe.throw(_("Purchase Invoice {0} is already submitted").format(self.submit_rv[0][0])) @@ -228,10 +248,12 @@ class PurchaseReceipt(BuyingController): self.check_on_hold_or_closed_status() # Check if Purchase Invoice has been submitted against current Purchase Order - submitted = frappe.db.sql("""select t1.name + submitted = frappe.db.sql( + """select t1.name from `tabPurchase Invoice` t1,`tabPurchase Invoice Item` t2 where t1.name = t2.parent and t2.purchase_receipt = %s and t1.docstatus = 1""", - self.name) + self.name, + ) if submitted: frappe.throw(_("Purchase Invoice {0} is already submitted").format(submitted[0][0])) @@ -243,19 +265,24 @@ class PurchaseReceipt(BuyingController): self.update_stock_ledger() self.make_gl_entries_on_cancel() self.repost_future_sle_and_gle() - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") self.delete_auto_created_batches() self.set_consumed_qty_in_po() @frappe.whitelist() def get_current_stock(self): - for d in self.get('supplied_items'): + for d in self.get("supplied_items"): if self.supplier_warehouse: - bin = frappe.db.sql("select actual_qty from `tabBin` where item_code = %s and warehouse = %s", (d.rm_item_code, self.supplier_warehouse), as_dict = 1) - d.current_stock = bin and flt(bin[0]['actual_qty']) or 0 + bin = frappe.db.sql( + "select actual_qty from `tabBin` where item_code = %s and warehouse = %s", + (d.rm_item_code, self.supplier_warehouse), + as_dict=1, + ) + d.current_stock = bin and flt(bin[0]["actual_qty"]) or 0 def get_gl_entries(self, warehouse_account=None): from erpnext.accounts.general_ledger import process_gl_map + gl_entries = [] self.make_item_gl_entries(gl_entries, warehouse_account=warehouse_account) @@ -276,31 +303,46 @@ class PurchaseReceipt(BuyingController): warehouse_with_no_account = [] stock_items = self.get_stock_items() - provisional_accounting_for_non_stock_items = \ - cint(frappe.db.get_value('Company', self.company, 'enable_provisional_accounting_for_non_stock_items')) + provisional_accounting_for_non_stock_items = cint( + frappe.db.get_value( + "Company", self.company, "enable_provisional_accounting_for_non_stock_items" + ) + ) exchange_rate_map, net_rate_map = get_purchase_document_details(self) for d in self.get("items"): if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty): if warehouse_account.get(d.warehouse): - stock_value_diff = frappe.db.get_value("Stock Ledger Entry", - {"voucher_type": "Purchase Receipt", "voucher_no": self.name, - "voucher_detail_no": d.name, "warehouse": d.warehouse, "is_cancelled": 0}, "stock_value_difference") + stock_value_diff = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": self.name, + "voucher_detail_no": d.name, + "warehouse": d.warehouse, + "is_cancelled": 0, + }, + "stock_value_difference", + ) warehouse_account_name = warehouse_account[d.warehouse]["account"] warehouse_account_currency = warehouse_account[d.warehouse]["account_currency"] supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account") - supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get("account_currency") + supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get( + "account_currency" + ) remarks = self.get("remarks") or _("Accounting Entry for Stock") # If PR is sub-contracted and fg item rate is zero # in that case if account for source and target warehouse are same, # then GL entries should not be posted - if flt(stock_value_diff) == flt(d.rm_supp_cost) \ - and warehouse_account.get(self.supplier_warehouse) \ - and warehouse_account_name == supplier_warehouse_account: - continue + if ( + flt(stock_value_diff) == flt(d.rm_supp_cost) + and warehouse_account.get(self.supplier_warehouse) + and warehouse_account_name == supplier_warehouse_account + ): + continue self.add_gl_entry( gl_entries=gl_entries, @@ -311,18 +353,24 @@ class PurchaseReceipt(BuyingController): remarks=remarks, against_account=stock_rbnb, account_currency=warehouse_account_currency, - item=d) + item=d, + ) # GL Entry for from warehouse or Stock Received but not billed # Intentionally passed negative debit amount to avoid incorrect GL Entry validation - credit_currency = get_account_currency(warehouse_account[d.from_warehouse]['account']) \ - if d.from_warehouse else get_account_currency(stock_rbnb) + credit_currency = ( + get_account_currency(warehouse_account[d.from_warehouse]["account"]) + if d.from_warehouse + else get_account_currency(stock_rbnb) + ) - credit_amount = flt(d.base_net_amount, d.precision("base_net_amount")) \ - if credit_currency == self.company_currency else flt(d.net_amount, d.precision("net_amount")) + credit_amount = ( + flt(d.base_net_amount, d.precision("base_net_amount")) + if credit_currency == self.company_currency + else flt(d.net_amount, d.precision("net_amount")) + ) if credit_amount: - account = warehouse_account[d.from_warehouse]['account'] \ - if d.from_warehouse else stock_rbnb + account = warehouse_account[d.from_warehouse]["account"] if d.from_warehouse else stock_rbnb self.add_gl_entry( gl_entries=gl_entries, @@ -334,16 +382,20 @@ class PurchaseReceipt(BuyingController): against_account=warehouse_account_name, debit_in_account_currency=-1 * credit_amount, account_currency=credit_currency, - item=d) + item=d, + ) # check if the exchange rate has changed - if d.get('purchase_invoice'): - if exchange_rate_map[d.purchase_invoice] and \ - self.conversion_rate != exchange_rate_map[d.purchase_invoice] and \ - d.net_rate == net_rate_map[d.purchase_invoice_item]: + if d.get("purchase_invoice"): + if ( + exchange_rate_map[d.purchase_invoice] + and self.conversion_rate != exchange_rate_map[d.purchase_invoice] + and d.net_rate == net_rate_map[d.purchase_invoice_item] + ): - discrepancy_caused_by_exchange_rate_difference = (d.qty * d.net_rate) * \ - (exchange_rate_map[d.purchase_invoice] - self.conversion_rate) + discrepancy_caused_by_exchange_rate_difference = (d.qty * d.net_rate) * ( + exchange_rate_map[d.purchase_invoice] - self.conversion_rate + ) self.add_gl_entry( gl_entries=gl_entries, @@ -355,7 +407,8 @@ class PurchaseReceipt(BuyingController): against_account=self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, account_currency=credit_currency, - item=d) + item=d, + ) self.add_gl_entry( gl_entries=gl_entries, @@ -367,14 +420,18 @@ class PurchaseReceipt(BuyingController): against_account=self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, account_currency=credit_currency, - item=d) + item=d, + ) # Amount added through landed-cos-voucher if d.landed_cost_voucher_amount and landed_cost_entries: for account, amount in landed_cost_entries[(d.item_code, d.name)].items(): account_currency = get_account_currency(account) - credit_amount = (flt(amount["base_amount"]) if (amount["base_amount"] or - account_currency!=self.company_currency) else flt(amount["amount"])) + credit_amount = ( + flt(amount["base_amount"]) + if (amount["base_amount"] or account_currency != self.company_currency) + else flt(amount["amount"]) + ) self.add_gl_entry( gl_entries=gl_entries, @@ -387,7 +444,8 @@ class PurchaseReceipt(BuyingController): credit_in_account_currency=flt(amount["amount"]), account_currency=account_currency, project=d.project, - item=d) + item=d, + ) # sub-contracting warehouse if flt(d.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse): @@ -400,22 +458,32 @@ class PurchaseReceipt(BuyingController): remarks=remarks, against_account=warehouse_account_name, account_currency=supplier_warehouse_account_currency, - item=d) + item=d, + ) # divisional loss adjustment - valuation_amount_as_per_doc = flt(d.base_net_amount, d.precision("base_net_amount")) + \ - flt(d.landed_cost_voucher_amount) + flt(d.rm_supp_cost) + flt(d.item_tax_amount) + valuation_amount_as_per_doc = ( + flt(d.base_net_amount, d.precision("base_net_amount")) + + flt(d.landed_cost_voucher_amount) + + flt(d.rm_supp_cost) + + flt(d.item_tax_amount) + ) - divisional_loss = flt(valuation_amount_as_per_doc - stock_value_diff, - d.precision("base_net_amount")) + divisional_loss = flt( + valuation_amount_as_per_doc - stock_value_diff, d.precision("base_net_amount") + ) if divisional_loss: if self.is_return or flt(d.item_tax_amount): loss_account = expenses_included_in_valuation else: - loss_account = self.get_company_default("default_expense_account", ignore_validation=True) or stock_rbnb + loss_account = ( + self.get_company_default("default_expense_account", ignore_validation=True) or stock_rbnb + ) - cost_center = d.cost_center or frappe.get_cached_value("Company", self.company, "cost_center") + cost_center = d.cost_center or frappe.get_cached_value( + "Company", self.company, "cost_center" + ) self.add_gl_entry( gl_entries=gl_entries, @@ -427,20 +495,31 @@ class PurchaseReceipt(BuyingController): against_account=warehouse_account_name, account_currency=credit_currency, project=d.project, - item=d) + item=d, + ) - 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 not d.is_fixed_asset and flt(d.qty) and provisional_accounting_for_non_stock_items: + 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 not d.is_fixed_asset + and flt(d.qty) + and provisional_accounting_for_non_stock_items + ): self.add_provisional_gl_entry(d, gl_entries, self.posting_date) if warehouse_with_no_account: - frappe.msgprint(_("No accounting entries for the following warehouses") + ": \n" + - "\n".join(warehouse_with_no_account)) + frappe.msgprint( + _("No accounting entries for the following warehouses") + + ": \n" + + "\n".join(warehouse_with_no_account) + ) def add_provisional_gl_entry(self, item, gl_entries, posting_date, reverse=0): - provisional_expense_account = self.get('provisional_expense_account') + provisional_expense_account = self.get("provisional_expense_account") credit_currency = get_account_currency(provisional_expense_account) debit_currency = get_account_currency(item.expense_account) expense_account = item.expense_account @@ -449,7 +528,9 @@ class PurchaseReceipt(BuyingController): if reverse: multiplication_factor = -1 - expense_account = frappe.db.get_value('Purchase Receipt Item', {'name': item.get('pr_detail')}, ['expense_account']) + expense_account = frappe.db.get_value( + "Purchase Receipt Item", {"name": item.get("pr_detail")}, ["expense_account"] + ) self.add_gl_entry( gl_entries=gl_entries, @@ -463,7 +544,8 @@ class PurchaseReceipt(BuyingController): project=item.project, voucher_detail_no=item.name, item=item, - posting_date=posting_date) + posting_date=posting_date, + ) self.add_gl_entry( gl_entries=gl_entries, @@ -473,27 +555,35 @@ class PurchaseReceipt(BuyingController): credit=0.0, remarks=remarks, against_account=provisional_expense_account, - account_currency = debit_currency, + account_currency=debit_currency, project=item.project, voucher_detail_no=item.name, item=item, - posting_date=posting_date) + posting_date=posting_date, + ) def make_tax_gl_entries(self, gl_entries): if erpnext.is_perpetual_inventory_enabled(self.company): expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") - negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get('items')]) + negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get("items")]) # Cost center-wise amount breakup for other charges included for valuation valuation_tax = {} for tax in self.get("taxes"): - if tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount): + if tax.category in ("Valuation", "Valuation and Total") and flt( + tax.base_tax_amount_after_discount_amount + ): if not tax.cost_center: - frappe.throw(_("Cost Center is required in row {0} in Taxes table for type {1}").format(tax.idx, _(tax.category))) + frappe.throw( + _("Cost Center is required in row {0} in Taxes table for type {1}").format( + tax.idx, _(tax.category) + ) + ) valuation_tax.setdefault(tax.name, 0) - valuation_tax[tax.name] += \ - (tax.add_deduct_tax == "Add" and 1 or -1) * flt(tax.base_tax_amount_after_discount_amount) + valuation_tax[tax.name] += (tax.add_deduct_tax == "Add" and 1 or -1) * flt( + tax.base_tax_amount_after_discount_amount + ) if negative_expense_to_be_booked and valuation_tax: # Backward compatibility: @@ -502,10 +592,13 @@ class PurchaseReceipt(BuyingController): # post valuation related charges on "Stock Received But Not Billed" # introduced in 2014 for backward compatibility of expenses already booked in expenses_included_in_valuation account - negative_expense_booked_in_pi = frappe.db.sql("""select name from `tabPurchase Invoice Item` pi + negative_expense_booked_in_pi = frappe.db.sql( + """select name from `tabPurchase Invoice Item` pi where docstatus = 1 and purchase_receipt=%s and exists(select name from `tabGL Entry` where voucher_type='Purchase Invoice' - and voucher_no=pi.parent and account=%s)""", (self.name, expenses_included_in_valuation)) + and voucher_no=pi.parent and account=%s)""", + (self.name, expenses_included_in_valuation), + ) against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0]) total_valuation_amount = sum(valuation_tax.values()) @@ -523,7 +616,9 @@ class PurchaseReceipt(BuyingController): if i == len(valuation_tax): applicable_amount = amount_including_divisional_loss else: - applicable_amount = negative_expense_to_be_booked * (valuation_tax[tax.name] / total_valuation_amount) + applicable_amount = negative_expense_to_be_booked * ( + valuation_tax[tax.name] / total_valuation_amount + ) amount_including_divisional_loss -= applicable_amount self.add_gl_entry( @@ -534,13 +629,28 @@ class PurchaseReceipt(BuyingController): credit=applicable_amount, remarks=self.remarks or _("Accounting Entry for Stock"), against_account=against_account, - item=tax) + item=tax, + ) i += 1 - def add_gl_entry(self, gl_entries, account, cost_center, debit, credit, remarks, against_account, - debit_in_account_currency=None, credit_in_account_currency=None, account_currency=None, - project=None, voucher_detail_no=None, item=None, posting_date=None): + def add_gl_entry( + self, + gl_entries, + account, + cost_center, + debit, + credit, + remarks, + against_account, + debit_in_account_currency=None, + credit_in_account_currency=None, + account_currency=None, + project=None, + voucher_detail_no=None, + item=None, + posting_date=None, + ): gl_entry = { "account": account, @@ -580,17 +690,19 @@ class PurchaseReceipt(BuyingController): def add_asset_gl_entries(self, item, gl_entries): arbnb_account = self.get_company_default("asset_received_but_not_billed") # This returns category's cwip account if not then fallback to company's default cwip account - cwip_account = get_asset_account("capital_work_in_progress_account", asset_category = item.asset_category, \ - company = self.company) + cwip_account = get_asset_account( + "capital_work_in_progress_account", asset_category=item.asset_category, company=self.company + ) - asset_amount = flt(item.net_amount) + flt(item.item_tax_amount/self.conversion_rate) + asset_amount = flt(item.net_amount) + flt(item.item_tax_amount / self.conversion_rate) base_asset_amount = flt(item.base_net_amount + item.item_tax_amount) remarks = self.get("remarks") or _("Accounting Entry for Asset") cwip_account_currency = get_account_currency(cwip_account) # debit cwip account - debit_in_account_currency = (base_asset_amount - if cwip_account_currency == self.company_currency else asset_amount) + debit_in_account_currency = ( + base_asset_amount if cwip_account_currency == self.company_currency else asset_amount + ) self.add_gl_entry( gl_entries=gl_entries, @@ -601,12 +713,14 @@ class PurchaseReceipt(BuyingController): remarks=remarks, against_account=arbnb_account, debit_in_account_currency=debit_in_account_currency, - item=item) + item=item, + ) asset_rbnb_currency = get_account_currency(arbnb_account) # credit arbnb account - credit_in_account_currency = (base_asset_amount - if asset_rbnb_currency == self.company_currency else asset_amount) + credit_in_account_currency = ( + base_asset_amount if asset_rbnb_currency == self.company_currency else asset_amount + ) self.add_gl_entry( gl_entries=gl_entries, @@ -617,13 +731,17 @@ class PurchaseReceipt(BuyingController): remarks=remarks, against_account=cwip_account, credit_in_account_currency=credit_in_account_currency, - item=item) + item=item, + ) def add_lcv_gl_entries(self, item, gl_entries): - expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation") + expenses_included_in_asset_valuation = self.get_company_default( + "expenses_included_in_asset_valuation" + ) if not is_cwip_accounting_enabled(item.asset_category): - asset_account = get_asset_category_account(asset_category=item.asset_category, \ - fieldname='fixed_asset_account', company=self.company) + asset_account = get_asset_category_account( + asset_category=item.asset_category, fieldname="fixed_asset_account", company=self.company + ) else: # This returns company's default cwip account asset_account = get_asset_account("capital_work_in_progress_account", company=self.company) @@ -639,7 +757,8 @@ class PurchaseReceipt(BuyingController): remarks=remarks, against_account=asset_account, project=item.project, - item=item) + item=item, + ) self.add_gl_entry( gl_entries=gl_entries, @@ -650,11 +769,12 @@ class PurchaseReceipt(BuyingController): remarks=remarks, against_account=expenses_included_in_asset_valuation, project=item.project, - item=item) + item=item, + ) def update_assets(self, item, valuation_rate): - assets = frappe.db.get_all('Asset', - filters={ 'purchase_receipt': self.name, 'item_code': item.item_code } + assets = frappe.db.get_all( + "Asset", filters={"purchase_receipt": self.name, "item_code": item.item_code} ) for asset in assets: @@ -670,7 +790,7 @@ class PurchaseReceipt(BuyingController): updated_pr = [self.name] for d in self.get("items"): if d.get("purchase_invoice") and d.get("purchase_invoice_item"): - d.db_set('billed_amt', d.amount, update_modified=update_modified) + d.db_set("billed_amt", d.amount, update_modified=update_modified) elif d.purchase_order_item: updated_pr += update_billed_amount_based_on_po(d.purchase_order_item, update_modified) @@ -680,24 +800,35 @@ class PurchaseReceipt(BuyingController): self.load_from_db() + def update_billed_amount_based_on_po(po_detail, update_modified=True): # Billed against Sales Order directly - billed_against_po = frappe.db.sql("""select sum(amount) from `tabPurchase Invoice Item` - where po_detail=%s and (pr_detail is null or pr_detail = '') and docstatus=1""", po_detail) + billed_against_po = frappe.db.sql( + """select sum(amount) from `tabPurchase Invoice Item` + where po_detail=%s and (pr_detail is null or pr_detail = '') and docstatus=1""", + po_detail, + ) billed_against_po = billed_against_po and billed_against_po[0][0] or 0 # Get all Purchase Receipt Item rows against the Purchase Order Item row - pr_details = frappe.db.sql("""select pr_item.name, pr_item.amount, pr_item.parent + pr_details = frappe.db.sql( + """select pr_item.name, pr_item.amount, pr_item.parent from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr where pr.name=pr_item.parent and pr_item.purchase_order_item=%s and pr.docstatus=1 and pr.is_return = 0 - order by pr.posting_date asc, pr.posting_time asc, pr.name asc""", po_detail, as_dict=1) + order by pr.posting_date asc, pr.posting_time asc, pr.name asc""", + po_detail, + as_dict=1, + ) updated_pr = [] for pr_item in pr_details: # Get billed amount directly against Purchase Receipt - billed_amt_agianst_pr = frappe.db.sql("""select sum(amount) from `tabPurchase Invoice Item` - where pr_detail=%s and docstatus=1""", pr_item.name) + billed_amt_agianst_pr = frappe.db.sql( + """select sum(amount) from `tabPurchase Invoice Item` + where pr_detail=%s and docstatus=1""", + pr_item.name, + ) billed_amt_agianst_pr = billed_amt_agianst_pr and billed_amt_agianst_pr[0][0] or 0 # Distribute billed amount directly against PO between PRs based on FIFO @@ -710,12 +841,19 @@ def update_billed_amount_based_on_po(po_detail, update_modified=True): billed_amt_agianst_pr += billed_against_po billed_against_po = 0 - frappe.db.set_value("Purchase Receipt Item", pr_item.name, "billed_amt", billed_amt_agianst_pr, update_modified=update_modified) + frappe.db.set_value( + "Purchase Receipt Item", + pr_item.name, + "billed_amt", + billed_amt_agianst_pr, + update_modified=update_modified, + ) updated_pr.append(pr_item.parent) return updated_pr + def update_billing_percentage(pr_doc, update_modified=True): # Reload as billed amount was set in db directly pr_doc.load_from_db() @@ -723,15 +861,15 @@ def update_billing_percentage(pr_doc, update_modified=True): # Update Billing % based on pending accepted qty total_amount, total_billed_amount = 0, 0 for item in pr_doc.items: - return_data = frappe.db.get_list("Purchase Receipt", - fields = [ - "sum(abs(`tabPurchase Receipt Item`.qty)) as qty" - ], - filters = [ + return_data = frappe.db.get_list( + "Purchase Receipt", + fields=["sum(abs(`tabPurchase Receipt Item`.qty)) as qty"], + filters=[ ["Purchase Receipt", "docstatus", "=", 1], ["Purchase Receipt", "is_return", "=", 1], - ["Purchase Receipt Item", "purchase_receipt_item", "=", item.name] - ]) + ["Purchase Receipt Item", "purchase_receipt_item", "=", item.name], + ], + ) returned_qty = return_data[0].qty if return_data else 0 returned_amount = flt(returned_qty) * flt(item.rate) @@ -749,11 +887,12 @@ def update_billing_percentage(pr_doc, update_modified=True): pr_doc.set_status(update=True) pr_doc.notify_update() + @frappe.whitelist() def make_purchase_invoice(source_name, target_doc=None): from erpnext.accounts.party import get_payment_terms_template - doc = frappe.get_doc('Purchase Receipt', source_name) + doc = frappe.get_doc("Purchase Receipt", source_name) returned_qty_map = get_returned_qty_map(source_name) invoiced_qty_map = get_invoiced_qty_map(source_name) @@ -762,7 +901,9 @@ def make_purchase_invoice(source_name, target_doc=None): frappe.throw(_("All items have already been Invoiced/Returned")) doc = frappe.get_doc(target) - doc.payment_terms_template = get_payment_terms_template(source.supplier, "Supplier", source.company) + doc.payment_terms_template = get_payment_terms_template( + source.supplier, "Supplier", source.company + ) doc.run_method("onload") doc.run_method("set_missing_values") doc.run_method("calculate_taxes_and_totals") @@ -770,14 +911,20 @@ def make_purchase_invoice(source_name, target_doc=None): def update_item(source_doc, target_doc, source_parent): target_doc.qty, returned_qty = get_pending_qty(source_doc) - if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"): + if frappe.db.get_single_value( + "Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice" + ): target_doc.rejected_qty = 0 - target_doc.stock_qty = flt(target_doc.qty) * flt(target_doc.conversion_factor, target_doc.precision("conversion_factor")) + target_doc.stock_qty = flt(target_doc.qty) * flt( + target_doc.conversion_factor, target_doc.precision("conversion_factor") + ) returned_qty_map[source_doc.name] = returned_qty def get_pending_qty(item_row): qty = item_row.qty - if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"): + if frappe.db.get_single_value( + "Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice" + ): qty = item_row.received_qty pending_qty = qty - invoiced_qty_map.get(item_row.name, 0) returned_qty = flt(returned_qty_map.get(item_row.name, 0)) @@ -790,69 +937,85 @@ def make_purchase_invoice(source_name, target_doc=None): returned_qty = 0 return pending_qty, returned_qty - - doclist = get_mapped_doc("Purchase Receipt", source_name, { - "Purchase Receipt": { - "doctype": "Purchase Invoice", - "field_map": { - "supplier_warehouse":"supplier_warehouse", - "is_return": "is_return", - "bill_date": "bill_date" + doclist = get_mapped_doc( + "Purchase Receipt", + source_name, + { + "Purchase Receipt": { + "doctype": "Purchase Invoice", + "field_map": { + "supplier_warehouse": "supplier_warehouse", + "is_return": "is_return", + "bill_date": "bill_date", + }, + "validation": { + "docstatus": ["=", 1], + }, }, - "validation": { - "docstatus": ["=", 1], + "Purchase Receipt Item": { + "doctype": "Purchase Invoice Item", + "field_map": { + "name": "pr_detail", + "parent": "purchase_receipt", + "purchase_order_item": "po_detail", + "purchase_order": "purchase_order", + "is_fixed_asset": "is_fixed_asset", + "asset_location": "asset_location", + "asset_category": "asset_category", + }, + "postprocess": update_item, + "filter": lambda d: get_pending_qty(d)[0] <= 0 + if not doc.get("is_return") + else get_pending_qty(d)[0] > 0, }, + "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "add_if_empty": True}, }, - "Purchase Receipt Item": { - "doctype": "Purchase Invoice Item", - "field_map": { - "name": "pr_detail", - "parent": "purchase_receipt", - "purchase_order_item": "po_detail", - "purchase_order": "purchase_order", - "is_fixed_asset": "is_fixed_asset", - "asset_location": "asset_location", - "asset_category": 'asset_category' - }, - "postprocess": update_item, - "filter": lambda d: get_pending_qty(d)[0] <= 0 if not doc.get("is_return") else get_pending_qty(d)[0] > 0 - }, - "Purchase Taxes and Charges": { - "doctype": "Purchase Taxes and Charges", - "add_if_empty": True - } - }, target_doc, set_missing_values) + target_doc, + set_missing_values, + ) - doclist.set_onload('ignore_price_list', True) + doclist.set_onload("ignore_price_list", True) return doclist + def get_invoiced_qty_map(purchase_receipt): """returns a map: {pr_detail: invoiced_qty}""" invoiced_qty_map = {} - for pr_detail, qty in frappe.db.sql("""select pr_detail, qty from `tabPurchase Invoice Item` - where purchase_receipt=%s and docstatus=1""", purchase_receipt): - if not invoiced_qty_map.get(pr_detail): - invoiced_qty_map[pr_detail] = 0 - invoiced_qty_map[pr_detail] += qty + for pr_detail, qty in frappe.db.sql( + """select pr_detail, qty from `tabPurchase Invoice Item` + where purchase_receipt=%s and docstatus=1""", + purchase_receipt, + ): + if not invoiced_qty_map.get(pr_detail): + invoiced_qty_map[pr_detail] = 0 + invoiced_qty_map[pr_detail] += qty return invoiced_qty_map + def get_returned_qty_map(purchase_receipt): """returns a map: {so_detail: returned_qty}""" - returned_qty_map = frappe._dict(frappe.db.sql("""select pr_item.purchase_receipt_item, abs(pr_item.qty) as qty + returned_qty_map = frappe._dict( + frappe.db.sql( + """select pr_item.purchase_receipt_item, abs(pr_item.qty) as qty from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr where pr.name = pr_item.parent and pr.docstatus = 1 and pr.is_return = 1 and pr.return_against = %s - """, purchase_receipt)) + """, + purchase_receipt, + ) + ) return returned_qty_map + @frappe.whitelist() def make_purchase_return(source_name, target_doc=None): from erpnext.controllers.sales_and_purchase_return import make_return_doc + return make_return_doc("Purchase Receipt", source_name, target_doc) @@ -861,35 +1024,47 @@ def update_purchase_receipt_status(docname, status): pr = frappe.get_doc("Purchase Receipt", docname) pr.update_status(status) + @frappe.whitelist() -def make_stock_entry(source_name,target_doc=None): +def make_stock_entry(source_name, target_doc=None): def set_missing_values(source, target): target.stock_entry_type = "Material Transfer" - target.purpose = "Material Transfer" + target.purpose = "Material Transfer" - doclist = get_mapped_doc("Purchase Receipt", source_name,{ - "Purchase Receipt": { - "doctype": "Stock Entry", - }, - "Purchase Receipt Item": { - "doctype": "Stock Entry Detail", - "field_map": { - "warehouse": "s_warehouse", - "parent": "reference_purchase_receipt", - "batch_no": "batch_no" + doclist = get_mapped_doc( + "Purchase Receipt", + source_name, + { + "Purchase Receipt": { + "doctype": "Stock Entry", + }, + "Purchase Receipt Item": { + "doctype": "Stock Entry Detail", + "field_map": { + "warehouse": "s_warehouse", + "parent": "reference_purchase_receipt", + "batch_no": "batch_no", + }, }, }, - }, target_doc, set_missing_values) + target_doc, + set_missing_values, + ) return doclist + @frappe.whitelist() def make_inter_company_delivery_note(source_name, target_doc=None): return make_inter_company_transaction("Purchase Receipt", source_name, target_doc) + def get_item_account_wise_additional_cost(purchase_document): - landed_cost_vouchers = frappe.get_all("Landed Cost Purchase Receipt", fields=["parent"], - filters = {"receipt_document": purchase_document, "docstatus": 1}) + landed_cost_vouchers = frappe.get_all( + "Landed Cost Purchase Receipt", + fields=["parent"], + filters={"receipt_document": purchase_document, "docstatus": 1}, + ) if not landed_cost_vouchers: return @@ -899,9 +1074,9 @@ def get_item_account_wise_additional_cost(purchase_document): for lcv in landed_cost_vouchers: landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", lcv.parent) - #Use amount field for total item cost for manually cost distributed LCVs - if landed_cost_voucher_doc.distribute_charges_based_on == 'Distribute Manually': - based_on_field = 'amount' + # Use amount field for total item cost for manually cost distributed LCVs + if landed_cost_voucher_doc.distribute_charges_based_on == "Distribute Manually": + based_on_field = "amount" else: based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on) @@ -914,18 +1089,20 @@ def get_item_account_wise_additional_cost(purchase_document): if item.receipt_document == purchase_document: for account in landed_cost_voucher_doc.taxes: item_account_wise_cost.setdefault((item.item_code, item.purchase_receipt_item), {}) - item_account_wise_cost[(item.item_code, item.purchase_receipt_item)].setdefault(account.expense_account, { - "amount": 0.0, - "base_amount": 0.0 - }) + item_account_wise_cost[(item.item_code, item.purchase_receipt_item)].setdefault( + account.expense_account, {"amount": 0.0, "base_amount": 0.0} + ) - item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account]["amount"] += \ - account.amount * item.get(based_on_field) / total_item_cost + item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][ + "amount" + ] += (account.amount * item.get(based_on_field) / total_item_cost) - item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account]["base_amount"] += \ - account.base_amount * item.get(based_on_field) / total_item_cost + item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][ + "base_amount" + ] += (account.base_amount * item.get(based_on_field) / total_item_cost) return item_account_wise_cost + def on_doctype_update(): frappe.db.add_index("Purchase Receipt", ["supplier", "is_return", "return_against"]) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py index bdc5435988..06ba936556 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py @@ -3,35 +3,23 @@ from frappe import _ def get_data(): return { - 'fieldname': 'purchase_receipt_no', - 'non_standard_fieldnames': { - 'Purchase Invoice': 'purchase_receipt', - 'Asset': 'purchase_receipt', - 'Landed Cost Voucher': 'receipt_document', - 'Auto Repeat': 'reference_document', - 'Purchase Receipt': 'return_against' + "fieldname": "purchase_receipt_no", + "non_standard_fieldnames": { + "Purchase Invoice": "purchase_receipt", + "Asset": "purchase_receipt", + "Landed Cost Voucher": "receipt_document", + "Auto Repeat": "reference_document", + "Purchase Receipt": "return_against", }, - 'internal_links': { - 'Purchase Order': ['items', 'purchase_order'], - 'Project': ['items', 'project'], - 'Quality Inspection': ['items', 'quality_inspection'], + "internal_links": { + "Purchase Order": ["items", "purchase_order"], + "Project": ["items", "project"], + "Quality Inspection": ["items", "quality_inspection"], }, - 'transactions': [ - { - 'label': _('Related'), - 'items': ['Purchase Invoice', 'Landed Cost Voucher', 'Asset'] - }, - { - 'label': _('Reference'), - 'items': ['Purchase Order', 'Quality Inspection', 'Project'] - }, - { - 'label': _('Returns'), - 'items': ['Purchase Receipt'] - }, - { - 'label': _('Subscription'), - 'items': ['Auto Repeat'] - }, - ] + "transactions": [ + {"label": _("Related"), "items": ["Purchase Invoice", "Landed Cost Voucher", "Asset"]}, + {"label": _("Reference"), "items": ["Purchase Order", "Quality Inspection", "Project"]}, + {"label": _("Returns"), "items": ["Purchase Receipt"]}, + {"label": _("Subscription"), "items": ["Auto Repeat"]}, + ], } diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 0017fa7ee1..a6f82b08dc 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -26,15 +26,11 @@ class TestPurchaseReceipt(FrappeTestCase): def test_purchase_receipt_received_qty(self): """ - 1. Test if received qty is validated against accepted + rejected - 2. Test if received qty is auto set on save + 1. Test if received qty is validated against accepted + rejected + 2. Test if received qty is auto set on save """ pr = make_purchase_receipt( - qty=1, - rejected_qty=1, - received_qty=3, - item_code="_Test Item Home Desktop 200", - do_not_save=True + qty=1, rejected_qty=1, received_qty=3, item_code="_Test Item Home Desktop 200", do_not_save=True ) self.assertRaises(QtyMismatchError, pr.save) @@ -51,11 +47,8 @@ class TestPurchaseReceipt(FrappeTestCase): sl_entry = frappe.db.get_all( "Stock Ledger Entry", - { - "voucher_type": "Purchase Receipt", - "voucher_no": pr.name - }, - ['actual_qty'] + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + ["actual_qty"], ) self.assertEqual(len(sl_entry), 1) @@ -65,47 +58,44 @@ class TestPurchaseReceipt(FrappeTestCase): sl_entry_cancelled = frappe.db.get_all( "Stock Ledger Entry", - { - "voucher_type": "Purchase Receipt", - "voucher_no": pr.name - }, - ['actual_qty'], - order_by='creation' + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + ["actual_qty"], + order_by="creation", ) self.assertEqual(len(sl_entry_cancelled), 2) self.assertEqual(sl_entry_cancelled[1].actual_qty, -0.5) def test_make_purchase_invoice(self): - if not frappe.db.exists('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice'): - frappe.get_doc({ - 'doctype': 'Payment Terms Template', - 'template_name': '_Test Payment Terms Template For Purchase Invoice', - 'allocate_payment_based_on_payment_terms': 1, - 'terms': [ - { - 'doctype': 'Payment Terms Template Detail', - 'invoice_portion': 50.00, - 'credit_days_based_on': 'Day(s) after invoice date', - 'credit_days': 00 - }, - { - 'doctype': 'Payment Terms Template Detail', - 'invoice_portion': 50.00, - 'credit_days_based_on': 'Day(s) after invoice date', - 'credit_days': 30 - }] - }).insert() + if not frappe.db.exists( + "Payment Terms Template", "_Test Payment Terms Template For Purchase Invoice" + ): + frappe.get_doc( + { + "doctype": "Payment Terms Template", + "template_name": "_Test Payment Terms Template For Purchase Invoice", + "allocate_payment_based_on_payment_terms": 1, + "terms": [ + { + "doctype": "Payment Terms Template Detail", + "invoice_portion": 50.00, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": 00, + }, + { + "doctype": "Payment Terms Template Detail", + "invoice_portion": 50.00, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": 30, + }, + ], + } + ).insert() template = frappe.db.get_value( - "Payment Terms Template", - "_Test Payment Terms Template For Purchase Invoice" - ) - old_template_in_supplier = frappe.db.get_value( - "Supplier", - "_Test Supplier", - "payment_terms" + "Payment Terms Template", "_Test Payment Terms Template For Purchase Invoice" ) + old_template_in_supplier = frappe.db.get_value("Supplier", "_Test Supplier", "payment_terms") frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", template) pr = make_purchase_receipt(do_not_save=True) @@ -123,23 +113,17 @@ class TestPurchaseReceipt(FrappeTestCase): # test if payment terms are fetched and set in PI self.assertEqual(pi.payment_terms_template, template) - self.assertEqual(pi.payment_schedule[0].payment_amount, flt(pi.grand_total)/2) + self.assertEqual(pi.payment_schedule[0].payment_amount, flt(pi.grand_total) / 2) self.assertEqual(pi.payment_schedule[0].invoice_portion, 50) - self.assertEqual(pi.payment_schedule[1].payment_amount, flt(pi.grand_total)/2) + self.assertEqual(pi.payment_schedule[1].payment_amount, flt(pi.grand_total) / 2) self.assertEqual(pi.payment_schedule[1].invoice_portion, 50) # teardown - pi.delete() # draft PI + pi.delete() # draft PI pr.cancel() - frappe.db.set_value( - "Supplier", - "_Test Supplier", - "payment_terms", - old_template_in_supplier - ) + frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", old_template_in_supplier) frappe.get_doc( - "Payment Terms Template", - "_Test Payment Terms Template For Purchase Invoice" + "Payment Terms Template", "_Test Payment Terms Template For Purchase Invoice" ).delete() def test_purchase_receipt_no_gl_entry(self): @@ -147,27 +131,19 @@ class TestPurchaseReceipt(FrappeTestCase): existing_bin_qty, existing_bin_stock_value = frappe.db.get_value( "Bin", - { - "item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC" - }, - ["actual_qty", "stock_value"] + {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, + ["actual_qty", "stock_value"], ) if existing_bin_qty < 0: make_stock_entry( - item_code="_Test Item", - target="_Test Warehouse - _TC", - qty=abs(existing_bin_qty) + item_code="_Test Item", target="_Test Warehouse - _TC", qty=abs(existing_bin_qty) ) existing_bin_qty, existing_bin_stock_value = frappe.db.get_value( "Bin", - { - "item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC" - }, - ["actual_qty", "stock_value"] + {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, + ["actual_qty", "stock_value"], ) pr = make_purchase_receipt() @@ -178,20 +154,15 @@ class TestPurchaseReceipt(FrappeTestCase): "voucher_type": "Purchase Receipt", "voucher_no": pr.name, "item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC" + "warehouse": "_Test Warehouse - _TC", }, - "stock_value_difference" + "stock_value_difference", ) self.assertEqual(stock_value_difference, 250) current_bin_stock_value = frappe.db.get_value( - "Bin", - { - "item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC" - }, - "stock_value" + "Bin", {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, "stock_value" ) self.assertEqual(current_bin_stock_value, existing_bin_stock_value + 250) @@ -200,7 +171,7 @@ class TestPurchaseReceipt(FrappeTestCase): pr.cancel() def test_batched_serial_no_purchase(self): - item = frappe.db.exists("Item", {'item_name': 'Batched Serialized Item'}) + item = frappe.db.exists("Item", {"item_name": "Batched Serialized Item"}) if not item: item = create_item("Batched Serialized Item") item.has_batch_no = 1 @@ -210,34 +181,30 @@ class TestPurchaseReceipt(FrappeTestCase): item.serial_no_series = "BS-.####" item.save() else: - item = frappe.get_doc("Item", {'item_name': 'Batched Serialized Item'}) + item = frappe.get_doc("Item", {"item_name": "Batched Serialized Item"}) pr = make_purchase_receipt(item_code=item.name, qty=5, rate=500) - self.assertTrue( - frappe.db.get_value('Batch', {'item': item.name, 'reference_name': pr.name}) - ) + self.assertTrue(frappe.db.get_value("Batch", {"item": item.name, "reference_name": pr.name})) pr.load_from_db() batch_no = pr.items[0].batch_no pr.cancel() - self.assertFalse( - frappe.db.get_value('Batch', {'item': item.name, 'reference_name': pr.name}) - ) - self.assertFalse(frappe.db.get_all('Serial No', {'batch_no': batch_no})) + self.assertFalse(frappe.db.get_value("Batch", {"item": item.name, "reference_name": pr.name})) + self.assertFalse(frappe.db.get_all("Serial No", {"batch_no": batch_no})) def test_duplicate_serial_nos(self): from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note - item = frappe.db.exists("Item", {'item_name': 'Test Serialized Item 123'}) + item = frappe.db.exists("Item", {"item_name": "Test Serialized Item 123"}) if not item: item = create_item("Test Serialized Item 123") item.has_serial_no = 1 item.serial_no_series = "TSI123-.####" item.save() else: - item = frappe.get_doc("Item", {'item_name': 'Test Serialized Item 123'}) + item = frappe.get_doc("Item", {"item_name": "Test Serialized Item 123"}) # First make purchase receipt pr = make_purchase_receipt(item_code=item.name, qty=2, rate=500) @@ -245,12 +212,8 @@ class TestPurchaseReceipt(FrappeTestCase): serial_nos = frappe.db.get_value( "Stock Ledger Entry", - { - "voucher_type": "Purchase Receipt", - "voucher_no": pr.name, - "item_code": item.name - }, - "serial_no" + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "item_code": item.name}, + "serial_no", ) serial_nos = get_serial_nos(serial_nos) @@ -262,21 +225,16 @@ class TestPurchaseReceipt(FrappeTestCase): item_code=item.name, qty=2, rate=500, - serial_no='\n'.join(serial_nos), - company='_Test Company 1', + serial_no="\n".join(serial_nos), + company="_Test Company 1", do_not_submit=True, - warehouse = 'Stores - _TC1' + warehouse="Stores - _TC1", ) self.assertRaises(SerialNoDuplicateError, pr_different_company.submit) # Then made delivery note to remove the serial nos from stock - dn = create_delivery_note( - item_code=item.name, - qty=2, - rate=1500, - serial_no='\n'.join(serial_nos) - ) + dn = create_delivery_note(item_code=item.name, qty=2, rate=1500, serial_no="\n".join(serial_nos)) dn.load_from_db() self.assertEquals(get_serial_nos(dn.items[0].serial_no), serial_nos) @@ -288,8 +246,8 @@ class TestPurchaseReceipt(FrappeTestCase): qty=2, rate=500, posting_date=posting_date, - serial_no='\n'.join(serial_nos), - do_not_submit=True + serial_no="\n".join(serial_nos), + do_not_submit=True, ) self.assertRaises(SerialNoExistsInFutureTransaction, pr1.submit) @@ -300,29 +258,28 @@ class TestPurchaseReceipt(FrappeTestCase): qty=2, rate=500, posting_date=posting_date, - serial_no='\n'.join(serial_nos), + serial_no="\n".join(serial_nos), company="_Test Company 1", do_not_submit=True, - warehouse="Stores - _TC1" + warehouse="Stores - _TC1", ) self.assertRaises(SerialNoExistsInFutureTransaction, pr2.submit) # Receive the same serial nos after the delivery note posting date and time - make_purchase_receipt( - item_code=item.name, - qty=2, - rate=500, - serial_no='\n'.join(serial_nos) - ) + make_purchase_receipt(item_code=item.name, qty=2, rate=500, serial_no="\n".join(serial_nos)) # Raise the error for backdated deliver note entry cancel self.assertRaises(SerialNoExistsInFutureTransaction, dn.cancel) def test_purchase_receipt_gl_entry(self): - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", - get_multiple_items = True, get_taxes_and_charges = True) + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", + get_multiple_items=True, + get_taxes_and_charges=True, + ) self.assertEqual(cint(erpnext.is_perpetual_inventory_enabled(pr.company)), 1) @@ -338,14 +295,14 @@ class TestPurchaseReceipt(FrappeTestCase): stock_in_hand_account: [750.0, 0.0], "Stock Received But Not Billed - TCP1": [0.0, 500.0], "_Test Account Shipping Charges - TCP1": [0.0, 100.0], - "_Test Account Customs Duty - TCP1": [0.0, 150.0] + "_Test Account Customs Duty - TCP1": [0.0, 150.0], } else: expected_values = { stock_in_hand_account: [375.0, 0.0], fixed_asset_account: [375.0, 0.0], "Stock Received But Not Billed - TCP1": [0.0, 500.0], - "_Test Account Shipping Charges - TCP1": [0.0, 250.0] + "_Test Account Shipping Charges - TCP1": [0.0, 250.0], } for gle in gl_entries: self.assertEqual(expected_values[gle.account][0], gle.debit) @@ -358,22 +315,19 @@ class TestPurchaseReceipt(FrappeTestCase): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry frappe.db.set_value( - "Buying Settings", None, - "backflush_raw_materials_of_subcontract_based_on", "BOM" + "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM" ) make_stock_entry( - item_code="_Test Item", qty=100, - target="_Test Warehouse 1 - _TC", basic_rate=100 + item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100 ) make_stock_entry( - item_code="_Test Item Home Desktop 100", qty=100, - target="_Test Warehouse 1 - _TC", basic_rate=100 - ) - pr = make_purchase_receipt( - item_code="_Test FG Item", qty=10, - rate=500, is_subcontracted="Yes" + item_code="_Test Item Home Desktop 100", + qty=100, + target="_Test Warehouse 1 - _TC", + basic_rate=100, ) + pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=500, is_subcontracted="Yes") self.assertEqual(len(pr.get("supplied_items")), 2) rm_supp_cost = sum(d.amount for d in pr.get("supplied_items")) @@ -383,32 +337,35 @@ class TestPurchaseReceipt(FrappeTestCase): def test_subcontracting_gle_fg_item_rate_zero(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + frappe.db.set_value( - "Buying Settings", None, - "backflush_raw_materials_of_subcontract_based_on", "BOM" + "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM" ) se1 = make_stock_entry( item_code="_Test Item", target="Work In Progress - TCP1", - qty=100, basic_rate=100, - company="_Test Company with perpetual inventory" + 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" + qty=100, + basic_rate=100, + company="_Test Company with perpetual inventory", ) pr = make_purchase_receipt( item_code="_Test FG Item", - qty=10, rate=0, + qty=10, + rate=0, is_subcontracted="Yes", company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work In Progress - TCP1" + supplier_warehouse="Work In Progress - TCP1", ) gl_entries = get_gl_entries("Purchase Receipt", pr.name) @@ -421,9 +378,9 @@ class TestPurchaseReceipt(FrappeTestCase): def test_subcontracting_over_receipt(self): """ - Behaviour: Raise multiple PRs against one PO that in total - receive more than the required qty in the PO. - Expected Result: Error Raised for Over Receipt against PO. + Behaviour: Raise multiple PRs against one PO that in total + receive more than the required qty in the PO. + Expected Result: Error Raised for Over Receipt against PO. """ from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt from erpnext.buying.doctype.purchase_order.purchase_order import ( @@ -440,24 +397,23 @@ class TestPurchaseReceipt(FrappeTestCase): item_code = "_Test Subcontracted FG Item 1" make_subcontracted_item(item_code=item_code) - po = create_purchase_order(item_code=item_code, qty=1, include_exploded_items=0, - is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") + 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=10, basic_rate=100 + target="_Test Warehouse - _TC", item_code="Test Extra Item 1", qty=10, basic_rate=100 ) make_stock_entry( - target="_Test Warehouse - _TC", - item_code = "_Test FG Item", - qty=1, basic_rate=100 + target="_Test Warehouse - _TC", item_code="_Test FG Item", qty=1, basic_rate=100 ) make_stock_entry( - target="_Test Warehouse - _TC", - item_code = "Test Extra Item 2", - qty=1, basic_rate=100 + target="_Test Warehouse - _TC", item_code="Test Extra Item 2", qty=1, basic_rate=100 ) rm_items = [ @@ -467,7 +423,7 @@ class TestPurchaseReceipt(FrappeTestCase): "item_name": "_Test FG Item", "qty": po.supplied_items[0].required_qty, "warehouse": "_Test Warehouse - _TC", - "stock_uom": "Nos" + "stock_uom": "Nos", }, { "item_code": item_code, @@ -475,8 +431,8 @@ class TestPurchaseReceipt(FrappeTestCase): "item_name": "Test Extra Item 1", "qty": po.supplied_items[1].required_qty, "warehouse": "_Test Warehouse - _TC", - "stock_uom": "Nos" - } + "stock_uom": "Nos", + }, ] rm_item_string = json.dumps(rm_items) se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) @@ -495,15 +451,10 @@ class TestPurchaseReceipt(FrappeTestCase): pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) pr_row_1_serial_no = pr.get("items")[0].serial_no - self.assertEqual( - frappe.db.get_value("Serial No", pr_row_1_serial_no, "supplier"), - pr.supplier - ) + self.assertEqual(frappe.db.get_value("Serial No", pr_row_1_serial_no, "supplier"), pr.supplier) pr.cancel() - self.assertFalse( - frappe.db.get_value("Serial No", pr_row_1_serial_no, "warehouse") - ) + self.assertFalse(frappe.db.get_value("Serial No", pr_row_1_serial_no, "warehouse")) def test_rejected_serial_no(self): pr = frappe.copy_doc(test_records[0]) @@ -518,32 +469,34 @@ class TestPurchaseReceipt(FrappeTestCase): accepted_serial_nos = pr.get("items")[0].serial_no.split("\n") self.assertEqual(len(accepted_serial_nos), 3) for serial_no in accepted_serial_nos: - self.assertEqual(frappe.db.get_value("Serial No", serial_no, "warehouse"), - pr.get("items")[0].warehouse) + self.assertEqual( + frappe.db.get_value("Serial No", serial_no, "warehouse"), pr.get("items")[0].warehouse + ) rejected_serial_nos = pr.get("items")[0].rejected_serial_no.split("\n") self.assertEqual(len(rejected_serial_nos), 2) for serial_no in rejected_serial_nos: - self.assertEqual(frappe.db.get_value("Serial No", serial_no, "warehouse"), - pr.get("items")[0].rejected_warehouse) + 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" + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", ) return_pr = make_purchase_receipt( company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1", + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", is_return=1, return_against=pr.name, qty=-2, - do_not_submit=1 + do_not_submit=1, ) return_pr.items[0].purchase_receipt_item = pr.items[0].name return_pr.submit() @@ -551,16 +504,12 @@ class TestPurchaseReceipt(FrappeTestCase): # check sle outgoing_rate = frappe.db.get_value( "Stock Ledger Entry", - { - "voucher_type": "Purchase Receipt", - "voucher_no": return_pr.name - }, - "outgoing_rate" + {"voucher_type": "Purchase Receipt", "voucher_no": return_pr.name}, + "outgoing_rate", ) self.assertEqual(outgoing_rate, 50) - # check gl entries for return gl_entries = get_gl_entries("Purchase Receipt", return_pr.name) @@ -586,6 +535,7 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(pr.per_returned, 40) from erpnext.controllers.sales_and_purchase_return import make_return_doc + return_pr_2 = make_return_doc("Purchase Receipt", pr.name) # Check if unreturned amount is mapped in 2nd return @@ -608,7 +558,7 @@ class TestPurchaseReceipt(FrappeTestCase): # PR should be completed on billing all unreturned amount self.assertEqual(pr.items[0].billed_amt, 150) self.assertEqual(pr.per_billed, 100) - self.assertEqual(pr.status, 'Completed') + self.assertEqual(pr.status, "Completed") pi.load_from_db() pi.cancel() @@ -622,18 +572,18 @@ class TestPurchaseReceipt(FrappeTestCase): def test_purchase_return_full(self): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1" + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", ) return_pr = make_purchase_receipt( company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1", + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", is_return=1, return_against=pr.name, qty=-5, - do_not_submit=1 + do_not_submit=1, ) return_pr.items[0].purchase_receipt_item = pr.items[0].name return_pr.submit() @@ -646,7 +596,7 @@ class TestPurchaseReceipt(FrappeTestCase): # Check if Original PR updated self.assertEqual(pr.items[0].returned_qty, 5) self.assertEqual(pr.per_returned, 100) - self.assertEqual(pr.status, 'Return Issued') + self.assertEqual(pr.status, "Return Issued") return_pr.cancel() pr.cancel() @@ -654,32 +604,32 @@ class TestPurchaseReceipt(FrappeTestCase): def test_purchase_return_for_rejected_qty(self): from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse - rejected_warehouse="_Test Rejected Warehouse - TCP1" + rejected_warehouse = "_Test Rejected Warehouse - TCP1" if not frappe.db.exists("Warehouse", rejected_warehouse): get_warehouse( - company = "_Test Company with perpetual inventory", - abbr = " - TCP1", - warehouse_name = "_Test Rejected Warehouse" + company="_Test Company with perpetual inventory", + abbr=" - TCP1", + warehouse_name="_Test Rejected Warehouse", ).name pr = make_purchase_receipt( company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1", + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", qty=2, rejected_qty=2, - rejected_warehouse=rejected_warehouse + rejected_warehouse=rejected_warehouse, ) return_pr = make_purchase_receipt( company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1", + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", is_return=1, return_against=pr.name, qty=-2, - rejected_qty = -2, - rejected_warehouse=rejected_warehouse + rejected_qty=-2, + rejected_warehouse=rejected_warehouse, ) actual_qty = frappe.db.get_value( @@ -687,9 +637,9 @@ class TestPurchaseReceipt(FrappeTestCase): { "voucher_type": "Purchase Receipt", "voucher_no": return_pr.name, - "warehouse": return_pr.items[0].rejected_warehouse + "warehouse": return_pr.items[0].rejected_warehouse, }, - "actual_qty" + "actual_qty", ) self.assertEqual(actual_qty, -2) @@ -697,7 +647,6 @@ class TestPurchaseReceipt(FrappeTestCase): return_pr.cancel() pr.cancel() - def test_purchase_return_for_serialized_items(self): def _check_serial_no_values(serial_no, field_values): serial_no = frappe.get_doc("Serial No", serial_no) @@ -710,24 +659,22 @@ class TestPurchaseReceipt(FrappeTestCase): serial_no = get_serial_nos(pr.get("items")[0].serial_no)[0] - _check_serial_no_values(serial_no, { - "warehouse": "_Test Warehouse - _TC", - "purchase_document_no": pr.name - }) + _check_serial_no_values( + serial_no, {"warehouse": "_Test Warehouse - _TC", "purchase_document_no": pr.name} + ) return_pr = make_purchase_receipt( item_code="_Test Serialized Item With Series", qty=-1, is_return=1, return_against=pr.name, - serial_no=serial_no + serial_no=serial_no, ) - _check_serial_no_values(serial_no, { - "warehouse": "", - "purchase_document_no": pr.name, - "delivery_document_no": return_pr.name - }) + _check_serial_no_values( + serial_no, + {"warehouse": "", "purchase_document_no": pr.name, "delivery_document_no": return_pr.name}, + ) return_pr.cancel() pr.reload() @@ -735,20 +682,12 @@ class TestPurchaseReceipt(FrappeTestCase): def test_purchase_return_for_multi_uom(self): item_code = "_Test Purchase Return For Multi-UOM" - if not frappe.db.exists('Item', item_code): - item = make_item(item_code, {'stock_uom': 'Box'}) - row = item.append('uoms', { - 'uom': 'Unit', - 'conversion_factor': 0.1 - }) + if not frappe.db.exists("Item", item_code): + item = make_item(item_code, {"stock_uom": "Box"}) + row = item.append("uoms", {"uom": "Unit", "conversion_factor": 0.1}) row.db_update() - pr = make_purchase_receipt( - item_code=item_code, - qty=1, - uom="Box", - conversion_factor=1.0 - ) + pr = make_purchase_receipt(item_code=item_code, qty=1, uom="Box", conversion_factor=1.0) return_pr = make_purchase_receipt( item_code=item_code, qty=-10, @@ -756,7 +695,7 @@ class TestPurchaseReceipt(FrappeTestCase): stock_uom="Box", conversion_factor=0.1, is_return=1, - return_against=pr.name + return_against=pr.name, ) self.assertEqual(abs(return_pr.items[0].stock_qty), 1.0) @@ -772,18 +711,16 @@ class TestPurchaseReceipt(FrappeTestCase): pr = make_purchase_receipt() update_purchase_receipt_status(pr.name, "Closed") - self.assertEqual( - frappe.db.get_value("Purchase Receipt", pr.name, "status"), "Closed" - ) + self.assertEqual(frappe.db.get_value("Purchase Receipt", pr.name, "status"), "Closed") pr.reload() pr.cancel() def test_pr_billing_status(self): """Flow: - 1. PO -> PR1 -> PI - 2. PO -> PI - 3. PO -> PR2. + 1. PO -> PR1 -> PI + 2. PO -> PI + 3. PO -> PR2. """ from erpnext.buying.doctype.purchase_order.purchase_order import ( make_purchase_invoice as make_purchase_invoice_from_po, @@ -845,19 +782,15 @@ class TestPurchaseReceipt(FrappeTestCase): item = make_item(item_code, dict(has_serial_no=1)) serial_no = "12903812901" - pr_doc = make_purchase_receipt(item_code=item_code, - qty=1, serial_no = serial_no) + pr_doc = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no) self.assertEqual( serial_no, frappe.db.get_value( "Serial No", - { - "purchase_document_type": "Purchase Receipt", - "purchase_document_no": pr_doc.name - }, - "name" - ) + {"purchase_document_type": "Purchase Receipt", "purchase_document_no": pr_doc.name}, + "name", + ), ) pr_doc.cancel() @@ -874,12 +807,9 @@ class TestPurchaseReceipt(FrappeTestCase): serial_no, frappe.db.get_value( "Serial No", - { - "purchase_document_type": "Purchase Receipt", - "purchase_document_no": new_pr_doc.name - }, - "name" - ) + {"purchase_document_type": "Purchase Receipt", "purchase_document_no": new_pr_doc.name}, + "name", + ), ) new_pr_doc.cancel() @@ -887,40 +817,52 @@ class TestPurchaseReceipt(FrappeTestCase): def test_auto_asset_creation(self): asset_item = "Test Asset Item" - if not frappe.db.exists('Item', asset_item): - asset_category = frappe.get_all('Asset Category') + if not frappe.db.exists("Item", asset_item): + asset_category = frappe.get_all("Asset Category") if asset_category: asset_category = asset_category[0].name if not asset_category: - doc = frappe.get_doc({ - 'doctype': 'Asset Category', - 'asset_category_name': 'Test Asset Category', - 'depreciation_method': 'Straight Line', - 'total_number_of_depreciations': 12, - 'frequency_of_depreciation': 1, - 'accounts': [{ - 'company_name': '_Test Company', - 'fixed_asset_account': '_Test Fixed Asset - _TC', - 'accumulated_depreciation_account': '_Test Accumulated Depreciations - _TC', - 'depreciation_expense_account': '_Test Depreciations - _TC' - }] - }).insert() + doc = frappe.get_doc( + { + "doctype": "Asset Category", + "asset_category_name": "Test Asset Category", + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 12, + "frequency_of_depreciation": 1, + "accounts": [ + { + "company_name": "_Test Company", + "fixed_asset_account": "_Test Fixed Asset - _TC", + "accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC", + "depreciation_expense_account": "_Test Depreciations - _TC", + } + ], + } + ).insert() asset_category = doc.name - item_data = make_item(asset_item, {'is_stock_item':0, - 'stock_uom': 'Box', 'is_fixed_asset': 1, 'auto_create_assets': 1, - 'asset_category': asset_category, 'asset_naming_series': 'ABC.###'}) + item_data = make_item( + asset_item, + { + "is_stock_item": 0, + "stock_uom": "Box", + "is_fixed_asset": 1, + "auto_create_assets": 1, + "asset_category": asset_category, + "asset_naming_series": "ABC.###", + }, + ) asset_item = item_data.item_code pr = make_purchase_receipt(item_code=asset_item, qty=3) - assets = frappe.db.get_all('Asset', filters={'purchase_receipt': pr.name}) + assets = frappe.db.get_all("Asset", filters={"purchase_receipt": pr.name}) self.assertEqual(len(assets), 3) - location = frappe.db.get_value('Asset', assets[0].name, 'location') + location = frappe.db.get_value("Asset", assets[0].name, "location") self.assertEqual(location, "Test Location") pr.cancel() @@ -930,17 +872,18 @@ class TestPurchaseReceipt(FrappeTestCase): pr = make_purchase_receipt(item_code="Test Asset Item", qty=1) - asset = frappe.get_doc("Asset", { - 'purchase_receipt': pr.name - }) + asset = frappe.get_doc("Asset", {"purchase_receipt": pr.name}) asset.available_for_use_date = frappe.utils.nowdate() asset.gross_purchase_amount = 50.0 - asset.append("finance_books", { - "expected_value_after_useful_life": 10, - "depreciation_method": "Straight Line", - "total_number_of_depreciations": 3, - "frequency_of_depreciation": 1 - }) + asset.append( + "finance_books", + { + "expected_value_after_useful_life": 10, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 1, + }, + ) asset.submit() pr_return = make_purchase_return(pr.name) @@ -960,36 +903,27 @@ class TestPurchaseReceipt(FrappeTestCase): cost_center = "_Test Cost Center for BS Account - TCP1" create_cost_center( cost_center_name="_Test Cost Center for BS Account", - company="_Test Company with perpetual inventory" + company="_Test Company with perpetual inventory", ) - if not frappe.db.exists('Location', 'Test Location'): - frappe.get_doc({ - 'doctype': 'Location', - 'location_name': 'Test Location' - }).insert() + if not frappe.db.exists("Location", "Test Location"): + frappe.get_doc({"doctype": "Location", "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" + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", ) - stock_in_hand_account = get_inventory_account( - pr.company, pr.get("items")[0].warehouse - ) + stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse) gl_entries = get_gl_entries("Purchase Receipt", pr.name) self.assertTrue(gl_entries) expected_values = { - "Stock Received But Not Billed - TCP1": { - "cost_center": cost_center - }, - stock_in_hand_account: { - "cost_center": cost_center - } + "Stock Received But Not Billed - TCP1": {"cost_center": cost_center}, + stock_in_hand_account: {"cost_center": cost_center}, } for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) @@ -997,33 +931,24 @@ class TestPurchaseReceipt(FrappeTestCase): pr.cancel() def test_purchase_receipt_cost_center_with_balance_sheet_account(self): - if not frappe.db.exists('Location', 'Test Location'): - frappe.get_doc({ - 'doctype': 'Location', - 'location_name': 'Test Location' - }).insert() + if not frappe.db.exists("Location", "Test Location"): + frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert() pr = make_purchase_receipt( company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1" + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", ) - stock_in_hand_account = get_inventory_account( - pr.company, pr.get("items")[0].warehouse - ) + stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse) gl_entries = get_gl_entries("Purchase Receipt", pr.name) self.assertTrue(gl_entries) - cost_center = pr.get('items')[0].cost_center + cost_center = pr.get("items")[0].cost_center expected_values = { - "Stock Received But Not Billed - TCP1": { - "cost_center": cost_center - }, - stock_in_hand_account: { - "cost_center": cost_center - } + "Stock Received But Not Billed - TCP1": {"cost_center": cost_center}, + stock_in_hand_account: {"cost_center": cost_center}, } for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) @@ -1039,11 +964,7 @@ class TestPurchaseReceipt(FrappeTestCase): po = create_purchase_order() pr = create_pr_against_po(po.name) - pr1 = make_purchase_receipt( - qty=-1, - is_return=1, return_against=pr.name, - do_not_submit=True - ) + pr1 = make_purchase_receipt(qty=-1, is_return=1, return_against=pr.name, do_not_submit=True) pr1.items[0].purchase_order = po.name pr1.items[0].purchase_order_item = po.items[0].name pr1.items[0].purchase_receipt_item = pr.items[0].name @@ -1060,14 +981,17 @@ class TestPurchaseReceipt(FrappeTestCase): 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", { - "item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC", - "qty": 1, - "received_qty": 1, - "rate": 100, - "conversion_factor": 1.0, - }) + pr1.append( + "items", + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "qty": 1, + "received_qty": 1, + "rate": 100, + "conversion_factor": 1.0, + }, + ) pr1.submit() pi1 = make_purchase_invoice(pr1.name) @@ -1076,11 +1000,7 @@ class TestPurchaseReceipt(FrappeTestCase): pi1.save() pi1.submit() - pr2 = make_purchase_receipt( - qty=-2, - is_return=1, return_against=pr1.name, - do_not_submit=True - ) + pr2 = make_purchase_receipt(qty=-2, is_return=1, return_against=pr1.name, do_not_submit=True) pr2.items[0].purchase_receipt_item = pr1.items[0].name pr2.submit() @@ -1094,26 +1014,25 @@ class TestPurchaseReceipt(FrappeTestCase): 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) + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", do_not_save=1 + ) - pr.supplier_warehouse = '' - pr.items[0].from_warehouse = 'Work In Progress - TCP1' + pr.supplier_warehouse = "" + pr.items[0].from_warehouse = "Work In Progress - TCP1" pr.submit() - gl_entries = get_gl_entries('Purchase Receipt', pr.name) - sl_entries = get_sl_entries('Purchase Receipt', pr.name) + gl_entries = get_gl_entries("Purchase Receipt", pr.name) + sl_entries = get_sl_entries("Purchase Receipt", pr.name) self.assertFalse(gl_entries) - expected_sle = { - 'Work In Progress - TCP1': -5, - 'Stores - TCP1': 5 - } + expected_sle = {"Work In Progress - TCP1": -5, "Stores - TCP1": 5} for sle in sl_entries: self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty) @@ -1125,48 +1044,45 @@ class TestPurchaseReceipt(FrappeTestCase): create_warehouse( "_Test Warehouse for Valuation", company="_Test Company with perpetual inventory", - properties={"account": '_Test Account Stock In Hand - TCP1'} + properties={"account": "_Test Account Stock In Hand - TCP1"}, ) pr1 = make_purchase_receipt( - warehouse = '_Test Warehouse for Valuation - TCP1', - company="_Test Company with perpetual inventory" + 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 + company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", do_not_save=1 ) - pr.items[0].from_warehouse = '_Test Warehouse for Valuation - TCP1' - pr.supplier_warehouse = '' + pr.items[0].from_warehouse = "_Test Warehouse for Valuation - TCP1" + pr.supplier_warehouse = "" - - pr.append('taxes', { - 'charge_type': 'On Net Total', - 'account_head': '_Test Account Shipping Charges - TCP1', - 'category': 'Valuation and Total', - 'cost_center': 'Main - TCP1', - 'description': 'Test', - 'rate': 9 - }) + pr.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Shipping Charges - TCP1", + "category": "Valuation and Total", + "cost_center": "Main - TCP1", + "description": "Test", + "rate": 9, + }, + ) pr.submit() - gl_entries = get_gl_entries('Purchase Receipt', pr.name) - sl_entries = get_sl_entries('Purchase Receipt', pr.name) + gl_entries = get_gl_entries("Purchase Receipt", pr.name) + sl_entries = get_sl_entries("Purchase Receipt", pr.name) expected_gle = [ - ['Stock In Hand - TCP1', 272.5, 0.0], - ['_Test Account Stock In Hand - TCP1', 0.0, 250.0], - ['_Test Account Shipping Charges - TCP1', 0.0, 22.5] + ["Stock In Hand - TCP1", 272.5, 0.0], + ["_Test Account Stock In Hand - TCP1", 0.0, 250.0], + ["_Test Account Shipping Charges - TCP1", 0.0, 22.5], ] - expected_sle = { - '_Test Warehouse for Valuation - TCP1': -5, - 'Stores - TCP1': 5 - } + expected_sle = {"_Test Warehouse for Valuation - TCP1": -5, "Stores - TCP1": 5} for sle in sl_entries: self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty) @@ -1179,7 +1095,6 @@ class TestPurchaseReceipt(FrappeTestCase): pr.cancel() pr1.cancel() - def test_subcontracted_pr_for_multi_transfer_batches(self): from erpnext.buying.doctype.purchase_order.purchase_order import ( make_purchase_receipt, @@ -1194,49 +1109,57 @@ class TestPurchaseReceipt(FrappeTestCase): update_backflush_based_on("Material Transferred for Subcontract") item_code = "_Test Subcontracted FG Item 3" - make_item('Sub Contracted Raw Material 3', { - 'is_stock_item': 1, - 'is_sub_contracted_item': 1, - 'has_batch_no': 1, - 'create_new_batch': 1 - }) + make_item( + "Sub Contracted Raw Material 3", + {"is_stock_item": 1, "is_sub_contracted_item": 1, "has_batch_no": 1, "create_new_batch": 1}, + ) - create_subcontracted_item(item_code=item_code, has_batch_no=1, - raw_materials=["Sub Contracted Raw Material 3"]) + create_subcontracted_item( + item_code=item_code, has_batch_no=1, raw_materials=["Sub Contracted Raw Material 3"] + ) order_qty = 500 - po = create_purchase_order(item_code=item_code, qty=order_qty, - is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + item_code=item_code, + qty=order_qty, + is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC", + ) - ste1=make_stock_entry(target="_Test Warehouse - _TC", - item_code = "Sub Contracted Raw Material 3", qty=300, basic_rate=100) - ste2=make_stock_entry(target="_Test Warehouse - _TC", - item_code = "Sub Contracted Raw Material 3", qty=200, basic_rate=100) + ste1 = make_stock_entry( + target="_Test Warehouse - _TC", + item_code="Sub Contracted Raw Material 3", + qty=300, + basic_rate=100, + ) + ste2 = make_stock_entry( + target="_Test Warehouse - _TC", + item_code="Sub Contracted Raw Material 3", + qty=200, + basic_rate=100, + ) - transferred_batch = { - ste1.items[0].batch_no : 300, - ste2.items[0].batch_no : 200 - } + transferred_batch = {ste1.items[0].batch_no: 300, ste2.items[0].batch_no: 200} rm_items = [ { - "item_code":item_code, - "rm_item_code":"Sub Contracted Raw Material 3", - "item_name":"_Test Item", - "qty":300, - "warehouse":"_Test Warehouse - _TC", - "stock_uom":"Nos", - "name": po.supplied_items[0].name + "item_code": item_code, + "rm_item_code": "Sub Contracted Raw Material 3", + "item_name": "_Test Item", + "qty": 300, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + "name": po.supplied_items[0].name, }, { - "item_code":item_code, - "rm_item_code":"Sub Contracted Raw Material 3", - "item_name":"_Test Item", - "qty":200, - "warehouse":"_Test Warehouse - _TC", - "stock_uom":"Nos", - "name": po.supplied_items[0].name - } + "item_code": item_code, + "rm_item_code": "Sub Contracted Raw Material 3", + "item_name": "_Test Item", + "qty": 200, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + "name": po.supplied_items[0].name, + }, ] rm_item_string = json.dumps(rm_items) @@ -1248,11 +1171,8 @@ class TestPurchaseReceipt(FrappeTestCase): supplied_qty = frappe.db.get_value( "Purchase Order Item Supplied", - { - "parent": po.name, - "rm_item_code": "Sub Contracted Raw Material 3" - }, - "supplied_qty" + {"parent": po.name, "rm_item_code": "Sub Contracted Raw Material 3"}, + "supplied_qty", ) self.assertEqual(supplied_qty, 500.00) @@ -1272,12 +1192,11 @@ class TestPurchaseReceipt(FrappeTestCase): ste1.cancel() po.cancel() - def test_po_to_pi_and_po_to_pr_worflow_full(self): """Test following behaviour: - - Create PO - - Create PI from PO and submit - - Create PR from PO and submit + - Create PO + - Create PI from PO and submit + - Create PR from PO and submit """ from erpnext.buying.doctype.purchase_order import purchase_order, test_purchase_order @@ -1296,16 +1215,16 @@ class TestPurchaseReceipt(FrappeTestCase): def test_po_to_pi_and_po_to_pr_worflow_partial(self): """Test following behaviour: - - Create PO - - Create partial PI from PO and submit - - Create PR from PO and submit + - Create PO + - Create partial PI from PO and submit + - Create PR from PO and submit """ from erpnext.buying.doctype.purchase_order import purchase_order, test_purchase_order po = test_purchase_order.create_purchase_order() pi = purchase_order.make_purchase_invoice(po.name) - pi.items[0].qty /= 2 # roughly 50%, ^ this function only creates PI with 1 item. + pi.items[0].qty /= 2 # roughly 50%, ^ this function only creates PI with 1 item. pi.submit() pr = purchase_order.make_purchase_receipt(po.name) @@ -1329,11 +1248,14 @@ class TestPurchaseReceipt(FrappeTestCase): make_purchase_invoice as create_purchase_invoice, ) - pi = create_purchase_invoice(company="_Test Company with perpetual inventory", - cost_center = "Main - TCP1", - warehouse = "Stores - TCP1", - expense_account ="_Test Account Cost for Goods Sold - TCP1", - currency = "USD", conversion_rate = 70) + pi = create_purchase_invoice( + company="_Test Company with perpetual inventory", + cost_center="Main - TCP1", + warehouse="Stores - TCP1", + expense_account="_Test Account Cost for Goods Sold - TCP1", + currency="USD", + conversion_rate=70, + ) pr = create_purchase_receipt(pi.name) pr.conversion_rate = 80 @@ -1345,19 +1267,16 @@ class TestPurchaseReceipt(FrappeTestCase): # Get exchnage gain and loss account exchange_gain_loss_account = frappe.db.get_value( - 'Company', pr.company, 'exchange_gain_loss_account' + "Company", pr.company, "exchange_gain_loss_account" ) # fetching the latest GL Entry with exchange gain and loss account account amount = frappe.db.get_value( - 'GL Entry', - { - 'account': exchange_gain_loss_account, - 'voucher_no': pr.name - }, - 'credit' + "GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pr.name}, "credit" + ) + discrepancy_caused_by_exchange_rate_diff = abs( + pi.items[0].base_net_amount - pr.items[0].base_net_amount ) - discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount) self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount) @@ -1379,7 +1298,7 @@ class TestPurchaseReceipt(FrappeTestCase): po = create_purchase_order(qty=10, rate=100, do_not_save=1) create_payment_terms_template() - po.payment_terms_template = 'Test Receivable Template' + po.payment_terms_template = "Test Receivable Template" po.submit() pr = make_pr_against_po(po.name, received_qty=10) @@ -1406,7 +1325,9 @@ class TestPurchaseReceipt(FrappeTestCase): account = "Stock Received But Not Billed - TCP1" make_item(item_code) - se = make_stock_entry(item_code=item_code, from_warehouse=warehouse, qty=50, do_not_save=True, rate=0) + se = make_stock_entry( + item_code=item_code, from_warehouse=warehouse, qty=50, do_not_save=True, rate=0 + ) se.items[0].allow_zero_valuation_rate = 1 se.save() se.submit() @@ -1427,93 +1348,112 @@ class TestPurchaseReceipt(FrappeTestCase): def get_sl_entries(voucher_type, voucher_no): - return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference + return frappe.db.sql( + """ select actual_qty, warehouse, stock_value_difference from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s - order by posting_time desc""", (voucher_type, voucher_no), as_dict=1) + order by posting_time desc""", + (voucher_type, voucher_no), + as_dict=1, + ) + def get_gl_entries(voucher_type, voucher_no): - return frappe.db.sql("""select account, debit, credit, cost_center, is_cancelled + return frappe.db.sql( + """select account, debit, credit, cost_center, is_cancelled from `tabGL Entry` where voucher_type=%s and voucher_no=%s - order by account desc""", (voucher_type, voucher_no), as_dict=1) + order by account desc""", + (voucher_type, voucher_no), + as_dict=1, + ) + def get_taxes(**args): args = frappe._dict(args) - return [{'account_head': '_Test Account Shipping Charges - TCP1', - 'add_deduct_tax': 'Add', - 'category': 'Valuation and Total', - 'charge_type': 'Actual', - 'cost_center': args.cost_center or 'Main - TCP1', - 'description': 'Shipping Charges', - 'doctype': 'Purchase Taxes and Charges', - 'parentfield': 'taxes', - 'rate': 100.0, - 'tax_amount': 100.0}, - {'account_head': '_Test Account VAT - TCP1', - 'add_deduct_tax': 'Add', - 'category': 'Total', - 'charge_type': 'Actual', - 'cost_center': args.cost_center or 'Main - TCP1', - 'description': 'VAT', - 'doctype': 'Purchase Taxes and Charges', - 'parentfield': 'taxes', - 'rate': 120.0, - 'tax_amount': 120.0}, - {'account_head': '_Test Account Customs Duty - TCP1', - 'add_deduct_tax': 'Add', - 'category': 'Valuation', - 'charge_type': 'Actual', - 'cost_center': args.cost_center or 'Main - TCP1', - 'description': 'Customs Duty', - 'doctype': 'Purchase Taxes and Charges', - 'parentfield': 'taxes', - 'rate': 150.0, - 'tax_amount': 150.0}] + return [ + { + "account_head": "_Test Account Shipping Charges - TCP1", + "add_deduct_tax": "Add", + "category": "Valuation and Total", + "charge_type": "Actual", + "cost_center": args.cost_center or "Main - TCP1", + "description": "Shipping Charges", + "doctype": "Purchase Taxes and Charges", + "parentfield": "taxes", + "rate": 100.0, + "tax_amount": 100.0, + }, + { + "account_head": "_Test Account VAT - TCP1", + "add_deduct_tax": "Add", + "category": "Total", + "charge_type": "Actual", + "cost_center": args.cost_center or "Main - TCP1", + "description": "VAT", + "doctype": "Purchase Taxes and Charges", + "parentfield": "taxes", + "rate": 120.0, + "tax_amount": 120.0, + }, + { + "account_head": "_Test Account Customs Duty - TCP1", + "add_deduct_tax": "Add", + "category": "Valuation", + "charge_type": "Actual", + "cost_center": args.cost_center or "Main - TCP1", + "description": "Customs Duty", + "doctype": "Purchase Taxes and Charges", + "parentfield": "taxes", + "rate": 150.0, + "tax_amount": 150.0, + }, + ] + def get_items(**args): args = frappe._dict(args) - return [{ - "base_amount": 250.0, - "conversion_factor": 1.0, - "description": "_Test Item", - "doctype": "Purchase Receipt Item", - "item_code": "_Test Item", - "item_name": "_Test Item", - "parentfield": "items", - "qty": 5.0, - "rate": 50.0, - "received_qty": 5.0, - "rejected_qty": 0.0, - "stock_uom": "_Test UOM", - "uom": "_Test UOM", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "cost_center": args.cost_center or "Main - _TC" - }, - { - "base_amount": 250.0, - "conversion_factor": 1.0, - "description": "_Test Item Home Desktop 100", - "doctype": "Purchase Receipt Item", - "item_code": "_Test Item Home Desktop 100", - "item_name": "_Test Item Home Desktop 100", - "parentfield": "items", - "qty": 5.0, - "rate": 50.0, - "received_qty": 5.0, - "rejected_qty": 0.0, - "stock_uom": "_Test UOM", - "uom": "_Test UOM", - "warehouse": args.warehouse or "_Test Warehouse 1 - _TC", - "cost_center": args.cost_center or "Main - _TC" - }] + return [ + { + "base_amount": 250.0, + "conversion_factor": 1.0, + "description": "_Test Item", + "doctype": "Purchase Receipt Item", + "item_code": "_Test Item", + "item_name": "_Test Item", + "parentfield": "items", + "qty": 5.0, + "rate": 50.0, + "received_qty": 5.0, + "rejected_qty": 0.0, + "stock_uom": "_Test UOM", + "uom": "_Test UOM", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "cost_center": args.cost_center or "Main - _TC", + }, + { + "base_amount": 250.0, + "conversion_factor": 1.0, + "description": "_Test Item Home Desktop 100", + "doctype": "Purchase Receipt Item", + "item_code": "_Test Item Home Desktop 100", + "item_name": "_Test Item Home Desktop 100", + "parentfield": "items", + "qty": 5.0, + "rate": 50.0, + "received_qty": 5.0, + "rejected_qty": 0.0, + "stock_uom": "_Test UOM", + "uom": "_Test UOM", + "warehouse": args.warehouse or "_Test Warehouse 1 - _TC", + "cost_center": args.cost_center or "Main - _TC", + }, + ] + def make_purchase_receipt(**args): - if not frappe.db.exists('Location', 'Test Location'): - frappe.get_doc({ - 'doctype': 'Location', - 'location_name': 'Test Location' - }).insert() + if not frappe.db.exists("Location", "Test Location"): + frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert() frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) pr = frappe.new_doc("Purchase Receipt") @@ -1537,28 +1477,34 @@ def make_purchase_receipt(**args): item_code = args.item or args.item_code or "_Test Item" uom = args.uom or frappe.db.get_value("Item", item_code, "stock_uom") or "_Test UOM" - pr.append("items", { - "item_code": item_code, - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": qty, - "received_qty": received_qty, - "rejected_qty": rejected_qty, - "rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" if rejected_qty != 0 else "", - "rate": args.rate if args.rate != None else 50, - "conversion_factor": args.conversion_factor or 1.0, - "stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0), - "serial_no": args.serial_no, - "batch_no": args.batch_no, - "stock_uom": args.stock_uom or "_Test UOM", - "uom": uom, - "cost_center": args.cost_center or frappe.get_cached_value('Company', pr.company, 'cost_center'), - "asset_location": args.location or "Test Location" - }) + pr.append( + "items", + { + "item_code": item_code, + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": qty, + "received_qty": received_qty, + "rejected_qty": rejected_qty, + "rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" + if rejected_qty != 0 + else "", + "rate": args.rate if args.rate != None else 50, + "conversion_factor": args.conversion_factor or 1.0, + "stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0), + "serial_no": args.serial_no, + "batch_no": args.batch_no, + "stock_uom": args.stock_uom or "_Test UOM", + "uom": uom, + "cost_center": args.cost_center + or frappe.get_cached_value("Company", pr.company, "cost_center"), + "asset_location": args.location or "Test Location", + }, + ) if args.get_multiple_items: pr.items = [] - company_cost_center = frappe.get_cached_value('Company', pr.company, 'cost_center') + company_cost_center = frappe.get_cached_value("Company", pr.company, "cost_center") cost_center = args.cost_center or company_cost_center for item in get_items(warehouse=args.warehouse, cost_center=cost_center): @@ -1574,33 +1520,44 @@ def make_purchase_receipt(**args): pr.submit() return pr + def create_subcontracted_item(**args): from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom args = frappe._dict(args) - if not frappe.db.exists('Item', args.item_code): - make_item(args.item_code, { - 'is_stock_item': 1, - 'is_sub_contracted_item': 1, - 'has_batch_no': args.get("has_batch_no") or 0 - }) + if not frappe.db.exists("Item", args.item_code): + make_item( + args.item_code, + { + "is_stock_item": 1, + "is_sub_contracted_item": 1, + "has_batch_no": args.get("has_batch_no") or 0, + }, + ) if not args.raw_materials: - if not frappe.db.exists('Item', "Test Extra Item 1"): - make_item("Test Extra Item 1", { - 'is_stock_item': 1, - }) + if not frappe.db.exists("Item", "Test Extra Item 1"): + make_item( + "Test Extra Item 1", + { + "is_stock_item": 1, + }, + ) - if not frappe.db.exists('Item', "Test Extra Item 2"): - make_item("Test Extra Item 2", { - 'is_stock_item': 1, - }) + if not frappe.db.exists("Item", "Test Extra Item 2"): + make_item( + "Test Extra Item 2", + { + "is_stock_item": 1, + }, + ) - args.raw_materials = ['_Test FG Item', 'Test Extra Item 1'] + args.raw_materials = ["_Test FG Item", "Test Extra Item 1"] + + if not frappe.db.get_value("BOM", {"item": args.item_code}, "name"): + make_bom(item=args.item_code, raw_materials=args.get("raw_materials")) - if not frappe.db.get_value('BOM', {'item': args.item_code}, 'name'): - make_bom(item = args.item_code, raw_materials = args.get("raw_materials")) test_dependencies = ["BOM", "Item Price", "Location"] -test_records = frappe.get_test_records('Purchase Receipt') +test_records = frappe.get_test_records("Purchase Receipt") diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 4e472a92dc..623fbde2b0 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -24,11 +24,16 @@ class PutawayRule(Document): self.set_stock_capacity() def validate_duplicate_rule(self): - existing_rule = frappe.db.exists("Putaway Rule", {"item_code": self.item_code, "warehouse": self.warehouse}) + existing_rule = frappe.db.exists( + "Putaway Rule", {"item_code": self.item_code, "warehouse": self.warehouse} + ) if existing_rule and existing_rule != self.name: - frappe.throw(_("Putaway Rule already exists for Item {0} in Warehouse {1}.") - .format(frappe.bold(self.item_code), frappe.bold(self.warehouse)), - title=_("Duplicate")) + frappe.throw( + _("Putaway Rule already exists for Item {0} in Warehouse {1}.").format( + frappe.bold(self.item_code), frappe.bold(self.warehouse) + ), + title=_("Duplicate"), + ) def validate_priority(self): if self.priority < 1: @@ -37,18 +42,24 @@ class PutawayRule(Document): def validate_warehouse_and_company(self): company = frappe.db.get_value("Warehouse", self.warehouse, "company") if company != self.company: - frappe.throw(_("Warehouse {0} does not belong to Company {1}.") - .format(frappe.bold(self.warehouse), frappe.bold(self.company)), - title=_("Invalid Warehouse")) + frappe.throw( + _("Warehouse {0} does not belong to Company {1}.").format( + frappe.bold(self.warehouse), frappe.bold(self.company) + ), + title=_("Invalid Warehouse"), + ) def validate_capacity(self): stock_uom = frappe.db.get_value("Item", self.item_code, "stock_uom") balance_qty = get_stock_balance(self.item_code, self.warehouse, nowdate()) if flt(self.stock_capacity) < flt(balance_qty): - frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} {2}.") - .format(self.item_code, frappe.bold(balance_qty), stock_uom), - title=_("Insufficient Capacity")) + frappe.throw( + _( + "Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} {2}." + ).format(self.item_code, frappe.bold(balance_qty), stock_uom), + title=_("Insufficient Capacity"), + ) if not self.capacity: frappe.throw(_("Capacity must be greater than 0"), title=_("Invalid")) @@ -56,23 +67,26 @@ class PutawayRule(Document): def set_stock_capacity(self): self.stock_capacity = (flt(self.conversion_factor) or 1) * flt(self.capacity) + @frappe.whitelist() def get_available_putaway_capacity(rule): - stock_capacity, item_code, warehouse = frappe.db.get_value("Putaway Rule", rule, - ["stock_capacity", "item_code", "warehouse"]) + stock_capacity, item_code, warehouse = frappe.db.get_value( + "Putaway Rule", rule, ["stock_capacity", "item_code", "warehouse"] + ) balance_qty = get_stock_balance(item_code, warehouse, nowdate()) free_space = flt(stock_capacity) - flt(balance_qty) return free_space if free_space > 0 else 0 + @frappe.whitelist() def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): - """ Applies Putaway Rule on line items. + """Applies Putaway Rule on line items. - items: List of Purchase Receipt/Stock Entry Items - company: Company in the Purchase Receipt/Stock Entry - doctype: Doctype to apply rule on - purpose: Purpose of Stock Entry - sync (optional): Sync with client side only for client side calls + items: List of Purchase Receipt/Stock Entry Items + company: Company in the Purchase Receipt/Stock Entry + doctype: Doctype to apply rule on + purpose: Purpose of Stock Entry + sync (optional): Sync with client side only for client side calls """ if isinstance(items, str): items = json.loads(items) @@ -89,16 +103,18 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): item.conversion_factor = flt(item.conversion_factor) or 1.0 pending_qty, item_code = flt(item.qty), item.item_code pending_stock_qty = flt(item.transfer_qty) if doctype == "Stock Entry" else flt(item.stock_qty) - uom_must_be_whole_number = frappe.db.get_value('UOM', item.uom, 'must_be_whole_number') + uom_must_be_whole_number = frappe.db.get_value("UOM", item.uom, "must_be_whole_number") if not pending_qty or not item_code: updated_table = add_row(item, pending_qty, source_warehouse or item.warehouse, updated_table) continue - at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse) + at_capacity, rules = get_ordered_putaway_rules( + item_code, company, source_warehouse=source_warehouse + ) if not rules: - warehouse = source_warehouse or item.get('warehouse') + warehouse = source_warehouse or item.get("warehouse") if at_capacity: # rules available, but no free space items_not_accomodated.append([item_code, pending_qty]) @@ -117,23 +133,28 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): for rule in item_wise_rules[key]: if pending_stock_qty > 0 and rule.free_space: - stock_qty_to_allocate = flt(rule.free_space) if pending_stock_qty >= flt(rule.free_space) else pending_stock_qty + stock_qty_to_allocate = ( + flt(rule.free_space) if pending_stock_qty >= flt(rule.free_space) else pending_stock_qty + ) qty_to_allocate = stock_qty_to_allocate / item.conversion_factor if uom_must_be_whole_number: qty_to_allocate = floor(qty_to_allocate) stock_qty_to_allocate = qty_to_allocate * item.conversion_factor - if not qty_to_allocate: break + if not qty_to_allocate: + break - updated_table = add_row(item, qty_to_allocate, rule.warehouse, updated_table, - rule.name, serial_nos=serial_nos) + updated_table = add_row( + item, qty_to_allocate, rule.warehouse, updated_table, rule.name, serial_nos=serial_nos + ) pending_stock_qty -= stock_qty_to_allocate pending_qty -= qty_to_allocate rule["free_space"] -= stock_qty_to_allocate - if not pending_stock_qty > 0: break + if not pending_stock_qty > 0: + break # if pending qty after applying all rules, add row without warehouse if pending_stock_qty > 0: @@ -146,13 +167,14 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): items[:] = updated_table frappe.msgprint(_("Applied putaway rules."), alert=True) - if sync and json.loads(sync): # sync with client side + if sync and json.loads(sync): # sync with client side return items -def _items_changed(old, new, doctype: str) -> bool: - """ Check if any items changed by application of putaway rules. - If not, changing item table can have side effects since `name` items also changes. +def _items_changed(old, new, doctype: str) -> bool: + """Check if any items changed by application of putaway rules. + + If not, changing item table can have side effects since `name` items also changes. """ if len(old) != len(new): return True @@ -161,13 +183,22 @@ def _items_changed(old, new, doctype: str) -> bool: if doctype == "Stock Entry": compare_keys = ("item_code", "t_warehouse", "transfer_qty", "serial_no") - sort_key = lambda item: (item.item_code, cstr(item.t_warehouse), # noqa - flt(item.transfer_qty), cstr(item.serial_no)) + sort_key = lambda item: ( # noqa + item.item_code, + cstr(item.t_warehouse), + flt(item.transfer_qty), + cstr(item.serial_no), + ) else: # purchase receipt / invoice compare_keys = ("item_code", "warehouse", "stock_qty", "received_qty", "serial_no") - sort_key = lambda item: (item.item_code, cstr(item.warehouse), # noqa - flt(item.stock_qty), flt(item.received_qty), cstr(item.serial_no)) + sort_key = lambda item: ( # noqa + item.item_code, + cstr(item.warehouse), + flt(item.stock_qty), + flt(item.received_qty), + cstr(item.serial_no), + ) old_sorted = sorted(old, key=sort_key) new_sorted = sorted(new, key=sort_key) @@ -182,18 +213,16 @@ def _items_changed(old, new, doctype: str) -> bool: def get_ordered_putaway_rules(item_code, company, source_warehouse=None): """Returns an ordered list of putaway rules to apply on an item.""" - filters = { - "item_code": item_code, - "company": company, - "disable": 0 - } + filters = {"item_code": item_code, "company": company, "disable": 0} if source_warehouse: filters.update({"warehouse": ["!=", source_warehouse]}) - rules = frappe.get_all("Putaway Rule", + rules = frappe.get_all( + "Putaway Rule", fields=["name", "item_code", "stock_capacity", "priority", "warehouse"], filters=filters, - order_by="priority asc, capacity desc") + order_by="priority asc, capacity desc", + ) if not rules: return False, None @@ -211,10 +240,11 @@ def get_ordered_putaway_rules(item_code, company, source_warehouse=None): # then there is not enough space left in any rule return True, None - vacant_rules = sorted(vacant_rules, key = lambda i: (i['priority'], -i['free_space'])) + vacant_rules = sorted(vacant_rules, key=lambda i: (i["priority"], -i["free_space"])) return False, vacant_rules + def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=None): new_updated_table_row = copy.deepcopy(item) new_updated_table_row.idx = 1 if not updated_table else cint(updated_table[-1].idx) + 1 @@ -223,7 +253,9 @@ def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=N if item.doctype == "Stock Entry Detail": new_updated_table_row.t_warehouse = warehouse - new_updated_table_row.transfer_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) + new_updated_table_row.transfer_qty = flt(to_allocate) * flt( + new_updated_table_row.conversion_factor + ) else: new_updated_table_row.stock_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) new_updated_table_row.warehouse = warehouse @@ -238,6 +270,7 @@ def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=N updated_table.append(new_updated_table_row) return updated_table + def show_unassigned_items_message(items_not_accomodated): msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

    " formatted_item_rows = "" @@ -247,7 +280,9 @@ def show_unassigned_items_message(items_not_accomodated): formatted_item_rows += """ {0} {1} - """.format(item_link, frappe.bold(entry[1])) + """.format( + item_link, frappe.bold(entry[1]) + ) msg += """ @@ -257,13 +292,17 @@ def show_unassigned_items_message(items_not_accomodated): {2}
    - """.format(_("Item"), _("Unassigned Qty"), formatted_item_rows) + """.format( + _("Item"), _("Unassigned Qty"), formatted_item_rows + ) frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True) + def get_serial_nos_to_allocate(serial_nos, to_allocate): if serial_nos: - allocated_serial_nos = serial_nos[0: cint(to_allocate)] - serial_nos[:] = serial_nos[cint(to_allocate):] # pop out allocated serial nos and modify list + allocated_serial_nos = serial_nos[0 : cint(to_allocate)] + serial_nos[:] = serial_nos[cint(to_allocate) :] # pop out allocated serial nos and modify list return "\n".join(allocated_serial_nos) if allocated_serial_nos else "" - else: return "" + else: + return "" diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py index 0ec812c655..ab0ca106a8 100644 --- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py @@ -15,12 +15,9 @@ from erpnext.stock.get_item_details import get_conversion_factor class TestPutawayRule(FrappeTestCase): def setUp(self): if not frappe.db.exists("Item", "_Rice"): - make_item("_Rice", { - 'is_stock_item': 1, - 'has_batch_no' : 1, - 'create_new_batch': 1, - 'stock_uom': 'Kg' - }) + make_item( + "_Rice", {"is_stock_item": 1, "has_batch_no": 1, "create_new_batch": 1, "stock_uom": "Kg"} + ) if not frappe.db.exists("Warehouse", {"warehouse_name": "Rack 1"}): create_warehouse("Rack 1") @@ -36,10 +33,10 @@ class TestPutawayRule(FrappeTestCase): new_uom.save() def assertUnchangedItemsOnResave(self, doc): - """ Check if same items remain even after reapplication of rules. + """Check if same items remain even after reapplication of rules. - This is required since some business logic like subcontracting - depends on `name` of items to be same if item isn't changed. + This is required since some business logic like subcontracting + depends on `name` of items to be same if item isn't changed. """ doc.reload() old_items = {d.name for d in doc.items} @@ -49,13 +46,14 @@ class TestPutawayRule(FrappeTestCase): def test_putaway_rules_priority(self): """Test if rule is applied by priority, irrespective of free space.""" - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, - uom="Kg") - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=300, - uom="Kg", priority=2) + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg" + ) + rule_2 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_2, capacity=300, uom="Kg", priority=2 + ) - pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, - do_not_submit=1) + pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 200) self.assertEqual(pr.items[0].warehouse, self.warehouse_1) @@ -71,16 +69,19 @@ class TestPutawayRule(FrappeTestCase): def test_putaway_rules_with_same_priority(self): """Test if rule with more free space is applied, among two rules with same priority and capacity.""" - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=500, - uom="Kg") - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=500, - uom="Kg") + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=500, uom="Kg" + ) + rule_2 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_2, capacity=500, uom="Kg" + ) # out of 500 kg capacity, occupy 100 kg in warehouse_1 - stock_receipt = make_stock_entry(item_code="_Rice", target=self.warehouse_1, qty=100, basic_rate=50) + stock_receipt = make_stock_entry( + item_code="_Rice", target=self.warehouse_1, qty=100, basic_rate=50 + ) - pr = make_purchase_receipt(item_code="_Rice", qty=700, apply_putaway_rule=1, - do_not_submit=1) + pr = make_purchase_receipt(item_code="_Rice", qty=700, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 500) # warehouse_2 has 500 kg free space, it is given priority @@ -96,13 +97,14 @@ class TestPutawayRule(FrappeTestCase): def test_putaway_rules_with_insufficient_capacity(self): """Test if qty exceeding capacity, is handled.""" - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=100, - uom="Kg") - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=200, - uom="Kg") + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=100, uom="Kg" + ) + rule_2 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_2, capacity=200, uom="Kg" + ) - pr = make_purchase_receipt(item_code="_Rice", qty=350, apply_putaway_rule=1, - do_not_submit=1) + pr = make_purchase_receipt(item_code="_Rice", qty=350, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 200) self.assertEqual(pr.items[0].warehouse, self.warehouse_2) @@ -118,24 +120,32 @@ class TestPutawayRule(FrappeTestCase): """Test rules applied on uom other than stock uom.""" item = frappe.get_doc("Item", "_Rice") if not frappe.db.get_value("UOM Conversion Detail", {"parent": "_Rice", "uom": "Bag"}): - item.append("uoms", { - "uom": "Bag", - "conversion_factor": 1000 - }) + item.append("uoms", {"uom": "Bag", "conversion_factor": 1000}) item.save() - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=3, - uom="Bag") + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=3, uom="Bag" + ) self.assertEqual(rule_1.stock_capacity, 3000) - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=4, - uom="Bag") + rule_2 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_2, capacity=4, uom="Bag" + ) self.assertEqual(rule_2.stock_capacity, 4000) # populate 'Rack 1' with 1 Bag, making the free space 2 Bags - stock_receipt = make_stock_entry(item_code="_Rice", target=self.warehouse_1, qty=1000, basic_rate=50) + stock_receipt = make_stock_entry( + item_code="_Rice", target=self.warehouse_1, qty=1000, basic_rate=50 + ) - pr = make_purchase_receipt(item_code="_Rice", qty=6, uom="Bag", stock_uom="Kg", - conversion_factor=1000, apply_putaway_rule=1, do_not_submit=1) + pr = make_purchase_receipt( + item_code="_Rice", + qty=6, + uom="Bag", + stock_uom="Kg", + conversion_factor=1000, + apply_putaway_rule=1, + do_not_submit=1, + ) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 4) self.assertEqual(pr.items[0].warehouse, self.warehouse_2) @@ -151,25 +161,30 @@ class TestPutawayRule(FrappeTestCase): """Test if whole UOMs are handled.""" item = frappe.get_doc("Item", "_Rice") if not frappe.db.get_value("UOM Conversion Detail", {"parent": "_Rice", "uom": "Bag"}): - item.append("uoms", { - "uom": "Bag", - "conversion_factor": 1000 - }) + item.append("uoms", {"uom": "Bag", "conversion_factor": 1000}) item.save() frappe.db.set_value("UOM", "Bag", "must_be_whole_number", 1) # Putaway Rule in different UOM - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=1, - uom="Bag") + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=1, uom="Bag" + ) self.assertEqual(rule_1.stock_capacity, 1000) # Putaway Rule in Stock UOM rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=500) self.assertEqual(rule_2.stock_capacity, 500) # total capacity is 1500 Kg - pr = make_purchase_receipt(item_code="_Rice", qty=2, uom="Bag", stock_uom="Kg", - conversion_factor=1000, apply_putaway_rule=1, do_not_submit=1) + pr = make_purchase_receipt( + item_code="_Rice", + qty=2, + uom="Bag", + stock_uom="Kg", + conversion_factor=1000, + apply_putaway_rule=1, + do_not_submit=1, + ) self.assertEqual(len(pr.items), 1) self.assertEqual(pr.items[0].qty, 1) self.assertEqual(pr.items[0].warehouse, self.warehouse_1) @@ -184,23 +199,26 @@ class TestPutawayRule(FrappeTestCase): def test_putaway_rules_with_reoccurring_item(self): """Test rules on same item entered multiple times with different rate.""" - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, - uom="Kg") + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg" + ) # total capacity is 200 Kg - pr = make_purchase_receipt(item_code="_Rice", qty=100, apply_putaway_rule=1, - do_not_submit=1) - pr.append("items", { - "item_code": "_Rice", - "warehouse": "_Test Warehouse - _TC", - "qty": 200, - "uom": "Kg", - "stock_uom": "Kg", - "stock_qty": 200, - "received_qty": 200, - "rate": 100, - "conversion_factor": 1.0, - }) # same item entered again in PR but with different rate + pr = make_purchase_receipt(item_code="_Rice", qty=100, apply_putaway_rule=1, do_not_submit=1) + pr.append( + "items", + { + "item_code": "_Rice", + "warehouse": "_Test Warehouse - _TC", + "qty": 200, + "uom": "Kg", + "stock_uom": "Kg", + "stock_qty": 200, + "received_qty": 200, + "rate": 100, + "conversion_factor": 1.0, + }, + ) # same item entered again in PR but with different rate pr.save() self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 100) @@ -208,7 +226,7 @@ class TestPutawayRule(FrappeTestCase): self.assertEqual(pr.items[0].putaway_rule, rule_1.name) # same rule applied to second item row # with previous assignment considered - self.assertEqual(pr.items[1].qty, 100) # 100 unassigned in second row from 200 + self.assertEqual(pr.items[1].qty, 100) # 100 unassigned in second row from 200 self.assertEqual(pr.items[1].warehouse, self.warehouse_1) self.assertEqual(pr.items[1].putaway_rule, rule_1.name) @@ -219,13 +237,13 @@ class TestPutawayRule(FrappeTestCase): def test_validate_over_receipt_in_warehouse(self): """Test if overreceipt is blocked in the presence of putaway rules.""" - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, - uom="Kg") + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg" + ) - pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, - do_not_submit=1) + pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 1) - self.assertEqual(pr.items[0].qty, 200) # 100 is unassigned fro 300 Kg + self.assertEqual(pr.items[0].qty, 200) # 100 is unassigned fro 300 Kg self.assertEqual(pr.items[0].warehouse, self.warehouse_1) self.assertEqual(pr.items[0].putaway_rule, rule_1.name) @@ -240,21 +258,29 @@ class TestPutawayRule(FrappeTestCase): def test_putaway_rule_on_stock_entry_material_transfer(self): """Test if source warehouse is considered while applying rules.""" - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, - uom="Kg") # higher priority - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=100, - uom="Kg", priority=2) + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg" + ) # higher priority + rule_2 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_2, capacity=100, uom="Kg", priority=2 + ) - stock_entry = make_stock_entry(item_code="_Rice", source=self.warehouse_1, qty=200, - target="_Test Warehouse - _TC", purpose="Material Transfer", - apply_putaway_rule=1, do_not_submit=1) + stock_entry = make_stock_entry( + item_code="_Rice", + source=self.warehouse_1, + qty=200, + target="_Test Warehouse - _TC", + purpose="Material Transfer", + apply_putaway_rule=1, + do_not_submit=1, + ) stock_entry_item = stock_entry.get("items")[0] # since source warehouse is Rack 1, rule 1 (for Rack 1) will be avoided # even though it has more free space and higher priority self.assertEqual(stock_entry_item.t_warehouse, self.warehouse_2) - self.assertEqual(stock_entry_item.qty, 100) # unassigned 100 out of 200 Kg + self.assertEqual(stock_entry_item.qty, 100) # unassigned 100 out of 200 Kg self.assertEqual(stock_entry_item.putaway_rule, rule_2.name) self.assertUnchangedItemsOnResave(stock_entry) @@ -265,37 +291,48 @@ class TestPutawayRule(FrappeTestCase): def test_putaway_rule_on_stock_entry_material_transfer_reoccuring_item(self): """Test if reoccuring item is correctly considered.""" - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=300, - uom="Kg") - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=600, - uom="Kg", priority=2) + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=300, uom="Kg" + ) + rule_2 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_2, capacity=600, uom="Kg", priority=2 + ) # create SE with first row having source warehouse as Rack 2 - stock_entry = make_stock_entry(item_code="_Rice", source=self.warehouse_2, qty=200, - target="_Test Warehouse - _TC", purpose="Material Transfer", - apply_putaway_rule=1, do_not_submit=1) + stock_entry = make_stock_entry( + item_code="_Rice", + source=self.warehouse_2, + qty=200, + target="_Test Warehouse - _TC", + purpose="Material Transfer", + apply_putaway_rule=1, + do_not_submit=1, + ) # Add rows with source warehouse as Rack 1 - stock_entry.extend("items", [ - { - "item_code": "_Rice", - "s_warehouse": self.warehouse_1, - "t_warehouse": "_Test Warehouse - _TC", - "qty": 100, - "basic_rate": 50, - "conversion_factor": 1.0, - "transfer_qty": 100 - }, - { - "item_code": "_Rice", - "s_warehouse": self.warehouse_1, - "t_warehouse": "_Test Warehouse - _TC", - "qty": 200, - "basic_rate": 60, - "conversion_factor": 1.0, - "transfer_qty": 200 - } - ]) + stock_entry.extend( + "items", + [ + { + "item_code": "_Rice", + "s_warehouse": self.warehouse_1, + "t_warehouse": "_Test Warehouse - _TC", + "qty": 100, + "basic_rate": 50, + "conversion_factor": 1.0, + "transfer_qty": 100, + }, + { + "item_code": "_Rice", + "s_warehouse": self.warehouse_1, + "t_warehouse": "_Test Warehouse - _TC", + "qty": 200, + "basic_rate": 60, + "conversion_factor": 1.0, + "transfer_qty": 200, + }, + ], + ) stock_entry.save() @@ -323,19 +360,24 @@ class TestPutawayRule(FrappeTestCase): def test_putaway_rule_on_stock_entry_material_transfer_batch_serial_item(self): """Test if batch and serial items are split correctly.""" if not frappe.db.exists("Item", "Water Bottle"): - make_item("Water Bottle", { - "is_stock_item": 1, - "has_batch_no" : 1, - "create_new_batch": 1, - "has_serial_no": 1, - "serial_no_series": "BOTTL-.####", - "stock_uom": "Nos" - }) + make_item( + "Water Bottle", + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "has_serial_no": 1, + "serial_no_series": "BOTTL-.####", + "stock_uom": "Nos", + }, + ) - rule_1 = create_putaway_rule(item_code="Water Bottle", warehouse=self.warehouse_1, capacity=3, - uom="Nos") - rule_2 = create_putaway_rule(item_code="Water Bottle", warehouse=self.warehouse_2, capacity=2, - uom="Nos") + rule_1 = create_putaway_rule( + item_code="Water Bottle", warehouse=self.warehouse_1, capacity=3, uom="Nos" + ) + rule_2 = create_putaway_rule( + item_code="Water Bottle", warehouse=self.warehouse_2, capacity=2, uom="Nos" + ) make_new_batch(batch_id="BOTTL-BATCH-1", item_code="Water Bottle") @@ -344,12 +386,20 @@ class TestPutawayRule(FrappeTestCase): pr.save() pr.submit() - serial_nos = frappe.get_list("Serial No", filters={"purchase_document_no": pr.name, "status": "Active"}) + serial_nos = frappe.get_list( + "Serial No", filters={"purchase_document_no": pr.name, "status": "Active"} + ) serial_nos = [d.name for d in serial_nos] - stock_entry = make_stock_entry(item_code="Water Bottle", source="_Test Warehouse - _TC", qty=5, - target="Finished Goods - _TC", purpose="Material Transfer", - apply_putaway_rule=1, do_not_save=1) + stock_entry = make_stock_entry( + item_code="Water Bottle", + source="_Test Warehouse - _TC", + qty=5, + target="Finished Goods - _TC", + purpose="Material Transfer", + apply_putaway_rule=1, + do_not_save=1, + ) stock_entry.items[0].batch_no = "BOTTL-BATCH-1" stock_entry.items[0].serial_no = "\n".join(serial_nos) stock_entry.save() @@ -375,14 +425,21 @@ class TestPutawayRule(FrappeTestCase): def test_putaway_rule_on_stock_entry_material_receipt(self): """Test if rules are applied in Stock Entry of type Receipt.""" - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, - uom="Kg") # more capacity - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=100, - uom="Kg") + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg" + ) # more capacity + rule_2 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_2, capacity=100, uom="Kg" + ) - stock_entry = make_stock_entry(item_code="_Rice", qty=100, - target="_Test Warehouse - _TC", purpose="Material Receipt", - apply_putaway_rule=1, do_not_submit=1) + stock_entry = make_stock_entry( + item_code="_Rice", + qty=100, + target="_Test Warehouse - _TC", + purpose="Material Receipt", + apply_putaway_rule=1, + do_not_submit=1, + ) stock_entry_item = stock_entry.get("items")[0] @@ -400,8 +457,7 @@ class TestPutawayRule(FrappeTestCase): from erpnext.stock.dashboard.warehouse_capacity_dashboard import get_data item = "_Rice" - rule = create_putaway_rule(item_code=item, warehouse=self.warehouse_1, capacity=500, - uom="Kg") + rule = create_putaway_rule(item_code=item, warehouse=self.warehouse_1, capacity=500, uom="Kg") capacities = get_data(warehouse=self.warehouse_1) for capacity in capacities: @@ -411,6 +467,7 @@ class TestPutawayRule(FrappeTestCase): get_data(warehouse=self.warehouse_1) rule.delete() + def create_putaway_rule(**args): args = frappe._dict(args) putaway = frappe.new_doc("Putaway Rule") @@ -423,7 +480,9 @@ def create_putaway_rule(**args): putaway.capacity = args.capacity or 1 putaway.stock_uom = frappe.db.get_value("Item", putaway.item_code, "stock_uom") putaway.uom = args.uom or putaway.stock_uom - putaway.conversion_factor = get_conversion_factor(putaway.item_code, putaway.uom)['conversion_factor'] + putaway.conversion_factor = get_conversion_factor(putaway.item_code, putaway.uom)[ + "conversion_factor" + ] if not args.do_not_save: putaway.save() diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 4e3b80aa76..331d3e812b 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -18,8 +18,8 @@ class QualityInspection(Document): if not self.readings and self.item_code: self.get_item_specification_details() - if self.inspection_type=="In Process" and self.reference_type=="Job Card": - item_qi_template = frappe.db.get_value("Item", self.item_code, 'quality_inspection_template') + if self.inspection_type == "In Process" and self.reference_type == "Job Card": + item_qi_template = frappe.db.get_value("Item", self.item_code, "quality_inspection_template") parameters = get_template_details(item_qi_template) for reading in self.readings: for d in parameters: @@ -33,26 +33,28 @@ class QualityInspection(Document): @frappe.whitelist() def get_item_specification_details(self): if not self.quality_inspection_template: - self.quality_inspection_template = frappe.db.get_value('Item', - self.item_code, 'quality_inspection_template') + self.quality_inspection_template = frappe.db.get_value( + "Item", self.item_code, "quality_inspection_template" + ) - if not self.quality_inspection_template: return + if not self.quality_inspection_template: + return - self.set('readings', []) + self.set("readings", []) parameters = get_template_details(self.quality_inspection_template) for d in parameters: - child = self.append('readings', {}) + child = self.append("readings", {}) child.update(d) child.status = "Accepted" @frappe.whitelist() def get_quality_inspection_template(self): - template = '' + template = "" if self.bom_no: - template = frappe.db.get_value('BOM', self.bom_no, 'quality_inspection_template') + template = frappe.db.get_value("BOM", self.bom_no, "quality_inspection_template") if not template: - template = frappe.db.get_value('BOM', self.item_code, 'quality_inspection_template') + template = frappe.db.get_value("BOM", self.item_code, "quality_inspection_template") self.quality_inspection_template = template self.get_item_specification_details() @@ -66,21 +68,25 @@ class QualityInspection(Document): def update_qc_reference(self): quality_inspection = self.name if self.docstatus == 1 else "" - if self.reference_type == 'Job Card': + if self.reference_type == "Job Card": if self.reference_name: - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tab{doctype}` SET quality_inspection = %s, modified = %s WHERE name = %s and production_item = %s - """.format(doctype=self.reference_type), - (quality_inspection, self.modified, self.reference_name, self.item_code)) + """.format( + doctype=self.reference_type + ), + (quality_inspection, self.modified, self.reference_name, self.item_code), + ) else: args = [quality_inspection, self.modified, self.reference_name, self.item_code] - doctype = self.reference_type + ' Item' + doctype = self.reference_type + " Item" - if self.reference_type == 'Stock Entry': - doctype = 'Stock Entry Detail' + if self.reference_type == "Stock Entry": + doctype = "Stock Entry Detail" if self.reference_type and self.reference_name: conditions = "" @@ -88,11 +94,12 @@ class QualityInspection(Document): conditions += " and t1.batch_no = %s" args.append(self.batch_no) - if self.docstatus == 2: # if cancel, then remove qi link wherever same name + if self.docstatus == 2: # if cancel, then remove qi link wherever same name conditions += " and t1.quality_inspection = %s" args.append(self.name) - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tab{child_doc}` t1, `tab{parent_doc}` t2 SET @@ -102,12 +109,15 @@ class QualityInspection(Document): and t1.item_code = %s and t1.parent = t2.name {conditions} - """.format(parent_doc=self.reference_type, child_doc=doctype, conditions=conditions), - args) + """.format( + parent_doc=self.reference_type, child_doc=doctype, conditions=conditions + ), + args, + ) def inspect_and_set_status(self): for reading in self.readings: - if not reading.manual_inspection: # dont auto set status if manual + 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: @@ -129,13 +139,16 @@ class QualityInspection(Document): 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 + if not result: + return False return True 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")) + 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) @@ -145,12 +158,17 @@ class QualityInspection(Document): 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")) + 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")) + frappe.throw( + _("Row #{0}: Acceptance Criteria Formula is incorrect.").format(reading.idx), + title=_("Invalid Formula"), + ) def get_formula_evaluation_data(self, reading): data = {} @@ -168,6 +186,7 @@ class QualityInspection(Document): def calculate_mean(self, reading): """Calculate mean of all non-empty readings.""" from statistics import mean + readings_list = [] for i in range(1, 11): @@ -178,65 +197,90 @@ class QualityInspection(Document): 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): if filters.get("from"): from frappe.desk.reportview import get_match_cond + mcond = get_match_cond(filters["from"]) cond, qi_condition = "", "and (quality_inspection is null or quality_inspection = '')" if filters.get("parent"): - if filters.get('from') in ['Purchase Invoice Item', 'Purchase Receipt Item']\ - and filters.get("inspection_type") != "In Process": + if ( + filters.get("from") in ["Purchase Invoice Item", "Purchase Receipt Item"] + and filters.get("inspection_type") != "In Process" + ): cond = """and item_code in (select name from `tabItem` where inspection_required_before_purchase = 1)""" - elif filters.get('from') in ['Sales Invoice Item', 'Delivery Note Item']\ - and filters.get("inspection_type") != "In Process": + elif ( + filters.get("from") in ["Sales Invoice Item", "Delivery Note Item"] + and filters.get("inspection_type") != "In Process" + ): cond = """and item_code in (select name from `tabItem` where inspection_required_before_delivery = 1)""" - elif filters.get('from') == 'Stock Entry Detail': + elif filters.get("from") == "Stock Entry Detail": cond = """and s_warehouse is null""" - if filters.get('from') in ['Supplier Quotation Item']: + if filters.get("from") in ["Supplier Quotation Item"]: qi_condition = "" - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT item_code FROM `tab{doc}` WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s {qi_condition} {cond} {mcond} ORDER BY item_code limit {start}, {page_len} - """.format(doc=filters.get('from'), - cond = cond, mcond = mcond, start = start, - page_len = page_len, qi_condition = qi_condition), - {'parent': filters.get('parent'), 'txt': "%%%s%%" % txt}) + """.format( + doc=filters.get("from"), + cond=cond, + mcond=mcond, + start=start, + page_len=page_len, + qi_condition=qi_condition, + ), + {"parent": filters.get("parent"), "txt": "%%%s%%" % txt}, + ) elif filters.get("reference_name"): - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT production_item FROM `tab{doc}` WHERE name = %(reference_name)s and docstatus < 2 and production_item like %(txt)s {qi_condition} {cond} {mcond} ORDER BY production_item LIMIT {start}, {page_len} - """.format(doc=filters.get("from"), - cond = cond, mcond = mcond, start = start, - page_len = page_len, qi_condition = qi_condition), - {'reference_name': filters.get('reference_name'), 'txt': "%%%s%%" % txt}) + """.format( + doc=filters.get("from"), + cond=cond, + mcond=mcond, + start=start, + page_len=page_len, + qi_condition=qi_condition, + ), + {"reference_name": filters.get("reference_name"), "txt": "%%%s%%" % txt}, + ) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def quality_inspection_query(doctype, txt, searchfield, start, page_len, filters): - return frappe.get_all('Quality Inspection', + return frappe.get_all( + "Quality Inspection", limit_start=start, limit_page_length=page_len, - filters = { - 'docstatus': 1, - 'name': ('like', '%%%s%%' % txt), - 'item_code': filters.get("item_code"), - 'reference_name': ('in', [filters.get("reference_name", ''), '']) - }, as_list=1) + filters={ + "docstatus": 1, + "name": ("like", "%%%s%%" % txt), + "item_code": filters.get("item_code"), + "reference_name": ("in", [filters.get("reference_name", ""), ""]), + }, + as_list=1, + ) + @frappe.whitelist() def make_quality_inspection(source_name, target_doc=None): @@ -244,19 +288,18 @@ def make_quality_inspection(source_name, target_doc=None): doc.inspected_by = frappe.session.user doc.get_quality_inspection_template() - doc = get_mapped_doc("BOM", source_name, { - 'BOM': { - "doctype": "Quality Inspection", - "validation": { - "docstatus": ["=", 1] - }, - "field_map": { - "name": "bom_no", - "item": "item_code", - "stock_uom": "uom", - "stock_qty": "qty" - }, - } - }, target_doc, postprocess) + doc = get_mapped_doc( + "BOM", + source_name, + { + "BOM": { + "doctype": "Quality Inspection", + "validation": {"docstatus": ["=", 1]}, + "field_map": {"name": "bom_no", "item": "item_code", "stock_uom": "uom", "stock_qty": "qty"}, + } + }, + target_doc, + postprocess, + ) return doc diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index 601ca054b5..144f13880b 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -22,16 +22,11 @@ class TestQualityInspection(FrappeTestCase): def setUp(self): super().setUp() create_item("_Test Item with QA") - frappe.db.set_value( - "Item", "_Test Item with QA", "inspection_required_before_delivery", 1 - ) + frappe.db.set_value("Item", "_Test Item with QA", "inspection_required_before_delivery", 1) def test_qa_for_delivery(self): make_stock_entry( - item_code="_Test Item with QA", - target="_Test Warehouse - _TC", - qty=1, - basic_rate=100 + item_code="_Test Item with QA", target="_Test Warehouse - _TC", qty=1, basic_rate=100 ) dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) @@ -71,21 +66,18 @@ class TestQualityInspection(FrappeTestCase): "specification": "Iron Content", # numeric reading "min_value": 0.1, "max_value": 0.9, - "reading_1": "0.4" + "reading_1": "0.4", }, { "specification": "Particle Inspection Needed", # non-numeric reading "numeric": 0, "value": "Yes", - "reading_value": "Yes" - } + "reading_value": "Yes", + }, ] qa = create_quality_inspection( - reference_type="Delivery Note", - reference_name=dn.name, - readings=readings, - do_not_save=True + reference_type="Delivery Note", reference_name=dn.name, readings=readings, do_not_save=True ) qa.save() @@ -104,13 +96,13 @@ class TestQualityInspection(FrappeTestCase): "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", # 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", # numeric reading @@ -118,22 +110,19 @@ class TestQualityInspection(FrappeTestCase): "acceptance_formula": "mean < 0.9", "reading_1": "0.5", "reading_2": "0.7", - "reading_3": "random text" # check if random string input causes issues + "reading_3": "random text", # check if random string input causes issues }, { "specification": "Calcium Content", # non-numeric reading "formula_based_criteria": 1, "numeric": 0, "acceptance_formula": "reading_value in ('Grade A', 'Grade B', 'Grade C')", - "reading_value": "Grade B" - } + "reading_value": "Grade B", + }, ] qa = create_quality_inspection( - reference_type="Delivery Note", - reference_name=dn.name, - readings=readings, - do_not_save=True + reference_type="Delivery Note", reference_name=dn.name, readings=readings, do_not_save=True ) qa.save() @@ -167,32 +156,26 @@ class TestQualityInspection(FrappeTestCase): qty=1, basic_rate=100, inspection_required=True, - do_not_submit=True + do_not_submit=True, ) readings = [ - { - "specification": "Iron Content", - "min_value": 0.1, - "max_value": 0.9, - "reading_1": "0.4" - } + {"specification": "Iron Content", "min_value": 0.1, "max_value": 0.9, "reading_1": "0.4"} ] qa = create_quality_inspection( - reference_type="Stock Entry", - reference_name=se.name, - readings=readings, - status="Rejected" + reference_type="Stock Entry", reference_name=se.name, readings=readings, status="Rejected" ) frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Stop") se.reload() - self.assertRaises(QualityInspectionRejectedError, se.submit) # when blocked in Stock settings, block rejected QI + self.assertRaises( + QualityInspectionRejectedError, se.submit + ) # when blocked in Stock settings, block rejected QI frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Warn") se.reload() - se.submit() # when allowed in Stock settings, allow rejected QI + se.submit() # when allowed in Stock settings, allow rejected QI # teardown qa.reload() @@ -201,6 +184,7 @@ class TestQualityInspection(FrappeTestCase): se.cancel() frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Stop") + def create_quality_inspection(**args): args = frappe._dict(args) qa = frappe.new_doc("Quality Inspection") @@ -238,8 +222,6 @@ def create_quality_inspection(**args): 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() + frappe.get_doc( + {"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter} + ).insert() 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 7f8c871a93..9b8f5d6378 100644 --- a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py +++ b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py @@ -9,11 +9,22 @@ from frappe.model.document import Document class QualityInspectionTemplate(Document): pass -def get_template_details(template): - if not template: return [] - return frappe.get_all('Item Quality Inspection Parameter', - fields=["specification", "value", "acceptance_formula", - "numeric", "formula_based_criteria", "min_value", "max_value"], - filters={'parenttype': 'Quality Inspection Template', 'parent': template}, - order_by="idx") +def get_template_details(template): + if not template: + return [] + + return frappe.get_all( + "Item Quality Inspection Parameter", + fields=[ + "specification", + "value", + "acceptance_formula", + "numeric", + "formula_based_criteria", + "min_value", + "max_value", + ], + filters={"parenttype": "Quality Inspection Template", "parent": template}, + order_by="idx", + ) diff --git a/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py b/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py index 7a0f5d0821..846be0b9bd 100644 --- a/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py +++ b/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py @@ -12,24 +12,25 @@ from erpnext.stock.utils import get_stock_balance, get_stock_value_on class QuickStockBalance(Document): pass + @frappe.whitelist() def get_stock_item_details(warehouse, date, item=None, barcode=None): out = {} if barcode: out["item"] = frappe.db.get_value( - "Item Barcode", filters={"barcode": barcode}, fieldname=["parent"]) + "Item Barcode", filters={"barcode": barcode}, fieldname=["parent"] + ) if not out["item"]: - frappe.throw( - _("Invalid Barcode. There is no Item attached to this barcode.")) + frappe.throw(_("Invalid Barcode. There is no Item attached to this barcode.")) else: out["item"] = item - barcodes = frappe.db.get_values("Item Barcode", filters={"parent": out["item"]}, - fieldname=["barcode"]) + barcodes = frappe.db.get_values( + "Item Barcode", filters={"parent": out["item"]}, fieldname=["barcode"] + ) out["barcodes"] = [x[0] for x in barcodes] out["qty"] = get_stock_balance(out["item"], warehouse, date) out["value"] = get_stock_value_on(warehouse, date, out["item"]) - out["image"] = frappe.db.get_value("Item", - filters={"name": out["item"]}, fieldname=["image"]) + out["image"] = frappe.db.get_value("Item", filters={"name": out["item"]}, fieldname=["image"]) return out 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 f8ec784697..ec1d140447 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -23,7 +23,7 @@ class RepostItemValuation(Document): self.set_company() def reset_field_values(self): - if self.based_on == 'Transaction': + if self.based_on == "Transaction": self.item_code = None self.warehouse = None @@ -38,20 +38,20 @@ class RepostItemValuation(Document): def set_status(self, status=None, write=True): status = status or self.status if not status: - self.status = 'Queued' + self.status = "Queued" else: self.status = status if write: - self.db_set('status', self.status) + self.db_set("status", self.status) def on_submit(self): """During tests reposts are executed immediately. Exceptions: - 1. "Repost Item Valuation" document has self.flags.dont_run_in_test - 2. global flag frappe.flags.dont_execute_stock_reposts is set + 1. "Repost Item Valuation" document has self.flags.dont_run_in_test + 2. global flag frappe.flags.dont_execute_stock_reposts is set - These flags are useful for asserting real time behaviour like quantity updates. + These flags are useful for asserting real time behaviour like quantity updates. """ if not frappe.flags.in_test: @@ -63,14 +63,14 @@ class RepostItemValuation(Document): @frappe.whitelist() def restart_reposting(self): - self.set_status('Queued', write=False) + self.set_status("Queued", write=False) self.current_index = 0 self.distinct_item_and_warehouse = None self.items_to_be_repost = None self.db_update() def deduplicate_similar_repost(self): - """ Deduplicate similar reposts based on item-warehouse-posting combination.""" + """Deduplicate similar reposts based on item-warehouse-posting combination.""" if self.based_on != "Item and Warehouse": return @@ -82,7 +82,8 @@ class RepostItemValuation(Document): "posting_time": self.posting_time, } - frappe.db.sql(""" + frappe.db.sql( + """ update `tabRepost Item Valuation` set status = 'Skipped' WHERE item_code = %(item_code)s @@ -93,9 +94,10 @@ class RepostItemValuation(Document): and status = 'Queued' and based_on = 'Item and Warehouse' """, - filters + filters, ) + def on_doctype_update(): frappe.db.add_index("Repost Item Valuation", ["warehouse", "item_code"], "item_warehouse") @@ -105,14 +107,14 @@ def repost(doc): if not frappe.db.exists("Repost Item Valuation", doc.name): return - doc.set_status('In Progress') + doc.set_status("In Progress") if not frappe.flags.in_test: frappe.db.commit() repost_sl_entries(doc) repost_gl_entries(doc) - doc.set_status('Completed') + doc.set_status("Completed") except (Exception, JobTimeoutException): frappe.db.rollback() @@ -122,32 +124,47 @@ def repost(doc): message = frappe.message_log.pop() if traceback: message += "
    " + "Traceback:
    " + traceback - frappe.db.set_value(doc.doctype, doc.name, 'error_log', message) + frappe.db.set_value(doc.doctype, doc.name, "error_log", message) notify_error_to_stock_managers(doc, message) - doc.set_status('Failed') + doc.set_status("Failed") raise finally: if not frappe.flags.in_test: 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, doc=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, + doc=doc, + ) 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) + 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': + if doc.based_on == "Transaction": ref_doc = frappe.get_doc(doc.voucher_type, doc.voucher_no) doc_items, doc_warehouses = ref_doc.get_items_and_warehouses() @@ -161,8 +178,14 @@ def repost_gl_entries(doc): items = [doc.item_code] warehouses = [doc.warehouse] - update_gl_entries_after(doc.posting_date, doc.posting_time, - for_warehouses=warehouses, for_items=items, company=doc.company) + update_gl_entries_after( + doc.posting_date, + doc.posting_time, + for_warehouses=warehouses, + for_items=items, + company=doc.company, + ) + def notify_error_to_stock_managers(doc, traceback): recipients = get_users_with_role("Stock Manager") @@ -170,13 +193,20 @@ def notify_error_to_stock_managers(doc, traceback): 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.") + 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) + def repost_entries(): if not in_configured_timeslot(): return @@ -184,8 +214,8 @@ def repost_entries(): riv_entries = get_repost_item_valuation_entries() for row in riv_entries: - doc = frappe.get_doc('Repost Item Valuation', row.name) - if doc.status in ('Queued', 'In Progress'): + doc = frappe.get_doc("Repost Item Valuation", row.name) + if doc.status in ("Queued", "In Progress"): repost(doc) doc.deduplicate_similar_repost() @@ -193,14 +223,19 @@ def repost_entries(): if riv_entries: return - for d in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}): + for d in frappe.get_all("Company", filters={"enable_perpetual_inventory": 1}): check_if_stock_and_account_balance_synced(today(), d.name) + def get_repost_item_valuation_entries(): - return frappe.db.sql(""" SELECT name from `tabRepost Item Valuation` + return frappe.db.sql( + """ SELECT name from `tabRepost Item Valuation` WHERE status in ('Queued', 'In Progress') and creation <= %s and docstatus = 1 ORDER BY timestamp(posting_date, posting_time) asc, creation asc - """, now(), as_dict=1) + """, + now(), + as_dict=1, + ) def in_configured_timeslot(repost_settings=None, current_time=None): 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 index 78b432d564..f3bebad5c0 100644 --- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -153,7 +153,7 @@ class TestRepostItemValuation(unittest.TestCase): posting_date=today, posting_time="00:01:00", ) - riv.flags.dont_run_in_test = True # keep it queued + riv.flags.dont_run_in_test = True # keep it queued riv.submit() stock_settings = frappe.get_doc("Stock Settings") diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 2808c219ea..316c897da0 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -24,16 +24,45 @@ from erpnext.controllers.stock_controller import StockController from erpnext.stock.get_item_details import get_reserved_qty_for_so -class SerialNoCannotCreateDirectError(ValidationError): pass -class SerialNoCannotCannotChangeError(ValidationError): pass -class SerialNoNotRequiredError(ValidationError): pass -class SerialNoRequiredError(ValidationError): pass -class SerialNoQtyError(ValidationError): pass -class SerialNoItemError(ValidationError): pass -class SerialNoWarehouseError(ValidationError): pass -class SerialNoBatchError(ValidationError): pass -class SerialNoNotExistsError(ValidationError): pass -class SerialNoDuplicateError(ValidationError): pass +class SerialNoCannotCreateDirectError(ValidationError): + pass + + +class SerialNoCannotCannotChangeError(ValidationError): + pass + + +class SerialNoNotRequiredError(ValidationError): + pass + + +class SerialNoRequiredError(ValidationError): + pass + + +class SerialNoQtyError(ValidationError): + pass + + +class SerialNoItemError(ValidationError): + pass + + +class SerialNoWarehouseError(ValidationError): + pass + + +class SerialNoBatchError(ValidationError): + pass + + +class SerialNoNotExistsError(ValidationError): + pass + + +class SerialNoDuplicateError(ValidationError): + pass + class SerialNo(StockController): def __init__(self, *args, **kwargs): @@ -42,7 +71,12 @@ class SerialNo(StockController): def validate(self): if self.get("__islocal") and self.warehouse and not self.via_stock_ledger: - frappe.throw(_("New Serial No cannot have Warehouse. Warehouse must be set by Stock Entry or Purchase Receipt"), SerialNoCannotCreateDirectError) + frappe.throw( + _( + "New Serial No cannot have Warehouse. Warehouse must be set by Stock Entry or Purchase Receipt" + ), + SerialNoCannotCreateDirectError, + ) self.set_maintenance_status() self.validate_warehouse() @@ -77,22 +111,21 @@ class SerialNo(StockController): def validate_warehouse(self): if not self.get("__islocal"): - item_code, warehouse = frappe.db.get_value("Serial No", - self.name, ["item_code", "warehouse"]) + item_code, warehouse = frappe.db.get_value("Serial No", self.name, ["item_code", "warehouse"]) if not self.via_stock_ledger and item_code != self.item_code: - frappe.throw(_("Item Code cannot be changed for Serial No."), - SerialNoCannotCannotChangeError) + frappe.throw(_("Item Code cannot be changed for Serial No."), SerialNoCannotCannotChangeError) if not self.via_stock_ledger and warehouse != self.warehouse: - frappe.throw(_("Warehouse cannot be changed for Serial No."), - SerialNoCannotCannotChangeError) + frappe.throw(_("Warehouse cannot be changed for Serial No."), SerialNoCannotCannotChangeError) def validate_item(self): """ - Validate whether serial no is required for this item + Validate whether serial no is required for this item """ item = frappe.get_cached_doc("Item", self.item_code) - if item.has_serial_no!=1: - frappe.throw(_("Item {0} is not setup for Serial Nos. Check Item master").format(self.item_code)) + if item.has_serial_no != 1: + frappe.throw( + _("Item {0} is not setup for Serial Nos. Check Item master").format(self.item_code) + ) self.item_group = item.item_group self.description = item.description @@ -108,17 +141,24 @@ class SerialNo(StockController): self.purchase_time = purchase_sle.posting_time self.purchase_rate = purchase_sle.incoming_rate if purchase_sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"): - self.supplier, self.supplier_name = \ - frappe.db.get_value(purchase_sle.voucher_type, purchase_sle.voucher_no, - ["supplier", "supplier_name"]) + self.supplier, self.supplier_name = frappe.db.get_value( + purchase_sle.voucher_type, purchase_sle.voucher_no, ["supplier", "supplier_name"] + ) # If sales return entry - if self.purchase_document_type == 'Delivery Note': + if self.purchase_document_type == "Delivery Note": self.sales_invoice = None else: - for fieldname in ("purchase_document_type", "purchase_document_no", - "purchase_date", "purchase_time", "purchase_rate", "supplier", "supplier_name"): - self.set(fieldname, None) + for fieldname in ( + "purchase_document_type", + "purchase_document_no", + "purchase_date", + "purchase_time", + "purchase_rate", + "supplier", + "supplier_name", + ): + self.set(fieldname, None) def set_sales_details(self, delivery_sle): if delivery_sle: @@ -126,18 +166,25 @@ class SerialNo(StockController): self.delivery_document_no = delivery_sle.voucher_no self.delivery_date = delivery_sle.posting_date self.delivery_time = delivery_sle.posting_time - if delivery_sle.voucher_type in ("Delivery Note", "Sales Invoice"): - self.customer, self.customer_name = \ - frappe.db.get_value(delivery_sle.voucher_type, delivery_sle.voucher_no, - ["customer", "customer_name"]) + if delivery_sle.voucher_type in ("Delivery Note", "Sales Invoice"): + self.customer, self.customer_name = frappe.db.get_value( + delivery_sle.voucher_type, delivery_sle.voucher_no, ["customer", "customer_name"] + ) if self.warranty_period: - self.warranty_expiry_date = add_days(cstr(delivery_sle.posting_date), - cint(self.warranty_period)) + self.warranty_expiry_date = add_days( + cstr(delivery_sle.posting_date), cint(self.warranty_period) + ) else: - for fieldname in ("delivery_document_type", "delivery_document_no", - "delivery_date", "delivery_time", "customer", "customer_name", - "warranty_expiry_date"): - self.set(fieldname, None) + for fieldname in ( + "delivery_document_type", + "delivery_document_no", + "delivery_date", + "delivery_time", + "customer", + "customer_name", + "warranty_expiry_date", + ): + self.set(fieldname, None) def get_last_sle(self, serial_no=None): entries = {} @@ -159,7 +206,8 @@ class SerialNo(StockController): if not serial_no: serial_no = self.name - for sle in frappe.db.sql(""" + for sle in frappe.db.sql( + """ SELECT voucher_type, voucher_no, posting_date, posting_time, incoming_rate, actual_qty, serial_no FROM @@ -175,25 +223,30 @@ class SerialNo(StockController): ORDER BY posting_date desc, posting_time desc, creation desc""", ( - self.item_code, self.company, + self.item_code, + self.company, serial_no, - serial_no+'\n%', - '%\n'+serial_no, - '%\n'+serial_no+'\n%' + serial_no + "\n%", + "%\n" + serial_no, + "%\n" + serial_no + "\n%", ), - as_dict=1): - if serial_no.upper() in get_serial_nos(sle.serial_no): - if cint(sle.actual_qty) > 0: - sle_dict.setdefault("incoming", []).append(sle) - else: - sle_dict.setdefault("outgoing", []).append(sle) + as_dict=1, + ): + if serial_no.upper() in get_serial_nos(sle.serial_no): + if cint(sle.actual_qty) > 0: + sle_dict.setdefault("incoming", []).append(sle) + else: + sle_dict.setdefault("outgoing", []).append(sle) return sle_dict def on_trash(self): - sl_entries = frappe.db.sql("""select serial_no from `tabStock Ledger Entry` + sl_entries = frappe.db.sql( + """select serial_no from `tabStock Ledger Entry` where serial_no like %s and item_code=%s and is_cancelled=0""", - ("%%%s%%" % self.name, self.item_code), as_dict=True) + ("%%%s%%" % self.name, self.item_code), + as_dict=True, + ) # Find the exact match sle_exists = False @@ -203,7 +256,9 @@ class SerialNo(StockController): break if sle_exists: - frappe.throw(_("Cannot delete Serial No {0}, as it is used in stock transactions").format(self.name)) + frappe.throw( + _("Cannot delete Serial No {0}, as it is used in stock transactions").format(self.name) + ) def update_serial_no_reference(self, serial_no=None): last_sle = self.get_last_sle(serial_no) @@ -212,57 +267,95 @@ class SerialNo(StockController): self.set_maintenance_status() self.set_status() + def process_serial_no(sle): item_det = get_item_details(sle.item_code) validate_serial_no(sle, item_det) update_serial_nos(sle, item_det) + def validate_serial_no(sle, item_det): serial_nos = get_serial_nos(sle.serial_no) if sle.serial_no else [] validate_material_transfer_entry(sle) - if item_det.has_serial_no==0: + if item_det.has_serial_no == 0: if serial_nos: - frappe.throw(_("Item {0} is not setup for Serial Nos. Column must be blank").format(sle.item_code), - SerialNoNotRequiredError) + frappe.throw( + _("Item {0} is not setup for Serial Nos. Column must be blank").format(sle.item_code), + SerialNoNotRequiredError, + ) 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)) + frappe.throw( + _("Serial No {0} quantity {1} cannot be a fraction").format(sle.item_code, sle.actual_qty) + ) if len(serial_nos) and len(serial_nos) != abs(cint(sle.actual_qty)): - frappe.throw(_("{0} Serial Numbers required for Item {1}. You have provided {2}.").format(abs(sle.actual_qty), sle.item_code, len(serial_nos)), - SerialNoQtyError) + frappe.throw( + _("{0} Serial Numbers required for Item {1}. You have provided {2}.").format( + abs(sle.actual_qty), sle.item_code, len(serial_nos) + ), + SerialNoQtyError, + ) if len(serial_nos) != len(set(serial_nos)): - frappe.throw(_("Duplicate Serial No entered for Item {0}").format(sle.item_code), SerialNoDuplicateError) + frappe.throw( + _("Duplicate Serial No entered for Item {0}").format(sle.item_code), SerialNoDuplicateError + ) 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", "purchase_document_type", - "purchase_document_no", "company", "status"], as_dict=1) + sr = frappe.db.get_value( + "Serial No", + serial_no, + [ + "name", + "item_code", + "batch_no", + "sales_order", + "delivery_document_no", + "delivery_document_type", + "warehouse", + "purchase_document_type", + "purchase_document_no", + "company", + "status", + ], + as_dict=1, + ) - if sr.item_code!=sle.item_code: + 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, - sle.item_code), SerialNoItemError) + 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_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) + 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): - return_against = frappe.db.get_value(sle.voucher_type, sle.voucher_no, 'return_against') + if ( + sr.delivery_document_no + and sle.voucher_type not in ["Stock Entry", "Stock Reconciliation"] + and sle.voucher_type == sr.delivery_document_type + ): + return_against = frappe.db.get_value(sle.voucher_type, sle.voucher_no, "return_against") if return_against and return_against != sr.delivery_document_no: frappe.throw(_("Serial no {0} has been already returned").format(sr.name)) if cint(sle.actual_qty) < 0: - if sr.warehouse!=sle.warehouse: - frappe.throw(_("Serial No {0} does not belong to Warehouse {1}").format(serial_no, - sle.warehouse), SerialNoWarehouseError) + if sr.warehouse != sle.warehouse: + frappe.throw( + _("Serial No {0} does not belong to Warehouse {1}").format(serial_no, sle.warehouse), + SerialNoWarehouseError, + ) if not sr.purchase_document_no: frappe.throw(_("Serial No {0} not in stock").format(serial_no), SerialNoNotExistsError) @@ -270,66 +363,100 @@ def validate_serial_no(sle, item_det): if sle.voucher_type in ("Delivery Note", "Sales Invoice"): if sr.batch_no and sr.batch_no != sle.batch_no: - frappe.throw(_("Serial No {0} does not belong to Batch {1}").format(serial_no, - sle.batch_no), SerialNoBatchError) + frappe.throw( + _("Serial No {0} does not belong to Batch {1}").format(serial_no, sle.batch_no), + SerialNoBatchError, + ) if not sle.is_cancelled and not sr.warehouse: - frappe.throw(_("Serial No {0} does not belong to any Warehouse") - .format(serial_no), SerialNoWarehouseError) + frappe.throw( + _("Serial No {0} does not belong to any Warehouse").format(serial_no), + SerialNoWarehouseError, + ) # if Sales Order reference in Serial No validate the Delivery Note or Invoice is against the same if sr.sales_order: if sle.voucher_type == "Sales Invoice": - if not frappe.db.exists("Sales Invoice Item", {"parent": sle.voucher_no, - "item_code": sle.item_code, "sales_order": sr.sales_order}): + if not frappe.db.exists( + "Sales Invoice Item", + {"parent": sle.voucher_no, "item_code": sle.item_code, "sales_order": sr.sales_order}, + ): frappe.throw( - _("Cannot deliver Serial No {0} of item {1} as it is reserved to fullfill Sales Order {2}") - .format(sr.name, sle.item_code, sr.sales_order) + _( + "Cannot deliver Serial No {0} of item {1} as it is reserved to fullfill Sales Order {2}" + ).format(sr.name, sle.item_code, sr.sales_order) ) elif sle.voucher_type == "Delivery Note": - if not frappe.db.exists("Delivery Note Item", {"parent": sle.voucher_no, - "item_code": sle.item_code, "against_sales_order": sr.sales_order}): - invoice = frappe.db.get_value("Delivery Note Item", {"parent": sle.voucher_no, - "item_code": sle.item_code}, "against_sales_invoice") - if not invoice or frappe.db.exists("Sales Invoice Item", - {"parent": invoice, "item_code": sle.item_code, - "sales_order": sr.sales_order}): + if not frappe.db.exists( + "Delivery Note Item", + { + "parent": sle.voucher_no, + "item_code": sle.item_code, + "against_sales_order": sr.sales_order, + }, + ): + invoice = frappe.db.get_value( + "Delivery Note Item", + {"parent": sle.voucher_no, "item_code": sle.item_code}, + "against_sales_invoice", + ) + if not invoice or frappe.db.exists( + "Sales Invoice Item", + {"parent": invoice, "item_code": sle.item_code, "sales_order": sr.sales_order}, + ): frappe.throw( - _("Cannot deliver Serial No {0} of item {1} as it is reserved to fullfill Sales Order {2}") - .format(sr.name, sle.item_code, sr.sales_order) + _( + "Cannot deliver Serial No {0} of item {1} as it is reserved to fullfill Sales Order {2}" + ).format(sr.name, sle.item_code, sr.sales_order) ) # if Sales Order reference in Delivery Note or Invoice validate SO reservations for item if sle.voucher_type == "Sales Invoice": - sales_order = frappe.db.get_value("Sales Invoice Item", {"parent": sle.voucher_no, - "item_code": sle.item_code}, "sales_order") + sales_order = frappe.db.get_value( + "Sales Invoice Item", + {"parent": sle.voucher_no, "item_code": sle.item_code}, + "sales_order", + ) if sales_order and get_reserved_qty_for_so(sales_order, sle.item_code): validate_so_serial_no(sr, sales_order) elif sle.voucher_type == "Delivery Note": - sales_order = frappe.get_value("Delivery Note Item", {"parent": sle.voucher_no, - "item_code": sle.item_code}, "against_sales_order") + sales_order = frappe.get_value( + "Delivery Note Item", + {"parent": sle.voucher_no, "item_code": sle.item_code}, + "against_sales_order", + ) if sales_order and get_reserved_qty_for_so(sales_order, sle.item_code): validate_so_serial_no(sr, sales_order) else: - sales_invoice = frappe.get_value("Delivery Note Item", {"parent": sle.voucher_no, - "item_code": sle.item_code}, "against_sales_invoice") + sales_invoice = frappe.get_value( + "Delivery Note Item", + {"parent": sle.voucher_no, "item_code": sle.item_code}, + "against_sales_invoice", + ) if sales_invoice: - sales_order = frappe.db.get_value("Sales Invoice Item", { - "parent": sales_invoice, "item_code": sle.item_code}, "sales_order") + sales_order = frappe.db.get_value( + "Sales Invoice Item", + {"parent": sales_invoice, "item_code": sle.item_code}, + "sales_order", + ) if sales_order and get_reserved_qty_for_so(sales_order, sle.item_code): validate_so_serial_no(sr, sales_order) elif cint(sle.actual_qty) < 0: # transfer out frappe.throw(_("Serial No {0} not in stock").format(serial_no), SerialNoNotExistsError) 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) + frappe.throw( + _("Serial Nos Required for Serialized Item {0}").format(sle.item_code), SerialNoRequiredError + ) elif serial_nos: # SLE is being cancelled and has serial nos for serial_no in serial_nos: check_serial_no_validity_on_cancel(serial_no, sle) + def check_serial_no_validity_on_cancel(serial_no, sle): - sr = frappe.db.get_value("Serial No", serial_no, ["name", "warehouse", "company", "status"], as_dict=1) + sr = frappe.db.get_value( + "Serial No", serial_no, ["name", "warehouse", "company", "status"], as_dict=1 + ) sr_link = frappe.utils.get_link_to_form("Serial No", serial_no) doc_link = frappe.utils.get_link_to_form(sle.voucher_type, sle.voucher_no) actual_qty = cint(sle.actual_qty) @@ -339,57 +466,65 @@ def check_serial_no_validity_on_cancel(serial_no, sle): if sr and (actual_qty < 0 or is_stock_reco) and (sr.warehouse and sr.warehouse != sle.warehouse): # receipt(inward) is being cancelled msg = _("Cannot cancel {0} {1} as Serial No {2} does not belong to the warehouse {3}").format( - sle.voucher_type, doc_link, sr_link, frappe.bold(sle.warehouse)) + sle.voucher_type, doc_link, sr_link, frappe.bold(sle.warehouse) + ) elif sr and actual_qty > 0 and not is_stock_reco: # delivery is being cancelled, check for warehouse. if sr.warehouse: # serial no is active in another warehouse/company. msg = _("Cannot cancel {0} {1} as Serial No {2} is active in warehouse {3}").format( - sle.voucher_type, doc_link, sr_link, frappe.bold(sr.warehouse)) + sle.voucher_type, doc_link, sr_link, frappe.bold(sr.warehouse) + ) elif sr.company != sle.company and sr.status == "Delivered": # serial no is inactive (allowed) or delivered from another company (block). msg = _("Cannot cancel {0} {1} as Serial No {2} does not belong to the company {3}").format( - sle.voucher_type, doc_link, sr_link, frappe.bold(sle.company)) + sle.voucher_type, doc_link, sr_link, frappe.bold(sle.company) + ) if msg: frappe.throw(msg, title=_("Cannot cancel")) -def validate_material_transfer_entry(sle_doc): - sle_doc.update({ - "skip_update_serial_no": False, - "skip_serial_no_validaiton": False - }) - 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"): +def validate_material_transfer_entry(sle_doc): + sle_doc.update({"skip_update_serial_no": False, "skip_serial_no_validaiton": False}) + + 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 else: sle_doc.skip_serial_no_validaiton = True -def validate_so_serial_no(sr, sales_order): - if not sr.sales_order or sr.sales_order!= sales_order: - msg = (_("Sales Order {0} has reservation for the item {1}, you can only deliver reserved {1} against {0}.") - .format(sales_order, sr.item_code)) - frappe.throw(_("""{0} Serial No {1} cannot be delivered""") - .format(msg, sr.name)) +def validate_so_serial_no(sr, sales_order): + if not sr.sales_order or sr.sales_order != sales_order: + msg = _( + "Sales Order {0} has reservation for the item {1}, you can only deliver reserved {1} against {0}." + ).format(sales_order, sr.item_code) + + frappe.throw(_("""{0} Serial No {1} cannot be delivered""").format(msg, sr.name)) + def has_serial_no_exists(sn, sle): - if (sn.warehouse and not sle.skip_serial_no_validaiton - and sle.voucher_type != 'Stock Reconciliation'): + if ( + sn.warehouse and not sle.skip_serial_no_validaiton and sle.voucher_type != "Stock Reconciliation" + ): return True if sn.company != sle.company: return False + def allow_serial_nos_with_different_item(sle_serial_no, sle): """ - Allows same serial nos for raw materials and finished goods - in Manufacture / Repack type Stock Entry + Allows same serial nos for raw materials and finished goods + in Manufacture / Repack type Stock Entry """ allow_serial_nos = False - if sle.voucher_type=="Stock Entry" and cint(sle.actual_qty) > 0: + if sle.voucher_type == "Stock Entry" and cint(sle.actual_qty) > 0: 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"): @@ -400,16 +535,24 @@ def allow_serial_nos_with_different_item(sle_serial_no, sle): return allow_serial_nos + def update_serial_nos(sle, item_det): - if sle.skip_update_serial_no: return - 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: + if sle.skip_update_serial_no: + return + 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) sle.db_set("serial_no", serial_nos) validate_serial_no(sle, item_det) if sle.serial_no: auto_make_serial_nos(sle) + def get_auto_serial_nos(serial_no_series, qty): serial_nos = [] for i in range(cint(qty)): @@ -417,22 +560,24 @@ def get_auto_serial_nos(serial_no_series, qty): return "\n".join(serial_nos) + def get_new_serial_number(series): sr_no = make_autoname(series, "Serial No") if frappe.db.exists("Serial No", sr_no): sr_no = get_new_serial_number(series) return sr_no + def auto_make_serial_nos(args): - serial_nos = get_serial_nos(args.get('serial_no')) + serial_nos = get_serial_nos(args.get("serial_no")) created_numbers = [] - voucher_type = args.get('voucher_type') - item_code = args.get('item_code') + voucher_type = args.get("voucher_type") + item_code = args.get("item_code") for serial_no in serial_nos: is_new = False if frappe.db.exists("Serial No", serial_no): sr = frappe.get_cached_doc("Serial No", serial_no) - elif args.get('actual_qty', 0) > 0: + elif args.get("actual_qty", 0) > 0: sr = frappe.new_doc("Serial No") is_new = True @@ -440,7 +585,7 @@ def auto_make_serial_nos(args): if is_new: created_numbers.append(sr.name) - form_links = list(map(lambda d: 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") @@ -452,29 +597,41 @@ def auto_make_serial_nos(args): if len(form_links) == 1: frappe.msgprint(_("Serial No {0} Created").format(form_links[0]), singular_title) elif len(form_links) > 0: - message = _("The following serial numbers were created:

    {0}").format(get_items_html(form_links, item_code)) + message = _("The following serial numbers were created:

    {0}").format( + get_items_html(form_links, item_code) + ) frappe.msgprint(message, multiple_title) + def get_items_html(serial_nos, item_code): - body = ', '.join(serial_nos) - return '''
    + body = ", ".join(serial_nos) + return """
    {0}: {1} Serial Numbers
    {2}
    - '''.format(item_code, len(serial_nos), body) + """.format( + item_code, len(serial_nos), body + ) def get_item_details(item_code): - return frappe.db.sql("""select name, has_batch_no, docstatus, + return frappe.db.sql( + """select name, has_batch_no, docstatus, is_stock_item, has_serial_no, serial_no_series - from tabItem where name=%s""", item_code, as_dict=True)[0] + from tabItem where name=%s""", + item_code, + as_dict=True, + )[0] + def get_serial_nos(serial_no): if isinstance(serial_no, list): return serial_no - return [s.strip() for s in cstr(serial_no).strip().upper().replace(',', '\n').split('\n') - if s.strip()] + return [ + s.strip() for s in cstr(serial_no).strip().upper().replace(",", "\n").split("\n") if s.strip() + ] + def clean_serial_no_string(serial_no: str) -> str: if not serial_no: @@ -483,20 +640,23 @@ def clean_serial_no_string(serial_no: str) -> str: serial_no_list = get_serial_nos(serial_no) return "\n".join(serial_no_list) + def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False): for field in ["item_code", "work_order", "company", "batch_no", "supplier", "location"]: if args.get(field): serial_no_doc.set(field, args.get(field)) serial_no_doc.via_stock_ledger = args.get("via_stock_ledger") or True - serial_no_doc.warehouse = (args.get("warehouse") - if args.get("actual_qty", 0) > 0 else None) + serial_no_doc.warehouse = args.get("warehouse") if args.get("actual_qty", 0) > 0 else None if is_new: serial_no_doc.serial_no = serial_no - if (serial_no_doc.sales_order and args.get("voucher_type") == "Stock Entry" - and not args.get("actual_qty", 0) > 0): + if ( + serial_no_doc.sales_order + and args.get("voucher_type") == "Stock Entry" + and not args.get("actual_qty", 0) > 0 + ): serial_no_doc.sales_order = None serial_no_doc.validate_item() @@ -509,19 +669,27 @@ def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False): return serial_no_doc -def update_serial_nos_after_submit(controller, parentfield): - stock_ledger_entries = frappe.db.sql("""select voucher_detail_no, serial_no, actual_qty, warehouse - from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s""", - (controller.doctype, controller.name), as_dict=True) - if not stock_ledger_entries: return +def update_serial_nos_after_submit(controller, parentfield): + stock_ledger_entries = frappe.db.sql( + """select voucher_detail_no, serial_no, actual_qty, warehouse + from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s""", + (controller.doctype, controller.name), + as_dict=True, + ) + + if not stock_ledger_entries: + return for d in controller.get(parentfield): if d.serial_no: continue - update_rejected_serial_nos = True if (controller.doctype in ("Purchase Receipt", "Purchase Invoice") - and d.rejected_qty) else False + update_rejected_serial_nos = ( + True + if (controller.doctype in ("Purchase Receipt", "Purchase Invoice") and d.rejected_qty) + else False + ) accepted_serial_nos_updated = False if controller.doctype == "Stock Entry": @@ -532,58 +700,73 @@ def update_serial_nos_after_submit(controller, parentfield): qty = d.stock_qty else: warehouse = d.warehouse - qty = (d.qty if controller.doctype == "Stock Reconciliation" - else d.stock_qty) + qty = d.qty if controller.doctype == "Stock Reconciliation" else d.stock_qty for sle in stock_ledger_entries: - if sle.voucher_detail_no==d.name: - if not accepted_serial_nos_updated and qty and abs(sle.actual_qty) == abs(qty) \ - and sle.warehouse == warehouse and sle.serial_no != d.serial_no: - d.serial_no = sle.serial_no - frappe.db.set_value(d.doctype, d.name, "serial_no", sle.serial_no) - accepted_serial_nos_updated = True - if not update_rejected_serial_nos: - break - elif update_rejected_serial_nos and abs(sle.actual_qty)==d.rejected_qty \ - and sle.warehouse == d.rejected_warehouse and sle.serial_no != d.rejected_serial_no: - d.rejected_serial_no = sle.serial_no - frappe.db.set_value(d.doctype, d.name, "rejected_serial_no", sle.serial_no) - update_rejected_serial_nos = False - if accepted_serial_nos_updated: - break + if sle.voucher_detail_no == d.name: + if ( + not accepted_serial_nos_updated + and qty + and abs(sle.actual_qty) == abs(qty) + and sle.warehouse == warehouse + and sle.serial_no != d.serial_no + ): + d.serial_no = sle.serial_no + frappe.db.set_value(d.doctype, d.name, "serial_no", sle.serial_no) + accepted_serial_nos_updated = True + if not update_rejected_serial_nos: + break + elif ( + update_rejected_serial_nos + and abs(sle.actual_qty) == d.rejected_qty + and sle.warehouse == d.rejected_warehouse + and sle.serial_no != d.rejected_serial_no + ): + d.rejected_serial_no = sle.serial_no + frappe.db.set_value(d.doctype, d.name, "rejected_serial_no", sle.serial_no) + update_rejected_serial_nos = False + if accepted_serial_nos_updated: + break + def update_maintenance_status(): - serial_nos = frappe.db.sql('''select name from `tabSerial No` where (amc_expiry_date<%s or - warranty_expiry_date<%s) and maintenance_status not in ('Out of Warranty', 'Out of AMC')''', - (nowdate(), nowdate())) + serial_nos = frappe.db.sql( + """select name from `tabSerial No` where (amc_expiry_date<%s or + warranty_expiry_date<%s) and maintenance_status not in ('Out of Warranty', 'Out of AMC')""", + (nowdate(), nowdate()), + ) for serial_no in serial_nos: doc = frappe.get_doc("Serial No", serial_no[0]) doc.set_maintenance_status() - frappe.db.set_value('Serial No', doc.name, 'maintenance_status', doc.maintenance_status) + frappe.db.set_value("Serial No", doc.name, "maintenance_status", doc.maintenance_status) + def get_delivery_note_serial_no(item_code, qty, delivery_note): - serial_nos = '' - dn_serial_nos = frappe.db.sql_list(""" select name from `tabSerial No` + serial_nos = "" + dn_serial_nos = frappe.db.sql_list( + """ select name from `tabSerial No` where item_code = %(item_code)s and delivery_document_no = %(delivery_note)s - and sales_invoice is null limit {0}""".format(cint(qty)), { - 'item_code': item_code, - 'delivery_note': delivery_note - }) + and sales_invoice is null limit {0}""".format( + cint(qty) + ), + {"item_code": item_code, "delivery_note": delivery_note}, + ) - if dn_serial_nos and len(dn_serial_nos)>0: - serial_nos = '\n'.join(dn_serial_nos) + if dn_serial_nos and len(dn_serial_nos) > 0: + serial_nos = "\n".join(dn_serial_nos) return serial_nos + @frappe.whitelist() def auto_fetch_serial_number( - qty: float, - item_code: str, - warehouse: str, - posting_date: Optional[str] = None, - batch_nos: Optional[Union[str, List[str]]] = None, - for_doctype: Optional[str] = None, - exclude_sr_nos: Optional[List[str]] = None - ) -> List[str]: + qty: float, + item_code: str, + warehouse: str, + posting_date: Optional[str] = None, + batch_nos: Optional[Union[str, List[str]]] = None, + for_doctype: Optional[str] = None, + exclude_sr_nos: Optional[List[str]] = None, +) -> List[str]: filters = frappe._dict({"item_code": item_code, "warehouse": warehouse}) @@ -604,24 +787,26 @@ def auto_fetch_serial_number( filters.expiry_date = posting_date serial_numbers = [] - if for_doctype == 'POS Invoice': + if for_doctype == "POS Invoice": exclude_sr_nos.extend(get_pos_reserved_serial_nos(filters)) serial_numbers = fetch_serial_numbers(filters, qty, do_not_include=exclude_sr_nos) - return sorted([d.get('name') for d in serial_numbers]) + return sorted([d.get("name") for d in serial_numbers]) + def get_delivered_serial_nos(serial_nos): - ''' + """ Returns serial numbers that delivered from the list of serial numbers - ''' + """ from frappe.query_builder.functions import Coalesce SerialNo = frappe.qb.DocType("Serial No") serial_nos = get_serial_nos(serial_nos) - query = frappe.qb.select(SerialNo.name).from_(SerialNo).where( - (SerialNo.name.isin(serial_nos)) - & (Coalesce(SerialNo.delivery_document_type, "") != "") + query = ( + frappe.qb.select(SerialNo.name) + .from_(SerialNo) + .where((SerialNo.name.isin(serial_nos)) & (Coalesce(SerialNo.delivery_document_type, "") != "")) ) result = query.run() @@ -629,6 +814,7 @@ def get_delivered_serial_nos(serial_nos): delivered_serial_nos = [row[0] for row in result] return delivered_serial_nos + @frappe.whitelist() def get_pos_reserved_serial_nos(filters): if isinstance(filters, str): @@ -636,21 +822,19 @@ def get_pos_reserved_serial_nos(filters): POSInvoice = frappe.qb.DocType("POS Invoice") POSInvoiceItem = frappe.qb.DocType("POS Invoice Item") - query = frappe.qb.from_( - POSInvoice - ).from_( - POSInvoiceItem - ).select( - POSInvoice.is_return, - POSInvoiceItem.serial_no - ).where( - (POSInvoice.name == POSInvoiceItem.parent) - & (POSInvoice.docstatus == 1) - & (POSInvoiceItem.docstatus == 1) - & (POSInvoiceItem.item_code == filters.get('item_code')) - & (POSInvoiceItem.warehouse == filters.get('warehouse')) - & (POSInvoiceItem.serial_no.isnotnull()) - & (POSInvoiceItem.serial_no != '') + query = ( + frappe.qb.from_(POSInvoice) + .from_(POSInvoiceItem) + .select(POSInvoice.is_return, POSInvoiceItem.serial_no) + .where( + (POSInvoice.name == POSInvoiceItem.parent) + & (POSInvoice.docstatus == 1) + & (POSInvoiceItem.docstatus == 1) + & (POSInvoiceItem.item_code == filters.get("item_code")) + & (POSInvoiceItem.warehouse == filters.get("warehouse")) + & (POSInvoiceItem.serial_no.isnotnull()) + & (POSInvoiceItem.serial_no != "") + ) ) pos_transacted_sr_nos = query.run(as_dict=True) @@ -668,6 +852,7 @@ def get_pos_reserved_serial_nos(filters): return reserved_sr_nos + def fetch_serial_numbers(filters, qty, do_not_include=None): if do_not_include is None: do_not_include = [] @@ -677,17 +862,16 @@ def fetch_serial_numbers(filters, qty, do_not_include=None): serial_no = frappe.qb.DocType("Serial No") query = ( - frappe.qb - .from_(serial_no) - .select(serial_no.name) - .where( - (serial_no.item_code == filters["item_code"]) - & (serial_no.warehouse == filters["warehouse"]) - & (Coalesce(serial_no.sales_invoice, "") == "") - & (Coalesce(serial_no.delivery_document_no, "") == "") - ) - .orderby(serial_no.creation) - .limit(qty or 1) + frappe.qb.from_(serial_no) + .select(serial_no.name) + .where( + (serial_no.item_code == filters["item_code"]) + & (serial_no.warehouse == filters["warehouse"]) + & (Coalesce(serial_no.sales_invoice, "") == "") + & (Coalesce(serial_no.delivery_document_no, "") == "") + ) + .orderby(serial_no.creation) + .limit(qty or 1) ) if do_not_include: @@ -698,8 +882,9 @@ def fetch_serial_numbers(filters, qty, do_not_include=None): if expiry_date: batch = frappe.qb.DocType("Batch") - query = (query - .left_join(batch).on(serial_no.batch_no == batch.name) + query = ( + query.left_join(batch) + .on(serial_no.batch_no == batch.name) .where(Coalesce(batch.expiry_date, "4000-12-31") >= expiry_date) ) diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py index 7df0a56b7f..68623fba11 100644 --- a/erpnext/stock/doctype/serial_no/test_serial_no.py +++ b/erpnext/stock/doctype/serial_no/test_serial_no.py @@ -18,12 +18,10 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_i from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse test_dependencies = ["Item"] -test_records = frappe.get_test_records('Serial No') - +test_records = frappe.get_test_records("Serial No") class TestSerialNo(FrappeTestCase): - def tearDown(self): frappe.db.rollback() @@ -48,7 +46,9 @@ class TestSerialNo(FrappeTestCase): se = make_serialized_item(target_warehouse="_Test Warehouse - _TC") serial_nos = get_serial_nos(se.get("items")[0].serial_no) - dn = create_delivery_note(item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0]) + dn = create_delivery_note( + item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0] + ) serial_no = frappe.get_doc("Serial No", serial_nos[0]) @@ -60,8 +60,13 @@ class TestSerialNo(FrappeTestCase): self.assertEqual(serial_no.delivery_document_no, dn.name) wh = create_warehouse("_Test Warehouse", company="_Test Company 1") - pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0], - company="_Test Company 1", warehouse=wh) + pr = make_purchase_receipt( + item_code="_Test Serialized Item With Series", + qty=1, + serial_no=serial_nos[0], + company="_Test Company 1", + warehouse=wh, + ) serial_no.reload() @@ -74,9 +79,9 @@ class TestSerialNo(FrappeTestCase): def test_inter_company_transfer_intermediate_cancellation(self): """ - Receive into and Deliver Serial No from one company. - Then Receive into and Deliver from second company. - Try to cancel intermediate receipts/deliveries to test if it is blocked. + Receive into and Deliver Serial No from one company. + Then Receive into and Deliver from second company. + Try to cancel intermediate receipts/deliveries to test if it is blocked. """ se = make_serialized_item(target_warehouse="_Test Warehouse - _TC") serial_nos = get_serial_nos(se.get("items")[0].serial_no) @@ -89,8 +94,9 @@ class TestSerialNo(FrappeTestCase): self.assertEqual(sn_doc.warehouse, "_Test Warehouse - _TC") self.assertEqual(sn_doc.purchase_document_no, se.name) - dn = create_delivery_note(item_code="_Test Serialized Item With Series", - qty=1, serial_no=serial_nos[0]) + dn = create_delivery_note( + item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0] + ) sn_doc.reload() # check Serial No details after delivery from **first** company self.assertEqual(sn_doc.status, "Delivered") @@ -104,8 +110,13 @@ class TestSerialNo(FrappeTestCase): # receive serial no in second company wh = create_warehouse("_Test Warehouse", company="_Test Company 1") - pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", - qty=1, serial_no=serial_nos[0], company="_Test Company 1", warehouse=wh) + pr = make_purchase_receipt( + item_code="_Test Serialized Item With Series", + qty=1, + serial_no=serial_nos[0], + company="_Test Company 1", + warehouse=wh, + ) sn_doc.reload() self.assertEqual(sn_doc.warehouse, wh) @@ -114,8 +125,13 @@ class TestSerialNo(FrappeTestCase): self.assertRaises(frappe.ValidationError, dn.cancel) # deliver from second company - dn_2 = create_delivery_note(item_code="_Test Serialized Item With Series", - qty=1, serial_no=serial_nos[0], company="_Test Company 1", warehouse=wh) + dn_2 = create_delivery_note( + item_code="_Test Serialized Item With Series", + qty=1, + serial_no=serial_nos[0], + company="_Test Company 1", + warehouse=wh, + ) sn_doc.reload() # check Serial No details after delivery from **second** company @@ -131,9 +147,9 @@ class TestSerialNo(FrappeTestCase): def test_inter_company_transfer_fallback_on_cancel(self): """ - Test Serial No state changes on cancellation. - If Delivery cancelled, it should fall back on last Receipt in the same company. - If Receipt is cancelled, it should be Inactive in the same company. + Test Serial No state changes on cancellation. + If Delivery cancelled, it should fall back on last Receipt in the same company. + If Receipt is cancelled, it should be Inactive in the same company. """ # Receipt in **first** company se = make_serialized_item(target_warehouse="_Test Warehouse - _TC") @@ -141,17 +157,28 @@ class TestSerialNo(FrappeTestCase): sn_doc = frappe.get_doc("Serial No", serial_nos[0]) # Delivery from first company - dn = create_delivery_note(item_code="_Test Serialized Item With Series", - qty=1, serial_no=serial_nos[0]) + dn = create_delivery_note( + item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0] + ) # Receipt in **second** company wh = create_warehouse("_Test Warehouse", company="_Test Company 1") - pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", - qty=1, serial_no=serial_nos[0], company="_Test Company 1", warehouse=wh) + pr = make_purchase_receipt( + item_code="_Test Serialized Item With Series", + qty=1, + serial_no=serial_nos[0], + company="_Test Company 1", + warehouse=wh, + ) # Delivery from second company - dn_2 = create_delivery_note(item_code="_Test Serialized Item With Series", - qty=1, serial_no=serial_nos[0], company="_Test Company 1", warehouse=wh) + dn_2 = create_delivery_note( + item_code="_Test Serialized Item With Series", + qty=1, + serial_no=serial_nos[0], + company="_Test Company 1", + warehouse=wh, + ) sn_doc.reload() self.assertEqual(sn_doc.status, "Delivered") @@ -184,12 +211,11 @@ class TestSerialNo(FrappeTestCase): def test_auto_creation_of_serial_no(self): """ - Test if auto created Serial No excludes existing serial numbers + Test if auto created Serial No excludes existing serial numbers """ - item_code = make_item("_Test Auto Serial Item ", { - "has_serial_no": 1, - "serial_no_series": "XYZ.###" - }).item_code + item_code = make_item( + "_Test Auto Serial Item ", {"has_serial_no": 1, "serial_no_series": "XYZ.###"} + ).item_code # Reserve XYZ005 pr_1 = make_purchase_receipt(item_code=item_code, qty=1, serial_no="XYZ005") @@ -203,7 +229,7 @@ class TestSerialNo(FrappeTestCase): def test_serial_no_sanitation(self): "Test if Serial No input is sanitised before entering the DB." item_code = "_Test Serialized Item" - test_records = frappe.get_test_records('Stock Entry') + test_records = frappe.get_test_records("Stock Entry") se = frappe.copy_doc(test_records[0]) se.get("items")[0].item_code = item_code @@ -217,37 +243,43 @@ class TestSerialNo(FrappeTestCase): self.assertEqual(se.get("items")[0].serial_no, "_TS1\n_TS2\n_TS3\n_TS4 - 2021") def test_correct_serial_no_incoming_rate(self): - """ Check correct consumption rate based on serial no record. - """ + """Check correct consumption rate based on serial no record.""" item_code = "_Test Serialized Item" warehouse = "_Test Warehouse - _TC" serial_nos = ["LOWVALUATION", "HIGHVALUATION"] - in1 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=1, rate=42, - serial_no=serial_nos[0]) - in2 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=1, rate=113, - serial_no=serial_nos[1]) + in1 = make_stock_entry( + item_code=item_code, to_warehouse=warehouse, qty=1, rate=42, serial_no=serial_nos[0] + ) + in2 = make_stock_entry( + item_code=item_code, to_warehouse=warehouse, qty=1, rate=113, serial_no=serial_nos[1] + ) - out = create_delivery_note(item_code=item_code, qty=1, serial_no=serial_nos[0], do_not_submit=True) + out = create_delivery_note( + item_code=item_code, qty=1, serial_no=serial_nos[0], do_not_submit=True + ) # change serial no out.items[0].serial_no = serial_nos[1] out.save() out.submit() - value_diff = frappe.db.get_value("Stock Ledger Entry", - {"voucher_no": out.name, "voucher_type": "Delivery Note"}, - "stock_value_difference" - ) + value_diff = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": out.name, "voucher_type": "Delivery Note"}, + "stock_value_difference", + ) self.assertEqual(value_diff, -113) def test_auto_fetch(self): - item_code = make_item(properties={ - "has_serial_no": 1, - "has_batch_no": 1, - "create_new_batch": 1, - "serial_no_series": "TEST.#######" - }).name + item_code = make_item( + properties={ + "has_serial_no": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "serial_no_series": "TEST.#######", + } + ).name warehouse = "_Test Warehouse - _TC" in1 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=5) @@ -260,8 +292,8 @@ class TestSerialNo(FrappeTestCase): batch2 = in2.items[0].batch_no batch_wise_serials = { - batch1 : get_serial_nos(in1.items[0].serial_no), - batch2: get_serial_nos(in2.items[0].serial_no) + batch1: get_serial_nos(in1.items[0].serial_no), + batch2: get_serial_nos(in2.items[0].serial_no), } # Test FIFO @@ -270,12 +302,15 @@ class TestSerialNo(FrappeTestCase): # partial FIFO partial_fetch = auto_fetch_serial_number(2, item_code, warehouse) - self.assertTrue(set(partial_fetch).issubset(set(first_fetch)), - msg=f"{partial_fetch} should be subset of {first_fetch}") + self.assertTrue( + set(partial_fetch).issubset(set(first_fetch)), + msg=f"{partial_fetch} should be subset of {first_fetch}", + ) # exclusion - remaining = auto_fetch_serial_number(3, item_code, warehouse, - exclude_sr_nos=json.dumps(partial_fetch)) + remaining = auto_fetch_serial_number( + 3, item_code, warehouse, exclude_sr_nos=json.dumps(partial_fetch) + ) self.assertEqual(sorted(remaining + partial_fetch), first_fetch) # batchwise @@ -288,10 +323,14 @@ class TestSerialNo(FrappeTestCase): # multi batch all_serials = [sr for sr_list in batch_wise_serials.values() for sr in sr_list] - fetched_serials = auto_fetch_serial_number(10, item_code, warehouse, batch_nos=list(batch_wise_serials.keys())) + fetched_serials = auto_fetch_serial_number( + 10, item_code, warehouse, batch_nos=list(batch_wise_serials.keys()) + ) self.assertEqual(sorted(all_serials), fetched_serials) # expiry date frappe.db.set_value("Batch", batch1, "expiry_date", "1980-01-01") - non_expired_serials = auto_fetch_serial_number(5, item_code, warehouse, posting_date="2021-01-01", batch_nos=batch1) + non_expired_serials = auto_fetch_serial_number( + 5, item_code, warehouse, posting_date="2021-01-01", batch_nos=batch1 + ) self.assertEqual(non_expired_serials, []) diff --git a/erpnext/stock/doctype/shipment/shipment.py b/erpnext/stock/doctype/shipment/shipment.py index 666de57f34..42a67f42be 100644 --- a/erpnext/stock/doctype/shipment/shipment.py +++ b/erpnext/stock/doctype/shipment/shipment.py @@ -17,22 +17,22 @@ class Shipment(Document): self.validate_pickup_time() self.set_value_of_goods() if self.docstatus == 0: - self.status = 'Draft' + self.status = "Draft" def on_submit(self): if not self.shipment_parcel: - frappe.throw(_('Please enter Shipment Parcel information')) + frappe.throw(_("Please enter Shipment Parcel information")) if self.value_of_goods == 0: - frappe.throw(_('Value of goods cannot be 0')) - self.db_set('status', 'Submitted') + frappe.throw(_("Value of goods cannot be 0")) + self.db_set("status", "Submitted") def on_cancel(self): - self.db_set('status', 'Cancelled') + self.db_set("status", "Cancelled") def validate_weight(self): for parcel in self.shipment_parcel: if flt(parcel.weight) <= 0: - frappe.throw(_('Parcel weight cannot be 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): @@ -44,26 +44,34 @@ class Shipment(Document): value_of_goods += flt(entry.get("grand_total")) self.value_of_goods = value_of_goods if value_of_goods else self.value_of_goods + @frappe.whitelist() def get_address_name(ref_doctype, docname): # Return address name return get_party_shipping_address(ref_doctype, docname) + @frappe.whitelist() def get_contact_name(ref_doctype, docname): # Return address name return get_default_contact(ref_doctype, docname) + @frappe.whitelist() def get_company_contact(user): - contact = frappe.db.get_value('User', user, [ - 'first_name', - 'last_name', - 'email', - 'phone', - 'mobile_no', - 'gender', - ], as_dict=1) + contact = frappe.db.get_value( + "User", + user, + [ + "first_name", + "last_name", + "email", + "phone", + "mobile_no", + "gender", + ], + as_dict=1, + ) if not contact.phone: contact.phone = contact.mobile_no return contact diff --git a/erpnext/stock/doctype/shipment/test_shipment.py b/erpnext/stock/doctype/shipment/test_shipment.py index 317abb6d03..ae97e7af36 100644 --- a/erpnext/stock/doctype/shipment/test_shipment.py +++ b/erpnext/stock/doctype/shipment/test_shipment.py @@ -13,13 +13,14 @@ class TestShipment(FrappeTestCase): def test_shipment_from_delivery_note(self): delivery_note = create_test_delivery_note() delivery_note.submit() - shipment = create_test_shipment([ delivery_note ]) + shipment = create_test_shipment([delivery_note]) shipment.submit() second_shipment = make_shipment(delivery_note.name) self.assertEqual(second_shipment.value_of_goods, delivery_note.grand_total) self.assertEqual(len(second_shipment.shipment_delivery_note), 1) self.assertEqual(second_shipment.shipment_delivery_note[0].delivery_note, delivery_note.name) + def create_test_delivery_note(): company = get_shipment_company() customer = get_shipment_customer() @@ -30,25 +31,26 @@ def create_test_delivery_note(): delivery_note = frappe.new_doc("Delivery Note") delivery_note.company = company.name delivery_note.posting_date = posting_date.strftime("%Y-%m-%d") - delivery_note.posting_time = '10:00' + delivery_note.posting_time = "10:00" delivery_note.customer = customer.name - delivery_note.append('items', + delivery_note.append( + "items", { "item_code": item.name, "item_name": item.item_name, - "description": 'Test delivery note for shipment', + "description": "Test delivery note for shipment", "qty": 5, - "uom": 'Nos', - "warehouse": 'Stores - _TC', + "uom": "Nos", + "warehouse": "Stores - _TC", "rate": item.standard_rate, - "cost_center": 'Main - _TC' - } + "cost_center": "Main - _TC", + }, ) delivery_note.insert() return delivery_note -def create_test_shipment(delivery_notes = None): +def create_test_shipment(delivery_notes=None): company = get_shipment_company() company_address = get_shipment_company_address(company.name) customer = get_shipment_customer() @@ -57,45 +59,35 @@ def create_test_shipment(delivery_notes = None): posting_date = date.today() + timedelta(days=5) shipment = frappe.new_doc("Shipment") - shipment.pickup_from_type = 'Company' + shipment.pickup_from_type = "Company" shipment.pickup_company = company.name shipment.pickup_address_name = company_address.name - shipment.delivery_to_type = 'Customer' + shipment.delivery_to_type = "Customer" shipment.delivery_customer = customer.name shipment.delivery_address_name = customer_address.name shipment.delivery_contact_name = customer_contact.name - shipment.pallets = 'No' - shipment.shipment_type = 'Goods' + shipment.pallets = "No" + shipment.shipment_type = "Goods" shipment.value_of_goods = 1000 - shipment.pickup_type = 'Pickup' + shipment.pickup_type = "Pickup" shipment.pickup_date = posting_date.strftime("%Y-%m-%d") - shipment.pickup_from = '09:00' - shipment.pickup_to = '17:00' - shipment.description_of_content = 'unit test entry' + shipment.pickup_from = "09:00" + shipment.pickup_to = "17:00" + shipment.description_of_content = "unit test entry" for delivery_note in delivery_notes: - shipment.append('shipment_delivery_note', - { - "delivery_note": delivery_note.name - } - ) - shipment.append('shipment_parcel', - { - "length": 5, - "width": 5, - "height": 5, - "weight": 5, - "count": 5 - } + shipment.append("shipment_delivery_note", {"delivery_note": delivery_note.name}) + shipment.append( + "shipment_parcel", {"length": 5, "width": 5, "height": 5, "weight": 5, "count": 5} ) shipment.insert() return shipment def get_shipment_customer_contact(customer_name): - contact_fname = 'Customer Shipment' - contact_lname = 'Testing' - customer_name = contact_fname + ' ' + contact_lname - contacts = frappe.get_all("Contact", fields=["name"], filters = {"name": customer_name}) + contact_fname = "Customer Shipment" + contact_lname = "Testing" + customer_name = contact_fname + " " + contact_lname + contacts = frappe.get_all("Contact", fields=["name"], filters={"name": customer_name}) if len(contacts): return contacts[0] else: @@ -103,104 +95,106 @@ def get_shipment_customer_contact(customer_name): def get_shipment_customer_address(customer_name): - address_title = customer_name + ' address 123' - customer_address = frappe.get_all("Address", fields=["name"], filters = {"address_title": address_title}) + address_title = customer_name + " address 123" + customer_address = frappe.get_all( + "Address", fields=["name"], filters={"address_title": address_title} + ) if len(customer_address): return customer_address[0] else: return create_shipment_address(address_title, customer_name, 81929) + def get_shipment_customer(): - customer_name = 'Shipment Customer' - customer = frappe.get_all("Customer", fields=["name"], filters = {"name": customer_name}) + customer_name = "Shipment Customer" + customer = frappe.get_all("Customer", fields=["name"], filters={"name": customer_name}) if len(customer): return customer[0] else: return create_shipment_customer(customer_name) + def get_shipment_company_address(company_name): - address_title = company_name + ' address 123' - addresses = frappe.get_all("Address", fields=["name"], filters = {"address_title": address_title}) + address_title = company_name + " address 123" + addresses = frappe.get_all("Address", fields=["name"], filters={"address_title": address_title}) if len(addresses): return addresses[0] else: return create_shipment_address(address_title, company_name, 80331) + def get_shipment_company(): return frappe.get_doc("Company", "_Test Company") + def get_shipment_item(company_name): - item_name = 'Testing Shipment item' - items = frappe.get_all("Item", + item_name = "Testing Shipment item" + items = frappe.get_all( + "Item", fields=["name", "item_name", "item_code", "standard_rate"], - filters = {"item_name": item_name} + filters={"item_name": item_name}, ) if len(items): return items[0] else: return create_shipment_item(item_name, company_name) + def create_shipment_address(address_title, company_name, postal_code): address = frappe.new_doc("Address") address.address_title = address_title - address.address_type = 'Shipping' - address.address_line1 = company_name + ' address line 1' - address.city = 'Random City' + address.address_type = "Shipping" + address.address_line1 = company_name + " address line 1" + address.city = "Random City" address.postal_code = postal_code - address.country = 'Germany' + address.country = "Germany" address.insert() return address def create_customer_contact(fname, lname): customer = frappe.new_doc("Contact") - customer.customer_name = fname + ' ' + lname + customer.customer_name = fname + " " + lname customer.first_name = fname customer.last_name = lname customer.is_primary_contact = 1 customer.is_billing_contact = 1 - customer.append('email_ids', - { - 'email_id': 'randomme@email.com', - 'is_primary': 1 - } + customer.append("email_ids", {"email_id": "randomme@email.com", "is_primary": 1}) + customer.append( + "phone_nos", {"phone": "123123123", "is_primary_phone": 1, "is_primary_mobile_no": 1} ) - customer.append('phone_nos', - { - 'phone': '123123123', - 'is_primary_phone': 1, - 'is_primary_mobile_no': 1 - } - ) - customer.status = 'Passive' + customer.status = "Passive" customer.insert() return customer + def create_shipment_customer(customer_name): customer = frappe.new_doc("Customer") customer.customer_name = customer_name - customer.customer_type = 'Company' - customer.customer_group = 'All Customer Groups' - customer.territory = 'All Territories' - customer.gst_category = 'Unregistered' + customer.customer_type = "Company" + customer.customer_group = "All Customer Groups" + customer.territory = "All Territories" + customer.gst_category = "Unregistered" customer.insert() return customer + def create_material_receipt(item, company): posting_date = date.today() stock = frappe.new_doc("Stock Entry") stock.company = company - stock.stock_entry_type = 'Material Receipt' + stock.stock_entry_type = "Material Receipt" stock.posting_date = posting_date.strftime("%Y-%m-%d") - stock.append('items', + stock.append( + "items", { - "t_warehouse": 'Stores - _TC', + "t_warehouse": "Stores - _TC", "item_code": item.name, "qty": 5, - "uom": 'Nos', + "uom": "Nos", "basic_rate": item.standard_rate, - "cost_center": 'Main - _TC' - } + "cost_center": "Main - _TC", + }, ) stock.insert() stock.submit() @@ -210,14 +204,9 @@ def create_shipment_item(item_name, company_name): item = frappe.new_doc("Item") item.item_name = item_name item.item_code = item_name - item.item_group = 'All Item Groups' - item.stock_uom = 'Nos' + item.item_group = "All Item Groups" + item.stock_uom = "Nos" item.standard_rate = 50 - item.append('item_defaults', - { - "company": company_name, - "default_warehouse": 'Stores - _TC' - } - ) + item.append("item_defaults", {"company": company_name, "default_warehouse": "Stores - _TC"}) item.insert() return item diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 99cf4de5de..bc54f7f84e 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -38,20 +38,28 @@ from erpnext.stock.utils import get_bin, get_incoming_rate class FinishedGoodError(frappe.ValidationError): pass + + class IncorrectValuationRateError(frappe.ValidationError): pass + + class DuplicateEntryForWorkOrderError(frappe.ValidationError): pass + + class OperationsNotCompleteError(frappe.ValidationError): pass + + class MaxSampleAlreadyRetainedError(frappe.ValidationError): pass + from erpnext.controllers.stock_controller import StockController -form_grid_templates = { - "items": "templates/form_grid/stock_entry_grid.html" -} +form_grid_templates = {"items": "templates/form_grid/stock_entry_grid.html"} + class StockEntry(StockController): def get_feed(self): @@ -63,16 +71,18 @@ class StockEntry(StockController): def before_validate(self): from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule - apply_rule = self.apply_putaway_rule and (self.purpose in ["Material Transfer", "Material Receipt"]) + + apply_rule = self.apply_putaway_rule and ( + self.purpose in ["Material Transfer", "Material Receipt"] + ) if self.get("items") and apply_rule: - apply_putaway_rule(self.doctype, self.get("items"), self.company, - purpose=self.purpose) + apply_putaway_rule(self.doctype, self.get("items"), self.company, purpose=self.purpose) def validate(self): self.pro_doc = frappe._dict() if self.work_order: - self.pro_doc = frappe.get_doc('Work Order', self.work_order) + self.pro_doc = frappe.get_doc("Work Order", self.work_order) self.validate_posting_time() self.validate_purpose() @@ -103,10 +113,10 @@ class StockEntry(StockController): if not self.from_bom: self.fg_completed_qty = 0.0 - if self._action == 'submit': - self.make_batches('t_warehouse') + if self._action == "submit": + self.make_batches("t_warehouse") else: - set_batch_nos(self, 's_warehouse') + set_batch_nos(self, "s_warehouse") self.validate_serialized_batch() self.set_actual_qty() @@ -138,10 +148,10 @@ class StockEntry(StockController): if self.work_order and self.purpose == "Manufacture": self.update_so_in_serial_number() - if self.purpose == 'Material Transfer' and self.add_to_transit: - self.set_material_request_transfer_status('In Transit') - if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: - self.set_material_request_transfer_status('Completed') + if self.purpose == "Material Transfer" and self.add_to_transit: + self.set_material_request_transfer_status("In Transit") + if self.purpose == "Material Transfer" and self.outgoing_stock_entry: + self.set_material_request_transfer_status("Completed") def on_cancel(self): self.update_purchase_order_supplied_items() @@ -152,7 +162,7 @@ class StockEntry(StockController): self.update_work_order() self.update_stock_ledger() - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") self.make_gl_entries_on_cancel() self.repost_future_sle_and_gle() @@ -162,15 +172,16 @@ class StockEntry(StockController): self.delete_auto_created_batches() self.delete_linked_stock_entry() - if self.purpose == 'Material Transfer' and self.add_to_transit: - self.set_material_request_transfer_status('Not Started') - if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: - self.set_material_request_transfer_status('In Transit') + if self.purpose == "Material Transfer" and self.add_to_transit: + self.set_material_request_transfer_status("Not Started") + if self.purpose == "Material Transfer" and self.outgoing_stock_entry: + self.set_material_request_transfer_status("In Transit") def set_job_card_data(self): if self.job_card and not self.work_order: - data = frappe.db.get_value('Job Card', - self.job_card, ['for_quantity', 'work_order', 'bom_no'], as_dict=1) + data = frappe.db.get_value( + "Job Card", self.job_card, ["for_quantity", "work_order", "bom_no"], as_dict=1 + ) self.fg_completed_qty = data.for_quantity self.work_order = data.work_order self.from_bom = 1 @@ -178,25 +189,37 @@ class StockEntry(StockController): def validate_work_order_status(self): pro_doc = frappe.get_doc("Work Order", self.work_order) - if pro_doc.status == 'Completed': + if pro_doc.status == "Completed": frappe.throw(_("Cannot cancel transaction for Completed Work Order.")) def validate_purpose(self): - valid_purposes = ["Material Issue", "Material Receipt", "Material Transfer", - "Material Transfer for Manufacture", "Manufacture", "Repack", "Send to Subcontractor", - "Material Consumption for Manufacture"] + valid_purposes = [ + "Material Issue", + "Material Receipt", + "Material Transfer", + "Material Transfer for Manufacture", + "Manufacture", + "Repack", + "Send to Subcontractor", + "Material Consumption for Manufacture", + ] if self.purpose not in valid_purposes: frappe.throw(_("Purpose must be one of {0}").format(comma_or(valid_purposes))) - if self.job_card and self.purpose not in ['Material Transfer for Manufacture', 'Repack']: - frappe.throw(_("For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry") - .format(self.job_card)) + if self.job_card and self.purpose not in ["Material Transfer for Manufacture", "Repack"]: + frappe.throw( + _( + "For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry" + ).format(self.job_card) + ) def delete_linked_stock_entry(self): if self.purpose == "Send to Warehouse": - for d in frappe.get_all("Stock Entry", filters={"docstatus": 0, - "outgoing_stock_entry": self.name, "purpose": "Receive at Warehouse"}): + for d in frappe.get_all( + "Stock Entry", + filters={"docstatus": 0, "outgoing_stock_entry": self.name, "purpose": "Receive at Warehouse"}, + ): frappe.delete_doc("Stock Entry", d.name) def set_transfer_qty(self): @@ -205,80 +228,117 @@ class StockEntry(StockController): frappe.throw(_("Row {0}: Qty is mandatory").format(item.idx)) if not flt(item.conversion_factor): frappe.throw(_("Row {0}: UOM Conversion Factor is mandatory").format(item.idx)) - item.transfer_qty = flt(flt(item.qty) * flt(item.conversion_factor), - self.precision("transfer_qty", item)) + item.transfer_qty = flt( + flt(item.qty) * flt(item.conversion_factor), self.precision("transfer_qty", item) + ) def update_cost_in_project(self): - if (self.work_order and not frappe.db.get_value("Work Order", - self.work_order, "update_consumed_material_cost_in_project")): + if self.work_order and not frappe.db.get_value( + "Work Order", self.work_order, "update_consumed_material_cost_in_project" + ): return if self.project: - amount = frappe.db.sql(""" select ifnull(sum(sed.amount), 0) + amount = frappe.db.sql( + """ select ifnull(sum(sed.amount), 0) from `tabStock Entry` se, `tabStock Entry Detail` sed where se.docstatus = 1 and se.project = %s and sed.parent = se.name - and (sed.t_warehouse is null or sed.t_warehouse = '')""", self.project, as_list=1) + and (sed.t_warehouse is null or sed.t_warehouse = '')""", + self.project, + as_list=1, + ) amount = amount[0][0] if amount else 0 - additional_costs = frappe.db.sql(""" select ifnull(sum(sed.base_amount), 0) + additional_costs = frappe.db.sql( + """ select ifnull(sum(sed.base_amount), 0) from `tabStock Entry` se, `tabLanded Cost Taxes and Charges` sed where se.docstatus = 1 and se.project = %s and sed.parent = se.name - and se.purpose = 'Manufacture'""", self.project, as_list=1) + and se.purpose = 'Manufacture'""", + self.project, + as_list=1, + ) additional_cost_amt = additional_costs[0][0] if additional_costs else 0 amount += additional_cost_amt - frappe.db.set_value('Project', self.project, 'total_consumed_material_cost', amount) + frappe.db.set_value("Project", self.project, "total_consumed_material_cost", amount) def validate_item(self): stock_items = self.get_stock_items() serialized_items = self.get_serialized_items() for item in self.get("items"): if flt(item.qty) and flt(item.qty) < 0: - frappe.throw(_("Row {0}: The item {1}, quantity must be positive number") - .format(item.idx, frappe.bold(item.item_code))) + frappe.throw( + _("Row {0}: The item {1}, quantity must be positive number").format( + item.idx, frappe.bold(item.item_code) + ) + ) if item.item_code not in stock_items: frappe.throw(_("{0} is not a stock Item").format(item.item_code)) - item_details = self.get_item_details(frappe._dict( - {"item_code": item.item_code, "company": self.company, - "project": self.project, "uom": item.uom, 's_warehouse': item.s_warehouse}), - for_update=True) + item_details = self.get_item_details( + frappe._dict( + { + "item_code": item.item_code, + "company": self.company, + "project": self.project, + "uom": item.uom, + "s_warehouse": item.s_warehouse, + } + ), + for_update=True, + ) - for f in ("uom", "stock_uom", "description", "item_name", "expense_account", - "cost_center", "conversion_factor"): - if f == "stock_uom" or not item.get(f): - item.set(f, item_details.get(f)) - if f == 'conversion_factor' and item.uom == item_details.get('stock_uom'): - item.set(f, item_details.get(f)) + for f in ( + "uom", + "stock_uom", + "description", + "item_name", + "expense_account", + "cost_center", + "conversion_factor", + ): + if f == "stock_uom" or not item.get(f): + item.set(f, item_details.get(f)) + if f == "conversion_factor" and item.uom == item_details.get("stock_uom"): + item.set(f, item_details.get(f)) if not item.transfer_qty and item.qty: - item.transfer_qty = flt(flt(item.qty) * flt(item.conversion_factor), - self.precision("transfer_qty", item)) + item.transfer_qty = flt( + flt(item.qty) * flt(item.conversion_factor), self.precision("transfer_qty", item) + ) - if (self.purpose in ("Material Transfer", "Material Transfer for Manufacture") + if ( + self.purpose in ("Material Transfer", "Material Transfer for Manufacture") and not item.serial_no - and item.item_code in serialized_items): - frappe.throw(_("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code), - frappe.MandatoryError) + and item.item_code in serialized_items + ): + frappe.throw( + _("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code), + frappe.MandatoryError, + ) def validate_qty(self): manufacture_purpose = ["Manufacture", "Material Consumption for Manufacture"] if self.purpose in manufacture_purpose and self.work_order: - if not frappe.get_value('Work Order', self.work_order, 'skip_transfer'): + if not frappe.get_value("Work Order", self.work_order, "skip_transfer"): item_code = [] for item in self.items: - if cstr(item.t_warehouse) == '': - req_items = frappe.get_all('Work Order Item', - filters={'parent': self.work_order, 'item_code': item.item_code}, fields=["item_code"]) + if cstr(item.t_warehouse) == "": + req_items = frappe.get_all( + "Work Order Item", + filters={"parent": self.work_order, "item_code": item.item_code}, + fields=["item_code"], + ) - transferred_materials = frappe.db.sql(""" + transferred_materials = frappe.db.sql( + """ select sum(qty) as qty from `tabStock Entry` se,`tabStock Entry Detail` sed @@ -286,7 +346,10 @@ class StockEntry(StockController): se.name = sed.parent and se.docstatus=1 and (se.purpose='Material Transfer for Manufacture' or se.purpose='Manufacture') and sed.item_code=%s and se.work_order= %s and ifnull(sed.t_warehouse, '') != '' - """, (item.item_code, self.work_order), as_dict=1) + """, + (item.item_code, self.work_order), + as_dict=1, + ) stock_qty = flt(item.qty) trans_qty = flt(transferred_materials[0].qty) @@ -304,8 +367,11 @@ class StockEntry(StockController): for item_code, qty_list in item_wise_qty.items(): total = flt(sum(qty_list), frappe.get_precision("Stock Entry Detail", "qty")) if self.fg_completed_qty != total: - frappe.throw(_("The finished product {0} quantity {1} and For Quantity {2} cannot be different") - .format(frappe.bold(item_code), frappe.bold(total), frappe.bold(self.fg_completed_qty))) + frappe.throw( + _("The finished product {0} quantity {1} and For Quantity {2} cannot be different").format( + frappe.bold(item_code), frappe.bold(total), frappe.bold(self.fg_completed_qty) + ) + ) def validate_difference_account(self): if not cint(erpnext.is_perpetual_inventory_enabled(self.company)): @@ -313,33 +379,53 @@ class StockEntry(StockController): for d in self.get("items"): if not d.expense_account: - frappe.throw(_("Please enter Difference Account or set default Stock Adjustment Account for company {0}") - .format(frappe.bold(self.company))) + frappe.throw( + _( + "Please enter Difference Account or set default Stock Adjustment Account for company {0}" + ).format(frappe.bold(self.company)) + ) - elif self.is_opening == "Yes" and frappe.db.get_value("Account", d.expense_account, "report_type") == "Profit and Loss": - frappe.throw(_("Difference Account must be a Asset/Liability type account, since this Stock Entry is an Opening Entry"), OpeningEntryAccountError) + elif ( + self.is_opening == "Yes" + and frappe.db.get_value("Account", d.expense_account, "report_type") == "Profit and Loss" + ): + frappe.throw( + _( + "Difference Account must be a Asset/Liability type account, since this Stock Entry is an Opening Entry" + ), + OpeningEntryAccountError, + ) def validate_warehouse(self): """perform various (sometimes conditional) validations on warehouse""" - source_mandatory = ["Material Issue", "Material Transfer", "Send to Subcontractor", "Material Transfer for Manufacture", - "Material Consumption for Manufacture"] + source_mandatory = [ + "Material Issue", + "Material Transfer", + "Send to Subcontractor", + "Material Transfer for Manufacture", + "Material Consumption for Manufacture", + ] - target_mandatory = ["Material Receipt", "Material Transfer", "Send to Subcontractor", - "Material Transfer for Manufacture"] + target_mandatory = [ + "Material Receipt", + "Material Transfer", + "Send to Subcontractor", + "Material Transfer for Manufacture", + ] validate_for_manufacture = any([d.bom_no for d in self.get("items")]) if self.purpose in source_mandatory and self.purpose not in target_mandatory: self.to_warehouse = None - for d in self.get('items'): + for d in self.get("items"): d.t_warehouse = None elif self.purpose in target_mandatory and self.purpose not in source_mandatory: self.from_warehouse = None - for d in self.get('items'): + for d in self.get("items"): d.s_warehouse = None - for d in self.get('items'): + for d in self.get("items"): if not d.s_warehouse and not d.t_warehouse: d.s_warehouse = self.from_warehouse d.t_warehouse = self.to_warehouse @@ -356,7 +442,6 @@ class StockEntry(StockController): else: frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx)) - if self.purpose == "Manufacture": if validate_for_manufacture: if d.is_finished_item or d.is_scrap_item or d.is_process_loss: @@ -368,18 +453,26 @@ class StockEntry(StockController): if not d.s_warehouse: frappe.throw(_("Source warehouse is mandatory for row {0}").format(d.idx)) - if cstr(d.s_warehouse) == cstr(d.t_warehouse) and not self.purpose == "Material Transfer for Manufacture": + if ( + cstr(d.s_warehouse) == cstr(d.t_warehouse) + and not self.purpose == "Material Transfer for Manufacture" + ): frappe.throw(_("Source and target warehouse cannot be same for row {0}").format(d.idx)) if not (d.s_warehouse or d.t_warehouse): frappe.throw(_("Atleast one warehouse is mandatory")) def validate_work_order(self): - if self.purpose in ("Manufacture", "Material Transfer for Manufacture", "Material Consumption for Manufacture"): + if self.purpose in ( + "Manufacture", + "Material Transfer for Manufacture", + "Material Consumption for Manufacture", + ): # check if work order is entered - if (self.purpose=="Manufacture" or self.purpose=="Material Consumption for Manufacture") \ - and self.work_order: + if ( + self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture" + ) and self.work_order: if not self.fg_completed_qty: frappe.throw(_("For Quantity (Manufactured Qty) is mandatory")) self.check_if_operations_completed() @@ -390,40 +483,66 @@ class StockEntry(StockController): def check_if_operations_completed(self): """Check if Time Sheets are completed against before manufacturing to capture operating costs.""" prod_order = frappe.get_doc("Work Order", self.work_order) - allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", - "overproduction_percentage_for_work_order")) + allowance_percentage = flt( + frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order") + ) for d in prod_order.get("operations"): total_completed_qty = flt(self.fg_completed_qty) + flt(prod_order.produced_qty) - completed_qty = d.completed_qty + (allowance_percentage/100 * d.completed_qty) + completed_qty = d.completed_qty + (allowance_percentage / 100 * d.completed_qty) if total_completed_qty > flt(completed_qty): - job_card = frappe.db.get_value('Job Card', {'operation_id': d.name}, 'name') + job_card = frappe.db.get_value("Job Card", {"operation_id": d.name}, "name") if not job_card: - frappe.throw(_("Work Order {0}: Job Card not found for the operation {1}") - .format(self.work_order, d.operation)) + frappe.throw( + _("Work Order {0}: Job Card not found for the operation {1}").format( + self.work_order, d.operation + ) + ) - work_order_link = frappe.utils.get_link_to_form('Work Order', self.work_order) - job_card_link = frappe.utils.get_link_to_form('Job Card', job_card) - frappe.throw(_("Row #{0}: Operation {1} is not completed for {2} qty of finished goods in Work Order {3}. Please update operation status via Job Card {4}.") - .format(d.idx, frappe.bold(d.operation), frappe.bold(total_completed_qty), work_order_link, job_card_link), OperationsNotCompleteError) + work_order_link = frappe.utils.get_link_to_form("Work Order", self.work_order) + job_card_link = frappe.utils.get_link_to_form("Job Card", job_card) + frappe.throw( + _( + "Row #{0}: Operation {1} is not completed for {2} qty of finished goods in Work Order {3}. Please update operation status via Job Card {4}." + ).format( + d.idx, + frappe.bold(d.operation), + frappe.bold(total_completed_qty), + work_order_link, + job_card_link, + ), + OperationsNotCompleteError, + ) def check_duplicate_entry_for_work_order(self): - other_ste = [t[0] for t in frappe.db.get_values("Stock Entry", { - "work_order": self.work_order, - "purpose": self.purpose, - "docstatus": ["!=", 2], - "name": ["!=", self.name] - }, "name")] + other_ste = [ + t[0] + for t in frappe.db.get_values( + "Stock Entry", + { + "work_order": self.work_order, + "purpose": self.purpose, + "docstatus": ["!=", 2], + "name": ["!=", self.name], + }, + "name", + ) + ] if other_ste: - production_item, qty = frappe.db.get_value("Work Order", - self.work_order, ["production_item", "qty"]) + production_item, qty = frappe.db.get_value( + "Work Order", self.work_order, ["production_item", "qty"] + ) args = other_ste + [production_item] - fg_qty_already_entered = frappe.db.sql("""select sum(transfer_qty) + fg_qty_already_entered = frappe.db.sql( + """select sum(transfer_qty) from `tabStock Entry Detail` where parent in (%s) and item_code = %s - and ifnull(s_warehouse,'')='' """ % (", ".join(["%s" * len(other_ste)]), "%s"), args)[0][0] + and ifnull(s_warehouse,'')='' """ + % (", ".join(["%s" * len(other_ste)]), "%s"), + args, + )[0][0] if fg_qty_already_entered and fg_qty_already_entered >= qty: frappe.throw( _("Stock Entries already created for Work Order {0}: {1}").format( @@ -435,34 +554,57 @@ class StockEntry(StockController): def set_actual_qty(self): from erpnext.stock.stock_ledger import is_negative_stock_allowed - for d in self.get('items'): + for d in self.get("items"): allow_negative_stock = is_negative_stock_allowed(item_code=d.item_code) - previous_sle = get_previous_sle({ - "item_code": d.item_code, - "warehouse": d.s_warehouse or d.t_warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time - }) + previous_sle = get_previous_sle( + { + "item_code": d.item_code, + "warehouse": d.s_warehouse or d.t_warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + } + ) # get actual stock at source warehouse d.actual_qty = previous_sle.get("qty_after_transaction") or 0 # validate qty during submit - if d.docstatus==1 and d.s_warehouse and not allow_negative_stock and flt(d.actual_qty, d.precision("actual_qty")) < flt(d.transfer_qty, d.precision("actual_qty")): - frappe.throw(_("Row {0}: Quantity not available for {4} in warehouse {1} at posting time of the entry ({2} {3})").format(d.idx, - frappe.bold(d.s_warehouse), formatdate(self.posting_date), - format_time(self.posting_time), frappe.bold(d.item_code)) - + '

    ' + _("Available quantity is {0}, you need {1}").format(frappe.bold(d.actual_qty), - frappe.bold(d.transfer_qty)), - NegativeStockError, title=_('Insufficient Stock')) + if ( + d.docstatus == 1 + and d.s_warehouse + and not allow_negative_stock + and flt(d.actual_qty, d.precision("actual_qty")) + < flt(d.transfer_qty, d.precision("actual_qty")) + ): + frappe.throw( + _( + "Row {0}: Quantity not available for {4} in warehouse {1} at posting time of the entry ({2} {3})" + ).format( + d.idx, + frappe.bold(d.s_warehouse), + formatdate(self.posting_date), + format_time(self.posting_time), + frappe.bold(d.item_code), + ) + + "

    " + + _("Available quantity is {0}, you need {1}").format( + frappe.bold(d.actual_qty), frappe.bold(d.transfer_qty) + ), + NegativeStockError, + title=_("Insufficient Stock"), + ) def set_serial_nos(self, work_order): - previous_se = frappe.db.get_value("Stock Entry", {"work_order": work_order, - "purpose": "Material Transfer for Manufacture"}, "name") + previous_se = frappe.db.get_value( + "Stock Entry", + {"work_order": work_order, "purpose": "Material Transfer for Manufacture"}, + "name", + ) - for d in self.get('items'): - transferred_serial_no = frappe.db.get_value("Stock Entry Detail",{"parent": previous_se, - "item_code": d.item_code}, "serial_no") + for d in self.get("items"): + transferred_serial_no = frappe.db.get_value( + "Stock Entry Detail", {"parent": previous_se, "item_code": d.item_code}, "serial_no" + ) if transferred_serial_no: d.serial_no = transferred_serial_no @@ -470,8 +612,8 @@ class StockEntry(StockController): @frappe.whitelist() def get_stock_and_rate(self): """ - Updates rate and availability of all the items. - Called from Update Rate and Availability button. + Updates rate and availability of all the items. + Called from Update Rate and Availability button. """ self.set_work_order_details() self.set_transfer_qty() @@ -488,38 +630,52 @@ class StockEntry(StockController): 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, scrapped and finished items """ # Set rate for outgoing items - outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate, raise_error_if_no_rate) - finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item or d.is_process_loss) + outgoing_items_cost = self.set_rate_for_outgoing_items( + reset_outgoing_rate, raise_error_if_no_rate + ) + finished_item_qty = sum( + d.transfer_qty for d in self.items if d.is_finished_item or d.is_process_loss + ) # Set basic rate for incoming items - for d in self.get('items'): - if d.s_warehouse or d.set_basic_rate_manually: continue + for d in self.get("items"): + if d.s_warehouse or d.set_basic_rate_manually: + continue 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(finished_item_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) 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, batch_no=d.batch_no) + 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, + batch_no=d.batch_no, + ) d.basic_rate = flt(d.basic_rate, d.precision("basic_rate")) if d.is_process_loss: - d.basic_rate = flt(0.) + d.basic_rate = flt(0.0) 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, raise_error_if_no_rate=True): outgoing_items_cost = 0.0 - for d in self.get('items'): + for d in self.get("items"): if d.s_warehouse: if reset_outgoing_rate: args = self.get_args_for_incoming_rate(d) @@ -534,19 +690,21 @@ class StockEntry(StockController): return outgoing_items_cost def get_args_for_incoming_rate(self, item): - return frappe._dict({ - "item_code": item.item_code, - "warehouse": item.s_warehouse or item.t_warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "qty": item.s_warehouse and -1*flt(item.transfer_qty) or flt(item.transfer_qty), - "serial_no": item.serial_no, - "batch_no": item.batch_no, - "voucher_type": self.doctype, - "voucher_no": self.name, - "company": self.company, - "allow_zero_valuation": item.allow_zero_valuation_rate, - }) + return frappe._dict( + { + "item_code": item.item_code, + "warehouse": item.s_warehouse or item.t_warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "qty": item.s_warehouse and -1 * flt(item.transfer_qty) or flt(item.transfer_qty), + "serial_no": item.serial_no, + "batch_no": item.batch_no, + "voucher_type": self.doctype, + "voucher_no": self.name, + "company": self.company, + "allow_zero_valuation": item.allow_zero_valuation_rate, + } + ) 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] @@ -562,9 +720,11 @@ class StockEntry(StockController): scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item]) # Get raw materials cost from BOM if multiple material consumption entries - if not outgoing_items_cost and frappe.db.get_single_value("Manufacturing Settings", "material_consumption", cache=True): + if not outgoing_items_cost and frappe.db.get_single_value( + "Manufacturing Settings", "material_consumption", cache=True + ): 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()]) + outgoing_items_cost = sum([flt(row.qty) * flt(row.rate) for row in bom_items.values()]) return flt((outgoing_items_cost - scrap_items_cost) / finished_item_qty) @@ -596,8 +756,10 @@ class StockEntry(StockController): for d in self.get("items"): if d.transfer_qty: d.amount = flt(flt(d.basic_amount) + flt(d.additional_cost), d.precision("amount")) - d.valuation_rate = flt(flt(d.basic_rate) + (flt(d.additional_cost) / flt(d.transfer_qty)), - d.precision("valuation_rate")) + d.valuation_rate = flt( + flt(d.basic_rate) + (flt(d.additional_cost) / flt(d.transfer_qty)), + d.precision("valuation_rate"), + ) def set_total_incoming_outgoing_value(self): self.total_incoming_value = self.total_outgoing_value = 0.0 @@ -611,92 +773,120 @@ class StockEntry(StockController): def set_total_amount(self): self.total_amount = None - if self.purpose not in ['Manufacture', 'Repack']: + if self.purpose not in ["Manufacture", "Repack"]: self.total_amount = sum([flt(item.amount) for item in self.get("items")]) def set_stock_entry_type(self): if self.purpose: - self.stock_entry_type = frappe.get_cached_value('Stock Entry Type', - {'purpose': self.purpose}, 'name') + self.stock_entry_type = frappe.get_cached_value( + "Stock Entry Type", {"purpose": self.purpose}, "name" + ) def set_purpose_for_stock_entry(self): if self.stock_entry_type and not self.purpose: - self.purpose = frappe.get_cached_value('Stock Entry Type', - self.stock_entry_type, 'purpose') + self.purpose = frappe.get_cached_value("Stock Entry Type", self.stock_entry_type, "purpose") def validate_duplicate_serial_no(self): warehouse_wise_serial_nos = {} # In case of repack the source and target serial nos could be same - for warehouse in ['s_warehouse', 't_warehouse']: + for warehouse in ["s_warehouse", "t_warehouse"]: serial_nos = [] for row in self.items: - if not (row.serial_no and row.get(warehouse)): continue + if not (row.serial_no and row.get(warehouse)): + continue for sn in get_serial_nos(row.serial_no): if sn in serial_nos: - frappe.throw(_('The serial no {0} has added multiple times in the stock entry {1}') - .format(frappe.bold(sn), self.name)) + frappe.throw( + _("The serial no {0} has added multiple times in the stock entry {1}").format( + frappe.bold(sn), self.name + ) + ) serial_nos.append(sn) def validate_purchase_order(self): """Throw exception if more raw material is transferred against Purchase Order than in the raw materials supplied table""" - backflush_raw_materials_based_on = frappe.db.get_single_value("Buying Settings", - "backflush_raw_materials_of_subcontract_based_on") + backflush_raw_materials_based_on = frappe.db.get_single_value( + "Buying Settings", "backflush_raw_materials_of_subcontract_based_on" + ) - qty_allowance = flt(frappe.db.get_single_value("Buying Settings", - "over_transfer_allowance")) + qty_allowance = flt(frappe.db.get_single_value("Buying Settings", "over_transfer_allowance")) - if not (self.purpose == "Send to Subcontractor" and self.purchase_order): return + if not (self.purpose == "Send to Subcontractor" and self.purchase_order): + return - if (backflush_raw_materials_based_on == 'BOM'): + if backflush_raw_materials_based_on == "BOM": purchase_order = frappe.get_doc("Purchase Order", self.purchase_order) for se_item in self.items: item_code = se_item.original_item or se_item.item_code precision = cint(frappe.db.get_default("float_precision")) or 3 - required_qty = sum([flt(d.required_qty) for d in purchase_order.supplied_items \ - if d.rm_item_code == item_code]) + required_qty = sum( + [flt(d.required_qty) for d in purchase_order.supplied_items if d.rm_item_code == item_code] + ) - total_allowed = required_qty + (required_qty * (qty_allowance/100)) + total_allowed = required_qty + (required_qty * (qty_allowance / 100)) if not required_qty: - bom_no = frappe.db.get_value("Purchase Order Item", + bom_no = frappe.db.get_value( + "Purchase Order Item", {"parent": self.purchase_order, "item_code": se_item.subcontracted_item}, - "bom") + "bom", + ) if se_item.allow_alternative_item: - original_item_code = frappe.get_value("Item Alternative", {"alternative_item_code": item_code}, "item_code") + original_item_code = frappe.get_value( + "Item Alternative", {"alternative_item_code": item_code}, "item_code" + ) - required_qty = sum([flt(d.required_qty) for d in purchase_order.supplied_items \ - if d.rm_item_code == original_item_code]) + required_qty = sum( + [ + flt(d.required_qty) + for d in purchase_order.supplied_items + if d.rm_item_code == original_item_code + ] + ) - total_allowed = required_qty + (required_qty * (qty_allowance/100)) + total_allowed = required_qty + (required_qty * (qty_allowance / 100)) if not required_qty: - frappe.throw(_("Item {0} not found in 'Raw Materials Supplied' table in Purchase Order {1}") - .format(se_item.item_code, self.purchase_order)) - total_supplied = frappe.db.sql("""select sum(transfer_qty) + frappe.throw( + _("Item {0} not found in 'Raw Materials Supplied' table in Purchase Order {1}").format( + se_item.item_code, self.purchase_order + ) + ) + total_supplied = frappe.db.sql( + """select sum(transfer_qty) from `tabStock Entry Detail`, `tabStock Entry` where `tabStock Entry`.purchase_order = %s and `tabStock Entry`.docstatus = 1 and `tabStock Entry Detail`.item_code = %s and `tabStock Entry Detail`.parent = `tabStock Entry`.name""", - (self.purchase_order, se_item.item_code))[0][0] + (self.purchase_order, se_item.item_code), + )[0][0] if flt(total_supplied, precision) > flt(total_allowed, precision): - frappe.throw(_("Row {0}# Item {1} cannot be transferred more than {2} against Purchase Order {3}") - .format(se_item.idx, se_item.item_code, total_allowed, self.purchase_order)) + frappe.throw( + _("Row {0}# Item {1} cannot be transferred more than {2} against Purchase Order {3}").format( + se_item.idx, se_item.item_code, total_allowed, self.purchase_order + ) + ) elif backflush_raw_materials_based_on == "Material Transferred for Subcontract": for row in self.items: if not row.subcontracted_item: - frappe.throw(_("Row {0}: Subcontracted Item is mandatory for the raw material {1}") - .format(row.idx, frappe.bold(row.item_code))) + frappe.throw( + _("Row {0}: Subcontracted Item is mandatory for the raw material {1}").format( + row.idx, frappe.bold(row.item_code) + ) + ) elif not row.po_detail: filters = { - "parent": self.purchase_order, "docstatus": 1, - "rm_item_code": row.item_code, "main_item_code": row.subcontracted_item + "parent": self.purchase_order, + "docstatus": 1, + "rm_item_code": row.item_code, + "main_item_code": row.subcontracted_item, } po_detail = frappe.db.get_value("Purchase Order Item Supplied", filters, "name") @@ -704,7 +894,7 @@ class StockEntry(StockController): row.db_set("po_detail", po_detail) def validate_bom(self): - for d in self.get('items'): + for d in self.get("items"): if d.bom_no and d.is_finished_item: item_code = d.original_item or d.item_code validate_bom_no(item_code, d.bom_no) @@ -722,7 +912,7 @@ class StockEntry(StockController): for d in self.items: if d.t_warehouse and not d.s_warehouse: - if self.purpose=="Repack" or d.item_code == finished_item: + if self.purpose == "Repack" or d.item_code == finished_item: d.is_finished_item = 1 else: d.is_scrap_item = 1 @@ -741,19 +931,17 @@ class StockEntry(StockController): def validate_finished_goods(self): """ - 1. Check if FG exists (mfg, repack) - 2. Check if Multiple FG Items are present (mfg) - 3. Check FG Item and Qty against WO if present (mfg) + 1. Check if FG exists (mfg, repack) + 2. Check if Multiple FG Items are present (mfg) + 3. Check FG Item and Qty against WO if present (mfg) """ production_item, wo_qty, finished_items = None, 0, [] - wo_details = frappe.db.get_value( - "Work Order", self.work_order, ["production_item", "qty"] - ) + wo_details = frappe.db.get_value("Work Order", self.work_order, ["production_item", "qty"]) if wo_details: production_item, wo_qty = wo_details - for d in self.get('items'): + for d in self.get("items"): if d.is_finished_item: if not self.work_order: # Independent MFG Entry/ Repack Entry, no WO to match against @@ -761,12 +949,16 @@ class StockEntry(StockController): continue 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) + 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) + frappe.throw( + _("Quantity in row {0} ({1}) must be same as manufactured quantity {2}").format( + d.idx, d.transfer_qty, self.fg_completed_qty + ) ) finished_items.append(d.item_code) @@ -774,28 +966,31 @@ class StockEntry(StockController): if not finished_items: frappe.throw( msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name), - title=_("Missing Finished Good"), exc=FinishedGoodError + title=_("Missing Finished Good"), + exc=FinishedGoodError, ) if self.purpose == "Manufacture": if len(set(finished_items)) > 1: frappe.throw( msg=_("Multiple items cannot be marked as finished item"), - title=_("Note"), exc=FinishedGoodError + title=_("Note"), + exc=FinishedGoodError, ) allowance_percentage = flt( frappe.db.get_single_value( - "Manufacturing Settings","overproduction_percentage_for_work_order" + "Manufacturing Settings", "overproduction_percentage_for_work_order" ) ) - allowed_qty = wo_qty + ((allowance_percentage/100) * wo_qty) + allowed_qty = wo_qty + ((allowance_percentage / 100) * wo_qty) # No work order could mean independent Manufacture entry, if so skip validation if self.work_order and 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) + _("For quantity {0} should not be greater than work order quantity {1}").format( + flt(self.fg_completed_qty), wo_qty + ) ) def update_stock_ledger(self): @@ -817,35 +1012,38 @@ class StockEntry(StockController): def get_finished_item_row(self): finished_item_row = None if self.purpose in ("Manufacture", "Repack"): - for d in self.get('items'): + 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'): + 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 - }) + 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): + 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'): + 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) - }) + 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 @@ -875,40 +1073,55 @@ class StockEntry(StockController): continue item_account_wise_additional_cost.setdefault((d.item_code, d.name), {}) - item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, { - "amount": 0.0, - "base_amount": 0.0 - }) + item_account_wise_additional_cost[(d.item_code, d.name)].setdefault( + t.expense_account, {"amount": 0.0, "base_amount": 0.0} + ) multiply_based_on = d.basic_amount if total_basic_amount else d.qty - item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["amount"] += \ + item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["amount"] += ( flt(t.amount * multiply_based_on) / divide_based_on + ) - item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["base_amount"] += \ + item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["base_amount"] += ( flt(t.base_amount * multiply_based_on) / divide_based_on + ) if item_account_wise_additional_cost: for d in self.get("items"): - for account, amount in item_account_wise_additional_cost.get((d.item_code, d.name), {}).items(): - if not amount: continue + for account, amount in item_account_wise_additional_cost.get( + (d.item_code, d.name), {} + ).items(): + if not amount: + continue - gl_entries.append(self.get_gl_dict({ - "account": account, - "against": d.expense_account, - "cost_center": d.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit_in_account_currency": flt(amount["amount"]), - "credit": flt(amount["base_amount"]) - }, item=d)) + gl_entries.append( + self.get_gl_dict( + { + "account": account, + "against": d.expense_account, + "cost_center": d.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit_in_account_currency": flt(amount["amount"]), + "credit": flt(amount["base_amount"]), + }, + item=d, + ) + ) - gl_entries.append(self.get_gl_dict({ - "account": d.expense_account, - "against": account, - "cost_center": d.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": -1 * amount['base_amount'] # put it as negative credit instead of debit purposefully - }, item=d)) + gl_entries.append( + self.get_gl_dict( + { + "account": d.expense_account, + "against": account, + "cost_center": d.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit": -1 + * amount["base_amount"], # put it as negative credit instead of debit purposefully + }, + item=d, + ) + ) return process_gl_map(gl_entries) @@ -917,11 +1130,13 @@ class StockEntry(StockController): if flt(pro_doc.docstatus) != 1: frappe.throw(_("Work Order {0} must be submitted").format(self.work_order)) - if pro_doc.status == 'Stopped': - frappe.throw(_("Transaction not allowed against stopped Work Order {0}").format(self.work_order)) + if pro_doc.status == "Stopped": + frappe.throw( + _("Transaction not allowed against stopped Work Order {0}").format(self.work_order) + ) if self.job_card: - job_doc = frappe.get_doc('Job Card', self.job_card) + job_doc = frappe.get_doc("Job Card", self.job_card) job_doc.set_transferred_qty(update_status=True) job_doc.set_transferred_qty_in_job_card(self) @@ -941,73 +1156,95 @@ class StockEntry(StockController): @frappe.whitelist() def get_item_details(self, args=None, for_update=False): - item = frappe.db.sql("""select i.name, i.stock_uom, i.description, i.image, i.item_name, i.item_group, + item = frappe.db.sql( + """select i.name, i.stock_uom, i.description, i.image, i.item_name, i.item_group, i.has_batch_no, i.sample_quantity, i.has_serial_no, i.allow_alternative_item, id.expense_account, id.buying_cost_center from `tabItem` i LEFT JOIN `tabItem Default` id ON i.name=id.parent and id.company=%s where i.name=%s and i.disabled=0 and (i.end_of_life is null or i.end_of_life='0000-00-00' or i.end_of_life > %s)""", - (self.company, args.get('item_code'), nowdate()), as_dict = 1) + (self.company, args.get("item_code"), nowdate()), + as_dict=1, + ) if not item: - frappe.throw(_("Item {0} is not active or end of life has been reached").format(args.get("item_code"))) + frappe.throw( + _("Item {0} is not active or end of life has been reached").format(args.get("item_code")) + ) item = item[0] item_group_defaults = get_item_group_defaults(item.name, self.company) brand_defaults = get_brand_defaults(item.name, self.company) - ret = frappe._dict({ - 'uom' : item.stock_uom, - 'stock_uom' : item.stock_uom, - 'description' : item.description, - 'image' : item.image, - 'item_name' : item.item_name, - 'cost_center' : get_default_cost_center(args, item, item_group_defaults, brand_defaults, self.company), - 'qty' : args.get("qty"), - 'transfer_qty' : args.get('qty'), - 'conversion_factor' : 1, - 'batch_no' : '', - 'actual_qty' : 0, - 'basic_rate' : 0, - 'serial_no' : '', - 'has_serial_no' : item.has_serial_no, - 'has_batch_no' : item.has_batch_no, - 'sample_quantity' : item.sample_quantity, - 'expense_account' : item.expense_account - }) + ret = frappe._dict( + { + "uom": item.stock_uom, + "stock_uom": item.stock_uom, + "description": item.description, + "image": item.image, + "item_name": item.item_name, + "cost_center": get_default_cost_center( + args, item, item_group_defaults, brand_defaults, self.company + ), + "qty": args.get("qty"), + "transfer_qty": args.get("qty"), + "conversion_factor": 1, + "batch_no": "", + "actual_qty": 0, + "basic_rate": 0, + "serial_no": "", + "has_serial_no": item.has_serial_no, + "has_batch_no": item.has_batch_no, + "sample_quantity": item.sample_quantity, + "expense_account": item.expense_account, + } + ) - if self.purpose == 'Send to Subcontractor': + if self.purpose == "Send to Subcontractor": ret["allow_alternative_item"] = item.allow_alternative_item # update uom if args.get("uom") and for_update: - ret.update(get_uom_details(args.get('item_code'), args.get('uom'), args.get('qty'))) + ret.update(get_uom_details(args.get("item_code"), args.get("uom"), args.get("qty"))) - if self.purpose == 'Material Issue': - ret["expense_account"] = (item.get("expense_account") or - item_group_defaults.get("expense_account") or - frappe.get_cached_value('Company', self.company, "default_expense_account")) + if self.purpose == "Material Issue": + ret["expense_account"] = ( + item.get("expense_account") + or item_group_defaults.get("expense_account") + or frappe.get_cached_value("Company", self.company, "default_expense_account") + ) - for company_field, field in {'stock_adjustment_account': 'expense_account', - 'cost_center': 'cost_center'}.items(): + for company_field, field in { + "stock_adjustment_account": "expense_account", + "cost_center": "cost_center", + }.items(): if not ret.get(field): - ret[field] = frappe.get_cached_value('Company', self.company, company_field) + ret[field] = frappe.get_cached_value("Company", self.company, company_field) - args['posting_date'] = self.posting_date - args['posting_time'] = self.posting_time + args["posting_date"] = self.posting_date + args["posting_time"] = self.posting_time - stock_and_rate = get_warehouse_details(args) if args.get('warehouse') else {} + stock_and_rate = get_warehouse_details(args) if args.get("warehouse") else {} ret.update(stock_and_rate) # automatically select batch for outgoing item - if (args.get('s_warehouse', None) and args.get('qty') and - ret.get('has_batch_no') and not args.get('batch_no')): - args.batch_no = get_batch_no(args['item_code'], args['s_warehouse'], args['qty']) + if ( + args.get("s_warehouse", None) + and args.get("qty") + and ret.get("has_batch_no") + and not args.get("batch_no") + ): + args.batch_no = get_batch_no(args["item_code"], args["s_warehouse"], args["qty"]) - if self.purpose == "Send to Subcontractor" and self.get("purchase_order") and args.get('item_code'): - subcontract_items = frappe.get_all("Purchase Order Item Supplied", - {"parent": self.purchase_order, "rm_item_code": args.get('item_code')}, "main_item_code") + if ( + self.purpose == "Send to Subcontractor" and self.get("purchase_order") and args.get("item_code") + ): + subcontract_items = frappe.get_all( + "Purchase Order Item Supplied", + {"parent": self.purchase_order, "rm_item_code": args.get("item_code")}, + "main_item_code", + ) if subcontract_items and len(subcontract_items) == 1: ret["subcontracted_item"] = subcontract_items[0].main_item_code @@ -1018,46 +1255,57 @@ class StockEntry(StockController): def set_items_for_stock_in(self): self.items = [] - if self.outgoing_stock_entry and self.purpose == 'Material Transfer': - doc = frappe.get_doc('Stock Entry', self.outgoing_stock_entry) + if self.outgoing_stock_entry and self.purpose == "Material Transfer": + doc = frappe.get_doc("Stock Entry", self.outgoing_stock_entry) if doc.per_transferred == 100: - frappe.throw(_("Goods are already received against the outward entry {0}") - .format(doc.name)) + frappe.throw(_("Goods are already received against the outward entry {0}").format(doc.name)) for d in doc.items: - self.append('items', { - 's_warehouse': d.t_warehouse, - 'item_code': d.item_code, - 'qty': d.qty, - 'uom': d.uom, - 'against_stock_entry': d.parent, - 'ste_detail': d.name, - 'stock_uom': d.stock_uom, - 'conversion_factor': d.conversion_factor, - 'serial_no': d.serial_no, - 'batch_no': d.batch_no - }) + self.append( + "items", + { + "s_warehouse": d.t_warehouse, + "item_code": d.item_code, + "qty": d.qty, + "uom": d.uom, + "against_stock_entry": d.parent, + "ste_detail": d.name, + "stock_uom": d.stock_uom, + "conversion_factor": d.conversion_factor, + "serial_no": d.serial_no, + "batch_no": d.batch_no, + }, + ) @frappe.whitelist() def get_items(self): - self.set('items', []) + self.set("items", []) self.validate_work_order() if not self.posting_date or not self.posting_time: frappe.throw(_("Posting date and posting time is mandatory")) self.set_work_order_details() - self.flags.backflush_based_on = frappe.db.get_single_value("Manufacturing Settings", - "backflush_raw_materials_based_on") + self.flags.backflush_based_on = frappe.db.get_single_value( + "Manufacturing Settings", "backflush_raw_materials_based_on" + ) if self.bom_no: - backflush_based_on = frappe.db.get_single_value("Manufacturing Settings", - "backflush_raw_materials_based_on") + backflush_based_on = frappe.db.get_single_value( + "Manufacturing Settings", "backflush_raw_materials_based_on" + ) - if self.purpose in ["Material Issue", "Material Transfer", "Manufacture", "Repack", - "Send to Subcontractor", "Material Transfer for Manufacture", "Material Consumption for Manufacture"]: + if self.purpose in [ + "Material Issue", + "Material Transfer", + "Manufacture", + "Repack", + "Send to Subcontractor", + "Material Transfer for Manufacture", + "Material Consumption for Manufacture", + ]: if self.work_order and self.purpose == "Material Transfer for Manufacture": item_dict = self.get_pending_raw_materials(backflush_based_on) @@ -1066,14 +1314,20 @@ class StockEntry(StockController): item["to_warehouse"] = self.pro_doc.wip_warehouse self.add_to_stock_entry_detail(item_dict) - elif (self.work_order and (self.purpose == "Manufacture" - or self.purpose == "Material Consumption for Manufacture") and not self.pro_doc.skip_transfer - and self.flags.backflush_based_on == "Material Transferred for Manufacture"): + elif ( + self.work_order + and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture") + and not self.pro_doc.skip_transfer + and self.flags.backflush_based_on == "Material Transferred for Manufacture" + ): self.get_transfered_raw_materials() - elif (self.work_order and (self.purpose == "Manufacture" or - self.purpose == "Material Consumption for Manufacture") and self.flags.backflush_based_on== "BOM" - and frappe.db.get_single_value("Manufacturing Settings", "material_consumption")== 1): + elif ( + self.work_order + and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture") + and self.flags.backflush_based_on == "BOM" + and frappe.db.get_single_value("Manufacturing Settings", "material_consumption") == 1 + ): self.get_unconsumed_raw_materials() else: @@ -1082,31 +1336,36 @@ class StockEntry(StockController): item_dict = self.get_bom_raw_materials(self.fg_completed_qty) - #Get PO Supplied Items Details + # Get PO Supplied Items Details if self.purchase_order and self.purpose == "Send to Subcontractor": - #Get PO Supplied Items Details - item_wh = frappe._dict(frappe.db.sql(""" + # Get PO Supplied Items Details + item_wh = frappe._dict( + frappe.db.sql( + """ SELECT rm_item_code, reserve_warehouse FROM `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup WHERE - po.name = poitemsup.parent and po.name = %s """,self.purchase_order)) + po.name = poitemsup.parent and po.name = %s """, + self.purchase_order, + ) + ) for item in item_dict.values(): if self.pro_doc and cint(self.pro_doc.from_wip_warehouse): item["from_warehouse"] = self.pro_doc.wip_warehouse - #Get Reserve Warehouse from PO - if self.purchase_order and self.purpose=="Send to Subcontractor": + # Get Reserve Warehouse from PO + if self.purchase_order and self.purpose == "Send to Subcontractor": item["from_warehouse"] = item_wh.get(item.item_code) - item["to_warehouse"] = self.to_warehouse if self.purpose=="Send to Subcontractor" else "" + item["to_warehouse"] = self.to_warehouse if self.purpose == "Send to Subcontractor" else "" self.add_to_stock_entry_detail(item_dict) # fetch the serial_no of the first stock entry for the second stock entry if self.work_order and self.purpose == "Manufacture": self.set_serial_nos(self.work_order) - work_order = frappe.get_doc('Work Order', self.work_order) + work_order = frappe.get_doc("Work Order", self.work_order) add_additional_cost(self, work_order) # add finished goods item @@ -1123,7 +1382,7 @@ class StockEntry(StockController): if self.purpose != "Send to Subcontractor" and self.purpose in ["Manufacture", "Repack"]: scrap_item_dict = self.get_bom_scrap_material(self.fg_completed_qty) for item in scrap_item_dict.values(): - item.idx = '' + item.idx = "" if self.pro_doc and self.pro_doc.scrap_warehouse: item["to_warehouse"] = self.pro_doc.scrap_warehouse @@ -1136,7 +1395,7 @@ class StockEntry(StockController): if self.work_order: # common validations if not self.pro_doc: - self.pro_doc = frappe.get_doc('Work Order', self.work_order) + self.pro_doc = frappe.get_doc("Work Order", self.work_order) if self.pro_doc: self.bom_no = self.pro_doc.bom_no @@ -1167,11 +1426,18 @@ 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 + "is_finished_item": 1, } - if self.work_order and self.pro_doc.has_batch_no and cint(frappe.db.get_single_value('Manufacturing Settings', - 'make_serial_no_batch_from_work_order', cache=True)): + if ( + self.work_order + and self.pro_doc.has_batch_no + and cint( + frappe.db.get_single_value( + "Manufacturing Settings", "make_serial_no_batch_from_work_order", cache=True + ) + ) + ): self.set_batchwise_finished_goods(args, item) else: self.add_finished_goods(args, item) @@ -1180,12 +1446,12 @@ class StockEntry(StockController): filters = { "reference_name": self.pro_doc.name, "reference_doctype": self.pro_doc.doctype, - "qty_to_produce": (">", 0) + "qty_to_produce": (">", 0), } fields = ["qty_to_produce as qty", "produced_qty", "name"] - data = frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc") + data = frappe.get_all("Batch", filters=filters, fields=fields, order_by="creation asc") if not data: self.add_finished_goods(args, item) @@ -1200,7 +1466,7 @@ class StockEntry(StockController): if not batch_qty: continue - if qty <=0: + if qty <= 0: break fg_qty = batch_qty @@ -1214,23 +1480,27 @@ class StockEntry(StockController): self.add_finished_goods(args, item) def add_finished_goods(self, args, item): - self.add_to_stock_entry_detail({ - item.name: args - }, bom_no = self.bom_no) + self.add_to_stock_entry_detail({item.name: args}, bom_no=self.bom_no) def get_bom_raw_materials(self, qty): from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict # item dict = { item_code: {qty, description, stock_uom} } - item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=qty, - fetch_exploded = self.use_multi_level_bom, fetch_qty_in_stock_uom=False) + item_dict = get_bom_items_as_dict( + self.bom_no, + self.company, + qty=qty, + fetch_exploded=self.use_multi_level_bom, + fetch_qty_in_stock_uom=False, + ) - used_alternative_items = get_used_alternative_items(work_order = self.work_order) + used_alternative_items = get_used_alternative_items(work_order=self.work_order) for item in item_dict.values(): # if source warehouse presents in BOM set from_warehouse as bom source_warehouse if item["allow_alternative_item"]: - item["allow_alternative_item"] = frappe.db.get_value('Work Order', - self.work_order, "allow_alternative_item") + item["allow_alternative_item"] = frappe.db.get_value( + "Work Order", self.work_order, "allow_alternative_item" + ) item.from_warehouse = self.from_warehouse or item.source_warehouse or item.default_warehouse if item.item_code in used_alternative_items: @@ -1248,8 +1518,10 @@ class StockEntry(StockController): from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict # item dict = { item_code: {qty, description, stock_uom} } - item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=qty, - fetch_exploded = 0, fetch_scrap_items = 1) or {} + item_dict = ( + get_bom_items_as_dict(self.bom_no, self.company, qty=qty, fetch_exploded=0, fetch_scrap_items=1) + or {} + ) for item in item_dict.values(): item.from_warehouse = "" @@ -1263,16 +1535,18 @@ class StockEntry(StockController): if not item_row: item_row = frappe._dict({}) - item_row.update({ - 'uom': row.stock_uom, - 'from_warehouse': '', - 'qty': row.stock_qty + flt(item_row.stock_qty), - 'converison_factor': 1, - 'is_scrap_item': 1, - 'item_name': row.item_name, - 'description': row.description, - 'allow_zero_valuation_rate': 1 - }) + item_row.update( + { + "uom": row.stock_uom, + "from_warehouse": "", + "qty": row.stock_qty + flt(item_row.stock_qty), + "converison_factor": 1, + "is_scrap_item": 1, + "item_name": row.item_name, + "description": row.description, + "allow_zero_valuation_rate": 1, + } + ) item_dict[row.item_code] = item_row @@ -1285,21 +1559,25 @@ class StockEntry(StockController): if not self.pro_doc.operations: return [] - job_card = frappe.qb.DocType('Job Card') - job_card_scrap_item = frappe.qb.DocType('Job Card Scrap Item') + job_card = frappe.qb.DocType("Job Card") + job_card_scrap_item = frappe.qb.DocType("Job Card Scrap Item") scrap_items = ( frappe.qb.from_(job_card) .select( - Sum(job_card_scrap_item.stock_qty).as_('stock_qty'), - job_card_scrap_item.item_code, job_card_scrap_item.item_name, - job_card_scrap_item.description, job_card_scrap_item.stock_uom) + Sum(job_card_scrap_item.stock_qty).as_("stock_qty"), + job_card_scrap_item.item_code, + job_card_scrap_item.item_name, + job_card_scrap_item.description, + job_card_scrap_item.stock_uom, + ) .join(job_card_scrap_item) .on(job_card_scrap_item.parent == job_card.name) .where( (job_card_scrap_item.item_code.isnotnull()) & (job_card.work_order == self.work_order) - & (job_card.docstatus == 1)) + & (job_card.docstatus == 1) + ) .groupby(job_card_scrap_item.item_code) ).run(as_dict=1) @@ -1313,7 +1591,7 @@ class StockEntry(StockController): if used_scrap_items.get(row.item_code): used_scrap_items[row.item_code] -= row.stock_qty - if cint(frappe.get_cached_value('UOM', row.stock_uom, 'must_be_whole_number')): + if cint(frappe.get_cached_value("UOM", row.stock_uom, "must_be_whole_number")): row.stock_qty = frappe.utils.ceil(row.stock_qty) return scrap_items @@ -1324,16 +1602,14 @@ class StockEntry(StockController): def get_used_scrap_items(self): used_scrap_items = defaultdict(float) data = frappe.get_all( - 'Stock Entry', - fields = [ - '`tabStock Entry Detail`.`item_code`', '`tabStock Entry Detail`.`qty`' + "Stock Entry", + fields=["`tabStock Entry Detail`.`item_code`", "`tabStock Entry Detail`.`qty`"], + filters=[ + ["Stock Entry", "work_order", "=", self.work_order], + ["Stock Entry Detail", "is_scrap_item", "=", 1], + ["Stock Entry", "docstatus", "=", 1], + ["Stock Entry", "purpose", "in", ["Repack", "Manufacture"]], ], - filters = [ - ['Stock Entry', 'work_order', '=', self.work_order], - ['Stock Entry Detail', 'is_scrap_item', '=', 1], - ['Stock Entry', 'docstatus', '=', 1], - ['Stock Entry', 'purpose', 'in', ['Repack', 'Manufacture']] - ] ) for row in data: @@ -1343,10 +1619,11 @@ class StockEntry(StockController): def get_unconsumed_raw_materials(self): wo = frappe.get_doc("Work Order", self.work_order) - wo_items = frappe.get_all('Work Order Item', - filters={'parent': self.work_order}, - fields=["item_code", "source_warehouse", "required_qty", "consumed_qty", "transferred_qty"] - ) + wo_items = frappe.get_all( + "Work Order Item", + filters={"parent": self.work_order}, + fields=["item_code", "source_warehouse", "required_qty", "consumed_qty", "transferred_qty"], + ) work_order_qty = wo.material_transferred_for_manufacturing or wo.qty for item in wo_items: @@ -1363,21 +1640,24 @@ class StockEntry(StockController): qty = req_qty_each * flt(self.fg_completed_qty) if qty > 0: - self.add_to_stock_entry_detail({ - item.item_code: { - "from_warehouse": wo.wip_warehouse or item.source_warehouse, - "to_warehouse": "", - "qty": qty, - "item_name": item.item_name, - "description": item.description, - "stock_uom": item_account_details.stock_uom, - "expense_account": item_account_details.get("expense_account"), - "cost_center": item_account_details.get("buying_cost_center"), + self.add_to_stock_entry_detail( + { + item.item_code: { + "from_warehouse": wo.wip_warehouse or item.source_warehouse, + "to_warehouse": "", + "qty": qty, + "item_name": item.item_name, + "description": item.description, + "stock_uom": item_account_details.stock_uom, + "expense_account": item_account_details.get("expense_account"), + "cost_center": item_account_details.get("buying_cost_center"), + } } - }) + ) def get_transfered_raw_materials(self): - transferred_materials = frappe.db.sql(""" + transferred_materials = frappe.db.sql( + """ select item_name, original_item, item_code, sum(qty) as qty, sed.t_warehouse as warehouse, description, stock_uom, expense_account, cost_center @@ -1386,9 +1666,13 @@ class StockEntry(StockController): se.name = sed.parent and se.docstatus=1 and se.purpose='Material Transfer for Manufacture' and se.work_order= %s and ifnull(sed.t_warehouse, '') != '' group by sed.item_code, sed.t_warehouse - """, self.work_order, as_dict=1) + """, + self.work_order, + as_dict=1, + ) - materials_already_backflushed = frappe.db.sql(""" + materials_already_backflushed = frappe.db.sql( + """ select item_code, sed.s_warehouse as warehouse, sum(qty) as qty from @@ -1398,26 +1682,34 @@ class StockEntry(StockController): and (se.purpose='Manufacture' or se.purpose='Material Consumption for Manufacture') and se.work_order= %s and ifnull(sed.s_warehouse, '') != '' group by sed.item_code, sed.s_warehouse - """, self.work_order, as_dict=1) + """, + self.work_order, + as_dict=1, + ) - backflushed_materials= {} + backflushed_materials = {} for d in materials_already_backflushed: - backflushed_materials.setdefault(d.item_code,[]).append({d.warehouse: d.qty}) + backflushed_materials.setdefault(d.item_code, []).append({d.warehouse: d.qty}) - po_qty = frappe.db.sql("""select qty, produced_qty, material_transferred_for_manufacturing from - `tabWork Order` where name=%s""", self.work_order, as_dict=1)[0] + po_qty = frappe.db.sql( + """select qty, produced_qty, material_transferred_for_manufacturing from + `tabWork Order` where name=%s""", + self.work_order, + as_dict=1, + )[0] manufacturing_qty = flt(po_qty.qty) or 1 produced_qty = flt(po_qty.produced_qty) trans_qty = flt(po_qty.material_transferred_for_manufacturing) or 1 for item in transferred_materials: - qty= item.qty + qty = item.qty item_code = item.original_item or item.item_code - req_items = frappe.get_all('Work Order Item', - filters={'parent': self.work_order, 'item_code': item_code}, - fields=["required_qty", "consumed_qty"] - ) + req_items = frappe.get_all( + "Work Order Item", + filters={"parent": self.work_order, "item_code": item_code}, + fields=["required_qty", "consumed_qty"], + ) req_qty = flt(req_items[0].required_qty) if req_items else flt(4) req_qty_each = flt(req_qty / manufacturing_qty) @@ -1425,23 +1717,23 @@ class StockEntry(StockController): if trans_qty and manufacturing_qty > (produced_qty + flt(self.fg_completed_qty)): if qty >= req_qty: - qty = (req_qty/trans_qty) * flt(self.fg_completed_qty) + qty = (req_qty / trans_qty) * flt(self.fg_completed_qty) else: qty = qty - consumed_qty - if self.purpose == 'Manufacture': + if self.purpose == "Manufacture": # If Material Consumption is booked, must pull only remaining components to finish product if consumed_qty != 0: remaining_qty = consumed_qty - (produced_qty * req_qty_each) exhaust_qty = req_qty_each * produced_qty - if remaining_qty > exhaust_qty : - if (remaining_qty/(req_qty_each * flt(self.fg_completed_qty))) >= 1: - qty =0 + if remaining_qty > exhaust_qty: + if (remaining_qty / (req_qty_each * flt(self.fg_completed_qty))) >= 1: + qty = 0 else: qty = (req_qty_each * flt(self.fg_completed_qty)) - remaining_qty else: if self.flags.backflush_based_on == "Material Transferred for Manufacture": - qty = (item.qty/trans_qty) * flt(self.fg_completed_qty) + qty = (item.qty / trans_qty) * flt(self.fg_completed_qty) else: qty = req_qty_each * flt(self.fg_completed_qty) @@ -1449,45 +1741,51 @@ class StockEntry(StockController): precision = frappe.get_precision("Stock Entry Detail", "qty") for d in backflushed_materials.get(item.item_code): if d.get(item.warehouse) > 0: - if (qty > req_qty): - qty = ((flt(qty, precision) - flt(d.get(item.warehouse), precision)) + if qty > req_qty: + qty = ( + (flt(qty, precision) - flt(d.get(item.warehouse), precision)) / (flt(trans_qty, precision) - flt(produced_qty, precision)) ) * flt(self.fg_completed_qty) d[item.warehouse] -= qty - if cint(frappe.get_cached_value('UOM', item.stock_uom, 'must_be_whole_number')): + if cint(frappe.get_cached_value("UOM", item.stock_uom, "must_be_whole_number")): qty = frappe.utils.ceil(qty) if qty > 0: - self.add_to_stock_entry_detail({ - item.item_code: { - "from_warehouse": item.warehouse, - "to_warehouse": "", - "qty": qty, - "item_name": item.item_name, - "description": item.description, - "stock_uom": item.stock_uom, - "expense_account": item.expense_account, - "cost_center": item.buying_cost_center, - "original_item": item.original_item + self.add_to_stock_entry_detail( + { + item.item_code: { + "from_warehouse": item.warehouse, + "to_warehouse": "", + "qty": qty, + "item_name": item.item_name, + "description": item.description, + "stock_uom": item.stock_uom, + "expense_account": item.expense_account, + "cost_center": item.buying_cost_center, + "original_item": item.original_item, + } } - }) + ) def get_pending_raw_materials(self, backflush_based_on=None): """ - issue (item quantity) that is pending to issue or desire to transfer, - whichever is less + issue (item quantity) that is pending to issue or desire to transfer, + whichever is less """ item_dict = self.get_pro_order_required_items(backflush_based_on) max_qty = flt(self.pro_doc.qty) allow_overproduction = False - overproduction_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", - "overproduction_percentage_for_work_order")) + overproduction_percentage = flt( + frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order") + ) - to_transfer_qty = flt(self.pro_doc.material_transferred_for_manufacturing) + flt(self.fg_completed_qty) + to_transfer_qty = flt(self.pro_doc.material_transferred_for_manufacturing) + flt( + self.fg_completed_qty + ) transfer_limit_qty = max_qty + ((max_qty * overproduction_percentage) / 100) if transfer_limit_qty >= to_transfer_qty: @@ -1497,9 +1795,11 @@ class StockEntry(StockController): pending_to_issue = flt(item_details.required_qty) - flt(item_details.transferred_qty) desire_to_transfer = flt(self.fg_completed_qty) * flt(item_details.required_qty) / max_qty - if (desire_to_transfer <= pending_to_issue + if ( + desire_to_transfer <= pending_to_issue or (desire_to_transfer > 0 and backflush_based_on == "Material Transferred for Manufacture") - or allow_overproduction): + or allow_overproduction + ): item_dict[item]["qty"] = desire_to_transfer elif pending_to_issue > 0: item_dict[item]["qty"] = pending_to_issue @@ -1520,7 +1820,7 @@ class StockEntry(StockController): def get_pro_order_required_items(self, backflush_based_on=None): """ - Gets Work Order Required Items only if Stock Entry purpose is **Material Transferred for Manufacture**. + Gets Work Order Required Items only if Stock Entry purpose is **Material Transferred for Manufacture**. """ item_dict, job_card_items = frappe._dict(), [] work_order = frappe.get_doc("Work Order", self.work_order) @@ -1539,7 +1839,9 @@ class StockEntry(StockController): continue transfer_pending = flt(d.required_qty) > flt(d.transferred_qty) - can_transfer = transfer_pending or (backflush_based_on == "Material Transferred for Manufacture") + can_transfer = transfer_pending or ( + backflush_based_on == "Material Transferred for Manufacture" + ) if not can_transfer: continue @@ -1550,11 +1852,7 @@ class StockEntry(StockController): if consider_job_card: job_card_item = frappe.db.get_value( - "Job Card Item", - { - "item_code": d.item_code, - "parent": self.get("job_card") - } + "Job Card Item", {"item_code": d.item_code, "parent": self.get("job_card")} ) item_row["job_card_item"] = job_card_item or None @@ -1574,12 +1872,7 @@ class StockEntry(StockController): return [] job_card_items = frappe.get_all( - "Job Card Item", - filters={ - "parent": job_card - }, - fields=["item_code"], - distinct=True + "Job Card Item", filters={"parent": job_card}, fields=["item_code"], distinct=True ) return [d.item_code for d in job_card_items] @@ -1588,60 +1881,87 @@ class StockEntry(StockController): item_row = item_dict[d] stock_uom = item_row.get("stock_uom") or frappe.db.get_value("Item", d, "stock_uom") - se_child = self.append('items') + se_child = self.append("items") se_child.s_warehouse = item_row.get("from_warehouse") se_child.t_warehouse = item_row.get("to_warehouse") - se_child.item_code = item_row.get('item_code') or cstr(d) + se_child.item_code = item_row.get("item_code") or cstr(d) se_child.uom = item_row["uom"] if item_row.get("uom") else stock_uom se_child.stock_uom = stock_uom se_child.qty = flt(item_row["qty"], se_child.precision("qty")) se_child.allow_alternative_item = item_row.get("allow_alternative_item", 0) se_child.subcontracted_item = item_row.get("main_item_code") - se_child.cost_center = (item_row.get("cost_center") or - get_default_cost_center(item_row, company = self.company)) + se_child.cost_center = item_row.get("cost_center") or get_default_cost_center( + item_row, company=self.company + ) se_child.is_finished_item = item_row.get("is_finished_item", 0) se_child.is_scrap_item = item_row.get("is_scrap_item", 0) se_child.is_process_loss = item_row.get("is_process_loss", 0) - for field in ["idx", "po_detail", "original_item", "expense_account", - "description", "item_name", "serial_no", "batch_no", "allow_zero_valuation_rate"]: + for field in [ + "idx", + "po_detail", + "original_item", + "expense_account", + "description", + "item_name", + "serial_no", + "batch_no", + "allow_zero_valuation_rate", + ]: if item_row.get(field): se_child.set(field, item_row.get(field)) - if se_child.s_warehouse==None: + if se_child.s_warehouse == None: se_child.s_warehouse = self.from_warehouse - if se_child.t_warehouse==None: + if se_child.t_warehouse == None: se_child.t_warehouse = self.to_warehouse # in stock uom se_child.conversion_factor = flt(item_row.get("conversion_factor")) or 1 - se_child.transfer_qty = flt(item_row["qty"]*se_child.conversion_factor, se_child.precision("qty")) + se_child.transfer_qty = flt( + item_row["qty"] * se_child.conversion_factor, se_child.precision("qty") + ) - se_child.bom_no = bom_no # to be assigned for finished item + se_child.bom_no = bom_no # to be assigned for finished item se_child.job_card_item = item_row.get("job_card_item") if self.get("job_card") else None def validate_with_material_request(self): for item in self.get("items"): material_request = item.material_request or None material_request_item = item.material_request_item or None - if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: - parent_se = frappe.get_value("Stock Entry Detail", item.ste_detail, ['material_request','material_request_item'],as_dict=True) + if self.purpose == "Material Transfer" and self.outgoing_stock_entry: + parent_se = frappe.get_value( + "Stock Entry Detail", + item.ste_detail, + ["material_request", "material_request_item"], + as_dict=True, + ) if parent_se: material_request = parent_se.material_request material_request_item = parent_se.material_request_item if material_request: - mreq_item = frappe.db.get_value("Material Request Item", + mreq_item = frappe.db.get_value( + "Material Request Item", {"name": material_request_item, "parent": material_request}, - ["item_code", "warehouse", "idx"], as_dict=True) + ["item_code", "warehouse", "idx"], + as_dict=True, + ) if mreq_item.item_code != item.item_code: - frappe.throw(_("Item for row {0} does not match Material Request").format(item.idx), - frappe.MappingMismatchError) + frappe.throw( + _("Item for row {0} does not match Material Request").format(item.idx), + frappe.MappingMismatchError, + ) elif self.purpose == "Material Transfer" and self.add_to_transit: continue def validate_batch(self): - if self.purpose in ["Material Transfer for Manufacture", "Manufacture", "Repack", "Send to Subcontractor"]: + if self.purpose in [ + "Material Transfer for Manufacture", + "Manufacture", + "Repack", + "Send to Subcontractor", + ]: for item in self.get("items"): if item.batch_no: disabled = frappe.db.get_value("Batch", item.batch_no, "disabled") @@ -1649,30 +1969,34 @@ class StockEntry(StockController): expiry_date = frappe.db.get_value("Batch", item.batch_no, "expiry_date") if expiry_date: if getdate(self.posting_date) > getdate(expiry_date): - frappe.throw(_("Batch {0} of Item {1} has expired.") - .format(item.batch_no, item.item_code)) + frappe.throw(_("Batch {0} of Item {1} has expired.").format(item.batch_no, item.item_code)) else: - frappe.throw(_("Batch {0} of Item {1} is disabled.") - .format(item.batch_no, item.item_code)) + frappe.throw(_("Batch {0} of Item {1} is disabled.").format(item.batch_no, item.item_code)) def update_purchase_order_supplied_items(self): - if (self.purchase_order and - (self.purpose in ['Send to Subcontractor', 'Material Transfer'] or self.is_return)): + if self.purchase_order and ( + self.purpose in ["Send to Subcontractor", "Material Transfer"] or self.is_return + ): - #Get PO Supplied Items Details - item_wh = frappe._dict(frappe.db.sql(""" + # Get PO Supplied Items Details + item_wh = frappe._dict( + frappe.db.sql( + """ select rm_item_code, reserve_warehouse from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup where po.name = poitemsup.parent - and po.name = %s""", self.purchase_order)) + and po.name = %s""", + self.purchase_order, + ) + ) supplied_items = get_supplied_items(self.purchase_order) for name, item in supplied_items.items(): - frappe.db.set_value('Purchase Order Item Supplied', name, item) + frappe.db.set_value("Purchase Order Item Supplied", name, item) - #Update reserved sub contracted quantity in bin based on Supplied Item Details and + # Update reserved sub contracted quantity in bin based on Supplied Item Details and for d in self.get("items"): - item_code = d.get('original_item') or d.get('item_code') + item_code = d.get("original_item") or d.get("item_code") reserve_warehouse = item_wh.get(item_code) if not (reserve_warehouse and item_code): continue @@ -1680,12 +2004,17 @@ class StockEntry(StockController): stock_bin.update_reserved_qty_for_sub_contracting() def update_so_in_serial_number(self): - so_name, item_code = frappe.db.get_value("Work Order", self.work_order, ["sales_order", "production_item"]) + so_name, item_code = frappe.db.get_value( + "Work Order", self.work_order, ["sales_order", "production_item"] + ) if so_name and item_code: qty_to_reserve = get_reserved_qty_for_so(so_name, item_code) if qty_to_reserve: - reserved_qty = frappe.db.sql("""select count(name) from `tabSerial No` where item_code=%s and - sales_order=%s""", (item_code, so_name)) + reserved_qty = frappe.db.sql( + """select count(name) from `tabSerial No` where item_code=%s and + sales_order=%s""", + (item_code, so_name), + ) if reserved_qty and reserved_qty[0][0]: qty_to_reserve -= reserved_qty[0][0] if qty_to_reserve > 0: @@ -1696,7 +2025,7 @@ class StockEntry(StockController): for serial_no in serial_nos: if qty_to_reserve > 0: frappe.db.set_value("Serial No", serial_no, "sales_order", so_name) - qty_to_reserve -=1 + qty_to_reserve -= 1 def validate_reserved_serial_no_consumption(self): for item in self.items: @@ -1704,13 +2033,14 @@ class StockEntry(StockController): for sr in get_serial_nos(item.serial_no): sales_order = frappe.db.get_value("Serial No", sr, "sales_order") if sales_order: - msg = (_("(Serial No: {0}) cannot be consumed as it's reserverd to fullfill Sales Order {1}.") - .format(sr, sales_order)) + msg = _( + "(Serial No: {0}) cannot be consumed as it's reserverd to fullfill Sales Order {1}." + ).format(sr, sales_order) frappe.throw(_("Item {0} {1}").format(item.item_code, msg)) def update_transferred_qty(self): - if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: + if self.purpose == "Material Transfer" and self.outgoing_stock_entry: stock_entries = {} stock_entries_child_list = [] for d in self.items: @@ -1718,70 +2048,87 @@ class StockEntry(StockController): continue stock_entries_child_list.append(d.ste_detail) - transferred_qty = frappe.get_all("Stock Entry Detail", fields = ["sum(qty) as qty"], - filters = { 'against_stock_entry': d.against_stock_entry, - 'ste_detail': d.ste_detail,'docstatus': 1}) + transferred_qty = frappe.get_all( + "Stock Entry Detail", + fields=["sum(qty) as qty"], + filters={ + "against_stock_entry": d.against_stock_entry, + "ste_detail": d.ste_detail, + "docstatus": 1, + }, + ) - stock_entries[(d.against_stock_entry, d.ste_detail)] = (transferred_qty[0].qty - if transferred_qty and transferred_qty[0] else 0.0) or 0.0 + stock_entries[(d.against_stock_entry, d.ste_detail)] = ( + transferred_qty[0].qty if transferred_qty and transferred_qty[0] else 0.0 + ) or 0.0 - if not stock_entries: return None + if not stock_entries: + return None - cond = '' + cond = "" for data, transferred_qty in stock_entries.items(): cond += """ WHEN (parent = %s and name = %s) THEN %s - """ %(frappe.db.escape(data[0]), frappe.db.escape(data[1]), transferred_qty) + """ % ( + frappe.db.escape(data[0]), + frappe.db.escape(data[1]), + transferred_qty, + ) if stock_entries_child_list: - frappe.db.sql(""" UPDATE `tabStock Entry Detail` + frappe.db.sql( + """ UPDATE `tabStock Entry Detail` SET transferred_qty = CASE {cond} END WHERE - name in ({ste_details}) """.format(cond=cond, - ste_details = ','.join(['%s'] * len(stock_entries_child_list))), - tuple(stock_entries_child_list)) + name in ({ste_details}) """.format( + cond=cond, ste_details=",".join(["%s"] * len(stock_entries_child_list)) + ), + tuple(stock_entries_child_list), + ) args = { - 'source_dt': 'Stock Entry Detail', - 'target_field': 'transferred_qty', - 'target_ref_field': 'qty', - 'target_dt': 'Stock Entry Detail', - 'join_field': 'ste_detail', - 'target_parent_dt': 'Stock Entry', - 'target_parent_field': 'per_transferred', - 'source_field': 'qty', - 'percent_join_field': 'against_stock_entry' + "source_dt": "Stock Entry Detail", + "target_field": "transferred_qty", + "target_ref_field": "qty", + "target_dt": "Stock Entry Detail", + "join_field": "ste_detail", + "target_parent_dt": "Stock Entry", + "target_parent_field": "per_transferred", + "source_field": "qty", + "percent_join_field": "against_stock_entry", } self._update_percent_field_in_targets(args, update_modified=True) def update_quality_inspection(self): if self.inspection_required: - reference_type = reference_name = '' + reference_type = reference_name = "" if self.docstatus == 1: reference_name = self.name - reference_type = 'Stock Entry' + reference_type = "Stock Entry" for d in self.items: if d.quality_inspection: - frappe.db.set_value("Quality Inspection", d.quality_inspection, { - 'reference_type': reference_type, - 'reference_name': reference_name - }) + frappe.db.set_value( + "Quality Inspection", + d.quality_inspection, + {"reference_type": reference_type, "reference_name": reference_name}, + ) + def set_material_request_transfer_status(self, status): material_requests = [] if self.outgoing_stock_entry: - parent_se = frappe.get_value("Stock Entry", self.outgoing_stock_entry, 'add_to_transit') + parent_se = frappe.get_value("Stock Entry", self.outgoing_stock_entry, "add_to_transit") for item in self.items: material_request = item.material_request or None if self.purpose == "Material Transfer" and material_request not in material_requests: if self.outgoing_stock_entry and parent_se: - material_request = frappe.get_value("Stock Entry Detail", item.ste_detail, 'material_request') + material_request = frappe.get_value("Stock Entry Detail", item.ste_detail, "material_request") if material_request and material_request not in material_requests: material_requests.append(material_request) - frappe.db.set_value('Material Request', material_request, 'transfer_status', status) + frappe.db.set_value("Material Request", material_request, "transfer_status", status) def update_items_for_process_loss(self): process_loss_dict = {} @@ -1789,7 +2136,9 @@ class StockEntry(StockController): if not d.is_process_loss: continue - scrap_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_scrap_warehouse") + scrap_warehouse = frappe.db.get_single_value( + "Manufacturing Settings", "default_scrap_warehouse" + ) if scrap_warehouse is not None: d.t_warehouse = scrap_warehouse d.is_scrap_item = 0 @@ -1814,14 +2163,22 @@ class StockEntry(StockController): for row in self.items: if row.is_finished_item and row.item_code == self.pro_doc.production_item: if args.get("serial_no"): - row.serial_no = '\n'.join(args["serial_no"][0: cint(row.qty)]) + row.serial_no = "\n".join(args["serial_no"][0 : cint(row.qty)]) def get_serial_nos_for_fg(self, args): - fields = ["`tabStock Entry`.`name`", "`tabStock Entry Detail`.`qty`", - "`tabStock Entry Detail`.`serial_no`", "`tabStock Entry Detail`.`batch_no`"] + fields = [ + "`tabStock Entry`.`name`", + "`tabStock Entry Detail`.`qty`", + "`tabStock Entry Detail`.`serial_no`", + "`tabStock Entry Detail`.`batch_no`", + ] - filters = [["Stock Entry","work_order","=",self.work_order], ["Stock Entry","purpose","=","Manufacture"], - ["Stock Entry","docstatus","=",1], ["Stock Entry Detail","item_code","=",self.pro_doc.production_item]] + filters = [ + ["Stock Entry", "work_order", "=", self.work_order], + ["Stock Entry", "purpose", "=", "Manufacture"], + ["Stock Entry", "docstatus", "=", 1], + ["Stock Entry Detail", "item_code", "=", self.pro_doc.production_item], + ] stock_entries = frappe.get_all("Stock Entry", fields=fields, filters=filters) @@ -1836,85 +2193,98 @@ class StockEntry(StockController): return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos))) + @frappe.whitelist() def move_sample_to_retention_warehouse(company, items): if isinstance(items, str): items = json.loads(items) - retention_warehouse = frappe.db.get_single_value('Stock Settings', 'sample_retention_warehouse') + retention_warehouse = frappe.db.get_single_value("Stock Settings", "sample_retention_warehouse") stock_entry = frappe.new_doc("Stock Entry") stock_entry.company = company stock_entry.purpose = "Material Transfer" stock_entry.set_stock_entry_type() for item in items: - if item.get('sample_quantity') and item.get('batch_no'): - sample_quantity = validate_sample_quantity(item.get('item_code'), item.get('sample_quantity'), - item.get('transfer_qty') or item.get('qty'), item.get('batch_no')) + if item.get("sample_quantity") and item.get("batch_no"): + sample_quantity = validate_sample_quantity( + item.get("item_code"), + item.get("sample_quantity"), + item.get("transfer_qty") or item.get("qty"), + item.get("batch_no"), + ) if sample_quantity: - sample_serial_nos = '' - if item.get('serial_no'): - serial_nos = (item.get('serial_no')).split() - if serial_nos and len(serial_nos) > item.get('sample_quantity'): - serial_no_list = serial_nos[:-(len(serial_nos)-item.get('sample_quantity'))] - sample_serial_nos = '\n'.join(serial_no_list) + sample_serial_nos = "" + if item.get("serial_no"): + serial_nos = (item.get("serial_no")).split() + if serial_nos and len(serial_nos) > item.get("sample_quantity"): + serial_no_list = serial_nos[: -(len(serial_nos) - item.get("sample_quantity"))] + sample_serial_nos = "\n".join(serial_no_list) - stock_entry.append("items", { - "item_code": item.get('item_code'), - "s_warehouse": item.get('t_warehouse'), - "t_warehouse": retention_warehouse, - "qty": item.get('sample_quantity'), - "basic_rate": item.get('valuation_rate'), - 'uom': item.get('uom'), - 'stock_uom': item.get('stock_uom'), - "conversion_factor": 1.0, - "serial_no": sample_serial_nos, - 'batch_no': item.get('batch_no') - }) - if stock_entry.get('items'): + stock_entry.append( + "items", + { + "item_code": item.get("item_code"), + "s_warehouse": item.get("t_warehouse"), + "t_warehouse": retention_warehouse, + "qty": item.get("sample_quantity"), + "basic_rate": item.get("valuation_rate"), + "uom": item.get("uom"), + "stock_uom": item.get("stock_uom"), + "conversion_factor": 1.0, + "serial_no": sample_serial_nos, + "batch_no": item.get("batch_no"), + }, + ) + if stock_entry.get("items"): return stock_entry.as_dict() + @frappe.whitelist() def make_stock_in_entry(source_name, target_doc=None): - def set_missing_values(source, target): target.set_stock_entry_type() def update_item(source_doc, target_doc, source_parent): - target_doc.t_warehouse = '' + target_doc.t_warehouse = "" - if source_doc.material_request_item and source_doc.material_request : - add_to_transit = frappe.db.get_value('Stock Entry', source_name, 'add_to_transit') + if source_doc.material_request_item and source_doc.material_request: + add_to_transit = frappe.db.get_value("Stock Entry", source_name, "add_to_transit") if add_to_transit: - warehouse = frappe.get_value('Material Request Item', source_doc.material_request_item, 'warehouse') + warehouse = frappe.get_value( + "Material Request Item", source_doc.material_request_item, "warehouse" + ) target_doc.t_warehouse = warehouse target_doc.s_warehouse = source_doc.t_warehouse target_doc.qty = source_doc.qty - source_doc.transferred_qty - doclist = get_mapped_doc("Stock Entry", source_name, { - "Stock Entry": { - "doctype": "Stock Entry", - "field_map": { - "name": "outgoing_stock_entry" + doclist = get_mapped_doc( + "Stock Entry", + source_name, + { + "Stock Entry": { + "doctype": "Stock Entry", + "field_map": {"name": "outgoing_stock_entry"}, + "validation": {"docstatus": ["=", 1]}, }, - "validation": { - "docstatus": ["=", 1] - } - }, - "Stock Entry Detail": { - "doctype": "Stock Entry Detail", - "field_map": { - "name": "ste_detail", - "parent": "against_stock_entry", - "serial_no": "serial_no", - "batch_no": "batch_no" + "Stock Entry Detail": { + "doctype": "Stock Entry Detail", + "field_map": { + "name": "ste_detail", + "parent": "against_stock_entry", + "serial_no": "serial_no", + "batch_no": "batch_no", + }, + "postprocess": update_item, + "condition": lambda doc: flt(doc.qty) - flt(doc.transferred_qty) > 0.01, }, - "postprocess": update_item, - "condition": lambda doc: flt(doc.qty) - flt(doc.transferred_qty) > 0.01 }, - }, target_doc, set_missing_values) + target_doc, + set_missing_values, + ) return doclist + @frappe.whitelist() def get_work_order_details(work_order, company): work_order = frappe.get_doc("Work Order", work_order) @@ -1926,9 +2296,10 @@ def get_work_order_details(work_order, company): "use_multi_level_bom": work_order.use_multi_level_bom, "wip_warehouse": work_order.wip_warehouse, "fg_warehouse": work_order.fg_warehouse, - "fg_completed_qty": pending_qty_to_produce + "fg_completed_qty": pending_qty_to_produce, } + def get_operating_cost_per_unit(work_order=None, bom_no=None): operating_cost_per_unit = 0 if work_order: @@ -1947,54 +2318,78 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None): if bom.quantity: operating_cost_per_unit = flt(bom.operating_cost) / flt(bom.quantity) - if work_order and work_order.produced_qty and cint(frappe.db.get_single_value('Manufacturing Settings', - 'add_corrective_operation_cost_in_finished_good_valuation')): - operating_cost_per_unit += flt(work_order.corrective_operation_cost) / flt(work_order.produced_qty) + if ( + work_order + and work_order.produced_qty + and cint( + frappe.db.get_single_value( + "Manufacturing Settings", "add_corrective_operation_cost_in_finished_good_valuation" + ) + ) + ): + operating_cost_per_unit += flt(work_order.corrective_operation_cost) / flt( + work_order.produced_qty + ) return operating_cost_per_unit + def get_used_alternative_items(purchase_order=None, work_order=None): cond = "" if purchase_order: - cond = "and ste.purpose = 'Send to Subcontractor' and ste.purchase_order = '{0}'".format(purchase_order) + cond = "and ste.purpose = 'Send to Subcontractor' and ste.purchase_order = '{0}'".format( + purchase_order + ) elif work_order: - cond = "and ste.purpose = 'Material Transfer for Manufacture' and ste.work_order = '{0}'".format(work_order) + cond = "and ste.purpose = 'Material Transfer for Manufacture' and ste.work_order = '{0}'".format( + work_order + ) - if not cond: return {} + if not cond: + return {} used_alternative_items = {} - data = frappe.db.sql(""" select sted.original_item, sted.uom, sted.conversion_factor, + data = frappe.db.sql( + """ select sted.original_item, sted.uom, sted.conversion_factor, sted.item_code, sted.item_name, sted.conversion_factor,sted.stock_uom, sted.description from `tabStock Entry` ste, `tabStock Entry Detail` sted where sted.parent = ste.name and ste.docstatus = 1 and sted.original_item != sted.item_code - {0} """.format(cond), as_dict=1) + {0} """.format( + cond + ), + as_dict=1, + ) for d in data: used_alternative_items[d.original_item] = d return used_alternative_items + def get_valuation_rate_for_finished_good_entry(work_order): - work_order_qty = flt(frappe.get_cached_value("Work Order", - work_order, 'material_transferred_for_manufacturing')) + work_order_qty = flt( + frappe.get_cached_value("Work Order", work_order, "material_transferred_for_manufacturing") + ) field = "(SUM(total_outgoing_value) / %s) as valuation_rate" % (work_order_qty) - stock_data = frappe.get_all("Stock Entry", - fields = field, - filters = { + stock_data = frappe.get_all( + "Stock Entry", + fields=field, + filters={ "docstatus": 1, "purpose": "Material Transfer for Manufacture", - "work_order": work_order - } + "work_order": work_order, + }, ) if stock_data: return stock_data[0].valuation_rate + @frappe.whitelist() def get_uom_details(item_code, uom, qty): """Returns dict `{"conversion_factor": [value], "transfer_qty": qty * [value]}` @@ -2003,24 +2398,31 @@ def get_uom_details(item_code, uom, qty): conversion_factor = get_conversion_factor(item_code, uom).get("conversion_factor") if not conversion_factor: - frappe.msgprint(_("UOM coversion factor required for UOM: {0} in Item: {1}") - .format(uom, item_code)) - ret = {'uom' : ''} + frappe.msgprint( + _("UOM coversion factor required for UOM: {0} in Item: {1}").format(uom, item_code) + ) + ret = {"uom": ""} else: ret = { - 'conversion_factor' : flt(conversion_factor), - 'transfer_qty' : flt(qty) * flt(conversion_factor) + "conversion_factor": flt(conversion_factor), + "transfer_qty": flt(qty) * flt(conversion_factor), } return ret + @frappe.whitelist() def get_expired_batch_items(): - return frappe.db.sql("""select b.item, sum(sle.actual_qty) as qty, sle.batch_no, sle.warehouse, sle.stock_uom\ + return frappe.db.sql( + """select b.item, sum(sle.actual_qty) as qty, sle.batch_no, sle.warehouse, sle.stock_uom\ from `tabBatch` b, `tabStock Ledger Entry` sle where b.expiry_date <= %s and b.expiry_date is not NULL and b.batch_id = sle.batch_no and sle.is_cancelled = 0 - group by sle.warehouse, sle.item_code, sle.batch_no""",(nowdate()), as_dict=1) + group by sle.warehouse, sle.item_code, sle.batch_no""", + (nowdate()), + as_dict=1, + ) + @frappe.whitelist() def get_warehouse_details(args): @@ -2031,51 +2433,73 @@ def get_warehouse_details(args): ret = {} if args.warehouse and args.item_code: - args.update({ - "posting_date": args.posting_date, - "posting_time": args.posting_time, - }) + args.update( + { + "posting_date": args.posting_date, + "posting_time": args.posting_time, + } + ) ret = { - "actual_qty" : get_previous_sle(args).get("qty_after_transaction") or 0, - "basic_rate" : get_incoming_rate(args) + "actual_qty": get_previous_sle(args).get("qty_after_transaction") or 0, + "basic_rate": get_incoming_rate(args), } return ret + @frappe.whitelist() -def validate_sample_quantity(item_code, sample_quantity, qty, batch_no = None): +def validate_sample_quantity(item_code, sample_quantity, qty, batch_no=None): if cint(qty) < cint(sample_quantity): - frappe.throw(_("Sample quantity {0} cannot be more than received quantity {1}").format(sample_quantity, qty)) - retention_warehouse = frappe.db.get_single_value('Stock Settings', 'sample_retention_warehouse') + frappe.throw( + _("Sample quantity {0} cannot be more than received quantity {1}").format(sample_quantity, qty) + ) + retention_warehouse = frappe.db.get_single_value("Stock Settings", "sample_retention_warehouse") retainted_qty = 0 if batch_no: retainted_qty = get_batch_qty(batch_no, retention_warehouse, item_code) - max_retain_qty = frappe.get_value('Item', item_code, 'sample_quantity') + max_retain_qty = frappe.get_value("Item", item_code, "sample_quantity") if retainted_qty >= max_retain_qty: - frappe.msgprint(_("Maximum Samples - {0} have already been retained for Batch {1} and Item {2} in Batch {3}."). - format(retainted_qty, batch_no, item_code, batch_no), alert=True) + frappe.msgprint( + _( + "Maximum Samples - {0} have already been retained for Batch {1} and Item {2} in Batch {3}." + ).format(retainted_qty, batch_no, item_code, batch_no), + alert=True, + ) sample_quantity = 0 - qty_diff = max_retain_qty-retainted_qty + qty_diff = max_retain_qty - retainted_qty if cint(sample_quantity) > cint(qty_diff): - frappe.msgprint(_("Maximum Samples - {0} can be retained for Batch {1} and Item {2}."). - format(max_retain_qty, batch_no, item_code), alert=True) + frappe.msgprint( + _("Maximum Samples - {0} can be retained for Batch {1} and Item {2}.").format( + max_retain_qty, batch_no, item_code + ), + alert=True, + ) sample_quantity = qty_diff return sample_quantity -def get_supplied_items(purchase_order): - fields = ['`tabStock Entry Detail`.`transfer_qty`', '`tabStock Entry`.`is_return`', - '`tabStock Entry Detail`.`po_detail`', '`tabStock Entry Detail`.`item_code`'] - filters = [['Stock Entry', 'docstatus', '=', 1], ['Stock Entry', 'purchase_order', '=', purchase_order]] +def get_supplied_items(purchase_order): + fields = [ + "`tabStock Entry Detail`.`transfer_qty`", + "`tabStock Entry`.`is_return`", + "`tabStock Entry Detail`.`po_detail`", + "`tabStock Entry Detail`.`item_code`", + ] + + filters = [ + ["Stock Entry", "docstatus", "=", 1], + ["Stock Entry", "purchase_order", "=", purchase_order], + ] supplied_item_details = {} - for row in frappe.get_all('Stock Entry', fields = fields, filters = filters): + for row in frappe.get_all("Stock Entry", fields=fields, filters=filters): if not row.po_detail: continue key = row.po_detail if key not in supplied_item_details: - supplied_item_details.setdefault(key, - frappe._dict({'supplied_qty': 0, 'returned_qty':0, 'total_supplied_qty':0})) + supplied_item_details.setdefault( + key, frappe._dict({"supplied_qty": 0, "returned_qty": 0, "total_supplied_qty": 0}) + ) supplied_item = supplied_item_details[key] @@ -2084,6 +2508,8 @@ def get_supplied_items(purchase_order): else: supplied_item.supplied_qty += row.transfer_qty - supplied_item.total_supplied_qty = flt(supplied_item.supplied_qty) - flt(supplied_item.returned_qty) + supplied_item.total_supplied_qty = flt(supplied_item.supplied_qty) - flt( + supplied_item.returned_qty + ) return supplied_item_details diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py index 17266ad059..b3df7286ea 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py @@ -10,7 +10,7 @@ import erpnext @frappe.whitelist() def make_stock_entry(**args): - '''Helper function to make a Stock Entry + """Helper function to make a Stock Entry :item_code: Item to be moved :qty: Qty to be moved @@ -25,16 +25,16 @@ def make_stock_entry(**args): :purpose: Optional :do_not_save: Optional flag :do_not_submit: Optional flag - ''' + """ def process_serial_numbers(serial_nos_list): serial_nos_list = [ - '\n'.join(serial_num['serial_no'] for serial_num in serial_nos_list if serial_num.serial_no) + "\n".join(serial_num["serial_no"] for serial_num in serial_nos_list if serial_num.serial_no) ] - uniques = list(set(serial_nos_list[0].split('\n'))) + uniques = list(set(serial_nos_list[0].split("\n"))) - return '\n'.join(uniques) + return "\n".join(uniques) s = frappe.new_doc("Stock Entry") args = frappe._dict(args) @@ -60,7 +60,7 @@ def make_stock_entry(**args): s.apply_putaway_rule = args.apply_putaway_rule if isinstance(args.qty, str): - if '.' in args.qty: + if "." in args.qty: args.qty = flt(args.qty) else: args.qty = cint(args.qty) @@ -79,16 +79,16 @@ def make_stock_entry(**args): # company if not args.company: if args.source: - args.company = frappe.db.get_value('Warehouse', args.source, 'company') + args.company = frappe.db.get_value("Warehouse", args.source, "company") elif args.target: - args.company = frappe.db.get_value('Warehouse', args.target, 'company') + args.company = frappe.db.get_value("Warehouse", args.target, "company") # set vales from test if frappe.flags.in_test: if not args.company: - args.company = '_Test Company' + args.company = "_Test Company" if not args.item: - args.item = '_Test Item' + args.item = "_Test Item" s.company = args.company or erpnext.get_default_company() s.purchase_receipt_no = args.purchase_receipt_no @@ -96,40 +96,40 @@ def make_stock_entry(**args): s.sales_invoice_no = args.sales_invoice_no s.is_opening = args.is_opening or "No" if not args.cost_center: - args.cost_center = frappe.get_value('Company', s.company, 'cost_center') + args.cost_center = frappe.get_value("Company", s.company, "cost_center") if not args.expense_account and s.is_opening == "No": - args.expense_account = frappe.get_value('Company', s.company, 'stock_adjustment_account') + args.expense_account = frappe.get_value("Company", s.company, "stock_adjustment_account") # We can find out the serial number using the batch source document serial_number = args.serial_no if not args.serial_no and args.qty and args.batch_no: serial_number_list = frappe.get_list( - doctype='Stock Ledger Entry', - fields=['serial_no'], - filters={ - 'batch_no': args.batch_no, - 'warehouse': args.from_warehouse - } + doctype="Stock Ledger Entry", + fields=["serial_no"], + filters={"batch_no": args.batch_no, "warehouse": args.from_warehouse}, ) serial_number = process_serial_numbers(serial_number_list) args.serial_no = serial_number - s.append("items", { - "item_code": args.item, - "s_warehouse": args.source, - "t_warehouse": args.target, - "qty": args.qty, - "basic_rate": args.rate or args.basic_rate, - "conversion_factor": args.conversion_factor or 1.0, - "transfer_qty": flt(args.qty) * (flt(args.conversion_factor) or 1.0), - "serial_no": args.serial_no, - 'batch_no': args.batch_no, - 'cost_center': args.cost_center, - 'expense_account': args.expense_account - }) + s.append( + "items", + { + "item_code": args.item, + "s_warehouse": args.source, + "t_warehouse": args.target, + "qty": args.qty, + "basic_rate": args.rate or args.basic_rate, + "conversion_factor": args.conversion_factor or 1.0, + "transfer_qty": flt(args.qty) * (flt(args.conversion_factor) or 1.0), + "serial_no": args.serial_no, + "batch_no": args.batch_no, + "cost_center": args.cost_center, + "expense_account": args.expense_account, + }, + ) s.set_stock_entry_type() if not args.do_not_save: diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 54c0e43c5e..aeedcd1847 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -38,9 +38,14 @@ def get_sle(**args): condition += "`{0}`=%s".format(key) values.append(value) - return frappe.db.sql("""select * from `tabStock Ledger Entry` %s - order by timestamp(posting_date, posting_time) desc, creation desc limit 1"""% condition, - values, as_dict=1) + return frappe.db.sql( + """select * from `tabStock Ledger Entry` %s + order by timestamp(posting_date, posting_time) desc, creation desc limit 1""" + % condition, + values, + as_dict=1, + ) + class TestStockEntry(FrappeTestCase): def tearDown(self): @@ -53,36 +58,37 @@ class TestStockEntry(FrappeTestCase): item_code = "_Test Item 2" warehouse = "_Test Warehouse - _TC" - create_stock_reconciliation(item_code="_Test Item 2", warehouse="_Test Warehouse - _TC", - qty=0, rate=100) + create_stock_reconciliation( + item_code="_Test Item 2", warehouse="_Test Warehouse - _TC", qty=0, rate=100 + ) make_stock_entry(item_code=item_code, target=warehouse, qty=1, basic_rate=10) - sle = get_sle(item_code = item_code, warehouse = warehouse)[0] + sle = get_sle(item_code=item_code, warehouse=warehouse)[0] self.assertEqual([[1, 10]], frappe.safe_eval(sle.stock_queue)) # negative qty make_stock_entry(item_code=item_code, source=warehouse, qty=2, basic_rate=10) - sle = get_sle(item_code = item_code, warehouse = warehouse)[0] + sle = get_sle(item_code=item_code, warehouse=warehouse)[0] self.assertEqual([[-1, 10]], frappe.safe_eval(sle.stock_queue)) # further negative make_stock_entry(item_code=item_code, source=warehouse, qty=1) - sle = get_sle(item_code = item_code, warehouse = warehouse)[0] + sle = get_sle(item_code=item_code, warehouse=warehouse)[0] self.assertEqual([[-2, 10]], frappe.safe_eval(sle.stock_queue)) # move stock to positive make_stock_entry(item_code=item_code, target=warehouse, qty=3, basic_rate=20) - sle = get_sle(item_code = item_code, warehouse = warehouse)[0] + sle = get_sle(item_code=item_code, warehouse=warehouse)[0] self.assertEqual([[1, 20]], frappe.safe_eval(sle.stock_queue)) # incoming entry with diff rate make_stock_entry(item_code=item_code, target=warehouse, qty=1, basic_rate=30) - sle = get_sle(item_code = item_code, warehouse = warehouse)[0] + sle = get_sle(item_code=item_code, warehouse=warehouse)[0] - self.assertEqual([[1, 20],[1, 30]], frappe.safe_eval(sle.stock_queue)) + self.assertEqual([[1, 20], [1, 30]], frappe.safe_eval(sle.stock_queue)) frappe.db.set_default("allow_negative_stock", 0) @@ -92,37 +98,48 @@ class TestStockEntry(FrappeTestCase): self._test_auto_material_request("_Test Item", material_request_type="Transfer") def test_auto_material_request_for_variant(self): - fields = [{'field_name': 'reorder_levels'}] + fields = [{"field_name": "reorder_levels"}] set_item_variant_settings(fields) make_item_variant() template = frappe.get_doc("Item", "_Test Variant Item") if not template.reorder_levels: - template.append('reorder_levels', { - "material_request_type": "Purchase", - "warehouse": "_Test Warehouse - _TC", - "warehouse_reorder_level": 20, - "warehouse_reorder_qty": 20 - }) + template.append( + "reorder_levels", + { + "material_request_type": "Purchase", + "warehouse": "_Test Warehouse - _TC", + "warehouse_reorder_level": 20, + "warehouse_reorder_qty": 20, + }, + ) template.save() self._test_auto_material_request("_Test Variant Item-S") def test_auto_material_request_for_warehouse_group(self): - self._test_auto_material_request("_Test Item Warehouse Group Wise Reorder", warehouse="_Test Warehouse Group-C1 - _TC") + self._test_auto_material_request( + "_Test Item Warehouse Group Wise Reorder", warehouse="_Test Warehouse Group-C1 - _TC" + ) - def _test_auto_material_request(self, item_code, material_request_type="Purchase", warehouse="_Test Warehouse - _TC"): + def _test_auto_material_request( + self, item_code, material_request_type="Purchase", warehouse="_Test Warehouse - _TC" + ): variant = frappe.get_doc("Item", item_code) - projected_qty, actual_qty = frappe.db.get_value("Bin", {"item_code": item_code, - "warehouse": warehouse}, ["projected_qty", "actual_qty"]) or [0, 0] + projected_qty, actual_qty = frappe.db.get_value( + "Bin", {"item_code": item_code, "warehouse": warehouse}, ["projected_qty", "actual_qty"] + ) or [0, 0] # stock entry reqd for auto-reorder - create_stock_reconciliation(item_code=item_code, warehouse=warehouse, - qty = actual_qty + abs(projected_qty) + 10, rate=100) + create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=actual_qty + abs(projected_qty) + 10, rate=100 + ) - projected_qty = frappe.db.get_value("Bin", {"item_code": item_code, - "warehouse": warehouse}, "projected_qty") or 0 + projected_qty = ( + frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "projected_qty") + or 0 + ) frappe.db.set_value("Stock Settings", None, "auto_indent", 1) @@ -133,6 +150,7 @@ class TestStockEntry(FrappeTestCase): variant.save() from erpnext.stock.reorder_item import reorder_item + mr_list = reorder_item() frappe.db.set_value("Stock Settings", None, "auto_indent", 0) @@ -145,65 +163,113 @@ class TestStockEntry(FrappeTestCase): self.assertTrue(item_code in items) def test_material_receipt_gl_entry(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") - mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1", company= company, - qty=50, basic_rate=100, expense_account="Stock Adjustment - TCP1") + mr = make_stock_entry( + item_code="_Test Item", + target="Stores - TCP1", + company=company, + qty=50, + basic_rate=100, + expense_account="Stock Adjustment - TCP1", + ) stock_in_hand_account = get_inventory_account(mr.company, mr.get("items")[0].t_warehouse) - self.check_stock_ledger_entries("Stock Entry", mr.name, - [["_Test Item", "Stores - TCP1", 50.0]]) + self.check_stock_ledger_entries("Stock Entry", mr.name, [["_Test Item", "Stores - TCP1", 50.0]]) - self.check_gl_entries("Stock Entry", mr.name, - sorted([ - [stock_in_hand_account, 5000.0, 0.0], - ["Stock Adjustment - TCP1", 0.0, 5000.0] - ]) + self.check_gl_entries( + "Stock Entry", + mr.name, + sorted([[stock_in_hand_account, 5000.0, 0.0], ["Stock Adjustment - TCP1", 0.0, 5000.0]]), ) mr.cancel() - self.assertTrue(frappe.db.sql("""select * from `tabStock Ledger Entry` - where voucher_type='Stock Entry' and voucher_no=%s""", mr.name)) + self.assertTrue( + frappe.db.sql( + """select * from `tabStock Ledger Entry` + where voucher_type='Stock Entry' and voucher_no=%s""", + mr.name, + ) + ) - self.assertTrue(frappe.db.sql("""select * from `tabGL Entry` - where voucher_type='Stock Entry' and voucher_no=%s""", mr.name)) + self.assertTrue( + frappe.db.sql( + """select * from `tabGL Entry` + where voucher_type='Stock Entry' and voucher_no=%s""", + mr.name, + ) + ) def test_material_issue_gl_entry(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') - make_stock_entry(item_code="_Test Item", target="Stores - TCP1", company= company, - qty=50, basic_rate=100, expense_account="Stock Adjustment - TCP1") + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") + make_stock_entry( + item_code="_Test Item", + target="Stores - TCP1", + company=company, + qty=50, + basic_rate=100, + expense_account="Stock Adjustment - TCP1", + ) - mi = make_stock_entry(item_code="_Test Item", source="Stores - TCP1", company=company, - qty=40, expense_account="Stock Adjustment - TCP1") + mi = make_stock_entry( + item_code="_Test Item", + source="Stores - TCP1", + company=company, + qty=40, + expense_account="Stock Adjustment - TCP1", + ) - self.check_stock_ledger_entries("Stock Entry", mi.name, - [["_Test Item", "Stores - TCP1", -40.0]]) + self.check_stock_ledger_entries("Stock Entry", mi.name, [["_Test Item", "Stores - TCP1", -40.0]]) stock_in_hand_account = get_inventory_account(mi.company, "Stores - TCP1") - stock_value_diff = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Stock Entry", - "voucher_no": mi.name}, "stock_value_difference")) + stock_value_diff = abs( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Stock Entry", "voucher_no": mi.name}, + "stock_value_difference", + ) + ) - self.check_gl_entries("Stock Entry", mi.name, - sorted([ - [stock_in_hand_account, 0.0, stock_value_diff], - ["Stock Adjustment - TCP1", stock_value_diff, 0.0] - ]) + self.check_gl_entries( + "Stock Entry", + mi.name, + sorted( + [ + [stock_in_hand_account, 0.0, stock_value_diff], + ["Stock Adjustment - TCP1", stock_value_diff, 0.0], + ] + ), ) mi.cancel() def test_material_transfer_gl_entry(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") - item_code = 'Hand Sanitizer - 001' - create_item(item_code =item_code, is_stock_item = 1, - is_purchase_item=1, opening_stock=1000, valuation_rate=10, company=company, warehouse="Stores - TCP1") + item_code = "Hand Sanitizer - 001" + create_item( + item_code=item_code, + is_stock_item=1, + is_purchase_item=1, + opening_stock=1000, + valuation_rate=10, + company=company, + warehouse="Stores - TCP1", + ) - mtn = make_stock_entry(item_code=item_code, source="Stores - TCP1", - target="Finished Goods - TCP1", qty=45, company=company) + mtn = make_stock_entry( + item_code=item_code, + source="Stores - TCP1", + target="Finished Goods - TCP1", + qty=45, + company=company, + ) - self.check_stock_ledger_entries("Stock Entry", mtn.name, - [[item_code, "Stores - TCP1", -45.0], [item_code, "Finished Goods - TCP1", 45.0]]) + self.check_stock_ledger_entries( + "Stock Entry", + mtn.name, + [[item_code, "Stores - TCP1", -45.0], [item_code, "Finished Goods - TCP1", 45.0]], + ) source_warehouse_account = get_inventory_account(mtn.company, mtn.get("items")[0].s_warehouse) @@ -211,18 +277,33 @@ class TestStockEntry(FrappeTestCase): 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, as_dict=1)) + self.assertFalse( + frappe.db.sql( + """select * from `tabGL Entry` + 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", - "voucher_no": mtn.name, "warehouse": "Stores - TCP1"}, "stock_value_difference")) + stock_value_diff = abs( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Stock Entry", "voucher_no": mtn.name, "warehouse": "Stores - TCP1"}, + "stock_value_difference", + ) + ) - self.check_gl_entries("Stock Entry", mtn.name, - sorted([ - [source_warehouse_account, 0.0, stock_value_diff], - [target_warehouse_account, stock_value_diff, 0.0], - ]) + self.check_gl_entries( + "Stock Entry", + mtn.name, + sorted( + [ + [source_warehouse_account, 0.0, stock_value_diff], + [target_warehouse_account, stock_value_diff, 0.0], + ] + ), ) mtn.cancel() @@ -239,20 +320,23 @@ class TestStockEntry(FrappeTestCase): repack.items[0].transfer_qty = 100.0 repack.items[1].qty = 50.0 - repack.append("items", { - "conversion_factor": 1.0, - "cost_center": "_Test Cost Center - _TC", - "doctype": "Stock Entry Detail", - "expense_account": "Stock Adjustment - _TC", - "basic_rate": 150, - "item_code": "_Test Item 2", - "parentfield": "items", - "qty": 50.0, - "stock_uom": "_Test UOM", - "t_warehouse": "_Test Warehouse - _TC", - "transfer_qty": 50.0, - "uom": "_Test UOM" - }) + repack.append( + "items", + { + "conversion_factor": 1.0, + "cost_center": "_Test Cost Center - _TC", + "doctype": "Stock Entry Detail", + "expense_account": "Stock Adjustment - _TC", + "basic_rate": 150, + "item_code": "_Test Item 2", + "parentfield": "items", + "qty": 50.0, + "stock_uom": "_Test UOM", + "t_warehouse": "_Test Warehouse - _TC", + "transfer_qty": 50.0, + "uom": "_Test UOM", + }, + ) repack.set_stock_entry_type() repack.insert() @@ -265,12 +349,13 @@ class TestStockEntry(FrappeTestCase): # must raise error if 0 fg in repack entry self.assertRaises(FinishedGoodError, repack.validate_finished_goods) - repack.delete() # teardown + repack.delete() # teardown def test_repack_no_change_in_valuation(self): 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", - qty=50, basic_rate=100) + make_stock_entry( + item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=50, basic_rate=100 + ) repack = frappe.copy_doc(test_records[3]) repack.posting_date = nowdate() @@ -279,76 +364,113 @@ class TestStockEntry(FrappeTestCase): repack.insert() repack.submit() - self.check_stock_ledger_entries("Stock Entry", repack.name, - [["_Test Item", "_Test Warehouse - _TC", -50.0], - ["_Test Item Home Desktop 100", "_Test Warehouse - _TC", 1]]) + self.check_stock_ledger_entries( + "Stock Entry", + repack.name, + [ + ["_Test Item", "_Test Warehouse - _TC", -50.0], + ["_Test Item Home Desktop 100", "_Test Warehouse - _TC", 1], + ], + ) - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Stock Entry' and voucher_no=%s - order by account desc""", repack.name, as_dict=1) + order by account desc""", + repack.name, + as_dict=1, + ) self.assertFalse(gl_entries) def test_repack_with_additional_costs(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") - make_stock_entry(item_code="_Test Item", target="Stores - TCP1", company= company, - qty=50, basic_rate=100, expense_account="Stock Adjustment - TCP1") + make_stock_entry( + item_code="_Test Item", + target="Stores - TCP1", + company=company, + qty=50, + basic_rate=100, + expense_account="Stock Adjustment - TCP1", + ) - - repack = make_stock_entry(company = company, purpose="Repack", do_not_save=True) + repack = make_stock_entry(company=company, purpose="Repack", do_not_save=True) repack.posting_date = nowdate() repack.posting_time = nowtime() - expenses_included_in_valuation = frappe.get_value("Company", company, "expenses_included_in_valuation") + expenses_included_in_valuation = frappe.get_value( + "Company", company, "expenses_included_in_valuation" + ) items = get_multiple_items() repack.items = [] for item in items: repack.append("items", item) - repack.set("additional_costs", [ - { - "expense_account": expenses_included_in_valuation, - "description": "Actual Operating Cost", - "amount": 1000 - }, - { - "expense_account": expenses_included_in_valuation, - "description": "Additional Operating Cost", - "amount": 200 - }, - ]) + repack.set( + "additional_costs", + [ + { + "expense_account": expenses_included_in_valuation, + "description": "Actual Operating Cost", + "amount": 1000, + }, + { + "expense_account": expenses_included_in_valuation, + "description": "Additional Operating Cost", + "amount": 200, + }, + ], + ) repack.set_stock_entry_type() repack.insert() repack.submit() stock_in_hand_account = get_inventory_account(repack.company, repack.get("items")[1].t_warehouse) - rm_stock_value_diff = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Stock Entry", - "voucher_no": repack.name, "item_code": "_Test Item"}, "stock_value_difference")) + rm_stock_value_diff = abs( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Stock Entry", "voucher_no": repack.name, "item_code": "_Test Item"}, + "stock_value_difference", + ) + ) - fg_stock_value_diff = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Stock Entry", - "voucher_no": repack.name, "item_code": "_Test Item Home Desktop 100"}, "stock_value_difference")) + fg_stock_value_diff = abs( + frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Stock Entry", + "voucher_no": repack.name, + "item_code": "_Test Item Home Desktop 100", + }, + "stock_value_difference", + ) + ) stock_value_diff = flt(fg_stock_value_diff - rm_stock_value_diff, 2) self.assertEqual(stock_value_diff, 1200) - self.check_gl_entries("Stock Entry", repack.name, - sorted([ - [stock_in_hand_account, 1200, 0.0], - ["Expenses Included In Valuation - TCP1", 0.0, 1200.0] - ]) + self.check_gl_entries( + "Stock Entry", + repack.name, + sorted( + [[stock_in_hand_account, 1200, 0.0], ["Expenses Included In Valuation - TCP1", 0.0, 1200.0]] + ), ) def check_stock_ledger_entries(self, voucher_type, voucher_no, expected_sle): expected_sle.sort(key=lambda x: x[1]) # check stock ledger entries - sle = frappe.db.sql("""select item_code, warehouse, actual_qty + sle = frappe.db.sql( + """select item_code, warehouse, actual_qty from `tabStock Ledger Entry` where voucher_type = %s and voucher_no = %s order by item_code, warehouse, actual_qty""", - (voucher_type, voucher_no), as_list=1) + (voucher_type, voucher_no), + as_list=1, + ) self.assertTrue(sle) sle.sort(key=lambda x: x[1]) @@ -360,9 +482,13 @@ class TestStockEntry(FrappeTestCase): def check_gl_entries(self, voucher_type, voucher_no, expected_gl_entries): expected_gl_entries.sort(key=lambda x: x[0]) - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type=%s and voucher_no=%s - order by account asc, debit asc""", (voucher_type, voucher_no), as_list=1) + order by account asc, debit asc""", + (voucher_type, voucher_no), + as_list=1, + ) self.assertTrue(gl_entries) gl_entries.sort(key=lambda x: x[0]) @@ -463,7 +589,7 @@ class TestStockEntry(FrappeTestCase): def test_serial_item_error(self): se, serial_nos = self.test_serial_by_series() - if not frappe.db.exists('Serial No', 'ABCD'): + if not frappe.db.exists("Serial No", "ABCD"): make_serialized_item(item_code="_Test Serialized Item", serial_no="ABCD\nEFGH") se = frappe.copy_doc(test_records[0]) @@ -493,10 +619,14 @@ class TestStockEntry(FrappeTestCase): se.set_stock_entry_type() se.insert() se.submit() - self.assertTrue(frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse 1 - _TC") + self.assertTrue( + frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse 1 - _TC" + ) se.cancel() - self.assertTrue(frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse - _TC") + self.assertTrue( + frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse - _TC" + ) def test_serial_warehouse_error(self): make_serialized_item(target_warehouse="_Test Warehouse 1 - _TC") @@ -524,14 +654,16 @@ class TestStockEntry(FrappeTestCase): self.assertFalse(frappe.db.get_value("Serial No", serial_no, "warehouse")) def test_warehouse_company_validation(self): - company = frappe.db.get_value('Warehouse', '_Test Warehouse 2 - _TC1', 'company') - frappe.get_doc("User", "test2@example.com")\ - .add_roles("Sales User", "Sales Manager", "Stock User", "Stock Manager") + company = frappe.db.get_value("Warehouse", "_Test Warehouse 2 - _TC1", "company") + frappe.get_doc("User", "test2@example.com").add_roles( + "Sales User", "Sales Manager", "Stock User", "Stock Manager" + ) frappe.set_user("test2@example.com") from erpnext.stock.utils import InvalidWarehouseCompany + st1 = frappe.copy_doc(test_records[0]) - st1.get("items")[0].t_warehouse="_Test Warehouse 2 - _TC1" + st1.get("items")[0].t_warehouse = "_Test Warehouse 2 - _TC1" st1.set_stock_entry_type() st1.insert() self.assertRaises(InvalidWarehouseCompany, st1.submit) @@ -545,14 +677,15 @@ class TestStockEntry(FrappeTestCase): test_user.add_roles("Sales User", "Sales Manager", "Stock User") test_user.remove_roles("Stock Manager", "System Manager") - frappe.get_doc("User", "test2@example.com")\ - .add_roles("Sales User", "Sales Manager", "Stock User", "Stock Manager") + frappe.get_doc("User", "test2@example.com").add_roles( + "Sales User", "Sales Manager", "Stock User", "Stock Manager" + ) st1 = frappe.copy_doc(test_records[0]) st1.company = "_Test Company 1" frappe.set_user("test@example.com") - st1.get("items")[0].t_warehouse="_Test Warehouse 2 - _TC1" + st1.get("items")[0].t_warehouse = "_Test Warehouse 2 - _TC1" self.assertRaises(frappe.PermissionError, st1.insert) test_user.add_roles("System Manager") @@ -560,7 +693,7 @@ class TestStockEntry(FrappeTestCase): frappe.set_user("test2@example.com") st1 = frappe.copy_doc(test_records[0]) st1.company = "_Test Company 1" - st1.get("items")[0].t_warehouse="_Test Warehouse 2 - _TC1" + st1.get("items")[0].t_warehouse = "_Test Warehouse 2 - _TC1" st1.get("items")[0].expense_account = "Stock Adjustment - _TC1" st1.get("items")[0].cost_center = "Main - _TC1" st1.set_stock_entry_type() @@ -574,14 +707,14 @@ class TestStockEntry(FrappeTestCase): remove_user_permission("Company", "_Test Company 1", "test2@example.com") def test_freeze_stocks(self): - frappe.db.set_value('Stock Settings', None,'stock_auth_role', '') + frappe.db.set_value("Stock Settings", None, "stock_auth_role", "") # test freeze_stocks_upto frappe.db.set_value("Stock Settings", None, "stock_frozen_upto", add_days(nowdate(), 5)) se = frappe.copy_doc(test_records[0]).insert() self.assertRaises(StockFreezeError, se.submit) - frappe.db.set_value("Stock Settings", None, "stock_frozen_upto", '') + frappe.db.set_value("Stock Settings", None, "stock_frozen_upto", "") # test freeze_stocks_upto_days frappe.db.set_value("Stock Settings", None, "stock_frozen_upto_days", -1) @@ -597,20 +730,24 @@ class TestStockEntry(FrappeTestCase): from erpnext.manufacturing.doctype.work_order.work_order import ( make_stock_entry as _make_stock_entry, ) - bom_no, bom_operation_cost = frappe.db.get_value("BOM", {"item": "_Test FG Item 2", - "is_default": 1, "docstatus": 1}, ["name", "operating_cost"]) + + bom_no, bom_operation_cost = frappe.db.get_value( + "BOM", {"item": "_Test FG Item 2", "is_default": 1, "docstatus": 1}, ["name", "operating_cost"] + ) 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": 1.0, - "stock_uom": "_Test UOM", - "wip_warehouse": "_Test Warehouse - _TC", - "additional_operating_cost": 1000 - }) + work_order.update( + { + "company": "_Test Company", + "fg_warehouse": "_Test Warehouse 1 - _TC", + "production_item": "_Test FG Item 2", + "bom_no": bom_no, + "qty": 1.0, + "stock_uom": "_Test UOM", + "wip_warehouse": "_Test Warehouse - _TC", + "additional_operating_cost": 1000, + } + ) work_order.insert() work_order.submit() @@ -623,37 +760,41 @@ class TestStockEntry(FrappeTestCase): for d in stock_entry.get("items"): if d.item_code != "_Test FG Item 2": rm_cost += flt(d.amount) - fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item 2", stock_entry.get("items")))[0].amount - self.assertEqual(fg_cost, - flt(rm_cost + bom_operation_cost + work_order.additional_operating_cost, 2)) + fg_cost = list(filter(lambda x: x.item_code == "_Test FG Item 2", stock_entry.get("items")))[ + 0 + ].amount + self.assertEqual( + fg_cost, flt(rm_cost + bom_operation_cost + work_order.additional_operating_cost, 2) + ) def test_work_order_manufacture_with_material_consumption(self): from erpnext.manufacturing.doctype.work_order.work_order import ( make_stock_entry as _make_stock_entry, ) + frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "1") - bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item", - "is_default": 1, "docstatus": 1}) + bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item", "is_default": 1, "docstatus": 1}) work_order = frappe.new_doc("Work Order") - work_order.update({ - "company": "_Test Company", - "fg_warehouse": "_Test Warehouse 1 - _TC", - "production_item": "_Test FG Item", - "bom_no": bom_no, - "qty": 1.0, - "stock_uom": "_Test UOM", - "wip_warehouse": "_Test Warehouse - _TC" - }) + work_order.update( + { + "company": "_Test Company", + "fg_warehouse": "_Test Warehouse 1 - _TC", + "production_item": "_Test FG Item", + "bom_no": bom_no, + "qty": 1.0, + "stock_uom": "_Test UOM", + "wip_warehouse": "_Test Warehouse - _TC", + } + ) work_order.insert() work_order.submit() - make_stock_entry(item_code="_Test Item", - target="Stores - _TC", qty=10, basic_rate=5000.0) - make_stock_entry(item_code="_Test Item Home Desktop 100", - target="Stores - _TC", qty=10, basic_rate=1000.0) - + make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=5000.0) + make_stock_entry( + item_code="_Test Item Home Desktop 100", target="Stores - _TC", qty=10, basic_rate=1000.0 + ) s = frappe.get_doc(_make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1)) for d in s.get("items"): @@ -665,13 +806,12 @@ class TestStockEntry(FrappeTestCase): s = frappe.get_doc(_make_stock_entry(work_order.name, "Manufacture", 1)) s.save() rm_cost = 0 - for d in s.get('items'): + for d in s.get("items"): if d.s_warehouse: rm_cost += d.amount - fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item", s.get("items")))[0].amount + fg_cost = list(filter(lambda x: x.item_code == "_Test FG Item", s.get("items")))[0].amount scrap_cost = list(filter(lambda x: x.is_scrap_item, s.get("items")))[0].amount - self.assertEqual(fg_cost, - flt(rm_cost - scrap_cost, 2)) + self.assertEqual(fg_cost, flt(rm_cost - scrap_cost, 2)) # When Stock Entry has only FG + Scrap s.items.pop(0) @@ -679,31 +819,34 @@ class TestStockEntry(FrappeTestCase): s.submit() rm_cost = 0 - for d in s.get('items'): + for d in s.get("items"): if d.s_warehouse: rm_cost += d.amount self.assertEqual(rm_cost, 0) expected_fg_cost = s.get_basic_rate_for_manufactured_item(1) - fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item", s.get("items")))[0].amount + fg_cost = list(filter(lambda x: x.item_code == "_Test FG Item", s.get("items")))[0].amount self.assertEqual(flt(fg_cost, 2), flt(expected_fg_cost, 2)) def test_variant_work_order(self): - bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item", - "is_default": 1, "docstatus": 1}) + bom_no = frappe.db.get_value( + "BOM", {"item": "_Test Variant Item", "is_default": 1, "docstatus": 1} + ) - make_item_variant() # make variant of _Test Variant Item if absent + make_item_variant() # make variant of _Test Variant Item if absent work_order = frappe.new_doc("Work Order") - work_order.update({ - "company": "_Test Company", - "fg_warehouse": "_Test Warehouse 1 - _TC", - "production_item": "_Test Variant Item-S", - "bom_no": bom_no, - "qty": 1.0, - "stock_uom": "_Test UOM", - "wip_warehouse": "_Test Warehouse - _TC", - "skip_transfer": 1 - }) + work_order.update( + { + "company": "_Test Company", + "fg_warehouse": "_Test Warehouse 1 - _TC", + "production_item": "_Test Variant Item-S", + "bom_no": bom_no, + "qty": 1.0, + "stock_uom": "_Test UOM", + "wip_warehouse": "_Test Warehouse - _TC", + "skip_transfer": 1, + } + ) work_order.insert() work_order.submit() @@ -717,19 +860,29 @@ class TestStockEntry(FrappeTestCase): s1 = make_serialized_item(target_warehouse="_Test Warehouse - _TC") serial_nos = s1.get("items")[0].serial_no - s2 = make_stock_entry(item_code="_Test Serialized Item With Series", source="_Test Warehouse - _TC", - qty=2, basic_rate=100, purpose="Repack", serial_no=serial_nos, do_not_save=True) + s2 = make_stock_entry( + item_code="_Test Serialized Item With Series", + source="_Test Warehouse - _TC", + qty=2, + basic_rate=100, + purpose="Repack", + serial_no=serial_nos, + do_not_save=True, + ) - s2.append("items", { - "item_code": "_Test Serialized Item", - "t_warehouse": "_Test Warehouse - _TC", - "qty": 2, - "basic_rate": 120, - "expense_account": "Stock Adjustment - _TC", - "conversion_factor": 1.0, - "cost_center": "_Test Cost Center - _TC", - "serial_no": serial_nos - }) + s2.append( + "items", + { + "item_code": "_Test Serialized Item", + "t_warehouse": "_Test Warehouse - _TC", + "qty": 2, + "basic_rate": 120, + "expense_account": "Stock Adjustment - _TC", + "conversion_factor": 1.0, + "cost_center": "_Test Cost Center - _TC", + "serial_no": serial_nos, + }, + ) s2.submit() s2.cancel() @@ -739,10 +892,15 @@ class TestStockEntry(FrappeTestCase): from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse create_warehouse("Test Warehouse for Sample Retention") - frappe.db.set_value("Stock Settings", None, "sample_retention_warehouse", "Test Warehouse for Sample Retention - _TC") + frappe.db.set_value( + "Stock Settings", + None, + "sample_retention_warehouse", + "Test Warehouse for Sample Retention - _TC", + ) test_item_code = "Retain Sample Item" - if not frappe.db.exists('Item', test_item_code): + if not frappe.db.exists("Item", test_item_code): item = frappe.new_doc("Item") item.item_code = test_item_code item.item_name = "Retain Sample Item" @@ -758,44 +916,58 @@ class TestStockEntry(FrappeTestCase): receipt_entry = frappe.new_doc("Stock Entry") receipt_entry.company = "_Test Company" receipt_entry.purpose = "Material Receipt" - receipt_entry.append("items", { - "item_code": test_item_code, - "t_warehouse": "_Test Warehouse - _TC", - "qty": 40, - "basic_rate": 12, - "cost_center": "_Test Cost Center - _TC", - "sample_quantity": 4 - }) + receipt_entry.append( + "items", + { + "item_code": test_item_code, + "t_warehouse": "_Test Warehouse - _TC", + "qty": 40, + "basic_rate": 12, + "cost_center": "_Test Cost Center - _TC", + "sample_quantity": 4, + }, + ) receipt_entry.set_stock_entry_type() receipt_entry.insert() receipt_entry.submit() - retention_data = move_sample_to_retention_warehouse(receipt_entry.company, receipt_entry.get("items")) + retention_data = move_sample_to_retention_warehouse( + receipt_entry.company, receipt_entry.get("items") + ) retention_entry = frappe.new_doc("Stock Entry") retention_entry.company = retention_data.company retention_entry.purpose = retention_data.purpose - retention_entry.append("items", { - "item_code": test_item_code, - "t_warehouse": "Test Warehouse for Sample Retention - _TC", - "s_warehouse": "_Test Warehouse - _TC", - "qty": 4, - "basic_rate": 12, - "cost_center": "_Test Cost Center - _TC", - "batch_no": receipt_entry.get("items")[0].batch_no - }) + retention_entry.append( + "items", + { + "item_code": test_item_code, + "t_warehouse": "Test Warehouse for Sample Retention - _TC", + "s_warehouse": "_Test Warehouse - _TC", + "qty": 4, + "basic_rate": 12, + "cost_center": "_Test Cost Center - _TC", + "batch_no": receipt_entry.get("items")[0].batch_no, + }, + ) retention_entry.set_stock_entry_type() retention_entry.insert() retention_entry.submit() - qty_in_usable_warehouse = get_batch_qty(receipt_entry.get("items")[0].batch_no, "_Test Warehouse - _TC", "_Test Item") - qty_in_retention_warehouse = get_batch_qty(receipt_entry.get("items")[0].batch_no, "Test Warehouse for Sample Retention - _TC", "_Test Item") + qty_in_usable_warehouse = get_batch_qty( + receipt_entry.get("items")[0].batch_no, "_Test Warehouse - _TC", "_Test Item" + ) + qty_in_retention_warehouse = get_batch_qty( + receipt_entry.get("items")[0].batch_no, + "Test Warehouse for Sample Retention - _TC", + "_Test Item", + ) self.assertEqual(qty_in_usable_warehouse, 36) self.assertEqual(qty_in_retention_warehouse, 4) def test_quality_check(self): item_code = "_Test Item For QC" - if not frappe.db.exists('Item', item_code): + if not frappe.db.exists("Item", item_code): create_item(item_code) repack = frappe.copy_doc(test_records[3]) @@ -849,44 +1021,63 @@ class TestStockEntry(FrappeTestCase): # 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") + 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" + ) self.assertEqual(se.get("items")[0].allow_zero_valuation_rate, 1) self.assertEqual(se.get("items")[0].amount, 0) def test_zero_incoming_rate(self): - """ Make sure incoming rate of 0 is allowed while consuming. + """Make sure incoming rate of 0 is allowed while consuming. - qty | rate | valuation rate - 1 | 100 | 100 - 1 | 0 | 50 - -1 | 100 | 0 - -1 | 0 <--- assert this + qty | rate | valuation rate + 1 | 100 | 100 + 1 | 0 | 50 + -1 | 100 | 0 + -1 | 0 <--- assert this """ item_code = "_TestZeroVal" warehouse = "_Test Warehouse - _TC" - create_item('_TestZeroVal') + create_item("_TestZeroVal") _receipt = make_stock_entry(item_code=item_code, qty=1, to_warehouse=warehouse, rate=100) - receipt2 = make_stock_entry(item_code=item_code, qty=1, to_warehouse=warehouse, rate=0, do_not_save=True) + receipt2 = make_stock_entry( + item_code=item_code, qty=1, to_warehouse=warehouse, rate=0, do_not_save=True + ) receipt2.items[0].allow_zero_valuation_rate = 1 receipt2.save() receipt2.submit() issue = make_stock_entry(item_code=item_code, qty=1, from_warehouse=warehouse) - value_diff = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": issue.name, "voucher_type": "Stock Entry"}, "stock_value_difference") + value_diff = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": issue.name, "voucher_type": "Stock Entry"}, + "stock_value_difference", + ) self.assertEqual(value_diff, -100) issue2 = make_stock_entry(item_code=item_code, qty=1, from_warehouse=warehouse) - value_diff = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": issue2.name, "voucher_type": "Stock Entry"}, "stock_value_difference") + value_diff = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": issue2.name, "voucher_type": "Stock Entry"}, + "stock_value_difference", + ) self.assertEqual(value_diff, 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) @@ -895,52 +1086,61 @@ class TestStockEntry(FrappeTestCase): mr.save() mr.submit() - is_opening = frappe.db.get_value("GL Entry", - filters={"voucher_type": "Stock Entry", "voucher_no": mr.name}, fieldname="is_opening") + is_opening = frappe.db.get_value( + "GL Entry", + filters={"voucher_type": "Stock Entry", "voucher_no": mr.name}, + fieldname="is_opening", + ) self.assertEqual(is_opening, "Yes") 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" - }] - }) + 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", + } + ], + } + ) se.insert() se.submit() - self.check_gl_entries("Stock Entry", se.name, - sorted([ - ["Stock Adjustment - TCP1", 100.0, 0.0], - ["Miscellaneous Expenses - TCP1", 0.0, 100.0] - ]) + self.check_gl_entries( + "Stock Entry", + se.name, + sorted( + [["Stock Adjustment - TCP1", 100.0, 0.0], ["Miscellaneous Expenses - TCP1", 0.0, 100.0]] + ), ) def test_conversion_factor_change(self): @@ -971,15 +1171,15 @@ class TestStockEntry(FrappeTestCase): def test_additional_cost_distribution_manufacture(self): se = frappe.get_doc( - doctype="Stock Entry", - purpose="Manufacture", - additional_costs=[frappe._dict(base_amount=100)], - items=[ - frappe._dict(item_code="RM", basic_amount=10), - frappe._dict(item_code="FG", basic_amount=20, t_warehouse="X", is_finished_item=1), - frappe._dict(item_code="scrap", basic_amount=30, t_warehouse="X") - ], - ) + doctype="Stock Entry", + purpose="Manufacture", + additional_costs=[frappe._dict(base_amount=100)], + items=[ + frappe._dict(item_code="RM", basic_amount=10), + frappe._dict(item_code="FG", basic_amount=20, t_warehouse="X", is_finished_item=1), + frappe._dict(item_code="scrap", basic_amount=30, t_warehouse="X"), + ], + ) se.distribute_additional_costs() @@ -988,14 +1188,14 @@ class TestStockEntry(FrappeTestCase): def test_additional_cost_distribution_non_manufacture(self): se = frappe.get_doc( - doctype="Stock Entry", - purpose="Material Receipt", - additional_costs=[frappe._dict(base_amount=100)], - items=[ - frappe._dict(item_code="RECEIVED_1", basic_amount=20, t_warehouse="X"), - frappe._dict(item_code="RECEIVED_2", basic_amount=30, t_warehouse="X") - ], - ) + doctype="Stock Entry", + purpose="Material Receipt", + additional_costs=[frappe._dict(base_amount=100)], + items=[ + frappe._dict(item_code="RECEIVED_1", basic_amount=20, t_warehouse="X"), + frappe._dict(item_code="RECEIVED_2", basic_amount=30, t_warehouse="X"), + ], + ) se.distribute_additional_costs() @@ -1010,9 +1210,11 @@ class TestStockEntry(FrappeTestCase): stock_entry_type="Manufacture", company="_Test Company", items=[ - frappe._dict(item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC"), - frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC") - ] + frappe._dict( + item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC" + ), + frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC"), + ], ) # SE must have atleast one FG self.assertRaises(FinishedGoodError, se.save) @@ -1027,47 +1229,49 @@ class TestStockEntry(FrappeTestCase): # Check if FG cost is calculated based on RM total cost # RM total cost = 200, FG rate = 200/4(FG qty) = 50 - self.assertEqual(se.items[1].basic_rate, flt(se.items[0].basic_rate/4)) + self.assertEqual(se.items[1].basic_rate, flt(se.items[0].basic_rate / 4)) self.assertEqual(se.value_difference, 0.0) self.assertEqual(se.total_incoming_value, se.total_outgoing_value) @change_settings("Stock Settings", {"allow_negative_stock": 0}) def test_future_negative_sle(self): # Initialize item, batch, warehouse, opening qty - item_code = '_Test Future Neg Item' - batch_no = '_Test Future Neg Batch' - warehouses = [ - '_Test Future Neg Warehouse Source', - '_Test Future Neg Warehouse Destination' - ] + item_code = "_Test Future Neg Item" + batch_no = "_Test Future Neg Batch" + warehouses = ["_Test Future Neg Warehouse Source", "_Test Future Neg Warehouse Destination"] warehouse_names = initialize_records_for_future_negative_sle_test( - item_code, batch_no, warehouses, - opening_qty=2, posting_date='2021-07-01' + item_code, batch_no, warehouses, opening_qty=2, posting_date="2021-07-01" ) # Executing an illegal sequence should raise an error sequence_of_entries = [ - dict(item_code=item_code, + dict( + item_code=item_code, qty=2, from_warehouse=warehouse_names[0], to_warehouse=warehouse_names[1], batch_no=batch_no, - posting_date='2021-07-03', - purpose='Material Transfer'), - dict(item_code=item_code, + posting_date="2021-07-03", + purpose="Material Transfer", + ), + dict( + item_code=item_code, qty=2, from_warehouse=warehouse_names[1], to_warehouse=warehouse_names[0], batch_no=batch_no, - posting_date='2021-07-04', - purpose='Material Transfer'), - dict(item_code=item_code, + posting_date="2021-07-04", + purpose="Material Transfer", + ), + dict( + item_code=item_code, qty=2, from_warehouse=warehouse_names[0], to_warehouse=warehouse_names[1], batch_no=batch_no, - posting_date='2021-07-02', # Illegal SE - purpose='Material Transfer') + posting_date="2021-07-02", # Illegal SE + purpose="Material Transfer", + ), ] self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries) @@ -1077,74 +1281,74 @@ class TestStockEntry(FrappeTestCase): from erpnext.stock.doctype.batch.test_batch import TestBatch # Initialize item, batch, warehouse, opening qty - item_code = '_Test MultiBatch Item' + item_code = "_Test MultiBatch Item" TestBatch.make_batch_item(item_code) - batch_nos = [] # store generate batches - warehouse = '_Test Warehouse - _TC' + batch_nos = [] # store generate batches + warehouse = "_Test Warehouse - _TC" se1 = make_stock_entry( - item_code=item_code, - qty=2, - to_warehouse=warehouse, - posting_date='2021-09-01', - purpose='Material Receipt' - ) + item_code=item_code, + qty=2, + to_warehouse=warehouse, + posting_date="2021-09-01", + purpose="Material Receipt", + ) batch_nos.append(se1.items[0].batch_no) se2 = make_stock_entry( - item_code=item_code, - qty=2, - to_warehouse=warehouse, - posting_date='2021-09-03', - purpose='Material Receipt' - ) + item_code=item_code, + qty=2, + to_warehouse=warehouse, + posting_date="2021-09-03", + purpose="Material Receipt", + ) batch_nos.append(se2.items[0].batch_no) with self.assertRaises(NegativeStockError) as nse: - make_stock_entry(item_code=item_code, + make_stock_entry( + item_code=item_code, qty=1, from_warehouse=warehouse, batch_no=batch_nos[1], - posting_date='2021-09-02', # backdated consumption of 2nd batch - purpose='Material Issue') + posting_date="2021-09-02", # backdated consumption of 2nd batch + purpose="Material Issue", + ) def test_multi_batch_value_diff(self): - """ Test value difference on stock entry in case of multi-batch. - | Stock entry | batch | qty | rate | value diff on SE | - | --- | --- | --- | --- | --- | - | receipt | A | 1 | 10 | 30 | - | receipt | B | 1 | 20 | | - | issue | A | -1 | 10 | -30 (to assert after submit) | - | issue | B | -1 | 20 | | + """Test value difference on stock entry in case of multi-batch. + | Stock entry | batch | qty | rate | value diff on SE | + | --- | --- | --- | --- | --- | + | receipt | A | 1 | 10 | 30 | + | receipt | B | 1 | 20 | | + | issue | A | -1 | 10 | -30 (to assert after submit) | + | issue | B | -1 | 20 | | """ from erpnext.stock.doctype.batch.test_batch import TestBatch batch_nos = [] - item_code = '_TestMultibatchFifo' + item_code = "_TestMultibatchFifo" TestBatch.make_batch_item(item_code) - warehouse = '_Test Warehouse - _TC' + warehouse = "_Test Warehouse - _TC" receipt = make_stock_entry( - item_code=item_code, - qty=1, - rate=10, - to_warehouse=warehouse, - purpose='Material Receipt', - do_not_save=True - ) - receipt.append("items", frappe.copy_doc(receipt.items[0], ignore_no_copy=False).update({"basic_rate": 20}) ) + item_code=item_code, + qty=1, + rate=10, + to_warehouse=warehouse, + purpose="Material Receipt", + do_not_save=True, + ) + receipt.append( + "items", frappe.copy_doc(receipt.items[0], ignore_no_copy=False).update({"basic_rate": 20}) + ) receipt.save() receipt.submit() batch_nos.extend(row.batch_no for row in receipt.items) self.assertEqual(receipt.value_difference, 30) issue = make_stock_entry( - item_code=item_code, - qty=1, - from_warehouse=warehouse, - purpose='Material Issue', - do_not_save=True - ) + item_code=item_code, qty=1, from_warehouse=warehouse, purpose="Material Issue", do_not_save=True + ) issue.append("items", frappe.copy_doc(issue.items[0], ignore_no_copy=False)) for row, batch_no in zip(issue.items, batch_nos): row.batch_no = batch_no @@ -1154,6 +1358,7 @@ class TestStockEntry(FrappeTestCase): issue.reload() # reload because reposting current voucher updates rate self.assertEqual(issue.value_difference, -30) + def make_serialized_item(**args): args = frappe._dict(args) se = frappe.copy_doc(test_records[0]) @@ -1183,50 +1388,57 @@ def make_serialized_item(**args): se.submit() return se + def get_qty_after_transaction(**args): args = frappe._dict(args) - last_sle = get_previous_sle({ - "item_code": args.item_code or "_Test Item", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "posting_date": args.posting_date or nowdate(), - "posting_time": args.posting_time or nowtime() - }) + last_sle = get_previous_sle( + { + "item_code": args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "posting_date": args.posting_date or nowdate(), + "posting_time": args.posting_time or nowtime(), + } + ) return flt(last_sle.get("qty_after_transaction")) + def get_multiple_items(): return [ - { - "conversion_factor": 1.0, - "cost_center": "Main - TCP1", - "doctype": "Stock Entry Detail", - "expense_account": "Stock Adjustment - TCP1", - "basic_rate": 100, - "item_code": "_Test Item", - "qty": 50.0, - "s_warehouse": "Stores - TCP1", - "stock_uom": "_Test UOM", - "transfer_qty": 50.0, - "uom": "_Test UOM" - }, - { - "conversion_factor": 1.0, - "cost_center": "Main - TCP1", - "doctype": "Stock Entry Detail", - "expense_account": "Stock Adjustment - TCP1", - "basic_rate": 5000, - "item_code": "_Test Item Home Desktop 100", - "qty": 1, - "stock_uom": "_Test UOM", - "t_warehouse": "Stores - TCP1", - "transfer_qty": 1, - "uom": "_Test UOM" - } - ] + { + "conversion_factor": 1.0, + "cost_center": "Main - TCP1", + "doctype": "Stock Entry Detail", + "expense_account": "Stock Adjustment - TCP1", + "basic_rate": 100, + "item_code": "_Test Item", + "qty": 50.0, + "s_warehouse": "Stores - TCP1", + "stock_uom": "_Test UOM", + "transfer_qty": 50.0, + "uom": "_Test UOM", + }, + { + "conversion_factor": 1.0, + "cost_center": "Main - TCP1", + "doctype": "Stock Entry Detail", + "expense_account": "Stock Adjustment - TCP1", + "basic_rate": 5000, + "item_code": "_Test Item Home Desktop 100", + "qty": 1, + "stock_uom": "_Test UOM", + "t_warehouse": "Stores - TCP1", + "transfer_qty": 1, + "uom": "_Test UOM", + }, + ] + + +test_records = frappe.get_test_records("Stock Entry") -test_records = frappe.get_test_records('Stock Entry') def initialize_records_for_future_negative_sle_test( - item_code, batch_no, warehouses, opening_qty, posting_date): + item_code, batch_no, warehouses, opening_qty, posting_date +): from erpnext.stock.doctype.batch.test_batch import TestBatch, make_new_batch from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, @@ -1237,9 +1449,9 @@ def initialize_records_for_future_negative_sle_test( make_new_batch(item_code=item_code, batch_id=batch_no) warehouse_names = [create_warehouse(w) for w in warehouses] create_stock_reconciliation( - purpose='Opening Stock', + purpose="Opening Stock", posting_date=posting_date, - posting_time='20:00:20', + posting_time="20:00:20", item_code=item_code, warehouse=warehouse_names[0], valuation_rate=100, diff --git a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py index efd97c04ac..7258cfbe2c 100644 --- a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py +++ b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py @@ -8,5 +8,5 @@ from frappe.model.document import Document class StockEntryType(Document): def validate(self): - if self.add_to_transit and self.purpose != 'Material Transfer': + if self.add_to_transit and self.purpose != "Material Transfer": self.add_to_transit = 0 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 2f593041bf..5c1da420e2 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -14,11 +14,17 @@ from erpnext.accounts.utils import get_fiscal_year from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock -class StockFreezeError(frappe.ValidationError): pass -class BackDatedStockTransaction(frappe.ValidationError): pass +class StockFreezeError(frappe.ValidationError): + pass + + +class BackDatedStockTransaction(frappe.ValidationError): + pass + exclude_from_linked_with = True + class StockLedgerEntry(Document): def autoname(self): """ @@ -32,6 +38,7 @@ class StockLedgerEntry(Document): def validate(self): self.flags.ignore_submit_comment = True from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company + self.validate_mandatory() self.validate_item() self.validate_batch() @@ -42,24 +49,29 @@ class StockLedgerEntry(Document): self.block_transactions_against_group_warehouse() self.validate_with_last_transaction_posting_time() - def on_submit(self): self.check_stock_frozen_date() self.calculate_batch_qty() if not self.get("via_landed_cost_voucher"): from erpnext.stock.doctype.serial_no.serial_no import process_serial_no + process_serial_no(self) 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, "is_cancelled": 0}, - "sum(actual_qty)") or 0 + batch_qty = ( + frappe.db.get_value( + "Stock Ledger Entry", + {"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) def validate_mandatory(self): - mandatory = ['warehouse','posting_date','voucher_type','voucher_no','company'] + mandatory = ["warehouse", "posting_date", "voucher_type", "voucher_no", "company"] for k in mandatory: if not self.get(k): frappe.throw(_("{0} is required").format(self.meta.get_label(k))) @@ -68,9 +80,13 @@ class StockLedgerEntry(Document): frappe.throw(_("Actual Qty is mandatory")) def validate_item(self): - item_det = frappe.db.sql("""select name, item_name, has_batch_no, docstatus, + item_det = frappe.db.sql( + """select name, item_name, has_batch_no, docstatus, is_stock_item, has_variants, stock_uom, create_new_batch - from tabItem where name=%s""", self.item_code, as_dict=True) + from tabItem where name=%s""", + self.item_code, + as_dict=True, + ) if not item_det: frappe.throw(_("Item {0} not found").format(self.item_code)) @@ -82,39 +98,58 @@ class StockLedgerEntry(Document): # check if batch number is valid 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 + 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 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 and self.is_cancelled == 0: frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code)) if item_det.has_variants: - frappe.throw(_("Stock cannot exist for Item {0} since has variants").format(self.item_code), - ItemTemplateCannotHaveStock) + frappe.throw( + _("Stock cannot exist for Item {0} since has variants").format(self.item_code), + ItemTemplateCannotHaveStock, + ) self.stock_uom = item_det.stock_uom def check_stock_frozen_date(self): - stock_settings = frappe.get_cached_doc('Stock Settings') + stock_settings = frappe.get_cached_doc("Stock Settings") if stock_settings.stock_frozen_upto: - if (getdate(self.posting_date) <= getdate(stock_settings.stock_frozen_upto) - and stock_settings.stock_auth_role not in frappe.get_roles()): - frappe.throw(_("Stock transactions before {0} are frozen") - .format(formatdate(stock_settings.stock_frozen_upto)), StockFreezeError) + if ( + getdate(self.posting_date) <= getdate(stock_settings.stock_frozen_upto) + and stock_settings.stock_auth_role not in frappe.get_roles() + ): + frappe.throw( + _("Stock transactions before {0} are frozen").format( + formatdate(stock_settings.stock_frozen_upto) + ), + StockFreezeError, + ) stock_frozen_upto_days = cint(stock_settings.stock_frozen_upto_days) if stock_frozen_upto_days: - older_than_x_days_ago = (add_days(getdate(self.posting_date), stock_frozen_upto_days) <= date.today()) + older_than_x_days_ago = ( + add_days(getdate(self.posting_date), stock_frozen_upto_days) <= date.today() + ) if older_than_x_days_ago and stock_settings.stock_auth_role not in frappe.get_roles(): - frappe.throw(_("Not allowed to update stock transactions older than {0}").format(stock_frozen_upto_days), StockFreezeError) + frappe.throw( + _("Not allowed to update stock transactions older than {0}").format(stock_frozen_upto_days), + StockFreezeError, + ) def scrub_posting_time(self): - if not self.posting_time or self.posting_time == '00:0': - self.posting_time = '00:00' + if not self.posting_time or self.posting_time == "00:0": + self.posting_time = "00:00" def validate_batch(self): if self.batch_no and self.voucher_type != "Stock Entry": @@ -128,43 +163,61 @@ class StockLedgerEntry(Document): self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0] else: from erpnext.accounts.utils import validate_fiscal_year - validate_fiscal_year(self.posting_date, self.fiscal_year, self.company, - self.meta.get_label("posting_date"), self) + + validate_fiscal_year( + self.posting_date, self.fiscal_year, self.company, self.meta.get_label("posting_date"), self + ) def block_transactions_against_group_warehouse(self): from erpnext.stock.utils import is_group_warehouse + is_group_warehouse(self.warehouse) def validate_with_last_transaction_posting_time(self): - authorized_role = frappe.db.get_single_value("Stock Settings", "role_allowed_to_create_edit_back_dated_transactions") + 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(""" + last_transaction_time = frappe.db.sql( + """ select MAX(timestamp(posting_date, posting_time)) as posting_time from `tabStock Ledger Entry` where docstatus = 1 and is_cancelled = 0 and item_code = %s - and warehouse = %s""", (self.item_code, self.warehouse))[0][0] + 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 += "

    " + _("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 += "

    " + _( + "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 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'): + if not frappe.db.has_index("tabStock Ledger Entry", "posting_sort_index"): frappe.db.commit() - frappe.db.add_index("Stock Ledger Entry", + frappe.db.add_index( + "Stock Ledger Entry", fields=["posting_date", "posting_time", "name"], - index_name="posting_sort_index") + index_name="posting_sort_index", + ) frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"]) frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"]) 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 fc579958be..42956a129b 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 @@ -29,11 +29,17 @@ from erpnext.stock.stock_ledger import get_previous_sle class TestStockLedgerEntry(FrappeTestCase): def setUp(self): items = create_items() - reset('Stock Entry') + reset("Stock Entry") # 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) + 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" @@ -45,9 +51,11 @@ class TestStockLedgerEntry(FrappeTestCase): qty=50, rate=100, company=company, - expense_account = "Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC", - posting_date='2020-04-10', - posting_time='14:00' + expense_account="Stock Adjustment - _TC" + if frappe.get_all("Stock Ledger Entry") + else "Temporary Opening - _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 @@ -57,9 +65,11 @@ class TestStockLedgerEntry(FrappeTestCase): qty=10, rate=200, company=company, - expense_account="Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC", - posting_date='2020-04-20', - posting_time='14:00' + expense_account="Stock Adjustment - _TC" + if frappe.get_all("Stock Ledger Entry") + else "Temporary Opening - _TC", + posting_date="2020-04-20", + posting_time="14:00", ) # _Test Item for Reposting transferred from Stores to FG warehouse on 30-04-2020 @@ -69,28 +79,40 @@ class TestStockLedgerEntry(FrappeTestCase): target="Finished Goods - _TC", company=company, qty=10, - expense_account="Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC", - posting_date='2020-04-30', - posting_time='14:00' + expense_account="Stock Adjustment - _TC" + if frappe.get_all("Stock Ledger Entry") + else "Temporary Opening - _TC", + posting_date="2020-04-30", + posting_time="14:00", + ) + target_wh_sle = frappe.db.get_value( + "Stock Ledger Entry", + { + "item_code": "_Test Item for Reposting", + "warehouse": "Finished Goods - _TC", + "voucher_type": "Stock Entry", + "voucher_no": se.name, + }, + ["valuation_rate"], + as_dict=1, ) - target_wh_sle = frappe.db.get_value('Stock Ledger Entry', { - "item_code": "_Test Item for Reposting", - "warehouse": "Finished Goods - _TC", - "voucher_type": "Stock Entry", - "voucher_no": se.name - }, ["valuation_rate"], as_dict=1) 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') + repack = create_repack_entry(company=company, posting_date="2020-05-05", posting_time="14:00") - finished_item_sle = frappe.db.get_value('Stock Ledger Entry', { - "item_code": "_Test Finished Item for Reposting", - "warehouse": "Finished Goods - _TC", - "voucher_type": "Stock Entry", - "voucher_no": repack.name - }, ["incoming_rate", "valuation_rate"], as_dict=1) + finished_item_sle = frappe.db.get_value( + "Stock Ledger Entry", + { + "item_code": "_Test Finished Item for Reposting", + "warehouse": "Finished Goods - _TC", + "voucher_type": "Stock Entry", + "voucher_no": repack.name, + }, + ["incoming_rate", "valuation_rate"], + as_dict=1, + ) self.assertEqual(finished_item_sle.get("incoming_rate"), 540) self.assertEqual(finished_item_sle.get("valuation_rate"), 540) @@ -101,29 +123,37 @@ class TestStockLedgerEntry(FrappeTestCase): qty=50, rate=150, company=company, - expense_account ="Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC", - posting_date='2020-04-12', - posting_time='14:00' + expense_account="Stock Adjustment - _TC" + if frappe.get_all("Stock Ledger Entry") + else "Temporary Opening - _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' - }) + 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 = frappe.db.get_value('Stock Ledger Entry', { - "item_code": "_Test Finished Item for Reposting", - "warehouse": "Finished Goods - _TC", - "voucher_type": "Stock Entry", - "voucher_no": repack.name - }, ["incoming_rate", "valuation_rate"], as_dict=1) + finished_item_sle = frappe.db.get_value( + "Stock Ledger Entry", + { + "item_code": "_Test Finished Item for Reposting", + "warehouse": "Finished Goods - _TC", + "voucher_type": "Stock Entry", + "voucher_no": repack.name, + }, + ["incoming_rate", "valuation_rate"], + as_dict=1, + ) self.assertEqual(finished_item_sle.get("incoming_rate"), 790) self.assertEqual(finished_item_sle.get("valuation_rate"), 790) @@ -133,76 +163,133 @@ class TestStockLedgerEntry(FrappeTestCase): 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) + 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) + 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"]) + 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"]) + 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" + 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) + 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") + # 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) + 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") + 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", + 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"]) + ["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) + 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", + 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"]) + ["incoming_rate", "stock_value_difference"], + ) self.assertEqual(incoming_rate, 110) self.assertEqual(stock_value_difference, 220) @@ -218,55 +305,93 @@ class TestStockLedgerEntry(FrappeTestCase): def test_reposting_of_sales_return_for_packed_item(self): company = "_Test Company" - packed_item_code="_Test Item for Reposting" + 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) + 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") + # 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) + 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") + 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", + 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"]) + ["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) + 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", + 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"]) + ["incoming_rate", "stock_value_difference"], + ) self.assertEqual(incoming_rate, 101) self.assertEqual(stock_value_difference, 808) @@ -284,20 +409,35 @@ class TestStockLedgerEntry(FrappeTestCase): from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom company = "_Test Company" - rm_item_code="_Test Item for Reposting" + 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") + 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) + 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") + 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) @@ -308,8 +448,11 @@ class TestStockLedgerEntry(FrappeTestCase): 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") + 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 @@ -319,8 +462,9 @@ class TestStockLedgerEntry(FrappeTestCase): 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") + 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 try: @@ -331,8 +475,13 @@ class TestStockLedgerEntry(FrappeTestCase): frappe.set_user(user.name) 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) + 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) @@ -342,14 +491,17 @@ class TestStockLedgerEntry(FrappeTestCase): frappe.set_user(user.name) # 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 = 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() finally: - frappe.db.set_value("Stock Settings", None, "role_allowed_to_create_edit_back_dated_transactions", None) + frappe.db.set_value( + "Stock Settings", None, "role_allowed_to_create_edit_back_dated_transactions", None + ) frappe.set_user("Administrator") user.remove_roles("Stock Manager") @@ -359,13 +511,13 @@ class TestStockLedgerEntry(FrappeTestCase): # Incoming Entries for Stock Value check pr_entry_list = [ (item, warehouses[0], batches[0], 1, 100), - (item, warehouses[0], batches[1], 1, 50), + (item, warehouses[0], batches[1], 1, 50), (item, warehouses[0], batches[0], 1, 150), (item, warehouses[0], batches[1], 1, 100), ] prs = create_purchase_receipt_entries_for_batchwise_item_valuation_test(pr_entry_list) - sle_details = fetch_sle_details_for_doc_list(prs, ['stock_value']) - sv_list = [d['stock_value'] for d in sle_details] + sle_details = fetch_sle_details_for_doc_list(prs, ["stock_value"]) + sv_list = [d["stock_value"] for d in sle_details] expected_sv = [100, 150, 300, 400] self.assertEqual(expected_sv, sv_list, "Incorrect 'Stock Value' values") @@ -374,29 +526,33 @@ class TestStockLedgerEntry(FrappeTestCase): (item, warehouses[0], batches[1], 1, 200), (item, warehouses[0], batches[0], 1, 200), (item, warehouses[0], batches[1], 1, 200), - (item, warehouses[0], batches[0], 1, 200) + (item, warehouses[0], batches[0], 1, 200), ] dns = create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list) - sle_details = fetch_sle_details_for_doc_list(dns, ['stock_value_difference']) - svd_list = [-1 * d['stock_value_difference'] for d in sle_details] + sle_details = fetch_sle_details_for_doc_list(dns, ["stock_value_difference"]) + svd_list = [-1 * d["stock_value_difference"] for d in sle_details] expected_incoming_rates = expected_abs_svd = [75, 125, 75, 125] self.assertEqual(expected_abs_svd, svd_list, "Incorrect 'Stock Value Difference' values") for dn, incoming_rate in zip(dns, expected_incoming_rates): self.assertEqual( - dn.items[0].incoming_rate, incoming_rate, - "Incorrect 'Incoming Rate' values fetched for DN items" + dn.items[0].incoming_rate, + incoming_rate, + "Incorrect 'Incoming Rate' values fetched for DN items", ) - def assertSLEs(self, doc, expected_sles, sle_filters=None): - """ Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line""" + """Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line""" filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0} if sle_filters: filters.update(sle_filters) - sles = frappe.get_all("Stock Ledger Entry", fields=["*"], filters=filters, - order_by="timestamp(posting_date, posting_time), creation") + sles = frappe.get_all( + "Stock Ledger Entry", + fields=["*"], + filters=filters, + order_by="timestamp(posting_date, posting_time), creation", + ) for exp_sle, act_sle in zip(expected_sles, sles): for k, v in exp_sle.items(): @@ -409,13 +565,10 @@ class TestStockLedgerEntry(FrappeTestCase): self.assertEqual(v, act_value, msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}") - def test_batchwise_item_valuation_stock_reco(self): item, warehouses, batches = setup_item_valuation_test() - state = { - "stock_value" : 0.0, - "qty": 0.0 - } + state = {"stock_value": 0.0, "qty": 0.0} + def update_invariants(exp_sles): for sle in exp_sles: state["stock_value"] += sle["stock_value_difference"] @@ -423,33 +576,41 @@ class TestStockLedgerEntry(FrappeTestCase): sle["stock_value"] = state["stock_value"] sle["qty_after_transaction"] = state["qty"] - osr1 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=10, rate=100, batch_no=batches[1]) + osr1 = create_stock_reconciliation( + warehouse=warehouses[0], item_code=item, qty=10, rate=100, batch_no=batches[1] + ) expected_sles = [ {"actual_qty": 10, "stock_value_difference": 1000}, ] update_invariants(expected_sles) self.assertSLEs(osr1, expected_sles) - osr2 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=13, rate=200, batch_no=batches[0]) + osr2 = create_stock_reconciliation( + warehouse=warehouses[0], item_code=item, qty=13, rate=200, batch_no=batches[0] + ) expected_sles = [ - {"actual_qty": 13, "stock_value_difference": 200*13}, + {"actual_qty": 13, "stock_value_difference": 200 * 13}, ] update_invariants(expected_sles) self.assertSLEs(osr2, expected_sles) - sr1 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=5, rate=50, batch_no=batches[1]) + sr1 = create_stock_reconciliation( + warehouse=warehouses[0], item_code=item, qty=5, rate=50, batch_no=batches[1] + ) expected_sles = [ {"actual_qty": -10, "stock_value_difference": -10 * 100}, - {"actual_qty": 5, "stock_value_difference": 250} + {"actual_qty": 5, "stock_value_difference": 250}, ] update_invariants(expected_sles) self.assertSLEs(sr1, expected_sles) - sr2 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=20, rate=75, batch_no=batches[0]) + sr2 = create_stock_reconciliation( + warehouse=warehouses[0], item_code=item, qty=20, rate=75, batch_no=batches[0] + ) expected_sles = [ {"actual_qty": -13, "stock_value_difference": -13 * 200}, - {"actual_qty": 20, "stock_value_difference": 20 * 75} + {"actual_qty": 20, "stock_value_difference": 20 * 75}, ] update_invariants(expected_sles) self.assertSLEs(sr2, expected_sles) @@ -459,108 +620,190 @@ class TestStockLedgerEntry(FrappeTestCase): source = warehouses[0] target = warehouses[1] - unrelated_batch = make_stock_entry(item_code=item_code, target=source, batch_no=batches[1], - qty=5, rate=10) - self.assertSLEs(unrelated_batch, [ - {"actual_qty": 5, "stock_value_difference": 10 * 5}, - ]) + unrelated_batch = make_stock_entry( + item_code=item_code, target=source, batch_no=batches[1], qty=5, rate=10 + ) + self.assertSLEs( + unrelated_batch, + [ + {"actual_qty": 5, "stock_value_difference": 10 * 5}, + ], + ) - reciept = make_stock_entry(item_code=item_code, target=source, batch_no=batches[0], qty=5, rate=10) - self.assertSLEs(reciept, [ - {"actual_qty": 5, "stock_value_difference": 10 * 5}, - ]) + reciept = make_stock_entry( + item_code=item_code, target=source, batch_no=batches[0], qty=5, rate=10 + ) + self.assertSLEs( + reciept, + [ + {"actual_qty": 5, "stock_value_difference": 10 * 5}, + ], + ) - transfer = make_stock_entry(item_code=item_code, source=source, target=target, batch_no=batches[0], qty=5) - self.assertSLEs(transfer, [ - {"actual_qty": -5, "stock_value_difference": -10 * 5, "warehouse": source}, - {"actual_qty": 5, "stock_value_difference": 10 * 5, "warehouse": target} - ]) + transfer = make_stock_entry( + item_code=item_code, source=source, target=target, batch_no=batches[0], qty=5 + ) + self.assertSLEs( + transfer, + [ + {"actual_qty": -5, "stock_value_difference": -10 * 5, "warehouse": source}, + {"actual_qty": 5, "stock_value_difference": 10 * 5, "warehouse": target}, + ], + ) - backdated_receipt = make_stock_entry(item_code=item_code, target=source, batch_no=batches[0], - qty=5, rate=20, posting_date=add_days(today(), -1)) - self.assertSLEs(backdated_receipt, [ - {"actual_qty": 5, "stock_value_difference": 20 * 5}, - ]) + backdated_receipt = make_stock_entry( + item_code=item_code, + target=source, + batch_no=batches[0], + qty=5, + rate=20, + posting_date=add_days(today(), -1), + ) + self.assertSLEs( + backdated_receipt, + [ + {"actual_qty": 5, "stock_value_difference": 20 * 5}, + ], + ) # check reposted average rate in *future* transfer - self.assertSLEs(transfer, [ - {"actual_qty": -5, "stock_value_difference": -15 * 5, "warehouse": source, "stock_value": 15 * 5 + 10 * 5}, - {"actual_qty": 5, "stock_value_difference": 15 * 5, "warehouse": target, "stock_value": 15 * 5} - ]) + self.assertSLEs( + transfer, + [ + { + "actual_qty": -5, + "stock_value_difference": -15 * 5, + "warehouse": source, + "stock_value": 15 * 5 + 10 * 5, + }, + { + "actual_qty": 5, + "stock_value_difference": 15 * 5, + "warehouse": target, + "stock_value": 15 * 5, + }, + ], + ) - transfer_unrelated = make_stock_entry(item_code=item_code, source=source, - target=target, batch_no=batches[1], qty=5) - self.assertSLEs(transfer_unrelated, [ - {"actual_qty": -5, "stock_value_difference": -10 * 5, "warehouse": source, "stock_value": 15 * 5}, - {"actual_qty": 5, "stock_value_difference": 10 * 5, "warehouse": target, "stock_value": 15 * 5 + 10 * 5} - ]) + transfer_unrelated = make_stock_entry( + item_code=item_code, source=source, target=target, batch_no=batches[1], qty=5 + ) + self.assertSLEs( + transfer_unrelated, + [ + { + "actual_qty": -5, + "stock_value_difference": -10 * 5, + "warehouse": source, + "stock_value": 15 * 5, + }, + { + "actual_qty": 5, + "stock_value_difference": 10 * 5, + "warehouse": target, + "stock_value": 15 * 5 + 10 * 5, + }, + ], + ) def test_intermediate_average_batch_wise_valuation(self): - """ A batch has moving average up until posting time, + """A batch has moving average up until posting time, check if same is respected when backdated entry is inserted in middle""" item_code, warehouses, batches = setup_item_valuation_test() warehouse = warehouses[0] batch = batches[0] - yesterday = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batch, - qty=1, rate=10, posting_date=add_days(today(), -1)) - self.assertSLEs(yesterday, [ - {"actual_qty": 1, "stock_value_difference": 10}, - ]) + yesterday = make_stock_entry( + item_code=item_code, + target=warehouse, + batch_no=batch, + qty=1, + rate=10, + posting_date=add_days(today(), -1), + ) + self.assertSLEs( + yesterday, + [ + {"actual_qty": 1, "stock_value_difference": 10}, + ], + ) - tomorrow = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0], - qty=1, rate=30, posting_date=add_days(today(), 1)) - self.assertSLEs(tomorrow, [ - {"actual_qty": 1, "stock_value_difference": 30}, - ]) + tomorrow = make_stock_entry( + item_code=item_code, + target=warehouse, + batch_no=batches[0], + qty=1, + rate=30, + posting_date=add_days(today(), 1), + ) + self.assertSLEs( + tomorrow, + [ + {"actual_qty": 1, "stock_value_difference": 30}, + ], + ) - create_today = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0], - qty=1, rate=20) - self.assertSLEs(create_today, [ - {"actual_qty": 1, "stock_value_difference": 20}, - ]) + create_today = make_stock_entry( + item_code=item_code, target=warehouse, batch_no=batches[0], qty=1, rate=20 + ) + self.assertSLEs( + create_today, + [ + {"actual_qty": 1, "stock_value_difference": 20}, + ], + ) - consume_today = make_stock_entry(item_code=item_code, source=warehouse, batch_no=batches[0], - qty=1) - self.assertSLEs(consume_today, [ - {"actual_qty": -1, "stock_value_difference": -15}, - ]) + consume_today = make_stock_entry( + item_code=item_code, source=warehouse, batch_no=batches[0], qty=1 + ) + self.assertSLEs( + consume_today, + [ + {"actual_qty": -1, "stock_value_difference": -15}, + ], + ) - consume_tomorrow = make_stock_entry(item_code=item_code, source=warehouse, batch_no=batches[0], - qty=2, posting_date=add_days(today(), 2)) - self.assertSLEs(consume_tomorrow, [ - {"stock_value_difference": -(30 + 15), "stock_value": 0, "qty_after_transaction": 0}, - ]) + consume_tomorrow = make_stock_entry( + item_code=item_code, + source=warehouse, + batch_no=batches[0], + qty=2, + posting_date=add_days(today(), 2), + ) + self.assertSLEs( + consume_tomorrow, + [ + {"stock_value_difference": -(30 + 15), "stock_value": 0, "qty_after_transaction": 0}, + ], + ) def test_legacy_item_valuation_stock_entry(self): columns = [ - 'stock_value_difference', - 'stock_value', - 'actual_qty', - 'qty_after_transaction', - 'stock_queue', + "stock_value_difference", + "stock_value", + "actual_qty", + "qty_after_transaction", + "stock_queue", ] item, warehouses, batches = setup_item_valuation_test(use_batchwise_valuation=0) def check_sle_details_against_expected(sle_details, expected_sle_details, detail, columns): for i, (sle_vals, ex_sle_vals) in enumerate(zip(sle_details, expected_sle_details)): for col, sle_val, ex_sle_val in zip(columns, sle_vals, ex_sle_vals): - if col == 'stock_queue': + if col == "stock_queue": sle_val = get_stock_value_from_q(sle_val) ex_sle_val = get_stock_value_from_q(ex_sle_val) self.assertEqual( - sle_val, ex_sle_val, - f"Incorrect {col} value on transaction #: {i} in {detail}" + sle_val, ex_sle_val, f"Incorrect {col} value on transaction #: {i} in {detail}" ) # List used to defer assertions to prevent commits cause of error skipped rollback details_list = [] - # Test Material Receipt Entries se_entry_list_mr = [ - (item, None, warehouses[0], batches[0], 1, 50, "2021-01-21"), + (item, None, warehouses[0], batches[0], 1, 50, "2021-01-21"), (item, None, warehouses[0], batches[1], 1, 100, "2021-01-23"), ] ses = create_stock_entry_entries_for_batchwise_item_valuation_test( @@ -568,14 +811,10 @@ class TestStockLedgerEntry(FrappeTestCase): ) sle_details = fetch_sle_details_for_doc_list(ses, columns=columns, as_dict=0) expected_sle_details = [ - (50.0, 50.0, 1.0, 1.0, '[[1.0, 50.0]]'), - (100.0, 150.0, 1.0, 2.0, '[[1.0, 50.0], [1.0, 100.0]]'), + (50.0, 50.0, 1.0, 1.0, "[[1.0, 50.0]]"), + (100.0, 150.0, 1.0, 2.0, "[[1.0, 50.0], [1.0, 100.0]]"), ] - details_list.append(( - sle_details, expected_sle_details, - "Material Receipt Entries", columns - )) - + details_list.append((sle_details, expected_sle_details, "Material Receipt Entries", columns)) # Test Material Issue Entries se_entry_list_mi = [ @@ -585,14 +824,8 @@ class TestStockLedgerEntry(FrappeTestCase): se_entry_list_mi, "Material Issue" ) sle_details = fetch_sle_details_for_doc_list(ses, columns=columns, as_dict=0) - expected_sle_details = [ - (-50.0, 100.0, -1.0, 1.0, '[[1, 100.0]]') - ] - details_list.append(( - sle_details, expected_sle_details, - "Material Issue Entries", columns - )) - + expected_sle_details = [(-50.0, 100.0, -1.0, 1.0, "[[1, 100.0]]")] + details_list.append((sle_details, expected_sle_details, "Material Issue Entries", columns)) # Run assertions for details in details_list: @@ -602,10 +835,8 @@ class TestStockLedgerEntry(FrappeTestCase): item_code, warehouses, batches = setup_item_valuation_test(use_batchwise_valuation=0) warehouse = warehouses[0] - state = { - "qty": 0.0, - "stock_value": 0.0 - } + state = {"qty": 0.0, "stock_value": 0.0} + def update_invariants(exp_sles): for sle in exp_sles: state["stock_value"] += sle["stock_value_difference"] @@ -614,59 +845,131 @@ class TestStockLedgerEntry(FrappeTestCase): sle["qty_after_transaction"] = state["qty"] return exp_sles - old1 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0], - qty=10, rate=10) - self.assertSLEs(old1, update_invariants([ - {"actual_qty": 10, "stock_value_difference": 10*10, "stock_queue": [[10, 10]]}, - ])) - old2 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[1], - qty=10, rate=20) - self.assertSLEs(old2, update_invariants([ - {"actual_qty": 10, "stock_value_difference": 10*20, "stock_queue": [[10, 10], [10, 20]]}, - ])) - old3 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0], - qty=5, rate=15) + old1 = make_stock_entry( + item_code=item_code, target=warehouse, batch_no=batches[0], qty=10, rate=10 + ) + self.assertSLEs( + old1, + update_invariants( + [ + {"actual_qty": 10, "stock_value_difference": 10 * 10, "stock_queue": [[10, 10]]}, + ] + ), + ) + old2 = make_stock_entry( + item_code=item_code, target=warehouse, batch_no=batches[1], qty=10, rate=20 + ) + self.assertSLEs( + old2, + update_invariants( + [ + {"actual_qty": 10, "stock_value_difference": 10 * 20, "stock_queue": [[10, 10], [10, 20]]}, + ] + ), + ) + old3 = make_stock_entry( + item_code=item_code, target=warehouse, batch_no=batches[0], qty=5, rate=15 + ) - self.assertSLEs(old3, update_invariants([ - {"actual_qty": 5, "stock_value_difference": 5*15, "stock_queue": [[10, 10], [10, 20], [5, 15]]}, - ])) + self.assertSLEs( + old3, + update_invariants( + [ + { + "actual_qty": 5, + "stock_value_difference": 5 * 15, + "stock_queue": [[10, 10], [10, 20], [5, 15]], + }, + ] + ), + ) new1 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=40) batches.append(new1.items[0].batch_no) # assert old queue remains - self.assertSLEs(new1, update_invariants([ - {"actual_qty": 10, "stock_value_difference": 10*40, "stock_queue": [[10, 10], [10, 20], [5, 15]]}, - ])) + self.assertSLEs( + new1, + update_invariants( + [ + { + "actual_qty": 10, + "stock_value_difference": 10 * 40, + "stock_queue": [[10, 10], [10, 20], [5, 15]], + }, + ] + ), + ) new2 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=42) batches.append(new2.items[0].batch_no) - self.assertSLEs(new2, update_invariants([ - {"actual_qty": 10, "stock_value_difference": 10*42, "stock_queue": [[10, 10], [10, 20], [5, 15]]}, - ])) + self.assertSLEs( + new2, + update_invariants( + [ + { + "actual_qty": 10, + "stock_value_difference": 10 * 42, + "stock_queue": [[10, 10], [10, 20], [5, 15]], + }, + ] + ), + ) # consume old batch as per FIFO - consume_old1 = make_stock_entry(item_code=item_code, source=warehouse, qty=15, batch_no=batches[0]) - self.assertSLEs(consume_old1, update_invariants([ - {"actual_qty": -15, "stock_value_difference": -10*10 - 5*20, "stock_queue": [[5, 20], [5, 15]]}, - ])) + consume_old1 = make_stock_entry( + item_code=item_code, source=warehouse, qty=15, batch_no=batches[0] + ) + self.assertSLEs( + consume_old1, + update_invariants( + [ + { + "actual_qty": -15, + "stock_value_difference": -10 * 10 - 5 * 20, + "stock_queue": [[5, 20], [5, 15]], + }, + ] + ), + ) # consume new batch as per batch - consume_new2 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-1]) - self.assertSLEs(consume_new2, update_invariants([ - {"actual_qty": -10, "stock_value_difference": -10*42, "stock_queue": [[5, 20], [5, 15]]}, - ])) + consume_new2 = make_stock_entry( + item_code=item_code, source=warehouse, qty=10, batch_no=batches[-1] + ) + self.assertSLEs( + consume_new2, + update_invariants( + [ + {"actual_qty": -10, "stock_value_difference": -10 * 42, "stock_queue": [[5, 20], [5, 15]]}, + ] + ), + ) # finish all old batches - consume_old2 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[1]) - self.assertSLEs(consume_old2, update_invariants([ - {"actual_qty": -10, "stock_value_difference": -5*20 - 5*15, "stock_queue": []}, - ])) + consume_old2 = make_stock_entry( + item_code=item_code, source=warehouse, qty=10, batch_no=batches[1] + ) + self.assertSLEs( + consume_old2, + update_invariants( + [ + {"actual_qty": -10, "stock_value_difference": -5 * 20 - 5 * 15, "stock_queue": []}, + ] + ), + ) # finish all new batches - consume_new1 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-2]) - self.assertSLEs(consume_new1, update_invariants([ - {"actual_qty": -10, "stock_value_difference": -10*40, "stock_queue": []}, - ])) + consume_new1 = make_stock_entry( + item_code=item_code, source=warehouse, qty=10, batch_no=batches[-2] + ) + self.assertSLEs( + consume_new1, + update_invariants( + [ + {"actual_qty": -10, "stock_value_difference": -10 * 40, "stock_queue": []}, + ] + ), + ) def test_fifo_dependent_consumption(self): item = make_item("_TestFifoTransferRates") @@ -686,12 +989,12 @@ class TestStockLedgerEntry(FrappeTestCase): expected_queues = [] for idx, rate in enumerate(rates, start=1): - expected_queues.append( - {"stock_queue": [[10, 10 * i] for i in range(1, idx + 1)]} - ) + expected_queues.append({"stock_queue": [[10, 10 * i] for i in range(1, idx + 1)]}) self.assertSLEs(receipt, expected_queues) - transfer = make_stock_entry(item_code=item.name, source=source, target=target, qty=10, do_not_save=True, rate=10) + transfer = make_stock_entry( + item_code=item.name, source=source, target=target, qty=10, do_not_save=True, rate=10 + ) for rate in rates[1:]: row = frappe.copy_doc(transfer.items[0], ignore_no_copy=False) transfer.append("items", row) @@ -709,7 +1012,9 @@ class TestStockLedgerEntry(FrappeTestCase): rates = [10 * i for i in range(1, 5)] - receipt = make_stock_entry(item_code=rm.name, target=warehouse, qty=10, do_not_save=True, rate=10) + receipt = make_stock_entry( + item_code=rm.name, target=warehouse, qty=10, do_not_save=True, rate=10 + ) for rate in rates[1:]: row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False) row.basic_rate = rate @@ -718,26 +1023,30 @@ class TestStockLedgerEntry(FrappeTestCase): receipt.save() receipt.submit() - repack = make_stock_entry(item_code=rm.name, source=warehouse, qty=10, - do_not_save=True, rate=10, purpose="Repack") + repack = make_stock_entry( + item_code=rm.name, source=warehouse, qty=10, do_not_save=True, rate=10, purpose="Repack" + ) for rate in rates[1:]: row = frappe.copy_doc(repack.items[0], ignore_no_copy=False) repack.append("items", row) - repack.append("items", { - "item_code": packed.name, - "t_warehouse": warehouse, - "qty": 1, - "transfer_qty": 1, - }) + repack.append( + "items", + { + "item_code": packed.name, + "t_warehouse": warehouse, + "qty": 1, + "transfer_qty": 1, + }, + ) repack.save() repack.submit() # same exact queue should be transferred - self.assertSLEs(repack, [ - {"incoming_rate": sum(rates) * 10} - ], sle_filters={"item_code": packed.name}) + self.assertSLEs( + repack, [{"incoming_rate": sum(rates) * 10}], sle_filters={"item_code": packed.name} + ) def test_negative_fifo_valuation(self): """ @@ -750,19 +1059,14 @@ class TestStockLedgerEntry(FrappeTestCase): receipt = make_stock_entry(item_code=item, target=warehouse, qty=10, rate=10) consume1 = make_stock_entry(item_code=item, source=warehouse, qty=15) - self.assertSLEs(consume1, [ - {"stock_value": -5 * 10, "stock_queue": [[-5, 10]]} - ]) + self.assertSLEs(consume1, [{"stock_value": -5 * 10, "stock_queue": [[-5, 10]]}]) consume2 = make_stock_entry(item_code=item, source=warehouse, qty=5) - self.assertSLEs(consume2, [ - {"stock_value": -10 * 10, "stock_queue": [[-10, 10]]} - ]) + self.assertSLEs(consume2, [{"stock_value": -10 * 10, "stock_queue": [[-10, 10]]}]) receipt2 = make_stock_entry(item_code=item, target=warehouse, qty=15, rate=15) - self.assertSLEs(receipt2, [ - {"stock_queue": [[5, 15]], "stock_value_difference": 175} - ]) + self.assertSLEs(receipt2, [{"stock_queue": [[5, 15]], "stock_value_difference": 175}]) + def create_repack_entry(**args): args = frappe._dict(args) @@ -771,51 +1075,63 @@ def create_repack_entry(**args): 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 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( + "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.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.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"] + 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": @@ -827,7 +1143,10 @@ def create_items(): return items -def setup_item_valuation_test(valuation_method="FIFO", suffix=None, use_batchwise_valuation=1, batches_list=['X', 'Y']): + +def setup_item_valuation_test( + valuation_method="FIFO", suffix=None, use_batchwise_valuation=1, batches_list=["X", "Y"] +): from erpnext.stock.doctype.batch.batch import make_batch from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse @@ -837,9 +1156,9 @@ def setup_item_valuation_test(valuation_method="FIFO", suffix=None, use_batchwis item = make_item( f"IV - Test Item {valuation_method} {suffix}", - dict(valuation_method=valuation_method, has_batch_no=1, create_new_batch=1) + dict(valuation_method=valuation_method, has_batch_no=1, create_new_batch=1), ) - warehouses = [create_warehouse(f"IV - Test Warehouse {i}") for i in ['J', 'K']] + warehouses = [create_warehouse(f"IV - Test Warehouse {i}") for i in ["J", "K"]] batches = [f"IV - Test Batch {i} {valuation_method} {suffix}" for i in batches_list] for i, batch_id in enumerate(batches): @@ -847,11 +1166,9 @@ def setup_item_valuation_test(valuation_method="FIFO", suffix=None, use_batchwis ubw = use_batchwise_valuation if isinstance(use_batchwise_valuation, (list, tuple)): ubw = use_batchwise_valuation[i] - batch = frappe.get_doc(frappe._dict( - doctype="Batch", - batch_id=batch_id, - item=item.item_code, - use_batchwise_valuation=ubw + batch = frappe.get_doc( + frappe._dict( + doctype="Batch", batch_id=batch_id, item=item.item_code, use_batchwise_valuation=ubw ) ).insert() batch.use_batchwise_valuation = ubw @@ -859,8 +1176,10 @@ def setup_item_valuation_test(valuation_method="FIFO", suffix=None, use_batchwis return item.item_code, warehouses, batches + def create_purchase_receipt_entries_for_batchwise_item_valuation_test(pr_entry_list): from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + prs = [] for item, warehouse, batch_no, qty, rate in pr_entry_list: @@ -869,17 +1188,15 @@ def create_purchase_receipt_entries_for_batchwise_item_valuation_test(pr_entry_l return prs + def create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list): from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + dns = [] for item, warehouse, batch_no, qty, rate in dn_entry_list: so = make_sales_order( - rate=rate, - qty=qty, - item=item, - warehouse=warehouse, - against_blanket_order=0 + rate=rate, qty=qty, item=item, warehouse=warehouse, against_blanket_order=0 ) dn = make_delivery_note(so.name) @@ -889,20 +1206,25 @@ def create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list dns.append(dn) return dns + def fetch_sle_details_for_doc_list(doc_list, columns, as_dict=1): - return frappe.db.sql(f""" + return frappe.db.sql( + f""" SELECT { ', '.join(columns)} FROM `tabStock Ledger Entry` WHERE voucher_no IN %(voucher_nos)s and docstatus = 1 ORDER BY timestamp(posting_date, posting_time) ASC, CREATION ASC - """, dict( - voucher_nos=[doc.name for doc in doc_list] - ), as_dict=as_dict) + """, + dict(voucher_nos=[doc.name for doc in doc_list]), + as_dict=as_dict, + ) + def get_stock_value_from_q(q): - return sum(r*q for r,q in json.loads(q)) + return sum(r * q for r, q in json.loads(q)) + def create_stock_entry_entries_for_batchwise_item_valuation_test(se_entry_list, purpose): ses = [] @@ -913,23 +1235,17 @@ def create_stock_entry_entries_for_batchwise_item_valuation_test(se_entry_list, company="_Test Company", batch_no=batch, posting_date=posting_date, - purpose=purpose + purpose=purpose, ) if purpose == "Material Receipt": - args.update( - dict(to_warehouse=target, rate=rate) - ) + args.update(dict(to_warehouse=target, rate=rate)) elif purpose == "Material Issue": - args.update( - dict(from_warehouse=source) - ) + args.update(dict(from_warehouse=source)) elif purpose == "Material Transfer": - args.update( - dict(from_warehouse=source, to_warehouse=target) - ) + args.update(dict(from_warehouse=source, to_warehouse=target)) else: raise ValueError(f"Invalid purpose: {purpose}") @@ -937,6 +1253,7 @@ def create_stock_entry_entries_for_batchwise_item_valuation_test(se_entry_list, return ses + def get_unique_suffix(): # Used to isolate valuation sensitive # tests to prevent future tests from failing. @@ -944,7 +1261,6 @@ def get_unique_suffix(): class TestDeferredNaming(FrappeTestCase): - @classmethod def setUpClass(cls) -> None: super().setUpClass() @@ -957,10 +1273,22 @@ class TestDeferredNaming(FrappeTestCase): self.company = "_Test Company with perpetual inventory" def tearDown(self) -> None: - make_property_setter(doctype="GL Entry", for_doctype=True, - property="autoname", value=self.gle_autoname, property_type="Data", fieldname=None) - make_property_setter(doctype="Stock Ledger Entry", for_doctype=True, - property="autoname", value=self.sle_autoname, property_type="Data", fieldname=None) + make_property_setter( + doctype="GL Entry", + for_doctype=True, + property="autoname", + value=self.gle_autoname, + property_type="Data", + fieldname=None, + ) + make_property_setter( + doctype="Stock Ledger Entry", + for_doctype=True, + property="autoname", + value=self.sle_autoname, + property_type="Data", + fieldname=None, + ) # since deferred naming autocommits, commit all changes to avoid flake frappe.db.commit() # nosemgrep @@ -973,12 +1301,13 @@ class TestDeferredNaming(FrappeTestCase): return gle, sle def test_deferred_naming(self): - se = make_stock_entry(item_code=self.item, to_warehouse=self.warehouse, - qty=10, rate=100, company=self.company) + se = make_stock_entry( + item_code=self.item, to_warehouse=self.warehouse, qty=10, rate=100, company=self.company + ) gle, sle = self.get_gle_sles(se) rename_gle_sle_docs() - renamed_gle, renamed_sle = self.get_gle_sles(se) + renamed_gle, renamed_sle = self.get_gle_sles(se) self.assertFalse(gle & renamed_gle, msg="GLEs not renamed") self.assertFalse(sle & renamed_sle, msg="SLEs not renamed") @@ -987,15 +1316,22 @@ class TestDeferredNaming(FrappeTestCase): def test_hash_naming(self): # disable naming series for doctype in ("GL Entry", "Stock Ledger Entry"): - make_property_setter(doctype=doctype, for_doctype=True, - property="autoname", value="hash", property_type="Data", fieldname=None) + make_property_setter( + doctype=doctype, + for_doctype=True, + property="autoname", + value="hash", + property_type="Data", + fieldname=None, + ) - se = make_stock_entry(item_code=self.item, to_warehouse=self.warehouse, - qty=10, rate=100, company=self.company) + se = make_stock_entry( + item_code=self.item, to_warehouse=self.warehouse, qty=10, rate=100, company=self.company + ) gle, sle = self.get_gle_sles(se) rename_gle_sle_docs() - renamed_gle, renamed_sle = self.get_gle_sles(se) + renamed_gle, renamed_sle = self.get_gle_sles(se) self.assertEqual(gle, renamed_gle, msg="GLEs are renamed while using hash naming") self.assertEqual(sle, renamed_sle, msg="SLEs are renamed while using hash naming") diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 82a8c3717d..07a8566d4a 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -14,8 +14,13 @@ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.utils import get_stock_balance -class OpeningEntryAccountError(frappe.ValidationError): pass -class EmptyStockReconciliationItemsError(frappe.ValidationError): pass +class OpeningEntryAccountError(frappe.ValidationError): + pass + + +class EmptyStockReconciliationItemsError(frappe.ValidationError): + pass + class StockReconciliation(StockController): def __init__(self, *args, **kwargs): @@ -24,9 +29,11 @@ class StockReconciliation(StockController): def validate(self): if not self.expense_account: - self.expense_account = frappe.get_cached_value('Company', self.company, "stock_adjustment_account") + self.expense_account = frappe.get_cached_value( + "Company", self.company, "stock_adjustment_account" + ) if not self.cost_center: - self.cost_center = frappe.get_cached_value('Company', self.company, "cost_center") + self.cost_center = frappe.get_cached_value("Company", self.company, "cost_center") self.validate_posting_time() self.remove_items_with_no_change() self.validate_data() @@ -37,8 +44,8 @@ class StockReconciliation(StockController): self.set_total_qty_and_amount() self.validate_putaway_capacity() - if self._action=="submit": - self.make_batches('warehouse') + if self._action == "submit": + self.make_batches("warehouse") def on_submit(self): self.update_stock_ledger() @@ -46,10 +53,11 @@ class StockReconciliation(StockController): 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', 'Repost Item Valuation') + 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() @@ -57,13 +65,17 @@ class StockReconciliation(StockController): def remove_items_with_no_change(self): """Remove items if qty or rate is not changed""" self.difference_amount = 0.0 - def _changed(item): - item_dict = get_stock_balance_for(item.item_code, item.warehouse, - self.posting_date, self.posting_time, batch_no=item.batch_no) - if ((item.qty is None or item.qty==item_dict.get("qty")) and - (item.valuation_rate is None or item.valuation_rate==item_dict.get("rate")) and - (not item.serial_no or (item.serial_no == item_dict.get("serial_nos")) )): + def _changed(item): + item_dict = get_stock_balance_for( + item.item_code, item.warehouse, self.posting_date, self.posting_time, batch_no=item.batch_no + ) + + if ( + (item.qty is None or item.qty == item_dict.get("qty")) + and (item.valuation_rate is None or item.valuation_rate == item_dict.get("rate")) + and (not item.serial_no or (item.serial_no == item_dict.get("serial_nos"))) + ): return False else: # set default as current rates @@ -80,16 +92,20 @@ class StockReconciliation(StockController): item.current_qty = item_dict.get("qty") item.current_valuation_rate = item_dict.get("rate") - self.difference_amount += (flt(item.qty, item.precision("qty")) * \ - flt(item.valuation_rate or item_dict.get("rate"), item.precision("valuation_rate")) \ - - flt(item_dict.get("qty"), item.precision("qty")) * flt(item_dict.get("rate"), item.precision("valuation_rate"))) + self.difference_amount += flt(item.qty, item.precision("qty")) * flt( + item.valuation_rate or item_dict.get("rate"), item.precision("valuation_rate") + ) - flt(item_dict.get("qty"), item.precision("qty")) * flt( + item_dict.get("rate"), item.precision("valuation_rate") + ) return True items = list(filter(lambda d: _changed(d), self.items)) if not items: - frappe.throw(_("None of the items have any change in quantity or value."), - EmptyStockReconciliationItemsError) + frappe.throw( + _("None of the items have any change in quantity or value."), + EmptyStockReconciliationItemsError, + ) elif len(items) != len(self.items): self.items = items @@ -99,7 +115,7 @@ class StockReconciliation(StockController): def validate_data(self): def _get_msg(row_num, msg): - return _("Row # {0}:").format(row_num+1) + " " + msg + return _("Row # {0}:").format(row_num + 1) + " " + msg self.validation_messages = [] item_warehouse_combinations = [] @@ -109,7 +125,7 @@ class StockReconciliation(StockController): for row_num, row in enumerate(self.items): # find duplicates key = [row.item_code, row.warehouse] - for field in ['serial_no', 'batch_no']: + for field in ["serial_no", "batch_no"]: if row.get(field): key.append(row.get(field)) @@ -126,32 +142,35 @@ class StockReconciliation(StockController): # if both not specified if row.qty in ["", None] and row.valuation_rate in ["", None]: - self.validation_messages.append(_get_msg(row_num, - _("Please specify either Quantity or Valuation Rate or both"))) + self.validation_messages.append( + _get_msg(row_num, _("Please specify either Quantity or Valuation Rate or both")) + ) # do not allow negative quantity if flt(row.qty) < 0: - self.validation_messages.append(_get_msg(row_num, - _("Negative Quantity is not allowed"))) + self.validation_messages.append(_get_msg(row_num, _("Negative Quantity is not allowed"))) # do not allow negative valuation if flt(row.valuation_rate) < 0: - self.validation_messages.append(_get_msg(row_num, - _("Negative Valuation Rate is not allowed"))) + self.validation_messages.append(_get_msg(row_num, _("Negative Valuation Rate is not allowed"))) if row.qty and row.valuation_rate in ["", None]: - row.valuation_rate = get_stock_balance(row.item_code, row.warehouse, - self.posting_date, self.posting_time, with_valuation_rate=True)[1] + row.valuation_rate = get_stock_balance( + row.item_code, row.warehouse, self.posting_date, self.posting_time, with_valuation_rate=True + )[1] if not row.valuation_rate: # try if there is a buying price list in default currency - buying_rate = frappe.db.get_value("Item Price", {"item_code": row.item_code, - "buying": 1, "currency": default_currency}, "price_list_rate") + buying_rate = frappe.db.get_value( + "Item Price", + {"item_code": row.item_code, "buying": 1, "currency": default_currency}, + "price_list_rate", + ) if buying_rate: row.valuation_rate = buying_rate else: # get valuation rate from Item - row.valuation_rate = frappe.get_value('Item', row.item_code, 'valuation_rate') + row.valuation_rate = frappe.get_value("Item", row.item_code, "valuation_rate") # throw all validation messages if self.validation_messages: @@ -178,7 +197,9 @@ class StockReconciliation(StockController): # item should not be serialized if item.has_serial_no and not row.serial_no and not item.serial_no_series: - raise frappe.ValidationError(_("Serial no(s) required for serialized item {0}").format(item_code)) + raise frappe.ValidationError( + _("Serial no(s) required for serialized item {0}").format(item_code) + ) # item managed batch-wise not allowed if item.has_batch_no and not row.batch_no and not item.create_new_batch: @@ -191,8 +212,8 @@ class StockReconciliation(StockController): self.validation_messages.append(_("Row #") + " " + ("%d: " % (row.idx)) + cstr(e)) def update_stock_ledger(self): - """ find difference between current and expected entries - and create stock ledger entries based on the difference""" + """find difference between current and expected entries + and create stock ledger entries based on the difference""" from erpnext.stock.stock_ledger import get_previous_sle sl_entries = [] @@ -208,15 +229,20 @@ class StockReconciliation(StockController): self.get_sle_for_serialized_items(row, sl_entries) else: if row.serial_no or row.batch_no: - frappe.throw(_("Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it.") \ - .format(row.idx, frappe.bold(row.item_code))) + frappe.throw( + _( + "Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it." + ).format(row.idx, frappe.bold(row.item_code)) + ) - previous_sle = get_previous_sle({ - "item_code": row.item_code, - "warehouse": row.warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time - }) + previous_sle = get_previous_sle( + { + "item_code": row.item_code, + "warehouse": row.warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + } + ) if previous_sle: if row.qty in ("", None): @@ -226,12 +252,16 @@ class StockReconciliation(StockController): row.valuation_rate = previous_sle.get("valuation_rate", 0) if row.qty and not row.valuation_rate and not row.allow_zero_valuation_rate: - frappe.throw(_("Valuation Rate required for Item {0} at row {1}").format(row.item_code, row.idx)) + frappe.throw( + _("Valuation Rate required for Item {0} at row {1}").format(row.item_code, row.idx) + ) - if ((previous_sle and row.qty == previous_sle.get("qty_after_transaction") - and (row.valuation_rate == previous_sle.get("valuation_rate") or row.qty == 0)) - or (not previous_sle and not row.qty)): - continue + if ( + previous_sle + and row.qty == previous_sle.get("qty_after_transaction") + and (row.valuation_rate == previous_sle.get("valuation_rate") or row.qty == 0) + ) or (not previous_sle and not row.qty): + continue sl_entries.append(self.get_sle_for_items(row)) @@ -253,21 +283,24 @@ class StockReconciliation(StockController): serial_nos = get_serial_nos(row.serial_no) - # To issue existing serial nos if row.current_qty and (row.current_serial_no or row.batch_no): args = self.get_sle_for_items(row) - args.update({ - 'actual_qty': -1 * row.current_qty, - 'serial_no': row.current_serial_no, - 'batch_no': row.batch_no, - 'valuation_rate': row.current_valuation_rate - }) + args.update( + { + "actual_qty": -1 * row.current_qty, + "serial_no": row.current_serial_no, + "batch_no": row.batch_no, + "valuation_rate": row.current_valuation_rate, + } + ) if row.current_serial_no: - args.update({ - 'qty_after_transaction': 0, - }) + args.update( + { + "qty_after_transaction": 0, + } + ) sl_entries.append(args) @@ -275,42 +308,49 @@ class StockReconciliation(StockController): for serial_no in serial_nos: args = self.get_sle_for_items(row, [serial_no]) - previous_sle = get_previous_sle({ - "item_code": row.item_code, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "serial_no": serial_no - }) + previous_sle = get_previous_sle( + { + "item_code": row.item_code, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "serial_no": serial_no, + } + ) if previous_sle and row.warehouse != previous_sle.get("warehouse"): # If serial no exists in different warehouse - warehouse = previous_sle.get("warehouse", '') or row.warehouse + warehouse = previous_sle.get("warehouse", "") or row.warehouse if not qty_after_transaction: - qty_after_transaction = get_stock_balance(row.item_code, - warehouse, self.posting_date, self.posting_time) + qty_after_transaction = get_stock_balance( + row.item_code, warehouse, self.posting_date, self.posting_time + ) qty_after_transaction -= 1 new_args = args.copy() - new_args.update({ - 'actual_qty': -1, - 'qty_after_transaction': qty_after_transaction, - 'warehouse': warehouse, - 'valuation_rate': previous_sle.get("valuation_rate") - }) + new_args.update( + { + "actual_qty": -1, + "qty_after_transaction": qty_after_transaction, + "warehouse": warehouse, + "valuation_rate": previous_sle.get("valuation_rate"), + } + ) sl_entries.append(new_args) if row.qty: args = self.get_sle_for_items(row) - args.update({ - 'actual_qty': row.qty, - 'incoming_rate': row.valuation_rate, - 'valuation_rate': row.valuation_rate - }) + args.update( + { + "actual_qty": row.qty, + "incoming_rate": row.valuation_rate, + "valuation_rate": row.valuation_rate, + } + ) sl_entries.append(args) @@ -320,7 +360,8 @@ class StockReconciliation(StockController): def update_valuation_rate_for_serial_no(self): for d in self.items: - if not d.serial_no: continue + if not d.serial_no: + continue serial_nos = get_serial_nos(d.serial_no) self.update_valuation_rate_for_serial_nos(d, serial_nos) @@ -331,7 +372,7 @@ class StockReconciliation(StockController): return for d in serial_nos: - frappe.db.set_value("Serial No", d, 'purchase_rate', valuation_rate) + frappe.db.set_value("Serial No", d, "purchase_rate", valuation_rate) def get_sle_for_items(self, row, serial_nos=None): """Insert Stock Ledger Entries""" @@ -339,22 +380,24 @@ class StockReconciliation(StockController): if not serial_nos and row.serial_no: serial_nos = get_serial_nos(row.serial_no) - data = frappe._dict({ - "doctype": "Stock Ledger Entry", - "item_code": row.item_code, - "warehouse": row.warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "voucher_type": self.doctype, - "voucher_no": self.name, - "voucher_detail_no": row.name, - "company": self.company, - "stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"), - "is_cancelled": 1 if self.docstatus == 2 else 0, - "serial_no": '\n'.join(serial_nos) if serial_nos else '', - "batch_no": row.batch_no, - "valuation_rate": flt(row.valuation_rate, row.precision("valuation_rate")) - }) + data = frappe._dict( + { + "doctype": "Stock Ledger Entry", + "item_code": row.item_code, + "warehouse": row.warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "voucher_type": self.doctype, + "voucher_no": self.name, + "voucher_detail_no": row.name, + "company": self.company, + "stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"), + "is_cancelled": 1 if self.docstatus == 2 else 0, + "serial_no": "\n".join(serial_nos) if serial_nos else "", + "batch_no": row.batch_no, + "valuation_rate": flt(row.valuation_rate, row.precision("valuation_rate")), + } + ) if not row.batch_no: data.qty_after_transaction = flt(row.qty, row.precision("qty")) @@ -382,7 +425,7 @@ class StockReconciliation(StockController): for row in self.items: if row.serial_no or row.batch_no or row.current_serial_no: has_serial_no = True - serial_nos = '' + serial_nos = "" if row.current_serial_no: serial_nos = get_serial_nos(row.current_serial_no) @@ -395,10 +438,11 @@ class StockReconciliation(StockController): sl_entries = self.merge_similar_item_serial_nos(sl_entries) sl_entries.reverse() - allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) + allow_negative_stock = cint( + frappe.db.get_single_value("Stock Settings", "allow_negative_stock") + ) self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock) - def merge_similar_item_serial_nos(self, sl_entries): # If user has put the same item in multiple row with different serial no new_sl_entries = [] @@ -411,16 +455,16 @@ class StockReconciliation(StockController): key = (d.item_code, d.warehouse) if key not in merge_similar_entries: - d.total_amount = (d.actual_qty * d.valuation_rate) + d.total_amount = d.actual_qty * d.valuation_rate merge_similar_entries[key] = d elif d.serial_no: data = merge_similar_entries[key] data.actual_qty += d.actual_qty data.qty_after_transaction += d.qty_after_transaction - data.total_amount += (d.actual_qty * d.valuation_rate) + data.total_amount += d.actual_qty * d.valuation_rate data.valuation_rate = (data.total_amount) / data.actual_qty - data.serial_no += '\n' + d.serial_no + data.serial_no += "\n" + d.serial_no data.incoming_rate = (data.total_amount) / data.actual_qty @@ -433,8 +477,9 @@ class StockReconciliation(StockController): if not self.cost_center: msgprint(_("Please enter Cost Center"), raise_exception=1) - return super(StockReconciliation, self).get_gl_entries(warehouse_account, - self.expense_account, self.cost_center) + return super(StockReconciliation, self).get_gl_entries( + warehouse_account, self.expense_account, self.cost_center + ) def validate_expense_account(self): if not cint(erpnext.is_perpetual_inventory_enabled(self.company)): @@ -442,29 +487,39 @@ class StockReconciliation(StockController): if not self.expense_account: frappe.throw(_("Please enter Expense Account")) - elif self.purpose == "Opening Stock" or not frappe.db.sql("""select name from `tabStock Ledger Entry` limit 1"""): + elif self.purpose == "Opening Stock" or not frappe.db.sql( + """select name from `tabStock Ledger Entry` limit 1""" + ): if frappe.db.get_value("Account", self.expense_account, "report_type") == "Profit and Loss": - frappe.throw(_("Difference Account must be a Asset/Liability type account, since this Stock Reconciliation is an Opening Entry"), OpeningEntryAccountError) + frappe.throw( + _( + "Difference Account must be a Asset/Liability type account, since this Stock Reconciliation is an Opening Entry" + ), + OpeningEntryAccountError, + ) def set_zero_value_for_customer_provided_items(self): changed_any_values = False - for d in self.get('items'): - is_customer_item = frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item') + for d in self.get("items"): + is_customer_item = frappe.db.get_value("Item", d.item_code, "is_customer_provided_item") if is_customer_item and d.valuation_rate: d.valuation_rate = 0.0 changed_any_values = True if changed_any_values: - msgprint(_("Valuation rate for customer provided items has been set to zero."), - title=_("Note"), indicator="blue") - + msgprint( + _("Valuation rate for customer provided items has been set to zero."), + title=_("Note"), + indicator="blue", + ) def set_total_qty_and_amount(self): for d in self.get("items"): d.amount = flt(d.qty, d.precision("qty")) * flt(d.valuation_rate, d.precision("valuation_rate")) - d.current_amount = (flt(d.current_qty, - d.precision("current_qty")) * flt(d.current_valuation_rate, d.precision("current_valuation_rate"))) + d.current_amount = flt(d.current_qty, d.precision("current_qty")) * flt( + d.current_valuation_rate, d.precision("current_valuation_rate") + ) d.quantity_difference = flt(d.qty) - flt(d.current_qty) d.amount_difference = flt(d.amount) - flt(d.current_amount) @@ -476,25 +531,33 @@ class StockReconciliation(StockController): def submit(self): if len(self.items) > 100: - msgprint(_("The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Draft stage")) - self.queue_action('submit', timeout=4600) + msgprint( + _( + "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Draft stage" + ) + ) + self.queue_action("submit", timeout=4600) else: self._submit() def cancel(self): if len(self.items) > 100: - msgprint(_("The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage")) - self.queue_action('cancel', timeout=2000) + msgprint( + _( + "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage" + ) + ) + self.queue_action("cancel", timeout=2000) else: self._cancel() + @frappe.whitelist() -def get_items(warehouse, posting_date, posting_time, company, item_code=None, ignore_empty_stock=False): +def get_items( + warehouse, posting_date, posting_time, company, item_code=None, ignore_empty_stock=False +): ignore_empty_stock = cint(ignore_empty_stock) - items = [frappe._dict({ - 'item_code': item_code, - 'warehouse': warehouse - })] + items = [frappe._dict({"item_code": item_code, "warehouse": warehouse})] if not item_code: items = get_items_for_stock_reco(warehouse, company) @@ -504,8 +567,9 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None, ig for d in items: if d.item_code in itemwise_batch_data: - valuation_rate = get_stock_balance(d.item_code, d.warehouse, - posting_date, posting_time, with_valuation_rate=True)[1] + valuation_rate = get_stock_balance( + d.item_code, d.warehouse, posting_date, posting_time, with_valuation_rate=True + )[1] for row in itemwise_batch_data.get(d.item_code): if ignore_empty_stock and not row.qty: @@ -514,12 +578,22 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None, ig args = get_item_data(row, row.qty, valuation_rate) res.append(args) else: - stock_bal = get_stock_balance(d.item_code, d.warehouse, posting_date, posting_time, - with_valuation_rate=True , with_serial_no=cint(d.has_serial_no)) - qty, valuation_rate, serial_no = stock_bal[0], stock_bal[1], stock_bal[2] if cint(d.has_serial_no) else '' + stock_bal = get_stock_balance( + d.item_code, + d.warehouse, + posting_date, + posting_time, + with_valuation_rate=True, + with_serial_no=cint(d.has_serial_no), + ) + qty, valuation_rate, serial_no = ( + stock_bal[0], + stock_bal[1], + stock_bal[2] if cint(d.has_serial_no) else "", + ) if ignore_empty_stock and not stock_bal[0]: - continue + continue args = get_item_data(d, qty, valuation_rate, serial_no) @@ -527,9 +601,11 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None, ig return res + def get_items_for_stock_reco(warehouse, company): lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) - items = frappe.db.sql(f""" + items = frappe.db.sql( + f""" select i.name as item_code, i.item_name, bin.warehouse as warehouse, i.has_serial_no, i.has_batch_no from @@ -542,9 +618,12 @@ def get_items_for_stock_reco(warehouse, company): and exists( select name from `tabWarehouse` where lft >= {lft} and rgt <= {rgt} and name = bin.warehouse ) - """, as_dict=1) + """, + as_dict=1, + ) - items += frappe.db.sql(""" + items += frappe.db.sql( + """ select i.name as item_code, i.item_name, id.default_warehouse as warehouse, i.has_serial_no, i.has_batch_no from @@ -559,40 +638,50 @@ def get_items_for_stock_reco(warehouse, company): and IFNULL(i.disabled, 0) = 0 and id.company = %s group by i.name - """, (lft, rgt, company), as_dict=1) + """, + (lft, rgt, company), + as_dict=1, + ) # remove duplicates # check if item-warehouse key extracted from each entry exists in set iw_keys # and update iw_keys iw_keys = set() - items = [item for item in items if [(item.item_code, item.warehouse) not in iw_keys, iw_keys.add((item.item_code, item.warehouse))][0]] + items = [ + item + for item in items + if [ + (item.item_code, item.warehouse) not in iw_keys, + iw_keys.add((item.item_code, item.warehouse)), + ][0] + ] return items + def get_item_data(row, qty, valuation_rate, serial_no=None): return { - 'item_code': row.item_code, - 'warehouse': row.warehouse, - 'qty': qty, - 'item_name': row.item_name, - 'valuation_rate': valuation_rate, - 'current_qty': qty, - 'current_valuation_rate': valuation_rate, - 'current_serial_no': serial_no, - 'serial_no': serial_no, - 'batch_no': row.get('batch_no') + "item_code": row.item_code, + "warehouse": row.warehouse, + "qty": qty, + "item_name": row.item_name, + "valuation_rate": valuation_rate, + "current_qty": qty, + "current_valuation_rate": valuation_rate, + "current_serial_no": serial_no, + "serial_no": serial_no, + "batch_no": row.get("batch_no"), } + def get_itemwise_batch(warehouse, posting_date, company, item_code=None): from erpnext.stock.report.batch_wise_balance_history.batch_wise_balance_history import execute + itemwise_batch_data = {} - filters = frappe._dict({ - 'warehouse': warehouse, - 'from_date': posting_date, - 'to_date': posting_date, - 'company': company - }) + filters = frappe._dict( + {"warehouse": warehouse, "from_date": posting_date, "to_date": posting_date, "company": company} + ) if item_code: filters.item_code = item_code @@ -600,23 +689,28 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None): columns, data = execute(filters) for row in data: - itemwise_batch_data.setdefault(row[0], []).append(frappe._dict({ - 'item_code': row[0], - 'warehouse': warehouse, - 'qty': row[8], - 'item_name': row[1], - 'batch_no': row[4] - })) + itemwise_batch_data.setdefault(row[0], []).append( + frappe._dict( + { + "item_code": row[0], + "warehouse": warehouse, + "qty": row[8], + "item_name": row[1], + "batch_no": row[4], + } + ) + ) return itemwise_batch_data -@frappe.whitelist() -def get_stock_balance_for(item_code, warehouse, - posting_date, posting_time, batch_no=None, with_valuation_rate= True): - frappe.has_permission("Stock Reconciliation", "write", throw = True) - item_dict = frappe.db.get_value("Item", item_code, - ["has_serial_no", "has_batch_no"], as_dict=1) +@frappe.whitelist() +def get_stock_balance_for( + item_code, warehouse, posting_date, posting_time, batch_no=None, with_valuation_rate=True +): + frappe.has_permission("Stock Reconciliation", "write", throw=True) + + item_dict = frappe.db.get_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1) if not item_dict: # In cases of data upload to Items table @@ -625,8 +719,14 @@ def get_stock_balance_for(item_code, warehouse, serial_nos = "" with_serial_no = True if item_dict.get("has_serial_no") else False - data = get_stock_balance(item_code, warehouse, posting_date, posting_time, - with_valuation_rate=with_valuation_rate, with_serial_no=with_serial_no) + data = get_stock_balance( + item_code, + warehouse, + posting_date, + posting_time, + with_valuation_rate=with_valuation_rate, + with_serial_no=with_serial_no, + ) if with_serial_no: qty, rate, serial_nos = data @@ -634,20 +734,20 @@ def get_stock_balance_for(item_code, warehouse, qty, rate = data if item_dict.get("has_batch_no"): - qty = get_batch_qty(batch_no, warehouse, posting_date=posting_date, posting_time=posting_time) or 0 + qty = ( + get_batch_qty(batch_no, warehouse, posting_date=posting_date, posting_time=posting_time) or 0 + ) + + return {"qty": qty, "rate": rate, "serial_nos": serial_nos} - return { - 'qty': qty, - 'rate': rate, - 'serial_nos': serial_nos - } @frappe.whitelist() def get_difference_account(purpose, company): - if purpose == 'Stock Reconciliation': + if purpose == "Stock Reconciliation": account = get_company_default(company, "stock_adjustment_account") else: - account = frappe.db.get_value('Account', {'is_group': 0, - 'company': company, 'account_type': 'Temporary'}, 'name') + account = frappe.db.get_value( + "Account", {"is_group": 0, "company": company, "account_type": "Temporary"}, "name" + ) return account diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index e6b252e856..46a3f2a5b6 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -32,7 +32,6 @@ class TestStockReconciliation(FrappeTestCase): def tearDown(self): frappe.flags.dont_execute_stock_reposts = None - def test_reco_for_fifo(self): self._test_reco_sle_gle("FIFO") @@ -40,55 +39,72 @@ class TestStockReconciliation(FrappeTestCase): self._test_reco_sle_gle("Moving Average") def _test_reco_sle_gle(self, valuation_method): - se1, se2, se3 = insert_existing_sle(warehouse='Stores - TCP1') - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + 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]] + # posting_time, expected_stock_value, bin_qty, bin_valuation]] input_data = [ [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"] + [0, "", "2012-12-31", "12:10"], ] for d in input_data: set_valuation_method("_Test Item", valuation_method) - last_sle = get_previous_sle({ - "item_code": "_Test Item", - "warehouse": "Stores - TCP1", - "posting_date": d[2], - "posting_time": d[3] - }) + last_sle = get_previous_sle( + { + "item_code": "_Test Item", + "warehouse": "Stores - TCP1", + "posting_date": d[2], + "posting_time": d[3], + } + ) # submit stock reconciliation - stock_reco = create_stock_reconciliation(qty=d[0], rate=d[1], - posting_date=d[2], posting_time=d[3], warehouse="Stores - TCP1", - company=company, expense_account = "Stock Adjustment - TCP1") + stock_reco = create_stock_reconciliation( + qty=d[0], + rate=d[1], + posting_date=d[2], + posting_time=d[3], + warehouse="Stores - TCP1", + company=company, + expense_account="Stock Adjustment - TCP1", + ) # check stock value - sle = frappe.db.sql("""select * from `tabStock Ledger Entry` - where voucher_type='Stock Reconciliation' and voucher_no=%s""", stock_reco.name, as_dict=1) + sle = frappe.db.sql( + """select * from `tabStock Ledger Entry` + where voucher_type='Stock Reconciliation' and voucher_no=%s""", + stock_reco.name, + as_dict=1, + ) qty_after_transaction = flt(d[0]) if d[0] != "" else flt(last_sle.get("qty_after_transaction")) valuation_rate = flt(d[1]) if d[1] != "" else flt(last_sle.get("valuation_rate")) - if qty_after_transaction == last_sle.get("qty_after_transaction") \ - and valuation_rate == last_sle.get("valuation_rate"): - self.assertFalse(sle) + if qty_after_transaction == last_sle.get( + "qty_after_transaction" + ) and valuation_rate == last_sle.get("valuation_rate"): + self.assertFalse(sle) else: self.assertEqual(flt(sle[0].qty_after_transaction, 1), flt(qty_after_transaction, 1)) self.assertEqual(flt(sle[0].stock_value, 1), flt(qty_after_transaction * valuation_rate, 1)) # no gl entries - self.assertTrue(frappe.db.get_value("Stock Ledger Entry", - {"voucher_type": "Stock Reconciliation", "voucher_no": stock_reco.name})) + self.assertTrue( + frappe.db.get_value( + "Stock Ledger Entry", {"voucher_type": "Stock Reconciliation", "voucher_no": stock_reco.name} + ) + ) - acc_bal, stock_bal, wh_list = get_stock_and_account_balance("Stock In Hand - TCP1", - stock_reco.posting_date, stock_reco.company) + acc_bal, stock_bal, wh_list = get_stock_and_account_balance( + "Stock In Hand - TCP1", stock_reco.posting_date, stock_reco.company + ) self.assertEqual(flt(acc_bal, 1), flt(stock_bal, 1)) stock_reco.cancel() @@ -98,18 +114,33 @@ class TestStockReconciliation(FrappeTestCase): se1.cancel() def test_get_items(self): - 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", "company": "_Test Company"}) + 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", + "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) + create_item( + "_Test Stock Reco Item", + is_stock_item=1, + valuation_rate=100, + warehouse="_Test Warehouse Ledger 1 - _TC", + opening_stock=100, + ) items = get_items("_Test Warehouse Group 1 - _TC", nowdate(), nowtime(), "_Test Company") - self.assertEqual(["_Test Stock Reco Item", "_Test Warehouse Ledger 1 - _TC", 100], - [items[0]["item_code"], items[0]["warehouse"], items[0]["qty"]]) + self.assertEqual( + ["_Test Stock Reco Item", "_Test Warehouse Ledger 1 - _TC", 100], + [items[0]["item_code"], items[0]["warehouse"], items[0]["qty"]], + ) def test_stock_reco_for_serialized_item(self): to_delete_records = [] @@ -119,8 +150,9 @@ class TestStockReconciliation(FrappeTestCase): serial_item_code = "Stock-Reco-Serial-Item-1" serial_warehouse = "_Test Warehouse for Stock Reco1 - _TC" - sr = create_stock_reconciliation(item_code=serial_item_code, - warehouse = serial_warehouse, qty=5, rate=200) + sr = create_stock_reconciliation( + item_code=serial_item_code, warehouse=serial_warehouse, qty=5, rate=200 + ) serial_nos = get_serial_nos(sr.items[0].serial_no) self.assertEqual(len(serial_nos), 5) @@ -130,7 +162,7 @@ class TestStockReconciliation(FrappeTestCase): "warehouse": serial_warehouse, "posting_date": nowdate(), "posting_time": nowtime(), - "serial_no": sr.items[0].serial_no + "serial_no": sr.items[0].serial_no, } valuation_rate = get_incoming_rate(args) @@ -138,8 +170,9 @@ class TestStockReconciliation(FrappeTestCase): to_delete_records.append(sr.name) - sr = create_stock_reconciliation(item_code=serial_item_code, - warehouse = serial_warehouse, qty=5, rate=300) + sr = create_stock_reconciliation( + item_code=serial_item_code, warehouse=serial_warehouse, qty=5, rate=300 + ) serial_nos1 = get_serial_nos(sr.items[0].serial_no) self.assertEqual(len(serial_nos1), 5) @@ -149,7 +182,7 @@ class TestStockReconciliation(FrappeTestCase): "warehouse": serial_warehouse, "posting_date": nowdate(), "posting_time": nowtime(), - "serial_no": sr.items[0].serial_no + "serial_no": sr.items[0].serial_no, } valuation_rate = get_incoming_rate(args) @@ -162,7 +195,6 @@ class TestStockReconciliation(FrappeTestCase): stock_doc = frappe.get_doc("Stock Reconciliation", d) stock_doc.cancel() - def test_stock_reco_for_merge_serialized_item(self): to_delete_records = [] @@ -170,23 +202,34 @@ class TestStockReconciliation(FrappeTestCase): serial_item_code = "Stock-Reco-Serial-Item-2" serial_warehouse = "_Test Warehouse for Stock Reco1 - _TC" - sr = create_stock_reconciliation(item_code=serial_item_code, serial_no=random_string(6), - warehouse = serial_warehouse, qty=1, rate=100, do_not_submit=True, purpose='Opening Stock') + sr = create_stock_reconciliation( + item_code=serial_item_code, + serial_no=random_string(6), + warehouse=serial_warehouse, + qty=1, + rate=100, + do_not_submit=True, + purpose="Opening Stock", + ) for i in range(3): - sr.append('items', { - 'item_code': serial_item_code, - 'warehouse': serial_warehouse, - 'qty': 1, - 'valuation_rate': 100, - 'serial_no': random_string(6) - }) + sr.append( + "items", + { + "item_code": serial_item_code, + "warehouse": serial_warehouse, + "qty": 1, + "valuation_rate": 100, + "serial_no": random_string(6), + }, + ) sr.save() sr.submit() - sle_entries = frappe.get_all('Stock Ledger Entry', filters= {'voucher_no': sr.name}, - fields = ['name', 'incoming_rate']) + sle_entries = frappe.get_all( + "Stock Ledger Entry", filters={"voucher_no": sr.name}, fields=["name", "incoming_rate"] + ) self.assertEqual(len(sle_entries), 1) self.assertEqual(sle_entries[0].incoming_rate, 100) @@ -205,8 +248,9 @@ class TestStockReconciliation(FrappeTestCase): item_code = "Stock-Reco-batch-Item-1" warehouse = "_Test Warehouse for Stock Reco2 - _TC" - sr = create_stock_reconciliation(item_code=item_code, - warehouse = warehouse, qty=5, rate=200, do_not_submit=1) + sr = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=5, rate=200, do_not_submit=1 + ) sr.save() sr.submit() @@ -214,8 +258,9 @@ class TestStockReconciliation(FrappeTestCase): self.assertTrue(batch_no) to_delete_records.append(sr.name) - sr1 = create_stock_reconciliation(item_code=item_code, - warehouse = warehouse, qty=6, rate=300, batch_no=batch_no) + sr1 = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=6, rate=300, batch_no=batch_no + ) args = { "item_code": item_code, @@ -229,9 +274,9 @@ class TestStockReconciliation(FrappeTestCase): self.assertEqual(valuation_rate, 300) to_delete_records.append(sr1.name) - - sr2 = create_stock_reconciliation(item_code=item_code, - warehouse = warehouse, qty=0, rate=0, batch_no=batch_no) + sr2 = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=0, rate=0, batch_no=batch_no + ) stock_value = get_stock_value_on(warehouse, nowdate(), item_code) self.assertEqual(stock_value, 0) @@ -243,11 +288,12 @@ class TestStockReconciliation(FrappeTestCase): stock_doc.cancel() def test_customer_provided_items(self): - item_code = 'Stock-Reco-customer-Item-100' - create_item(item_code, is_customer_provided_item = 1, - customer = '_Test Customer', is_purchase_item = 0) + item_code = "Stock-Reco-customer-Item-100" + create_item( + item_code, is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0 + ) - sr = create_stock_reconciliation(item_code = item_code, qty = 10, rate = 420) + sr = create_stock_reconciliation(item_code=item_code, qty=10, rate=420) self.assertEqual(sr.get("items")[0].allow_zero_valuation_rate, 1) self.assertEqual(sr.get("items")[0].valuation_rate, 0) @@ -255,65 +301,79 @@ class TestStockReconciliation(FrappeTestCase): def test_backdated_stock_reco_qty_reposting(self): """ - Test if a backdated stock reco recalculates future qty until next reco. - ------------------------------------------- - Var | Doc | Qty | Balance - ------------------------------------------- - SR5 | Reco | 0 | 8 (posting date: today-4) [backdated] - PR1 | PR | 10 | 18 (posting date: today-3) - PR2 | PR | 1 | 19 (posting date: today-2) - SR4 | Reco | 0 | 6 (posting date: today-1) [backdated] - PR3 | PR | 1 | 7 (posting date: today) # can't post future PR + Test if a backdated stock reco recalculates future qty until next reco. + ------------------------------------------- + Var | Doc | Qty | Balance + ------------------------------------------- + SR5 | Reco | 0 | 8 (posting date: today-4) [backdated] + PR1 | PR | 10 | 18 (posting date: today-3) + PR2 | PR | 1 | 19 (posting date: today-2) + SR4 | Reco | 0 | 6 (posting date: today-1) [backdated] + PR3 | PR | 1 | 7 (posting date: today) # can't post future PR """ item_code = "Backdated-Reco-Item" warehouse = "_Test Warehouse - _TC" create_item(item_code) - pr1 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=10, rate=100, - posting_date=add_days(nowdate(), -3)) - pr2 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=1, rate=100, - posting_date=add_days(nowdate(), -2)) - pr3 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=1, rate=100, - posting_date=nowdate()) + pr1 = make_purchase_receipt( + item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3) + ) + pr2 = make_purchase_receipt( + item_code=item_code, warehouse=warehouse, qty=1, rate=100, posting_date=add_days(nowdate(), -2) + ) + pr3 = make_purchase_receipt( + item_code=item_code, warehouse=warehouse, qty=1, rate=100, posting_date=nowdate() + ) - pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, - "qty_after_transaction") - pr3_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, - "qty_after_transaction") + pr1_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction" + ) + pr3_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, "qty_after_transaction" + ) self.assertEqual(pr1_balance, 10) self.assertEqual(pr3_balance, 12) # post backdated stock reco in between - sr4 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=6, rate=100, - posting_date=add_days(nowdate(), -1)) - pr3_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, - "qty_after_transaction") + sr4 = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=6, rate=100, posting_date=add_days(nowdate(), -1) + ) + pr3_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, "qty_after_transaction" + ) self.assertEqual(pr3_balance, 7) # post backdated stock reco at the start - sr5 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=8, rate=100, - posting_date=add_days(nowdate(), -4)) - pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, - "qty_after_transaction") - pr2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, - "qty_after_transaction") - sr4_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, - "qty_after_transaction") + sr5 = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=8, rate=100, posting_date=add_days(nowdate(), -4) + ) + pr1_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction" + ) + pr2_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, "qty_after_transaction" + ) + sr4_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, "qty_after_transaction" + ) self.assertEqual(pr1_balance, 18) self.assertEqual(pr2_balance, 19) - self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected + self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected # cancel backdated stock reco and check future impact sr5.cancel() - pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, - "qty_after_transaction") - pr2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, - "qty_after_transaction") - sr4_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, - "qty_after_transaction") + pr1_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction" + ) + pr2_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, "qty_after_transaction" + ) + sr4_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, "qty_after_transaction" + ) self.assertEqual(pr1_balance, 10) self.assertEqual(pr2_balance, 11) - self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected + self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected # teardown sr4.cancel() @@ -324,13 +384,13 @@ class TestStockReconciliation(FrappeTestCase): @change_settings("Stock Settings", {"allow_negative_stock": 0}) def test_backdated_stock_reco_future_negative_stock(self): """ - Test if a backdated stock reco causes future negative stock and is blocked. - ------------------------------------------- - Var | Doc | Qty | Balance - ------------------------------------------- - PR1 | PR | 10 | 10 (posting date: today-2) - SR3 | Reco | 0 | 1 (posting date: today-1) [backdated & blocked] - DN2 | DN | -2 | 8(-1) (posting date: today) + Test if a backdated stock reco causes future negative stock and is blocked. + ------------------------------------------- + Var | Doc | Qty | Balance + ------------------------------------------- + PR1 | PR | 10 | 10 (posting date: today-2) + SR3 | Reco | 0 | 1 (posting date: today-1) [backdated & blocked] + DN2 | DN | -2 | 8(-1) (posting date: today) """ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.stock_ledger import NegativeStockError @@ -339,22 +399,31 @@ class TestStockReconciliation(FrappeTestCase): warehouse = "_Test Warehouse - _TC" create_item(item_code) + pr1 = make_purchase_receipt( + item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -2) + ) + dn2 = create_delivery_note( + item_code=item_code, warehouse=warehouse, qty=2, rate=120, posting_date=nowdate() + ) - pr1 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=10, rate=100, - posting_date=add_days(nowdate(), -2)) - dn2 = create_delivery_note(item_code=item_code, warehouse=warehouse, qty=2, rate=120, - posting_date=nowdate()) - - pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, - "qty_after_transaction") - dn2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": dn2.name, "is_cancelled": 0}, - "qty_after_transaction") + pr1_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction" + ) + dn2_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": dn2.name, "is_cancelled": 0}, "qty_after_transaction" + ) self.assertEqual(pr1_balance, 10) self.assertEqual(dn2_balance, 8) # check if stock reco is blocked - sr3 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=1, rate=100, - posting_date=add_days(nowdate(), -1), do_not_submit=True) + sr3 = create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=1, + rate=100, + posting_date=add_days(nowdate(), -1), + do_not_submit=True, + ) self.assertRaises(NegativeStockError, sr3.submit) # teardown @@ -362,16 +431,15 @@ class TestStockReconciliation(FrappeTestCase): dn2.cancel() pr1.cancel() - @change_settings("Stock Settings", {"allow_negative_stock": 0}) def test_backdated_stock_reco_cancellation_future_negative_stock(self): """ - Test if a backdated stock reco cancellation that causes future negative stock is blocked. - ------------------------------------------- - Var | Doc | Qty | Balance - ------------------------------------------- - SR | Reco | 100 | 100 (posting date: today-1) (shouldn't be cancelled after DN) - DN | DN | 100 | 0 (posting date: today) + Test if a backdated stock reco cancellation that causes future negative stock is blocked. + ------------------------------------------- + Var | Doc | Qty | Balance + ------------------------------------------- + SR | Reco | 100 | 100 (posting date: today-1) (shouldn't be cancelled after DN) + DN | DN | 100 | 0 (posting date: today) """ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.stock_ledger import NegativeStockError @@ -380,15 +448,21 @@ class TestStockReconciliation(FrappeTestCase): warehouse = "_Test Warehouse - _TC" create_item(item_code) + sr = create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=100, + rate=100, + posting_date=add_days(nowdate(), -1), + ) - sr = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=100, rate=100, - posting_date=add_days(nowdate(), -1)) + dn = create_delivery_note( + item_code=item_code, warehouse=warehouse, qty=100, rate=120, posting_date=nowdate() + ) - dn = create_delivery_note(item_code=item_code, warehouse=warehouse, qty=100, rate=120, - posting_date=nowdate()) - - dn_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": dn.name, "is_cancelled": 0}, - "qty_after_transaction") + dn_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": dn.name, "is_cancelled": 0}, "qty_after_transaction" + ) self.assertEqual(dn_balance, 0) # check if cancellation of stock reco is blocked @@ -400,12 +474,12 @@ class TestStockReconciliation(FrappeTestCase): def test_intermediate_sr_bin_update(self): """Bin should show correct qty even for backdated entries. - ------------------------------------------- - | creation | Var | Doc | Qty | balance qty - ------------------------------------------- - | 1 | SR | Reco | 10 | 10 (posting date: today+10) - | 3 | SR2 | Reco | 11 | 11 (posting date: today+11) - | 2 | DN | DN | 5 | 6 <-- assert in BIN (posting date: today+12) + ------------------------------------------- + | creation | Var | Doc | Qty | balance qty + ------------------------------------------- + | 1 | SR | Reco | 10 | 10 (posting date: today+10) + | 3 | SR2 | Reco | 11 | 11 (posting date: today+11) + | 2 | DN | DN | 5 | 6 <-- assert in BIN (posting date: today+12) """ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note @@ -417,26 +491,33 @@ class TestStockReconciliation(FrappeTestCase): warehouse = "_Test Warehouse - _TC" create_item(item_code) - sr = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=10, rate=100, - posting_date=add_days(nowdate(), 10)) + sr = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), 10) + ) - dn = create_delivery_note(item_code=item_code, warehouse=warehouse, qty=5, rate=120, - posting_date=add_days(nowdate(), 12)) - old_bin_qty = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty") + dn = create_delivery_note( + item_code=item_code, warehouse=warehouse, qty=5, rate=120, posting_date=add_days(nowdate(), 12) + ) + old_bin_qty = frappe.db.get_value( + "Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty" + ) - sr2 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=11, rate=100, - posting_date=add_days(nowdate(), 11)) - new_bin_qty = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty") + sr2 = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=11, rate=100, posting_date=add_days(nowdate(), 11) + ) + new_bin_qty = frappe.db.get_value( + "Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty" + ) self.assertEqual(old_bin_qty + 1, new_bin_qty) frappe.db.rollback() - def test_valid_batch(self): create_batch_item_with_batch("Testing Batch Item 1", "001") create_batch_item_with_batch("Testing Batch Item 2", "002") - sr = create_stock_reconciliation(item_code="Testing Batch Item 1", qty=1, rate=100, batch_no="002" - , do_not_submit=True) + sr = create_stock_reconciliation( + item_code="Testing Batch Item 1", qty=1, rate=100, batch_no="002", do_not_submit=True + ) self.assertRaises(frappe.ValidationError, sr.submit) def test_serial_no_cancellation(self): @@ -458,15 +539,17 @@ class TestStockReconciliation(FrappeTestCase): serial_nos.pop() new_serial_nos = "\n".join(serial_nos) - sr = create_stock_reconciliation(item_code=item.name, warehouse=warehouse, serial_no=new_serial_nos, qty=9) + sr = create_stock_reconciliation( + item_code=item.name, warehouse=warehouse, serial_no=new_serial_nos, qty=9 + ) sr.cancel() - active_sr_no = frappe.get_all("Serial No", - filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"}) + active_sr_no = frappe.get_all( + "Serial No", filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"} + ) self.assertEqual(len(active_sr_no), 10) - def test_serial_no_creation_and_inactivation(self): item = create_item("_TestItemCreatedWithStockReco", is_stock_item=1) if not item.has_serial_no: @@ -476,19 +559,27 @@ class TestStockReconciliation(FrappeTestCase): item_code = item.name warehouse = "_Test Warehouse - _TC" - sr = create_stock_reconciliation(item_code=item.name, warehouse=warehouse, - serial_no="SR-CREATED-SR-NO", qty=1, do_not_submit=True, rate=100) + sr = create_stock_reconciliation( + item_code=item.name, + warehouse=warehouse, + serial_no="SR-CREATED-SR-NO", + qty=1, + do_not_submit=True, + rate=100, + ) sr.save() self.assertEqual(cstr(sr.items[0].current_serial_no), "") sr.submit() - active_sr_no = frappe.get_all("Serial No", - filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"}) + active_sr_no = frappe.get_all( + "Serial No", filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"} + ) self.assertEqual(len(active_sr_no), 1) sr.cancel() - active_sr_no = frappe.get_all("Serial No", - filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"}) + active_sr_no = frappe.get_all( + "Serial No", filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"} + ) self.assertEqual(len(active_sr_no), 0) @@ -499,32 +590,51 @@ def create_batch_item_with_batch(item_name, batch_id): batch_item_doc.create_new_batch = 1 batch_item_doc.save(ignore_permissions=True) - if not frappe.db.exists('Batch', batch_id): - b = frappe.new_doc('Batch') + if not frappe.db.exists("Batch", batch_id): + b = frappe.new_doc("Batch") b.item = item_name b.batch_id = batch_id b.save() + def insert_existing_sle(warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - se1 = make_stock_entry(posting_date="2012-12-15", posting_time="02:00", item_code="_Test Item", - target=warehouse, qty=10, basic_rate=700) + se1 = make_stock_entry( + posting_date="2012-12-15", + posting_time="02:00", + item_code="_Test Item", + target=warehouse, + qty=10, + basic_rate=700, + ) - se2 = make_stock_entry(posting_date="2012-12-25", posting_time="03:00", item_code="_Test Item", - source=warehouse, qty=15) + se2 = make_stock_entry( + posting_date="2012-12-25", posting_time="03:00", item_code="_Test Item", source=warehouse, qty=15 + ) - se3 = make_stock_entry(posting_date="2013-01-05", posting_time="07:00", item_code="_Test Item", - target=warehouse, qty=15, basic_rate=1200) + 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"}) - create_warehouse("_Test Warehouse for Stock Reco2", - {"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"}) +def create_batch_or_serial_no_items(): + create_warehouse( + "_Test Warehouse for Stock Reco1", + {"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"}, + ) + + create_warehouse( + "_Test Warehouse for Stock Reco2", + {"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"}, + ) serial_item_doc = create_item("Stock-Reco-Serial-Item-1", is_stock_item=1) if not serial_item_doc.has_serial_no: @@ -545,6 +655,7 @@ def create_batch_or_serial_no_items(): serial_item_doc.batch_number_series = "BASR.#####" batch_item_doc.save(ignore_permissions=True) + def create_stock_reconciliation(**args): args = frappe._dict(args) sr = frappe.new_doc("Stock Reconciliation") @@ -553,20 +664,26 @@ def create_stock_reconciliation(**args): sr.posting_time = args.posting_time or nowtime() sr.set_posting_time = 1 sr.company = args.company or "_Test Company" - sr.expense_account = args.expense_account or \ - ("Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC") - sr.cost_center = args.cost_center \ - or frappe.get_cached_value("Company", sr.company, "cost_center") \ + sr.expense_account = args.expense_account or ( + "Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC" + ) + sr.cost_center = ( + args.cost_center + or frappe.get_cached_value("Company", sr.company, "cost_center") or "_Test Cost Center - _TC" + ) - sr.append("items", { - "item_code": args.item_code or "_Test Item", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty, - "valuation_rate": args.rate, - "serial_no": args.serial_no, - "batch_no": args.batch_no - }) + sr.append( + "items", + { + "item_code": args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": args.qty, + "valuation_rate": args.rate, + "serial_no": args.serial_no, + "batch_no": args.batch_no, + }, + ) try: if not args.do_not_submit: @@ -575,6 +692,7 @@ def create_stock_reconciliation(**args): pass return sr + def set_valuation_method(item_code, valuation_method): existing_valuation_method = get_valuation_method(item_code) if valuation_method == existing_valuation_method: @@ -582,11 +700,13 @@ def set_valuation_method(item_code, valuation_method): 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"]): + for warehouse in frappe.get_all( + "Warehouse", filters={"company": "_Test Company"}, fields=["name", "is_group"] + ): if not warehouse.is_group: - update_entries_after({ - "item_code": item_code, - "warehouse": warehouse.name - }, allow_negative_stock=1) + update_entries_after( + {"item_code": item_code, "warehouse": warehouse.name}, allow_negative_stock=1 + ) + test_dependencies = ["Item", "Warehouse"] diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py index bab521d69f..e0c8ed12e7 100644 --- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py +++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py @@ -6,8 +6,6 @@ from frappe.utils import add_to_date, get_datetime, get_time_str, time_diff_in_h class StockRepostingSettings(Document): - - def validate(self): self.set_minimum_reposting_time_slot() diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index c1293cbf0f..e592a4be3c 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -16,24 +16,40 @@ from erpnext.stock.utils import check_pending_reposting class StockSettings(Document): def validate(self): - for key in ["item_naming_by", "item_group", "stock_uom", - "allow_negative_stock", "default_warehouse", "set_qty_in_transactions_based_on_serial_no_input"]: - frappe.db.set_default(key, self.get(key, "")) + for key in [ + "item_naming_by", + "item_group", + "stock_uom", + "allow_negative_stock", + "default_warehouse", + "set_qty_in_transactions_based_on_serial_no_input", + ]: + frappe.db.set_default(key, self.get(key, "")) from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series - set_by_naming_series("Item", "item_code", - self.get("item_naming_by")=="Naming Series", hide_name_field=True, make_mandatory=0) + + set_by_naming_series( + "Item", + "item_code", + self.get("item_naming_by") == "Naming Series", + hide_name_field=True, + make_mandatory=0, + ) stock_frozen_limit = 356 submitted_stock_frozen = self.stock_frozen_upto_days or 0 if submitted_stock_frozen > stock_frozen_limit: self.stock_frozen_upto_days = stock_frozen_limit - frappe.msgprint (_("`Freeze Stocks Older Than` should be smaller than %d days.") %stock_frozen_limit) + frappe.msgprint( + _("`Freeze Stocks Older Than` should be smaller than %d days.") % stock_frozen_limit + ) # show/hide barcode field for name in ["barcode", "barcodes", "scan_barcode"]: - frappe.make_property_setter({'fieldname': name, 'property': 'hidden', - 'value': 0 if self.show_barcode_field else 1}, validate_fields_for_doctype=False) + frappe.make_property_setter( + {"fieldname": name, "property": "hidden", "value": 0 if self.show_barcode_field else 1}, + validate_fields_for_doctype=False, + ) self.validate_warehouses() self.cant_change_valuation_method() @@ -44,8 +60,12 @@ class StockSettings(Document): warehouse_fields = ["default_warehouse", "sample_retention_warehouse"] for field in warehouse_fields: if frappe.db.get_value("Warehouse", self.get(field), "is_group"): - frappe.throw(_("Group Warehouses cannot be used in transactions. Please change the value of {0}") \ - .format(frappe.bold(self.meta.get_field(field).label)), title =_("Incorrect Warehouse")) + frappe.throw( + _("Group Warehouses cannot be used in transactions. Please change the value of {0}").format( + frappe.bold(self.meta.get_field(field).label) + ), + title=_("Incorrect Warehouse"), + ) def cant_change_valuation_method(self): db_valuation_method = frappe.db.get_single_value("Stock Settings", "valuation_method") @@ -53,38 +73,73 @@ class StockSettings(Document): if db_valuation_method and db_valuation_method != self.valuation_method: # check if there are any stock ledger entries against items # which does not have it's own valuation method - sle = frappe.db.sql("""select name from `tabStock Ledger Entry` sle + sle = frappe.db.sql( + """select name from `tabStock Ledger Entry` sle where exists(select name from tabItem where name=sle.item_code and (valuation_method is null or valuation_method='')) limit 1 - """) + """ + ) if sle: - frappe.throw(_("Can't change the valuation method, as there are transactions against some items which do not have its own valuation method")) + frappe.throw( + _( + "Can't change the valuation method, as there are transactions against some items which do not have its own valuation method" + ) + ) def validate_clean_description_html(self): - if int(self.clean_description_html or 0) \ - and not int(self.db_get('clean_description_html') or 0): + if int(self.clean_description_html or 0) and not int(self.db_get("clean_description_html") or 0): # changed to text - frappe.enqueue('erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions', now=frappe.flags.in_test) + frappe.enqueue( + "erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions", + now=frappe.flags.in_test, + ) def validate_pending_reposts(self): if self.stock_frozen_upto: check_pending_reposting(self.stock_frozen_upto) - def on_update(self): self.toggle_warehouse_field_for_inter_warehouse_transfer() def toggle_warehouse_field_for_inter_warehouse_transfer(self): - make_property_setter("Sales Invoice Item", "target_warehouse", "hidden", 1 - cint(self.allow_from_dn), "Check", validate_fields_for_doctype=False) - make_property_setter("Delivery Note Item", "target_warehouse", "hidden", 1 - cint(self.allow_from_dn), "Check", validate_fields_for_doctype=False) - make_property_setter("Purchase Invoice Item", "from_warehouse", "hidden", 1 - cint(self.allow_from_pr), "Check", validate_fields_for_doctype=False) - make_property_setter("Purchase Receipt Item", "from_warehouse", "hidden", 1 - cint(self.allow_from_pr), "Check", validate_fields_for_doctype=False) + make_property_setter( + "Sales Invoice Item", + "target_warehouse", + "hidden", + 1 - cint(self.allow_from_dn), + "Check", + validate_fields_for_doctype=False, + ) + make_property_setter( + "Delivery Note Item", + "target_warehouse", + "hidden", + 1 - cint(self.allow_from_dn), + "Check", + validate_fields_for_doctype=False, + ) + make_property_setter( + "Purchase Invoice Item", + "from_warehouse", + "hidden", + 1 - cint(self.allow_from_pr), + "Check", + validate_fields_for_doctype=False, + ) + make_property_setter( + "Purchase Receipt Item", + "from_warehouse", + "hidden", + 1 - cint(self.allow_from_pr), + "Check", + validate_fields_for_doctype=False, + ) def clean_all_descriptions(): - for item in frappe.get_all('Item', ['name', 'description']): + for item in frappe.get_all("Item", ["name", "description"]): if item.description: clean_description = clean_html(item.description) if item.description != clean_description: - frappe.db.set_value('Item', item.name, 'description', clean_description) + frappe.db.set_value("Item", item.name, "description", clean_description) diff --git a/erpnext/stock/doctype/stock_settings/test_stock_settings.py b/erpnext/stock/doctype/stock_settings/test_stock_settings.py index 13496718ea..974e16339b 100644 --- a/erpnext/stock/doctype/stock_settings/test_stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/test_stock_settings.py @@ -13,35 +13,45 @@ class TestStockSettings(FrappeTestCase): frappe.db.set_value("Stock Settings", None, "clean_description_html", 0) def test_settings(self): - item = frappe.get_doc(dict( - doctype = 'Item', - item_code = 'Item for description test', - item_group = 'Products', - description = '

    Drawing No. 07-xxx-PO132
    1800 x 1685 x 750
    All parts made of Marine Ply
    Top w/ Corian dd
    CO, CS, VIP Day Cabin

    ' - )).insert() + item = frappe.get_doc( + dict( + doctype="Item", + item_code="Item for description test", + item_group="Products", + description='

    Drawing No. 07-xxx-PO132
    1800 x 1685 x 750
    All parts made of Marine Ply
    Top w/ Corian dd
    CO, CS, VIP Day Cabin

    ', + ) + ).insert() - settings = frappe.get_single('Stock Settings') + settings = frappe.get_single("Stock Settings") settings.clean_description_html = 1 settings.save() item.reload() - self.assertEqual(item.description, '

    Drawing No. 07-xxx-PO132
    1800 x 1685 x 750
    All parts made of Marine Ply
    Top w/ Corian dd
    CO, CS, VIP Day Cabin

    ') + self.assertEqual( + item.description, + "

    Drawing No. 07-xxx-PO132
    1800 x 1685 x 750
    All parts made of Marine Ply
    Top w/ Corian dd
    CO, CS, VIP Day Cabin

    ", + ) item.delete() def test_clean_html(self): - settings = frappe.get_single('Stock Settings') + settings = frappe.get_single("Stock Settings") settings.clean_description_html = 1 settings.save() - item = frappe.get_doc(dict( - doctype = 'Item', - item_code = 'Item for description test', - item_group = 'Products', - description = '

    Drawing No. 07-xxx-PO132
    1800 x 1685 x 750
    All parts made of Marine Ply
    Top w/ Corian dd
    CO, CS, VIP Day Cabin

    ' - )).insert() + item = frappe.get_doc( + dict( + doctype="Item", + item_code="Item for description test", + item_group="Products", + description='

    Drawing No. 07-xxx-PO132
    1800 x 1685 x 750
    All parts made of Marine Ply
    Top w/ Corian dd
    CO, CS, VIP Day Cabin

    ', + ) + ).insert() - self.assertEqual(item.description, '

    Drawing No. 07-xxx-PO132
    1800 x 1685 x 750
    All parts made of Marine Ply
    Top w/ Corian dd
    CO, CS, VIP Day Cabin

    ') + self.assertEqual( + item.description, + "

    Drawing No. 07-xxx-PO132
    1800 x 1685 x 750
    All parts made of Marine Ply
    Top w/ Corian dd
    CO, CS, VIP Day Cabin

    ", + ) item.delete() diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py index 08d7c99352..1e9d01aa4b 100644 --- a/erpnext/stock/doctype/warehouse/test_warehouse.py +++ b/erpnext/stock/doctype/warehouse/test_warehouse.py @@ -11,13 +11,14 @@ from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.warehouse.warehouse import convert_to_group_or_ledger, get_children -test_records = frappe.get_test_records('Warehouse') +test_records = frappe.get_test_records("Warehouse") + class TestWarehouse(FrappeTestCase): def setUp(self): super().setUp() - if not frappe.get_value('Item', '_Test Item'): - make_test_records('Item') + if not frappe.get_value("Item", "_Test Item"): + make_test_records("Item") def test_parent_warehouse(self): parent_warehouse = frappe.get_doc("Warehouse", "_Test Warehouse Group - _TC") @@ -26,8 +27,12 @@ class TestWarehouse(FrappeTestCase): def test_warehouse_hierarchy(self): p_warehouse = frappe.get_doc("Warehouse", "_Test Warehouse Group - _TC") - child_warehouses = frappe.db.sql("""select name, is_group, parent_warehouse from `tabWarehouse` wh - where wh.lft > %s and wh.rgt < %s""", (p_warehouse.lft, p_warehouse.rgt), as_dict=1) + child_warehouses = frappe.db.sql( + """select name, is_group, parent_warehouse from `tabWarehouse` wh + where wh.lft > %s and wh.rgt < %s""", + (p_warehouse.lft, p_warehouse.rgt), + as_dict=1, + ) for child_warehouse in child_warehouses: self.assertEqual(p_warehouse.name, child_warehouse.parent_warehouse) @@ -36,13 +41,13 @@ class TestWarehouse(FrappeTestCase): def test_unlinking_warehouse_from_item_defaults(self): company = "_Test Company" - warehouse_names = [f'_Test Warehouse {i} for Unlinking' for i in range(2)] + warehouse_names = [f"_Test Warehouse {i} for Unlinking" for i in range(2)] warehouse_ids = [] for warehouse in warehouse_names: warehouse_id = create_warehouse(warehouse, company=company) warehouse_ids.append(warehouse_id) - item_names = [f'_Test Item {i} for Unlinking' for i in range(2)] + item_names = [f"_Test Item {i} for Unlinking" for i in range(2)] for item, warehouse in zip(item_names, warehouse_ids): create_item(item, warehouse=warehouse, company=company) @@ -52,17 +57,14 @@ class TestWarehouse(FrappeTestCase): # Check Item existance for item in item_names: - self.assertTrue( - bool(frappe.db.exists("Item", item)), - f"{item} doesn't exist" - ) + self.assertTrue(bool(frappe.db.exists("Item", item)), f"{item} doesn't exist") item_doc = frappe.get_doc("Item", item) for item_default in item_doc.item_defaults: self.assertNotIn( item_default.default_warehouse, warehouse_ids, - f"{item} linked to {item_default.default_warehouse} in {warehouse_ids}." + f"{item} linked to {item_default.default_warehouse} in {warehouse_ids}.", ) def test_group_non_group_conversion(self): @@ -90,7 +92,7 @@ class TestWarehouse(FrappeTestCase): company = "_Test Company" children = get_children("Warehouse", parent=company, company=company, is_root=True) - self.assertTrue(any(wh['value'] == "_Test Warehouse - _TC" for wh in children)) + self.assertTrue(any(wh["value"] == "_Test Warehouse - _TC" for wh in children)) def create_warehouse(warehouse_name, properties=None, company=None): @@ -111,40 +113,46 @@ def create_warehouse(warehouse_name, properties=None, company=None): else: return warehouse_id + def get_warehouse(**args): args = frappe._dict(args) - if(frappe.db.exists("Warehouse", args.warehouse_name + " - " + args.abbr)): + if frappe.db.exists("Warehouse", args.warehouse_name + " - " + args.abbr): return frappe.get_doc("Warehouse", args.warehouse_name + " - " + args.abbr) else: - w = frappe.get_doc({ - "company": args.company or "_Test Company", - "doctype": "Warehouse", - "warehouse_name": args.warehouse_name, - "is_group": 0, - "account": get_warehouse_account(args.warehouse_name, args.company, args.abbr) - }) + w = frappe.get_doc( + { + "company": args.company or "_Test Company", + "doctype": "Warehouse", + "warehouse_name": args.warehouse_name, + "is_group": 0, + "account": get_warehouse_account(args.warehouse_name, args.company, args.abbr), + } + ) w.insert() return w + def get_warehouse_account(warehouse_name, company, company_abbr=None): if not company_abbr: - company_abbr = frappe.get_cached_value("Company", company, 'abbr') + company_abbr = frappe.get_cached_value("Company", company, "abbr") if not frappe.db.exists("Account", warehouse_name + " - " + company_abbr): return create_account( account_name=warehouse_name, parent_account=get_group_stock_account(company, company_abbr), - account_type='Stock', - company=company) + account_type="Stock", + company=company, + ) else: return warehouse_name + " - " + company_abbr def get_group_stock_account(company, company_abbr=None): - group_stock_account = frappe.db.get_value("Account", - filters={'account_type': 'Stock', 'is_group': 1, 'company': company}, fieldname='name') + group_stock_account = frappe.db.get_value( + "Account", filters={"account_type": "Stock", "is_group": 1, "company": company}, fieldname="name" + ) if not group_stock_account: if not company_abbr: - company_abbr = frappe.get_cached_value("Company", company, 'abbr') + company_abbr = frappe.get_cached_value("Company", company, "abbr") group_stock_account = "Current Assets - " + company_abbr return group_stock_account diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index 4c7f41dcb5..c892ba3ddc 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -14,23 +14,25 @@ from erpnext.stock import get_warehouse_account class Warehouse(NestedSet): - nsm_parent_field = 'parent_warehouse' + nsm_parent_field = "parent_warehouse" def autoname(self): if self.company: - suffix = " - " + frappe.get_cached_value('Company', self.company, "abbr") + suffix = " - " + frappe.get_cached_value("Company", self.company, "abbr") if not self.warehouse_name.endswith(suffix): self.name = self.warehouse_name + suffix else: self.name = self.warehouse_name def onload(self): - '''load account name for General Ledger Report''' - if self.company and cint(frappe.db.get_value("Company", self.company, "enable_perpetual_inventory")): + """load account name for General Ledger Report""" + if self.company and cint( + frappe.db.get_value("Company", self.company, "enable_perpetual_inventory") + ): account = self.account or get_warehouse_account(self) if account: - self.set_onload('account', account) + self.set_onload("account", account) load_address_and_contact(self) def on_update(self): @@ -43,9 +45,19 @@ class Warehouse(NestedSet): # delete bin bins = frappe.get_all("Bin", fields="*", filters={"warehouse": self.name}) for d in bins: - if d['actual_qty'] or d['reserved_qty'] or d['ordered_qty'] or \ - d['indented_qty'] or d['projected_qty'] or d['planned_qty']: - throw(_("Warehouse {0} can not be deleted as quantity exists for Item {1}").format(self.name, d['item_code'])) + if ( + d["actual_qty"] + or d["reserved_qty"] + or d["ordered_qty"] + or d["indented_qty"] + or d["projected_qty"] + or d["planned_qty"] + ): + throw( + _("Warehouse {0} can not be deleted as quantity exists for Item {1}").format( + self.name, d["item_code"] + ) + ) if self.check_if_sle_exists(): throw(_("Warehouse can not be deleted as stock ledger entry exists for this warehouse.")) @@ -90,23 +102,24 @@ class Warehouse(NestedSet): def unlink_from_items(self): frappe.db.set_value("Item Default", {"default_warehouse": self.name}, "default_warehouse", None) + @frappe.whitelist() def get_children(doctype, parent=None, company=None, is_root=False): if is_root: parent = "" - fields = ['name as value', 'is_group as expandable'] + fields = ["name as value", "is_group as expandable"] filters = [ - ['docstatus', '<', '2'], - ['ifnull(`parent_warehouse`, "")', '=', parent], - ['company', 'in', (company, None,'')] + ["docstatus", "<", "2"], + ['ifnull(`parent_warehouse`, "")', "=", parent], + ["company", "in", (company, None, "")], ] - warehouses = frappe.get_list(doctype, fields=fields, filters=filters, order_by='name') + warehouses = frappe.get_list(doctype, fields=fields, filters=filters, order_by="name") - company_currency = '' + company_currency = "" if company: - company_currency = frappe.get_cached_value('Company', company, 'default_currency') + company_currency = frappe.get_cached_value("Company", company, "default_currency") warehouse_wise_value = get_warehouse_wise_stock_value(company) @@ -117,14 +130,20 @@ def get_children(doctype, parent=None, company=None, is_root=False): wh["company_currency"] = company_currency return warehouses -def get_warehouse_wise_stock_value(company): - warehouses = frappe.get_all('Warehouse', - fields = ['name', 'parent_warehouse'], filters = {'company': company}) - parent_warehouse = {d.name : d.parent_warehouse for d in warehouses} - filters = {'warehouse': ('in', [data.name for data in warehouses])} - bin_data = frappe.get_all('Bin', fields = ['sum(stock_value) as stock_value', 'warehouse'], - filters = filters, group_by = 'warehouse') +def get_warehouse_wise_stock_value(company): + warehouses = frappe.get_all( + "Warehouse", fields=["name", "parent_warehouse"], filters={"company": company} + ) + parent_warehouse = {d.name: d.parent_warehouse for d in warehouses} + + filters = {"warehouse": ("in", [data.name for data in warehouses])} + bin_data = frappe.get_all( + "Bin", + fields=["sum(stock_value) as stock_value", "warehouse"], + filters=filters, + group_by="warehouse", + ) warehouse_wise_stock_value = defaultdict(float) for row in bin_data: @@ -132,23 +151,30 @@ def get_warehouse_wise_stock_value(company): continue warehouse_wise_stock_value[row.warehouse] = row.stock_value - update_value_in_parent_warehouse(warehouse_wise_stock_value, - parent_warehouse, row.warehouse, row.stock_value) + update_value_in_parent_warehouse( + warehouse_wise_stock_value, parent_warehouse, row.warehouse, row.stock_value + ) return warehouse_wise_stock_value -def update_value_in_parent_warehouse(warehouse_wise_stock_value, parent_warehouse_dict, warehouse, stock_value): + +def update_value_in_parent_warehouse( + warehouse_wise_stock_value, parent_warehouse_dict, warehouse, stock_value +): parent_warehouse = parent_warehouse_dict.get(warehouse) if not parent_warehouse: return warehouse_wise_stock_value[parent_warehouse] += flt(stock_value) - update_value_in_parent_warehouse(warehouse_wise_stock_value, parent_warehouse_dict, - parent_warehouse, stock_value) + update_value_in_parent_warehouse( + warehouse_wise_stock_value, parent_warehouse_dict, parent_warehouse, stock_value + ) + @frappe.whitelist() def add_node(): from frappe.desk.treeview import make_tree_args + args = make_tree_args(**frappe.form_dict) if cint(args.is_root): @@ -156,33 +182,37 @@ def add_node(): frappe.get_doc(args).insert() + @frappe.whitelist() def convert_to_group_or_ledger(docname=None): if not docname: docname = frappe.form_dict.docname return frappe.get_doc("Warehouse", docname).convert_to_group_or_ledger() + def get_child_warehouses(warehouse): from frappe.utils.nestedset import get_descendants_of children = get_descendants_of("Warehouse", warehouse, ignore_permissions=True, order_by="lft") - return children + [warehouse] # append self for backward compatibility + return children + [warehouse] # append self for backward compatibility + def get_warehouses_based_on_account(account, company=None): warehouses = [] - for d in frappe.get_all("Warehouse", fields = ["name", "is_group"], - filters = {"account": account}): + for d in frappe.get_all("Warehouse", fields=["name", "is_group"], filters={"account": account}): if d.is_group: warehouses.extend(get_child_warehouses(d.name)) else: warehouses.append(d.name) - if (not warehouses and company and - frappe.get_cached_value("Company", company, "default_inventory_account") == account): - warehouses = [d.name for d in frappe.get_all("Warehouse", filters={'is_group': 0})] + if ( + not warehouses + and company + and frappe.get_cached_value("Company", company, "default_inventory_account") == account + ): + warehouses = [d.name for d in frappe.get_all("Warehouse", filters={"is_group": 0})] if not warehouses: - frappe.throw(_("Warehouse not found against the account {0}") - .format(account)) + frappe.throw(_("Warehouse not found against the account {0}").format(account)) return warehouses diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 9bb41b9dbb..f72588e034 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -23,31 +23,38 @@ from erpnext.stock.doctype.item.item import get_item_defaults, get_uom_conv_fact from erpnext.stock.doctype.item_manufacturer.item_manufacturer import get_item_manufacturer_part_no from erpnext.stock.doctype.price_list.price_list import get_price_list_details -sales_doctypes = ['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice', 'POS Invoice'] -purchase_doctypes = ['Material Request', 'Supplier Quotation', 'Purchase Order', 'Purchase Receipt', 'Purchase Invoice'] +sales_doctypes = ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"] +purchase_doctypes = [ + "Material Request", + "Supplier Quotation", + "Purchase Order", + "Purchase Receipt", + "Purchase Invoice", +] + @frappe.whitelist() def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=True): """ - args = { - "item_code": "", - "warehouse": None, - "customer": "", - "conversion_rate": 1.0, - "selling_price_list": None, - "price_list_currency": None, - "plc_conversion_rate": 1.0, - "doctype": "", - "name": "", - "supplier": None, - "transaction_date": None, - "conversion_rate": 1.0, - "buying_price_list": None, - "is_subcontracted": "Yes" / "No", - "ignore_pricing_rule": 0/1 - "project": "" - "set_warehouse": "" - } + args = { + "item_code": "", + "warehouse": None, + "customer": "", + "conversion_rate": 1.0, + "selling_price_list": None, + "price_list_currency": None, + "plc_conversion_rate": 1.0, + "doctype": "", + "name": "", + "supplier": None, + "transaction_date": None, + "conversion_rate": 1.0, + "buying_price_list": None, + "is_subcontracted": "Yes" / "No", + "ignore_pricing_rule": 0/1 + "project": "" + "set_warehouse": "" + } """ args = process_args(args) @@ -61,16 +68,21 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru if isinstance(doc, str): doc = json.loads(doc) - if doc and doc.get('doctype') == 'Purchase Invoice': - args['bill_date'] = doc.get('bill_date') + if doc and doc.get("doctype") == "Purchase Invoice": + args["bill_date"] = doc.get("bill_date") if doc: - args['posting_date'] = doc.get('posting_date') - args['transaction_date'] = doc.get('transaction_date') + args["posting_date"] = doc.get("posting_date") + args["transaction_date"] = doc.get("transaction_date") get_item_tax_template(args, item, out) - out["item_tax_rate"] = get_item_tax_map(args.company, args.get("item_tax_template") if out.get("item_tax_template") is None \ - else out.get("item_tax_template"), as_json=True) + out["item_tax_rate"] = get_item_tax_map( + args.company, + args.get("item_tax_template") + if out.get("item_tax_template") is None + else out.get("item_tax_template"), + as_json=True, + ) get_party_item_code(args, item, out) @@ -83,12 +95,14 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru if args.customer and cint(args.is_pos): out.update(get_pos_profile_item_details(args.company, args, update_data=True)) - if (args.get("doctype") == "Material Request" and - args.get("material_request_type") == "Material Transfer"): + if ( + args.get("doctype") == "Material Request" + and args.get("material_request_type") == "Material Transfer" + ): out.update(get_bin_details(args.item_code, args.get("from_warehouse"))) elif out.get("warehouse"): - if doc and doc.get('doctype') == 'Purchase Order': + if doc and doc.get("doctype") == "Purchase Order": # calculate company_total_stock only for po bin_details = get_bin_details(args.item_code, out.warehouse, args.company) else: @@ -101,28 +115,27 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru if args.get(key) is None: args[key] = value - data = get_pricing_rule_for_item(args, out.price_list_rate, - doc, for_validate=for_validate) + data = get_pricing_rule_for_item(args, out.price_list_rate, doc, for_validate=for_validate) out.update(data) update_stock(args, out) if args.transaction_date and item.lead_time_days: - out.schedule_date = out.lead_time_date = add_days(args.transaction_date, - item.lead_time_days) + out.schedule_date = out.lead_time_date = add_days(args.transaction_date, item.lead_time_days) if args.get("is_subcontracted") == "Yes": - out.bom = args.get('bom') or get_default_bom(args.item_code) + out.bom = args.get("bom") or get_default_bom(args.item_code) get_gross_profit(out) - if args.doctype == 'Material Request': + if args.doctype == "Material Request": out.rate = args.rate or out.price_list_rate out.amount = flt(args.qty) * flt(out.rate) out = remove_standard_fields(out) return out + def remove_standard_fields(details): for key in child_table_fields + default_fields: details.pop(key, None) @@ -130,9 +143,14 @@ def remove_standard_fields(details): def update_stock(args, out): - if (args.get("doctype") == "Delivery Note" or - (args.get("doctype") == "Sales Invoice" and args.get('update_stock'))) \ - and out.warehouse and out.stock_qty > 0: + if ( + ( + args.get("doctype") == "Delivery Note" + or (args.get("doctype") == "Sales Invoice" and args.get("update_stock")) + ) + and out.warehouse + and out.stock_qty > 0 + ): if out.has_batch_no and not args.get("batch_no"): out.batch_no = get_batch_no(out.item_code, out.warehouse, out.qty) @@ -140,9 +158,9 @@ def update_stock(args, out): if actual_batch_qty: out.update(actual_batch_qty) - if out.has_serial_no and args.get('batch_no'): + if out.has_serial_no and args.get("batch_no"): reserved_so = get_so_reservation_for_item(args) - out.batch_no = args.get('batch_no') + out.batch_no = args.get("batch_no") out.serial_no = get_serial_no(out, args.serial_no, sales_order=reserved_so) elif out.has_serial_no: @@ -156,13 +174,14 @@ def set_valuation_rate(out, args): bundled_items = frappe.get_doc("Product Bundle", args.item_code) for bundle_item in bundled_items.items: - valuation_rate += \ - flt(get_valuation_rate(bundle_item.item_code, args.company, out.get("warehouse")).get("valuation_rate") \ - * bundle_item.qty) + valuation_rate += flt( + get_valuation_rate(bundle_item.item_code, args.company, out.get("warehouse")).get( + "valuation_rate" + ) + * bundle_item.qty + ) - out.update({ - "valuation_rate": valuation_rate - }) + out.update({"valuation_rate": valuation_rate}) else: out.update(get_valuation_rate(args.item_code, args.company, out.get("warehouse"))) @@ -185,11 +204,13 @@ def process_args(args): set_transaction_type(args) return args + def process_string_args(args): if isinstance(args, str): args = json.loads(args) return args + @frappe.whitelist() def get_item_code(barcode=None, serial_no=None): if barcode: @@ -209,6 +230,7 @@ def validate_item_details(args, item): throw(_("Please specify Company")) from erpnext.stock.doctype.item.item import validate_end_of_life + validate_end_of_life(item.name, item.end_of_life, item.disabled) if args.transaction_type == "selling" and cint(item.has_variants): @@ -222,37 +244,37 @@ def validate_item_details(args, item): def get_basic_details(args, item, overwrite_warehouse=True): """ :param args: { - "item_code": "", - "warehouse": None, - "customer": "", - "conversion_rate": 1.0, - "selling_price_list": None, - "price_list_currency": None, - "price_list_uom_dependant": None, - "plc_conversion_rate": 1.0, - "doctype": "", - "name": "", - "supplier": None, - "transaction_date": None, - "conversion_rate": 1.0, - "buying_price_list": None, - "is_subcontracted": "Yes" / "No", - "ignore_pricing_rule": 0/1 - "project": "", - barcode: "", - serial_no: "", - currency: "", - update_stock: "", - price_list: "", - company: "", - order_type: "", - is_pos: "", - project: "", - qty: "", - stock_qty: "", - conversion_factor: "", - against_blanket_order: 0/1 - } + "item_code": "", + "warehouse": None, + "customer": "", + "conversion_rate": 1.0, + "selling_price_list": None, + "price_list_currency": None, + "price_list_uom_dependant": None, + "plc_conversion_rate": 1.0, + "doctype": "", + "name": "", + "supplier": None, + "transaction_date": None, + "conversion_rate": 1.0, + "buying_price_list": None, + "is_subcontracted": "Yes" / "No", + "ignore_pricing_rule": 0/1 + "project": "", + barcode: "", + serial_no: "", + currency: "", + update_stock: "", + price_list: "", + company: "", + order_type: "", + is_pos: "", + project: "", + qty: "", + stock_qty: "", + conversion_factor: "", + against_blanket_order: 0/1 + } :param item: `item_code` of Item object :return: frappe._dict """ @@ -267,77 +289,98 @@ def get_basic_details(args, item, overwrite_warehouse=True): item_group_defaults = get_item_group_defaults(item.name, args.company) brand_defaults = get_brand_defaults(item.name, args.company) - defaults = frappe._dict({ - 'item_defaults': item_defaults, - 'item_group_defaults': item_group_defaults, - 'brand_defaults': brand_defaults - }) + defaults = frappe._dict( + { + "item_defaults": item_defaults, + "item_group_defaults": item_group_defaults, + "brand_defaults": brand_defaults, + } + ) warehouse = get_item_warehouse(item, args, overwrite_warehouse, defaults) - if args.get('doctype') == "Material Request" and not args.get('material_request_type'): - args['material_request_type'] = frappe.db.get_value('Material Request', - args.get('name'), 'material_request_type', cache=True) + if args.get("doctype") == "Material Request" and not args.get("material_request_type"): + args["material_request_type"] = frappe.db.get_value( + "Material Request", args.get("name"), "material_request_type", cache=True + ) expense_account = None - if args.get('doctype') == 'Purchase Invoice' and item.is_fixed_asset: + if args.get("doctype") == "Purchase Invoice" and item.is_fixed_asset: from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account - expense_account = get_asset_category_account(fieldname = "fixed_asset_account", item = args.item_code, company= args.company) - #Set the UOM to the Default Sales UOM or Default Purchase UOM if configured in the Item Master - if not args.get('uom'): - if args.get('doctype') in sales_doctypes: + expense_account = get_asset_category_account( + fieldname="fixed_asset_account", item=args.item_code, company=args.company + ) + + # Set the UOM to the Default Sales UOM or Default Purchase UOM if configured in the Item Master + if not args.get("uom"): + if args.get("doctype") in sales_doctypes: args.uom = item.sales_uom if item.sales_uom else item.stock_uom - elif (args.get('doctype') in ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice']) or \ - (args.get('doctype') == 'Material Request' and args.get('material_request_type') == 'Purchase'): + elif (args.get("doctype") in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]) or ( + args.get("doctype") == "Material Request" and args.get("material_request_type") == "Purchase" + ): args.uom = item.purchase_uom if item.purchase_uom else item.stock_uom else: args.uom = item.stock_uom - if (args.get("batch_no") and - item.name != frappe.get_cached_value('Batch', args.get("batch_no"), 'item')): - args['batch_no'] = '' + if args.get("batch_no") and item.name != frappe.get_cached_value( + "Batch", args.get("batch_no"), "item" + ): + args["batch_no"] = "" - out = frappe._dict({ - "item_code": item.name, - "item_name": item.item_name, - "description": cstr(item.description).strip(), - "image": cstr(item.image).strip(), - "warehouse": warehouse, - "income_account": get_default_income_account(args, item_defaults, item_group_defaults, brand_defaults), - "expense_account": expense_account or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults) , - "discount_account": get_default_discount_account(args, item_defaults), - "cost_center": get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults), - 'has_serial_no': item.has_serial_no, - 'has_batch_no': item.has_batch_no, - "batch_no": args.get("batch_no"), - "uom": args.uom, - "min_order_qty": flt(item.min_order_qty) if args.doctype == "Material Request" else "", - "qty": flt(args.qty) or 1.0, - "stock_qty": flt(args.qty) or 1.0, - "price_list_rate": 0.0, - "base_price_list_rate": 0.0, - "rate": 0.0, - "base_rate": 0.0, - "amount": 0.0, - "base_amount": 0.0, - "net_rate": 0.0, - "net_amount": 0.0, - "discount_percentage": 0.0, - "discount_amount": 0.0, - "supplier": get_default_supplier(args, item_defaults, item_group_defaults, brand_defaults), - "update_stock": args.get("update_stock") if args.get('doctype') in ['Sales Invoice', 'Purchase Invoice'] else 0, - "delivered_by_supplier": item.delivered_by_supplier if args.get("doctype") in ["Sales Order", "Sales Invoice"] else 0, - "is_fixed_asset": item.is_fixed_asset, - "last_purchase_rate": item.last_purchase_rate if args.get("doctype") in ["Purchase Order"] else 0, - "transaction_date": args.get("transaction_date"), - "against_blanket_order": args.get("against_blanket_order"), - "bom_no": item.get("default_bom"), - "weight_per_unit": args.get("weight_per_unit") or item.get("weight_per_unit"), - "weight_uom": args.get("weight_uom") or item.get("weight_uom"), - "grant_commission": item.get("grant_commission") - }) + out = frappe._dict( + { + "item_code": item.name, + "item_name": item.item_name, + "description": cstr(item.description).strip(), + "image": cstr(item.image).strip(), + "warehouse": warehouse, + "income_account": get_default_income_account( + args, item_defaults, item_group_defaults, brand_defaults + ), + "expense_account": expense_account + or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults), + "discount_account": get_default_discount_account(args, item_defaults), + "cost_center": get_default_cost_center( + args, item_defaults, item_group_defaults, brand_defaults + ), + "has_serial_no": item.has_serial_no, + "has_batch_no": item.has_batch_no, + "batch_no": args.get("batch_no"), + "uom": args.uom, + "min_order_qty": flt(item.min_order_qty) if args.doctype == "Material Request" else "", + "qty": flt(args.qty) or 1.0, + "stock_qty": flt(args.qty) or 1.0, + "price_list_rate": 0.0, + "base_price_list_rate": 0.0, + "rate": 0.0, + "base_rate": 0.0, + "amount": 0.0, + "base_amount": 0.0, + "net_rate": 0.0, + "net_amount": 0.0, + "discount_percentage": 0.0, + "discount_amount": 0.0, + "supplier": get_default_supplier(args, item_defaults, item_group_defaults, brand_defaults), + "update_stock": args.get("update_stock") + if args.get("doctype") in ["Sales Invoice", "Purchase Invoice"] + else 0, + "delivered_by_supplier": item.delivered_by_supplier + if args.get("doctype") in ["Sales Order", "Sales Invoice"] + else 0, + "is_fixed_asset": item.is_fixed_asset, + "last_purchase_rate": item.last_purchase_rate + if args.get("doctype") in ["Purchase Order"] + else 0, + "transaction_date": args.get("transaction_date"), + "against_blanket_order": args.get("against_blanket_order"), + "bom_no": item.get("default_bom"), + "weight_per_unit": args.get("weight_per_unit") or item.get("weight_per_unit"), + "weight_uom": args.get("weight_uom") or item.get("weight_uom"), + "grant_commission": item.get("grant_commission"), + } + ) if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): out.update(calculate_service_end_date(args, item)) @@ -346,26 +389,31 @@ def get_basic_details(args, item, overwrite_warehouse=True): if item.stock_uom == args.uom: out.conversion_factor = 1.0 else: - out.conversion_factor = args.conversion_factor or \ - get_conversion_factor(item.name, args.uom).get("conversion_factor") + out.conversion_factor = args.conversion_factor or get_conversion_factor(item.name, args.uom).get( + "conversion_factor" + ) args.conversion_factor = out.conversion_factor out.stock_qty = out.qty * out.conversion_factor args.stock_qty = out.stock_qty # calculate last purchase rate - if args.get('doctype') in purchase_doctypes: + if args.get("doctype") in purchase_doctypes: from erpnext.buying.doctype.purchase_order.purchase_order import item_last_purchase_rate - out.last_purchase_rate = item_last_purchase_rate(args.name, args.conversion_rate, item.name, out.conversion_factor) + + out.last_purchase_rate = item_last_purchase_rate( + args.name, args.conversion_rate, item.name, out.conversion_factor + ) # if default specified in item is for another company, fetch from company for d in [ ["Account", "income_account", "default_income_account"], ["Account", "expense_account", "default_expense_account"], ["Cost Center", "cost_center", "cost_center"], - ["Warehouse", "warehouse", ""]]: - if not out[d[1]]: - out[d[1]] = frappe.get_cached_value('Company', args.company, d[2]) if d[2] else None + ["Warehouse", "warehouse", ""], + ]: + if not out[d[1]]: + out[d[1]] = frappe.get_cached_value("Company", args.company, d[2]) if d[2] else None for fieldname in ("item_name", "item_group", "brand", "stock_uom"): out[fieldname] = item.get(fieldname) @@ -378,53 +426,58 @@ def get_basic_details(args, item, overwrite_warehouse=True): out["manufacturer_part_no"] = None out["manufacturer"] = None else: - data = frappe.get_value("Item", item.name, - ["default_item_manufacturer", "default_manufacturer_part_no"] , as_dict=1) + data = frappe.get_value( + "Item", item.name, ["default_item_manufacturer", "default_manufacturer_part_no"], as_dict=1 + ) if data: - out.update({ - "manufacturer": data.default_item_manufacturer, - "manufacturer_part_no": data.default_manufacturer_part_no - }) + out.update( + { + "manufacturer": data.default_item_manufacturer, + "manufacturer_part_no": data.default_manufacturer_part_no, + } + ) - child_doctype = args.doctype + ' Item' + child_doctype = args.doctype + " Item" meta = frappe.get_meta(child_doctype) if meta.get_field("barcode"): update_barcode_value(out) if out.get("weight_per_unit"): - out['total_weight'] = out.weight_per_unit * out.stock_qty + out["total_weight"] = out.weight_per_unit * out.stock_qty return out + def get_item_warehouse(item, args, overwrite_warehouse, defaults=None): if not defaults: - defaults = frappe._dict({ - 'item_defaults' : get_item_defaults(item.name, args.company), - 'item_group_defaults' : get_item_group_defaults(item.name, args.company), - 'brand_defaults' : get_brand_defaults(item.name, args.company) - }) + defaults = frappe._dict( + { + "item_defaults": get_item_defaults(item.name, args.company), + "item_group_defaults": get_item_group_defaults(item.name, args.company), + "brand_defaults": get_brand_defaults(item.name, args.company), + } + ) if overwrite_warehouse or not args.warehouse: warehouse = ( - args.get("set_warehouse") or - defaults.item_defaults.get("default_warehouse") or - defaults.item_group_defaults.get("default_warehouse") or - defaults.brand_defaults.get("default_warehouse") or - args.get('warehouse') + args.get("set_warehouse") + or defaults.item_defaults.get("default_warehouse") + or defaults.item_group_defaults.get("default_warehouse") + or defaults.brand_defaults.get("default_warehouse") + or args.get("warehouse") ) if not warehouse: defaults = frappe.defaults.get_defaults() or {} - warehouse_exists = frappe.db.exists("Warehouse", { - 'name': defaults.default_warehouse, - 'company': args.company - }) + warehouse_exists = frappe.db.exists( + "Warehouse", {"name": defaults.default_warehouse, "company": args.company} + ) if defaults.get("default_warehouse") and warehouse_exists: warehouse = defaults.default_warehouse else: - warehouse = args.get('warehouse') + warehouse = args.get("warehouse") if not warehouse: default_warehouse = frappe.db.get_single_value("Stock Settings", "default_warehouse") @@ -433,12 +486,14 @@ def get_item_warehouse(item, args, overwrite_warehouse, defaults=None): return warehouse + def update_barcode_value(out): barcode_data = get_barcode_data([out]) # If item has one barcode then update the value of the barcode field if barcode_data and len(barcode_data.get(out.item_code)) == 1: - out['barcode'] = barcode_data.get(out.item_code)[0] + out["barcode"] = barcode_data.get(out.item_code)[0] + def get_barcode_data(items_list): # get itemwise batch no data @@ -447,9 +502,13 @@ def get_barcode_data(items_list): itemwise_barcode = {} for item in items_list: - barcodes = frappe.db.sql(""" + barcodes = frappe.db.sql( + """ select barcode from `tabItem Barcode` where parent = %s - """, item.item_code, as_dict=1) + """, + item.item_code, + as_dict=1, + ) for barcode in barcodes: if item.item_code not in itemwise_barcode: @@ -458,6 +517,7 @@ def get_barcode_data(items_list): return itemwise_barcode + @frappe.whitelist() def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_tax_templates=None): out = {} @@ -483,22 +543,29 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_t out[item_code[1]] = {} item = frappe.get_cached_doc("Item", item_code[0]) - args = {"company": company, "tax_category": tax_category, "net_rate": item_rates.get(item_code[1])} + args = { + "company": company, + "tax_category": tax_category, + "net_rate": item_rates.get(item_code[1]), + } if item_tax_templates: args.update({"item_tax_template": item_tax_templates.get(item_code[1])}) get_item_tax_template(args, item, out[item_code[1]]) - out[item_code[1]]["item_tax_rate"] = get_item_tax_map(company, out[item_code[1]].get("item_tax_template"), as_json=True) + out[item_code[1]]["item_tax_rate"] = get_item_tax_map( + company, out[item_code[1]].get("item_tax_template"), as_json=True + ) return out + def get_item_tax_template(args, item, out): """ - args = { - "tax_category": None - "item_tax_template": None - } + args = { + "tax_category": None + "item_tax_template": None + } """ item_tax_template = None if item.taxes: @@ -511,6 +578,7 @@ def get_item_tax_template(args, item, out): item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out) item_group = item_group_doc.parent_item_group + def _get_item_tax_template(args, taxes, out=None, for_validate=False): if out is None: out = {} @@ -518,36 +586,43 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): taxes_with_no_validity = [] for tax in taxes: - tax_company = frappe.get_cached_value("Item Tax Template", tax.item_tax_template, 'company') - if tax_company == args['company']: - if (tax.valid_from or tax.maximum_net_rate): + tax_company = frappe.get_cached_value("Item Tax Template", tax.item_tax_template, "company") + if tax_company == args["company"]: + if tax.valid_from or tax.maximum_net_rate: # In purchase Invoice first preference will be given to supplier invoice date # if supplier date is not present then posting date - validation_date = args.get('transaction_date') or args.get('bill_date') or args.get('posting_date') + validation_date = ( + args.get("transaction_date") or args.get("bill_date") or args.get("posting_date") + ) - if getdate(tax.valid_from) <= getdate(validation_date) \ - and is_within_valid_range(args, tax): + if getdate(tax.valid_from) <= getdate(validation_date) and is_within_valid_range(args, tax): taxes_with_validity.append(tax) else: taxes_with_no_validity.append(tax) if taxes_with_validity: - taxes = sorted(taxes_with_validity, key = lambda i: i.valid_from, reverse=True) + taxes = sorted(taxes_with_validity, key=lambda i: i.valid_from, reverse=True) else: taxes = taxes_with_no_validity if for_validate: - return [tax.item_tax_template for tax in taxes if (cstr(tax.tax_category) == cstr(args.get('tax_category')) \ - and (tax.item_tax_template not in taxes))] + return [ + tax.item_tax_template + for tax in taxes + if ( + cstr(tax.tax_category) == cstr(args.get("tax_category")) + and (tax.item_tax_template not in taxes) + ) + ] # all templates have validity and no template is valid if not taxes_with_validity and (not taxes_with_no_validity): return None # do not change if already a valid template - if args.get('item_tax_template') in {t.item_tax_template for t in taxes}: - out["item_tax_template"] = args.get('item_tax_template') - return args.get('item_tax_template') + if args.get("item_tax_template") in {t.item_tax_template for t in taxes}: + out["item_tax_template"] = args.get("item_tax_template") + return args.get("item_tax_template") for tax in taxes: if cstr(tax.tax_category) == cstr(args.get("tax_category")): @@ -555,15 +630,17 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): return tax.item_tax_template return None + def is_within_valid_range(args, tax): if not flt(tax.maximum_net_rate): # No range specified, just ignore return True - elif flt(tax.minimum_net_rate) <= flt(args.get('net_rate')) <= flt(tax.maximum_net_rate): + elif flt(tax.minimum_net_rate) <= flt(args.get("net_rate")) <= flt(tax.maximum_net_rate): return True return False + @frappe.whitelist() def get_item_tax_map(company, item_tax_template, as_json=True): item_tax_map = {} @@ -575,6 +652,7 @@ def get_item_tax_map(company, item_tax_template, as_json=True): return json.dumps(item_tax_map) if as_json else item_tax_map + @frappe.whitelist() def calculate_service_end_date(args, item=None): args = process_args(args) @@ -593,53 +671,68 @@ def calculate_service_end_date(args, item=None): service_start_date = args.service_start_date if args.service_start_date else args.transaction_date service_end_date = add_months(service_start_date, item.get(no_of_months)) - deferred_detail = { - "service_start_date": service_start_date, - "service_end_date": service_end_date - } + deferred_detail = {"service_start_date": service_start_date, "service_end_date": service_end_date} deferred_detail[enable_deferred] = item.get(enable_deferred) deferred_detail[account] = get_default_deferred_account(args, item, fieldname=account) return deferred_detail + def get_default_income_account(args, item, item_group, brand): - return (item.get("income_account") + return ( + item.get("income_account") or item_group.get("income_account") or brand.get("income_account") - or args.income_account) + or args.income_account + ) + def get_default_expense_account(args, item, item_group, brand): - return (item.get("expense_account") + return ( + item.get("expense_account") or item_group.get("expense_account") or brand.get("expense_account") - or args.expense_account) + or args.expense_account + ) + def get_default_discount_account(args, item): - return (item.get("default_discount_account") - or args.discount_account) + return item.get("default_discount_account") or args.discount_account + def get_default_deferred_account(args, item, fieldname=None): if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): - return (item.get(fieldname) + return ( + item.get(fieldname) or args.get(fieldname) - or frappe.get_cached_value('Company', args.company, "default_"+fieldname)) + or frappe.get_cached_value("Company", args.company, "default_" + fieldname) + ) else: return None + def get_default_cost_center(args, item=None, item_group=None, brand=None, company=None): cost_center = None if not company and args.get("company"): company = args.get("company") - if args.get('project'): + if args.get("project"): cost_center = frappe.db.get_value("Project", args.get("project"), "cost_center", cache=True) if not cost_center and (item and item_group and brand): - if args.get('customer'): - cost_center = item.get('selling_cost_center') or item_group.get('selling_cost_center') or brand.get('selling_cost_center') + if args.get("customer"): + cost_center = ( + item.get("selling_cost_center") + or item_group.get("selling_cost_center") + or brand.get("selling_cost_center") + ) else: - cost_center = item.get('buying_cost_center') or item_group.get('buying_cost_center') or brand.get('buying_cost_center') + cost_center = ( + item.get("buying_cost_center") + or item_group.get("buying_cost_center") + or brand.get("buying_cost_center") + ) elif not cost_center and args.get("item_code") and company: for method in ["get_item_defaults", "get_item_group_defaults", "get_brand_defaults"]: @@ -652,20 +745,26 @@ def get_default_cost_center(args, item=None, item_group=None, brand=None, compan if not cost_center and args.get("cost_center"): cost_center = args.get("cost_center") - if (company and cost_center - and frappe.get_cached_value("Cost Center", cost_center, "company") != company): + if ( + company + and cost_center + and frappe.get_cached_value("Cost Center", cost_center, "company") != company + ): return None if not cost_center and company: - cost_center = frappe.get_cached_value("Company", - company, "cost_center") + cost_center = frappe.get_cached_value("Company", company, "cost_center") return cost_center + def get_default_supplier(args, item, item_group, brand): - return (item.get("default_supplier") + return ( + item.get("default_supplier") or item_group.get("default_supplier") - or brand.get("default_supplier")) + or brand.get("default_supplier") + ) + def get_price_list_rate(args, item_doc, out=None): if out is None: @@ -673,7 +772,7 @@ def get_price_list_rate(args, item_doc, out=None): meta = frappe.get_meta(args.parenttype or args.doctype) - if meta.get_field("currency") or args.get('currency'): + if meta.get_field("currency") or args.get("currency"): if not args.get("price_list_currency") or not args.get("plc_conversion_rate"): # if currency and plc_conversion_rate exist then # `get_price_list_currency_and_exchange_rate` has already been called @@ -695,54 +794,72 @@ def get_price_list_rate(args, item_doc, out=None): insert_item_price(args) return out - out.price_list_rate = flt(price_list_rate) * flt(args.plc_conversion_rate) \ - / flt(args.conversion_rate) + out.price_list_rate = ( + flt(price_list_rate) * flt(args.plc_conversion_rate) / flt(args.conversion_rate) + ) - if not out.price_list_rate and args.transaction_type=="buying": + if not out.price_list_rate and args.transaction_type == "buying": from erpnext.stock.doctype.item.item import get_last_purchase_details - out.update(get_last_purchase_details(item_doc.name, - args.name, args.conversion_rate)) + + out.update(get_last_purchase_details(item_doc.name, args.name, args.conversion_rate)) return out + def insert_item_price(args): """Insert Item Price if Price List and Price List Rate are specified and currency is the same""" - if frappe.db.get_value("Price List", args.price_list, "currency", cache=True) == args.currency \ - and cint(frappe.db.get_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing")): + if frappe.db.get_value( + "Price List", args.price_list, "currency", cache=True + ) == args.currency and cint( + frappe.db.get_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing") + ): if frappe.has_permission("Item Price", "write"): - price_list_rate = (args.rate / args.get('conversion_factor') - if args.get("conversion_factor") else args.rate) + price_list_rate = ( + args.rate / args.get("conversion_factor") if args.get("conversion_factor") else args.rate + ) - item_price = frappe.db.get_value('Item Price', - {'item_code': args.item_code, 'price_list': args.price_list, 'currency': args.currency}, - ['name', 'price_list_rate'], as_dict=1) + item_price = frappe.db.get_value( + "Item Price", + {"item_code": args.item_code, "price_list": args.price_list, "currency": args.currency}, + ["name", "price_list_rate"], + as_dict=1, + ) if item_price and item_price.name: - if item_price.price_list_rate != price_list_rate and frappe.db.get_single_value('Stock Settings', 'update_existing_price_list_rate'): - frappe.db.set_value('Item Price', item_price.name, "price_list_rate", price_list_rate) - frappe.msgprint(_("Item Price updated for {0} in Price List {1}").format(args.item_code, - args.price_list), alert=True) + if item_price.price_list_rate != price_list_rate and frappe.db.get_single_value( + "Stock Settings", "update_existing_price_list_rate" + ): + frappe.db.set_value("Item Price", item_price.name, "price_list_rate", price_list_rate) + frappe.msgprint( + _("Item Price updated for {0} in Price List {1}").format(args.item_code, args.price_list), + alert=True, + ) else: - item_price = frappe.get_doc({ - "doctype": "Item Price", - "price_list": args.price_list, - "item_code": args.item_code, - "currency": args.currency, - "price_list_rate": price_list_rate - }) + item_price = frappe.get_doc( + { + "doctype": "Item Price", + "price_list": args.price_list, + "item_code": args.item_code, + "currency": args.currency, + "price_list_rate": price_list_rate, + } + ) item_price.insert() - frappe.msgprint(_("Item Price added for {0} in Price List {1}").format(args.item_code, - args.price_list), alert=True) + frappe.msgprint( + _("Item Price added for {0} in Price List {1}").format(args.item_code, args.price_list), + alert=True, + ) + def get_item_price(args, item_code, ignore_party=False): """ - Get name, price_list_rate from Item Price based on conditions - Check if the desired qty is within the increment of the packing list. - :param args: dict (or frappe._dict) with mandatory fields price_list, uom - optional fields transaction_date, customer, supplier - :param item_code: str, Item Doctype field item_code + Get name, price_list_rate from Item Price based on conditions + Check if the desired qty is within the increment of the packing list. + :param args: dict (or frappe._dict) with mandatory fields price_list, uom + optional fields transaction_date, customer, supplier + :param item_code: str, Item Doctype field item_code """ - args['item_code'] = item_code + args["item_code"] = item_code conditions = """where item_code=%(item_code)s and price_list=%(price_list)s @@ -758,36 +875,42 @@ 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('transaction_date'): + if args.get("transaction_date"): conditions += """ and %(transaction_date)s between ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31')""" - if args.get('posting_date'): + if args.get("posting_date"): conditions += """ and %(posting_date)s between ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31')""" - return frappe.db.sql(""" select name, price_list_rate, uom + 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, batch_no desc, uom desc """.format( + conditions=conditions + ), + args, + ) + def get_price_list_rate_for(args, item_code): """ - :param customer: link to Customer DocType - :param supplier: link to Supplier DocType - :param price_list: str (Standard Buying or Standard Selling) - :param item_code: str, Item Doctype field item_code - :param qty: Desired Qty - :param transaction_date: Date of the price + :param customer: link to Customer DocType + :param supplier: link to Supplier DocType + :param price_list: str (Standard Buying or Standard Selling) + :param item_code: str, Item Doctype field item_code + :param qty: Desired Qty + :param transaction_date: Date of the price """ item_price_args = { - "item_code": item_code, - "price_list": args.get('price_list'), - "customer": args.get('customer'), - "supplier": args.get('supplier'), - "uom": args.get('uom'), - "transaction_date": args.get('transaction_date'), - "posting_date": args.get('posting_date'), - "batch_no": args.get('batch_no') + "item_code": item_code, + "price_list": args.get("price_list"), + "customer": args.get("customer"), + "supplier": args.get("supplier"), + "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 @@ -800,12 +923,15 @@ def get_price_list_rate_for(args, item_code): for field in ["customer", "supplier"]: del item_price_args[field] - general_price_list_rate = get_item_price(item_price_args, item_code, - ignore_party=args.get("ignore_party")) + general_price_list_rate = get_item_price( + item_price_args, item_code, ignore_party=args.get("ignore_party") + ) if not general_price_list_rate and args.get("uom") != args.get("stock_uom"): item_price_args["uom"] = args.get("stock_uom") - general_price_list_rate = get_item_price(item_price_args, item_code, ignore_party=args.get("ignore_party")) + general_price_list_rate = get_item_price( + item_price_args, item_code, ignore_party=args.get("ignore_party") + ) if general_price_list_rate: item_price_data = general_price_list_rate @@ -813,18 +939,19 @@ def get_price_list_rate_for(args, item_code): if item_price_data: if item_price_data[0][2] == args.get("uom"): return item_price_data[0][1] - elif not args.get('price_list_uom_dependant'): + elif not args.get("price_list_uom_dependant"): return flt(item_price_data[0][1] * flt(args.get("conversion_factor", 1))) else: return item_price_data[0][1] + def check_packing_list(price_list_rate_name, desired_qty, item_code): """ - Check if the desired qty is within the increment of the packing list. - :param price_list_rate_name: Name of Item Price - :param desired_qty: Desired Qt - :param item_code: str, Item Doctype field item_code - :param qty: Desired Qt + Check if the desired qty is within the increment of the packing list. + :param price_list_rate_name: Name of Item Price + :param desired_qty: Desired Qt + :param item_code: str, Item Doctype field item_code + :param qty: Desired Qt """ flag = True @@ -837,47 +964,62 @@ def check_packing_list(price_list_rate_name, desired_qty, item_code): return flag + def validate_conversion_rate(args, meta): from erpnext.controllers.accounts_controller import validate_conversion_rate - company_currency = frappe.get_cached_value('Company', args.company, "default_currency") - if (not args.conversion_rate and args.currency==company_currency): + company_currency = frappe.get_cached_value("Company", args.company, "default_currency") + if not args.conversion_rate and args.currency == company_currency: args.conversion_rate = 1.0 - if (not args.ignore_conversion_rate and args.conversion_rate == 1 and args.currency!=company_currency): - args.conversion_rate = get_exchange_rate(args.currency, - company_currency, args.transaction_date, "for_buying") or 1.0 + if ( + not args.ignore_conversion_rate + and args.conversion_rate == 1 + and args.currency != company_currency + ): + args.conversion_rate = ( + get_exchange_rate(args.currency, company_currency, args.transaction_date, "for_buying") or 1.0 + ) # validate currency conversion rate - validate_conversion_rate(args.currency, args.conversion_rate, - meta.get_label("conversion_rate"), args.company) + validate_conversion_rate( + args.currency, args.conversion_rate, meta.get_label("conversion_rate"), args.company + ) - args.conversion_rate = flt(args.conversion_rate, - get_field_precision(meta.get_field("conversion_rate"), - frappe._dict({"fields": args}))) + args.conversion_rate = flt( + args.conversion_rate, + get_field_precision(meta.get_field("conversion_rate"), frappe._dict({"fields": args})), + ) if args.price_list: - if (not args.plc_conversion_rate - and args.price_list_currency==frappe.db.get_value("Price List", args.price_list, "currency", cache=True)): + if not args.plc_conversion_rate and args.price_list_currency == frappe.db.get_value( + "Price List", args.price_list, "currency", cache=True + ): args.plc_conversion_rate = 1.0 # validate price list currency conversion rate if not args.get("price_list_currency"): throw(_("Price List Currency not selected")) else: - validate_conversion_rate(args.price_list_currency, args.plc_conversion_rate, - meta.get_label("plc_conversion_rate"), args.company) + validate_conversion_rate( + args.price_list_currency, + args.plc_conversion_rate, + meta.get_label("plc_conversion_rate"), + args.company, + ) if meta.get_field("plc_conversion_rate"): - args.plc_conversion_rate = flt(args.plc_conversion_rate, - get_field_precision(meta.get_field("plc_conversion_rate"), - frappe._dict({"fields": args}))) + args.plc_conversion_rate = flt( + args.plc_conversion_rate, + get_field_precision(meta.get_field("plc_conversion_rate"), frappe._dict({"fields": args})), + ) + def get_party_item_code(args, item_doc, out): - if args.transaction_type=="selling" and args.customer: + if args.transaction_type == "selling" and args.customer: out.customer_item_code = None - if args.quotation_to and args.quotation_to != 'Customer': + if args.quotation_to and args.quotation_to != "Customer": return customer_item_code = item_doc.get("customer_items", {"customer_name": args.customer}) @@ -890,15 +1032,16 @@ def get_party_item_code(args, item_doc, out): if customer_group_item_code and not customer_group_item_code[0].customer_name: out.customer_item_code = customer_group_item_code[0].ref_code - if args.transaction_type=="buying" and args.supplier: + if args.transaction_type == "buying" and args.supplier: item_supplier = item_doc.get("supplier_items", {"supplier": args.supplier}) out.supplier_part_no = item_supplier[0].supplier_part_no if item_supplier else None + def get_pos_profile_item_details(company, args, pos_profile=None, update_data=False): res = frappe._dict() if not frappe.flags.pos_profile and not pos_profile: - pos_profile = frappe.flags.pos_profile = get_pos_profile(company, args.get('pos_profile')) + pos_profile = frappe.flags.pos_profile = get_pos_profile(company, args.get("pos_profile")) if pos_profile: for fieldname in ("income_account", "cost_center", "warehouse", "expense_account"): @@ -910,70 +1053,89 @@ def get_pos_profile_item_details(company, args, pos_profile=None, update_data=Fa return res + @frappe.whitelist() def get_pos_profile(company, pos_profile=None, user=None): - if pos_profile: return frappe.get_cached_doc('POS Profile', pos_profile) + if pos_profile: + return frappe.get_cached_doc("POS Profile", pos_profile) if not user: - user = frappe.session['user'] + user = frappe.session["user"] condition = "pfu.user = %(user)s AND pfu.default=1" if user and company: condition = "pfu.user = %(user)s AND pf.company = %(company)s AND pfu.default=1" - pos_profile = frappe.db.sql("""SELECT pf.* + pos_profile = frappe.db.sql( + """SELECT pf.* FROM `tabPOS Profile` pf LEFT JOIN `tabPOS Profile User` pfu ON pf.name = pfu.parent WHERE {cond} AND pf.disabled = 0 - """.format(cond = condition), { - 'user': user, - 'company': company - }, as_dict=1) + """.format( + cond=condition + ), + {"user": user, "company": company}, + as_dict=1, + ) if not pos_profile and company: - pos_profile = frappe.db.sql("""SELECT pf.* + pos_profile = frappe.db.sql( + """SELECT pf.* FROM `tabPOS Profile` pf LEFT JOIN `tabPOS Profile User` pfu ON pf.name = pfu.parent WHERE pf.company = %(company)s AND pf.disabled = 0 - """, { - 'company': company - }, as_dict=1) + """, + {"company": company}, + as_dict=1, + ) return pos_profile and pos_profile[0] or None + def get_serial_nos_by_fifo(args, sales_order=None): if frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo"): - return "\n".join(frappe.db.sql_list("""select name from `tabSerial No` + return "\n".join( + frappe.db.sql_list( + """select name from `tabSerial No` where item_code=%(item_code)s and warehouse=%(warehouse)s and sales_order=IF(%(sales_order)s IS NULL, sales_order, %(sales_order)s) order by timestamp(purchase_date, purchase_time) asc limit %(qty)s""", - { - "item_code": args.item_code, - "warehouse": args.warehouse, - "qty": abs(cint(args.stock_qty)), - "sales_order": sales_order - })) + { + "item_code": args.item_code, + "warehouse": args.warehouse, + "qty": abs(cint(args.stock_qty)), + "sales_order": sales_order, + }, + ) + ) + def get_serial_no_batchwise(args, sales_order=None): if frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo"): - return "\n".join(frappe.db.sql_list("""select name from `tabSerial No` + return "\n".join( + frappe.db.sql_list( + """select name from `tabSerial No` where item_code=%(item_code)s and warehouse=%(warehouse)s and sales_order=IF(%(sales_order)s IS NULL, sales_order, %(sales_order)s) and batch_no=IF(%(batch_no)s IS NULL, batch_no, %(batch_no)s) order - by timestamp(purchase_date, purchase_time) asc limit %(qty)s""", { - "item_code": args.item_code, - "warehouse": args.warehouse, - "batch_no": args.batch_no, - "qty": abs(cint(args.stock_qty)), - "sales_order": sales_order - })) + by timestamp(purchase_date, purchase_time) asc limit %(qty)s""", + { + "item_code": args.item_code, + "warehouse": args.warehouse, + "batch_no": args.batch_no, + "qty": abs(cint(args.stock_qty)), + "sales_order": sales_order, + }, + ) + ) + @frappe.whitelist() def get_conversion_factor(item_code, uom): @@ -981,69 +1143,94 @@ def get_conversion_factor(item_code, uom): filters = {"parent": item_code, "uom": uom} if variant_of: filters["parent"] = ("in", (item_code, variant_of)) - conversion_factor = frappe.db.get_value("UOM Conversion Detail", - filters, "conversion_factor") + conversion_factor = frappe.db.get_value("UOM Conversion Detail", filters, "conversion_factor") if not conversion_factor: stock_uom = frappe.db.get_value("Item", item_code, "stock_uom") conversion_factor = get_uom_conv_factor(uom, stock_uom) return {"conversion_factor": conversion_factor or 1.0} + @frappe.whitelist() def get_projected_qty(item_code, warehouse): - return {"projected_qty": frappe.db.get_value("Bin", - {"item_code": item_code, "warehouse": warehouse}, "projected_qty")} + return { + "projected_qty": frappe.db.get_value( + "Bin", {"item_code": item_code, "warehouse": warehouse}, "projected_qty" + ) + } + @frappe.whitelist() def get_bin_details(item_code, warehouse, company=None): - bin_details = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, - ["projected_qty", "actual_qty", "reserved_qty"], as_dict=True, cache=True) \ - or {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0} + bin_details = frappe.db.get_value( + "Bin", + {"item_code": item_code, "warehouse": warehouse}, + ["projected_qty", "actual_qty", "reserved_qty"], + as_dict=True, + cache=True, + ) or {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0} if company: - bin_details['company_total_stock'] = get_company_total_stock(item_code, company) + bin_details["company_total_stock"] = get_company_total_stock(item_code, company) return bin_details + def get_company_total_stock(item_code, company): - return frappe.db.sql("""SELECT sum(actual_qty) from + return frappe.db.sql( + """SELECT sum(actual_qty) from (`tabBin` INNER JOIN `tabWarehouse` ON `tabBin`.warehouse = `tabWarehouse`.name) WHERE `tabWarehouse`.company = %s and `tabBin`.item_code = %s""", - (company, item_code))[0][0] + (company, item_code), + )[0][0] + @frappe.whitelist() def get_serial_no_details(item_code, warehouse, stock_qty, serial_no): - args = frappe._dict({"item_code":item_code, "warehouse":warehouse, "stock_qty":stock_qty, "serial_no":serial_no}) + args = frappe._dict( + {"item_code": item_code, "warehouse": warehouse, "stock_qty": stock_qty, "serial_no": serial_no} + ) serial_no = get_serial_no(args) - return {'serial_no': serial_no} + return {"serial_no": serial_no} + @frappe.whitelist() -def get_bin_details_and_serial_nos(item_code, warehouse, has_batch_no=None, stock_qty=None, serial_no=None): +def get_bin_details_and_serial_nos( + item_code, warehouse, has_batch_no=None, stock_qty=None, serial_no=None +): bin_details_and_serial_nos = {} bin_details_and_serial_nos.update(get_bin_details(item_code, warehouse)) if flt(stock_qty) > 0: if has_batch_no: - args = frappe._dict({"item_code":item_code, "warehouse":warehouse, "stock_qty":stock_qty}) + args = frappe._dict({"item_code": item_code, "warehouse": warehouse, "stock_qty": stock_qty}) serial_no = get_serial_no(args) - bin_details_and_serial_nos.update({'serial_no': serial_no}) + bin_details_and_serial_nos.update({"serial_no": serial_no}) return bin_details_and_serial_nos - bin_details_and_serial_nos.update(get_serial_no_details(item_code, warehouse, stock_qty, serial_no)) + bin_details_and_serial_nos.update( + get_serial_no_details(item_code, warehouse, stock_qty, serial_no) + ) return bin_details_and_serial_nos + @frappe.whitelist() def get_batch_qty_and_serial_no(batch_no, stock_qty, warehouse, item_code, has_serial_no): batch_qty_and_serial_no = {} batch_qty_and_serial_no.update(get_batch_qty(batch_no, warehouse, item_code)) - if (flt(batch_qty_and_serial_no.get('actual_batch_qty')) >= flt(stock_qty)) and has_serial_no: - args = frappe._dict({"item_code":item_code, "warehouse":warehouse, "stock_qty":stock_qty, "batch_no":batch_no}) + if (flt(batch_qty_and_serial_no.get("actual_batch_qty")) >= flt(stock_qty)) and has_serial_no: + args = frappe._dict( + {"item_code": item_code, "warehouse": warehouse, "stock_qty": stock_qty, "batch_no": batch_no} + ) serial_no = get_serial_no(args) - batch_qty_and_serial_no.update({'serial_no': serial_no}) + batch_qty_and_serial_no.update({"serial_no": serial_no}) return batch_qty_and_serial_no + @frappe.whitelist() def get_batch_qty(batch_no, warehouse, item_code): from erpnext.stock.doctype.batch import batch + if batch_no: - return {'actual_batch_qty': batch.get_batch_qty(batch_no, warehouse)} + return {"actual_batch_qty": batch.get_batch_qty(batch_no, warehouse)} + @frappe.whitelist() def apply_price_list(args, as_doc=False): @@ -1053,23 +1240,23 @@ def apply_price_list(args, as_doc=False): :param args: See below :param as_doc: Updates value in the passed dict - args = { - "doctype": "", - "name": "", - "items": [{"doctype": "", "name": "", "item_code": "", "brand": "", "item_group": ""}, ...], - "conversion_rate": 1.0, - "selling_price_list": None, - "price_list_currency": None, - "price_list_uom_dependant": None, - "plc_conversion_rate": 1.0, - "doctype": "", - "name": "", - "supplier": None, - "transaction_date": None, - "conversion_rate": 1.0, - "buying_price_list": None, - "ignore_pricing_rule": 0/1 - } + args = { + "doctype": "", + "name": "", + "items": [{"doctype": "", "name": "", "item_code": "", "brand": "", "item_group": ""}, ...], + "conversion_rate": 1.0, + "selling_price_list": None, + "price_list_currency": None, + "price_list_uom_dependant": None, + "plc_conversion_rate": 1.0, + "doctype": "", + "name": "", + "supplier": None, + "transaction_date": None, + "conversion_rate": 1.0, + "buying_price_list": None, + "ignore_pricing_rule": 0/1 + } """ args = process_args(args) @@ -1089,10 +1276,10 @@ def apply_price_list(args, as_doc=False): children.append(item_details) if as_doc: - args.price_list_currency = parent.price_list_currency, + args.price_list_currency = (parent.price_list_currency,) args.plc_conversion_rate = parent.plc_conversion_rate - if args.get('items'): - for i, item in enumerate(args.get('items')): + if args.get("items"): + for i, item in enumerate(args.get("items")): for fieldname in children[i]: # if the field exists in the original doc # update the value @@ -1100,26 +1287,25 @@ def apply_price_list(args, as_doc=False): item[fieldname] = children[i][fieldname] return args else: - return { - "parent": parent, - "children": children - } + return {"parent": parent, "children": children} + def apply_price_list_on_item(args): - item_doc = frappe.db.get_value("Item", args.item_code, ['name', 'variant_of'], as_dict=1) + item_doc = frappe.db.get_value("Item", args.item_code, ["name", "variant_of"], as_dict=1) item_details = get_price_list_rate(args, item_doc) item_details.update(get_pricing_rule_for_item(args, item_details.price_list_rate)) return item_details + def get_price_list_currency_and_exchange_rate(args): if not args.price_list: return {} - if args.doctype in ['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice']: + if args.doctype in ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]: args.update({"exchange_rate": "for_selling"}) - elif args.doctype in ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice']: + elif args.doctype in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]: args.update({"exchange_rate": "for_buying"}) price_list_details = get_price_list_details(args.price_list) @@ -1130,25 +1316,38 @@ def get_price_list_currency_and_exchange_rate(args): plc_conversion_rate = args.plc_conversion_rate company_currency = get_company_currency(args.company) - if (not plc_conversion_rate) or (price_list_currency and args.price_list_currency \ - and price_list_currency != args.price_list_currency): - # cksgb 19/09/2016: added args.transaction_date as posting_date argument for get_exchange_rate - plc_conversion_rate = get_exchange_rate(price_list_currency, company_currency, - args.transaction_date, args.exchange_rate) or plc_conversion_rate + if (not plc_conversion_rate) or ( + price_list_currency + and args.price_list_currency + and price_list_currency != args.price_list_currency + ): + # cksgb 19/09/2016: added args.transaction_date as posting_date argument for get_exchange_rate + plc_conversion_rate = ( + get_exchange_rate( + price_list_currency, company_currency, args.transaction_date, args.exchange_rate + ) + or plc_conversion_rate + ) + + return frappe._dict( + { + "price_list_currency": price_list_currency, + "price_list_uom_dependant": price_list_uom_dependant, + "plc_conversion_rate": plc_conversion_rate or 1, + } + ) - return frappe._dict({ - "price_list_currency": price_list_currency, - "price_list_uom_dependant": price_list_uom_dependant, - "plc_conversion_rate": plc_conversion_rate or 1 - }) @frappe.whitelist() def get_default_bom(item_code=None): if item_code: - bom = frappe.db.get_value("BOM", {"docstatus": 1, "is_default": 1, "is_active": 1, "item": item_code}) + bom = frappe.db.get_value( + "BOM", {"docstatus": 1, "is_default": 1, "is_active": 1, "item": item_code} + ) if bom: return bom + @frappe.whitelist() def get_valuation_rate(item_code, company, warehouse=None): item = get_item_defaults(item_code, company) @@ -1157,43 +1356,57 @@ def get_valuation_rate(item_code, company, warehouse=None): # item = frappe.get_doc("Item", item_code) if item.get("is_stock_item"): if not warehouse: - warehouse = item.get("default_warehouse") or item_group.get("default_warehouse") or brand.get("default_warehouse") + warehouse = ( + item.get("default_warehouse") + or item_group.get("default_warehouse") + or brand.get("default_warehouse") + ) - return frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, - ["valuation_rate"], as_dict=True) or {"valuation_rate": 0} + return frappe.db.get_value( + "Bin", {"item_code": item_code, "warehouse": warehouse}, ["valuation_rate"], as_dict=True + ) or {"valuation_rate": 0} elif not item.get("is_stock_item"): - valuation_rate =frappe.db.sql("""select sum(base_net_amount) / sum(qty*conversion_factor) + valuation_rate = frappe.db.sql( + """select sum(base_net_amount) / sum(qty*conversion_factor) from `tabPurchase Invoice Item` - where item_code = %s and docstatus=1""", item_code) + where item_code = %s and docstatus=1""", + item_code, + ) if valuation_rate: return {"valuation_rate": valuation_rate[0][0] or 0.0} else: return {"valuation_rate": 0.0} + def get_gross_profit(out): if out.valuation_rate: - out.update({ - "gross_profit": ((out.base_rate - out.valuation_rate) * out.stock_qty) - }) + out.update({"gross_profit": ((out.base_rate - out.valuation_rate) * out.stock_qty)}) return out + @frappe.whitelist() def get_serial_no(args, serial_nos=None, sales_order=None): serial_no = None if isinstance(args, str): args = json.loads(args) args = frappe._dict(args) - if args.get('doctype') == 'Sales Invoice' and not args.get('update_stock'): + if args.get("doctype") == "Sales Invoice" and not args.get("update_stock"): return "" - if args.get('warehouse') and args.get('stock_qty') and args.get('item_code'): - has_serial_no = frappe.get_value('Item', {'item_code': args.item_code}, "has_serial_no") - if args.get('batch_no') and has_serial_no == 1: + if args.get("warehouse") and args.get("stock_qty") and args.get("item_code"): + has_serial_no = frappe.get_value("Item", {"item_code": args.item_code}, "has_serial_no") + if args.get("batch_no") and has_serial_no == 1: return get_serial_no_batchwise(args, sales_order) elif has_serial_no == 1: - args = json.dumps({"item_code": args.get('item_code'),"warehouse": args.get('warehouse'),"stock_qty": args.get('stock_qty')}) + args = json.dumps( + { + "item_code": args.get("item_code"), + "warehouse": args.get("warehouse"), + "stock_qty": args.get("stock_qty"), + } + ) args = process_args(args) serial_no = get_serial_nos_by_fifo(args, sales_order) @@ -1210,53 +1423,68 @@ def update_party_blanket_order(args, out): if blanket_order_details: out.update(blanket_order_details) + @frappe.whitelist() def get_blanket_order_details(args): if isinstance(args, str): args = frappe._dict(json.loads(args)) blanket_order_details = None - condition = '' + condition = "" if args.item_code: if args.customer and args.doctype == "Sales Order": - condition = ' and bo.customer=%(customer)s' + condition = " and bo.customer=%(customer)s" elif args.supplier and args.doctype == "Purchase Order": - condition = ' and bo.supplier=%(supplier)s' + condition = " and bo.supplier=%(supplier)s" if args.blanket_order: - condition += ' and bo.name =%(blanket_order)s' + condition += " and bo.name =%(blanket_order)s" if args.transaction_date: - condition += ' and bo.to_date>=%(transaction_date)s' + condition += " and bo.to_date>=%(transaction_date)s" - blanket_order_details = frappe.db.sql(''' + blanket_order_details = frappe.db.sql( + """ select boi.rate as blanket_order_rate, bo.name as blanket_order from `tabBlanket Order` bo, `tabBlanket Order Item` boi where bo.company=%(company)s and boi.item_code=%(item_code)s and bo.docstatus=1 and bo.name = boi.parent {0} - '''.format(condition), args, as_dict=True) + """.format( + condition + ), + args, + as_dict=True, + ) - blanket_order_details = blanket_order_details[0] if blanket_order_details else '' + blanket_order_details = blanket_order_details[0] if blanket_order_details else "" return blanket_order_details + def get_so_reservation_for_item(args): reserved_so = None - if args.get('against_sales_order'): - if get_reserved_qty_for_so(args.get('against_sales_order'), args.get('item_code')): - reserved_so = args.get('against_sales_order') - elif args.get('against_sales_invoice'): - sales_order = frappe.db.sql("""select sales_order from `tabSales Invoice Item` where - parent=%s and item_code=%s""", (args.get('against_sales_invoice'), args.get('item_code'))) + if args.get("against_sales_order"): + if get_reserved_qty_for_so(args.get("against_sales_order"), args.get("item_code")): + reserved_so = args.get("against_sales_order") + elif args.get("against_sales_invoice"): + sales_order = frappe.db.sql( + """select sales_order from `tabSales Invoice Item` where + parent=%s and item_code=%s""", + (args.get("against_sales_invoice"), args.get("item_code")), + ) if sales_order and sales_order[0]: - if get_reserved_qty_for_so(sales_order[0][0], args.get('item_code')): + if get_reserved_qty_for_so(sales_order[0][0], args.get("item_code")): reserved_so = sales_order[0] elif args.get("sales_order"): - if get_reserved_qty_for_so(args.get('sales_order'), args.get('item_code')): - reserved_so = args.get('sales_order') + if get_reserved_qty_for_so(args.get("sales_order"), args.get("item_code")): + reserved_so = args.get("sales_order") return reserved_so + def get_reserved_qty_for_so(sales_order, item_code): - reserved_qty = frappe.db.sql("""select sum(qty) from `tabSales Order Item` + reserved_qty = frappe.db.sql( + """select sum(qty) from `tabSales Order Item` where parent=%s and item_code=%s and ensure_delivery_based_on_produced_serial_no=1 - """, (sales_order, item_code)) + """, + (sales_order, item_code), + ) if reserved_qty and reserved_qty[0][0]: return reserved_qty[0][0] else: diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py index 21f2573a27..a96ffefd47 100644 --- a/erpnext/stock/reorder_item.py +++ b/erpnext/stock/reorder_item.py @@ -13,22 +13,29 @@ import erpnext def reorder_item(): - """ Reorder item if stock reaches reorder level""" + """Reorder item if stock reaches reorder level""" # if initial setup not completed, return if not (frappe.db.a_row_exists("Company") and frappe.db.a_row_exists("Fiscal Year")): return - if cint(frappe.db.get_value('Stock Settings', None, 'auto_indent')): + if cint(frappe.db.get_value("Stock Settings", None, "auto_indent")): return _reorder_item() + def _reorder_item(): material_requests = {"Purchase": {}, "Transfer": {}, "Material Issue": {}, "Manufacture": {}} - warehouse_company = frappe._dict(frappe.db.sql("""select name, company from `tabWarehouse` - where disabled=0""")) - default_company = (erpnext.get_default_company() or - frappe.db.sql("""select name from tabCompany limit 1""")[0][0]) + warehouse_company = frappe._dict( + frappe.db.sql( + """select name, company from `tabWarehouse` + where disabled=0""" + ) + ) + default_company = ( + erpnext.get_default_company() or frappe.db.sql("""select name from tabCompany limit 1""")[0][0] + ) - items_to_consider = frappe.db.sql_list("""select name from `tabItem` item + items_to_consider = frappe.db.sql_list( + """select name from `tabItem` item where is_stock_item=1 and has_variants=0 and disabled=0 and (end_of_life is null or end_of_life='0000-00-00' or end_of_life > %(today)s) @@ -36,14 +43,17 @@ def _reorder_item(): or (variant_of is not null and variant_of != '' and exists (select name from `tabItem Reorder` ir where ir.parent=item.variant_of)) )""", - {"today": nowdate()}) + {"today": nowdate()}, + ) if not items_to_consider: return item_warehouse_projected_qty = get_item_warehouse_projected_qty(items_to_consider) - def add_to_material_request(item_code, warehouse, reorder_level, reorder_qty, material_request_type, warehouse_group=None): + def add_to_material_request( + item_code, warehouse, reorder_level, reorder_qty, material_request_type, warehouse_group=None + ): if warehouse not in warehouse_company: # a disabled warehouse return @@ -64,11 +74,9 @@ def _reorder_item(): company = warehouse_company.get(warehouse) or default_company - material_requests[material_request_type].setdefault(company, []).append({ - "item_code": item_code, - "warehouse": warehouse, - "reorder_qty": reorder_qty - }) + material_requests[material_request_type].setdefault(company, []).append( + {"item_code": item_code, "warehouse": warehouse, "reorder_qty": reorder_qty} + ) for item_code in items_to_consider: item = frappe.get_doc("Item", item_code) @@ -78,19 +86,30 @@ def _reorder_item(): if item.get("reorder_levels"): for d in item.get("reorder_levels"): - add_to_material_request(item_code, d.warehouse, d.warehouse_reorder_level, - d.warehouse_reorder_qty, d.material_request_type, warehouse_group=d.warehouse_group) + add_to_material_request( + item_code, + d.warehouse, + d.warehouse_reorder_level, + d.warehouse_reorder_qty, + d.material_request_type, + warehouse_group=d.warehouse_group, + ) if material_requests: return create_material_request(material_requests) + def get_item_warehouse_projected_qty(items_to_consider): item_warehouse_projected_qty = {} - for item_code, warehouse, projected_qty in frappe.db.sql("""select item_code, warehouse, projected_qty + for item_code, warehouse, projected_qty in frappe.db.sql( + """select item_code, warehouse, projected_qty from tabBin where item_code in ({0}) - and (warehouse != "" and warehouse is not null)"""\ - .format(", ".join(["%s"] * len(items_to_consider))), items_to_consider): + and (warehouse != "" and warehouse is not null)""".format( + ", ".join(["%s"] * len(items_to_consider)) + ), + items_to_consider, + ): if item_code not in item_warehouse_projected_qty: item_warehouse_projected_qty.setdefault(item_code, {}) @@ -102,15 +121,18 @@ def get_item_warehouse_projected_qty(items_to_consider): while warehouse_doc.parent_warehouse: if not item_warehouse_projected_qty.get(item_code, {}).get(warehouse_doc.parent_warehouse): - item_warehouse_projected_qty.setdefault(item_code, {})[warehouse_doc.parent_warehouse] = flt(projected_qty) + item_warehouse_projected_qty.setdefault(item_code, {})[warehouse_doc.parent_warehouse] = flt( + projected_qty + ) else: item_warehouse_projected_qty[item_code][warehouse_doc.parent_warehouse] += flt(projected_qty) warehouse_doc = frappe.get_doc("Warehouse", warehouse_doc.parent_warehouse) return item_warehouse_projected_qty + def create_material_request(material_requests): - """ Create indent on reaching reorder level """ + """Create indent on reaching reorder level""" mr_list = [] exceptions_list = [] @@ -131,11 +153,13 @@ def create_material_request(material_requests): continue mr = frappe.new_doc("Material Request") - mr.update({ - "company": company, - "transaction_date": nowdate(), - "material_request_type": "Material Transfer" if request_type=="Transfer" else request_type - }) + mr.update( + { + "company": company, + "transaction_date": nowdate(), + "material_request_type": "Material Transfer" if request_type == "Transfer" else request_type, + } + ) for d in items: d = frappe._dict(d) @@ -143,30 +167,37 @@ def create_material_request(material_requests): uom = item.stock_uom conversion_factor = 1.0 - if request_type == 'Purchase': + if request_type == "Purchase": uom = item.purchase_uom or item.stock_uom if uom != item.stock_uom: - conversion_factor = frappe.db.get_value("UOM Conversion Detail", - {'parent': item.name, 'uom': uom}, 'conversion_factor') or 1.0 + conversion_factor = ( + frappe.db.get_value( + "UOM Conversion Detail", {"parent": item.name, "uom": uom}, "conversion_factor" + ) + or 1.0 + ) must_be_whole_number = frappe.db.get_value("UOM", uom, "must_be_whole_number", cache=True) qty = d.reorder_qty / conversion_factor if must_be_whole_number: qty = ceil(qty) - mr.append("items", { - "doctype": "Material Request Item", - "item_code": d.item_code, - "schedule_date": add_days(nowdate(),cint(item.lead_time_days)), - "qty": qty, - "uom": uom, - "stock_uom": item.stock_uom, - "warehouse": d.warehouse, - "item_name": item.item_name, - "description": item.description, - "item_group": item.item_group, - "brand": item.brand, - }) + mr.append( + "items", + { + "doctype": "Material Request Item", + "item_code": d.item_code, + "schedule_date": add_days(nowdate(), cint(item.lead_time_days)), + "qty": qty, + "uom": uom, + "stock_uom": item.stock_uom, + "warehouse": d.warehouse, + "item_name": item.item_name, + "description": item.description, + "item_group": item.item_group, + "brand": item.brand, + }, + ) schedule_dates = [d.schedule_date for d in mr.items] mr.schedule_date = max(schedule_dates or [nowdate()]) @@ -180,10 +211,11 @@ def create_material_request(material_requests): if mr_list: if getattr(frappe.local, "reorder_email_notify", None) is None: - frappe.local.reorder_email_notify = cint(frappe.db.get_value('Stock Settings', None, - 'reorder_email_notify')) + frappe.local.reorder_email_notify = cint( + frappe.db.get_value("Stock Settings", None, "reorder_email_notify") + ) - if(frappe.local.reorder_email_notify): + if frappe.local.reorder_email_notify: send_email_notification(mr_list) if exceptions_list: @@ -191,33 +223,44 @@ def create_material_request(material_requests): return mr_list -def send_email_notification(mr_list): - """ Notify user about auto creation of indent""" - email_list = frappe.db.sql_list("""select distinct r.parent +def send_email_notification(mr_list): + """Notify user about auto creation of indent""" + + email_list = frappe.db.sql_list( + """select distinct r.parent from `tabHas Role` r, tabUser p where p.name = r.parent and p.enabled = 1 and p.docstatus < 2 and r.role in ('Purchase Manager','Stock Manager') - and p.name not in ('Administrator', 'All', 'Guest')""") + and p.name not in ('Administrator', 'All', 'Guest')""" + ) - msg = frappe.render_template("templates/emails/reorder_item.html", { - "mr_list": mr_list - }) + msg = frappe.render_template("templates/emails/reorder_item.html", {"mr_list": mr_list}) + + frappe.sendmail(recipients=email_list, subject=_("Auto Material Requests Generated"), message=msg) - frappe.sendmail(recipients=email_list, - subject=_('Auto Material Requests Generated'), message = msg) def notify_errors(exceptions_list): subject = _("[Important] [ERPNext] Auto Reorder Errors") - content = _("Dear System Manager,") + "
    " + _("An error occured for certain Items while creating Material Requests based on Re-order level. \ - Please rectify these issues :") + "
    " + content = ( + _("Dear System Manager,") + + "
    " + + _( + "An error occured for certain Items while creating Material Requests based on Re-order level. \ + Please rectify these issues :" + ) + + "
    " + ) for exception in exceptions_list: exception = json.loads(exception) - error_message = """
    {0}

    """.format(_(exception.get("message"))) + error_message = """
    {0}

    """.format( + _(exception.get("message")) + ) content += error_message content += _("Regards,") + "
    " + _("Administrator") from frappe.email import sendmail_to_system_managers + sendmail_to_system_managers(subject, content) diff --git a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py index 87097c72fa..3d9b046197 100644 --- a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py +++ b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py @@ -8,7 +8,8 @@ from frappe.utils import cint, getdate def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} float_precision = cint(frappe.db.get_default("float_precision")) or 3 @@ -22,22 +23,37 @@ def execute(filters=None): for batch in sorted(iwb_map[item][wh]): qty_dict = iwb_map[item][wh][batch] - data.append([item, item_map[item]["item_name"], item_map[item]["description"], wh, batch, - frappe.db.get_value('Batch', batch, 'expiry_date'), qty_dict.expiry_status - ]) - + data.append( + [ + item, + item_map[item]["item_name"], + item_map[item]["description"], + wh, + batch, + frappe.db.get_value("Batch", batch, "expiry_date"), + qty_dict.expiry_status, + ] + ) return columns, data + def get_columns(filters): """return columns based on filters""" - columns = [_("Item") + ":Link/Item:100"] + [_("Item Name") + "::150"] + [_("Description") + "::150"] + \ - [_("Warehouse") + ":Link/Warehouse:100"] + [_("Batch") + ":Link/Batch:100"] + [_("Expires On") + ":Date:90"] + \ - [_("Expiry (In Days)") + ":Int:120"] + columns = ( + [_("Item") + ":Link/Item:100"] + + [_("Item Name") + "::150"] + + [_("Description") + "::150"] + + [_("Warehouse") + ":Link/Warehouse:100"] + + [_("Batch") + ":Link/Batch:100"] + + [_("Expires On") + ":Date:90"] + + [_("Expiry (In Days)") + ":Int:120"] + ) return columns + def get_conditions(filters): conditions = "" if not filters.get("from_date"): @@ -50,14 +66,19 @@ def get_conditions(filters): return conditions + def get_stock_ledger_entries(filters): conditions = get_conditions(filters) - return frappe.db.sql("""select item_code, batch_no, warehouse, + return frappe.db.sql( + """select item_code, batch_no, warehouse, posting_date, actual_qty from `tabStock Ledger Entry` where is_cancelled = 0 - and docstatus < 2 and ifnull(batch_no, '') != '' %s order by item_code, warehouse""" % - conditions, as_dict=1) + and docstatus < 2 and ifnull(batch_no, '') != '' %s order by item_code, warehouse""" + % conditions, + as_dict=1, + ) + def get_item_warehouse_batch_map(filters, float_precision): sle = get_stock_ledger_entries(filters) @@ -67,13 +88,13 @@ def get_item_warehouse_batch_map(filters, float_precision): to_date = getdate(filters["to_date"]) for d in sle: - iwb_map.setdefault(d.item_code, {}).setdefault(d.warehouse, {})\ - .setdefault(d.batch_no, frappe._dict({ - "expires_on": None, "expiry_status": None})) + iwb_map.setdefault(d.item_code, {}).setdefault(d.warehouse, {}).setdefault( + d.batch_no, frappe._dict({"expires_on": None, "expiry_status": None}) + ) qty_dict = iwb_map[d.item_code][d.warehouse][d.batch_no] - expiry_date_unicode = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') + expiry_date_unicode = frappe.db.get_value("Batch", d.batch_no, "expiry_date") qty_dict.expires_on = expiry_date_unicode exp_date = frappe.utils.data.getdate(expiry_date_unicode) @@ -88,6 +109,7 @@ def get_item_warehouse_batch_map(filters, float_precision): return iwb_map + def get_item_details(filters): item_map = {} for d in frappe.db.sql("select name, item_name, description from tabItem", as_dict=1): diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index 9b21deabcd..8a13300dc8 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -8,7 +8,8 @@ from frappe.utils import cint, flt, getdate def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date")) @@ -26,11 +27,20 @@ def execute(filters=None): for batch in sorted(iwb_map[item][wh]): qty_dict = iwb_map[item][wh][batch] if qty_dict.opening_qty or qty_dict.in_qty or qty_dict.out_qty or qty_dict.bal_qty: - data.append([item, item_map[item]["item_name"], item_map[item]["description"], wh, batch, - flt(qty_dict.opening_qty, float_precision), flt(qty_dict.in_qty, float_precision), - flt(qty_dict.out_qty, float_precision), flt(qty_dict.bal_qty, float_precision), - item_map[item]["stock_uom"] - ]) + data.append( + [ + item, + item_map[item]["item_name"], + item_map[item]["description"], + wh, + batch, + flt(qty_dict.opening_qty, float_precision), + flt(qty_dict.in_qty, float_precision), + flt(qty_dict.out_qty, float_precision), + flt(qty_dict.bal_qty, float_precision), + item_map[item]["stock_uom"], + ] + ) return columns, data @@ -38,10 +48,18 @@ def execute(filters=None): def get_columns(filters): """return columns based on filters""" - columns = [_("Item") + ":Link/Item:100"] + [_("Item Name") + "::150"] + [_("Description") + "::150"] + \ - [_("Warehouse") + ":Link/Warehouse:100"] + [_("Batch") + ":Link/Batch:100"] + [_("Opening Qty") + ":Float:90"] + \ - [_("In Qty") + ":Float:80"] + [_("Out Qty") + ":Float:80"] + [_("Balance Qty") + ":Float:90"] + \ - [_("UOM") + "::90"] + columns = ( + [_("Item") + ":Link/Item:100"] + + [_("Item Name") + "::150"] + + [_("Description") + "::150"] + + [_("Warehouse") + ":Link/Warehouse:100"] + + [_("Batch") + ":Link/Batch:100"] + + [_("Opening Qty") + ":Float:90"] + + [_("In Qty") + ":Float:80"] + + [_("Out Qty") + ":Float:80"] + + [_("Balance Qty") + ":Float:90"] + + [_("UOM") + "::90"] + ) return columns @@ -66,13 +84,16 @@ def get_conditions(filters): # get all details def get_stock_ledger_entries(filters): conditions = get_conditions(filters) - return frappe.db.sql(""" + return frappe.db.sql( + """ select item_code, batch_no, warehouse, posting_date, sum(actual_qty) as actual_qty from `tabStock Ledger Entry` where is_cancelled = 0 and docstatus < 2 and ifnull(batch_no, '') != '' %s group by voucher_no, batch_no, item_code, warehouse - order by item_code, warehouse""" % - conditions, as_dict=1) + order by item_code, warehouse""" + % conditions, + as_dict=1, + ) def get_item_warehouse_batch_map(filters, float_precision): @@ -83,20 +104,21 @@ def get_item_warehouse_batch_map(filters, float_precision): to_date = getdate(filters["to_date"]) for d in sle: - iwb_map.setdefault(d.item_code, {}).setdefault(d.warehouse, {})\ - .setdefault(d.batch_no, frappe._dict({ - "opening_qty": 0.0, "in_qty": 0.0, "out_qty": 0.0, "bal_qty": 0.0 - })) + iwb_map.setdefault(d.item_code, {}).setdefault(d.warehouse, {}).setdefault( + d.batch_no, frappe._dict({"opening_qty": 0.0, "in_qty": 0.0, "out_qty": 0.0, "bal_qty": 0.0}) + ) qty_dict = iwb_map[d.item_code][d.warehouse][d.batch_no] if d.posting_date < from_date: - qty_dict.opening_qty = flt(qty_dict.opening_qty, float_precision) \ - + flt(d.actual_qty, float_precision) + qty_dict.opening_qty = flt(qty_dict.opening_qty, float_precision) + flt( + d.actual_qty, float_precision + ) elif d.posting_date >= from_date and d.posting_date <= to_date: if flt(d.actual_qty) > 0: qty_dict.in_qty = flt(qty_dict.in_qty, float_precision) + flt(d.actual_qty, float_precision) else: - qty_dict.out_qty = flt(qty_dict.out_qty, float_precision) \ - + abs(flt(d.actual_qty, float_precision)) + qty_dict.out_qty = flt(qty_dict.out_qty, float_precision) + abs( + flt(d.actual_qty, float_precision) + ) qty_dict.bal_qty = flt(qty_dict.bal_qty, float_precision) + flt(d.actual_qty, float_precision) diff --git a/erpnext/stock/report/bom_search/bom_search.py b/erpnext/stock/report/bom_search/bom_search.py index a22b224867..3be87abc39 100644 --- a/erpnext/stock/report/bom_search/bom_search.py +++ b/erpnext/stock/report/bom_search/bom_search.py @@ -10,11 +10,13 @@ def execute(filters=None): parents = { "Product Bundle Item": "Product Bundle", "BOM Explosion Item": "BOM", - "BOM Item": "BOM" + "BOM Item": "BOM", } - for doctype in ("Product Bundle Item", - "BOM Explosion Item" if filters.search_sub_assemblies else "BOM Item"): + for doctype in ( + "Product Bundle Item", + "BOM Explosion Item" if filters.search_sub_assemblies else "BOM Item", + ): all_boms = {} for d in frappe.get_all(doctype, fields=["parent", "item_code"]): all_boms.setdefault(d.parent, []).append(d.item_code) @@ -29,16 +31,13 @@ def execute(filters=None): if valid: data.append((parent, parents[doctype])) - return [{ - "fieldname": "parent", - "label": "BOM", - "width": 200, - "fieldtype": "Dynamic Link", - "options": "doctype" - }, - { - "fieldname": "doctype", - "label": "Type", - "width": 200, - "fieldtype": "Data" - }], data + return [ + { + "fieldname": "parent", + "label": "BOM", + "width": 200, + "fieldtype": "Dynamic Link", + "options": "doctype", + }, + {"fieldname": "doctype", "label": "Type", "width": 200, "fieldtype": "Data"}, + ], data diff --git a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py index 058af77aa2..4642a535b6 100644 --- a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py +++ b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py @@ -41,18 +41,8 @@ def validate_filters(filters: Filters) -> None: def get_columns() -> Columns: return [ - { - 'label': _('Item Group'), - 'fieldname': 'item_group', - 'fieldtype': 'Data', - 'width': '200' - }, - { - 'label': _('COGS Debit'), - 'fieldname': 'cogs_debit', - 'fieldtype': 'Currency', - 'width': '200' - } + {"label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Data", "width": "200"}, + {"label": _("COGS Debit"), "fieldname": "cogs_debit", "fieldtype": "Currency", "width": "200"}, ] @@ -67,11 +57,11 @@ def get_data(filters: Filters) -> Data: data = [] for item in leveled_dict.items(): i = item[1] - if i['agg_value'] == 0: + if i["agg_value"] == 0: continue - data.append(get_row(i['name'], i['agg_value'], i['is_group'], i['level'])) - if i['self_value'] < i['agg_value'] and i['self_value'] > 0: - data.append(get_row(i['name'], i['self_value'], 0, i['level'] + 1)) + data.append(get_row(i["name"], i["agg_value"], i["is_group"], i["level"])) + if i["self_value"] < i["agg_value"] and i["self_value"] > 0: + data.append(get_row(i["name"], i["self_value"], 0, i["level"] + 1)) return data @@ -79,8 +69,8 @@ def get_filtered_entries(filters: Filters) -> FilteredEntries: gl_entries = get_gl_entries(filters, []) filtered_entries = [] for entry in gl_entries: - posting_date = entry.get('posting_date') - from_date = filters.get('from_date') + posting_date = entry.get("posting_date") + from_date = filters.get("from_date") if date_diff(from_date, posting_date) > 0: continue filtered_entries.append(entry) @@ -88,10 +78,11 @@ def get_filtered_entries(filters: Filters) -> FilteredEntries: def get_stock_value_difference_list(filtered_entries: FilteredEntries) -> SVDList: - voucher_nos = [fe.get('voucher_no') for fe in filtered_entries] + voucher_nos = [fe.get("voucher_no") for fe in filtered_entries] svd_list = frappe.get_list( - 'Stock Ledger Entry', fields=['item_code','stock_value_difference'], - filters=[('voucher_no', 'in', voucher_nos), ("is_cancelled", "=", 0)] + "Stock Ledger Entry", + fields=["item_code", "stock_value_difference"], + filters=[("voucher_no", "in", voucher_nos), ("is_cancelled", "=", 0)], ) assign_item_groups_to_svd_list(svd_list) return svd_list @@ -99,7 +90,7 @@ def get_stock_value_difference_list(filtered_entries: FilteredEntries) -> SVDLis def get_leveled_dict() -> OrderedDict: item_groups_dict = get_item_groups_dict() - lr_list = sorted(item_groups_dict, key=lambda x : int(x[0])) + lr_list = sorted(item_groups_dict, key=lambda x: int(x[0])) leveled_dict = OrderedDict() current_level = 0 nesting_r = [] @@ -108,10 +99,10 @@ def get_leveled_dict() -> OrderedDict: nesting_r.pop() current_level -= 1 - leveled_dict[(l,r)] = { - 'level' : current_level, - 'name' : item_groups_dict[(l,r)]['name'], - 'is_group' : item_groups_dict[(l,r)]['is_group'] + leveled_dict[(l, r)] = { + "level": current_level, + "name": item_groups_dict[(l, r)]["name"], + "is_group": item_groups_dict[(l, r)]["is_group"], } if int(r) - int(l) > 1: @@ -123,38 +114,38 @@ def get_leveled_dict() -> OrderedDict: def assign_self_values(leveled_dict: OrderedDict, svd_list: SVDList) -> None: - key_dict = {v['name']:k for k, v in leveled_dict.items()} + key_dict = {v["name"]: k for k, v in leveled_dict.items()} for item in svd_list: key = key_dict[item.get("item_group")] - leveled_dict[key]['self_value'] += -item.get("stock_value_difference") + leveled_dict[key]["self_value"] += -item.get("stock_value_difference") def assign_agg_values(leveled_dict: OrderedDict) -> None: keys = list(leveled_dict.keys())[::-1] - prev_level = leveled_dict[keys[-1]]['level'] + prev_level = leveled_dict[keys[-1]]["level"] accu = [0] for k in keys[:-1]: - curr_level = leveled_dict[k]['level'] + curr_level = leveled_dict[k]["level"] if curr_level == prev_level: - accu[-1] += leveled_dict[k]['self_value'] - leveled_dict[k]['agg_value'] = leveled_dict[k]['self_value'] + accu[-1] += leveled_dict[k]["self_value"] + leveled_dict[k]["agg_value"] = leveled_dict[k]["self_value"] elif curr_level > prev_level: - accu.append(leveled_dict[k]['self_value']) - leveled_dict[k]['agg_value'] = accu[-1] + accu.append(leveled_dict[k]["self_value"]) + leveled_dict[k]["agg_value"] = accu[-1] elif curr_level < prev_level: - accu[-1] += leveled_dict[k]['self_value'] - leveled_dict[k]['agg_value'] = accu[-1] + accu[-1] += leveled_dict[k]["self_value"] + leveled_dict[k]["agg_value"] = accu[-1] prev_level = curr_level # root node rk = keys[-1] - leveled_dict[rk]['agg_value'] = sum(accu) + leveled_dict[rk]['self_value'] + leveled_dict[rk]["agg_value"] = sum(accu) + leveled_dict[rk]["self_value"] -def get_row(name:str, value:float, is_bold:int, indent:int) -> Row: +def get_row(name: str, value: float, is_bold: int, indent: int) -> Row: item_group = name if is_bold: item_group = frappe.bold(item_group) @@ -168,20 +159,20 @@ def assign_item_groups_to_svd_list(svd_list: SVDList) -> None: def get_item_groups_map(svd_list: SVDList) -> Dict[str, str]: - item_codes = set(i['item_code'] for i in svd_list) + item_codes = set(i["item_code"] for i in svd_list) ig_list = frappe.get_list( - 'Item', fields=['item_code','item_group'], - filters=[('item_code', 'in', item_codes)] + "Item", fields=["item_code", "item_group"], filters=[("item_code", "in", item_codes)] ) - return {i['item_code']:i['item_group'] for i in ig_list} + return {i["item_code"]: i["item_group"] for i in ig_list} def get_item_groups_dict() -> ItemGroupsDict: item_groups_list = frappe.get_all("Item Group", fields=("name", "is_group", "lft", "rgt")) - return {(i['lft'],i['rgt']):{'name':i['name'], 'is_group':i['is_group']} - for i in item_groups_list} + return { + (i["lft"], i["rgt"]): {"name": i["name"], "is_group": i["is_group"]} for i in item_groups_list + } def update_leveled_dict(leveled_dict: OrderedDict) -> None: for k in leveled_dict: - leveled_dict[k].update({'self_value':0, 'agg_value':0}) + leveled_dict[k].update({"self_value": 0, "agg_value": 0}) diff --git a/erpnext/stock/report/delayed_item_report/delayed_item_report.py b/erpnext/stock/report/delayed_item_report/delayed_item_report.py index 4ec36ea417..9df24d6559 100644 --- a/erpnext/stock/report/delayed_item_report/delayed_item_report.py +++ b/erpnext/stock/report/delayed_item_report/delayed_item_report.py @@ -7,11 +7,12 @@ from frappe import _ from frappe.utils import date_diff -def execute(filters=None, consolidated = False): +def execute(filters=None, consolidated=False): data, columns = DelayedItemReport(filters).run() return data, columns + class DelayedItemReport(object): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) @@ -23,28 +24,38 @@ class DelayedItemReport(object): conditions = "" doctype = self.filters.get("based_on") - child_doc= "%s Item" % doctype + child_doc = "%s Item" % doctype if doctype == "Sales Invoice": conditions = " and `tabSales Invoice`.update_stock = 1 and `tabSales Invoice`.is_pos = 0" if self.filters.get("item_group"): - conditions += " and `tab%s`.item_group = %s" % (child_doc, - frappe.db.escape(self.filters.get("item_group"))) + conditions += " and `tab%s`.item_group = %s" % ( + child_doc, + frappe.db.escape(self.filters.get("item_group")), + ) for field in ["customer", "customer_group", "company"]: if self.filters.get(field): - conditions += " and `tab%s`.%s = %s" % (doctype, - field, frappe.db.escape(self.filters.get(field))) + conditions += " and `tab%s`.%s = %s" % ( + doctype, + field, + frappe.db.escape(self.filters.get(field)), + ) sales_order_field = "against_sales_order" if doctype == "Sales Invoice": sales_order_field = "sales_order" if self.filters.get("sales_order"): - conditions = " and `tab%s`.%s = '%s'" %(child_doc, sales_order_field, self.filters.get("sales_order")) + conditions = " and `tab%s`.%s = '%s'" % ( + child_doc, + sales_order_field, + self.filters.get("sales_order"), + ) - self.transactions = frappe.db.sql(""" SELECT `tab{child_doc}`.item_code, `tab{child_doc}`.item_name, + self.transactions = frappe.db.sql( + """ SELECT `tab{child_doc}`.item_code, `tab{child_doc}`.item_name, `tab{child_doc}`.item_group, `tab{child_doc}`.qty, `tab{child_doc}`.rate, `tab{child_doc}`.amount, `tab{child_doc}`.so_detail, `tab{child_doc}`.{so_field} as sales_order, `tab{doctype}`.shipping_address_name, `tab{doctype}`.po_no, `tab{doctype}`.customer, @@ -54,10 +65,12 @@ class DelayedItemReport(object): `tab{child_doc}`.parent = `tab{doctype}`.name and `tab{doctype}`.docstatus = 1 and `tab{doctype}`.posting_date between %(from_date)s and %(to_date)s and `tab{child_doc}`.{so_field} is not null and `tab{child_doc}`.{so_field} != '' {cond} - """.format(cond=conditions, doctype=doctype, child_doc=child_doc, so_field=sales_order_field), { - 'from_date': self.filters.get('from_date'), - 'to_date': self.filters.get('to_date') - }, as_dict=1) + """.format( + cond=conditions, doctype=doctype, child_doc=child_doc, so_field=sales_order_field + ), + {"from_date": self.filters.get("from_date"), "to_date": self.filters.get("to_date")}, + as_dict=1, + ) if self.transactions: self.filter_transactions_data(consolidated) @@ -67,112 +80,85 @@ class DelayedItemReport(object): def filter_transactions_data(self, consolidated=False): sales_orders = [d.sales_order for d in self.transactions] doctype = "Sales Order" - filters = {'name': ('in', sales_orders)} + filters = {"name": ("in", sales_orders)} if not consolidated: sales_order_items = [d.so_detail for d in self.transactions] doctype = "Sales Order Item" - filters = {'parent': ('in', sales_orders), 'name': ('in', sales_order_items)} + filters = {"parent": ("in", sales_orders), "name": ("in", sales_order_items)} so_data = {} - for d in frappe.get_all(doctype, filters = filters, - fields = ["delivery_date", "parent", "name"]): + for d in frappe.get_all(doctype, filters=filters, fields=["delivery_date", "parent", "name"]): key = d.name if consolidated else (d.parent, d.name) if key not in so_data: so_data.setdefault(key, d.delivery_date) for row in self.transactions: key = row.sales_order if consolidated else (row.sales_order, row.so_detail) - row.update({ - 'delivery_date': so_data.get(key), - 'delayed_days': date_diff(row.posting_date, so_data.get(key)) - }) + row.update( + { + "delivery_date": so_data.get(key), + "delayed_days": date_diff(row.posting_date, so_data.get(key)), + } + ) return self.transactions def get_columns(self): based_on = self.filters.get("based_on") - return [{ - "label": _(based_on), - "fieldname": "name", - "fieldtype": "Link", - "options": based_on, - "width": 100 - }, - { - "label": _("Customer"), - "fieldname": "customer", - "fieldtype": "Link", - "options": "Customer", - "width": 200 - }, - { - "label": _("Shipping Address"), - "fieldname": "shipping_address_name", - "fieldtype": "Link", - "options": "Address", - "width": 120 - }, - { - "label": _("Expected Delivery Date"), - "fieldname": "delivery_date", - "fieldtype": "Date", - "width": 100 - }, - { - "label": _("Actual Delivery Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 100 - }, - { - "label": _("Item Code"), - "fieldname": "item_code", - "fieldtype": "Link", - "options": "Item", - "width": 100 - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 100 - }, - { - "label": _("Quantity"), - "fieldname": "qty", - "fieldtype": "Float", - "width": 100 - }, - { - "label": _("Rate"), - "fieldname": "rate", - "fieldtype": "Currency", - "width": 100 - }, - { - "label": _("Amount"), - "fieldname": "amount", - "fieldtype": "Currency", - "width": 100 - }, - { - "label": _("Delayed Days"), - "fieldname": "delayed_days", - "fieldtype": "Int", - "width": 100 - }, - { - "label": _("Sales Order"), - "fieldname": "sales_order", - "fieldtype": "Link", - "options": "Sales Order", - "width": 100 - }, - { - "label": _("Customer PO"), - "fieldname": "po_no", - "fieldtype": "Data", - "width": 100 - }] + return [ + { + "label": _(based_on), + "fieldname": "name", + "fieldtype": "Link", + "options": based_on, + "width": 100, + }, + { + "label": _("Customer"), + "fieldname": "customer", + "fieldtype": "Link", + "options": "Customer", + "width": 200, + }, + { + "label": _("Shipping Address"), + "fieldname": "shipping_address_name", + "fieldtype": "Link", + "options": "Address", + "width": 120, + }, + { + "label": _("Expected Delivery Date"), + "fieldname": "delivery_date", + "fieldtype": "Date", + "width": 100, + }, + { + "label": _("Actual Delivery Date"), + "fieldname": "posting_date", + "fieldtype": "Date", + "width": 100, + }, + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 100, + }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 100}, + {"label": _("Quantity"), "fieldname": "qty", "fieldtype": "Float", "width": 100}, + {"label": _("Rate"), "fieldname": "rate", "fieldtype": "Currency", "width": 100}, + {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 100}, + {"label": _("Delayed Days"), "fieldname": "delayed_days", "fieldtype": "Int", "width": 100}, + { + "label": _("Sales Order"), + "fieldname": "sales_order", + "fieldtype": "Link", + "options": "Sales Order", + "width": 100, + }, + {"label": _("Customer PO"), "fieldname": "po_no", "fieldtype": "Data", "width": 100}, + ] diff --git a/erpnext/stock/report/delayed_order_report/delayed_order_report.py b/erpnext/stock/report/delayed_order_report/delayed_order_report.py index 26090ab8ff..197218d7ff 100644 --- a/erpnext/stock/report/delayed_order_report/delayed_order_report.py +++ b/erpnext/stock/report/delayed_order_report/delayed_order_report.py @@ -14,6 +14,7 @@ def execute(filters=None): return columns, data + class DelayedOrderReport(DelayedItemReport): def run(self): return self.get_columns(), self.get_data(consolidated=True) or [] @@ -33,60 +34,48 @@ class DelayedOrderReport(DelayedItemReport): def get_columns(self): based_on = self.filters.get("based_on") - return [{ - "label": _(based_on), - "fieldname": "name", - "fieldtype": "Link", - "options": based_on, - "width": 100 - },{ - "label": _("Customer"), - "fieldname": "customer", - "fieldtype": "Link", - "options": "Customer", - "width": 200 - }, - { - "label": _("Shipping Address"), - "fieldname": "shipping_address_name", - "fieldtype": "Link", - "options": "Address", - "width": 140 - }, - { - "label": _("Expected Delivery Date"), - "fieldname": "delivery_date", - "fieldtype": "Date", - "width": 100 - }, - { - "label": _("Actual Delivery Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 100 - }, - { - "label": _("Amount"), - "fieldname": "grand_total", - "fieldtype": "Currency", - "width": 100 - }, - { - "label": _("Delayed Days"), - "fieldname": "delayed_days", - "fieldtype": "Int", - "width": 100 - }, - { - "label": _("Sales Order"), - "fieldname": "sales_order", - "fieldtype": "Link", - "options": "Sales Order", - "width": 150 - }, - { - "label": _("Customer PO"), - "fieldname": "po_no", - "fieldtype": "Data", - "width": 110 - }] + return [ + { + "label": _(based_on), + "fieldname": "name", + "fieldtype": "Link", + "options": based_on, + "width": 100, + }, + { + "label": _("Customer"), + "fieldname": "customer", + "fieldtype": "Link", + "options": "Customer", + "width": 200, + }, + { + "label": _("Shipping Address"), + "fieldname": "shipping_address_name", + "fieldtype": "Link", + "options": "Address", + "width": 140, + }, + { + "label": _("Expected Delivery Date"), + "fieldname": "delivery_date", + "fieldtype": "Date", + "width": 100, + }, + { + "label": _("Actual Delivery Date"), + "fieldname": "posting_date", + "fieldtype": "Date", + "width": 100, + }, + {"label": _("Amount"), "fieldname": "grand_total", "fieldtype": "Currency", "width": 100}, + {"label": _("Delayed Days"), "fieldname": "delayed_days", "fieldtype": "Int", "width": 100}, + { + "label": _("Sales Order"), + "fieldname": "sales_order", + "fieldtype": "Link", + "options": "Sales Order", + "width": 150, + }, + {"label": _("Customer PO"), "fieldname": "po_no", "fieldtype": "Data", "width": 110}, + ] diff --git a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py index b7ac7ff6a5..7a1b8c0cee 100644 --- a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py +++ b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py @@ -8,7 +8,8 @@ from erpnext.controllers.trends import get_columns, get_data def execute(filters=None): - if not filters: filters ={} + if not filters: + filters = {} data = [] conditions = get_columns(filters, "Delivery Note") data = get_data(filters, conditions) @@ -17,6 +18,7 @@ def execute(filters=None): return conditions["columns"], data, None, chart_data + def get_chart_data(data, filters): if not data: return [] @@ -27,7 +29,7 @@ def get_chart_data(data, filters): # consider only consolidated row data = [row for row in data if row[0]] - data = sorted(data, key = lambda i: i[-1],reverse=True) + data = sorted(data, key=lambda i: i[-1], reverse=True) if len(data) > 10: # get top 10 if data too long @@ -39,13 +41,8 @@ def get_chart_data(data, filters): return { "data": { - "labels" : labels, - "datasets" : [ - { - "name": _("Total Delivered Amount"), - "values": datapoints - } - ] + "labels": labels, + "datasets": [{"name": _("Total Delivered Amount"), "values": datapoints}], }, - "type" : "bar" + "type": "bar", } diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py index 6aa12ac2c9..bcc213905d 100644 --- a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py +++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py @@ -12,6 +12,7 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_data(filters): data = get_stock_ledger_entries(filters) itewise_balance_qty = {} @@ -23,6 +24,7 @@ def get_data(filters): res = validate_data(itewise_balance_qty) return res + def validate_data(itewise_balance_qty): res = [] for key, data in itewise_balance_qty.items(): @@ -33,6 +35,7 @@ def validate_data(itewise_balance_qty): return res + def get_incorrect_data(data): balance_qty = 0.0 for row in data: @@ -45,67 +48,84 @@ def get_incorrect_data(data): row.differnce = abs(flt(row.expected_balance_qty) - flt(row.qty_after_transaction)) return row + def get_stock_ledger_entries(report_filters): filters = {"is_cancelled": 0} - fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'actual_qty', - 'posting_date', 'posting_time', 'company', 'warehouse', 'qty_after_transaction', 'batch_no'] + fields = [ + "name", + "voucher_type", + "voucher_no", + "item_code", + "actual_qty", + "posting_date", + "posting_time", + "company", + "warehouse", + "qty_after_transaction", + "batch_no", + ] - for field in ['warehouse', 'item_code', 'company']: + for field in ["warehouse", "item_code", "company"]: if report_filters.get(field): filters[field] = report_filters.get(field) - return frappe.get_all('Stock Ledger Entry', fields = fields, filters = filters, - order_by = 'timestamp(posting_date, posting_time) asc, creation asc') + return frappe.get_all( + "Stock Ledger Entry", + fields=fields, + filters=filters, + order_by="timestamp(posting_date, posting_time) asc, creation asc", + ) + def get_columns(): - return [{ - 'label': _('Id'), - 'fieldtype': 'Link', - 'fieldname': 'name', - 'options': 'Stock Ledger Entry', - 'width': 120 - }, { - 'label': _('Posting Date'), - 'fieldtype': 'Date', - 'fieldname': 'posting_date', - 'width': 110 - }, { - 'label': _('Voucher Type'), - 'fieldtype': 'Link', - 'fieldname': 'voucher_type', - 'options': 'DocType', - 'width': 120 - }, { - 'label': _('Voucher No'), - 'fieldtype': 'Dynamic Link', - 'fieldname': 'voucher_no', - 'options': 'voucher_type', - 'width': 120 - }, { - 'label': _('Item Code'), - 'fieldtype': 'Link', - 'fieldname': 'item_code', - 'options': 'Item', - 'width': 120 - }, { - 'label': _('Warehouse'), - 'fieldtype': 'Link', - 'fieldname': 'warehouse', - 'options': 'Warehouse', - 'width': 120 - }, { - 'label': _('Expected Balance Qty'), - 'fieldtype': 'Float', - 'fieldname': 'expected_balance_qty', - 'width': 170 - }, { - 'label': _('Actual Balance Qty'), - 'fieldtype': 'Float', - 'fieldname': 'qty_after_transaction', - 'width': 150 - }, { - 'label': _('Difference'), - 'fieldtype': 'Float', - 'fieldname': 'differnce', - 'width': 110 - }] + return [ + { + "label": _("Id"), + "fieldtype": "Link", + "fieldname": "name", + "options": "Stock Ledger Entry", + "width": 120, + }, + {"label": _("Posting Date"), "fieldtype": "Date", "fieldname": "posting_date", "width": 110}, + { + "label": _("Voucher Type"), + "fieldtype": "Link", + "fieldname": "voucher_type", + "options": "DocType", + "width": 120, + }, + { + "label": _("Voucher No"), + "fieldtype": "Dynamic Link", + "fieldname": "voucher_no", + "options": "voucher_type", + "width": 120, + }, + { + "label": _("Item Code"), + "fieldtype": "Link", + "fieldname": "item_code", + "options": "Item", + "width": 120, + }, + { + "label": _("Warehouse"), + "fieldtype": "Link", + "fieldname": "warehouse", + "options": "Warehouse", + "width": 120, + }, + { + "label": _("Expected Balance Qty"), + "fieldtype": "Float", + "fieldname": "expected_balance_qty", + "width": 170, + }, + { + "label": _("Actual Balance Qty"), + "fieldtype": "Float", + "fieldname": "qty_after_transaction", + "width": 150, + }, + {"label": _("Difference"), "fieldtype": "Float", "fieldname": "differnce", "width": 110}, + ] diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py index be8597dfed..78c6961623 100644 --- a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py +++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py @@ -15,6 +15,7 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_data(filters): data = get_stock_ledger_entries(filters) serial_nos_data = prepare_serial_nos(data) @@ -22,6 +23,7 @@ def get_data(filters): return data + def prepare_serial_nos(data): serial_no_wise_data = {} for row in data: @@ -37,13 +39,16 @@ def prepare_serial_nos(data): return serial_no_wise_data + def get_incorrect_serial_nos(serial_nos_data): result = [] - total_value = frappe._dict({'qty': 0, 'valuation_rate': 0, 'serial_no': frappe.bold(_('Balance'))}) + total_value = frappe._dict( + {"qty": 0, "valuation_rate": 0, "serial_no": frappe.bold(_("Balance"))} + ) for serial_no, data in serial_nos_data.items(): - total_dict = frappe._dict({'qty': 0, 'valuation_rate': 0, 'serial_no': frappe.bold(_('Total'))}) + total_dict = frappe._dict({"qty": 0, "valuation_rate": 0, "serial_no": frappe.bold(_("Total"))}) if check_incorrect_serial_data(data, total_dict): result.extend(data) @@ -58,93 +63,111 @@ def get_incorrect_serial_nos(serial_nos_data): return result + def check_incorrect_serial_data(data, total_dict): incorrect_data = False for row in data: total_dict.qty += row.qty total_dict.valuation_rate += row.valuation_rate - if ((total_dict.qty == 0 and abs(total_dict.valuation_rate) > 0) or total_dict.qty < 0): + if (total_dict.qty == 0 and abs(total_dict.valuation_rate) > 0) or total_dict.qty < 0: incorrect_data = True return incorrect_data + def get_stock_ledger_entries(report_filters): - fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'serial_no as serial_nos', 'actual_qty', - 'posting_date', 'posting_time', 'company', 'warehouse', '(stock_value_difference / actual_qty) as valuation_rate'] + fields = [ + "name", + "voucher_type", + "voucher_no", + "item_code", + "serial_no as serial_nos", + "actual_qty", + "posting_date", + "posting_time", + "company", + "warehouse", + "(stock_value_difference / actual_qty) as valuation_rate", + ] - filters = {'serial_no': ("is", "set"), "is_cancelled": 0} + filters = {"serial_no": ("is", "set"), "is_cancelled": 0} - if report_filters.get('item_code'): - filters['item_code'] = report_filters.get('item_code') + if report_filters.get("item_code"): + filters["item_code"] = report_filters.get("item_code") - if report_filters.get('from_date') and report_filters.get('to_date'): - filters['posting_date'] = ('between', [report_filters.get('from_date'), report_filters.get('to_date')]) + if report_filters.get("from_date") and report_filters.get("to_date"): + filters["posting_date"] = ( + "between", + [report_filters.get("from_date"), report_filters.get("to_date")], + ) + + return frappe.get_all( + "Stock Ledger Entry", + fields=fields, + filters=filters, + order_by="timestamp(posting_date, posting_time) asc, creation asc", + ) - return frappe.get_all('Stock Ledger Entry', fields = fields, filters = filters, - order_by = 'timestamp(posting_date, posting_time) asc, creation asc') def get_columns(): - return [{ - 'label': _('Company'), - 'fieldtype': 'Link', - 'fieldname': 'company', - 'options': 'Company', - 'width': 120 - }, { - 'label': _('Id'), - 'fieldtype': 'Link', - 'fieldname': 'name', - 'options': 'Stock Ledger Entry', - 'width': 120 - }, { - 'label': _('Posting Date'), - 'fieldtype': 'Date', - 'fieldname': 'posting_date', - 'width': 90 - }, { - 'label': _('Posting Time'), - 'fieldtype': 'Time', - 'fieldname': 'posting_time', - 'width': 90 - }, { - 'label': _('Voucher Type'), - 'fieldtype': 'Link', - 'fieldname': 'voucher_type', - 'options': 'DocType', - 'width': 100 - }, { - 'label': _('Voucher No'), - 'fieldtype': 'Dynamic Link', - 'fieldname': 'voucher_no', - 'options': 'voucher_type', - 'width': 110 - }, { - 'label': _('Item Code'), - 'fieldtype': 'Link', - 'fieldname': 'item_code', - 'options': 'Item', - 'width': 120 - }, { - 'label': _('Warehouse'), - 'fieldtype': 'Link', - 'fieldname': 'warehouse', - 'options': 'Warehouse', - 'width': 120 - }, { - 'label': _('Serial No'), - 'fieldtype': 'Link', - 'fieldname': 'serial_no', - 'options': 'Serial No', - 'width': 100 - }, { - 'label': _('Qty'), - 'fieldtype': 'Float', - 'fieldname': 'qty', - 'width': 80 - }, { - 'label': _('Valuation Rate (In / Out)'), - 'fieldtype': 'Currency', - 'fieldname': 'valuation_rate', - 'width': 110 - }] + return [ + { + "label": _("Company"), + "fieldtype": "Link", + "fieldname": "company", + "options": "Company", + "width": 120, + }, + { + "label": _("Id"), + "fieldtype": "Link", + "fieldname": "name", + "options": "Stock Ledger Entry", + "width": 120, + }, + {"label": _("Posting Date"), "fieldtype": "Date", "fieldname": "posting_date", "width": 90}, + {"label": _("Posting Time"), "fieldtype": "Time", "fieldname": "posting_time", "width": 90}, + { + "label": _("Voucher Type"), + "fieldtype": "Link", + "fieldname": "voucher_type", + "options": "DocType", + "width": 100, + }, + { + "label": _("Voucher No"), + "fieldtype": "Dynamic Link", + "fieldname": "voucher_no", + "options": "voucher_type", + "width": 110, + }, + { + "label": _("Item Code"), + "fieldtype": "Link", + "fieldname": "item_code", + "options": "Item", + "width": 120, + }, + { + "label": _("Warehouse"), + "fieldtype": "Link", + "fieldname": "warehouse", + "options": "Warehouse", + "width": 120, + }, + { + "label": _("Serial No"), + "fieldtype": "Link", + "fieldname": "serial_no", + "options": "Serial No", + "width": 100, + }, + {"label": _("Qty"), "fieldtype": "Float", "fieldname": "qty", "width": 80}, + { + "label": _("Valuation Rate (In / Out)"), + "fieldtype": "Currency", + "fieldname": "valuation_rate", + "width": 110, + }, + ] diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py index 28e6cb2d27..23e3c8a97f 100644 --- a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py @@ -13,14 +13,18 @@ from erpnext.stock.utils import get_stock_value_on def execute(filters=None): if not erpnext.is_perpetual_inventory_enabled(filters.company): - frappe.throw(_("Perpetual inventory required for the company {0} to view this report.") - .format(filters.company)) + frappe.throw( + _("Perpetual inventory required for the company {0} to view this report.").format( + filters.company + ) + ) data = get_data(filters) columns = get_columns(filters) return columns, data + def get_unsync_date(filters): date = filters.from_date if not date: @@ -31,14 +35,16 @@ def get_unsync_date(filters): return while getdate(date) < getdate(today()): - account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(posting_date=date, - company=filters.company, account = filters.account) + account_bal, stock_bal, warehouse_list = get_stock_and_account_balance( + posting_date=date, company=filters.company, account=filters.account + ) if abs(account_bal - stock_bal) > 0.1: return date date = add_days(date, 1) + def get_data(report_filters): from_date = get_unsync_date(report_filters) @@ -48,7 +54,8 @@ def get_data(report_filters): result = [] voucher_wise_dict = {} - data = frappe.db.sql(''' + data = frappe.db.sql( + """ SELECT name, posting_date, posting_time, voucher_type, voucher_no, stock_value_difference, stock_value, warehouse, item_code @@ -59,14 +66,19 @@ def get_data(report_filters): = %s and company = %s and is_cancelled = 0 ORDER BY timestamp(posting_date, posting_time) asc, creation asc - ''', (from_date, report_filters.company), as_dict=1) + """, + (from_date, report_filters.company), + as_dict=1, + ) for d in data: voucher_wise_dict.setdefault((d.item_code, d.warehouse), []).append(d) closing_date = add_days(from_date, -1) for key, stock_data in voucher_wise_dict.items(): - prev_stock_value = get_stock_value_on(posting_date = closing_date, item_code=key[0], warehouse =key[1]) + prev_stock_value = get_stock_value_on( + posting_date=closing_date, item_code=key[0], warehouse=key[1] + ) for data in stock_data: expected_stock_value = prev_stock_value + data.stock_value_difference if abs(data.stock_value - expected_stock_value) > 0.1: @@ -76,6 +88,7 @@ def get_data(report_filters): return result + def get_columns(filters): return [ { @@ -83,60 +96,43 @@ def get_columns(filters): "fieldname": "name", "fieldtype": "Link", "options": "Stock Ledger Entry", - "width": "80" - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date" - }, - { - "label": _("Posting Time"), - "fieldname": "posting_time", - "fieldtype": "Time" - }, - { - "label": _("Voucher Type"), - "fieldname": "voucher_type", - "width": "110" + "width": "80", }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date"}, + {"label": _("Posting Time"), "fieldname": "posting_time", "fieldtype": "Time"}, + {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": "110"}, { "label": _("Voucher No"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", - "width": "110" + "width": "110", }, { "label": _("Item Code"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": "110" + "width": "110", }, { "label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", - "width": "110" + "width": "110", }, { "label": _("Expected Stock Value"), "fieldname": "expected_stock_value", "fieldtype": "Currency", - "width": "150" - }, - { - "label": _("Stock Value"), - "fieldname": "stock_value", - "fieldtype": "Currency", - "width": "120" + "width": "150", }, + {"label": _("Stock Value"), "fieldname": "stock_value", "fieldtype": "Currency", "width": "120"}, { "label": _("Difference Value"), "fieldname": "difference_value", "fieldtype": "Currency", - "width": "150" - } + "width": "150", + }, ] diff --git a/erpnext/stock/report/item_price_stock/item_price_stock.py b/erpnext/stock/report/item_price_stock/item_price_stock.py index 65af9f51cd..15218e63a8 100644 --- a/erpnext/stock/report/item_price_stock/item_price_stock.py +++ b/erpnext/stock/report/item_price_stock/item_price_stock.py @@ -7,10 +7,11 @@ from frappe import _ def execute(filters=None): columns, data = [], [] - columns=get_columns() - data=get_data(filters,columns) + columns = get_columns() + data = get_data(filters, columns) return columns, data + def get_columns(): return [ { @@ -18,77 +19,64 @@ def get_columns(): "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 120 - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 120 - }, - { - "label": _("Brand"), - "fieldname": "brand", - "fieldtype": "Data", - "width": 100 + "width": 120, }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 120}, + {"label": _("Brand"), "fieldname": "brand", "fieldtype": "Data", "width": 100}, { "label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", - "width": 120 + "width": 120, }, { "label": _("Stock Available"), "fieldname": "stock_available", "fieldtype": "Float", - "width": 120 + "width": 120, }, { "label": _("Buying Price List"), "fieldname": "buying_price_list", "fieldtype": "Link", "options": "Price List", - "width": 120 - }, - { - "label": _("Buying Rate"), - "fieldname": "buying_rate", - "fieldtype": "Currency", - "width": 120 + "width": 120, }, + {"label": _("Buying Rate"), "fieldname": "buying_rate", "fieldtype": "Currency", "width": 120}, { "label": _("Selling Price List"), "fieldname": "selling_price_list", "fieldtype": "Link", "options": "Price List", - "width": 120 + "width": 120, }, - { - "label": _("Selling Rate"), - "fieldname": "selling_rate", - "fieldtype": "Currency", - "width": 120 - } + {"label": _("Selling Rate"), "fieldname": "selling_rate", "fieldtype": "Currency", "width": 120}, ] + def get_data(filters, columns): item_price_qty_data = [] item_price_qty_data = get_item_price_qty_data(filters) return item_price_qty_data + def get_item_price_qty_data(filters): conditions = "" if filters.get("item_code"): conditions += "where a.item_code=%(item_code)s" - item_results = frappe.db.sql("""select a.item_code, a.item_name, a.name as price_list_name, + item_results = frappe.db.sql( + """select a.item_code, a.item_name, a.name as price_list_name, a.brand as brand, b.warehouse as warehouse, b.actual_qty as actual_qty from `tabItem Price` a left join `tabBin` b ON a.item_code = b.item_code - {conditions}""" - .format(conditions=conditions), filters, as_dict=1) + {conditions}""".format( + conditions=conditions + ), + filters, + as_dict=1, + ) price_list_names = list(set(item.price_list_name for item in item_results)) @@ -99,15 +87,15 @@ def get_item_price_qty_data(filters): if item_results: for item_dict in item_results: data = { - 'item_code': item_dict.item_code, - 'item_name': item_dict.item_name, - 'brand': item_dict.brand, - 'warehouse': item_dict.warehouse, - 'stock_available': item_dict.actual_qty or 0, - 'buying_price_list': "", - 'buying_rate': 0.0, - 'selling_price_list': "", - 'selling_rate': 0.0 + "item_code": item_dict.item_code, + "item_name": item_dict.item_name, + "brand": item_dict.brand, + "warehouse": item_dict.warehouse, + "stock_available": item_dict.actual_qty or 0, + "buying_price_list": "", + "buying_rate": 0.0, + "selling_price_list": "", + "selling_rate": 0.0, } price_list = item_dict["price_list_name"] @@ -122,6 +110,7 @@ def get_item_price_qty_data(filters): return result + def get_price_map(price_list_names, buying=0, selling=0): price_map = {} @@ -137,14 +126,12 @@ def get_price_map(price_list_names, buying=0, selling=0): else: filters["selling"] = 1 - pricing_details = frappe.get_all("Item Price", - fields = ["name", "price_list", "price_list_rate"], filters=filters) + pricing_details = frappe.get_all( + "Item Price", fields=["name", "price_list", "price_list_rate"], filters=filters + ) for d in pricing_details: name = d["name"] - price_map[name] = { - price_list_key :d["price_list"], - rate_key :d["price_list_rate"] - } + price_map[name] = {price_list_key: d["price_list"], rate_key: d["price_list_rate"]} return price_map diff --git a/erpnext/stock/report/item_prices/item_prices.py b/erpnext/stock/report/item_prices/item_prices.py index 0d0e8d2292..87f1a42e2b 100644 --- a/erpnext/stock/report/item_prices/item_prices.py +++ b/erpnext/stock/report/item_prices/item_prices.py @@ -8,7 +8,8 @@ from frappe.utils import flt def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns(filters) conditions = get_condition(filters) @@ -19,64 +20,95 @@ def execute(filters=None): val_rate_map = get_valuation_rate() from erpnext.accounts.utils import get_currency_precision + precision = get_currency_precision() or 2 data = [] for item in sorted(item_map): - data.append([item, item_map[item]["item_name"],item_map[item]["item_group"], - item_map[item]["brand"], item_map[item]["description"], item_map[item]["stock_uom"], - flt(last_purchase_rate.get(item, 0), precision), - flt(val_rate_map.get(item, 0), precision), - pl.get(item, {}).get("Selling"), - pl.get(item, {}).get("Buying"), - flt(bom_rate.get(item, 0), precision) - ]) + data.append( + [ + item, + item_map[item]["item_name"], + item_map[item]["item_group"], + item_map[item]["brand"], + item_map[item]["description"], + item_map[item]["stock_uom"], + flt(last_purchase_rate.get(item, 0), precision), + flt(val_rate_map.get(item, 0), precision), + pl.get(item, {}).get("Selling"), + pl.get(item, {}).get("Buying"), + flt(bom_rate.get(item, 0), precision), + ] + ) return columns, data + def get_columns(filters): """return columns based on filters""" - columns = [_("Item") + ":Link/Item:100", _("Item Name") + "::150",_("Item Group") + ":Link/Item Group:125", - _("Brand") + "::100", _("Description") + "::150", _("UOM") + ":Link/UOM:80", - _("Last Purchase Rate") + ":Currency:90", _("Valuation Rate") + ":Currency:80", _("Sales Price List") + "::180", - _("Purchase Price List") + "::180", _("BOM Rate") + ":Currency:90"] + columns = [ + _("Item") + ":Link/Item:100", + _("Item Name") + "::150", + _("Item Group") + ":Link/Item Group:125", + _("Brand") + "::100", + _("Description") + "::150", + _("UOM") + ":Link/UOM:80", + _("Last Purchase Rate") + ":Currency:90", + _("Valuation Rate") + ":Currency:80", + _("Sales Price List") + "::180", + _("Purchase Price List") + "::180", + _("BOM Rate") + ":Currency:90", + ] return columns + def get_item_details(conditions): """returns all items details""" item_map = {} - for i in frappe.db.sql("""select name, item_group, item_name, description, + for i in frappe.db.sql( + """select name, item_group, item_name, description, brand, stock_uom from tabItem %s - order by item_code, item_group""" % (conditions), as_dict=1): - item_map.setdefault(i.name, i) + order by item_code, item_group""" + % (conditions), + as_dict=1, + ): + item_map.setdefault(i.name, i) return item_map + def get_price_list(): """Get selling & buying price list of every item""" rate = {} - price_list = frappe.db.sql("""select ip.item_code, ip.buying, ip.selling, + price_list = frappe.db.sql( + """select ip.item_code, ip.buying, ip.selling, concat(ifnull(cu.symbol,ip.currency), " ", round(ip.price_list_rate,2), " - ", ip.price_list) as price from `tabItem Price` ip, `tabPrice List` pl, `tabCurrency` cu - where ip.price_list=pl.name and pl.currency=cu.name and pl.enabled=1""", as_dict=1) + where ip.price_list=pl.name and pl.currency=cu.name and pl.enabled=1""", + as_dict=1, + ) for j in price_list: if j.price: - rate.setdefault(j.item_code, {}).setdefault("Buying" if j.buying else "Selling", []).append(j.price) + rate.setdefault(j.item_code, {}).setdefault("Buying" if j.buying else "Selling", []).append( + j.price + ) item_rate_map = {} for item in rate: for buying_or_selling in rate[item]: - item_rate_map.setdefault(item, {}).setdefault(buying_or_selling, - ", ".join(rate[item].get(buying_or_selling, []))) + item_rate_map.setdefault(item, {}).setdefault( + buying_or_selling, ", ".join(rate[item].get(buying_or_selling, [])) + ) return item_rate_map + def get_last_purchase_rate(): item_last_purchase_rate_map = {} @@ -108,29 +140,38 @@ def get_last_purchase_rate(): return item_last_purchase_rate_map + def get_item_bom_rate(): """Get BOM rate of an item from BOM""" item_bom_map = {} - for b in frappe.db.sql("""select item, (total_cost/quantity) as bom_rate - from `tabBOM` where is_active=1 and is_default=1""", as_dict=1): - item_bom_map.setdefault(b.item, flt(b.bom_rate)) + for b in frappe.db.sql( + """select item, (total_cost/quantity) as bom_rate + from `tabBOM` where is_active=1 and is_default=1""", + as_dict=1, + ): + item_bom_map.setdefault(b.item, flt(b.bom_rate)) return item_bom_map + def get_valuation_rate(): """Get an average valuation rate of an item from all warehouses""" item_val_rate_map = {} - for d in frappe.db.sql("""select item_code, + for d in frappe.db.sql( + """select item_code, sum(actual_qty*valuation_rate)/sum(actual_qty) as val_rate - from tabBin where actual_qty > 0 group by item_code""", as_dict=1): - item_val_rate_map.setdefault(d.item_code, d.val_rate) + from tabBin where actual_qty > 0 group by item_code""", + as_dict=1, + ): + item_val_rate_map.setdefault(d.item_code, d.val_rate) return item_val_rate_map + def get_condition(filters): """Get Filter Items""" diff --git a/erpnext/stock/report/item_shortage_report/item_shortage_report.py b/erpnext/stock/report/item_shortage_report/item_shortage_report.py index 30c761421f..03a3a6a0b8 100644 --- a/erpnext/stock/report/item_shortage_report/item_shortage_report.py +++ b/erpnext/stock/report/item_shortage_report/item_shortage_report.py @@ -18,6 +18,7 @@ def execute(filters=None): return columns, data, None, chart_data + def get_conditions(filters): conditions = "" @@ -28,8 +29,10 @@ def get_conditions(filters): return conditions + def get_data(conditions, filters): - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT bin.warehouse, bin.item_code, @@ -51,10 +54,16 @@ def get_data(conditions, filters): AND warehouse.name = bin.warehouse AND bin.item_code=item.name {0} - ORDER BY bin.projected_qty;""".format(conditions), filters, as_dict=1) + ORDER BY bin.projected_qty;""".format( + conditions + ), + filters, + as_dict=1, + ) return data + def get_chart_data(data): labels, datapoints = [], [] @@ -67,18 +76,11 @@ def get_chart_data(data): datapoints = datapoints[:10] return { - "data": { - "labels": labels, - "datasets":[ - { - "name": _("Projected Qty"), - "values": datapoints - } - ] - }, - "type": "bar" + "data": {"labels": labels, "datasets": [{"name": _("Projected Qty"), "values": datapoints}]}, + "type": "bar", } + def get_columns(): columns = [ { @@ -86,76 +88,66 @@ def get_columns(): "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", - "width": 150 + "width": 150, }, { "label": _("Item"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 150 + "width": 150, }, { "label": _("Actual Quantity"), "fieldname": "actual_qty", "fieldtype": "Float", "width": 120, - "convertible": "qty" + "convertible": "qty", }, { "label": _("Ordered Quantity"), "fieldname": "ordered_qty", "fieldtype": "Float", "width": 120, - "convertible": "qty" + "convertible": "qty", }, { "label": _("Planned Quantity"), "fieldname": "planned_qty", "fieldtype": "Float", "width": 120, - "convertible": "qty" + "convertible": "qty", }, { "label": _("Reserved Quantity"), "fieldname": "reserved_qty", "fieldtype": "Float", "width": 120, - "convertible": "qty" + "convertible": "qty", }, { "label": _("Reserved Quantity for Production"), "fieldname": "reserved_qty_for_production", "fieldtype": "Float", "width": 120, - "convertible": "qty" + "convertible": "qty", }, { "label": _("Projected Quantity"), "fieldname": "projected_qty", "fieldtype": "Float", "width": 120, - "convertible": "qty" + "convertible": "qty", }, { "label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", - "width": 120 + "width": 120, }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 100 - }, - { - "label": _("Description"), - "fieldname": "description", - "fieldtype": "Data", - "width": 120 - } + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 100}, + {"label": _("Description"), "fieldname": "description", "fieldtype": "Data", "width": 120}, ] return columns diff --git a/erpnext/stock/report/item_variant_details/item_variant_details.py b/erpnext/stock/report/item_variant_details/item_variant_details.py index 10cef70215..d1bf2203f1 100644 --- a/erpnext/stock/report/item_variant_details/item_variant_details.py +++ b/erpnext/stock/report/item_variant_details/item_variant_details.py @@ -11,25 +11,21 @@ def execute(filters=None): data = get_data(filters.item) return columns, data + def get_data(item): if not item: return [] item_dicts = [] variant_results = frappe.db.get_all( - "Item", - fields=["name"], - filters={ - "variant_of": ["=", item], - "disabled": 0 - } + "Item", fields=["name"], filters={"variant_of": ["=", item], "disabled": 0} ) if not variant_results: frappe.msgprint(_("There aren't any item variants for the selected item")) return [] else: - variant_list = [variant['name'] for variant in variant_results] + variant_list = [variant["name"] for variant in variant_results] order_count_map = get_open_sales_orders_count(variant_list) stock_details_map = get_stock_details_map(variant_list) @@ -40,15 +36,13 @@ def get_data(item): attributes = frappe.db.get_all( "Item Variant Attribute", fields=["attribute"], - filters={ - "parent": ["in", variant_list] - }, - group_by="attribute" + filters={"parent": ["in", variant_list]}, + group_by="attribute", ) attribute_list = [row.get("attribute") for row in attributes] # Prepare dicts - variant_dicts = [{"variant_name": d['name']} for d in variant_results] + variant_dicts = [{"variant_name": d["name"]} for d in variant_results] for item_dict in variant_dicts: name = item_dict.get("variant_name") @@ -72,73 +66,66 @@ def get_data(item): return item_dicts + def get_columns(item): - columns = [{ - "fieldname": "variant_name", - "label": "Variant", - "fieldtype": "Link", - "options": "Item", - "width": 200 - }] + columns = [ + { + "fieldname": "variant_name", + "label": "Variant", + "fieldtype": "Link", + "options": "Item", + "width": 200, + } + ] item_doc = frappe.get_doc("Item", item) for entry in item_doc.attributes: - columns.append({ - "fieldname": frappe.scrub(entry.attribute), - "label": entry.attribute, - "fieldtype": "Data", - "width": 100 - }) + columns.append( + { + "fieldname": frappe.scrub(entry.attribute), + "label": entry.attribute, + "fieldtype": "Data", + "width": 100, + } + ) additional_columns = [ { "fieldname": "avg_buying_price_list_rate", "label": _("Avg. Buying Price List Rate"), "fieldtype": "Currency", - "width": 150 + "width": 150, }, { "fieldname": "avg_selling_price_list_rate", "label": _("Avg. Selling Price List Rate"), "fieldtype": "Currency", - "width": 150 - }, - { - "fieldname": "current_stock", - "label": _("Current Stock"), - "fieldtype": "Float", - "width": 120 - }, - { - "fieldname": "in_production", - "label": _("In Production"), - "fieldtype": "Float", - "width": 150 + "width": 150, }, + {"fieldname": "current_stock", "label": _("Current Stock"), "fieldtype": "Float", "width": 120}, + {"fieldname": "in_production", "label": _("In Production"), "fieldtype": "Float", "width": 150}, { "fieldname": "open_orders", "label": _("Open Sales Orders"), "fieldtype": "Float", - "width": 150 - } + "width": 150, + }, ] columns.extend(additional_columns) return columns + def get_open_sales_orders_count(variants_list): open_sales_orders = frappe.db.get_list( "Sales Order", - fields=[ - "name", - "`tabSales Order Item`.item_code" - ], + fields=["name", "`tabSales Order Item`.item_code"], filters=[ ["Sales Order", "docstatus", "=", 1], - ["Sales Order Item", "item_code", "in", variants_list] + ["Sales Order Item", "item_code", "in", variants_list], ], - distinct=1 + distinct=1, ) order_count_map = {} @@ -151,6 +138,7 @@ def get_open_sales_orders_count(variants_list): return order_count_map + def get_stock_details_map(variant_list): stock_details = frappe.db.get_all( "Bin", @@ -160,10 +148,8 @@ def get_stock_details_map(variant_list): "sum(projected_qty) as projected_qty", "item_code", ], - filters={ - "item_code": ["in", variant_list] - }, - group_by="item_code" + filters={"item_code": ["in", variant_list]}, + group_by="item_code", ) stock_details_map = {} @@ -171,11 +157,12 @@ def get_stock_details_map(variant_list): name = row.get("item_code") stock_details_map[name] = { "Inventory": row.get("actual_qty"), - "In Production": row.get("planned_qty") + "In Production": row.get("planned_qty"), } return stock_details_map + def get_buying_price_map(variant_list): buying = frappe.db.get_all( "Item Price", @@ -183,11 +170,8 @@ def get_buying_price_map(variant_list): "avg(price_list_rate) as avg_rate", "item_code", ], - filters={ - "item_code": ["in", variant_list], - "buying": 1 - }, - group_by="item_code" + filters={"item_code": ["in", variant_list], "buying": 1}, + group_by="item_code", ) buying_price_map = {} @@ -196,6 +180,7 @@ def get_buying_price_map(variant_list): return buying_price_map + def get_selling_price_map(variant_list): selling = frappe.db.get_all( "Item Price", @@ -203,11 +188,8 @@ def get_selling_price_map(variant_list): "avg(price_list_rate) as avg_rate", "item_code", ], - filters={ - "item_code": ["in", variant_list], - "selling": 1 - }, - group_by="item_code" + filters={"item_code": ["in", variant_list], "selling": 1}, + group_by="item_code", ) selling_price_map = {} @@ -216,17 +198,12 @@ def get_selling_price_map(variant_list): return selling_price_map + def get_attribute_values_map(variant_list): attribute_list = frappe.db.get_all( "Item Variant Attribute", - fields=[ - "attribute", - "attribute_value", - "parent" - ], - filters={ - "parent": ["in", variant_list] - } + fields=["attribute", "attribute_value", "parent"], + filters={"parent": ["in", variant_list]}, ) attr_val_map = {} diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py index cfa1e474c7..f308e9e41f 100644 --- a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py +++ b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py @@ -7,13 +7,14 @@ from frappe.utils import flt, getdate def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} float_precision = frappe.db.get_default("float_precision") condition = get_condition(filters) avg_daily_outgoing = 0 - diff = ((getdate(filters.get("to_date")) - getdate(filters.get("from_date"))).days)+1 + diff = ((getdate(filters.get("to_date")) - getdate(filters.get("from_date"))).days) + 1 if diff <= 0: frappe.throw(_("'From Date' must be after 'To Date'")) @@ -24,42 +25,72 @@ def execute(filters=None): data = [] for item in items: - total_outgoing = flt(consumed_item_map.get(item.name, 0)) + flt(delivered_item_map.get(item.name,0)) + total_outgoing = flt(consumed_item_map.get(item.name, 0)) + flt( + delivered_item_map.get(item.name, 0) + ) avg_daily_outgoing = flt(total_outgoing / diff, float_precision) reorder_level = (avg_daily_outgoing * flt(item.lead_time_days)) + flt(item.safety_stock) - data.append([item.name, item.item_name, item.item_group, item.brand, item.description, - item.safety_stock, item.lead_time_days, consumed_item_map.get(item.name, 0), - delivered_item_map.get(item.name,0), total_outgoing, avg_daily_outgoing, reorder_level]) + data.append( + [ + item.name, + item.item_name, + item.item_group, + item.brand, + item.description, + item.safety_stock, + item.lead_time_days, + consumed_item_map.get(item.name, 0), + delivered_item_map.get(item.name, 0), + total_outgoing, + avg_daily_outgoing, + reorder_level, + ] + ) + + return columns, data - return columns , data def get_columns(): - return[ - _("Item") + ":Link/Item:120", _("Item Name") + ":Data:120", _("Item Group") + ":Link/Item Group:100", - _("Brand") + ":Link/Brand:100", _("Description") + "::160", - _("Safety Stock") + ":Float:160", _("Lead Time Days") + ":Float:120", _("Consumed") + ":Float:120", - _("Delivered") + ":Float:120", _("Total Outgoing") + ":Float:120", _("Avg Daily Outgoing") + ":Float:160", - _("Reorder Level") + ":Float:120" + return [ + _("Item") + ":Link/Item:120", + _("Item Name") + ":Data:120", + _("Item Group") + ":Link/Item Group:100", + _("Brand") + ":Link/Brand:100", + _("Description") + "::160", + _("Safety Stock") + ":Float:160", + _("Lead Time Days") + ":Float:120", + _("Consumed") + ":Float:120", + _("Delivered") + ":Float:120", + _("Total Outgoing") + ":Float:120", + _("Avg Daily Outgoing") + ":Float:160", + _("Reorder Level") + ":Float:120", ] + def get_item_info(filters): from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition + conditions = [get_item_group_condition(filters.get("item_group"))] if filters.get("brand"): conditions.append("item.brand=%(brand)s") conditions.append("is_stock_item = 1") - return frappe.db.sql("""select name, item_name, description, brand, item_group, - safety_stock, lead_time_days from `tabItem` item where {}""" - .format(" and ".join(conditions)), filters, as_dict=1) + return frappe.db.sql( + """select name, item_name, description, brand, item_group, + safety_stock, lead_time_days from `tabItem` item where {}""".format( + " and ".join(conditions) + ), + filters, + as_dict=1, + ) def get_consumed_items(condition): purpose_to_exclude = [ "Material Transfer for Manufacture", "Material Transfer", - "Send to Subcontractor" + "Send to Subcontractor", ] condition += """ @@ -67,10 +98,13 @@ def get_consumed_items(condition): purpose is NULL or purpose not in ({}) ) - """.format(', '.join(f"'{p}'" for p in purpose_to_exclude)) + """.format( + ", ".join(f"'{p}'" for p in purpose_to_exclude) + ) condition = condition.replace("posting_date", "sle.posting_date") - consumed_items = frappe.db.sql(""" + consumed_items = frappe.db.sql( + """ select item_code, abs(sum(actual_qty)) as consumed_qty from `tabStock Ledger Entry` as sle left join `tabStock Entry` as se on sle.voucher_no = se.name @@ -79,22 +113,34 @@ def get_consumed_items(condition): and is_cancelled = 0 and voucher_type not in ('Delivery Note', 'Sales Invoice') %s - group by item_code""" % condition, as_dict=1) + group by item_code""" + % condition, + as_dict=1, + ) - consumed_items_map = {item.item_code : item.consumed_qty for item in consumed_items} + consumed_items_map = {item.item_code: item.consumed_qty for item in consumed_items} return consumed_items_map + def get_delivered_items(condition): - dn_items = frappe.db.sql("""select dn_item.item_code, sum(dn_item.stock_qty) as dn_qty + dn_items = frappe.db.sql( + """select dn_item.item_code, sum(dn_item.stock_qty) as dn_qty from `tabDelivery Note` dn, `tabDelivery Note Item` dn_item where dn.name = dn_item.parent and dn.docstatus = 1 %s - group by dn_item.item_code""" % (condition), as_dict=1) + group by dn_item.item_code""" + % (condition), + as_dict=1, + ) - si_items = frappe.db.sql("""select si_item.item_code, sum(si_item.stock_qty) as si_qty + si_items = frappe.db.sql( + """select si_item.item_code, sum(si_item.stock_qty) as si_qty from `tabSales Invoice` si, `tabSales Invoice Item` si_item where si.name = si_item.parent and si.docstatus = 1 and si.update_stock = 1 %s - group by si_item.item_code""" % (condition), as_dict=1) + group by si_item.item_code""" + % (condition), + as_dict=1, + ) dn_item_map = {} for item in dn_items: @@ -105,10 +151,14 @@ def get_delivered_items(condition): return dn_item_map + def get_condition(filters): conditions = "" if filters.get("from_date") and filters.get("to_date"): - conditions += " and posting_date between '%s' and '%s'" % (filters["from_date"],filters["to_date"]) + conditions += " and posting_date between '%s' and '%s'" % ( + filters["from_date"], + filters["to_date"], + ) else: frappe.throw(_("From and To dates required")) return conditions diff --git a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py index d9adceddf1..854875a053 100644 --- a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py +++ b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py @@ -44,7 +44,9 @@ def execute(filters=None): child_rows = [] for child_item_detail in required_items: - child_item_balance = stock_balance.get(child_item_detail.item_code, frappe._dict()).get(warehouse, frappe._dict()) + child_item_balance = stock_balance.get(child_item_detail.item_code, frappe._dict()).get( + warehouse, frappe._dict() + ) child_row = { "indent": 1, "parent_item": parent_item, @@ -73,16 +75,46 @@ def execute(filters=None): def get_columns(): columns = [ - {"fieldname": "item_code", "label": _("Item"), "fieldtype": "Link", "options": "Item", "width": 300}, - {"fieldname": "warehouse", "label": _("Warehouse"), "fieldtype": "Link", "options": "Warehouse", "width": 100}, + { + "fieldname": "item_code", + "label": _("Item"), + "fieldtype": "Link", + "options": "Item", + "width": 300, + }, + { + "fieldname": "warehouse", + "label": _("Warehouse"), + "fieldtype": "Link", + "options": "Warehouse", + "width": 100, + }, {"fieldname": "uom", "label": _("UOM"), "fieldtype": "Link", "options": "UOM", "width": 70}, {"fieldname": "bundle_qty", "label": _("Bundle Qty"), "fieldtype": "Float", "width": 100}, {"fieldname": "actual_qty", "label": _("Actual Qty"), "fieldtype": "Float", "width": 100}, {"fieldname": "minimum_qty", "label": _("Minimum Qty"), "fieldtype": "Float", "width": 100}, - {"fieldname": "item_group", "label": _("Item Group"), "fieldtype": "Link", "options": "Item Group", "width": 100}, - {"fieldname": "brand", "label": _("Brand"), "fieldtype": "Link", "options": "Brand", "width": 100}, + { + "fieldname": "item_group", + "label": _("Item Group"), + "fieldtype": "Link", + "options": "Item Group", + "width": 100, + }, + { + "fieldname": "brand", + "label": _("Brand"), + "fieldtype": "Link", + "options": "Brand", + "width": 100, + }, {"fieldname": "description", "label": _("Description"), "width": 140}, - {"fieldname": "company", "label": _("Company"), "fieldtype": "Link", "options": "Company", "width": 100} + { + "fieldname": "company", + "label": _("Company"), + "fieldtype": "Link", + "options": "Company", + "width": 100, + }, ] return columns @@ -92,12 +124,18 @@ def get_items(filters): item_details = frappe._dict() conditions = get_parent_item_conditions(filters) - parent_item_details = frappe.db.sql(""" + parent_item_details = frappe.db.sql( + """ select item.name as item_code, item.item_name, pb.description, item.item_group, item.brand, item.stock_uom from `tabItem` item inner join `tabProduct Bundle` pb on pb.new_item_code = item.name where ifnull(item.disabled, 0) = 0 {0} - """.format(conditions), filters, as_dict=1) # nosec + """.format( + conditions + ), + filters, + as_dict=1, + ) # nosec parent_items = [] for d in parent_item_details: @@ -105,7 +143,8 @@ def get_items(filters): item_details[d.item_code] = d if parent_items: - child_item_details = frappe.db.sql(""" + child_item_details = frappe.db.sql( + """ select pb.new_item_code as parent_item, pbi.item_code, item.item_name, pbi.description, item.item_group, item.brand, item.stock_uom, pbi.uom, pbi.qty @@ -113,7 +152,12 @@ def get_items(filters): inner join `tabProduct Bundle` pb on pb.name = pbi.parent inner join `tabItem` item on item.name = pbi.item_code where pb.new_item_code in ({0}) - """.format(", ".join(["%s"] * len(parent_items))), parent_items, as_dict=1) # nosec + """.format( + ", ".join(["%s"] * len(parent_items)) + ), + parent_items, + as_dict=1, + ) # nosec else: child_item_details = [] @@ -140,12 +184,14 @@ def get_stock_ledger_entries(filters, items): if not items: return [] - item_conditions_sql = ' and sle.item_code in ({})' \ - .format(', '.join(frappe.db.escape(i) for i in items)) + item_conditions_sql = " and sle.item_code in ({})".format( + ", ".join(frappe.db.escape(i) for i in items) + ) conditions = get_sle_conditions(filters) - return frappe.db.sql(""" + return frappe.db.sql( + """ select sle.item_code, sle.warehouse, sle.qty_after_transaction, sle.company from @@ -153,7 +199,10 @@ def get_stock_ledger_entries(filters, items): left join `tabStock Ledger Entry` sle2 on sle.item_code = sle2.item_code and sle.warehouse = sle2.warehouse and (sle.posting_date, sle.posting_time, sle.name) < (sle2.posting_date, sle2.posting_time, sle2.name) - where sle2.name is null and sle.docstatus < 2 %s %s""" % (item_conditions_sql, conditions), as_dict=1) # nosec + where sle2.name is null and sle.docstatus < 2 %s %s""" + % (item_conditions_sql, conditions), + as_dict=1, + ) # nosec def get_parent_item_conditions(filters): @@ -179,9 +228,14 @@ def get_sle_conditions(filters): conditions += " and sle.posting_date <= %s" % frappe.db.escape(filters.get("date")) if filters.get("warehouse"): - warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1) + warehouse_details = frappe.db.get_value( + "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 + ) if warehouse_details: - conditions += " and exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and sle.warehouse = wh.name)" % (warehouse_details.lft, warehouse_details.rgt) # nosec + conditions += ( + " and exists (select name from `tabWarehouse` wh \ + where wh.lft >= %s and wh.rgt <= %s and sle.warehouse = wh.name)" + % (warehouse_details.lft, warehouse_details.rgt) + ) # nosec return conditions diff --git a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py index 97384427fa..fe2d55a391 100644 --- a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py +++ b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py @@ -8,7 +8,8 @@ from erpnext.controllers.trends import get_columns, get_data def execute(filters=None): - if not filters: filters ={} + if not filters: + filters = {} data = [] conditions = get_columns(filters, "Purchase Receipt") data = get_data(filters, conditions) @@ -17,6 +18,7 @@ def execute(filters=None): return conditions["columns"], data, None, chart_data + def get_chart_data(data, filters): if not data: return [] @@ -27,7 +29,7 @@ def get_chart_data(data, filters): # consider only consolidated row data = [row for row in data if row[0]] - data = sorted(data, key = lambda i: i[-1], reverse=True) + data = sorted(data, key=lambda i: i[-1], reverse=True) if len(data) > 10: # get top 10 if data too long @@ -39,14 +41,9 @@ def get_chart_data(data, filters): return { "data": { - "labels" : labels, - "datasets" : [ - { - "name": _("Total Received Amount"), - "values": datapoints - } - ] + "labels": labels, + "datasets": [{"name": _("Total Received Amount"), "values": datapoints}], }, - "type" : "bar", - "colors":["#5e64ff"] + "type": "bar", + "colors": ["#5e64ff"], } diff --git a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py index 80ec848e5b..e439f51dd6 100644 --- a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py +++ b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py @@ -12,42 +12,43 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_columns(filters): - columns = [{ - 'label': _('Posting Date'), - 'fieldtype': 'Date', - 'fieldname': 'posting_date' - }, { - 'label': _('Posting Time'), - 'fieldtype': 'Time', - 'fieldname': 'posting_time' - }, { - 'label': _('Voucher Type'), - 'fieldtype': 'Link', - 'fieldname': 'voucher_type', - 'options': 'DocType', - 'width': 220 - }, { - 'label': _('Voucher No'), - 'fieldtype': 'Dynamic Link', - 'fieldname': 'voucher_no', - 'options': 'voucher_type', - 'width': 220 - }, { - 'label': _('Company'), - 'fieldtype': 'Link', - 'fieldname': 'company', - 'options': 'Company', - 'width': 220 - }, { - 'label': _('Warehouse'), - 'fieldtype': 'Link', - 'fieldname': 'warehouse', - 'options': 'Warehouse', - 'width': 220 - }] + columns = [ + {"label": _("Posting Date"), "fieldtype": "Date", "fieldname": "posting_date"}, + {"label": _("Posting Time"), "fieldtype": "Time", "fieldname": "posting_time"}, + { + "label": _("Voucher Type"), + "fieldtype": "Link", + "fieldname": "voucher_type", + "options": "DocType", + "width": 220, + }, + { + "label": _("Voucher No"), + "fieldtype": "Dynamic Link", + "fieldname": "voucher_no", + "options": "voucher_type", + "width": 220, + }, + { + "label": _("Company"), + "fieldtype": "Link", + "fieldname": "company", + "options": "Company", + "width": 220, + }, + { + "label": _("Warehouse"), + "fieldtype": "Link", + "fieldname": "warehouse", + "options": "Warehouse", + "width": 220, + }, + ] return columns + def get_data(filters): - return get_stock_ledger_entries(filters, '<=', order="asc") or [] + return get_stock_ledger_entries(filters, "<=", order="asc") or [] diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 7ca40033ed..1956238331 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -25,6 +25,7 @@ def execute(filters: Filters = None) -> Tuple: return columns, data, None, chart_data + def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> List[Dict]: "Returns ordered, formatted data with ranges." _func = itemgetter(1) @@ -38,31 +39,38 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li fifo_queue = sorted(filter(_func, item_dict["fifo_queue"]), key=_func) - if not fifo_queue: continue + if not fifo_queue: + continue average_age = get_average_age(fifo_queue, to_date) earliest_age = date_diff(to_date, fifo_queue[0][1]) latest_age = date_diff(to_date, fifo_queue[-1][1]) range1, range2, range3, above_range3 = get_range_age(filters, fifo_queue, to_date, item_dict) - row = [details.name, details.item_name, details.description, - details.item_group, details.brand] + row = [details.name, details.item_name, details.description, details.item_group, details.brand] if filters.get("show_warehouse_wise_stock"): row.append(details.warehouse) - row.extend([ - flt(item_dict.get("total_qty"), precision), - average_age, - range1, range2, range3, above_range3, - earliest_age, latest_age, - details.stock_uom - ]) + row.extend( + [ + flt(item_dict.get("total_qty"), precision), + average_age, + range1, + range2, + range3, + above_range3, + earliest_age, + latest_age, + details.stock_uom, + ] + ) data.append(row) return data + def get_average_age(fifo_queue: List, to_date: str) -> float: batch_age = age_qty = total_qty = 0.0 for batch in fifo_queue: @@ -77,6 +85,7 @@ def get_average_age(fifo_queue: List, to_date: str) -> float: return flt(age_qty / total_qty, 2) if total_qty else 0.0 + def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: Dict) -> Tuple: precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True)) @@ -98,6 +107,7 @@ def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: D return range1, range2, range3, above_range3 + def get_columns(filters: Filters) -> List[Dict]: range_columns = [] setup_ageing_columns(filters, range_columns) @@ -107,82 +117,55 @@ def get_columns(filters: Filters) -> List[Dict]: "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 100 - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 100 - }, - { - "label": _("Description"), - "fieldname": "description", - "fieldtype": "Data", - "width": 200 + "width": 100, }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 100}, + {"label": _("Description"), "fieldname": "description", "fieldtype": "Data", "width": 200}, { "label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", - "width": 100 + "width": 100, }, { "label": _("Brand"), "fieldname": "brand", "fieldtype": "Link", "options": "Brand", - "width": 100 - }] + "width": 100, + }, + ] if filters.get("show_warehouse_wise_stock"): - columns +=[{ - "label": _("Warehouse"), - "fieldname": "warehouse", - "fieldtype": "Link", - "options": "Warehouse", - "width": 100 - }] + columns += [ + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 100, + } + ] - columns.extend([ - { - "label": _("Available Qty"), - "fieldname": "qty", - "fieldtype": "Float", - "width": 100 - }, - { - "label": _("Average Age"), - "fieldname": "average_age", - "fieldtype": "Float", - "width": 100 - }]) + columns.extend( + [ + {"label": _("Available Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 100}, + {"label": _("Average Age"), "fieldname": "average_age", "fieldtype": "Float", "width": 100}, + ] + ) columns.extend(range_columns) - columns.extend([ - { - "label": _("Earliest"), - "fieldname": "earliest", - "fieldtype": "Int", - "width": 80 - }, - { - "label": _("Latest"), - "fieldname": "latest", - "fieldtype": "Int", - "width": 80 - }, - { - "label": _("UOM"), - "fieldname": "uom", - "fieldtype": "Link", - "options": "UOM", - "width": 100 - } - ]) + columns.extend( + [ + {"label": _("Earliest"), "fieldname": "earliest", "fieldtype": "Int", "width": 80}, + {"label": _("Latest"), "fieldname": "latest", "fieldtype": "Int", "width": 80}, + {"label": _("UOM"), "fieldname": "uom", "fieldtype": "Link", "options": "UOM", "width": 100}, + ] + ) return columns + def get_chart_data(data: List, filters: Filters) -> Dict: if not data: return [] @@ -192,7 +175,7 @@ def get_chart_data(data: List, filters: Filters) -> Dict: if filters.get("show_warehouse_wise_stock"): return {} - data.sort(key = lambda row: row[6], reverse=True) + data.sort(key=lambda row: row[6], reverse=True) if len(data) > 10: data = data[:10] @@ -202,42 +185,33 @@ def get_chart_data(data: List, filters: Filters) -> Dict: datapoints.append(row[6]) return { - "data" : { - "labels": labels, - "datasets": [ - { - "name": _("Average Age"), - "values": datapoints - } - ] - }, - "type" : "bar" + "data": {"labels": labels, "datasets": [{"name": _("Average Age"), "values": datapoints}]}, + "type": "bar", } + def setup_ageing_columns(filters: Filters, range_columns: List): ranges = [ f"0 - {filters['range1']}", f"{cint(filters['range1']) + 1} - {cint(filters['range2'])}", f"{cint(filters['range2']) + 1} - {cint(filters['range3'])}", - f"{cint(filters['range3']) + 1} - {_('Above')}" + f"{cint(filters['range3']) + 1} - {_('Above')}", ] for i, label in enumerate(ranges): - fieldname = 'range' + str(i+1) - add_column(range_columns, label=f"Age ({label})",fieldname=fieldname) + fieldname = "range" + str(i + 1) + add_column(range_columns, label=f"Age ({label})", fieldname=fieldname) -def add_column(range_columns: List, label:str, fieldname: str, fieldtype: str = 'Float', width: int = 140): - range_columns.append(dict( - label=label, - fieldname=fieldname, - fieldtype=fieldtype, - width=width - )) + +def add_column( + range_columns: List, label: str, fieldname: str, fieldtype: str = "Float", width: int = 140 +): + range_columns.append(dict(label=label, fieldname=fieldname, fieldtype=fieldtype, width=width)) class FIFOSlots: "Returns FIFO computed slots of inwarded stock as per date." - def __init__(self, filters: Dict = None , sle: List = None): + def __init__(self, filters: Dict = None, sle: List = None): self.item_details = {} self.transferred_item_details = {} self.serial_no_batch_purchase_details = {} @@ -246,13 +220,13 @@ class FIFOSlots: def generate(self) -> Dict: """ - Returns dict of the foll.g structure: - Key = Item A / (Item A, Warehouse A) - Key: { - 'details' -> Dict: ** item details **, - 'fifo_queue' -> List: ** list of lists containing entries/slots for existing stock, - consumed/updated and maintained via FIFO. ** - } + Returns dict of the foll.g structure: + Key = Item A / (Item A, Warehouse A) + Key: { + 'details' -> Dict: ** item details **, + 'fifo_queue' -> List: ** list of lists containing entries/slots for existing stock, + consumed/updated and maintained via FIFO. ** + } """ if self.sle is None: self.sle = self.__get_stock_ledger_entries() @@ -292,7 +266,9 @@ class FIFOSlots: return key, fifo_queue, transferred_item_key - def __compute_incoming_stock(self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List): + def __compute_incoming_stock( + self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List + ): "Update FIFO Queue on inward stock." transfer_data = self.transferred_item_details.get(transfer_key) @@ -318,7 +294,9 @@ class FIFOSlots: self.serial_no_batch_purchase_details.setdefault(serial_no, row.posting_date) fifo_queue.append([serial_no, row.posting_date]) - def __compute_outgoing_stock(self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List): + def __compute_outgoing_stock( + self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List + ): "Update FIFO Queue on outward stock." if serial_nos: fifo_queue[:] = [serial_no for serial_no in fifo_queue if serial_no[0] not in serial_nos] @@ -384,15 +362,13 @@ class FIFOSlots: def __aggregate_details_by_item(self, wh_wise_data: Dict) -> Dict: "Aggregate Item-Wh wise data into single Item entry." item_aggregated_data = {} - for key,row in wh_wise_data.items(): + for key, row in wh_wise_data.items(): item = key[0] if not item_aggregated_data.get(item): - item_aggregated_data.setdefault(item, { - "details": frappe._dict(), - "fifo_queue": [], - "qty_after_transaction": 0.0, - "total_qty": 0.0 - }) + item_aggregated_data.setdefault( + item, + {"details": frappe._dict(), "fifo_queue": [], "qty_after_transaction": 0.0, "total_qty": 0.0}, + ) item_row = item_aggregated_data.get(item) item_row["details"].update(row["details"]) item_row["fifo_queue"].extend(row["fifo_queue"]) @@ -404,19 +380,29 @@ class FIFOSlots: def __get_stock_ledger_entries(self) -> List[Dict]: sle = frappe.qb.DocType("Stock Ledger Entry") - item = self.__get_item_query() # used as derived table in sle query + item = self.__get_item_query() # used as derived table in sle query sle_query = ( - frappe.qb.from_(sle).from_(item) + frappe.qb.from_(sle) + .from_(item) .select( - item.name, item.item_name, item.item_group, - item.brand, item.description, - item.stock_uom, item.has_serial_no, - sle.actual_qty, sle.posting_date, - sle.voucher_type, sle.voucher_no, - sle.serial_no, sle.batch_no, - sle.qty_after_transaction, sle.warehouse - ).where( + item.name, + item.item_name, + item.item_group, + item.brand, + item.description, + item.stock_uom, + item.has_serial_no, + sle.actual_qty, + sle.posting_date, + sle.voucher_type, + sle.voucher_no, + sle.serial_no, + sle.batch_no, + sle.qty_after_transaction, + sle.warehouse, + ) + .where( (sle.item_code == item.name) & (sle.company == self.filters.get("company")) & (sle.posting_date <= self.filters.get("to_date")) @@ -427,9 +413,7 @@ class FIFOSlots: if self.filters.get("warehouse"): sle_query = self.__get_warehouse_conditions(sle, sle_query) - sle_query = sle_query.orderby( - sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty - ) + sle_query = sle_query.orderby(sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty) return sle_query.run(as_dict=True) @@ -437,8 +421,7 @@ class FIFOSlots: item_table = frappe.qb.DocType("Item") item = frappe.qb.from_("Item").select( - "name", "item_name", "description", "stock_uom", - "brand", "item_group", "has_serial_no" + "name", "item_name", "description", "stock_uom", "brand", "item_group", "has_serial_no" ) if self.filters.get("item_code"): @@ -451,18 +434,13 @@ class FIFOSlots: def __get_warehouse_conditions(self, sle, sle_query) -> str: warehouse = frappe.qb.DocType("Warehouse") - lft, rgt = frappe.db.get_value( - "Warehouse", - self.filters.get("warehouse"), - ['lft', 'rgt'] - ) + lft, rgt = frappe.db.get_value("Warehouse", self.filters.get("warehouse"), ["lft", "rgt"]) warehouse_results = ( frappe.qb.from_(warehouse) - .select("name").where( - (warehouse.lft >= lft) - & (warehouse.rgt <= rgt) - ).run() + .select("name") + .where((warehouse.lft >= lft) & (warehouse.rgt <= rgt)) + .run() ) warehouse_results = [x[0] for x in warehouse_results] diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index ca963b7486..fb36360623 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -10,9 +10,7 @@ from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, format_rep class TestStockAgeing(FrappeTestCase): def setUp(self) -> None: self.filters = frappe._dict( - company="_Test Company", - to_date="2021-12-10", - range1=30, range2=60, range3=90 + company="_Test Company", to_date="2021-12-10", range1=30, range2=60, range3=90 ) def test_normal_inward_outward_queue(self): @@ -20,28 +18,37 @@ class TestStockAgeing(FrappeTestCase): sle = [ frappe._dict( name="Flask Item", - actual_qty=30, qty_after_transaction=30, + actual_qty=30, + qty_after_transaction=30, warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", + posting_date="2021-12-01", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=20, qty_after_transaction=50, + actual_qty=20, + qty_after_transaction=50, warehouse="WH 1", - posting_date="2021-12-02", voucher_type="Stock Entry", + posting_date="2021-12-02", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-10), qty_after_transaction=40, + actual_qty=(-10), + qty_after_transaction=40, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="003", - has_serial_no=False, serial_no=None - ) + has_serial_no=False, + serial_no=None, + ), ] slots = FIFOSlots(self.filters, sle).generate() @@ -58,36 +65,48 @@ class TestStockAgeing(FrappeTestCase): sle = [ frappe._dict( name="Flask Item", - actual_qty=(-30), qty_after_transaction=(-30), + actual_qty=(-30), + qty_after_transaction=(-30), warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", + posting_date="2021-12-01", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=20, qty_after_transaction=(-10), + actual_qty=20, + qty_after_transaction=(-10), warehouse="WH 1", - posting_date="2021-12-02", voucher_type="Stock Entry", + posting_date="2021-12-02", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=20, qty_after_transaction=10, + actual_qty=20, + qty_after_transaction=10, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="003", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=10, qty_after_transaction=20, + actual_qty=10, + qty_after_transaction=20, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="004", - has_serial_no=False, serial_no=None - ) + has_serial_no=False, + serial_no=None, + ), ] slots = FIFOSlots(self.filters, sle).generate() @@ -107,28 +126,37 @@ class TestStockAgeing(FrappeTestCase): sle = [ frappe._dict( name="Flask Item", - actual_qty=30, qty_after_transaction=30, + actual_qty=30, + qty_after_transaction=30, warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", + posting_date="2021-12-01", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=0, qty_after_transaction=50, + actual_qty=0, + qty_after_transaction=50, warehouse="WH 1", - posting_date="2021-12-02", voucher_type="Stock Reconciliation", + posting_date="2021-12-02", + voucher_type="Stock Reconciliation", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-10), qty_after_transaction=40, + actual_qty=(-10), + qty_after_transaction=40, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="003", - has_serial_no=False, serial_no=None - ) + has_serial_no=False, + serial_no=None, + ), ] slots = FIFOSlots(self.filters, sle).generate() @@ -150,28 +178,37 @@ class TestStockAgeing(FrappeTestCase): sle = [ frappe._dict( name="Flask Item", - actual_qty=0, qty_after_transaction=1000, + actual_qty=0, + qty_after_transaction=1000, warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Reconciliation", + posting_date="2021-12-01", + voucher_type="Stock Reconciliation", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=0, qty_after_transaction=400, + actual_qty=0, + qty_after_transaction=400, warehouse="WH 1", - posting_date="2021-12-02", voucher_type="Stock Reconciliation", + posting_date="2021-12-02", + voucher_type="Stock Reconciliation", voucher_no="003", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-10), qty_after_transaction=390, + actual_qty=(-10), + qty_after_transaction=390, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="003", - has_serial_no=False, serial_no=None - ) + has_serial_no=False, + serial_no=None, + ), ] slots = FIFOSlots(self.filters, sle).generate() @@ -196,32 +233,41 @@ class TestStockAgeing(FrappeTestCase): sle = [ frappe._dict( name="Flask Item", - actual_qty=0, qty_after_transaction=1000, + actual_qty=0, + qty_after_transaction=1000, warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Reconciliation", + posting_date="2021-12-01", + voucher_type="Stock Reconciliation", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=0, qty_after_transaction=400, + actual_qty=0, + qty_after_transaction=400, warehouse="WH 2", - posting_date="2021-12-02", voucher_type="Stock Reconciliation", + posting_date="2021-12-02", + voucher_type="Stock Reconciliation", voucher_no="003", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-10), qty_after_transaction=990, + actual_qty=(-10), + qty_after_transaction=990, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="004", - has_serial_no=False, serial_no=None - ) + has_serial_no=False, + serial_no=None, + ), ] item_wise_slots, item_wh_wise_slots = generate_item_and_item_wh_wise_slots( - filters=self.filters,sle=sle + filters=self.filters, sle=sle ) # test without 'show_warehouse_wise_stock' @@ -234,7 +280,9 @@ class TestStockAgeing(FrappeTestCase): self.assertEqual(queue[1][0], 400.0) # test with 'show_warehouse_wise_stock' checked - item_wh_balances = [item_wh_wise_slots.get(i).get("qty_after_transaction") for i in item_wh_wise_slots] + item_wh_balances = [ + item_wh_wise_slots.get(i).get("qty_after_transaction") for i in item_wh_wise_slots + ] self.assertEqual(sum(item_wh_balances), item_result["qty_after_transaction"]) def test_repack_entry_same_item_split_rows(self): @@ -251,37 +299,49 @@ class TestStockAgeing(FrappeTestCase): Case most likely for batch items. Test time bucket computation. """ sle = [ - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=500, qty_after_transaction=500, + actual_qty=500, + qty_after_transaction=500, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-50), qty_after_transaction=450, + actual_qty=(-50), + qty_after_transaction=450, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-50), qty_after_transaction=400, + actual_qty=(-50), + qty_after_transaction=400, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=100, qty_after_transaction=500, + actual_qty=100, + qty_after_transaction=500, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), ] slots = FIFOSlots(self.filters, sle).generate() @@ -308,29 +368,38 @@ class TestStockAgeing(FrappeTestCase): Case most likely for batch items. Test time bucket computation. """ sle = [ - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=500, qty_after_transaction=500, + actual_qty=500, + qty_after_transaction=500, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-100), qty_after_transaction=400, + actual_qty=(-100), + qty_after_transaction=400, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=50, qty_after_transaction=450, + actual_qty=50, + qty_after_transaction=450, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), ] slots = FIFOSlots(self.filters, sle).generate() @@ -355,37 +424,49 @@ class TestStockAgeing(FrappeTestCase): Item 1 | 50 | 002 (repack) """ sle = [ - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=20, qty_after_transaction=20, + actual_qty=20, + qty_after_transaction=20, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-50), qty_after_transaction=(-30), + actual_qty=(-50), + qty_after_transaction=(-30), warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-50), qty_after_transaction=(-80), + actual_qty=(-50), + qty_after_transaction=(-80), warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=50, qty_after_transaction=(-30), + actual_qty=50, + qty_after_transaction=(-30), warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), ] fifo_slots = FIFOSlots(self.filters, sle) @@ -397,7 +478,7 @@ class TestStockAgeing(FrappeTestCase): self.assertEqual(queue[0][0], -30.0) # check transfer bucket - transfer_bucket = fifo_slots.transferred_item_details[('002', 'Flask Item', 'WH 1')] + transfer_bucket = fifo_slots.transferred_item_details[("002", "Flask Item", "WH 1")] self.assertEqual(transfer_bucket[0][0], 50) def test_repack_entry_same_item_overproduce(self): @@ -413,29 +494,38 @@ class TestStockAgeing(FrappeTestCase): Case most likely for batch items. Test time bucket computation. """ sle = [ - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=500, qty_after_transaction=500, + actual_qty=500, + qty_after_transaction=500, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-50), qty_after_transaction=450, + actual_qty=(-50), + qty_after_transaction=450, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=100, qty_after_transaction=550, + actual_qty=100, + qty_after_transaction=550, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), ] slots = FIFOSlots(self.filters, sle).generate() @@ -461,37 +551,49 @@ class TestStockAgeing(FrappeTestCase): Item 1 | 50 | 002 (repack) """ sle = [ - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=20, qty_after_transaction=20, + actual_qty=20, + qty_after_transaction=20, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-50), qty_after_transaction=(-30), + actual_qty=(-50), + qty_after_transaction=(-30), warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=50, qty_after_transaction=20, + actual_qty=50, + qty_after_transaction=20, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=50, qty_after_transaction=70, + actual_qty=50, + qty_after_transaction=70, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), ] fifo_slots = FIFOSlots(self.filters, sle) @@ -504,7 +606,7 @@ class TestStockAgeing(FrappeTestCase): self.assertEqual(queue[1][0], 50.0) # check transfer bucket - transfer_bucket = fifo_slots.transferred_item_details[('002', 'Flask Item', 'WH 1')] + transfer_bucket = fifo_slots.transferred_item_details[("002", "Flask Item", "WH 1")] self.assertFalse(transfer_bucket) def test_negative_stock_same_voucher(self): @@ -519,29 +621,38 @@ class TestStockAgeing(FrappeTestCase): Item 1 | 80 | 001 """ sle = [ - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=(-50), qty_after_transaction=(-50), + actual_qty=(-50), + qty_after_transaction=(-50), warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", + posting_date="2021-12-01", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=(-50), qty_after_transaction=(-100), + actual_qty=(-50), + qty_after_transaction=(-100), warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", + posting_date="2021-12-01", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=30, qty_after_transaction=(-70), + actual_qty=30, + qty_after_transaction=(-70), warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", + posting_date="2021-12-01", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), ] fifo_slots = FIFOSlots(self.filters, sle) @@ -549,59 +660,71 @@ class TestStockAgeing(FrappeTestCase): item_result = slots["Flask Item"] # check transfer bucket - transfer_bucket = fifo_slots.transferred_item_details[('001', 'Flask Item', 'WH 1')] + transfer_bucket = fifo_slots.transferred_item_details[("001", "Flask Item", "WH 1")] self.assertEqual(transfer_bucket[0][0], 20) self.assertEqual(transfer_bucket[1][0], 50) self.assertEqual(item_result["fifo_queue"][0][0], -70.0) - sle.append(frappe._dict( - name="Flask Item", - actual_qty=80, qty_after_transaction=10, - warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", - voucher_no="001", - has_serial_no=False, serial_no=None - )) + sle.append( + frappe._dict( + name="Flask Item", + actual_qty=80, + qty_after_transaction=10, + warehouse="WH 1", + posting_date="2021-12-01", + voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, + serial_no=None, + ) + ) fifo_slots = FIFOSlots(self.filters, sle) slots = fifo_slots.generate() item_result = slots["Flask Item"] - transfer_bucket = fifo_slots.transferred_item_details[('001', 'Flask Item', 'WH 1')] + transfer_bucket = fifo_slots.transferred_item_details[("001", "Flask Item", "WH 1")] self.assertFalse(transfer_bucket) self.assertEqual(item_result["fifo_queue"][0][0], 10.0) def test_precision(self): "Test if final balance qty is rounded off correctly." sle = [ - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=0.3, qty_after_transaction=0.3, + actual_qty=0.3, + qty_after_transaction=0.3, warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", + posting_date="2021-12-01", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=0.6, qty_after_transaction=0.9, + actual_qty=0.6, + qty_after_transaction=0.9, warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", + posting_date="2021-12-01", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), ] slots = FIFOSlots(self.filters, sle).generate() report_data = format_report_data(self.filters, slots, self.filters["to_date"]) - row = report_data[0] # first row in report + row = report_data[0] # first row in report bal_qty = row[5] - range_qty_sum = sum([i for i in row[7:11]]) # get sum of range balance + range_qty_sum = sum([i for i in row[7:11]]) # get sum of range balance # check if value of Available Qty column matches with range bucket post format self.assertEqual(bal_qty, 0.9) self.assertEqual(bal_qty, range_qty_sum) + def generate_item_and_item_wh_wise_slots(filters, sle): "Return results with and without 'show_warehouse_wise_stock'" item_wise_slots = FIFOSlots(filters, sle).generate() diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.py b/erpnext/stock/report/stock_analytics/stock_analytics.py index ddc831037b..da0776b9a8 100644 --- a/erpnext/stock/report/stock_analytics/stock_analytics.py +++ b/erpnext/stock/report/stock_analytics/stock_analytics.py @@ -25,84 +25,64 @@ def execute(filters=None): return columns, data, None, chart + def get_columns(filters): columns = [ - { - "label": _("Item"), - "options":"Item", - "fieldname": "name", - "fieldtype": "Link", - "width": 140 - }, + {"label": _("Item"), "options": "Item", "fieldname": "name", "fieldtype": "Link", "width": 140}, { "label": _("Item Name"), - "options":"Item", + "options": "Item", "fieldname": "item_name", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Item Group"), - "options":"Item Group", + "options": "Item Group", "fieldname": "item_group", "fieldtype": "Link", - "width": 140 + "width": 140, }, - { - "label": _("Brand"), - "fieldname": "brand", - "fieldtype": "Data", - "width": 120 - }, - { - "label": _("UOM"), - "fieldname": "uom", - "fieldtype": "Data", - "width": 120 - }] + {"label": _("Brand"), "fieldname": "brand", "fieldtype": "Data", "width": 120}, + {"label": _("UOM"), "fieldname": "uom", "fieldtype": "Data", "width": 120}, + ] ranges = get_period_date_ranges(filters) for dummy, end_date in ranges: period = get_period(end_date, filters) - columns.append({ - "label": _(period), - "fieldname":scrub(period), - "fieldtype": "Float", - "width": 120 - }) + columns.append( + {"label": _(period), "fieldname": scrub(period), "fieldtype": "Float", "width": 120} + ) return columns + def get_period_date_ranges(filters): - from dateutil.relativedelta import relativedelta - from_date = round_down_to_nearest_frequency(filters.from_date, filters.range) - to_date = getdate(filters.to_date) + from dateutil.relativedelta import relativedelta - increment = { - "Monthly": 1, - "Quarterly": 3, - "Half-Yearly": 6, - "Yearly": 12 - }.get(filters.range,1) + from_date = round_down_to_nearest_frequency(filters.from_date, filters.range) + to_date = getdate(filters.to_date) - periodic_daterange = [] - for dummy in range(1, 53, increment): - if filters.range == "Weekly": - period_end_date = from_date + relativedelta(days=6) - else: - period_end_date = from_date + relativedelta(months=increment, days=-1) + increment = {"Monthly": 1, "Quarterly": 3, "Half-Yearly": 6, "Yearly": 12}.get(filters.range, 1) - if period_end_date > to_date: - period_end_date = to_date - periodic_daterange.append([from_date, period_end_date]) + periodic_daterange = [] + for dummy in range(1, 53, increment): + if filters.range == "Weekly": + period_end_date = from_date + relativedelta(days=6) + else: + period_end_date = from_date + relativedelta(months=increment, days=-1) - from_date = period_end_date + relativedelta(days=1) - if period_end_date == to_date: - break + if period_end_date > to_date: + period_end_date = to_date + periodic_daterange.append([from_date, period_end_date]) - return periodic_daterange + from_date = period_end_date + relativedelta(days=1) + if period_end_date == to_date: + break + + return periodic_daterange def round_down_to_nearest_frequency(date: str, frequency: str) -> datetime.datetime: @@ -132,12 +112,12 @@ def round_down_to_nearest_frequency(date: str, frequency: str) -> datetime.datet def get_period(posting_date, filters): months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] - if filters.range == 'Weekly': + if filters.range == "Weekly": period = "Week " + str(posting_date.isocalendar()[1]) + " " + str(posting_date.year) - elif filters.range == 'Monthly': + elif filters.range == "Monthly": period = str(months[posting_date.month - 1]) + " " + str(posting_date.year) - elif filters.range == 'Quarterly': - period = "Quarter " + str(((posting_date.month-1)//3)+1) +" " + str(posting_date.year) + elif filters.range == "Quarterly": + period = "Quarter " + str(((posting_date.month - 1) // 3) + 1) + " " + str(posting_date.year) else: year = get_fiscal_year(posting_date, company=filters.company) period = str(year[2]) @@ -147,26 +127,26 @@ def get_period(posting_date, filters): def get_periodic_data(entry, filters): """Structured as: - Item 1 - - Balance (updated and carried forward): - - Warehouse A : bal_qty/value - - Warehouse B : bal_qty/value - - Jun 2021 (sum of warehouse quantities used in report) - - Warehouse A : bal_qty/value - - Warehouse B : bal_qty/value - - Jul 2021 (sum of warehouse quantities used in report) - - Warehouse A : bal_qty/value - - Warehouse B : bal_qty/value - Item 2 - - Balance (updated and carried forward): - - Warehouse A : bal_qty/value - - Warehouse B : bal_qty/value - - Jun 2021 (sum of warehouse quantities used in report) - - Warehouse A : bal_qty/value - - Warehouse B : bal_qty/value - - Jul 2021 (sum of warehouse quantities used in report) - - Warehouse A : bal_qty/value - - Warehouse B : bal_qty/value + Item 1 + - Balance (updated and carried forward): + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + - Jun 2021 (sum of warehouse quantities used in report) + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + - Jul 2021 (sum of warehouse quantities used in report) + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + Item 2 + - Balance (updated and carried forward): + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + - Jun 2021 (sum of warehouse quantities used in report) + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + - Jul 2021 (sum of warehouse quantities used in report) + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value """ periodic_data = {} for d in entry: @@ -176,31 +156,36 @@ def get_periodic_data(entry, filters): # if period against item does not exist yet, instantiate it # insert existing balance dict against period, and add/subtract to it if periodic_data.get(d.item_code) and not periodic_data.get(d.item_code).get(period): - previous_balance = periodic_data[d.item_code]['balance'].copy() + previous_balance = periodic_data[d.item_code]["balance"].copy() periodic_data[d.item_code][period] = previous_balance if d.voucher_type == "Stock Reconciliation": - if periodic_data.get(d.item_code) and periodic_data.get(d.item_code).get('balance').get(d.warehouse): - bal_qty = periodic_data[d.item_code]['balance'][d.warehouse] + if periodic_data.get(d.item_code) and periodic_data.get(d.item_code).get("balance").get( + d.warehouse + ): + bal_qty = periodic_data[d.item_code]["balance"][d.warehouse] qty_diff = d.qty_after_transaction - bal_qty else: qty_diff = d.actual_qty - if filters["value_quantity"] == 'Quantity': + if filters["value_quantity"] == "Quantity": value = qty_diff else: value = d.stock_value_difference # period-warehouse wise balance - periodic_data.setdefault(d.item_code, {}).setdefault('balance', {}).setdefault(d.warehouse, 0.0) + periodic_data.setdefault(d.item_code, {}).setdefault("balance", {}).setdefault(d.warehouse, 0.0) periodic_data.setdefault(d.item_code, {}).setdefault(period, {}).setdefault(d.warehouse, 0.0) - periodic_data[d.item_code]['balance'][d.warehouse] += value - periodic_data[d.item_code][period][d.warehouse] = periodic_data[d.item_code]['balance'][d.warehouse] + periodic_data[d.item_code]["balance"][d.warehouse] += value + periodic_data[d.item_code][period][d.warehouse] = periodic_data[d.item_code]["balance"][ + d.warehouse + ] return periodic_data + def get_data(filters): data = [] items = get_items(filters) @@ -229,14 +214,10 @@ def get_data(filters): return data + def get_chart_data(columns): labels = [d.get("label") for d in columns[5:]] - chart = { - "data": { - 'labels': labels, - 'datasets':[] - } - } + chart = {"data": {"labels": labels, "datasets": []}} chart["type"] = "line" return chart 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 6fd3fe7da4..99f820ecac 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 @@ -12,21 +12,25 @@ from erpnext.stock.doctype.warehouse.warehouse import get_warehouses_based_on_ac def execute(filters=None): if not erpnext.is_perpetual_inventory_enabled(filters.company): - frappe.throw(_("Perpetual inventory required for the company {0} to view this report.") - .format(filters.company)) + frappe.throw( + _("Perpetual inventory required for the company {0} to view this report.").format( + filters.company + ) + ) data = get_data(filters) columns = get_columns(filters) return columns, data + def get_data(report_filters): data = [] filters = { "is_cancelled": 0, "company": report_filters.company, - "posting_date": ("<=", report_filters.as_on_date) + "posting_date": ("<=", report_filters.as_on_date), } currency_precision = get_currency_precision() or 2 @@ -43,18 +47,28 @@ def get_data(report_filters): return data + def get_stock_ledger_data(report_filters, filters): if report_filters.account: - warehouses = get_warehouses_based_on_account(report_filters.account, - report_filters.company) + warehouses = get_warehouses_based_on_account(report_filters.account, report_filters.company) filters["warehouse"] = ("in", warehouses) - return frappe.get_all("Stock Ledger Entry", filters=filters, - fields = ["name", "voucher_type", "voucher_no", - "sum(stock_value_difference) as stock_value", "posting_date", "posting_time"], - group_by = "voucher_type, voucher_no", - order_by = "posting_date ASC, posting_time ASC") + return frappe.get_all( + "Stock Ledger Entry", + filters=filters, + fields=[ + "name", + "voucher_type", + "voucher_no", + "sum(stock_value_difference) as stock_value", + "posting_date", + "posting_time", + ], + group_by="voucher_type, voucher_no", + order_by="posting_date ASC, posting_time ASC", + ) + def get_gl_data(report_filters, filters): if report_filters.account: @@ -62,17 +76,22 @@ def get_gl_data(report_filters, filters): else: stock_accounts = get_stock_accounts(report_filters.company) - filters.update({ - "account": ("in", stock_accounts) - }) + filters.update({"account": ("in", stock_accounts)}) if filters.get("warehouse"): del filters["warehouse"] - gl_entries = frappe.get_all("GL Entry", filters=filters, - fields = ["name", "voucher_type", "voucher_no", - "sum(debit_in_account_currency) - sum(credit_in_account_currency) as account_value"], - group_by = "voucher_type, voucher_no") + gl_entries = frappe.get_all( + "GL Entry", + filters=filters, + fields=[ + "name", + "voucher_type", + "voucher_no", + "sum(debit_in_account_currency) - sum(credit_in_account_currency) as account_value", + ], + group_by="voucher_type, voucher_no", + ) voucher_wise_gl_data = {} for d in gl_entries: @@ -81,6 +100,7 @@ def get_gl_data(report_filters, filters): return voucher_wise_gl_data + def get_columns(filters): return [ { @@ -88,46 +108,29 @@ def get_columns(filters): "fieldname": "name", "fieldtype": "Link", "options": "Stock Ledger Entry", - "width": "80" - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date" - }, - { - "label": _("Posting Time"), - "fieldname": "posting_time", - "fieldtype": "Time" - }, - { - "label": _("Voucher Type"), - "fieldname": "voucher_type", - "width": "110" + "width": "80", }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date"}, + {"label": _("Posting Time"), "fieldname": "posting_time", "fieldtype": "Time"}, + {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": "110"}, { "label": _("Voucher No"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", - "width": "110" - }, - { - "label": _("Stock Value"), - "fieldname": "stock_value", - "fieldtype": "Currency", - "width": "120" + "width": "110", }, + {"label": _("Stock Value"), "fieldname": "stock_value", "fieldtype": "Currency", "width": "120"}, { "label": _("Account Value"), "fieldname": "account_value", "fieldtype": "Currency", - "width": "120" + "width": "120", }, { "label": _("Difference Value"), "fieldname": "difference_value", "fieldtype": "Currency", - "width": "120" - } + "width": "120", + }, ] diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 24f47c1946..afbc6fe249 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -16,9 +16,10 @@ from erpnext.stock.utils import add_additional_uom_columns, is_reposting_item_va def execute(filters=None): is_reposting_item_valuation_in_progress() - if not filters: filters = {} + if not filters: + filters = {} - to_date = filters.get('to_date') + to_date = filters.get("to_date") if filters.get("company"): company_currency = erpnext.get_company_currency(filters.get("company")) @@ -30,8 +31,8 @@ def execute(filters=None): items = get_items(filters) sle = get_stock_ledger_entries(filters, items) - if filters.get('show_stock_ageing_data'): - filters['show_warehouse_wise_stock'] = True + if filters.get("show_stock_ageing_data"): + filters["show_warehouse_wise_stock"] = True item_wise_fifo_queue = FIFOSlots(filters, sle).generate() # if no stock ledger entry found return @@ -57,12 +58,12 @@ def execute(filters=None): item_reorder_qty = item_reorder_detail_map[item + warehouse]["warehouse_reorder_qty"] report_data = { - 'currency': company_currency, - 'item_code': item, - 'warehouse': warehouse, - 'company': company, - 'reorder_level': item_reorder_level, - 'reorder_qty': item_reorder_qty, + "currency": company_currency, + "item_code": item, + "warehouse": warehouse, + "company": company, + "reorder_level": item_reorder_level, + "reorder_qty": item_reorder_qty, } report_data.update(item_map[item]) report_data.update(qty_dict) @@ -70,21 +71,18 @@ def execute(filters=None): if include_uom: conversion_factors.setdefault(item, item_map[item].conversion_factor) - if filters.get('show_stock_ageing_data'): - fifo_queue = item_wise_fifo_queue[(item, warehouse)].get('fifo_queue') + if filters.get("show_stock_ageing_data"): + fifo_queue = item_wise_fifo_queue[(item, warehouse)].get("fifo_queue") - stock_ageing_data = { - 'average_age': 0, - 'earliest_age': 0, - 'latest_age': 0 - } + stock_ageing_data = {"average_age": 0, "earliest_age": 0, "latest_age": 0} if fifo_queue: fifo_queue = sorted(filter(_func, fifo_queue), key=_func) - if not fifo_queue: continue + if not fifo_queue: + continue - stock_ageing_data['average_age'] = get_average_age(fifo_queue, to_date) - stock_ageing_data['earliest_age'] = date_diff(to_date, fifo_queue[0][1]) - stock_ageing_data['latest_age'] = date_diff(to_date, fifo_queue[-1][1]) + stock_ageing_data["average_age"] = get_average_age(fifo_queue, to_date) + stock_ageing_data["earliest_age"] = date_diff(to_date, fifo_queue[0][1]) + stock_ageing_data["latest_age"] = date_diff(to_date, fifo_queue[-1][1]) report_data.update(stock_ageing_data) @@ -93,38 +91,130 @@ def execute(filters=None): add_additional_uom_columns(columns, data, include_uom, conversion_factors) return columns, data + def get_columns(filters): """return columns""" columns = [ - {"label": _("Item"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 100}, + { + "label": _("Item"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 100, + }, {"label": _("Item Name"), "fieldname": "item_name", "width": 150}, - {"label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", "width": 100}, - {"label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", "width": 100}, - {"label": _("Stock UOM"), "fieldname": "stock_uom", "fieldtype": "Link", "options": "UOM", "width": 90}, - {"label": _("Balance Qty"), "fieldname": "bal_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Balance Value"), "fieldname": "bal_val", "fieldtype": "Currency", "width": 100, "options": "currency"}, - {"label": _("Opening Qty"), "fieldname": "opening_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Opening Value"), "fieldname": "opening_val", "fieldtype": "Currency", "width": 110, "options": "currency"}, - {"label": _("In Qty"), "fieldname": "in_qty", "fieldtype": "Float", "width": 80, "convertible": "qty"}, + { + "label": _("Item Group"), + "fieldname": "item_group", + "fieldtype": "Link", + "options": "Item Group", + "width": 100, + }, + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 100, + }, + { + "label": _("Stock UOM"), + "fieldname": "stock_uom", + "fieldtype": "Link", + "options": "UOM", + "width": 90, + }, + { + "label": _("Balance Qty"), + "fieldname": "bal_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Balance Value"), + "fieldname": "bal_val", + "fieldtype": "Currency", + "width": 100, + "options": "currency", + }, + { + "label": _("Opening Qty"), + "fieldname": "opening_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Opening Value"), + "fieldname": "opening_val", + "fieldtype": "Currency", + "width": 110, + "options": "currency", + }, + { + "label": _("In Qty"), + "fieldname": "in_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, {"label": _("In Value"), "fieldname": "in_val", "fieldtype": "Float", "width": 80}, - {"label": _("Out Qty"), "fieldname": "out_qty", "fieldtype": "Float", "width": 80, "convertible": "qty"}, + { + "label": _("Out Qty"), + "fieldname": "out_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, {"label": _("Out Value"), "fieldname": "out_val", "fieldtype": "Float", "width": 80}, - {"label": _("Valuation Rate"), "fieldname": "val_rate", "fieldtype": "Currency", "width": 90, "convertible": "rate", "options": "currency"}, - {"label": _("Reorder Level"), "fieldname": "reorder_level", "fieldtype": "Float", "width": 80, "convertible": "qty"}, - {"label": _("Reorder Qty"), "fieldname": "reorder_qty", "fieldtype": "Float", "width": 80, "convertible": "qty"}, - {"label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", "width": 100} + { + "label": _("Valuation Rate"), + "fieldname": "val_rate", + "fieldtype": "Currency", + "width": 90, + "convertible": "rate", + "options": "currency", + }, + { + "label": _("Reorder Level"), + "fieldname": "reorder_level", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Reorder Qty"), + "fieldname": "reorder_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 100, + }, ] - if filters.get('show_stock_ageing_data'): - columns += [{'label': _('Average Age'), 'fieldname': 'average_age', 'width': 100}, - {'label': _('Earliest Age'), 'fieldname': 'earliest_age', 'width': 100}, - {'label': _('Latest Age'), 'fieldname': 'latest_age', 'width': 100}] + if filters.get("show_stock_ageing_data"): + columns += [ + {"label": _("Average Age"), "fieldname": "average_age", "width": 100}, + {"label": _("Earliest Age"), "fieldname": "earliest_age", "width": 100}, + {"label": _("Latest Age"), "fieldname": "latest_age", "width": 100}, + ] - if filters.get('show_variant_attributes'): - columns += [{'label': att_name, 'fieldname': att_name, 'width': 100} for att_name in get_variants_attributes()] + if filters.get("show_variant_attributes"): + columns += [ + {"label": att_name, "fieldname": att_name, "width": 100} + for att_name in get_variants_attributes() + ] return columns + def get_conditions(filters): conditions = "" if not filters.get("from_date"): @@ -139,28 +229,37 @@ def get_conditions(filters): conditions += " and sle.company = %s" % frappe.db.escape(filters.get("company")) if filters.get("warehouse"): - warehouse_details = frappe.db.get_value("Warehouse", - filters.get("warehouse"), ["lft", "rgt"], as_dict=1) + warehouse_details = frappe.db.get_value( + "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 + ) if warehouse_details: - conditions += " and exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and sle.warehouse = wh.name)"%(warehouse_details.lft, - warehouse_details.rgt) + conditions += ( + " and exists (select name from `tabWarehouse` wh \ + where wh.lft >= %s and wh.rgt <= %s and sle.warehouse = wh.name)" + % (warehouse_details.lft, warehouse_details.rgt) + ) if filters.get("warehouse_type") and not filters.get("warehouse"): - conditions += " and exists (select name from `tabWarehouse` wh \ - where wh.warehouse_type = '%s' and sle.warehouse = wh.name)"%(filters.get("warehouse_type")) + conditions += ( + " and exists (select name from `tabWarehouse` wh \ + where wh.warehouse_type = '%s' and sle.warehouse = wh.name)" + % (filters.get("warehouse_type")) + ) return conditions + def get_stock_ledger_entries(filters, items): - item_conditions_sql = '' + item_conditions_sql = "" if items: - item_conditions_sql = ' and sle.item_code in ({})'\ - .format(', '.join(frappe.db.escape(i, percent=False) for i in items)) + item_conditions_sql = " and sle.item_code in ({})".format( + ", ".join(frappe.db.escape(i, percent=False) for i in items) + ) conditions = get_conditions(filters) - return frappe.db.sql(""" + return frappe.db.sql( + """ select sle.item_code, warehouse, sle.posting_date, sle.actual_qty, sle.valuation_rate, sle.company, sle.voucher_type, sle.qty_after_transaction, sle.stock_value_difference, @@ -169,8 +268,11 @@ def get_stock_ledger_entries(filters, items): `tabStock Ledger Entry` sle where sle.docstatus < 2 %s %s and is_cancelled = 0 - order by sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty""" % #nosec - (item_conditions_sql, conditions), as_dict=1) + order by sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty""" + % (item_conditions_sql, conditions), # nosec + as_dict=1, + ) + def get_item_warehouse_map(filters, sle): iwb_map = {} @@ -182,13 +284,19 @@ def get_item_warehouse_map(filters, sle): for d in sle: key = (d.company, d.item_code, d.warehouse) if key not in iwb_map: - iwb_map[key] = frappe._dict({ - "opening_qty": 0.0, "opening_val": 0.0, - "in_qty": 0.0, "in_val": 0.0, - "out_qty": 0.0, "out_val": 0.0, - "bal_qty": 0.0, "bal_val": 0.0, - "val_rate": 0.0 - }) + iwb_map[key] = frappe._dict( + { + "opening_qty": 0.0, + "opening_val": 0.0, + "in_qty": 0.0, + "in_val": 0.0, + "out_qty": 0.0, + "out_val": 0.0, + "bal_qty": 0.0, + "bal_val": 0.0, + "val_rate": 0.0, + } + ) qty_dict = iwb_map[(d.company, d.item_code, d.warehouse)] @@ -199,9 +307,11 @@ def get_item_warehouse_map(filters, sle): value_diff = flt(d.stock_value_difference) - if d.posting_date < from_date or (d.posting_date == from_date - and d.voucher_type == "Stock Reconciliation" and - frappe.db.get_value("Stock Reconciliation", d.voucher_no, "purpose") == "Opening Stock"): + if d.posting_date < from_date or ( + d.posting_date == from_date + and d.voucher_type == "Stock Reconciliation" + and frappe.db.get_value("Stock Reconciliation", d.voucher_no, "purpose") == "Opening Stock" + ): qty_dict.opening_qty += qty_diff qty_dict.opening_val += value_diff @@ -221,6 +331,7 @@ def get_item_warehouse_map(filters, sle): return iwb_map + def filter_items_with_no_transactions(iwb_map, float_precision): for (company, item, warehouse) in sorted(iwb_map): qty_dict = iwb_map[(company, item, warehouse)] @@ -237,6 +348,7 @@ def filter_items_with_no_transactions(iwb_map, float_precision): return iwb_map + def get_items(filters): "Get items based on item code, item group or brand." conditions = [] @@ -245,15 +357,17 @@ def get_items(filters): else: if filters.get("item_group"): conditions.append(get_item_group_condition(filters.get("item_group"))) - if filters.get("brand"): # used in stock analytics report + if filters.get("brand"): # used in stock analytics report conditions.append("item.brand=%(brand)s") items = [] if conditions: - items = frappe.db.sql_list("""select name from `tabItem` item where {}""" - .format(" and ".join(conditions)), filters) + items = frappe.db.sql_list( + """select name from `tabItem` item where {}""".format(" and ".join(conditions)), filters + ) return items + def get_item_details(items, sle, filters): item_details = {} if not items: @@ -265,10 +379,13 @@ def get_item_details(items, sle, filters): cf_field = cf_join = "" if filters.get("include_uom"): cf_field = ", ucd.conversion_factor" - cf_join = "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%s" \ + cf_join = ( + "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%s" % frappe.db.escape(filters.get("include_uom")) + ) - res = frappe.db.sql(""" + res = frappe.db.sql( + """ select item.name, item.item_name, item.description, item.item_group, item.brand, item.stock_uom %s from @@ -276,40 +393,57 @@ def get_item_details(items, sle, filters): %s where item.name in (%s) - """ % (cf_field, cf_join, ','.join(['%s'] *len(items))), items, as_dict=1) + """ + % (cf_field, cf_join, ",".join(["%s"] * len(items))), + items, + as_dict=1, + ) for item in res: item_details.setdefault(item.name, item) - if filters.get('show_variant_attributes', 0) == 1: + if filters.get("show_variant_attributes", 0) == 1: variant_values = get_variant_values_for(list(item_details)) item_details = {k: v.update(variant_values.get(k, {})) for k, v in item_details.items()} return item_details + def get_item_reorder_details(items): item_reorder_details = frappe._dict() if items: - item_reorder_details = frappe.db.sql(""" + item_reorder_details = frappe.db.sql( + """ select parent, warehouse, warehouse_reorder_qty, warehouse_reorder_level from `tabItem Reorder` where parent in ({0}) - """.format(', '.join(frappe.db.escape(i, percent=False) for i in items)), as_dict=1) + """.format( + ", ".join(frappe.db.escape(i, percent=False) for i in items) + ), + as_dict=1, + ) return dict((d.parent + d.warehouse, d) for d in item_reorder_details) + def get_variants_attributes(): - '''Return all item variant attributes.''' - return [i.name for i in frappe.get_all('Item Attribute')] + """Return all item variant attributes.""" + return [i.name for i in frappe.get_all("Item Attribute")] + def get_variant_values_for(items): - '''Returns variant values for items.''' + """Returns variant values for items.""" attribute_map = {} - for attr in frappe.db.sql('''select parent, attribute, attribute_value + for attr in frappe.db.sql( + """select parent, attribute, attribute_value from `tabItem Variant Attribute` where parent in (%s) - ''' % ", ".join(["%s"] * len(items)), tuple(items), as_dict=1): - attribute_map.setdefault(attr['parent'], {}) - attribute_map[attr['parent']].update({attr['attribute']: attr['attribute_value']}) + """ + % ", ".join(["%s"] * len(items)), + tuple(items), + as_dict=1, + ): + attribute_map.setdefault(attr["parent"], {}) + attribute_map[attr["parent"]].update({attr["attribute"]: attr["attribute_value"]}) return attribute_map diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 9fde47e061..409e238657 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -42,19 +42,13 @@ def execute(filters=None): actual_qty += flt(sle.actual_qty, precision) stock_value += sle.stock_value_difference - if sle.voucher_type == 'Stock Reconciliation' and not sle.actual_qty: + if sle.voucher_type == "Stock Reconciliation" and not sle.actual_qty: actual_qty = sle.qty_after_transaction stock_value = sle.stock_value - sle.update({ - "qty_after_transaction": actual_qty, - "stock_value": stock_value - }) + sle.update({"qty_after_transaction": actual_qty, "stock_value": stock_value}) - sle.update({ - "in_qty": max(sle.actual_qty, 0), - "out_qty": min(sle.actual_qty, 0) - }) + sle.update({"in_qty": max(sle.actual_qty, 0), "out_qty": min(sle.actual_qty, 0)}) if sle.serial_no: update_available_serial_nos(available_serial_nos, sle) @@ -67,13 +61,15 @@ def execute(filters=None): update_included_uom_in_report(columns, data, include_uom, conversion_factors) return columns, data + def update_available_serial_nos(available_serial_nos, sle): serial_nos = get_serial_nos(sle.serial_no) key = (sle.item_code, sle.warehouse) if key not in available_serial_nos: - stock_balance = get_stock_balance_for(sle.item_code, sle.warehouse, sle.date.split(' ')[0], - sle.date.split(' ')[1]) - serials = get_serial_nos(stock_balance['serial_nos']) if stock_balance['serial_nos'] else [] + stock_balance = get_stock_balance_for( + sle.item_code, sle.warehouse, sle.date.split(" ")[0], sle.date.split(" ")[1] + ) + serials = get_serial_nos(stock_balance["serial_nos"]) if stock_balance["serial_nos"] else [] available_serial_nos.setdefault(key, serials) existing_serial_no = available_serial_nos[key] @@ -89,45 +85,158 @@ def update_available_serial_nos(available_serial_nos, sle): else: existing_serial_no.append(sn) - sle.balance_serial_no = '\n'.join(existing_serial_no) + sle.balance_serial_no = "\n".join(existing_serial_no) + def get_columns(): columns = [ {"label": _("Date"), "fieldname": "date", "fieldtype": "Datetime", "width": 150}, - {"label": _("Item"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 100}, + { + "label": _("Item"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 100, + }, {"label": _("Item Name"), "fieldname": "item_name", "width": 100}, - {"label": _("Stock UOM"), "fieldname": "stock_uom", "fieldtype": "Link", "options": "UOM", "width": 90}, - {"label": _("In Qty"), "fieldname": "in_qty", "fieldtype": "Float", "width": 80, "convertible": "qty"}, - {"label": _("Out Qty"), "fieldname": "out_qty", "fieldtype": "Float", "width": 80, "convertible": "qty"}, - {"label": _("Balance Qty"), "fieldname": "qty_after_transaction", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 150}, - {"label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", "width": 150}, - {"label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", "width": 100}, - {"label": _("Brand"), "fieldname": "brand", "fieldtype": "Link", "options": "Brand", "width": 100}, + { + "label": _("Stock UOM"), + "fieldname": "stock_uom", + "fieldtype": "Link", + "options": "UOM", + "width": 90, + }, + { + "label": _("In Qty"), + "fieldname": "in_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Out Qty"), + "fieldname": "out_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Balance Qty"), + "fieldname": "qty_after_transaction", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Voucher #"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": 150, + }, + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 150, + }, + { + "label": _("Item Group"), + "fieldname": "item_group", + "fieldtype": "Link", + "options": "Item Group", + "width": 100, + }, + { + "label": _("Brand"), + "fieldname": "brand", + "fieldtype": "Link", + "options": "Brand", + "width": 100, + }, {"label": _("Description"), "fieldname": "description", "width": 200}, - {"label": _("Incoming Rate"), "fieldname": "incoming_rate", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency", "convertible": "rate"}, - {"label": _("Valuation Rate"), "fieldname": "valuation_rate", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency", "convertible": "rate"}, - {"label": _("Balance Value"), "fieldname": "stock_value", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency"}, - {"label": _("Value Change"), "fieldname": "stock_value_difference", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency"}, + { + "label": _("Incoming Rate"), + "fieldname": "incoming_rate", + "fieldtype": "Currency", + "width": 110, + "options": "Company:company:default_currency", + "convertible": "rate", + }, + { + "label": _("Valuation Rate"), + "fieldname": "valuation_rate", + "fieldtype": "Currency", + "width": 110, + "options": "Company:company:default_currency", + "convertible": "rate", + }, + { + "label": _("Balance Value"), + "fieldname": "stock_value", + "fieldtype": "Currency", + "width": 110, + "options": "Company:company:default_currency", + }, + { + "label": _("Value Change"), + "fieldname": "stock_value_difference", + "fieldtype": "Currency", + "width": 110, + "options": "Company:company:default_currency", + }, {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110}, - {"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 100}, - {"label": _("Batch"), "fieldname": "batch_no", "fieldtype": "Link", "options": "Batch", "width": 100}, - {"label": _("Serial No"), "fieldname": "serial_no", "fieldtype": "Link", "options": "Serial No", "width": 100}, + { + "label": _("Voucher #"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": 100, + }, + { + "label": _("Batch"), + "fieldname": "batch_no", + "fieldtype": "Link", + "options": "Batch", + "width": 100, + }, + { + "label": _("Serial No"), + "fieldname": "serial_no", + "fieldtype": "Link", + "options": "Serial No", + "width": 100, + }, {"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100}, - {"label": _("Project"), "fieldname": "project", "fieldtype": "Link", "options": "Project", "width": 100}, - {"label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", "width": 110} + { + "label": _("Project"), + "fieldname": "project", + "fieldtype": "Link", + "options": "Project", + "width": 100, + }, + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 110, + }, ] return columns def get_stock_ledger_entries(filters, items): - item_conditions_sql = '' + item_conditions_sql = "" if items: - item_conditions_sql = 'and sle.item_code in ({})'\ - .format(', '.join(frappe.db.escape(i) for i in items)) + item_conditions_sql = "and sle.item_code in ({})".format( + ", ".join(frappe.db.escape(i) for i in items) + ) - sl_entries = frappe.db.sql(""" + sl_entries = frappe.db.sql( + """ SELECT concat_ws(" ", posting_date, posting_time) AS date, item_code, @@ -153,8 +262,12 @@ def get_stock_ledger_entries(filters, items): {item_conditions_sql} ORDER BY posting_date asc, posting_time asc, creation asc - """.format(sle_conditions=get_sle_conditions(filters), item_conditions_sql=item_conditions_sql), - filters, as_dict=1) + """.format( + sle_conditions=get_sle_conditions(filters), item_conditions_sql=item_conditions_sql + ), + filters, + as_dict=1, + ) return sl_entries @@ -171,8 +284,9 @@ def get_items(filters): items = [] if conditions: - items = frappe.db.sql_list("""select name from `tabItem` item where {}""" - .format(" and ".join(conditions)), filters) + items = frappe.db.sql_list( + """select name from `tabItem` item where {}""".format(" and ".join(conditions)), filters + ) return items @@ -187,10 +301,13 @@ def get_item_details(items, sl_entries, include_uom): cf_field = cf_join = "" if include_uom: cf_field = ", ucd.conversion_factor" - cf_join = "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%s" \ + cf_join = ( + "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%s" % frappe.db.escape(include_uom) + ) - res = frappe.db.sql(""" + res = frappe.db.sql( + """ select item.name, item.item_name, item.description, item.item_group, item.brand, item.stock_uom {cf_field} from @@ -198,7 +315,12 @@ def get_item_details(items, sl_entries, include_uom): {cf_join} where item.name in ({item_codes}) - """.format(cf_field=cf_field, cf_join=cf_join, item_codes=','.join(['%s'] *len(items))), items, as_dict=1) + """.format( + cf_field=cf_field, cf_join=cf_join, item_codes=",".join(["%s"] * len(items)) + ), + items, + as_dict=1, + ) for item in res: item_details.setdefault(item.name, item) @@ -227,16 +349,20 @@ def get_opening_balance(filters, columns, sl_entries): return from erpnext.stock.stock_ledger import get_previous_sle - last_entry = get_previous_sle({ - "item_code": filters.item_code, - "warehouse_condition": get_warehouse_condition(filters.warehouse), - "posting_date": filters.from_date, - "posting_time": "00:00:00" - }) + + last_entry = get_previous_sle( + { + "item_code": filters.item_code, + "warehouse_condition": get_warehouse_condition(filters.warehouse), + "posting_date": filters.from_date, + "posting_time": "00:00:00", + } + ) # check if any SLEs are actually Opening Stock Reconciliation for sle in sl_entries: - if (sle.get("voucher_type") == "Stock Reconciliation" + if ( + sle.get("voucher_type") == "Stock Reconciliation" and sle.get("date").split()[0] == filters.from_date and frappe.db.get_value("Stock Reconciliation", sle.voucher_no, "purpose") == "Opening Stock" ): @@ -247,7 +373,7 @@ def get_opening_balance(filters, columns, sl_entries): "item_code": _("'Opening'"), "qty_after_transaction": last_entry.get("qty_after_transaction", 0), "valuation_rate": last_entry.get("valuation_rate", 0), - "stock_value": last_entry.get("stock_value", 0) + "stock_value": last_entry.get("stock_value", 0), } return row @@ -256,18 +382,22 @@ def get_opening_balance(filters, columns, sl_entries): def get_warehouse_condition(warehouse): warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1) if warehouse_details: - return " exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and warehouse = wh.name)"%(warehouse_details.lft, - warehouse_details.rgt) + return ( + " exists (select name from `tabWarehouse` wh \ + where wh.lft >= %s and wh.rgt <= %s and warehouse = wh.name)" + % (warehouse_details.lft, warehouse_details.rgt) + ) - return '' + return "" def get_item_group_condition(item_group): item_group_details = frappe.db.get_value("Item Group", item_group, ["lft", "rgt"], as_dict=1) if item_group_details: - return "item.item_group in (select ig.name from `tabItem Group` ig \ - where ig.lft >= %s and ig.rgt <= %s and item.item_group = ig.name)"%(item_group_details.lft, - item_group_details.rgt) + return ( + "item.item_group in (select ig.name from `tabItem Group` ig \ + where ig.lft >= %s and ig.rgt <= %s and item.item_group = ig.name)" + % (item_group_details.lft, item_group_details.rgt) + ) - return '' + return "" diff --git a/erpnext/stock/report/stock_ledger/test_stock_ledger_report.py b/erpnext/stock/report/stock_ledger/test_stock_ledger_report.py index 163b2057c9..f93bd663db 100644 --- a/erpnext/stock/report/stock_ledger/test_stock_ledger_report.py +++ b/erpnext/stock/report/stock_ledger/test_stock_ledger_report.py @@ -17,8 +17,10 @@ class TestStockLedgerReeport(FrappeTestCase): def setUp(self) -> None: make_serial_item_with_serial("_Test Stock Report Serial Item") self.filters = frappe._dict( - company="_Test Company", from_date=today(), to_date=add_days(today(), 30), - item_code="_Test Stock Report Serial Item" + company="_Test Company", + from_date=today(), + to_date=add_days(today(), 30), + item_code="_Test Stock Report Serial Item", ) def tearDown(self) -> None: @@ -38,4 +40,3 @@ class TestStockLedgerReeport(FrappeTestCase): self.assertEqual(data[0].out_qty, -1) self.assertEqual(data[0].serial_no, serials_added[1]) self.assertEqual(data[0].balance_serial_no, serials_added[0]) - diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py index 1ba2482935..6cc9061685 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py @@ -40,11 +40,7 @@ def get_stock_ledger_entries(filters): return frappe.get_all( "Stock Ledger Entry", fields=SLE_FIELDS, - filters={ - "item_code": filters.item_code, - "warehouse": filters.warehouse, - "is_cancelled": 0 - }, + filters={"item_code": filters.item_code, "warehouse": filters.warehouse, "is_cancelled": 0}, order_by="timestamp(posting_date, posting_time), creation", ) @@ -62,7 +58,7 @@ def add_invariant_check_fields(sles): fifo_value += qty * rate if sle.actual_qty < 0: - sle.consumption_rate = sle.stock_value_difference / sle.actual_qty + sle.consumption_rate = sle.stock_value_difference / sle.actual_qty balance_qty += sle.actual_qty balance_stock_value += sle.stock_value_difference @@ -90,14 +86,16 @@ def add_invariant_check_fields(sles): sle.valuation_diff = ( sle.valuation_rate - sle.balance_value_by_qty if sle.balance_value_by_qty else None ) - sle.diff_value_diff = sle.stock_value_from_diff - sle.stock_value + sle.diff_value_diff = sle.stock_value_from_diff - sle.stock_value if idx > 0: sle.fifo_stock_diff = sle.fifo_stock_value - sles[idx - 1].fifo_stock_value sle.fifo_difference_diff = sle.fifo_stock_diff - sle.stock_value_difference if sle.batch_no: - sle.use_batchwise_valuation = frappe.db.get_value("Batch", sle.batch_no, "use_batchwise_valuation", cache=True) + sle.use_batchwise_valuation = frappe.db.get_value( + "Batch", sle.batch_no, "use_batchwise_valuation", cache=True + ) return sles @@ -183,7 +181,6 @@ def get_columns(): "fieldtype": "Data", "label": "FIFO/LIFO Queue", }, - { "fieldname": "fifo_queue_qty", "fieldtype": "Float", @@ -244,7 +241,6 @@ def get_columns(): "fieldtype": "Float", "label": "(I) Valuation Rate as per FIFO", }, - { "fieldname": "fifo_valuation_diff", "fieldtype": "Float", 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 a28b75250b..49e797d6a3 100644 --- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py +++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py @@ -32,8 +32,9 @@ def execute(filters=None): continue # item = item_map.setdefault(bin.item_code, get_item(bin.item_code)) - company = warehouse_company.setdefault(bin.warehouse, - frappe.db.get_value("Warehouse", bin.warehouse, "company")) + company = warehouse_company.setdefault( + bin.warehouse, frappe.db.get_value("Warehouse", bin.warehouse, "company") + ) if filters.brand and filters.brand != item.brand: continue @@ -59,10 +60,29 @@ def execute(filters=None): if reserved_qty_for_pos: bin.projected_qty -= reserved_qty_for_pos - data.append([item.name, item.item_name, item.description, item.item_group, item.brand, bin.warehouse, - item.stock_uom, bin.actual_qty, bin.planned_qty, bin.indented_qty, bin.ordered_qty, - bin.reserved_qty, bin.reserved_qty_for_production, bin.reserved_qty_for_sub_contract, reserved_qty_for_pos, - bin.projected_qty, re_order_level, re_order_qty, shortage_qty]) + data.append( + [ + item.name, + item.item_name, + item.description, + item.item_group, + item.brand, + bin.warehouse, + item.stock_uom, + bin.actual_qty, + bin.planned_qty, + bin.indented_qty, + bin.ordered_qty, + bin.reserved_qty, + bin.reserved_qty_for_production, + bin.reserved_qty_for_sub_contract, + reserved_qty_for_pos, + bin.projected_qty, + re_order_level, + re_order_qty, + shortage_qty, + ] + ) if include_uom: conversion_factors.append(item.conversion_factor) @@ -70,66 +90,180 @@ def execute(filters=None): update_included_uom_in_report(columns, data, include_uom, conversion_factors) return columns, data + def get_columns(): return [ - {"label": _("Item Code"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 140}, + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 140, + }, {"label": _("Item Name"), "fieldname": "item_name", "width": 100}, {"label": _("Description"), "fieldname": "description", "width": 200}, - {"label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", "width": 100}, - {"label": _("Brand"), "fieldname": "brand", "fieldtype": "Link", "options": "Brand", "width": 100}, - {"label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", "width": 120}, - {"label": _("UOM"), "fieldname": "stock_uom", "fieldtype": "Link", "options": "UOM", "width": 100}, - {"label": _("Actual Qty"), "fieldname": "actual_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Planned Qty"), "fieldname": "planned_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Requested Qty"), "fieldname": "indented_qty", "fieldtype": "Float", "width": 110, "convertible": "qty"}, - {"label": _("Ordered Qty"), "fieldname": "ordered_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Reserved Qty"), "fieldname": "reserved_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Reserved for Production"), "fieldname": "reserved_qty_for_production", "fieldtype": "Float", - "width": 100, "convertible": "qty"}, - {"label": _("Reserved for Sub Contracting"), "fieldname": "reserved_qty_for_sub_contract", "fieldtype": "Float", - "width": 100, "convertible": "qty"}, - {"label": _("Reserved for POS Transactions"), "fieldname": "reserved_qty_for_pos", "fieldtype": "Float", - "width": 100, "convertible": "qty"}, - {"label": _("Projected Qty"), "fieldname": "projected_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Reorder Level"), "fieldname": "re_order_level", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Reorder Qty"), "fieldname": "re_order_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Shortage Qty"), "fieldname": "shortage_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"} + { + "label": _("Item Group"), + "fieldname": "item_group", + "fieldtype": "Link", + "options": "Item Group", + "width": 100, + }, + { + "label": _("Brand"), + "fieldname": "brand", + "fieldtype": "Link", + "options": "Brand", + "width": 100, + }, + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 120, + }, + { + "label": _("UOM"), + "fieldname": "stock_uom", + "fieldtype": "Link", + "options": "UOM", + "width": 100, + }, + { + "label": _("Actual Qty"), + "fieldname": "actual_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Planned Qty"), + "fieldname": "planned_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Requested Qty"), + "fieldname": "indented_qty", + "fieldtype": "Float", + "width": 110, + "convertible": "qty", + }, + { + "label": _("Ordered Qty"), + "fieldname": "ordered_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Reserved Qty"), + "fieldname": "reserved_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Reserved for Production"), + "fieldname": "reserved_qty_for_production", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Reserved for Sub Contracting"), + "fieldname": "reserved_qty_for_sub_contract", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Reserved for POS Transactions"), + "fieldname": "reserved_qty_for_pos", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Projected Qty"), + "fieldname": "projected_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Reorder Level"), + "fieldname": "re_order_level", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Reorder Qty"), + "fieldname": "re_order_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Shortage Qty"), + "fieldname": "shortage_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, ] + def get_bin_list(filters): conditions = [] if filters.item_code: - conditions.append("item_code = '%s' "%filters.item_code) + conditions.append("item_code = '%s' " % filters.item_code) if filters.warehouse: - warehouse_details = frappe.db.get_value("Warehouse", filters.warehouse, ["lft", "rgt"], as_dict=1) + warehouse_details = frappe.db.get_value( + "Warehouse", filters.warehouse, ["lft", "rgt"], as_dict=1 + ) if warehouse_details: - conditions.append(" exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and bin.warehouse = wh.name)"%(warehouse_details.lft, - warehouse_details.rgt)) + conditions.append( + " exists (select name from `tabWarehouse` wh \ + where wh.lft >= %s and wh.rgt <= %s and bin.warehouse = wh.name)" + % (warehouse_details.lft, warehouse_details.rgt) + ) - bin_list = frappe.db.sql("""select item_code, warehouse, actual_qty, planned_qty, indented_qty, + bin_list = frappe.db.sql( + """select item_code, warehouse, actual_qty, planned_qty, indented_qty, ordered_qty, reserved_qty, reserved_qty_for_production, reserved_qty_for_sub_contract, projected_qty from tabBin bin {conditions} order by item_code, warehouse - """.format(conditions=" where " + " and ".join(conditions) if conditions else ""), as_dict=1) + """.format( + conditions=" where " + " and ".join(conditions) if conditions else "" + ), + as_dict=1, + ) return bin_list + def get_item_map(item_code, include_uom): """Optimization: get only the item doc and re_order_levels table""" condition = "" if item_code: - condition = 'and item_code = {0}'.format(frappe.db.escape(item_code, percent=False)) + condition = "and item_code = {0}".format(frappe.db.escape(item_code, percent=False)) cf_field = cf_join = "" if include_uom: cf_field = ", ucd.conversion_factor" - cf_join = "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%(include_uom)s" + cf_join = ( + "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%(include_uom)s" + ) - items = frappe.db.sql(""" + items = frappe.db.sql( + """ select item.name, item.item_name, item.description, item.item_group, item.brand, item.stock_uom{cf_field} from `tabItem` item {cf_join} @@ -137,16 +271,21 @@ def get_item_map(item_code, include_uom): and item.disabled=0 {condition} and (item.end_of_life > %(today)s or item.end_of_life is null or item.end_of_life='0000-00-00') - and exists (select name from `tabBin` bin where bin.item_code=item.name)"""\ - .format(cf_field=cf_field, cf_join=cf_join, condition=condition), - {"today": today(), "include_uom": include_uom}, as_dict=True) + and exists (select name from `tabBin` bin where bin.item_code=item.name)""".format( + cf_field=cf_field, cf_join=cf_join, condition=condition + ), + {"today": today(), "include_uom": include_uom}, + as_dict=True, + ) condition = "" if item_code: - condition = 'where parent={0}'.format(frappe.db.escape(item_code, percent=False)) + condition = "where parent={0}".format(frappe.db.escape(item_code, percent=False)) reorder_levels = frappe._dict() - for ir in frappe.db.sql("""select * from `tabItem Reorder` {condition}""".format(condition=condition), as_dict=1): + for ir in frappe.db.sql( + """select * from `tabItem Reorder` {condition}""".format(condition=condition), as_dict=1 + ): if ir.parent not in reorder_levels: reorder_levels[ir.parent] = [] diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py index a7b48356b8..70f04da475 100644 --- a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py +++ b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py @@ -12,12 +12,14 @@ def execute(filters=None): data = get_data(filters.warehouse) return columns, data + def validate_warehouse(filters): company = filters.company warehouse = filters.warehouse if not frappe.db.exists("Warehouse", {"name": warehouse, "company": company}): frappe.throw(_("Warehouse: {0} does not belong to {1}").format(warehouse, company)) + def get_columns(): columns = [ { @@ -25,49 +27,37 @@ def get_columns(): "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 200 - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 200 - }, - { - "label": _("Serial No Count"), - "fieldname": "total", - "fieldtype": "Float", - "width": 150 - }, - { - "label": _("Stock Qty"), - "fieldname": "stock_qty", - "fieldtype": "Float", - "width": 150 - }, - { - "label": _("Difference"), - "fieldname": "difference", - "fieldtype": "Float", - "width": 150 + "width": 200, }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 200}, + {"label": _("Serial No Count"), "fieldname": "total", "fieldtype": "Float", "width": 150}, + {"label": _("Stock Qty"), "fieldname": "stock_qty", "fieldtype": "Float", "width": 150}, + {"label": _("Difference"), "fieldname": "difference", "fieldtype": "Float", "width": 150}, ] return columns -def get_data(warehouse): - serial_item_list = frappe.get_all("Item", filters={ - 'has_serial_no': True, - }, fields=['item_code', 'item_name']) - status_list = ['Active', 'Expired'] +def get_data(warehouse): + serial_item_list = frappe.get_all( + "Item", + filters={ + "has_serial_no": True, + }, + fields=["item_code", "item_name"], + ) + + status_list = ["Active", "Expired"] data = [] for item in serial_item_list: - total_serial_no = frappe.db.count("Serial No", - filters={"item_code": item.item_code, "status": ("in", status_list), "warehouse": warehouse}) + total_serial_no = frappe.db.count( + "Serial No", + filters={"item_code": item.item_code, "status": ("in", status_list), "warehouse": warehouse}, + ) - actual_qty = frappe.db.get_value('Bin', fieldname=['actual_qty'], - filters={"warehouse": warehouse, "item_code": item.item_code}) + actual_qty = frappe.db.get_value( + "Bin", fieldname=["actual_qty"], filters={"warehouse": warehouse, "item_code": item.item_code} + ) # frappe.db.get_value returns null if no record exist. if not actual_qty: diff --git a/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py b/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py index d1748ed24b..5430fe6969 100644 --- a/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py +++ b/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py @@ -20,11 +20,11 @@ def execute(filters=None): if consumed_details.get(item_code): for cd in consumed_details.get(item_code): - if (cd.voucher_no not in material_transfer_vouchers): + if cd.voucher_no not in material_transfer_vouchers: if cd.voucher_type in ["Delivery Note", "Sales Invoice"]: delivered_qty += abs(flt(cd.actual_qty)) delivered_amount += abs(flt(cd.stock_value_difference)) - elif cd.voucher_type!="Delivery Note": + elif cd.voucher_type != "Delivery Note": consumed_qty += abs(flt(cd.actual_qty)) consumed_amount += abs(flt(cd.stock_value_difference)) @@ -32,66 +32,98 @@ def execute(filters=None): total_qty += delivered_qty + consumed_qty total_amount += delivered_amount + consumed_amount - row = [cd.item_code, cd.item_name, cd.description, cd.stock_uom, \ - consumed_qty, consumed_amount, delivered_qty, delivered_amount, \ - total_qty, total_amount, ','.join(list(set(suppliers)))] + row = [ + cd.item_code, + cd.item_name, + cd.description, + cd.stock_uom, + consumed_qty, + consumed_amount, + delivered_qty, + delivered_amount, + total_qty, + total_amount, + ",".join(list(set(suppliers))), + ] data.append(row) return columns, data + def get_columns(filters): """return columns based on filters""" - columns = [_("Item") + ":Link/Item:100"] + [_("Item Name") + "::100"] + \ - [_("Description") + "::150"] + [_("UOM") + ":Link/UOM:90"] + \ - [_("Consumed Qty") + ":Float:110"] + [_("Consumed Amount") + ":Currency:130"] + \ - [_("Delivered Qty") + ":Float:110"] + [_("Delivered Amount") + ":Currency:130"] + \ - [_("Total Qty") + ":Float:110"] + [_("Total Amount") + ":Currency:130"] + \ - [_("Supplier(s)") + "::250"] + columns = ( + [_("Item") + ":Link/Item:100"] + + [_("Item Name") + "::100"] + + [_("Description") + "::150"] + + [_("UOM") + ":Link/UOM:90"] + + [_("Consumed Qty") + ":Float:110"] + + [_("Consumed Amount") + ":Currency:130"] + + [_("Delivered Qty") + ":Float:110"] + + [_("Delivered Amount") + ":Currency:130"] + + [_("Total Qty") + ":Float:110"] + + [_("Total Amount") + ":Currency:130"] + + [_("Supplier(s)") + "::250"] + ) return columns + def get_conditions(filters): conditions = "" values = [] - if filters.get('from_date') and filters.get('to_date'): + if filters.get("from_date") and filters.get("to_date"): conditions = "and sle.posting_date>=%s and sle.posting_date<=%s" - values = [filters.get('from_date'), filters.get('to_date')] + values = [filters.get("from_date"), filters.get("to_date")] return conditions, values + def get_consumed_details(filters): conditions, values = get_conditions(filters) consumed_details = {} - for d in frappe.db.sql("""select sle.item_code, i.item_name, i.description, + for d in frappe.db.sql( + """select sle.item_code, i.item_name, i.description, i.stock_uom, sle.actual_qty, sle.stock_value_difference, sle.voucher_no, sle.voucher_type from `tabStock Ledger Entry` sle, `tabItem` i - where sle.is_cancelled = 0 and sle.item_code=i.name and sle.actual_qty < 0 %s""" % conditions, values, as_dict=1): - consumed_details.setdefault(d.item_code, []).append(d) + where sle.is_cancelled = 0 and sle.item_code=i.name and sle.actual_qty < 0 %s""" + % conditions, + values, + as_dict=1, + ): + consumed_details.setdefault(d.item_code, []).append(d) return consumed_details + def get_suppliers_details(filters): item_supplier_map = {} - supplier = filters.get('supplier') + supplier = filters.get("supplier") - for d in frappe.db.sql("""select pr.supplier, pri.item_code from + for d in frappe.db.sql( + """select pr.supplier, pri.item_code from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri where pr.name=pri.parent and pr.docstatus=1 and pri.item_code=(select name from `tabItem` where - is_stock_item=1 and name=pri.item_code)""", as_dict=1): - item_supplier_map.setdefault(d.item_code, []).append(d.supplier) + is_stock_item=1 and name=pri.item_code)""", + as_dict=1, + ): + item_supplier_map.setdefault(d.item_code, []).append(d.supplier) - for d in frappe.db.sql("""select pr.supplier, pri.item_code from + for d in frappe.db.sql( + """select pr.supplier, pri.item_code from `tabPurchase Invoice` pr, `tabPurchase Invoice Item` pri where pr.name=pri.parent and pr.docstatus=1 and ifnull(pr.update_stock, 0) = 1 and pri.item_code=(select name from `tabItem` - where is_stock_item=1 and name=pri.item_code)""", as_dict=1): - if d.item_code not in item_supplier_map: - item_supplier_map.setdefault(d.item_code, []).append(d.supplier) + where is_stock_item=1 and name=pri.item_code)""", + as_dict=1, + ): + if d.item_code not in item_supplier_map: + item_supplier_map.setdefault(d.item_code, []).append(d.supplier) if supplier: invalid_items = [] @@ -104,6 +136,9 @@ def get_suppliers_details(filters): return item_supplier_map + def get_material_transfer_vouchers(): - return frappe.db.sql_list("""select name from `tabStock Entry` where - purpose='Material Transfer' and docstatus=1""") + return frappe.db.sql_list( + """select name from `tabStock Entry` where + purpose='Material Transfer' and docstatus=1""" + ) diff --git a/erpnext/stock/report/test_reports.py b/erpnext/stock/report/test_reports.py index 76c20798bf..55b910432a 100644 --- a/erpnext/stock/report/test_reports.py +++ b/erpnext/stock/report/test_reports.py @@ -43,8 +43,18 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [ }, ), ("Warehouse wise Item Balance Age and Value", {"_optional": True}), - ("Item Variant Details", {"item": "_Test Variant Item",}), - ("Total Stock Summary", {"group_by": "warehouse",}), + ( + "Item Variant Details", + { + "item": "_Test Variant Item", + }, + ), + ( + "Total Stock Summary", + { + "group_by": "warehouse", + }, + ), ("Batch Item Expiry Status", {}), ("Incorrect Stock Value Report", {"company": "_Test Company with perpetual inventory"}), ("Incorrect Serial No Valuation", {}), @@ -54,12 +64,7 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [ ("Delayed Item Report", {"based_on": "Sales Invoice"}), ("Delayed Item Report", {"based_on": "Delivery Note"}), ("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}), - ("Stock Ledger Invariant Check", - { - "warehouse": "_Test Warehouse - _TC", - "item": "_Test Item" - } - ), + ("Stock Ledger Invariant Check", {"warehouse": "_Test Warehouse - _TC", "item": "_Test Item"}), ] OPTIONAL_FILTERS = { diff --git a/erpnext/stock/report/total_stock_summary/total_stock_summary.py b/erpnext/stock/report/total_stock_summary/total_stock_summary.py index 6f27558b88..21529da2a1 100644 --- a/erpnext/stock/report/total_stock_summary/total_stock_summary.py +++ b/erpnext/stock/report/total_stock_summary/total_stock_summary.py @@ -15,6 +15,7 @@ def execute(filters=None): return columns, stock + def get_columns(): columns = [ _("Company") + ":Link/Company:250", @@ -26,13 +27,16 @@ def get_columns(): return columns + def get_total_stock(filters): conditions = "" columns = "" if filters.get("group_by") == "Warehouse": if filters.get("company"): - conditions += " AND warehouse.company = %s" % frappe.db.escape(filters.get("company"), percent=False) + conditions += " AND warehouse.company = %s" % frappe.db.escape( + filters.get("company"), percent=False + ) conditions += " GROUP BY ledger.warehouse, item.item_code" columns += "'' as company, ledger.warehouse" @@ -40,7 +44,8 @@ def get_total_stock(filters): conditions += " GROUP BY warehouse.company, item.item_code" columns += " warehouse.company, '' as warehouse" - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT %s, item.item_code, @@ -53,4 +58,6 @@ def get_total_stock(filters): INNER JOIN `tabWarehouse` warehouse ON warehouse.name = ledger.warehouse WHERE - ledger.actual_qty != 0 %s""" % (columns, conditions)) + ledger.actual_qty != 0 %s""" + % (columns, conditions) + ) 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 22bdb89198..a54373f364 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 @@ -21,7 +21,8 @@ from erpnext.stock.utils import is_reposting_item_valuation_in_progress def execute(filters=None): is_reposting_item_valuation_in_progress() - if not filters: filters = {} + if not filters: + filters = {} validate_filters(filters) @@ -39,7 +40,8 @@ def execute(filters=None): item_value = {} for (company, item, warehouse) in sorted(iwb_map): - if not item_map.get(item): continue + if not item_map.get(item): + continue row = [] qty_dict = iwb_map[(company, item, warehouse)] @@ -50,13 +52,13 @@ def execute(filters=None): total_stock_value += qty_dict.bal_val if wh.name == warehouse else 0.00 item_balance[(item, item_map[item]["item_group"])].append(row) - item_value.setdefault((item, item_map[item]["item_group"]),[]) + item_value.setdefault((item, item_map[item]["item_group"]), []) item_value[(item, item_map[item]["item_group"])].append(total_stock_value) - # sum bal_qty by item for (item, item_group), wh_balance in item_balance.items(): - if not item_ageing.get(item): continue + if not item_ageing.get(item): + continue total_stock_value = sum(item_value[(item, item_group)]) row = [item, item_group, total_stock_value] @@ -81,17 +83,19 @@ def execute(filters=None): add_warehouse_column(columns, warehouse_list) return columns, data + def get_columns(filters): """return columns""" columns = [ - _("Item")+":Link/Item:180", - _("Item Group")+"::100", - _("Value")+":Currency:100", - _("Age")+":Float:60", + _("Item") + ":Link/Item:180", + _("Item Group") + "::100", + _("Value") + ":Currency:100", + _("Age") + ":Float:60", ] return columns + def validate_filters(filters): if not (filters.get("item_code") or filters.get("warehouse")): sle_count = flt(frappe.db.sql("""select count(name) from `tabStock Ledger Entry`""")[0][0]) @@ -100,11 +104,12 @@ def validate_filters(filters): if not filters.get("company"): filters["company"] = frappe.defaults.get_user_default("Company") + def get_warehouse_list(filters): from frappe.core.doctype.user_permission.user_permission import get_permitted_documents - condition = '' - user_permitted_warehouse = get_permitted_documents('Warehouse') + condition = "" + user_permitted_warehouse = get_permitted_documents("Warehouse") value = () if user_permitted_warehouse: condition = "and name in %s" @@ -113,13 +118,20 @@ def get_warehouse_list(filters): condition = "and name = %s" value = filters.get("warehouse") - return frappe.db.sql("""select name + return frappe.db.sql( + """select name from `tabWarehouse` where is_group = 0 - {condition}""".format(condition=condition), value, as_dict=1) + {condition}""".format( + condition=condition + ), + value, + as_dict=1, + ) + def add_warehouse_column(columns, warehouse_list): if len(warehouse_list) > 1: - columns += [_("Total Qty")+":Int:50"] + columns += [_("Total Qty") + ":Int:50"] for wh in warehouse_list: - columns += [_(wh.name)+":Int:54"] + columns += [_(wh.name) + ":Int:54"] diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index 62017e4159..e05d1c3a29 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -15,16 +15,20 @@ def repost(only_actual=False, allow_negative_stock=False, allow_zero_rate=False, frappe.db.auto_commit_on_many_writes = 1 if allow_negative_stock: - existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") + existing_allow_negative_stock = frappe.db.get_value( + "Stock Settings", None, "allow_negative_stock" + ) frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) - item_warehouses = frappe.db.sql(""" + item_warehouses = frappe.db.sql( + """ select distinct item_code, warehouse from (select item_code, warehouse from tabBin union select item_code, warehouse from `tabStock Ledger Entry`) a - """) + """ + ) for d in item_warehouses: try: repost_stock(d[0], d[1], allow_zero_rate, only_actual, only_bin, allow_negative_stock) @@ -33,11 +37,20 @@ def repost(only_actual=False, allow_negative_stock=False, allow_zero_rate=False, frappe.db.rollback() if allow_negative_stock: - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) + frappe.db.set_value( + "Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock + ) frappe.db.auto_commit_on_many_writes = 0 -def repost_stock(item_code, warehouse, allow_zero_rate=False, - only_actual=False, only_bin=False, allow_negative_stock=False): + +def repost_stock( + item_code, + warehouse, + allow_zero_rate=False, + only_actual=False, + only_bin=False, + allow_negative_stock=False, +): if not only_bin: repost_actual_qty(item_code, warehouse, allow_zero_rate, allow_negative_stock) @@ -47,35 +60,42 @@ def repost_stock(item_code, warehouse, allow_zero_rate=False, "reserved_qty": get_reserved_qty(item_code, warehouse), "indented_qty": get_indented_qty(item_code, warehouse), "ordered_qty": get_ordered_qty(item_code, warehouse), - "planned_qty": get_planned_qty(item_code, warehouse) + "planned_qty": get_planned_qty(item_code, warehouse), } if only_bin: - qty_dict.update({ - "actual_qty": get_balance_qty_from_sle(item_code, warehouse) - }) + qty_dict.update({"actual_qty": get_balance_qty_from_sle(item_code, warehouse)}) update_bin_qty(item_code, warehouse, qty_dict) + def repost_actual_qty(item_code, warehouse, allow_zero_rate=False, allow_negative_stock=False): - 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 - }) + 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` + balance_qty = frappe.db.sql( + """select qty_after_transaction from `tabStock Ledger Entry` 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)) + limit 1""", + (item_code, warehouse), + ) return flt(balance_qty[0][0]) if balance_qty else 0.0 + def get_reserved_qty(item_code, warehouse): - reserved_qty = frappe.db.sql(""" + reserved_qty = frappe.db.sql( + """ select sum(dnpi_qty * ((so_item_qty - so_item_delivered_qty) / so_item_qty)) from @@ -115,58 +135,76 @@ def get_reserved_qty(item_code, warehouse): ) tab where so_item_qty >= so_item_delivered_qty - """, (item_code, warehouse, item_code, warehouse)) + """, + (item_code, warehouse, item_code, warehouse), + ) return flt(reserved_qty[0][0]) if reserved_qty else 0 + def get_indented_qty(item_code, warehouse): # Ordered Qty is always maintained in stock UOM - inward_qty = frappe.db.sql(""" + inward_qty = frappe.db.sql( + """ select sum(mr_item.stock_qty - mr_item.ordered_qty) from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr where mr_item.item_code=%s and mr_item.warehouse=%s and mr.material_request_type in ('Purchase', 'Manufacture', 'Customer Provided', 'Material Transfer') and mr_item.stock_qty > mr_item.ordered_qty and mr_item.parent=mr.name and mr.status!='Stopped' and mr.docstatus=1 - """, (item_code, warehouse)) + """, + (item_code, warehouse), + ) inward_qty = flt(inward_qty[0][0]) if inward_qty else 0 - outward_qty = frappe.db.sql(""" + outward_qty = frappe.db.sql( + """ select sum(mr_item.stock_qty - mr_item.ordered_qty) from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr where mr_item.item_code=%s and mr_item.warehouse=%s and mr.material_request_type = 'Material Issue' and mr_item.stock_qty > mr_item.ordered_qty and mr_item.parent=mr.name and mr.status!='Stopped' and mr.docstatus=1 - """, (item_code, warehouse)) + """, + (item_code, warehouse), + ) outward_qty = flt(outward_qty[0][0]) if outward_qty else 0 requested_qty = inward_qty - outward_qty return requested_qty + def get_ordered_qty(item_code, warehouse): - ordered_qty = frappe.db.sql(""" + ordered_qty = frappe.db.sql( + """ select sum((po_item.qty - po_item.received_qty)*po_item.conversion_factor) from `tabPurchase Order Item` po_item, `tabPurchase Order` po where po_item.item_code=%s and po_item.warehouse=%s and po_item.qty > po_item.received_qty and po_item.parent=po.name and po.status not in ('Closed', 'Delivered') and po.docstatus=1 - and po_item.delivered_by_supplier = 0""", (item_code, warehouse)) + and po_item.delivered_by_supplier = 0""", + (item_code, warehouse), + ) return flt(ordered_qty[0][0]) if ordered_qty else 0 + def get_planned_qty(item_code, warehouse): - planned_qty = frappe.db.sql(""" + planned_qty = frappe.db.sql( + """ select sum(qty - produced_qty) from `tabWork Order` where production_item = %s and fg_warehouse = %s and status not in ("Stopped", "Completed", "Closed") - and docstatus=1 and qty > produced_qty""", (item_code, warehouse)) + and docstatus=1 and qty > produced_qty""", + (item_code, warehouse), + ) return flt(planned_qty[0][0]) if planned_qty else 0 def update_bin_qty(item_code, warehouse, qty_dict=None): from erpnext.stock.utils import get_bin + bin = get_bin(item_code, warehouse) mismatch = False for field, value in qty_dict.items(): @@ -180,41 +218,54 @@ def update_bin_qty(item_code, warehouse, qty_dict=None): bin.db_update() bin.clear_cache() -def set_stock_balance_as_per_serial_no(item_code=None, posting_date=None, posting_time=None, - fiscal_year=None): - if not posting_date: posting_date = nowdate() - if not posting_time: posting_time = nowtime() - condition = " and item.name='%s'" % item_code.replace("'", "\'") if item_code else "" +def set_stock_balance_as_per_serial_no( + item_code=None, posting_date=None, posting_time=None, fiscal_year=None +): + if not posting_date: + posting_date = nowdate() + if not posting_time: + posting_time = nowtime() - bin = frappe.db.sql("""select bin.item_code, bin.warehouse, bin.actual_qty, item.stock_uom + condition = " and item.name='%s'" % item_code.replace("'", "'") if item_code else "" + + bin = frappe.db.sql( + """select bin.item_code, bin.warehouse, bin.actual_qty, item.stock_uom from `tabBin` bin, tabItem item - where bin.item_code = item.name and item.has_serial_no = 1 %s""" % condition) + where bin.item_code = item.name and item.has_serial_no = 1 %s""" + % condition + ) for d in bin: - serial_nos = frappe.db.sql("""select count(name) from `tabSerial No` - where item_code=%s and warehouse=%s and docstatus < 2""", (d[0], d[1])) + serial_nos = frappe.db.sql( + """select count(name) from `tabSerial No` + where item_code=%s and warehouse=%s and docstatus < 2""", + (d[0], d[1]), + ) - sle = frappe.db.sql("""select valuation_rate, company from `tabStock Ledger Entry` + sle = frappe.db.sql( + """select valuation_rate, company from `tabStock Ledger Entry` where item_code = %s and warehouse = %s and is_cancelled = 0 - order by posting_date desc limit 1""", (d[0], d[1])) + order by posting_date desc limit 1""", + (d[0], d[1]), + ) sle_dict = { - 'doctype' : 'Stock Ledger Entry', - 'item_code' : d[0], - 'warehouse' : d[1], - 'transaction_date' : nowdate(), - 'posting_date' : posting_date, - 'posting_time' : posting_time, - 'voucher_type' : 'Stock Reconciliation (Manual)', - 'voucher_no' : '', - 'voucher_detail_no' : '', - 'actual_qty' : flt(serial_nos[0][0]) - flt(d[2]), - 'stock_uom' : d[3], - 'incoming_rate' : sle and flt(serial_nos[0][0]) > flt(d[2]) and flt(sle[0][0]) or 0, - 'company' : sle and cstr(sle[0][1]) or 0, - 'batch_no' : '', - 'serial_no' : '' + "doctype": "Stock Ledger Entry", + "item_code": d[0], + "warehouse": d[1], + "transaction_date": nowdate(), + "posting_date": posting_date, + "posting_time": posting_time, + "voucher_type": "Stock Reconciliation (Manual)", + "voucher_no": "", + "voucher_detail_no": "", + "actual_qty": flt(serial_nos[0][0]) - flt(d[2]), + "stock_uom": d[3], + "incoming_rate": sle and flt(serial_nos[0][0]) > flt(d[2]) and flt(sle[0][0]) or 0, + "company": sle and cstr(sle[0][1]) or 0, + "batch_no": "", + "serial_no": "", } sle_doc = frappe.get_doc(sle_dict) @@ -223,16 +274,17 @@ def set_stock_balance_as_per_serial_no(item_code=None, posting_date=None, postin sle_doc.insert() args = sle_dict.copy() - args.update({ - "sle_id": sle_doc.name - }) + args.update({"sle_id": sle_doc.name}) + + create_repost_item_valuation_entry( + { + "item_code": d[0], + "warehouse": d[1], + "posting_date": posting_date, + "posting_time": posting_time, + } + ) - create_repost_item_valuation_entry({ - "item_code": d[0], - "warehouse": d[1], - "posting_date": posting_date, - "posting_time": posting_time - }) def reset_serial_no_status_and_warehouse(serial_nos=None): if not serial_nos: diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 1aaaad2dff..967b2b2294 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -22,28 +22,32 @@ from erpnext.stock.utils import ( from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero -class NegativeStockError(frappe.ValidationError): pass +class NegativeStockError(frappe.ValidationError): + pass + + class SerialNoExistsInFutureTransaction(frappe.ValidationError): pass def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): - """ Create SL entries from SL entry dicts + """Create SL entries from SL entry dicts - args: - - allow_negative_stock: disable negative stock valiations if true - - via_landed_cost_voucher: landed cost voucher cancels and reposts - entries of purchase document. This flag is used to identify if - cancellation and repost is happening via landed cost voucher, in - such cases certain validations need to be ignored (like negative - stock) + args: + - allow_negative_stock: disable negative stock valiations if true + - via_landed_cost_voucher: landed cost voucher cancels and reposts + entries of purchase document. This flag is used to identify if + cancellation and repost is happening via landed cost voucher, in + such cases certain validations need to be ignored (like negative + stock) """ from erpnext.controllers.stock_controller import future_sle_exists + if sl_entries: cancel = sl_entries[0].get("is_cancelled") if cancel: validate_cancellation(sl_entries) - set_as_cancel(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no')) + set_as_cancel(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no")) args = get_args_for_future_sle(sl_entries[0]) future_sle_exists(args, sl_entries) @@ -53,19 +57,21 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc validate_serial_no(sle) if cancel: - sle['actual_qty'] = -flt(sle.get('actual_qty')) + 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("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("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": + if sle.get("actual_qty") or sle.get("voucher_type") == "Stock Reconciliation": sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher) args = sle_doc.as_dict() @@ -74,13 +80,16 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc # preserve previous_qty_after_transaction for qty reposting args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction") - is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item') + is_stock_item = frappe.get_cached_value("Item", args.get("item_code"), "is_stock_item") if is_stock_item: bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse")) repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher) update_bin_qty(bin_name, args) else: - frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code"))) + frappe.msgprint( + _("Item {0} ignored since it is not a stock item").format(args.get("item_code")) + ) + def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False): if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation": @@ -92,28 +101,35 @@ def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_vou # Reposts only current voucher SL Entries # Updates valuation rate, stock value, stock queue for current transaction - update_entries_after({ - "item_code": args.get('item_code'), - "warehouse": args.get('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.get('name'), - "creation": args.get('creation') - }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) + update_entries_after( + { + "item_code": args.get("item_code"), + "warehouse": args.get("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.get("name"), + "creation": args.get("creation"), + }, + allow_negative_stock=allow_negative_stock, + via_landed_cost_voucher=via_landed_cost_voucher, + ) # update qty in future sle and Validate negative qty update_qty_in_future_sle(args, allow_negative_stock) def get_args_for_future_sle(row): - return frappe._dict({ - 'voucher_type': row.get('voucher_type'), - 'voucher_no': row.get('voucher_no'), - 'posting_date': row.get('posting_date'), - 'posting_time': row.get('posting_time') - }) + return frappe._dict( + { + "voucher_type": row.get("voucher_type"), + "voucher_no": row.get("voucher_no"), + "posting_date": row.get("posting_date"), + "posting_time": row.get("posting_time"), + } + ) + def validate_serial_no(sle): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -121,58 +137,79 @@ def validate_serial_no(sle): for sn in get_serial_nos(sle.serial_no): args = copy.deepcopy(sle) args.serial_no = sn - args.warehouse = '' + args.warehouse = "" vouchers = [] - for row in get_stock_ledger_entries(args, '>'): + for row in get_stock_ledger_entries(args, ">"): voucher_type = frappe.bold(row.voucher_type) voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no)) - vouchers.append(f'{voucher_type} {voucher_no}') + vouchers.append(f"{voucher_type} {voucher_no}") if vouchers: serial_no = frappe.bold(sn) - msg = (f'''The serial no {serial_no} has been used in the future transactions so you need to cancel them first. - The list of the transactions are as below.''' + '

    • ') + msg = ( + f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first. + The list of the transactions are as below.""" + + "

      • " + ) - msg += '
      • '.join(vouchers) - msg += '
      ' + msg += "
    • ".join(vouchers) + msg += "
    " - title = 'Cannot Submit' if not sle.get('is_cancelled') else 'Cannot Cancel' + title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel" frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction) + def validate_cancellation(args): if args[0].get("is_cancelled"): - repost_entry = frappe.db.get_value("Repost Item Valuation", { - 'voucher_type': args[0].voucher_type, - 'voucher_no': args[0].voucher_no, - 'docstatus': 1 - }, ['name', 'status'], as_dict=1) + repost_entry = frappe.db.get_value( + "Repost Item Valuation", + {"voucher_type": args[0].voucher_type, "voucher_no": args[0].voucher_no, "docstatus": 1}, + ["name", "status"], + as_dict=1, + ) if repost_entry: - if repost_entry.status == 'In Progress': - frappe.throw(_("Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet.")) - if repost_entry.status == 'Queued': + if repost_entry.status == "In Progress": + frappe.throw( + _( + "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet." + ) + ) + if repost_entry.status == "Queued": doc = frappe.get_doc("Repost Item Valuation", repost_entry.name) doc.flags.ignore_permissions = True doc.cancel() doc.delete() + def set_as_cancel(voucher_type, voucher_no): - frappe.db.sql("""update `tabStock Ledger Entry` set is_cancelled=1, + frappe.db.sql( + """update `tabStock Ledger Entry` set is_cancelled=1, modified=%s, modified_by=%s where voucher_type=%s and voucher_no=%s and is_cancelled = 0""", - (now(), frappe.session.user, voucher_type, voucher_no)) + (now(), frappe.session.user, voucher_type, voucher_no), + ) + def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False): args["doctype"] = "Stock Ledger Entry" sle = frappe.get_doc(args) sle.flags.ignore_permissions = 1 - sle.allow_negative_stock=allow_negative_stock + sle.allow_negative_stock = allow_negative_stock sle.via_landed_cost_voucher = via_landed_cost_voucher sle.submit() return sle -def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negative_stock=None, via_landed_cost_voucher=False, doc=None): + +def repost_future_sle( + args=None, + voucher_type=None, + voucher_no=None, + allow_negative_stock=None, + via_landed_cost_voucher=False, + doc=None, +): if not args and voucher_type and voucher_no: args = get_items_to_be_repost(voucher_type, voucher_no, doc) @@ -182,20 +219,28 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat while i < len(args): validate_item_warehouse(args[i]) - obj = update_entries_after({ - 'item_code': args[i].get('item_code'), - 'warehouse': args[i].get('warehouse'), - 'posting_date': args[i].get('posting_date'), - 'posting_time': args[i].get('posting_time'), - 'creation': args[i].get('creation'), - 'distinct_item_warehouses': distinct_item_warehouses - }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) + obj = update_entries_after( + { + "item_code": args[i].get("item_code"), + "warehouse": args[i].get("warehouse"), + "posting_date": args[i].get("posting_date"), + "posting_time": args[i].get("posting_time"), + "creation": args[i].get("creation"), + "distinct_item_warehouses": distinct_item_warehouses, + }, + allow_negative_stock=allow_negative_stock, + via_landed_cost_voucher=via_landed_cost_voucher, + ) - distinct_item_warehouses[(args[i].get('item_code'), args[i].get('warehouse'))].reposting_status = True + distinct_item_warehouses[ + (args[i].get("item_code"), args[i].get("warehouse")) + ].reposting_status = True if obj.new_items_found: for item_wh, data in distinct_item_warehouses.items(): - if ('args_idx' not in data and not data.reposting_status) or (data.sle_changed and data.reposting_status): + if ("args_idx" not in data and not data.reposting_status) or ( + data.sle_changed and data.reposting_status + ): data.args_idx = len(args) args.append(data.sle) elif data.sle_changed and not data.reposting_status: @@ -210,82 +255,104 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat if doc and args: update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses) + def validate_item_warehouse(args): - for field in ['item_code', 'warehouse', 'posting_date', 'posting_time']: + for field in ["item_code", "warehouse", "posting_date", "posting_time"]: if not args.get(field): - validation_msg = f'The field {frappe.unscrub(args.get(field))} is required for the reposting' + validation_msg = f"The field {frappe.unscrub(args.get(field))} is required for the reposting" frappe.throw(_(validation_msg)) + def update_args_in_repost_item_valuation(doc, index, args, distinct_item_warehouses): - frappe.db.set_value(doc.doctype, doc.name, { - 'items_to_be_repost': json.dumps(args, default=str), - 'distinct_item_and_warehouse': json.dumps({str(k): v for k,v in distinct_item_warehouses.items()}, default=str), - 'current_index': index - }) + frappe.db.set_value( + doc.doctype, + doc.name, + { + "items_to_be_repost": json.dumps(args, default=str), + "distinct_item_and_warehouse": json.dumps( + {str(k): v for k, v in distinct_item_warehouses.items()}, default=str + ), + "current_index": index, + }, + ) frappe.db.commit() - frappe.publish_realtime('item_reposting_progress', { - 'name': doc.name, - 'items_to_be_repost': json.dumps(args, default=str), - 'current_index': index - }) + frappe.publish_realtime( + "item_reposting_progress", + {"name": doc.name, "items_to_be_repost": json.dumps(args, default=str), "current_index": index}, + ) + def get_items_to_be_repost(voucher_type, voucher_no, doc=None): if doc and doc.items_to_be_repost: return json.loads(doc.items_to_be_repost) or [] - return frappe.db.get_all("Stock Ledger Entry", + 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", "creation"], order_by="creation asc", - group_by="item_code, warehouse" + group_by="item_code, warehouse", ) + def get_distinct_item_warehouse(args=None, doc=None): distinct_item_warehouses = {} if doc and doc.distinct_item_and_warehouse: distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse) - distinct_item_warehouses = {frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()} + distinct_item_warehouses = { + frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items() + } else: for i, d in enumerate(args): - distinct_item_warehouses.setdefault((d.item_code, d.warehouse), frappe._dict({ - "reposting_status": False, - "sle": d, - "args_idx": i - })) + distinct_item_warehouses.setdefault( + (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i}) + ) return distinct_item_warehouses + def get_current_index(doc=None): if doc and doc.current_index: return doc.current_index + class update_entries_after(object): """ - update valution rate and qty after transaction - from the current time-bucket onwards + update valution rate and qty after transaction + from the current time-bucket onwards - :param args: args as dict + :param args: args as dict - args = { - "item_code": "ABC", - "warehouse": "XYZ", - "posting_date": "2012-12-12", - "posting_time": "12:00" - } + args = { + "item_code": "ABC", + "warehouse": "XYZ", + "posting_date": "2012-12-12", + "posting_time": "12:00" + } """ - def __init__(self, args, allow_zero_rate=False, allow_negative_stock=None, via_landed_cost_voucher=False, verbose=1): + + def __init__( + self, + args, + allow_zero_rate=False, + allow_negative_stock=None, + via_landed_cost_voucher=False, + verbose=1, + ): self.exceptions = {} self.verbose = verbose self.allow_zero_rate = allow_zero_rate self.via_landed_cost_voucher = via_landed_cost_voucher self.item_code = args.get("item_code") - self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(item_code=self.item_code) + self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed( + item_code=self.item_code + ) self.args = frappe._dict(args) if self.args.sle_id: - self.args['name'] = self.args.sle_id + self.args["name"] = self.args.sle_id self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company") self.get_precision() @@ -299,28 +366,29 @@ class update_entries_after(object): 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) + 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 + Get previous sl entries for current item for each related warehouse + and assigns into self.data dict - :Data Structure: + :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 = { + 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()) @@ -331,11 +399,13 @@ class update_entries_after(object): for key in ("qty_after_transaction", "valuation_rate", "stock_value"): setattr(warehouse_dict, key, flt(previous_sle.get(key))) - 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 - }) + 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, + } + ) def build(self): from erpnext.controllers.stock_controller import future_sle_exists @@ -368,9 +438,10 @@ class update_entries_after(object): self.process_sle(sle) def get_sle_against_current_voucher(self): - self.args['time_format'] = '%H:%i:%s' + self.args["time_format"] = "%H:%i:%s" - return frappe.db.sql(""" + return frappe.db.sql( + """ select *, timestamp(posting_date, posting_time) as "timestamp" from @@ -384,22 +455,29 @@ class update_entries_after(object): order by creation ASC for update - """, self.args, as_dict=1) + """, + self.args, + as_dict=1, + ) 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}) + args = self.data[self.args.warehouse].previous_sle or frappe._dict( + {"item_code": self.item_code, "warehouse": self.args.warehouse} + ) return list(self.get_sle_after_datetime(args)) def get_dependent_entries_to_fix(self, entries_to_fix, sle): - dependant_sle = get_sle_by_voucher_detail_no(sle.dependant_sle_voucher_detail_no, - excluded_sle=sle.name) + dependant_sle = get_sle_by_voucher_detail_no( + sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name + ) if not dependant_sle: return entries_to_fix - elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse: + elif ( + dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse + ): return entries_to_fix elif dependant_sle.item_code != self.item_code: self.update_distinct_item_warehouses(dependant_sle) @@ -411,14 +489,14 @@ class update_entries_after(object): def update_distinct_item_warehouses(self, dependant_sle): key = (dependant_sle.item_code, dependant_sle.warehouse) - val = frappe._dict({ - "sle": dependant_sle - }) + val = frappe._dict({"sle": dependant_sle}) if key not in self.distinct_item_warehouses: self.distinct_item_warehouses[key] = val self.new_items_found = True else: - existing_sle_posting_date = self.distinct_item_warehouses[key].get("sle", {}).get("posting_date") + existing_sle_posting_date = ( + self.distinct_item_warehouses[key].get("sle", {}).get("posting_date") + ) if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date): val.sle_changed = True self.distinct_item_warehouses[key] = val @@ -427,12 +505,13 @@ class update_entries_after(object): def append_future_sle_for_dependant(self, dependant_sle, entries_to_fix): 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}) + 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) - return sorted(entries_to_fix, key=lambda k: k['timestamp']) + return sorted(entries_to_fix, key=lambda k: k["timestamp"]) def process_sle(self, sle): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -457,22 +536,30 @@ class update_entries_after(object): if sle.voucher_type == "Stock Reconciliation": self.wh_data.qty_after_transaction = sle.qty_after_transaction - self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate) - elif sle.batch_no and frappe.db.get_value("Batch", sle.batch_no, "use_batchwise_valuation", cache=True): + self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt( + self.wh_data.valuation_rate + ) + elif sle.batch_no and frappe.db.get_value( + "Batch", sle.batch_no, "use_batchwise_valuation", cache=True + ): self.update_batched_values(sle) else: - if sle.voucher_type=="Stock Reconciliation" and not sle.batch_no: + if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no: # assert self.wh_data.valuation_rate = sle.valuation_rate self.wh_data.qty_after_transaction = sle.qty_after_transaction - self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate) + self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt( + self.wh_data.valuation_rate + ) if self.valuation_method != "Moving Average": self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]] else: if self.valuation_method == "Moving Average": self.get_moving_average_values(sle) 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) + self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt( + self.wh_data.valuation_rate + ) else: self.update_queue_values(sle) @@ -489,17 +576,16 @@ class update_entries_after(object): 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" + sle.doctype = "Stock Ledger Entry" frappe.get_doc(sle).db_update() if not self.args.get("sle_id"): 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 + validate negative stock for entries current datetime onwards + will not consider cancelled entries """ diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) @@ -528,13 +614,24 @@ class update_entries_after(object): self.recalculate_amounts_in_stock_entry(sle.voucher_no) 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"): + 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, sle = sle) + + rate = get_rate_for_return( + sle.voucher_type, + sle.voucher_no, + sle.item_code, + voucher_detail_no=sle.voucher_detail_no, + sle=sle, + ) else: if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"): rate_field = "valuation_rate" @@ -542,8 +639,9 @@ class update_entries_after(object): 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]) + 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 @@ -553,15 +651,18 @@ class update_entries_after(object): 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) + 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 + 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) @@ -591,24 +692,33 @@ class update_entries_after(object): # 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) + 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", + frappe.db.set_value( + "Packed Item", {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code}, - "incoming_rate", outgoing_rate) + "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) + 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) + 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.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted") == 'Yes': + if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted") == "Yes": doc = frappe.get_doc(sle.voucher_type, sle.voucher_no) doc.update_valuation_rate(reset_outgoing_rate=False) - for d in (doc.items + doc.supplied_items): + for d in doc.items + doc.supplied_items: d.db_update() def get_serialized_values(self, sle): @@ -635,29 +745,34 @@ class update_entries_after(object): new_stock_qty = self.wh_data.qty_after_transaction + actual_qty if new_stock_qty > 0: - new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.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.wh_data.valuation_rate = new_stock_value / new_stock_qty 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) + allow_zero_rate = self.check_if_allow_zero_valuation_rate( + sle.voucher_type, sle.voucher_detail_no + ) if not allow_zero_rate: self.wh_data.valuation_rate = self.get_fallback_rate(sle) def get_incoming_value_for_serial_nos(self, sle, serial_nos): # get rate from serial nos within same company - all_serial_nos = frappe.get_all("Serial No", - fields=["purchase_rate", "name", "company"], - filters = {'name': ('in', serial_nos)}) + all_serial_nos = frappe.get_all( + "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)} + ) - incoming_values = sum(flt(d.purchase_rate) for d in all_serial_nos if d.company==sle.company) + incoming_values = sum(flt(d.purchase_rate) for d in all_serial_nos if d.company == sle.company) # Get rate for serial nos which has been transferred to other company - invalid_serial_nos = [d.name for d in all_serial_nos if d.company!=sle.company] + invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company] for serial_no in invalid_serial_nos: - incoming_rate = frappe.db.sql(""" + incoming_rate = frappe.db.sql( + """ select incoming_rate from `tabStock Ledger Entry` where @@ -671,7 +786,9 @@ class update_entries_after(object): ) order by posting_date desc limit 1 - """, (sle.company, serial_no, serial_no+'\n%', '%\n'+serial_no, '%\n'+serial_no+'\n%')) + """, + (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"), + ) incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0 @@ -685,15 +802,17 @@ class update_entries_after(object): if flt(self.wh_data.qty_after_transaction) <= 0: self.wh_data.valuation_rate = sle.incoming_rate else: - new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + \ - (actual_qty * sle.incoming_rate) + new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + ( + actual_qty * sle.incoming_rate + ) self.wh_data.valuation_rate = new_stock_value / new_stock_qty elif sle.outgoing_rate: if new_stock_qty: - new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + \ - (actual_qty * sle.outgoing_rate) + new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + ( + actual_qty * sle.outgoing_rate + ) self.wh_data.valuation_rate = new_stock_value / new_stock_qty else: @@ -708,7 +827,9 @@ class update_entries_after(object): # Get valuation rate from previous SLE or Item master, if item does not have the # allow zero valuration rate flag set 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) + 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.wh_data.valuation_rate = self.get_fallback_rate(sle) @@ -717,7 +838,9 @@ class update_entries_after(object): actual_qty = flt(sle.actual_qty) outgoing_rate = flt(sle.outgoing_rate) - self.wh_data.qty_after_transaction = round_off_if_near_zero(self.wh_data.qty_after_transaction + actual_qty) + self.wh_data.qty_after_transaction = round_off_if_near_zero( + self.wh_data.qty_after_transaction + actual_qty + ) if self.valuation_method == "LIFO": stock_queue = LIFOValuation(self.wh_data.stock_queue) @@ -729,24 +852,33 @@ class update_entries_after(object): if actual_qty > 0: stock_queue.add_stock(qty=actual_qty, rate=incoming_rate) else: + def rate_generator() -> float: - allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, 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: return self.get_fallback_rate(sle) else: return 0.0 - stock_queue.remove_stock(qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator) + stock_queue.remove_stock( + qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator + ) _qty, stock_value = stock_queue.get_total_stock_and_value() stock_value_difference = stock_value - prev_stock_value self.wh_data.stock_queue = stock_queue.state - self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + stock_value_difference) + self.wh_data.stock_value = round_off_if_near_zero( + self.wh_data.stock_value + stock_value_difference + ) 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]) + self.wh_data.stock_queue.append( + [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate] + ) if self.wh_data.qty_after_transaction: self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction @@ -755,14 +887,21 @@ class update_entries_after(object): incoming_rate = flt(sle.incoming_rate) actual_qty = flt(sle.actual_qty) - self.wh_data.qty_after_transaction = round_off_if_near_zero(self.wh_data.qty_after_transaction + actual_qty) + self.wh_data.qty_after_transaction = round_off_if_near_zero( + self.wh_data.qty_after_transaction + actual_qty + ) if actual_qty > 0: stock_value_difference = incoming_rate * actual_qty else: - outgoing_rate = get_batch_incoming_rate(item_code=sle.item_code, - warehouse=sle.warehouse, batch_no=sle.batch_no, posting_date=sle.posting_date, - posting_time=sle.posting_time, creation=sle.creation) + outgoing_rate = get_batch_incoming_rate( + item_code=sle.item_code, + warehouse=sle.warehouse, + batch_no=sle.batch_no, + posting_date=sle.posting_date, + posting_time=sle.posting_time, + creation=sle.creation, + ) if outgoing_rate is None: # This can *only* happen if qty available for the batch is zero. # in such case fall back various other rates. @@ -771,7 +910,9 @@ class update_entries_after(object): outgoing_rate = self.get_fallback_rate(sle) stock_value_difference = outgoing_rate * actual_qty - self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + stock_value_difference) + self.wh_data.stock_value = round_off_if_near_zero( + self.wh_data.stock_value + stock_value_difference + ) if self.wh_data.qty_after_transaction: self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction @@ -790,10 +931,17 @@ class update_entries_after(object): def get_fallback_rate(self, sle) -> float: """When exact incoming rate isn't available use any of other "average" rates as fallback. - This should only get used for negative stock.""" - return 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), company=sle.company, batch_no=sle.batch_no) + This should only get used for negative stock.""" + return 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), + company=sle.company, + batch_no=sle.batch_no, + ) def get_sle_before_datetime(self, args): """get previous stock ledger entry before current time-bucket""" @@ -810,18 +958,27 @@ class update_entries_after(object): for warehouse, exceptions in self.exceptions.items(): deficiency = min(e["diff"] for e in exceptions) - if ((exceptions[0]["voucher_type"], 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', exceptions[0]["item_code"]), - frappe.get_desk_link('Warehouse', warehouse)) + abs(deficiency), + frappe.get_desk_link("Item", exceptions[0]["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', exceptions[0]["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"])) + 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", exceptions[0]["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 msg: msg_list.append(msg) @@ -829,7 +986,7 @@ class update_entries_after(object): if msg_list: message = "\n\n".join(msg_list) if self.verbose: - frappe.throw(message, NegativeStockError, title=_('Insufficient Stock')) + frappe.throw(message, NegativeStockError, title=_("Insufficient Stock")) else: raise NegativeStockError(message) @@ -838,19 +995,16 @@ class update_entries_after(object): for warehouse, data in self.data.items(): bin_name = get_or_make_bin(self.item_code, warehouse) - updated_values = { - "actual_qty": data.qty_after_transaction, - "stock_value": data.stock_value - } + updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value} if data.valuation_rate is not None: updated_values["valuation_rate"] = data.valuation_rate - frappe.db.set_value('Bin', bin_name, updated_values) + frappe.db.set_value("Bin", bin_name, updated_values) def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): """get stock ledger entries filtered by specific posting datetime conditions""" - args['time_format'] = '%H:%i:%s' + args["time_format"] = "%H:%i:%s" if not args.get("posting_date"): args["posting_date"] = "1900-01-01" if not args.get("posting_time"): @@ -861,7 +1015,8 @@ def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): voucher_no = args.get("voucher_no") voucher_condition = f"and voucher_no != '{voucher_no}'" - sle = frappe.db.sql(""" + sle = frappe.db.sql( + """ select *, timestamp(posting_date, posting_time) as "timestamp" from `tabStock Ledger Entry` where item_code = %(item_code)s @@ -871,32 +1026,48 @@ def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) order by timestamp(posting_date, posting_time) desc, creation desc limit 1 - for update""".format(voucher_condition=voucher_condition), args, as_dict=1) + for update""".format( + voucher_condition=voucher_condition + ), + args, + as_dict=1, + ) return sle[0] if sle else frappe._dict() + def get_previous_sle(args, for_update=False): """ - get the last sle on or before the current time-bucket, - to get actual qty before transaction, this function - is called from various transaction like stock entry, reco etc + get the last sle on or before the current time-bucket, + to get actual qty before transaction, this function + is called from various transaction like stock entry, reco etc - args = { - "item_code": "ABC", - "warehouse": "XYZ", - "posting_date": "2012-12-12", - "posting_time": "12:00", - "sle": "name of reference Stock Ledger Entry" - } + args = { + "item_code": "ABC", + "warehouse": "XYZ", + "posting_date": "2012-12-12", + "posting_time": "12:00", + "sle": "name of reference Stock Ledger Entry" + } """ args["name"] = args.get("sle", None) or "" sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update) return sle and sle[0] or {} -def get_stock_ledger_entries(previous_sle, operator=None, - order="desc", limit=None, for_update=False, debug=False, check_serial_no=True): + +def get_stock_ledger_entries( + previous_sle, + operator=None, + order="desc", + limit=None, + for_update=False, + debug=False, + check_serial_no=True, +): """get stock ledger entries filtered by specific posting datetime conditions""" - conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(operator) + conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format( + operator + ) if previous_sle.get("warehouse"): conditions += " and warehouse = %(warehouse)s" elif previous_sle.get("warehouse_condition"): @@ -905,15 +1076,21 @@ def get_stock_ledger_entries(previous_sle, operator=None, if check_serial_no and previous_sle.get("serial_no"): # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no")))) serial_no = previous_sle.get("serial_no") - conditions += (""" and + conditions += ( + """ and ( serial_no = {0} or serial_no like {1} or serial_no like {2} or serial_no like {3} ) - """).format(frappe.db.escape(serial_no), frappe.db.escape('{}\n%'.format(serial_no)), - frappe.db.escape('%\n{}'.format(serial_no)), frappe.db.escape('%\n{}\n%'.format(serial_no))) + """ + ).format( + frappe.db.escape(serial_no), + frappe.db.escape("{}\n%".format(serial_no)), + frappe.db.escape("%\n{}".format(serial_no)), + frappe.db.escape("%\n{}\n%".format(serial_no)), + ) if not previous_sle.get("posting_date"): previous_sle["posting_date"] = "1900-01-01" @@ -923,70 +1100,95 @@ def get_stock_ledger_entries(previous_sle, operator=None, if operator in (">", "<=") and previous_sle.get("name"): conditions += " and name!=%(name)s" - return frappe.db.sql(""" + return frappe.db.sql( + """ 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""" % { + %(limit)s %(for_update)s""" + % { "conditions": conditions, "limit": limit or "", "for_update": for_update and "for update" or "", - "order": order - }, previous_sle, as_dict=1, debug=debug) + "order": order, + }, + previous_sle, + as_dict=1, + debug=debug, + ) + 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) + 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_batch_incoming_rate(item_code, warehouse, batch_no, posting_date, posting_time, creation=None): - Timestamp = CustomFunction('timestamp', ['date', 'time']) +def get_batch_incoming_rate( + item_code, warehouse, batch_no, posting_date, posting_time, creation=None +): + + Timestamp = CustomFunction("timestamp", ["date", "time"]) sle = frappe.qb.DocType("Stock Ledger Entry") - timestamp_condition = (Timestamp(sle.posting_date, sle.posting_time) < Timestamp(posting_date, posting_time)) + timestamp_condition = Timestamp(sle.posting_date, sle.posting_time) < Timestamp( + posting_date, posting_time + ) if creation: timestamp_condition |= ( - (Timestamp(sle.posting_date, sle.posting_time) == Timestamp(posting_date, posting_time)) - & (sle.creation < creation) - ) + Timestamp(sle.posting_date, sle.posting_time) == Timestamp(posting_date, posting_time) + ) & (sle.creation < creation) batch_details = ( - frappe.qb - .from_(sle) - .select( - Sum(sle.stock_value_difference).as_("batch_value"), - Sum(sle.actual_qty).as_("batch_qty") - ) - .where( - (sle.item_code == item_code) - & (sle.warehouse == warehouse) - & (sle.batch_no == batch_no) - & (sle.is_cancelled == 0) - ) - .where(timestamp_condition) + frappe.qb.from_(sle) + .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty")) + .where( + (sle.item_code == item_code) + & (sle.warehouse == warehouse) + & (sle.batch_no == batch_no) + & (sle.is_cancelled == 0) + ) + .where(timestamp_condition) ).run(as_dict=True) if batch_details and batch_details[0].batch_qty: return batch_details[0].batch_value / batch_details[0].batch_qty -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, batch_no=None): +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, + batch_no=None, +): if not company: - company = frappe.get_cached_value("Warehouse", warehouse, "company") + company = frappe.get_cached_value("Warehouse", warehouse, "company") last_valuation_rate = None # Get moving average rate of a specific batch number if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"): - last_valuation_rate = frappe.db.sql(""" + last_valuation_rate = frappe.db.sql( + """ select sum(stock_value_difference) / sum(actual_qty) from `tabStock Ledger Entry` where @@ -996,11 +1198,13 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, AND is_cancelled = 0 AND NOT (voucher_no = %s AND voucher_type = %s) """, - (item_code, warehouse, batch_no, voucher_no, voucher_type)) + (item_code, warehouse, batch_no, voucher_no, voucher_type), + ) # Get valuation rate from last sle for the same item and warehouse if not last_valuation_rate or last_valuation_rate[0][0] is None: - last_valuation_rate = frappe.db.sql("""select valuation_rate + last_valuation_rate = frappe.db.sql( + """select valuation_rate from `tabStock Ledger Entry` force index (item_warehouse) where item_code = %s @@ -1008,18 +1212,23 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, AND valuation_rate >= 0 AND is_cancelled = 0 AND NOT (voucher_no = %s AND voucher_type = %s) - order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type)) + order by posting_date desc, posting_time desc, name desc limit 1""", + (item_code, warehouse, voucher_no, voucher_type), + ) if not last_valuation_rate: # Get valuation rate from last sle for the item against any warehouse - last_valuation_rate = frappe.db.sql("""select valuation_rate + last_valuation_rate = frappe.db.sql( + """select valuation_rate from `tabStock Ledger Entry` force index (item_code) where item_code = %s AND valuation_rate > 0 AND is_cancelled = 0 AND NOT(voucher_no = %s AND voucher_type = %s) - order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, voucher_no, voucher_type)) + 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]) @@ -1034,18 +1243,36 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, if not valuation_rate: # try in price list - valuation_rate = frappe.db.get_value('Item Price', - dict(item_code=item_code, buying=1, currency=currency), - 'price_list_rate') + valuation_rate = frappe.db.get_value( + "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate" + ) - if not allow_zero_rate and not valuation_rate and raise_error_if_no_rate \ - and cint(erpnext.is_perpetual_inventory_enabled(company)): + if ( + not allow_zero_rate + and not valuation_rate + and raise_error_if_no_rate + and cint(erpnext.is_perpetual_inventory_enabled(company)) + ): form_link = get_link_to_form("Item", item_code) - message = _("Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}.").format(form_link, voucher_type, voucher_no) + message = _( + "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}." + ).format(form_link, voucher_type, voucher_no) message += "

    " + _("Here are the options to proceed:") - solutions = "
  • " + _("If the item is transacting as a Zero Valuation Rate item in this entry, please enable 'Allow Zero Valuation Rate' in the {0} Item table.").format(voucher_type) + "
  • " - solutions += "
  • " + _("If not, you can Cancel / Submit this entry") + " {0} ".format(frappe.bold("after")) + _("performing either one below:") + "
  • " + solutions = ( + "
  • " + + _( + "If the item is transacting as a Zero Valuation Rate item in this entry, please enable 'Allow Zero Valuation Rate' in the {0} Item table." + ).format(voucher_type) + + "
  • " + ) + solutions += ( + "
  • " + + _("If not, you can Cancel / Submit this entry") + + " {0} ".format(frappe.bold("after")) + + _("performing either one below:") + + "
  • " + ) sub_solutions = "
    • " + _("Create an incoming stock transaction for the Item.") + "
    • " sub_solutions += "
    • " + _("Mention Valuation Rate in the Item master.") + "
    " msg = message + solutions + sub_solutions + "" @@ -1054,6 +1281,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, return valuation_rate + def update_qty_in_future_sle(args, allow_negative_stock=False): """Recalculate Qty after Transaction in future SLEs based on current SLE.""" datetime_limit_condition = "" @@ -1070,7 +1298,8 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): # add condition to update SLEs before this date & time datetime_limit_condition = get_datetime_limit_condition(detail) - frappe.db.sql(""" + frappe.db.sql( + """ update `tabStock Ledger Entry` set qty_after_transaction = qty_after_transaction + {qty_shift} where @@ -1085,10 +1314,15 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): ) ) {datetime_limit_condition} - """.format(qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition), args) + """.format( + qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition + ), + args, + ) validate_negative_qty_in_future_sle(args, allow_negative_stock) + def get_stock_reco_qty_shift(args): stock_reco_qty_shift = 0 if args.get("is_cancelled"): @@ -1100,8 +1334,9 @@ def get_stock_reco_qty_shift(args): stock_reco_qty_shift = flt(args.actual_qty) else: # reco is being submitted - last_balance = get_previous_sle_of_current_voucher(args, - exclude_current_voucher=True).get("qty_after_transaction") + last_balance = get_previous_sle_of_current_voucher(args, exclude_current_voucher=True).get( + "qty_after_transaction" + ) if last_balance is not None: stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance) @@ -1110,10 +1345,12 @@ def get_stock_reco_qty_shift(args): return stock_reco_qty_shift + def get_next_stock_reco(args): """Returns next nearest stock reconciliaton's details.""" - return frappe.db.sql(""" + return frappe.db.sql( + """ select name, posting_date, posting_time, creation, voucher_no from @@ -1131,7 +1368,11 @@ def get_next_stock_reco(args): ) ) limit 1 - """, args, as_dict=1) + """, + args, + as_dict=1, + ) + def get_datetime_limit_condition(detail): return f""" @@ -1143,6 +1384,7 @@ def get_datetime_limit_condition(detail): ) )""" + def validate_negative_qty_in_future_sle(args, allow_negative_stock=False): if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code): return @@ -1151,32 +1393,40 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False): neg_sle = get_future_sle_with_negative_qty(args) if neg_sle: - message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( + message = _( + "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction." + ).format( abs(neg_sle[0]["qty_after_transaction"]), - frappe.get_desk_link('Item', args.item_code), - frappe.get_desk_link('Warehouse', args.warehouse), - neg_sle[0]["posting_date"], neg_sle[0]["posting_time"], - frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"])) - - frappe.throw(message, NegativeStockError, title=_('Insufficient Stock')) + frappe.get_desk_link("Item", args.item_code), + frappe.get_desk_link("Warehouse", args.warehouse), + neg_sle[0]["posting_date"], + neg_sle[0]["posting_time"], + frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]), + ) + frappe.throw(message, NegativeStockError, title=_("Insufficient Stock")) if not args.batch_no: return neg_batch_sle = get_future_sle_with_negative_batch_qty(args) if neg_batch_sle: - message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( + message = _( + "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction." + ).format( abs(neg_batch_sle[0]["cumulative_total"]), - frappe.get_desk_link('Batch', args.batch_no), - frappe.get_desk_link('Warehouse', args.warehouse), - neg_batch_sle[0]["posting_date"], neg_batch_sle[0]["posting_time"], - frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"])) + frappe.get_desk_link("Batch", args.batch_no), + frappe.get_desk_link("Warehouse", args.warehouse), + neg_batch_sle[0]["posting_date"], + neg_batch_sle[0]["posting_time"], + frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]), + ) frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch")) def get_future_sle_with_negative_qty(args): - return frappe.db.sql(""" + return frappe.db.sql( + """ select qty_after_transaction, posting_date, posting_time, voucher_type, voucher_no @@ -1190,11 +1440,15 @@ def get_future_sle_with_negative_qty(args): and qty_after_transaction < 0 order by timestamp(posting_date, posting_time) asc limit 1 - """, args, as_dict=1) + """, + args, + as_dict=1, + ) def get_future_sle_with_negative_batch_qty(args): - return frappe.db.sql(""" + return frappe.db.sql( + """ with batch_ledger as ( select posting_date, posting_time, voucher_type, voucher_no, @@ -1212,7 +1466,10 @@ def get_future_sle_with_negative_batch_qty(args): cumulative_total < 0.0 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s) limit 1 - """, args, as_dict=1) + """, + args, + as_dict=1, + ) def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool: diff --git a/erpnext/stock/tests/test_valuation.py b/erpnext/stock/tests/test_valuation.py index b64ff8e28c..506a666c28 100644 --- a/erpnext/stock/tests/test_valuation.py +++ b/erpnext/stock/tests/test_valuation.py @@ -16,7 +16,6 @@ stock_queue_generator = st.lists(st.tuples(qty_gen, value_gen), min_size=10) class TestFIFOValuation(unittest.TestCase): - def setUp(self): self.queue = FIFOValuation([]) @@ -29,7 +28,9 @@ class TestFIFOValuation(unittest.TestCase): self.assertAlmostEqual(sum(q for q, _ in self.queue), qty, msg=f"queue: {self.queue}", places=4) def assertTotalValue(self, value): - self.assertAlmostEqual(sum(q * r for q, r in self.queue), value, msg=f"queue: {self.queue}", places=2) + self.assertAlmostEqual( + sum(q * r for q, r in self.queue), value, msg=f"queue: {self.queue}", places=2 + ) def test_simple_addition(self): self.queue.add_stock(1, 10) @@ -55,7 +56,6 @@ class TestFIFOValuation(unittest.TestCase): self.queue.add_stock(6, 10) self.assertEqual(self.queue, [[1, 10]]) - def test_negative_stock(self): self.queue.remove_stock(1, 5) self.assertEqual(self.queue, [[-1, 5]]) @@ -75,7 +75,6 @@ class TestFIFOValuation(unittest.TestCase): self.queue.remove_stock(1, 20) self.assertEqual(self.queue, [[1, 10]]) - def test_remove_multiple_bins(self): self.queue.add_stock(1, 10) self.queue.add_stock(2, 20) @@ -85,7 +84,6 @@ class TestFIFOValuation(unittest.TestCase): self.queue.remove_stock(4) self.assertEqual(self.queue, [[5, 20]]) - def test_remove_multiple_bins_with_rate(self): self.queue.add_stock(1, 10) self.queue.add_stock(2, 20) @@ -143,7 +141,9 @@ class TestFIFOValuation(unittest.TestCase): else: qty = abs(qty) consumed = self.queue.remove_stock(qty) - self.assertAlmostEqual(qty, sum(q for q, _ in consumed), msg=f"incorrect consumption {consumed}") + self.assertAlmostEqual( + qty, sum(q for q, _ in consumed), msg=f"incorrect consumption {consumed}" + ) total_qty -= qty self.assertTotalQty(total_qty) @@ -164,7 +164,9 @@ class TestFIFOValuation(unittest.TestCase): else: qty = abs(qty) consumed = self.queue.remove_stock(qty) - self.assertAlmostEqual(qty, sum(q for q, _ in consumed), msg=f"incorrect consumption {consumed}") + self.assertAlmostEqual( + qty, sum(q for q, _ in consumed), msg=f"incorrect consumption {consumed}" + ) total_qty -= qty total_value -= sum(q * r for q, r in consumed) self.assertTotalQty(total_qty) @@ -172,7 +174,6 @@ class TestFIFOValuation(unittest.TestCase): class TestLIFOValuation(unittest.TestCase): - def setUp(self): self.stack = LIFOValuation([]) @@ -185,7 +186,9 @@ class TestLIFOValuation(unittest.TestCase): self.assertAlmostEqual(sum(q for q, _ in self.stack), qty, msg=f"stack: {self.stack}", places=4) def assertTotalValue(self, value): - self.assertAlmostEqual(sum(q * r for q, r in self.stack), value, msg=f"stack: {self.stack}", places=2) + self.assertAlmostEqual( + sum(q * r for q, r in self.stack), value, msg=f"stack: {self.stack}", places=2 + ) def test_simple_addition(self): self.stack.add_stock(1, 10) @@ -248,7 +251,6 @@ class TestLIFOValuation(unittest.TestCase): consumed = self.stack.remove_stock(5) self.assertEqual(consumed, [[5, 5]]) - @given(stock_queue_generator) def test_lifo_qty_hypothesis(self, stock_stack): self.stack = LIFOValuation([]) @@ -263,7 +265,9 @@ class TestLIFOValuation(unittest.TestCase): else: qty = abs(qty) consumed = self.stack.remove_stock(qty) - self.assertAlmostEqual(qty, sum(q for q, _ in consumed), msg=f"incorrect consumption {consumed}") + self.assertAlmostEqual( + qty, sum(q for q, _ in consumed), msg=f"incorrect consumption {consumed}" + ) total_qty -= qty self.assertTotalQty(total_qty) @@ -284,12 +288,15 @@ class TestLIFOValuation(unittest.TestCase): else: qty = abs(qty) consumed = self.stack.remove_stock(qty) - self.assertAlmostEqual(qty, sum(q for q, _ in consumed), msg=f"incorrect consumption {consumed}") + self.assertAlmostEqual( + qty, sum(q for q, _ in consumed), msg=f"incorrect consumption {consumed}" + ) total_qty -= qty total_value -= sum(q * r for q, r in consumed) self.assertTotalQty(total_qty) self.assertTotalValue(total_value) + class TestLIFOValuationSLE(FrappeTestCase): ITEM_CODE = "_Test LIFO item" WAREHOUSE = "_Test Warehouse - _TC" @@ -309,7 +316,9 @@ class TestLIFOValuationSLE(FrappeTestCase): return make_stock_entry(**kwargs) def assertStockQueue(self, se, expected_queue): - sle_name = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": se.name, "is_cancelled": 0, "voucher_type": "Stock Entry"}) + sle_name = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": se.name, "is_cancelled": 0, "voucher_type": "Stock Entry"} + ) sle = frappe.get_doc("Stock Ledger Entry", sle_name) stock_queue = json.loads(sle.stock_queue) @@ -321,7 +330,6 @@ class TestLIFOValuationSLE(FrappeTestCase): if total_qty > 0: self.assertEqual(stock_queue, expected_queue) - def test_lifo_values(self): in1 = self._make_stock_entry(1, 1) @@ -340,7 +348,7 @@ class TestLIFOValuationSLE(FrappeTestCase): self.assertStockQueue(out2, [[1, 1]]) in4 = self._make_stock_entry(4, 4) - self.assertStockQueue(in4, [[1, 1], [4,4]]) + self.assertStockQueue(in4, [[1, 1], [4, 4]]) out3 = self._make_stock_entry(-5) self.assertStockQueue(out3, []) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index e20538928e..741646dfff 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -12,8 +12,13 @@ import erpnext from erpnext.stock.valuation import FIFOValuation, LIFOValuation -class InvalidWarehouseCompany(frappe.ValidationError): pass -class PendingRepostingError(frappe.ValidationError): pass +class InvalidWarehouseCompany(frappe.ValidationError): + pass + + +class PendingRepostingError(frappe.ValidationError): + pass + def get_stock_value_from_bin(warehouse=None, item_code=None): values = {} @@ -26,22 +31,27 @@ def get_stock_value_from_bin(warehouse=None, item_code=None): and w2.lft between w1.lft and w1.rgt ) """ - values['warehouse'] = warehouse + values["warehouse"] = warehouse if item_code: conditions += " and `tabBin`.item_code = %(item_code)s" - values['item_code'] = item_code + values["item_code"] = item_code - query = """select sum(stock_value) from `tabBin`, `tabItem` where 1 = 1 - and `tabItem`.name = `tabBin`.item_code and ifnull(`tabItem`.disabled, 0) = 0 %s""" % conditions + query = ( + """select sum(stock_value) from `tabBin`, `tabItem` where 1 = 1 + and `tabItem`.name = `tabBin`.item_code and ifnull(`tabItem`.disabled, 0) = 0 %s""" + % conditions + ) stock_value = frappe.db.sql(query, values) return stock_value + def get_stock_value_on(warehouse=None, posting_date=None, item_code=None): - if not posting_date: posting_date = nowdate() + if not posting_date: + posting_date = nowdate() values, condition = [posting_date], "" @@ -63,13 +73,19 @@ def get_stock_value_on(warehouse=None, posting_date=None, item_code=None): values.append(item_code) condition += " AND item_code = %s" - stock_ledger_entries = frappe.db.sql(""" + stock_ledger_entries = frappe.db.sql( + """ 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) + """.format( + condition + ), + values, + as_dict=1, + ) sle_map = {} for sle in stock_ledger_entries: @@ -78,23 +94,32 @@ def get_stock_value_on(warehouse=None, posting_date=None, item_code=None): return sum(sle_map.values()) + @frappe.whitelist() -def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None, - with_valuation_rate=False, with_serial_no=False): +def get_stock_balance( + item_code, + warehouse, + posting_date=None, + posting_time=None, + with_valuation_rate=False, + with_serial_no=False, +): """Returns stock balance quantity at given warehouse on given posting date or current date. If `with_valuation_rate` is True, will return tuple (qty, rate)""" from erpnext.stock.stock_ledger import get_previous_sle - if posting_date is None: posting_date = nowdate() - if posting_time is None: posting_time = nowtime() + if posting_date is None: + posting_date = nowdate() + if posting_time is None: + posting_time = nowtime() args = { "item_code": item_code, - "warehouse":warehouse, + "warehouse": warehouse, "posting_date": posting_date, - "posting_time": posting_time + "posting_time": posting_time, } last_entry = get_previous_sle(args) @@ -103,33 +128,41 @@ def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None if with_serial_no: serial_nos = get_serial_nos_data_after_transactions(args) - return ((last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos) - if last_entry else (0.0, 0.0, None)) + return ( + (last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos) + if last_entry + else (0.0, 0.0, None) + ) else: - return (last_entry.qty_after_transaction, last_entry.valuation_rate) if last_entry else (0.0, 0.0) + return ( + (last_entry.qty_after_transaction, last_entry.valuation_rate) if last_entry else (0.0, 0.0) + ) else: return last_entry.qty_after_transaction if last_entry else 0.0 + def get_serial_nos_data_after_transactions(args): from pypika import CustomFunction serial_nos = set() args = frappe._dict(args) - sle = frappe.qb.DocType('Stock Ledger Entry') - Timestamp = CustomFunction('timestamp', ['date', 'time']) + sle = frappe.qb.DocType("Stock Ledger Entry") + Timestamp = CustomFunction("timestamp", ["date", "time"]) - stock_ledger_entries = frappe.qb.from_( - sle - ).select( - 'serial_no','actual_qty' - ).where( - (sle.item_code == args.item_code) - & (sle.warehouse == args.warehouse) - & (Timestamp(sle.posting_date, sle.posting_time) < Timestamp(args.posting_date, args.posting_time)) - & (sle.is_cancelled == 0) - ).orderby( - sle.posting_date, sle.posting_time, sle.creation - ).run(as_dict=1) + stock_ledger_entries = ( + frappe.qb.from_(sle) + .select("serial_no", "actual_qty") + .where( + (sle.item_code == args.item_code) + & (sle.warehouse == args.warehouse) + & ( + Timestamp(sle.posting_date, sle.posting_time) < Timestamp(args.posting_date, args.posting_time) + ) + & (sle.is_cancelled == 0) + ) + .orderby(sle.posting_date, sle.posting_time, sle.creation) + .run(as_dict=1) + ) for stock_ledger_entry in stock_ledger_entries: changed_serial_no = get_serial_nos_data(stock_ledger_entry.serial_no) @@ -138,12 +171,15 @@ def get_serial_nos_data_after_transactions(args): else: serial_nos.difference_update(changed_serial_no) - return '\n'.join(serial_nos) + return "\n".join(serial_nos) + def get_serial_nos_data(serial_nos): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + return get_serial_nos(serial_nos) + @frappe.whitelist() def get_latest_stock_qty(item_code, warehouse=None): values, condition = [item_code], "" @@ -160,37 +196,48 @@ def get_latest_stock_qty(item_code, warehouse=None): values.append(warehouse) condition += " AND warehouse = %s" - actual_qty = frappe.db.sql("""select sum(actual_qty) from tabBin - where item_code=%s {0}""".format(condition), values)[0][0] + actual_qty = frappe.db.sql( + """select sum(actual_qty) from tabBin + where item_code=%s {0}""".format( + condition + ), + values, + )[0][0] return actual_qty def get_latest_stock_balance(): bin_map = {} - for d in frappe.db.sql("""SELECT item_code, warehouse, stock_value as stock_value - FROM tabBin""", as_dict=1): - bin_map.setdefault(d.warehouse, {}).setdefault(d.item_code, flt(d.stock_value)) + for d in frappe.db.sql( + """SELECT item_code, warehouse, stock_value as stock_value + FROM tabBin""", + as_dict=1, + ): + bin_map.setdefault(d.warehouse, {}).setdefault(d.item_code, flt(d.stock_value)) return bin_map + def get_bin(item_code, warehouse): bin = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}) if not bin: bin_obj = _create_bin(item_code, warehouse) else: - bin_obj = frappe.get_doc('Bin', bin, for_update=True) + bin_obj = frappe.get_doc("Bin", bin, for_update=True) bin_obj.flags.ignore_permissions = True return bin_obj -def get_or_make_bin(item_code: str , warehouse: str) -> str: - bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse}) + +def get_or_make_bin(item_code: str, warehouse: str) -> str: + bin_record = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}) if not bin_record: bin_obj = _create_bin(item_code, warehouse) bin_record = bin_obj.name return bin_record + def _create_bin(item_code, warehouse): """Create a bin and take care of concurrent inserts.""" @@ -206,6 +253,7 @@ def _create_bin(item_code, warehouse): return bin_obj + @frappe.whitelist() def get_incoming_rate(args, raise_error_if_no_rate=True): """Get Incoming Rate based on valuation method""" @@ -214,19 +262,21 @@ def get_incoming_rate(args, raise_error_if_no_rate=True): get_previous_sle, get_valuation_rate, ) + if isinstance(args, str): args = json.loads(args) - voucher_no = args.get('voucher_no') or args.get('name') + voucher_no = args.get("voucher_no") or args.get("name") in_rate = None if (args.get("serial_no") or "").strip(): in_rate = get_avg_purchase_rate(args.get("serial_no")) - elif args.get("batch_no") and \ - frappe.db.get_value("Batch", args.get("batch_no"), "use_batchwise_valuation", cache=True): + elif args.get("batch_no") and frappe.db.get_value( + "Batch", args.get("batch_no"), "use_batchwise_valuation", cache=True + ): in_rate = get_batch_incoming_rate( - item_code=args.get('item_code'), - warehouse=args.get('warehouse'), + item_code=args.get("item_code"), + warehouse=args.get("warehouse"), batch_no=args.get("batch_no"), posting_date=args.get("posting_date"), posting_time=args.get("posting_time"), @@ -234,40 +284,62 @@ def get_incoming_rate(args, raise_error_if_no_rate=True): else: valuation_method = get_valuation_method(args.get("item_code")) previous_sle = get_previous_sle(args) - if valuation_method in ('FIFO', 'LIFO'): + if valuation_method in ("FIFO", "LIFO"): if previous_sle: - previous_stock_queue = json.loads(previous_sle.get('stock_queue', '[]') or '[]') - in_rate = _get_fifo_lifo_rate(previous_stock_queue, args.get("qty") or 0, valuation_method) if previous_stock_queue else 0 - elif valuation_method == 'Moving Average': - in_rate = previous_sle.get('valuation_rate') or 0 + previous_stock_queue = json.loads(previous_sle.get("stock_queue", "[]") or "[]") + in_rate = ( + _get_fifo_lifo_rate(previous_stock_queue, args.get("qty") or 0, valuation_method) + if previous_stock_queue + else 0 + ) + elif valuation_method == "Moving Average": + in_rate = previous_sle.get("valuation_rate") or 0 if in_rate is None: - in_rate = get_valuation_rate(args.get('item_code'), args.get('warehouse'), - args.get('voucher_type'), voucher_no, args.get('allow_zero_valuation'), - currency=erpnext.get_company_currency(args.get('company')), company=args.get('company'), - raise_error_if_no_rate=raise_error_if_no_rate, batch_no=args.get("batch_no")) + in_rate = get_valuation_rate( + args.get("item_code"), + args.get("warehouse"), + args.get("voucher_type"), + voucher_no, + args.get("allow_zero_valuation"), + currency=erpnext.get_company_currency(args.get("company")), + company=args.get("company"), + raise_error_if_no_rate=raise_error_if_no_rate, + batch_no=args.get("batch_no"), + ) return flt(in_rate) + def get_avg_purchase_rate(serial_nos): """get average value of serial numbers""" serial_nos = get_valid_serial_nos(serial_nos) - return flt(frappe.db.sql("""select avg(purchase_rate) from `tabSerial No` - where name in (%s)""" % ", ".join(["%s"] * len(serial_nos)), - tuple(serial_nos))[0][0]) + return flt( + frappe.db.sql( + """select avg(purchase_rate) from `tabSerial No` + where name in (%s)""" + % ", ".join(["%s"] * len(serial_nos)), + tuple(serial_nos), + )[0][0] + ) + def get_valuation_method(item_code): """get valuation method from item or default""" - val_method = frappe.db.get_value('Item', item_code, 'valuation_method', cache=True) + val_method = frappe.db.get_value("Item", item_code, "valuation_method", cache=True) if not val_method: - val_method = frappe.db.get_value("Stock Settings", None, "valuation_method", cache=True) or "FIFO" + val_method = ( + frappe.db.get_value("Stock Settings", None, "valuation_method", cache=True) or "FIFO" + ) return val_method + def get_fifo_rate(previous_stock_queue, qty): """get FIFO (average) Rate from Queue""" return _get_fifo_lifo_rate(previous_stock_queue, qty, "FIFO") + def get_lifo_rate(previous_stock_queue, qty): """get LIFO (average) Rate from Queue""" return _get_fifo_lifo_rate(previous_stock_queue, qty, "LIFO") @@ -286,10 +358,11 @@ def _get_fifo_lifo_rate(previous_stock_queue, qty, method): total_qty, total_value = ValuationKlass(popped_bins).get_total_stock_and_value() return total_value / total_qty if total_qty else 0.0 -def get_valid_serial_nos(sr_nos, qty=0, item_code=''): + +def get_valid_serial_nos(sr_nos, qty=0, item_code=""): """split serial nos, validate and return list of valid serial nos""" # TODO: remove duplicates in client side - serial_nos = cstr(sr_nos).strip().replace(',', '\n').split('\n') + serial_nos = cstr(sr_nos).strip().replace(",", "\n").split("\n") valid_serial_nos = [] for val in serial_nos: @@ -305,19 +378,29 @@ def get_valid_serial_nos(sr_nos, qty=0, item_code=''): return valid_serial_nos + def validate_warehouse_company(warehouse, company): warehouse_company = frappe.db.get_value("Warehouse", warehouse, "company", cache=True) if warehouse_company and warehouse_company != company: - frappe.throw(_("Warehouse {0} does not belong to company {1}").format(warehouse, company), - InvalidWarehouseCompany) + frappe.throw( + _("Warehouse {0} does not belong to company {1}").format(warehouse, company), + InvalidWarehouseCompany, + ) + def is_group_warehouse(warehouse): if frappe.db.get_value("Warehouse", warehouse, "is_group", cache=True): frappe.throw(_("Group node warehouse is not allowed to select for transactions")) + def validate_disabled_warehouse(warehouse): if frappe.db.get_value("Warehouse", warehouse, "disabled", cache=True): - frappe.throw(_("Disabled Warehouse {0} cannot be used for this transaction.").format(get_link_to_form('Warehouse', warehouse))) + frappe.throw( + _("Disabled Warehouse {0} cannot be used for this transaction.").format( + get_link_to_form("Warehouse", warehouse) + ) + ) + def update_included_uom_in_report(columns, result, include_uom, conversion_factors): if not include_uom or not conversion_factors: @@ -335,11 +418,14 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto convertible_columns.setdefault(key, d.get("convertible")) # Add new column to show qty/rate as per the selected UOM - columns.insert(idx+1, { - 'label': "{0} (per {1})".format(d.get("label"), include_uom), - 'fieldname': "{0}_{1}".format(d.get("fieldname"), frappe.scrub(include_uom)), - 'fieldtype': 'Currency' if d.get("convertible") == 'rate' else 'Float' - }) + columns.insert( + idx + 1, + { + "label": "{0} (per {1})".format(d.get("label"), include_uom), + "fieldname": "{0}_{1}".format(d.get("fieldname"), frappe.scrub(include_uom)), + "fieldtype": "Currency" if d.get("convertible") == "rate" else "Float", + }, + ) update_dict_values = [] for row_idx, row in enumerate(result): @@ -351,13 +437,13 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto if not conversion_factors[row_idx]: conversion_factors[row_idx] = 1 - if convertible_columns.get(key) == 'rate': + if convertible_columns.get(key) == "rate": new_value = flt(value) * conversion_factors[row_idx] else: new_value = flt(value) / conversion_factors[row_idx] if not is_dict_obj: - row.insert(key+1, new_value) + row.insert(key + 1, new_value) else: new_key = "{0}_{1}".format(key, frappe.scrub(include_uom)) update_dict_values.append([row, new_key, new_value]) @@ -366,11 +452,17 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto row, key, value = data row[key] = value + def get_available_serial_nos(args): - return frappe.db.sql(""" SELECT name from `tabSerial No` + return frappe.db.sql( + """ SELECT name from `tabSerial No` WHERE item_code = %(item_code)s and warehouse = %(warehouse)s and timestamp(purchase_date, purchase_time) <= timestamp(%(posting_date)s, %(posting_time)s) - """, args, as_dict=1) + """, + args, + as_dict=1, + ) + def add_additional_uom_columns(columns, result, include_uom, conversion_factors): if not include_uom or not conversion_factors: @@ -379,70 +471,80 @@ def add_additional_uom_columns(columns, result, include_uom, conversion_factors) convertible_column_map = {} for col_idx in list(reversed(range(0, len(columns)))): col = columns[col_idx] - if isinstance(col, dict) and col.get('convertible') in ['rate', 'qty']: + if isinstance(col, dict) and col.get("convertible") in ["rate", "qty"]: next_col = col_idx + 1 columns.insert(next_col, col.copy()) - columns[next_col]['fieldname'] += '_alt' - convertible_column_map[col.get('fieldname')] = frappe._dict({ - 'converted_col': columns[next_col]['fieldname'], - 'for_type': col.get('convertible') - }) - if col.get('convertible') == 'rate': - columns[next_col]['label'] += ' (per {})'.format(include_uom) + columns[next_col]["fieldname"] += "_alt" + convertible_column_map[col.get("fieldname")] = frappe._dict( + {"converted_col": columns[next_col]["fieldname"], "for_type": col.get("convertible")} + ) + if col.get("convertible") == "rate": + columns[next_col]["label"] += " (per {})".format(include_uom) else: - columns[next_col]['label'] += ' ({})'.format(include_uom) + columns[next_col]["label"] += " ({})".format(include_uom) for row_idx, row in enumerate(result): for convertible_col, data in convertible_column_map.items(): - conversion_factor = conversion_factors[row.get('item_code')] or 1 + conversion_factor = conversion_factors[row.get("item_code")] or 1 for_type = data.for_type value_before_conversion = row.get(convertible_col) - if for_type == 'rate': + if for_type == "rate": row[data.converted_col] = flt(value_before_conversion) * conversion_factor else: row[data.converted_col] = flt(value_before_conversion) / conversion_factor result[row_idx] = row + def get_incoming_outgoing_rate_for_cancel(item_code, voucher_type, voucher_no, voucher_detail_no): - outgoing_rate = frappe.db.sql("""SELECT abs(stock_value_difference / actual_qty) + outgoing_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 and voucher_detail_no = %s ORDER BY CREATION DESC limit 1""", - (voucher_type, voucher_no, item_code, voucher_detail_no)) + (voucher_type, voucher_no, item_code, voucher_detail_no), + ) outgoing_rate = outgoing_rate[0][0] if outgoing_rate else 0.0 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']]}) + 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) + frappe.msgprint( + _("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1 + ) + def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool: """Check if there are pending reposting job till the specified posting date.""" filters = { "docstatus": 1, - "status": ["in", ["Queued","In Progress", "Failed"]], + "status": ["in", ["Queued", "In Progress", "Failed"]], "posting_date": ["<=", posting_date], } - reposting_pending = frappe.db.exists("Repost Item Valuation", filters) + reposting_pending = frappe.db.exists("Repost Item Valuation", filters) if reposting_pending and throw_error: - msg = _("Stock/Accounts can not be frozen as processing of backdated entries is going on. Please try again later.") - frappe.msgprint(msg, - raise_exception=PendingRepostingError, - title="Stock Reposting Ongoing", - indicator="red", - primary_action={ - "label": _("Show pending entries"), - "client_action": "erpnext.route_to_pending_reposts", - "args": filters, - } - ) + msg = _( + "Stock/Accounts can not be frozen as processing of backdated entries is going on. Please try again later." + ) + frappe.msgprint( + msg, + raise_exception=PendingRepostingError, + title="Stock Reposting Ongoing", + indicator="red", + primary_action={ + "label": _("Show pending entries"), + "client_action": "erpnext.route_to_pending_reposts", + "args": filters, + }, + ) return bool(reposting_pending) diff --git a/erpnext/stock/valuation.py b/erpnext/stock/valuation.py index e2bd1ad4df..648b218287 100644 --- a/erpnext/stock/valuation.py +++ b/erpnext/stock/valuation.py @@ -11,7 +11,6 @@ RATE = 1 class BinWiseValuation(ABC): - @abstractmethod def add_stock(self, qty: float, rate: float) -> None: pass @@ -61,7 +60,9 @@ class FIFOValuation(BinWiseValuation): # specifying the attributes to save resources # ref: https://docs.python.org/3/reference/datamodel.html#slots - __slots__ = ["queue",] + __slots__ = [ + "queue", + ] def __init__(self, state: Optional[List[StockBin]]): self.queue: List[StockBin] = state if state is not None else [] @@ -74,9 +75,9 @@ class FIFOValuation(BinWiseValuation): def add_stock(self, qty: float, rate: float) -> None: """Update fifo queue with new stock. - args: - qty: new quantity to add - rate: incoming rate of new quantity""" + args: + qty: new quantity to add + rate: incoming rate of new quantity""" if not len(self.queue): self.queue.append([0, 0]) @@ -101,12 +102,12 @@ class FIFOValuation(BinWiseValuation): """Remove stock from the queue and return popped bins. args: - qty: quantity to remove - rate: outgoing rate - rate_generator: function to be called if queue is not found and rate is required. + qty: quantity to remove + rate: outgoing rate + rate_generator: function to be called if queue is not found and rate is required. """ if not rate_generator: - rate_generator = lambda : 0.0 # noqa + rate_generator = lambda: 0.0 # noqa consumed_bins = [] while qty: @@ -126,7 +127,9 @@ class FIFOValuation(BinWiseValuation): if index is None: # nosemgrep new_stock_value = sum(d[QTY] * d[RATE] for d in self.queue) - qty * outgoing_rate new_stock_qty = sum(d[QTY] for d in self.queue) - qty - self.queue = [[new_stock_qty, new_stock_value / new_stock_qty if new_stock_qty > 0 else outgoing_rate]] + self.queue = [ + [new_stock_qty, new_stock_value / new_stock_qty if new_stock_qty > 0 else outgoing_rate] + ] consumed_bins.append([qty, outgoing_rate]) break else: @@ -169,7 +172,9 @@ class LIFOValuation(BinWiseValuation): # specifying the attributes to save resources # ref: https://docs.python.org/3/reference/datamodel.html#slots - __slots__ = ["stack",] + __slots__ = [ + "stack", + ] def __init__(self, state: Optional[List[StockBin]]): self.stack: List[StockBin] = state if state is not None else [] @@ -182,11 +187,11 @@ class LIFOValuation(BinWiseValuation): def add_stock(self, qty: float, rate: float) -> None: """Update lifo stack with new stock. - args: - qty: new quantity to add - rate: incoming rate of new quantity. + args: + qty: new quantity to add + rate: incoming rate of new quantity. - Behaviour of this is same as FIFO valuation. + Behaviour of this is same as FIFO valuation. """ if not len(self.stack): self.stack.append([0, 0]) @@ -205,19 +210,18 @@ class LIFOValuation(BinWiseValuation): else: # new balance qty is still negative, maintain same rate self.stack[-1][QTY] = qty - def remove_stock( self, qty: float, outgoing_rate: float = 0.0, rate_generator: Callable[[], float] = None ) -> List[StockBin]: """Remove stock from the stack and return popped bins. args: - qty: quantity to remove - rate: outgoing rate - ignored. Kept for backwards compatibility. - rate_generator: function to be called if stack is not found and rate is required. + qty: quantity to remove + rate: outgoing rate - ignored. Kept for backwards compatibility. + rate_generator: function to be called if stack is not found and rate is required. """ if not rate_generator: - rate_generator = lambda : 0.0 # noqa + rate_generator = lambda: 0.0 # noqa consumed_bins = [] while qty: @@ -254,7 +258,7 @@ def round_off_if_near_zero(number: float, precision: int = 7) -> float: """Rounds off the number to zero only if number is close to zero for decimal specified in precision. Precision defaults to 7. """ - if abs(0.0 - flt(number)) < (1.0 / (10 ** precision)): + if abs(0.0 - flt(number)) < (1.0 / (10**precision)): return 0.0 return flt(number) diff --git a/erpnext/support/__init__.py b/erpnext/support/__init__.py index ac23ede49e..7b6845d2fd 100644 --- a/erpnext/support/__init__.py +++ b/erpnext/support/__init__.py @@ -1,5 +1,5 @@ install_docs = [ - {'doctype':'Role', 'role_name':'Support Team', 'name':'Support Team'}, - {'doctype':'Role', 'role_name':'Maintenance User', 'name':'Maintenance User'}, - {'doctype':'Role', 'role_name':'Maintenance Manager', 'name':'Maintenance Manager'} + {"doctype": "Role", "role_name": "Support Team", "name": "Support Team"}, + {"doctype": "Role", "role_name": "Maintenance User", "name": "Maintenance User"}, + {"doctype": "Role", "role_name": "Maintenance Manager", "name": "Maintenance Manager"}, ] diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index e211e24c40..08a06b19b4 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -50,23 +50,26 @@ class Issue(Document): self.customer = contact.get_link_for("Customer") if not self.company: - self.company = frappe.db.get_value("Lead", self.lead, "company") or \ - frappe.db.get_default("Company") + self.company = frappe.db.get_value("Lead", self.lead, "company") or frappe.db.get_default( + "Company" + ) def create_communication(self): communication = frappe.new_doc("Communication") - communication.update({ - "communication_type": "Communication", - "communication_medium": "Email", - "sent_or_received": "Received", - "email_status": "Open", - "subject": self.subject, - "sender": self.raised_by, - "content": self.description, - "status": "Linked", - "reference_doctype": "Issue", - "reference_name": self.name - }) + communication.update( + { + "communication_type": "Communication", + "communication_medium": "Email", + "sent_or_received": "Received", + "email_status": "Open", + "subject": self.subject, + "sender": self.raised_by, + "content": self.description, + "status": "Linked", + "reference_doctype": "Issue", + "reference_name": self.name, + } + ) communication.ignore_permissions = True communication.ignore_mandatory = True communication.save() @@ -97,23 +100,31 @@ class Issue(Document): # Replicate linked Communications # TODO: get all communications in timeline before this, and modify them to append them to new doc comm_to_split_from = frappe.get_doc("Communication", communication_id) - communications = frappe.get_all("Communication", - filters={"reference_doctype": "Issue", + communications = frappe.get_all( + "Communication", + filters={ + "reference_doctype": "Issue", "reference_name": comm_to_split_from.reference_name, - "creation": (">=", comm_to_split_from.creation)}) + "creation": (">=", comm_to_split_from.creation), + }, + ) for communication in communications: doc = frappe.get_doc("Communication", communication.name) doc.reference_name = replicated_issue.name doc.save(ignore_permissions=True) - frappe.get_doc({ - "doctype": "Comment", - "comment_type": "Info", - "reference_doctype": "Issue", - "reference_name": replicated_issue.name, - "content": " - Split the Issue from {1}".format(self.name, frappe.bold(self.name)), - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Comment", + "comment_type": "Info", + "reference_doctype": "Issue", + "reference_name": replicated_issue.name, + "content": " - Split the Issue from {1}".format( + self.name, frappe.bold(self.name) + ), + } + ).insert(ignore_permissions=True) return replicated_issue.name @@ -121,6 +132,7 @@ class Issue(Document): self.db_set("resolution_time", None) self.db_set("user_resolution_time", None) + def get_list_context(context=None): return { "title": _("Issues"), @@ -128,7 +140,7 @@ def get_list_context(context=None): "row_template": "templates/includes/issue_row.html", "show_sidebar": True, "show_search": True, - "no_breadcrumbs": True + "no_breadcrumbs": True, } @@ -145,7 +157,8 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord ignore_permissions = False if is_website_user(): - if not filters: filters = {} + if not filters: + filters = {} if customer: filters["customer"] = customer @@ -154,7 +167,9 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord ignore_permissions = True - return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions) + return get_list( + doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions + ) @frappe.whitelist() @@ -163,16 +178,24 @@ def set_multiple_status(names, status): for name in json.loads(names): frappe.db.set_value("Issue", name, "status", status) + @frappe.whitelist() def set_status(name, status): frappe.db.set_value("Issue", name, "status", status) + def auto_close_tickets(): """Auto-close replied support tickets after 7 days""" - auto_close_after_days = frappe.db.get_value("Support Settings", "Support Settings", "close_issue_after_days") or 7 + auto_close_after_days = ( + frappe.db.get_value("Support Settings", "Support Settings", "close_issue_after_days") or 7 + ) - issues = frappe.db.sql(""" select name from tabIssue where status='Replied' and - modified 1 pm, Response Time -> 4 hrs, Resolution Time -> 6 hrs frappe.flags.current_time = get_datetime("2021-11-01 13:00") - issue = make_issue(frappe.flags.current_time, index=1, issue_type='Critical') # Applies 24hr working time SLA + issue = make_issue( + frappe.flags.current_time, index=1, issue_type="Critical" + ) # Applies 24hr working time SLA create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) - self.assertEquals(issue.agreement_status, 'First Response Due') + self.assertEquals(issue.agreement_status, "First Response Due") self.assertEquals(issue.response_by, get_datetime("2021-11-01 17:00")) self.assertEquals(issue.resolution_by, get_datetime("2021-11-01 19:00")) @@ -158,9 +162,9 @@ class TestIssue(TestSetUp): frappe.flags.current_time = get_datetime("2021-11-01 14:00") create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time) issue.reload() - issue.status = 'Replied' + issue.status = "Replied" issue.save() - self.assertEquals(issue.agreement_status, 'Resolution Due') + self.assertEquals(issue.agreement_status, "Resolution Due") self.assertEquals(issue.on_hold_since, frappe.flags.current_time) self.assertEquals(issue.first_responded_on, frappe.flags.current_time) @@ -168,7 +172,7 @@ class TestIssue(TestSetUp): frappe.flags.current_time = get_datetime("2021-11-01 15:00") create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) issue.reload() - self.assertEquals(issue.status, 'Open') + self.assertEquals(issue.status, "Open") # Hold Time + 1 Hrs self.assertEquals(issue.total_hold_time, 3600) # Resolution By should increase by one hrs @@ -178,19 +182,19 @@ class TestIssue(TestSetUp): frappe.flags.current_time = get_datetime("2021-11-01 16:00") create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time) issue.reload() - issue.status = 'Replied' + issue.status = "Replied" issue.save() - self.assertEquals(issue.agreement_status, 'Resolution Due') + self.assertEquals(issue.agreement_status, "Resolution Due") # Customer Closed → 10 pm frappe.flags.current_time = get_datetime("2021-11-01 22:00") - issue.status = 'Closed' + issue.status = "Closed" issue.save() # Hold Time + 6 Hrs self.assertEquals(issue.total_hold_time, 3600 + 21600) # Resolution By should increase by 6 hrs self.assertEquals(issue.resolution_by, get_datetime("2021-11-02 02:00")) - self.assertEquals(issue.agreement_status, 'Fulfilled') + self.assertEquals(issue.agreement_status, "Fulfilled") self.assertEquals(issue.resolution_date, frappe.flags.current_time) # Customer Open → 3 am i.e after resolution by is crossed @@ -201,15 +205,15 @@ class TestIssue(TestSetUp): self.assertEquals(issue.total_hold_time, 3600 + 21600 + 18000) # Resolution By should increase by 5 hrs self.assertEquals(issue.resolution_by, get_datetime("2021-11-02 07:00")) - self.assertEquals(issue.agreement_status, 'Resolution Due') + self.assertEquals(issue.agreement_status, "Resolution Due") self.assertFalse(issue.resolution_date) # We Closed → 4 am, SLA should be Fulfilled frappe.flags.current_time = get_datetime("2021-11-02 04:00") - issue.status = 'Closed' + issue.status = "Closed" issue.save() self.assertEquals(issue.resolution_by, get_datetime("2021-11-02 07:00")) - self.assertEquals(issue.agreement_status, 'Fulfilled') + self.assertEquals(issue.agreement_status, "Fulfilled") self.assertEquals(issue.resolution_date, frappe.flags.current_time) def test_recording_of_assignment_on_first_reponse_failure(self): @@ -219,11 +223,7 @@ class TestIssue(TestSetUp): issue = make_issue(frappe.flags.current_time, index=1) create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) - add_assignment({ - 'doctype': issue.doctype, - 'name': issue.name, - 'assign_to': ['test@admin.com'] - }) + add_assignment({"doctype": issue.doctype, "name": issue.name, "assign_to": ["test@admin.com"]}) issue.reload() # send a reply failing response SLA @@ -232,12 +232,15 @@ class TestIssue(TestSetUp): # assert if a new timeline item has been added # to record the assignment - comment = frappe.db.exists('Comment', { - 'reference_doctype': 'Issue', - 'reference_name': issue.name, - 'comment_type': 'Assigned', - 'content': _('First Response SLA Failed by {}').format('test') - }) + comment = frappe.db.exists( + "Comment", + { + "reference_doctype": "Issue", + "reference_name": issue.name, + "comment_type": "Assigned", + "content": _("First Response SLA Failed by {}").format("test"), + }, + ) self.assertTrue(comment) def test_agreement_status_on_response(self): @@ -245,7 +248,7 @@ class TestIssue(TestSetUp): issue = make_issue(frappe.flags.current_time, index=1) create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) - self.assertTrue(issue.status == 'Open') + self.assertTrue(issue.status == "Open") # send a reply within response SLA frappe.flags.current_time = get_datetime("2021-11-02 11:00") @@ -253,7 +256,8 @@ class TestIssue(TestSetUp): issue.reload() self.assertEquals(issue.first_responded_on, frappe.flags.current_time) - self.assertEquals(issue.agreement_status, 'Resolution Due') + self.assertEquals(issue.agreement_status, "Resolution Due") + class TestFirstResponseTime(TestSetUp): # working hours used in all cases: Mon-Fri, 10am to 6pm @@ -262,209 +266,268 @@ class TestFirstResponseTime(TestSetUp): # issue creation and first response are on the same day def test_first_response_time_case1(self): """ - Test frt when issue creation and first response are during working hours on the same day. + Test frt when issue creation and first response are during working hours on the same day. """ - issue = create_issue_and_communication(get_datetime("06-28-2021 11:00"), get_datetime("06-28-2021 12:00")) + issue = create_issue_and_communication( + get_datetime("06-28-2021 11:00"), get_datetime("06-28-2021 12:00") + ) self.assertEqual(issue.first_response_time, 3600.0) def test_first_response_time_case2(self): """ - Test frt when issue creation was during working hours, but first response is sent after working hours on the same day. + Test frt when issue creation was during working hours, but first response is sent after working hours on the same day. """ - issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("06-28-2021 20:00")) + issue = create_issue_and_communication( + get_datetime("06-28-2021 12:00"), get_datetime("06-28-2021 20:00") + ) self.assertEqual(issue.first_response_time, 21600.0) def test_first_response_time_case3(self): """ - Test frt when issue creation was before working hours but first response is sent during working hours on the same day. + Test frt when issue creation was before working hours but first response is sent during working hours on the same day. """ - issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("06-28-2021 12:00")) + issue = create_issue_and_communication( + get_datetime("06-28-2021 6:00"), get_datetime("06-28-2021 12:00") + ) self.assertEqual(issue.first_response_time, 7200.0) def test_first_response_time_case4(self): """ - Test frt when both issue creation and first response were after working hours on the same day. + Test frt when both issue creation and first response were after working hours on the same day. """ - issue = create_issue_and_communication(get_datetime("06-28-2021 19:00"), get_datetime("06-28-2021 20:00")) + issue = create_issue_and_communication( + get_datetime("06-28-2021 19:00"), get_datetime("06-28-2021 20:00") + ) self.assertEqual(issue.first_response_time, 1.0) def test_first_response_time_case5(self): """ - Test frt when both issue creation and first response are on the same day, but it's not a work day. + Test frt when both issue creation and first response are on the same day, but it's not a work day. """ - issue = create_issue_and_communication(get_datetime("06-27-2021 10:00"), get_datetime("06-27-2021 11:00")) + issue = create_issue_and_communication( + get_datetime("06-27-2021 10:00"), get_datetime("06-27-2021 11:00") + ) self.assertEqual(issue.first_response_time, 1.0) # issue creation and first response are on consecutive days def test_first_response_time_case6(self): """ - Test frt when the issue was created before working hours and the first response is also sent before working hours, but on the next day. + Test frt when the issue was created before working hours and the first response is also sent before working hours, but on the next day. """ - issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("06-29-2021 6:00")) + issue = create_issue_and_communication( + get_datetime("06-28-2021 6:00"), get_datetime("06-29-2021 6:00") + ) self.assertEqual(issue.first_response_time, 28800.0) def test_first_response_time_case7(self): """ - Test frt when the issue was created before working hours and the first response is sent during working hours, but on the next day. + Test frt when the issue was created before working hours and the first response is sent during working hours, but on the next day. """ - issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("06-29-2021 11:00")) + issue = create_issue_and_communication( + get_datetime("06-28-2021 6:00"), get_datetime("06-29-2021 11:00") + ) self.assertEqual(issue.first_response_time, 32400.0) def test_first_response_time_case8(self): """ - Test frt when the issue was created before working hours and the first response is sent after working hours, but on the next day. + Test frt when the issue was created before working hours and the first response is sent after working hours, but on the next day. """ - issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("06-29-2021 20:00")) + issue = create_issue_and_communication( + get_datetime("06-28-2021 6:00"), get_datetime("06-29-2021 20:00") + ) self.assertEqual(issue.first_response_time, 57600.0) def test_first_response_time_case9(self): """ - Test frt when the issue was created before working hours and the first response is sent on the next day, which is not a work day. + Test frt when the issue was created before working hours and the first response is sent on the next day, which is not a work day. """ - issue = create_issue_and_communication(get_datetime("06-25-2021 6:00"), get_datetime("06-26-2021 11:00")) + issue = create_issue_and_communication( + get_datetime("06-25-2021 6:00"), get_datetime("06-26-2021 11:00") + ) self.assertEqual(issue.first_response_time, 28800.0) def test_first_response_time_case10(self): """ - Test frt when the issue was created during working hours and the first response is sent before working hours, but on the next day. + Test frt when the issue was created during working hours and the first response is sent before working hours, but on the next day. """ - issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("06-29-2021 6:00")) + issue = create_issue_and_communication( + get_datetime("06-28-2021 12:00"), get_datetime("06-29-2021 6:00") + ) self.assertEqual(issue.first_response_time, 21600.0) def test_first_response_time_case11(self): """ - Test frt when the issue was created during working hours and the first response is also sent during working hours, but on the next day. + Test frt when the issue was created during working hours and the first response is also sent during working hours, but on the next day. """ - issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("06-29-2021 11:00")) + issue = create_issue_and_communication( + get_datetime("06-28-2021 12:00"), get_datetime("06-29-2021 11:00") + ) self.assertEqual(issue.first_response_time, 25200.0) def test_first_response_time_case12(self): """ - Test frt when the issue was created during working hours and the first response is sent after working hours, but on the next day. + Test frt when the issue was created during working hours and the first response is sent after working hours, but on the next day. """ - issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("06-29-2021 20:00")) + issue = create_issue_and_communication( + get_datetime("06-28-2021 12:00"), get_datetime("06-29-2021 20:00") + ) self.assertEqual(issue.first_response_time, 50400.0) def test_first_response_time_case13(self): """ - Test frt when the issue was created during working hours and the first response is sent on the next day, which is not a work day. + Test frt when the issue was created during working hours and the first response is sent on the next day, which is not a work day. """ - issue = create_issue_and_communication(get_datetime("06-25-2021 12:00"), get_datetime("06-26-2021 11:00")) + issue = create_issue_and_communication( + get_datetime("06-25-2021 12:00"), get_datetime("06-26-2021 11:00") + ) self.assertEqual(issue.first_response_time, 21600.0) def test_first_response_time_case14(self): """ - Test frt when the issue was created after working hours and the first response is sent before working hours, but on the next day. + Test frt when the issue was created after working hours and the first response is sent before working hours, but on the next day. """ - issue = create_issue_and_communication(get_datetime("06-28-2021 20:00"), get_datetime("06-29-2021 6:00")) + issue = create_issue_and_communication( + get_datetime("06-28-2021 20:00"), get_datetime("06-29-2021 6:00") + ) self.assertEqual(issue.first_response_time, 1.0) def test_first_response_time_case15(self): """ - Test frt when the issue was created after working hours and the first response is sent during working hours, but on the next day. + Test frt when the issue was created after working hours and the first response is sent during working hours, but on the next day. """ - issue = create_issue_and_communication(get_datetime("06-28-2021 20:00"), get_datetime("06-29-2021 11:00")) + issue = create_issue_and_communication( + get_datetime("06-28-2021 20:00"), get_datetime("06-29-2021 11:00") + ) self.assertEqual(issue.first_response_time, 3600.0) def test_first_response_time_case16(self): """ - Test frt when the issue was created after working hours and the first response is also sent after working hours, but on the next day. + Test frt when the issue was created after working hours and the first response is also sent after working hours, but on the next day. """ - issue = create_issue_and_communication(get_datetime("06-28-2021 20:00"), get_datetime("06-29-2021 20:00")) + issue = create_issue_and_communication( + get_datetime("06-28-2021 20:00"), get_datetime("06-29-2021 20:00") + ) self.assertEqual(issue.first_response_time, 28800.0) def test_first_response_time_case17(self): """ - Test frt when the issue was created after working hours and the first response is sent on the next day, which is not a work day. + Test frt when the issue was created after working hours and the first response is sent on the next day, which is not a work day. """ - issue = create_issue_and_communication(get_datetime("06-25-2021 20:00"), get_datetime("06-26-2021 11:00")) + issue = create_issue_and_communication( + get_datetime("06-25-2021 20:00"), get_datetime("06-26-2021 11:00") + ) self.assertEqual(issue.first_response_time, 1.0) # issue creation and first response are a few days apart def test_first_response_time_case18(self): """ - Test frt when the issue was created before working hours and the first response is also sent before working hours, but after a few days. + Test frt when the issue was created before working hours and the first response is also sent before working hours, but after a few days. """ - issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("07-01-2021 6:00")) + issue = create_issue_and_communication( + get_datetime("06-28-2021 6:00"), get_datetime("07-01-2021 6:00") + ) self.assertEqual(issue.first_response_time, 86400.0) def test_first_response_time_case19(self): """ - Test frt when the issue was created before working hours and the first response is sent during working hours, but after a few days. + Test frt when the issue was created before working hours and the first response is sent during working hours, but after a few days. """ - issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("07-01-2021 11:00")) + issue = create_issue_and_communication( + get_datetime("06-28-2021 6:00"), get_datetime("07-01-2021 11:00") + ) self.assertEqual(issue.first_response_time, 90000.0) def test_first_response_time_case20(self): """ - Test frt when the issue was created before working hours and the first response is sent after working hours, but after a few days. + Test frt when the issue was created before working hours and the first response is sent after working hours, but after a few days. """ - issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("07-01-2021 20:00")) + issue = create_issue_and_communication( + get_datetime("06-28-2021 6:00"), get_datetime("07-01-2021 20:00") + ) self.assertEqual(issue.first_response_time, 115200.0) def test_first_response_time_case21(self): """ - Test frt when the issue was created before working hours and the first response is sent after a few days, on a holiday. + Test frt when the issue was created before working hours and the first response is sent after a few days, on a holiday. """ - issue = create_issue_and_communication(get_datetime("06-25-2021 6:00"), get_datetime("06-27-2021 11:00")) + issue = create_issue_and_communication( + get_datetime("06-25-2021 6:00"), get_datetime("06-27-2021 11:00") + ) self.assertEqual(issue.first_response_time, 28800.0) def test_first_response_time_case22(self): """ - Test frt when the issue was created during working hours and the first response is sent before working hours, but after a few days. + Test frt when the issue was created during working hours and the first response is sent before working hours, but after a few days. """ - issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("07-01-2021 6:00")) + issue = create_issue_and_communication( + get_datetime("06-28-2021 12:00"), get_datetime("07-01-2021 6:00") + ) self.assertEqual(issue.first_response_time, 79200.0) def test_first_response_time_case23(self): """ - Test frt when the issue was created during working hours and the first response is also sent during working hours, but after a few days. + Test frt when the issue was created during working hours and the first response is also sent during working hours, but after a few days. """ - issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("07-01-2021 11:00")) + issue = create_issue_and_communication( + get_datetime("06-28-2021 12:00"), get_datetime("07-01-2021 11:00") + ) self.assertEqual(issue.first_response_time, 82800.0) def test_first_response_time_case24(self): """ - Test frt when the issue was created during working hours and the first response is sent after working hours, but after a few days. + Test frt when the issue was created during working hours and the first response is sent after working hours, but after a few days. """ - issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("07-01-2021 20:00")) + issue = create_issue_and_communication( + get_datetime("06-28-2021 12:00"), get_datetime("07-01-2021 20:00") + ) self.assertEqual(issue.first_response_time, 108000.0) def test_first_response_time_case25(self): """ - Test frt when the issue was created during working hours and the first response is sent after a few days, on a holiday. + Test frt when the issue was created during working hours and the first response is sent after a few days, on a holiday. """ - issue = create_issue_and_communication(get_datetime("06-25-2021 12:00"), get_datetime("06-27-2021 11:00")) + issue = create_issue_and_communication( + get_datetime("06-25-2021 12:00"), get_datetime("06-27-2021 11:00") + ) self.assertEqual(issue.first_response_time, 21600.0) def test_first_response_time_case26(self): """ - Test frt when the issue was created after working hours and the first response is sent before working hours, but after a few days. + Test frt when the issue was created after working hours and the first response is sent before working hours, but after a few days. """ - issue = create_issue_and_communication(get_datetime("06-28-2021 20:00"), get_datetime("07-01-2021 6:00")) + issue = create_issue_and_communication( + get_datetime("06-28-2021 20:00"), get_datetime("07-01-2021 6:00") + ) self.assertEqual(issue.first_response_time, 57600.0) def test_first_response_time_case27(self): """ - Test frt when the issue was created after working hours and the first response is sent during working hours, but after a few days. + Test frt when the issue was created after working hours and the first response is sent during working hours, but after a few days. """ - issue = create_issue_and_communication(get_datetime("06-28-2021 20:00"), get_datetime("07-01-2021 11:00")) + issue = create_issue_and_communication( + get_datetime("06-28-2021 20:00"), get_datetime("07-01-2021 11:00") + ) self.assertEqual(issue.first_response_time, 61200.0) def test_first_response_time_case28(self): """ - Test frt when the issue was created after working hours and the first response is also sent after working hours, but after a few days. + Test frt when the issue was created after working hours and the first response is also sent after working hours, but after a few days. """ - issue = create_issue_and_communication(get_datetime("06-28-2021 20:00"), get_datetime("07-01-2021 20:00")) + issue = create_issue_and_communication( + get_datetime("06-28-2021 20:00"), get_datetime("07-01-2021 20:00") + ) self.assertEqual(issue.first_response_time, 86400.0) def test_first_response_time_case29(self): """ - Test frt when the issue was created after working hours and the first response is sent after a few days, on a holiday. + Test frt when the issue was created after working hours and the first response is sent after a few days, on a holiday. """ - issue = create_issue_and_communication(get_datetime("06-25-2021 20:00"), get_datetime("06-27-2021 11:00")) + issue = create_issue_and_communication( + get_datetime("06-25-2021 20:00"), get_datetime("06-27-2021 11:00") + ) self.assertEqual(issue.first_response_time, 1.0) + def create_issue_and_communication(issue_creation, first_responded_on): issue = make_issue(issue_creation, index=1) sender = create_user("test@admin.com") @@ -474,25 +537,28 @@ def create_issue_and_communication(issue_creation, first_responded_on): return issue + def make_issue(creation=None, customer=None, index=0, priority=None, issue_type=None): - if issue_type and not frappe.db.exists('Issue Type', issue_type): - doc = frappe.new_doc('Issue Type') + if issue_type and not frappe.db.exists("Issue Type", issue_type): + doc = frappe.new_doc("Issue Type") doc.name = issue_type doc.insert() - 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, - "opening_date": creation, - "service_level_agreement_creation": creation, - "company": "_Test Company" - }).insert(ignore_permissions=True) + 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, + "opening_date": creation, + "service_level_agreement_creation": creation, + "company": "_Test Company", + } + ).insert(ignore_permissions=True) return issue @@ -503,45 +569,50 @@ def create_customer(name, customer_group, territory): create_territory(territory) if not frappe.db.exists("Customer", {"customer_name": name}): - frappe.get_doc({ - "doctype": "Customer", - "customer_name": name, - "customer_group": customer_group, - "territory": territory - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Customer", + "customer_name": name, + "customer_group": customer_group, + "territory": territory, + } + ).insert(ignore_permissions=True) def create_customer_group(customer_group): if not frappe.db.exists("Customer Group", {"customer_group_name": customer_group}): - frappe.get_doc({ - "doctype": "Customer Group", - "customer_group_name": customer_group - }).insert(ignore_permissions=True) + frappe.get_doc({"doctype": "Customer Group", "customer_group_name": customer_group}).insert( + ignore_permissions=True + ) def create_territory(territory): if not frappe.db.exists("Territory", {"territory_name": territory}): - frappe.get_doc({ - "doctype": "Territory", - "territory_name": territory, - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Territory", + "territory_name": territory, + } + ).insert(ignore_permissions=True) def create_communication(reference_name, sender, sent_or_received, creation): - communication = frappe.get_doc({ - "doctype": "Communication", - "communication_type": "Communication", - "communication_medium": "Email", - "sent_or_received": sent_or_received, - "email_status": "Open", - "subject": "Test Issue", - "sender": sender, - "content": "Test", - "status": "Linked", - "reference_doctype": "Issue", - "creation": creation, - "reference_name": reference_name - }) + communication = frappe.get_doc( + { + "doctype": "Communication", + "communication_type": "Communication", + "communication_medium": "Email", + "sent_or_received": sent_or_received, + "email_status": "Open", + "subject": "Test Issue", + "sender": sender, + "content": "Test", + "status": "Linked", + "reference_doctype": "Issue", + "creation": creation, + "reference_name": reference_name, + } + ) communication.save() diff --git a/erpnext/support/doctype/issue_priority/test_issue_priority.py b/erpnext/support/doctype/issue_priority/test_issue_priority.py index d2b1415d33..c30540b927 100644 --- a/erpnext/support/doctype/issue_priority/test_issue_priority.py +++ b/erpnext/support/doctype/issue_priority/test_issue_priority.py @@ -7,7 +7,6 @@ import frappe class TestIssuePriority(unittest.TestCase): - def test_priorities(self): make_priorities() priorities = frappe.get_list("Issue Priority") @@ -15,14 +14,13 @@ class TestIssuePriority(unittest.TestCase): for priority in priorities: self.assertIn(priority.name, ["Low", "Medium", "High"]) + def make_priorities(): insert_priority("Low") insert_priority("Medium") insert_priority("High") + def insert_priority(name): if not frappe.db.exists("Issue Priority", name): - frappe.get_doc({ - "doctype": "Issue Priority", - "name": name - }).insert(ignore_permissions=True) + frappe.get_doc({"doctype": "Issue Priority", "name": name}).insert(ignore_permissions=True) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 526b6aa249..e49f212f10 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -42,16 +42,24 @@ class ServiceLevelAgreement(Document): for priority in self.priorities: # Check if response and resolution time is set for every priority if not priority.response_time: - frappe.throw(_("Set Response Time for Priority {0} in row {1}.").format(priority.priority, priority.idx)) + frappe.throw( + _("Set Response Time for Priority {0} in row {1}.").format(priority.priority, priority.idx) + ) if self.apply_sla_for_resolution: if not priority.resolution_time: - frappe.throw(_("Set Response Time for Priority {0} in row {1}.").format(priority.priority, priority.idx)) + frappe.throw( + _("Set Response Time for Priority {0} in row {1}.").format(priority.priority, priority.idx) + ) response = priority.response_time resolution = priority.resolution_time if response > resolution: - frappe.throw(_("Response Time for {0} priority in row {1} can't be greater than Resolution Time.").format(priority.priority, priority.idx)) + frappe.throw( + _("Response Time for {0} priority in row {1} can't be greater than Resolution Time.").format( + priority.priority, priority.idx + ) + ) priorities.append(priority.priority) @@ -74,9 +82,14 @@ class ServiceLevelAgreement(Document): support_days.append(support_and_resolution.workday) support_and_resolution.idx = week.index(support_and_resolution.workday) + 1 - if to_timedelta(support_and_resolution.start_time) >= to_timedelta(support_and_resolution.end_time): - frappe.throw(_("Start Time can't be greater than or equal to End Time for {0}.").format( - support_and_resolution.workday)) + if to_timedelta(support_and_resolution.start_time) >= to_timedelta( + support_and_resolution.end_time + ): + frappe.throw( + _("Start Time can't be greater than or equal to End Time for {0}.").format( + support_and_resolution.workday + ) + ) # Check for repeated workday if not len(set(support_days)) == len(support_days): @@ -84,51 +97,76 @@ class ServiceLevelAgreement(Document): frappe.throw(_("Workday {0} has been repeated.").format(repeated_days)) def validate_doc(self): - if self.enabled and self.document_type == "Issue" \ - and not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): - frappe.throw(_("{0} is not enabled in {1}").format(frappe.bold("Track Service Level Agreement"), - get_link_to_form("Support Settings", "Support Settings"))) + if ( + self.enabled + and self.document_type == "Issue" + and not frappe.db.get_single_value("Support Settings", "track_service_level_agreement") + ): + frappe.throw( + _("{0} is not enabled in {1}").format( + frappe.bold("Track Service Level Agreement"), + get_link_to_form("Support Settings", "Support Settings"), + ) + ) - if self.default_service_level_agreement and frappe.db.exists("Service Level Agreement", { - "document_type": self.document_type, - "default_service_level_agreement": "1", - "name": ["!=", self.name] - }): - frappe.throw(_("Default Service Level Agreement for {0} already exists.").format(self.document_type)) + if self.default_service_level_agreement and frappe.db.exists( + "Service Level Agreement", + { + "document_type": self.document_type, + "default_service_level_agreement": "1", + "name": ["!=", self.name], + }, + ): + frappe.throw( + _("Default Service Level Agreement for {0} already exists.").format(self.document_type) + ) if self.start_date and self.end_date: self.validate_from_to_dates(self.start_date, self.end_date) - if self.entity_type and self.entity and frappe.db.exists("Service Level Agreement", { - "entity_type": self.entity_type, - "entity": self.entity, - "name": ["!=", self.name] - }): - frappe.throw(_("Service Level Agreement for {0} {1} already exists.").format( - frappe.bold(self.entity_type), frappe.bold(self.entity))) + if ( + self.entity_type + and self.entity + and frappe.db.exists( + "Service Level Agreement", + {"entity_type": self.entity_type, "entity": self.entity, "name": ["!=", self.name]}, + ) + ): + frappe.throw( + _("Service Level Agreement for {0} {1} already exists.").format( + frappe.bold(self.entity_type), frappe.bold(self.entity) + ) + ) def validate_selected_doctype(self): invalid_doctypes = list(frappe.model.core_doctypes_list) - invalid_doctypes.extend(['Cost Center', 'Company']) - valid_document_types = frappe.get_all('DocType', { - 'issingle': 0, - 'istable': 0, - 'is_submittable': 0, - 'name': ['not in', invalid_doctypes], - 'module': ['not in', ["Email", "Core", "Custom", "Event Streaming", "Social", "Data Migration", "Geo", "Desk"]] - }, pluck="name") + invalid_doctypes.extend(["Cost Center", "Company"]) + valid_document_types = frappe.get_all( + "DocType", + { + "issingle": 0, + "istable": 0, + "is_submittable": 0, + "name": ["not in", invalid_doctypes], + "module": [ + "not in", + ["Email", "Core", "Custom", "Event Streaming", "Social", "Data Migration", "Geo", "Desk"], + ], + }, + pluck="name", + ) if self.document_type not in valid_document_types: - frappe.throw( - msg=_("Please select valid document type."), - title=_("Invalid Document Type") - ) + frappe.throw(msg=_("Please select valid document type."), title=_("Invalid Document Type")) def validate_status_field(self): meta = frappe.get_meta(self.document_type) if not meta.get_field("status"): - frappe.throw(_("The Document Type {0} must have a Status field to configure Service Level Agreement").format( - frappe.bold(self.document_type))) + frappe.throw( + _( + "The Document Type {0} must have a Status field to configure Service Level Agreement" + ).format(frappe.bold(self.document_type)) + ) def validate_condition(self): temp_doc = frappe.new_doc(self.document_type) @@ -141,11 +179,13 @@ class ServiceLevelAgreement(Document): def get_service_level_agreement_priority(self, priority): priority = frappe.get_doc("Service Level Priority", {"priority": priority, "parent": self.name}) - return frappe._dict({ - "priority": priority.priority, - "response_time": priority.response_time, - "resolution_time": priority.resolution_time - }) + return frappe._dict( + { + "priority": priority.priority, + "response_time": priority.response_time, + "resolution_time": priority.resolution_time, + } + ) def before_insert(self): # no need to set up SLA fields for Issue dt as they are standard fields in Issue @@ -176,46 +216,50 @@ class ServiceLevelAgreement(Document): if not meta.has_field(field.get("fieldname")): last_index += 1 - frappe.get_doc({ - "doctype": "DocField", - "idx": last_index, - "parenttype": "DocType", - "parentfield": "fields", - "parent": self.document_type, - "label": field.get("label"), - "fieldname": field.get("fieldname"), - "fieldtype": field.get("fieldtype"), - "collapsible": field.get("collapsible"), - "options": field.get("options"), - "read_only": field.get("read_only"), - "hidden": field.get("hidden"), - "description": field.get("description"), - "default": field.get("default"), - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "DocField", + "idx": last_index, + "parenttype": "DocType", + "parentfield": "fields", + "parent": self.document_type, + "label": field.get("label"), + "fieldname": field.get("fieldname"), + "fieldtype": field.get("fieldtype"), + "collapsible": field.get("collapsible"), + "options": field.get("options"), + "read_only": field.get("read_only"), + "hidden": field.get("hidden"), + "description": field.get("description"), + "default": field.get("default"), + } + ).insert(ignore_permissions=True) else: existing_field = meta.get_field(field.get("fieldname")) self.reset_field_properties(existing_field, "DocField", field) # to update meta and modified timestamp - frappe.get_doc('DocType', self.document_type).save(ignore_permissions=True) + frappe.get_doc("DocType", self.document_type).save(ignore_permissions=True) def create_custom_fields(self, meta, service_level_agreement_fields): for field in service_level_agreement_fields: if not meta.has_field(field.get("fieldname")): - frappe.get_doc({ - "doctype": "Custom Field", - "dt": self.document_type, - "label": field.get("label"), - "fieldname": field.get("fieldname"), - "fieldtype": field.get("fieldtype"), - "insert_after": "append", - "collapsible": field.get("collapsible"), - "options": field.get("options"), - "read_only": field.get("read_only"), - "hidden": field.get("hidden"), - "description": field.get("description"), - "default": field.get("default"), - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Custom Field", + "dt": self.document_type, + "label": field.get("label"), + "fieldname": field.get("fieldname"), + "fieldtype": field.get("fieldtype"), + "insert_after": "append", + "collapsible": field.get("collapsible"), + "options": field.get("options"), + "read_only": field.get("read_only"), + "hidden": field.get("hidden"), + "description": field.get("description"), + "default": field.get("default"), + } + ).insert(ignore_permissions=True) else: existing_field = meta.get_field(field.get("fieldname")) self.reset_field_properties(existing_field, "Custom Field", field) @@ -236,57 +280,73 @@ class ServiceLevelAgreement(Document): def check_agreement_status(): - service_level_agreements = frappe.get_all("Service Level Agreement", filters=[ - {"enabled": 1}, - {"default_service_level_agreement": 0} - ], fields=["name"]) + service_level_agreements = frappe.get_all( + "Service Level Agreement", + filters=[{"enabled": 1}, {"default_service_level_agreement": 0}], + fields=["name"], + ) for service_level_agreement in service_level_agreements: doc = frappe.get_doc("Service Level Agreement", service_level_agreement.name) if doc.end_date and getdate(doc.end_date) < getdate(frappe.utils.getdate()): frappe.db.set_value("Service Level Agreement", service_level_agreement.name, "enabled", 0) + def get_active_service_level_agreement_for(doc): if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): return filters = [ - ["Service Level Agreement", "document_type", "=", doc.get('doctype')], - ["Service Level Agreement", "enabled", "=", 1] + ["Service Level Agreement", "document_type", "=", doc.get("doctype")], + ["Service Level Agreement", "enabled", "=", 1], ] - if doc.get('priority'): - filters.append(["Service Level Priority", "priority", "=", doc.get('priority')]) + if doc.get("priority"): + filters.append(["Service Level Priority", "priority", "=", doc.get("priority")]) or_filters = [] - if doc.get('service_level_agreement'): + if doc.get("service_level_agreement"): or_filters = [ - ["Service Level Agreement", "name", "=", doc.get('service_level_agreement')], + ["Service Level Agreement", "name", "=", doc.get("service_level_agreement")], ] - customer = doc.get('customer') + customer = doc.get("customer") if customer: - or_filters.extend([ - ["Service Level Agreement", "entity", "in", [customer] + get_customer_group(customer) + get_customer_territory(customer)], - ["Service Level Agreement", "entity_type", "is", "not set"] - ]) - else: - or_filters.append( - ["Service Level Agreement", "entity_type", "is", "not set"] + or_filters.extend( + [ + [ + "Service Level Agreement", + "entity", + "in", + [customer] + get_customer_group(customer) + get_customer_territory(customer), + ], + ["Service Level Agreement", "entity_type", "is", "not set"], + ] ) + else: + or_filters.append(["Service Level Agreement", "entity_type", "is", "not set"]) - default_sla_filter = filters + [["Service Level Agreement", "default_service_level_agreement", "=", 1]] - default_sla = frappe.get_all("Service Level Agreement", filters=default_sla_filter, - fields=["name", "default_priority", "apply_sla_for_resolution", "condition"]) + default_sla_filter = filters + [ + ["Service Level Agreement", "default_service_level_agreement", "=", 1] + ] + default_sla = frappe.get_all( + "Service Level Agreement", + filters=default_sla_filter, + fields=["name", "default_priority", "apply_sla_for_resolution", "condition"], + ) filters += [["Service Level Agreement", "default_service_level_agreement", "=", 0]] - agreements = frappe.get_all("Service Level Agreement", filters=filters, or_filters=or_filters, - fields=["name", "default_priority", "apply_sla_for_resolution", "condition"]) + agreements = frappe.get_all( + "Service Level Agreement", + filters=filters, + or_filters=or_filters, + fields=["name", "default_priority", "apply_sla_for_resolution", "condition"], + ) # check if the current document on which SLA is to be applied fulfills all the conditions filtered_agreements = [] for agreement in agreements: - condition = agreement.get('condition') + condition = agreement.get("condition") if not condition or (condition and frappe.safe_eval(condition, None, get_context(doc))): filtered_agreements.append(agreement) @@ -295,8 +355,14 @@ def get_active_service_level_agreement_for(doc): return filtered_agreements[0] if filtered_agreements else None + def get_context(doc): - return {"doc": doc.as_dict(), "nowdate": nowdate, "frappe": frappe._dict(utils=get_safe_globals().get("frappe").get("utils"))} + return { + "doc": doc.as_dict(), + "nowdate": nowdate, + "frappe": frappe._dict(utils=get_safe_globals().get("frappe").get("utils")), + } + def get_customer_group(customer): customer_groups = [] @@ -325,22 +391,33 @@ def get_service_level_agreement_filters(doctype, name, customer=None): filters = [ ["Service Level Agreement", "document_type", "=", doctype], - ["Service Level Agreement", "enabled", "=", 1] + ["Service Level Agreement", "enabled", "=", 1], ] - or_filters = [ - ["Service Level Agreement", "default_service_level_agreement", "=", 1] - ] + or_filters = [["Service Level Agreement", "default_service_level_agreement", "=", 1]] if customer: # Include SLA with No Entity and Entity Type or_filters.append( - ["Service Level Agreement", "entity", "in", [""] + [customer] + get_customer_group(customer) + get_customer_territory(customer)] + [ + "Service Level Agreement", + "entity", + "in", + [""] + [customer] + get_customer_group(customer) + get_customer_territory(customer), + ] ) return { - "priority": [priority.priority for priority in frappe.get_all("Service Level Priority", filters={"parent": name}, fields=["priority"])], - "service_level_agreements": [d.name for d in frappe.get_all("Service Level Agreement", filters=filters, or_filters=or_filters)] + "priority": [ + priority.priority + for priority in frappe.get_all( + "Service Level Priority", filters={"parent": name}, fields=["priority"] + ) + ], + "service_level_agreements": [ + d.name + for d in frappe.get_all("Service Level Agreement", filters=filters, or_filters=or_filters) + ], } @@ -366,7 +443,9 @@ def get_documents_with_active_service_level_agreement(): def set_documents_with_active_service_level_agreement(): - active = [sla.document_type for sla in frappe.get_all("Service Level Agreement", fields=["document_type"])] + active = [ + sla.document_type for sla in frappe.get_all("Service Level Agreement", fields=["document_type"]) + ] frappe.cache().hset("service_level_agreement", "active", active) return active @@ -414,7 +493,7 @@ def process_sla(doc, sla): def handle_status_change(doc, apply_sla_for_resolution): now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - prev_status = frappe.db.get_value(doc.doctype, doc.name, 'status') + prev_status = frappe.db.get_value(doc.doctype, doc.name, "status") hold_statuses = get_hold_statuses(doc.service_level_agreement) fulfillment_statuses = get_fulfillment_statuses(doc.service_level_agreement) @@ -429,9 +508,9 @@ def handle_status_change(doc, apply_sla_for_resolution): return status not in hold_statuses and status not in fulfillment_statuses def set_first_response(): - if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): + if doc.meta.has_field("first_responded_on") and not doc.get("first_responded_on"): doc.first_responded_on = now_time - if get_datetime(doc.get('first_responded_on')) > get_datetime(doc.get('response_by')): + if get_datetime(doc.get("first_responded_on")) > get_datetime(doc.get("response_by")): record_assigned_users_on_failure(doc) def calculate_hold_hours(): @@ -444,7 +523,7 @@ def handle_status_change(doc, apply_sla_for_resolution): doc.total_hold_time = (doc.total_hold_time or 0) + current_hold_hours doc.on_hold_since = None - if ((is_open_status(prev_status) and not is_open_status(doc.status)) or doc.flags.on_first_reply): + if (is_open_status(prev_status) and not is_open_status(doc.status)) or doc.flags.on_first_reply: set_first_response() # Open to Replied @@ -492,22 +571,28 @@ def handle_status_change(doc, apply_sla_for_resolution): def get_fulfillment_statuses(service_level_agreement): - return [entry.status for entry in frappe.db.get_all("SLA Fulfilled On Status", filters={ - "parent": service_level_agreement - }, fields=["status"])] + return [ + entry.status + for entry in frappe.db.get_all( + "SLA Fulfilled On Status", filters={"parent": service_level_agreement}, fields=["status"] + ) + ] def get_hold_statuses(service_level_agreement): - return [entry.status for entry in frappe.db.get_all("Pause SLA On Status", filters={ - "parent": service_level_agreement - }, fields=["status"])] + return [ + entry.status + for entry in frappe.db.get_all( + "Pause SLA On Status", filters={"parent": service_level_agreement}, fields=["status"] + ) + ] def update_response_and_resolution_metrics(doc, apply_sla_for_resolution): priority = get_response_and_resolution_duration(doc) start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) set_response_by(doc, start_date_time, priority) - if apply_sla_for_resolution and not doc.get('on_hold_since'): # resolution_by is reset if on hold + if apply_sla_for_resolution and not doc.get("on_hold_since"): # resolution_by is reset if on hold set_resolution_by(doc, start_date_time, priority) @@ -526,9 +611,13 @@ def get_expected_time_for(parameter, service_level, start_date_time): current_weekday = weekdays[current_date_time.weekday()] if not is_holiday(current_date_time, holidays) and current_weekday in support_days: - if getdate(current_date_time) == getdate(start_date_time) \ - and get_time_in_timedelta(current_date_time.time()) > support_days[current_weekday].start_time: - start_time = current_date_time - datetime(current_date_time.year, current_date_time.month, current_date_time.day) + if ( + getdate(current_date_time) == getdate(start_date_time) + and get_time_in_timedelta(current_date_time.time()) > support_days[current_weekday].start_time + ): + start_time = current_date_time - datetime( + current_date_time.year, current_date_time.month, current_date_time.day + ) else: start_time = support_days[current_weekday].start_time @@ -572,10 +661,12 @@ def get_allotted_seconds(parameter, service_level): def get_support_days(service_level): support_days = {} for service in service_level.get("support_and_resolution"): - support_days[service.workday] = frappe._dict({ - "start_time": service.start_time, - "end_time": service.end_time, - }) + support_days[service.workday] = frappe._dict( + { + "start_time": service.start_time, + "end_time": service.end_time, + } + ) return support_days @@ -588,15 +679,20 @@ def set_resolution_time(doc): if not doc.meta.has_field("user_resolution_time"): return - communications = frappe.get_all("Communication", filters={ - "reference_doctype": doc.doctype, - "reference_name": doc.name - }, fields=["sent_or_received", "name", "creation"], order_by="creation") + communications = frappe.get_all( + "Communication", + filters={"reference_doctype": doc.doctype, "reference_name": doc.name}, + fields=["sent_or_received", "name", "creation"], + order_by="creation", + ) pending_time = [] for i in range(len(communications)): - if communications[i].sent_or_received == "Received" and communications[i-1].sent_or_received == "Sent": - wait_time = time_diff_in_seconds(communications[i].creation, communications[i-1].creation) + if ( + communications[i].sent_or_received == "Received" + and communications[i - 1].sent_or_received == "Sent" + ): + wait_time = time_diff_in_seconds(communications[i].creation, communications[i - 1].creation) if wait_time > 0: pending_time.append(wait_time) @@ -606,25 +702,35 @@ def set_resolution_time(doc): def change_service_level_agreement_and_priority(self): - if self.service_level_agreement and frappe.db.exists("Issue", self.name) and \ - frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): + if ( + self.service_level_agreement + and frappe.db.exists("Issue", self.name) + and frappe.db.get_single_value("Support Settings", "track_service_level_agreement") + ): if not self.priority == frappe.db.get_value("Issue", self.name, "priority"): - self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) + self.set_response_and_resolution_time( + priority=self.priority, service_level_agreement=self.service_level_agreement + ) frappe.msgprint(_("Priority has been changed to {0}.").format(self.priority)) - if not self.service_level_agreement == frappe.db.get_value("Issue", self.name, "service_level_agreement"): - self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) - frappe.msgprint(_("Service Level Agreement has been changed to {0}.").format(self.service_level_agreement)) + if not self.service_level_agreement == frappe.db.get_value( + "Issue", self.name, "service_level_agreement" + ): + self.set_response_and_resolution_time( + priority=self.priority, service_level_agreement=self.service_level_agreement + ) + frappe.msgprint( + _("Service Level Agreement has been changed to {0}.").format(self.service_level_agreement) + ) def get_response_and_resolution_duration(doc): sla = frappe.get_doc("Service Level Agreement", doc.service_level_agreement) priority = sla.get_service_level_agreement_priority(doc.priority) - priority.update({ - "support_and_resolution": sla.support_and_resolution, - "holiday_list": sla.holiday_list - }) + priority.update( + {"support_and_resolution": sla.support_and_resolution, "holiday_list": sla.holiday_list} + ) return priority @@ -632,14 +738,16 @@ def reset_service_level_agreement(doc, reason, user): if not frappe.db.get_single_value("Support Settings", "allow_resetting_service_level_agreement"): frappe.throw(_("Allow Resetting Service Level Agreement from Support Settings.")) - frappe.get_doc({ - "doctype": "Comment", - "comment_type": "Info", - "reference_doctype": doc.doctype, - "reference_name": doc.name, - "comment_email": user, - "content": " resetted Service Level Agreement - {0}".format(_(reason)), - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Comment", + "comment_type": "Info", + "reference_doctype": doc.doctype, + "reference_name": doc.name, + "comment_email": user, + "content": " resetted Service Level Agreement - {0}".format(_(reason)), + } + ).insert(ignore_permissions=True) doc.service_level_agreement_creation = now_datetime(doc.get("owner")) doc.save() @@ -665,28 +773,30 @@ def on_communication_update(doc, status): if not parent: return - if not parent.meta.has_field('service_level_agreement'): + if not parent.meta.has_field("service_level_agreement"): return if ( - doc.sent_or_received == "Received" # a reply is received - and parent.get('status') == 'Open' # issue status is set as open from communication.py + doc.sent_or_received == "Received" # a reply is received + and parent.get("status") == "Open" # issue status is set as open from communication.py and parent.get_doc_before_save() - and parent.get('status') != parent._doc_before_save.get('status') # status changed + and parent.get("status") != parent._doc_before_save.get("status") # status changed ): # undo the status change in db # since prev status is fetched from db frappe.db.set_value( - parent.doctype, parent.name, - 'status', parent._doc_before_save.get('status'), - update_modified=False + parent.doctype, + parent.name, + "status", + parent._doc_before_save.get("status"), + update_modified=False, ) elif ( - doc.sent_or_received == "Sent" # a reply is sent - and parent.get('first_responded_on') # first_responded_on is set from communication.py + doc.sent_or_received == "Sent" # a reply is sent + and parent.get("first_responded_on") # first_responded_on is set from communication.py and parent.get_doc_before_save() - and not parent._doc_before_save.get('first_responded_on') # first_responded_on was not set + and not parent._doc_before_save.get("first_responded_on") # first_responded_on was not set ): # reset first_responded_on since it will be handled/set later on parent.first_responded_on = None @@ -695,7 +805,9 @@ def on_communication_update(doc, status): else: return - for_resolution = frappe.db.get_value('Service Level Agreement', parent.service_level_agreement, 'apply_sla_for_resolution') + for_resolution = frappe.db.get_value( + "Service Level Agreement", parent.service_level_agreement, "apply_sla_for_resolution" + ) handle_status_change(parent, for_resolution) update_response_and_resolution_metrics(parent, for_resolution) @@ -705,36 +817,42 @@ def on_communication_update(doc, status): def reset_expected_response_and_resolution(doc): - if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): + if doc.meta.has_field("first_responded_on") and not doc.get("first_responded_on"): doc.response_by = None - if doc.meta.has_field("resolution_by") and not doc.get('resolution_date'): + if doc.meta.has_field("resolution_by") and not doc.get("resolution_date"): doc.resolution_by = None def set_response_by(doc, start_date_time, priority): if doc.meta.has_field("response_by"): - doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) - if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time') and not doc.get('first_responded_on'): - doc.response_by = add_to_date(doc.response_by, seconds=round(doc.get('total_hold_time'))) + doc.response_by = get_expected_time_for( + parameter="response", service_level=priority, start_date_time=start_date_time + ) + if ( + doc.meta.has_field("total_hold_time") + and doc.get("total_hold_time") + and not doc.get("first_responded_on") + ): + doc.response_by = add_to_date(doc.response_by, seconds=round(doc.get("total_hold_time"))) def set_resolution_by(doc, start_date_time, priority): if doc.meta.has_field("resolution_by"): - doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) - if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time'): - doc.resolution_by = add_to_date(doc.resolution_by, seconds=round(doc.get('total_hold_time'))) + doc.resolution_by = get_expected_time_for( + parameter="resolution", service_level=priority, start_date_time=start_date_time + ) + if doc.meta.has_field("total_hold_time") and doc.get("total_hold_time"): + doc.resolution_by = add_to_date(doc.resolution_by, seconds=round(doc.get("total_hold_time"))) def record_assigned_users_on_failure(doc): assigned_users = doc.get_assigned_users() if assigned_users: from frappe.utils import get_fullname - assigned_users = ', '.join((get_fullname(user) for user in assigned_users)) - message = _('First Response SLA Failed by {}').format(assigned_users) - doc.add_comment( - comment_type='Assigned', - text=message - ) + + assigned_users = ", ".join((get_fullname(user) for user in assigned_users)) + message = _("First Response SLA Failed by {}").format(assigned_users) + doc.add_comment(comment_type="Assigned", text=message) def get_service_level_agreement_fields(): @@ -743,71 +861,57 @@ def get_service_level_agreement_fields(): "collapsible": 1, "fieldname": "service_level_section", "fieldtype": "Section Break", - "label": "Service Level" + "label": "Service Level", }, { "fieldname": "service_level_agreement", "fieldtype": "Link", "label": "Service Level Agreement", - "options": "Service Level Agreement" - }, - { - "fieldname": "priority", - "fieldtype": "Link", - "label": "Priority", - "options": "Issue Priority" - }, - { - "fieldname": "response_by", - "fieldtype": "Datetime", - "label": "Response By", - "read_only": 1 + "options": "Service Level Agreement", }, + {"fieldname": "priority", "fieldtype": "Link", "label": "Priority", "options": "Issue Priority"}, + {"fieldname": "response_by", "fieldtype": "Datetime", "label": "Response By", "read_only": 1}, { "fieldname": "first_responded_on", "fieldtype": "Datetime", "label": "First Responded On", "no_copy": 1, - "read_only": 1 + "read_only": 1, }, { "fieldname": "on_hold_since", "fieldtype": "Datetime", "hidden": 1, "label": "On Hold Since", - "read_only": 1 + "read_only": 1, }, { "fieldname": "total_hold_time", "fieldtype": "Duration", "label": "Total Hold Time", - "read_only": 1 - }, - { - "fieldname": "cb", - "fieldtype": "Column Break", - "read_only": 1 + "read_only": 1, }, + {"fieldname": "cb", "fieldtype": "Column Break", "read_only": 1}, { "default": "First Response Due", "fieldname": "agreement_status", "fieldtype": "Select", "label": "Service Level Agreement Status", "options": "First Response Due\nResolution Due\nFulfilled\nFailed", - "read_only": 1 + "read_only": 1, }, { "fieldname": "resolution_by", "fieldtype": "Datetime", "label": "Resolution By", - "read_only": 1 + "read_only": 1, }, { "fieldname": "service_level_agreement_creation", "fieldtype": "Datetime", "hidden": 1, "label": "Service Level Agreement Creation", - "read_only": 1 + "read_only": 1, }, { "depends_on": "eval:!doc.__islocal", @@ -815,8 +919,8 @@ def get_service_level_agreement_fields(): "fieldtype": "Datetime", "label": "Resolution Date", "no_copy": 1, - "read_only": 1 - } + "read_only": 1, + }, ] @@ -826,21 +930,21 @@ def update_agreement_status_on_custom_status(doc): def update_agreement_status(doc, apply_sla_for_resolution): - if (doc.meta.has_field("agreement_status")): + if doc.meta.has_field("agreement_status"): # if SLA is applied for resolution check for response and resolution, else only response if apply_sla_for_resolution: - if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): + if doc.meta.has_field("first_responded_on") and not doc.get("first_responded_on"): doc.agreement_status = "First Response Due" - elif doc.meta.has_field("resolution_date") and not doc.get('resolution_date'): + elif doc.meta.has_field("resolution_date") and not doc.get("resolution_date"): doc.agreement_status = "Resolution Due" - elif get_datetime(doc.get('resolution_date')) <= get_datetime(doc.get('resolution_by')): + elif get_datetime(doc.get("resolution_date")) <= get_datetime(doc.get("resolution_by")): doc.agreement_status = "Fulfilled" else: doc.agreement_status = "Failed" else: - if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): + if doc.meta.has_field("first_responded_on") and not doc.get("first_responded_on"): doc.agreement_status = "First Response Due" - elif get_datetime(doc.get('first_responded_on')) <= get_datetime(doc.get('response_by')): + elif get_datetime(doc.get("first_responded_on")) <= get_datetime(doc.get("response_by")): doc.agreement_status = "Fulfilled" else: doc.agreement_status = "Failed" @@ -853,6 +957,7 @@ def is_holiday(date, holidays): def get_time_in_timedelta(time): """Converts datetime.time(10, 36, 55, 961454) to datetime.timedelta(seconds=38215).""" import datetime + return datetime.timedelta(hours=time.hour, minutes=time.minute, seconds=time.second) @@ -865,7 +970,7 @@ def convert_utc_to_user_timezone(utc_timestamp, user): from pytz import UnknownTimeZoneError, timezone user_tz = get_tz(user) - utcnow = timezone('UTC').localize(utc_timestamp) + utcnow = timezone("UTC").localize(utc_timestamp) try: return utcnow.astimezone(timezone(user_tz)) except UnknownTimeZoneError: @@ -884,11 +989,7 @@ def get_user_time(user, to_string=False): @frappe.whitelist() def get_sla_doctypes(): doctypes = [] - data = frappe.get_all('Service Level Agreement', - {'enabled': 1}, - ['document_type'], - distinct=1 - ) + data = frappe.get_all("Service Level Agreement", {"enabled": 1}, ["document_type"], distinct=1) for entry in data: doctypes.append(entry.document_type) diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index a34124fba2..4e00138906 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -20,51 +20,122 @@ class TestServiceLevelAgreement(unittest.TestCase): def test_service_level_agreement(self): # Default Service Level Agreement - create_default_service_level_agreement = create_service_level_agreement(default_service_level_agreement=1, - holiday_list="__Test Holiday List", entity_type=None, entity=None, response_time=14400, resolution_time=21600) + create_default_service_level_agreement = create_service_level_agreement( + default_service_level_agreement=1, + holiday_list="__Test Holiday List", + entity_type=None, + entity=None, + response_time=14400, + resolution_time=21600, + ) - get_default_service_level_agreement = get_service_level_agreement(default_service_level_agreement=1) + get_default_service_level_agreement = get_service_level_agreement( + default_service_level_agreement=1 + ) - self.assertEqual(create_default_service_level_agreement.name, get_default_service_level_agreement.name) - self.assertEqual(create_default_service_level_agreement.entity_type, get_default_service_level_agreement.entity_type) - self.assertEqual(create_default_service_level_agreement.entity, get_default_service_level_agreement.entity) - self.assertEqual(create_default_service_level_agreement.default_service_level_agreement, get_default_service_level_agreement.default_service_level_agreement) + self.assertEqual( + create_default_service_level_agreement.name, get_default_service_level_agreement.name + ) + self.assertEqual( + create_default_service_level_agreement.entity_type, + get_default_service_level_agreement.entity_type, + ) + self.assertEqual( + create_default_service_level_agreement.entity, get_default_service_level_agreement.entity + ) + self.assertEqual( + create_default_service_level_agreement.default_service_level_agreement, + get_default_service_level_agreement.default_service_level_agreement, + ) # Service Level Agreement for Customer customer = create_customer() - create_customer_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0, - holiday_list="__Test Holiday List", entity_type="Customer", entity=customer, - response_time=7200, resolution_time=10800) - get_customer_service_level_agreement = get_service_level_agreement(entity_type="Customer", entity=customer) + create_customer_service_level_agreement = create_service_level_agreement( + default_service_level_agreement=0, + holiday_list="__Test Holiday List", + entity_type="Customer", + entity=customer, + response_time=7200, + resolution_time=10800, + ) + get_customer_service_level_agreement = get_service_level_agreement( + entity_type="Customer", entity=customer + ) - self.assertEqual(create_customer_service_level_agreement.name, get_customer_service_level_agreement.name) - self.assertEqual(create_customer_service_level_agreement.entity_type, get_customer_service_level_agreement.entity_type) - self.assertEqual(create_customer_service_level_agreement.entity, get_customer_service_level_agreement.entity) - self.assertEqual(create_customer_service_level_agreement.default_service_level_agreement, get_customer_service_level_agreement.default_service_level_agreement) + self.assertEqual( + create_customer_service_level_agreement.name, get_customer_service_level_agreement.name + ) + self.assertEqual( + create_customer_service_level_agreement.entity_type, + get_customer_service_level_agreement.entity_type, + ) + self.assertEqual( + create_customer_service_level_agreement.entity, get_customer_service_level_agreement.entity + ) + self.assertEqual( + create_customer_service_level_agreement.default_service_level_agreement, + get_customer_service_level_agreement.default_service_level_agreement, + ) # Service Level Agreement for Customer Group customer_group = create_customer_group() - create_customer_group_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0, - holiday_list="__Test Holiday List", entity_type="Customer Group", entity=customer_group, - response_time=7200, resolution_time=10800) - get_customer_group_service_level_agreement = get_service_level_agreement(entity_type="Customer Group", entity=customer_group) + create_customer_group_service_level_agreement = create_service_level_agreement( + default_service_level_agreement=0, + holiday_list="__Test Holiday List", + entity_type="Customer Group", + entity=customer_group, + response_time=7200, + resolution_time=10800, + ) + get_customer_group_service_level_agreement = get_service_level_agreement( + entity_type="Customer Group", entity=customer_group + ) - self.assertEqual(create_customer_group_service_level_agreement.name, get_customer_group_service_level_agreement.name) - self.assertEqual(create_customer_group_service_level_agreement.entity_type, get_customer_group_service_level_agreement.entity_type) - self.assertEqual(create_customer_group_service_level_agreement.entity, get_customer_group_service_level_agreement.entity) - self.assertEqual(create_customer_group_service_level_agreement.default_service_level_agreement, get_customer_group_service_level_agreement.default_service_level_agreement) + self.assertEqual( + create_customer_group_service_level_agreement.name, + get_customer_group_service_level_agreement.name, + ) + self.assertEqual( + create_customer_group_service_level_agreement.entity_type, + get_customer_group_service_level_agreement.entity_type, + ) + self.assertEqual( + create_customer_group_service_level_agreement.entity, + get_customer_group_service_level_agreement.entity, + ) + self.assertEqual( + create_customer_group_service_level_agreement.default_service_level_agreement, + get_customer_group_service_level_agreement.default_service_level_agreement, + ) # Service Level Agreement for Territory territory = create_territory() - create_territory_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0, + create_territory_service_level_agreement = create_service_level_agreement( + default_service_level_agreement=0, holiday_list="__Test Holiday List", - entity_type="Territory", entity=territory, response_time=7200, resolution_time=10800) - get_territory_service_level_agreement = get_service_level_agreement(entity_type="Territory", entity=territory) + entity_type="Territory", + entity=territory, + response_time=7200, + resolution_time=10800, + ) + get_territory_service_level_agreement = get_service_level_agreement( + entity_type="Territory", entity=territory + ) - self.assertEqual(create_territory_service_level_agreement.name, get_territory_service_level_agreement.name) - self.assertEqual(create_territory_service_level_agreement.entity_type, get_territory_service_level_agreement.entity_type) - self.assertEqual(create_territory_service_level_agreement.entity, get_territory_service_level_agreement.entity) - self.assertEqual(create_territory_service_level_agreement.default_service_level_agreement, get_territory_service_level_agreement.default_service_level_agreement) + self.assertEqual( + create_territory_service_level_agreement.name, get_territory_service_level_agreement.name + ) + self.assertEqual( + create_territory_service_level_agreement.entity_type, + get_territory_service_level_agreement.entity_type, + ) + self.assertEqual( + create_territory_service_level_agreement.entity, get_territory_service_level_agreement.entity + ) + self.assertEqual( + create_territory_service_level_agreement.default_service_level_agreement, + get_territory_service_level_agreement.default_service_level_agreement, + ) def test_custom_field_creation_for_sla_on_standard_dt(self): # Default Service Level Agreement @@ -72,10 +143,12 @@ class TestServiceLevelAgreement(unittest.TestCase): lead_sla = create_service_level_agreement( default_service_level_agreement=1, holiday_list="__Test Holiday List", - entity_type=None, entity=None, - response_time=14400, resolution_time=21600, + entity_type=None, + entity=None, + response_time=14400, + resolution_time=21600, doctype=doctype, - sla_fulfilled_on=[{"status": "Converted"}] + sla_fulfilled_on=[{"status": "Converted"}], ) # check default SLA for lead @@ -86,27 +159,35 @@ class TestServiceLevelAgreement(unittest.TestCase): sla_fields = get_service_level_agreement_fields() for field in sla_fields: - self.assertTrue(frappe.db.exists("Custom Field", {"dt": doctype, "fieldname": field.get("fieldname")})) + self.assertTrue( + frappe.db.exists("Custom Field", {"dt": doctype, "fieldname": field.get("fieldname")}) + ) def test_docfield_creation_for_sla_on_custom_dt(self): doctype = create_custom_doctype() sla = create_service_level_agreement( default_service_level_agreement=1, holiday_list="__Test Holiday List", - entity_type=None, entity=None, - response_time=14400, resolution_time=21600, - doctype=doctype.name + entity_type=None, + entity=None, + response_time=14400, + resolution_time=21600, + doctype=doctype.name, ) # check default SLA for custom dt - default_sla = get_service_level_agreement(default_service_level_agreement=1, doctype=doctype.name) + default_sla = get_service_level_agreement( + default_service_level_agreement=1, doctype=doctype.name + ) self.assertEqual(sla.name, default_sla.name) # check SLA docfields created sla_fields = get_service_level_agreement_fields() for field in sla_fields: - self.assertTrue(frappe.db.exists("DocField", {"fieldname": field.get("fieldname"), "parent": doctype.name})) + self.assertTrue( + frappe.db.exists("DocField", {"fieldname": field.get("fieldname"), "parent": doctype.name}) + ) def test_sla_application(self): # Default Service Level Agreement @@ -114,10 +195,12 @@ class TestServiceLevelAgreement(unittest.TestCase): lead_sla = create_service_level_agreement( default_service_level_agreement=1, holiday_list="__Test Holiday List", - entity_type=None, entity=None, - response_time=14400, resolution_time=21600, + entity_type=None, + entity=None, + response_time=14400, + resolution_time=21600, doctype=doctype, - sla_fulfilled_on=[{"status": "Converted"}] + sla_fulfilled_on=[{"status": "Converted"}], ) # make lead with default SLA @@ -130,21 +213,23 @@ class TestServiceLevelAgreement(unittest.TestCase): frappe.flags.current_time = datetime.datetime(2019, 3, 4, 15, 0) lead.reload() - lead.status = 'Converted' + lead.status = "Converted" lead.save() - self.assertEqual(lead.agreement_status, 'Fulfilled') + self.assertEqual(lead.agreement_status, "Fulfilled") def test_hold_time(self): doctype = "Lead" create_service_level_agreement( default_service_level_agreement=1, holiday_list="__Test Holiday List", - entity_type=None, entity=None, - response_time=14400, resolution_time=21600, + entity_type=None, + entity=None, + response_time=14400, + resolution_time=21600, doctype=doctype, sla_fulfilled_on=[{"status": "Converted"}], - pause_sla_on=[{"status": "Replied"}] + pause_sla_on=[{"status": "Replied"}], ) creation = datetime.datetime(2020, 3, 4, 4, 0) @@ -152,7 +237,7 @@ class TestServiceLevelAgreement(unittest.TestCase): frappe.flags.current_time = datetime.datetime(2020, 3, 4, 4, 15) lead.reload() - lead.status = 'Replied' + lead.status = "Replied" lead.save() lead.reload() @@ -160,7 +245,7 @@ class TestServiceLevelAgreement(unittest.TestCase): frappe.flags.current_time = datetime.datetime(2020, 3, 4, 5, 5) lead.reload() - lead.status = 'Converted' + lead.status = "Converted" lead.save() lead.reload() @@ -172,12 +257,13 @@ class TestServiceLevelAgreement(unittest.TestCase): create_service_level_agreement( default_service_level_agreement=1, holiday_list="__Test Holiday List", - entity_type=None, entity=None, + entity_type=None, + entity=None, response_time=14400, doctype=doctype, sla_fulfilled_on=[{"status": "Replied"}], pause_sla_on=[], - apply_sla_for_resolution=0 + apply_sla_for_resolution=0, ) creation = datetime.datetime(2019, 3, 4, 12, 0) @@ -187,22 +273,23 @@ class TestServiceLevelAgreement(unittest.TestCase): # failed with response time only frappe.flags.current_time = datetime.datetime(2019, 3, 4, 16, 5) lead.reload() - lead.status = 'Replied' + lead.status = "Replied" lead.save() lead.reload() - self.assertEqual(lead.agreement_status, 'Failed') + self.assertEqual(lead.agreement_status, "Failed") def test_fulfilled_sla_for_response_only(self): doctype = "Lead" lead_sla = create_service_level_agreement( default_service_level_agreement=1, holiday_list="__Test Holiday List", - entity_type=None, entity=None, + entity_type=None, + entity=None, response_time=14400, doctype=doctype, sla_fulfilled_on=[{"status": "Replied"}], - apply_sla_for_resolution=0 + apply_sla_for_resolution=0, ) # fulfilled with response time only @@ -214,11 +301,11 @@ class TestServiceLevelAgreement(unittest.TestCase): frappe.flags.current_time = datetime.datetime(2019, 3, 4, 15, 30) lead.reload() - lead.status = 'Replied' + lead.status = "Replied" lead.save() lead.reload() - self.assertEqual(lead.agreement_status, 'Fulfilled') + self.assertEqual(lead.agreement_status, "Fulfilled") def test_service_level_agreement_filters(self): doctype = "Lead" @@ -226,29 +313,30 @@ class TestServiceLevelAgreement(unittest.TestCase): default_service_level_agreement=0, doctype=doctype, holiday_list="__Test Holiday List", - entity_type=None, entity=None, + entity_type=None, + entity=None, condition='doc.source == "Test Source"', response_time=14400, sla_fulfilled_on=[{"status": "Replied"}], - apply_sla_for_resolution=0 + apply_sla_for_resolution=0, ) creation = datetime.datetime(2019, 3, 4, 12, 0) lead = make_lead(creation=creation, index=4) - applied_sla = frappe.db.get_value('Lead', lead.name, 'service_level_agreement') + applied_sla = frappe.db.get_value("Lead", lead.name, "service_level_agreement") self.assertFalse(applied_sla) - source = frappe.get_doc(doctype='Lead Source', source_name='Test Source') + source = frappe.get_doc(doctype="Lead Source", source_name="Test Source") source.insert(ignore_if_duplicate=True) lead.source = "Test Source" lead.save() - applied_sla = frappe.db.get_value('Lead', lead.name, 'service_level_agreement') + applied_sla = frappe.db.get_value("Lead", lead.name, "service_level_agreement") self.assertEqual(applied_sla, lead_sla.name) # check if SLA is removed if condition fails lead.reload() lead.source = None lead.save() - applied_sla = frappe.db.get_value('Lead', lead.name, 'service_level_agreement') + applied_sla = frappe.db.get_value("Lead", lead.name, "service_level_agreement") self.assertFalse(applied_sla) def tearDown(self): @@ -256,130 +344,150 @@ class TestServiceLevelAgreement(unittest.TestCase): frappe.delete_doc("Service Level Agreement", d.name, force=1) -def get_service_level_agreement(default_service_level_agreement=None, entity_type=None, entity=None, doctype="Issue"): +def get_service_level_agreement( + default_service_level_agreement=None, entity_type=None, entity=None, doctype="Issue" +): if default_service_level_agreement: - filters = {"default_service_level_agreement": default_service_level_agreement, "document_type": doctype} + filters = { + "default_service_level_agreement": default_service_level_agreement, + "document_type": doctype, + } else: filters = {"entity_type": entity_type, "entity": entity} service_level_agreement = frappe.get_doc("Service Level Agreement", filters) return service_level_agreement -def create_service_level_agreement(default_service_level_agreement, holiday_list, response_time, entity_type, - entity, resolution_time=0, doctype="Issue", condition="", sla_fulfilled_on=[], pause_sla_on=[], apply_sla_for_resolution=1, - service_level=None, start_time="10:00:00", end_time="18:00:00"): + +def create_service_level_agreement( + default_service_level_agreement, + holiday_list, + response_time, + entity_type, + entity, + resolution_time=0, + doctype="Issue", + condition="", + sla_fulfilled_on=[], + pause_sla_on=[], + apply_sla_for_resolution=1, + service_level=None, + start_time="10:00:00", + end_time="18:00:00", +): make_holiday_list() make_priorities() if not sla_fulfilled_on: - sla_fulfilled_on = [ - {"status": "Resolved"}, - {"status": "Closed"} - ] + sla_fulfilled_on = [{"status": "Resolved"}, {"status": "Closed"}] pause_sla_on = [{"status": "Replied"}] if doctype == "Issue" else pause_sla_on - service_level_agreement = frappe._dict({ - "doctype": "Service Level Agreement", - "enabled": 1, - "document_type": doctype, - "service_level": service_level or "__Test {} SLA".format(entity_type if entity_type else "Default"), - "default_service_level_agreement": default_service_level_agreement, - "condition": condition, - "default_priority": "Medium", - "holiday_list": holiday_list, - "entity_type": entity_type, - "entity": entity, - "start_date": frappe.utils.getdate(), - "end_date": frappe.utils.add_to_date(frappe.utils.getdate(), days=100), - "apply_sla_for_resolution": apply_sla_for_resolution, - "priorities": [ - { - "priority": "Low", - "response_time": response_time, - "resolution_time": resolution_time, - }, - { - "priority": "Medium", - "response_time": response_time, - "default_priority": 1, - "resolution_time": resolution_time, - }, - { - "priority": "High", - "response_time": response_time, - "resolution_time": resolution_time, - } - ], - "sla_fulfilled_on": sla_fulfilled_on, - "pause_sla_on": pause_sla_on, - "support_and_resolution": [ - { - "workday": "Monday", - "start_time": start_time, - "end_time": end_time, - }, - { - "workday": "Tuesday", - "start_time": start_time, - "end_time": end_time, - }, - { - "workday": "Wednesday", - "start_time": start_time, - "end_time": end_time, - }, - { - "workday": "Thursday", - "start_time": start_time, - "end_time": end_time, - }, - { - "workday": "Friday", - "start_time": start_time, - "end_time": end_time, - } - ] - }) + service_level_agreement = frappe._dict( + { + "doctype": "Service Level Agreement", + "enabled": 1, + "document_type": doctype, + "service_level": service_level + or "__Test {} SLA".format(entity_type if entity_type else "Default"), + "default_service_level_agreement": default_service_level_agreement, + "condition": condition, + "default_priority": "Medium", + "holiday_list": holiday_list, + "entity_type": entity_type, + "entity": entity, + "start_date": frappe.utils.getdate(), + "end_date": frappe.utils.add_to_date(frappe.utils.getdate(), days=100), + "apply_sla_for_resolution": apply_sla_for_resolution, + "priorities": [ + { + "priority": "Low", + "response_time": response_time, + "resolution_time": resolution_time, + }, + { + "priority": "Medium", + "response_time": response_time, + "default_priority": 1, + "resolution_time": resolution_time, + }, + { + "priority": "High", + "response_time": response_time, + "resolution_time": resolution_time, + }, + ], + "sla_fulfilled_on": sla_fulfilled_on, + "pause_sla_on": pause_sla_on, + "support_and_resolution": [ + { + "workday": "Monday", + "start_time": start_time, + "end_time": end_time, + }, + { + "workday": "Tuesday", + "start_time": start_time, + "end_time": end_time, + }, + { + "workday": "Wednesday", + "start_time": start_time, + "end_time": end_time, + }, + { + "workday": "Thursday", + "start_time": start_time, + "end_time": end_time, + }, + { + "workday": "Friday", + "start_time": start_time, + "end_time": end_time, + }, + ], + } + ) filters = { "default_service_level_agreement": service_level_agreement.default_service_level_agreement, - "service_level": service_level_agreement.service_level + "service_level": service_level_agreement.service_level, } if not default_service_level_agreement: - filters.update({ - "entity_type": entity_type, - "entity": entity - }) + filters.update({"entity_type": entity_type, "entity": entity}) sla = frappe.db.exists("Service Level Agreement", filters) if sla: frappe.delete_doc("Service Level Agreement", sla, force=1) - return frappe.get_doc(service_level_agreement).insert(ignore_permissions=True, ignore_if_duplicate=True) + return frappe.get_doc(service_level_agreement).insert( + ignore_permissions=True, ignore_if_duplicate=True + ) def create_customer(): - customer = frappe.get_doc({ - "doctype": "Customer", - "customer_name": "_Test Customer", - "customer_group": "Commercial", - "customer_type": "Individual", - "territory": "Rest Of The World" - }) + customer = frappe.get_doc( + { + "doctype": "Customer", + "customer_name": "_Test Customer", + "customer_group": "Commercial", + "customer_type": "Individual", + "territory": "Rest Of The World", + } + ) if not frappe.db.exists("Customer", "_Test Customer"): customer.insert(ignore_permissions=True) return customer.name else: return frappe.db.exists("Customer", "_Test Customer") + def create_customer_group(): - customer_group = frappe.get_doc({ - "doctype": "Customer Group", - "customer_group_name": "_Test SLA Customer Group" - }) + customer_group = frappe.get_doc( + {"doctype": "Customer Group", "customer_group_name": "_Test SLA Customer Group"} + ) if not frappe.db.exists("Customer Group", {"customer_group_name": "_Test SLA Customer Group"}): customer_group.insert() @@ -387,11 +495,14 @@ def create_customer_group(): else: return frappe.db.exists("Customer Group", {"customer_group_name": "_Test SLA Customer Group"}) + def create_territory(): - territory = frappe.get_doc({ - "doctype": "Territory", - "territory_name": "_Test SLA Territory", - }) + territory = frappe.get_doc( + { + "doctype": "Territory", + "territory_name": "_Test SLA Territory", + } + ) if not frappe.db.exists("Territory", {"territory_name": "_Test SLA Territory"}): territory.insert() @@ -399,102 +510,116 @@ def create_territory(): else: return frappe.db.exists("Territory", {"territory_name": "_Test SLA Territory"}) + def create_service_level_agreements_for_issues(): - create_service_level_agreement(default_service_level_agreement=1, holiday_list="__Test Holiday List", - entity_type=None, entity=None, response_time=14400, resolution_time=21600) + create_service_level_agreement( + default_service_level_agreement=1, + holiday_list="__Test Holiday List", + entity_type=None, + entity=None, + response_time=14400, + resolution_time=21600, + ) create_customer() - create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List", - entity_type="Customer", entity="_Test Customer", response_time=7200, resolution_time=10800) + create_service_level_agreement( + default_service_level_agreement=0, + holiday_list="__Test Holiday List", + entity_type="Customer", + entity="_Test Customer", + response_time=7200, + resolution_time=10800, + ) create_customer_group() - create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List", - entity_type="Customer Group", entity="_Test SLA Customer Group", response_time=7200, resolution_time=10800) + create_service_level_agreement( + default_service_level_agreement=0, + holiday_list="__Test Holiday List", + entity_type="Customer Group", + entity="_Test SLA Customer Group", + response_time=7200, + resolution_time=10800, + ) create_territory() - create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List", - entity_type="Territory", entity="_Test SLA Territory", response_time=7200, resolution_time=10800) + create_service_level_agreement( + default_service_level_agreement=0, + holiday_list="__Test Holiday List", + entity_type="Territory", + entity="_Test SLA Territory", + response_time=7200, + resolution_time=10800, + ) create_service_level_agreement( - default_service_level_agreement=0, holiday_list="__Test Holiday List", - entity_type=None, entity=None, response_time=14400, resolution_time=21600, - service_level="24-hour-SLA", start_time="00:00:00", end_time="23:59:59", - condition="doc.issue_type == 'Critical'" + default_service_level_agreement=0, + holiday_list="__Test Holiday List", + entity_type=None, + entity=None, + response_time=14400, + resolution_time=21600, + service_level="24-hour-SLA", + start_time="00:00:00", + end_time="23:59:59", + condition="doc.issue_type == 'Critical'", ) + def make_holiday_list(): holiday_list = frappe.db.exists("Holiday List", "__Test Holiday List") if not holiday_list: - holiday_list = frappe.get_doc({ - "doctype": "Holiday List", - "holiday_list_name": "__Test Holiday List", - "from_date": "2019-01-01", - "to_date": "2019-12-31", - "holidays": [ - { - "description": "Test Holiday 1", - "holiday_date": "2019-03-05" - }, - { - "description": "Test Holiday 2", - "holiday_date": "2019-03-07" - }, - { - "description": "Test Holiday 3", - "holiday_date": "2019-02-11" - }, - ] - }).insert() + holiday_list = frappe.get_doc( + { + "doctype": "Holiday List", + "holiday_list_name": "__Test Holiday List", + "from_date": "2019-01-01", + "to_date": "2019-12-31", + "holidays": [ + {"description": "Test Holiday 1", "holiday_date": "2019-03-05"}, + {"description": "Test Holiday 2", "holiday_date": "2019-03-07"}, + {"description": "Test Holiday 3", "holiday_date": "2019-02-11"}, + ], + } + ).insert() + def create_custom_doctype(): if not frappe.db.exists("DocType", "Test SLA on Custom Dt"): - doc = frappe.get_doc({ + doc = frappe.get_doc( + { "doctype": "DocType", "module": "Support", "custom": 1, "fields": [ - { - "label": "Date", - "fieldname": "date", - "fieldtype": "Date" - }, - { - "label": "Description", - "fieldname": "desc", - "fieldtype": "Long Text" - }, - { - "label": "Email ID", - "fieldname": "email_id", - "fieldtype": "Link", - "options": "Customer" - }, + {"label": "Date", "fieldname": "date", "fieldtype": "Date"}, + {"label": "Description", "fieldname": "desc", "fieldtype": "Long Text"}, + {"label": "Email ID", "fieldname": "email_id", "fieldtype": "Link", "options": "Customer"}, { "label": "Status", "fieldname": "status", "fieldtype": "Select", - "options": "Open\nReplied\nClosed" - } + "options": "Open\nReplied\nClosed", + }, ], - "permissions": [{ - "role": "System Manager", - "read": 1, - "write": 1 - }], + "permissions": [{"role": "System Manager", "read": 1, "write": 1}], "name": "Test SLA on Custom Dt", - }) + } + ) doc.insert() return doc else: return frappe.get_doc("DocType", "Test SLA on Custom Dt") + def make_lead(creation=None, index=0): - return frappe.get_doc({ - "doctype": "Lead", - "email_id": "test_lead1@example{0}.com".format(index), - "lead_name": "_Test Lead {0}".format(index), - "status": "Open", - "creation": creation, - "service_level_agreement_creation": creation, - "priority": "Medium" - }).insert(ignore_permissions=True) + return frappe.get_doc( + { + "doctype": "Lead", + "email_id": "test_lead1@example{0}.com".format(index), + "lead_name": "_Test Lead {0}".format(index), + "status": "Open", + "creation": creation, + "service_level_agreement_creation": creation, + "priority": "Medium", + } + ).insert(ignore_permissions=True) diff --git a/erpnext/support/doctype/warranty_claim/test_warranty_claim.py b/erpnext/support/doctype/warranty_claim/test_warranty_claim.py index f022d55a4b..19e23493fe 100644 --- a/erpnext/support/doctype/warranty_claim/test_warranty_claim.py +++ b/erpnext/support/doctype/warranty_claim/test_warranty_claim.py @@ -5,7 +5,8 @@ import unittest import frappe -test_records = frappe.get_test_records('Warranty Claim') +test_records = frappe.get_test_records("Warranty Claim") + class TestWarrantyClaim(unittest.TestCase): pass diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.py b/erpnext/support/doctype/warranty_claim/warranty_claim.py index 87e9541026..5e2ea067a8 100644 --- a/erpnext/support/doctype/warranty_claim/warranty_claim.py +++ b/erpnext/support/doctype/warranty_claim/warranty_claim.py @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt - import frappe from frappe import _, session from frappe.utils import now_datetime @@ -15,27 +14,33 @@ class WarrantyClaim(TransactionBase): return _("{0}: From {1}").format(self.status, self.customer_name) def validate(self): - if session['user'] != 'Guest' and not self.customer: + if session["user"] != "Guest" and not self.customer: frappe.throw(_("Customer is required")) - if self.status=="Closed" and not self.resolution_date and \ - frappe.db.get_value("Warranty Claim", self.name, "status")!="Closed": + if ( + self.status == "Closed" + and not self.resolution_date + and frappe.db.get_value("Warranty Claim", self.name, "status") != "Closed" + ): self.resolution_date = now_datetime() def on_cancel(self): - lst = frappe.db.sql("""select t1.name + lst = frappe.db.sql( + """select t1.name from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 where t2.parent = t1.name and t2.prevdoc_docname = %s and t1.docstatus!=2""", - (self.name)) + (self.name), + ) if lst: - lst1 = ','.join(x[0] for x in lst) + lst1 = ",".join(x[0] for x in lst) frappe.throw(_("Cancel Material Visit {0} before cancelling this Warranty Claim").format(lst1)) else: - frappe.db.set(self, 'status', 'Cancelled') + frappe.db.set(self, "status", "Cancelled") def on_update(self): pass + @frappe.whitelist() def make_maintenance_visit(source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc, map_child_doc @@ -44,25 +49,25 @@ def make_maintenance_visit(source_name, target_doc=None): target_doc.prevdoc_doctype = source_parent.doctype target_doc.prevdoc_docname = source_parent.name - visit = frappe.db.sql("""select t1.name + visit = frappe.db.sql( + """select t1.name from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 where t2.parent=t1.name and t2.prevdoc_docname=%s - and t1.docstatus=1 and t1.completion_status='Fully Completed'""", source_name) + and t1.docstatus=1 and t1.completion_status='Fully Completed'""", + source_name, + ) if not visit: - target_doc = get_mapped_doc("Warranty Claim", source_name, { - "Warranty Claim": { - "doctype": "Maintenance Visit", - "field_map": {} - } - }, target_doc) + target_doc = get_mapped_doc( + "Warranty Claim", + source_name, + {"Warranty Claim": {"doctype": "Maintenance Visit", "field_map": {}}}, + target_doc, + ) source_doc = frappe.get_doc("Warranty Claim", source_name) if source_doc.get("item_code"): - table_map = { - "doctype": "Maintenance Visit Purpose", - "postprocess": _update_links - } + table_map = {"doctype": "Maintenance Visit Purpose", "postprocess": _update_links} map_child_doc(source_doc, target_doc, table_map, source_doc) return target_doc diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py index 2ab0fb88a7..5b51ef81c7 100644 --- a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py +++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py @@ -7,21 +7,17 @@ import frappe def execute(filters=None): columns = [ + {"fieldname": "creation_date", "label": "Date", "fieldtype": "Date", "width": 300}, { - 'fieldname': 'creation_date', - 'label': 'Date', - 'fieldtype': 'Date', - 'width': 300 - }, - { - 'fieldname': 'first_response_time', - 'fieldtype': 'Duration', - 'label': 'First Response Time', - 'width': 300 + "fieldname": "first_response_time", + "fieldtype": "Duration", + "label": "First Response Time", + "width": 300, }, ] - data = frappe.db.sql(''' + data = frappe.db.sql( + """ SELECT date(creation) as creation_date, avg(first_response_time) as avg_response_time @@ -31,6 +27,8 @@ def execute(filters=None): and first_response_time > 0 GROUP BY creation_date ORDER BY creation_date desc - ''', (filters.from_date, filters.to_date)) + """, + (filters.from_date, filters.to_date), + ) return columns, data diff --git a/erpnext/support/report/issue_analytics/issue_analytics.py b/erpnext/support/report/issue_analytics/issue_analytics.py index 056f2e0b41..00ba25a6a9 100644 --- a/erpnext/support/report/issue_analytics/issue_analytics.py +++ b/erpnext/support/report/issue_analytics/issue_analytics.py @@ -14,6 +14,7 @@ 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""" @@ -30,101 +31,98 @@ class IssueAnalytics(object): 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 - }) + 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 == "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 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 - }) + 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": _(period), "fieldname": scrub(period), "fieldtype": "Int", "width": 120} + ) - self.columns.append({ - 'label': _('Total'), - 'fieldname': 'total', - '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'] + 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': + 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) + 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) + 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 MO, relativedelta + 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) + increment = {"Monthly": 1, "Quarterly": 3, "Half-Yearly": 6, "Yearly": 12}.get( + self.filters.range, 1 + ) - if self.filters.range in ['Monthly', 'Quarterly']: + if self.filters.range in ["Monthly", "Quarterly"]: from_date = from_date.replace(day=1) - elif self.filters.range == 'Yearly': + 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': + 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) @@ -141,25 +139,26 @@ class IssueAnalytics(object): def get_issues(self): filters = self.get_common_filters() self.field_map = { - 'Customer': 'customer', - 'Issue Type': 'issue_type', - 'Issue Priority': 'priority', - 'Assigned To': '_assign' + "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 + self.entries = frappe.db.get_all( + "Issue", + fields=[self.field_map.get(self.filters.based_on), "name", "opening_date"], + filters=filters, ) def get_common_filters(self): filters = {} - filters['opening_date'] = ('between', [self.filters.from_date, self.filters.to_date]) + 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') + '%') + if self.filters.get("assigned_to"): + filters["_assign"] = ("like", "%" + self.filters.get("assigned_to") + "%") - for entry in ['company', 'status', 'priority', 'customer', 'project']: + for entry in ["company", "status", "priority", "customer", "project"]: if self.filters.get(entry): filters[entry] = self.filters.get(entry) @@ -170,14 +169,14 @@ class IssueAnalytics(object): self.get_periodic_data() for entity, period_data in self.issue_periodic_data.items(): - 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} + 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: @@ -186,7 +185,7 @@ class IssueAnalytics(object): row[scrub(period)] = amount total += amount - row['total'] = total + row["total"] = total self.data.append(row) @@ -194,9 +193,9 @@ class IssueAnalytics(object): self.issue_periodic_data = frappe._dict() for d in self.entries: - period = self.get_period(d.get('opening_date')) + period = self.get_period(d.get("opening_date")) - if self.filters.based_on == 'Assigned To': + 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) @@ -206,18 +205,12 @@ class IssueAnalytics(object): field = self.field_map.get(self.filters.based_on) value = d.get(field) if not value: - value = _('Not Specified') + 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' - } + labels = [d.get("label") for d in self.columns[1 : length - 1]] + self.chart = {"data": {"labels": labels, "datasets": []}, "type": "line"} diff --git a/erpnext/support/report/issue_analytics/test_issue_analytics.py b/erpnext/support/report/issue_analytics/test_issue_analytics.py index ba4dc54887..169392e5e9 100644 --- a/erpnext/support/report/issue_analytics/test_issue_analytics.py +++ b/erpnext/support/report/issue_analytics/test_issue_analytics.py @@ -10,7 +10,8 @@ from erpnext.support.doctype.service_level_agreement.test_service_level_agreemen ) from erpnext.support.report.issue_analytics.issue_analytics import execute -months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] +months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + class TestIssueAnalytics(unittest.TestCase): @classmethod @@ -23,8 +24,8 @@ class TestIssueAnalytics(unittest.TestCase): self.current_month = str(months[current_month_date.month - 1]).lower() self.last_month = str(months[last_month_date.month - 1]).lower() if current_month_date.year != last_month_date.year: - self.current_month += '_' + str(current_month_date.year) - self.last_month += '_' + str(last_month_date.year) + self.current_month += "_" + str(current_month_date.year) + self.last_month += "_" + str(last_month_date.year) def test_issue_analytics(self): create_service_level_agreements_for_issues() @@ -38,146 +39,88 @@ class TestIssueAnalytics(unittest.TestCase): def compare_result_for_customer(self): filters = { - 'company': '_Test Company', - 'based_on': 'Customer', - 'from_date': add_months(getdate(), -1), - 'to_date': getdate(), - 'range': 'Monthly' + "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 - } + {"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 + 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' + "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 - } + {"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 + 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' + "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 - } + {"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 + 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' + "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 - } + {"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 + 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() + 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(): @@ -189,29 +132,15 @@ def create_records(): 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 - }) + 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 - }) + 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 - }) + 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 - }) + add_assignment( + {"assign_to": ["test@example.com", "test1@example.com"], "doctype": "Issue", "name": issue.name} + ) diff --git a/erpnext/support/report/issue_summary/issue_summary.py b/erpnext/support/report/issue_summary/issue_summary.py index 67fe345d5f..c80ce88222 100644 --- a/erpnext/support/report/issue_summary/issue_summary.py +++ b/erpnext/support/report/issue_summary/issue_summary.py @@ -12,6 +12,7 @@ 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 {}) @@ -27,83 +28,78 @@ class IssueSummary(object): 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 - }) + 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 == "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 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 - }) + 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', 'On Hold', 'Resolved', 'Closed'] + self.statuses = ["Open", "Replied", "On Hold", "Resolved", "Closed"] for status in self.statuses: - self.columns.append({ - 'label': _(status), - 'fieldname': scrub(status), - 'fieldtype': 'Int', - 'width': 80 - }) + 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.columns.append( + {"label": _("Total Issues"), "fieldname": "total_issues", "fieldtype": "Int", "width": 100} + ) self.sla_status_map = { - 'SLA Failed': 'failed', - 'SLA Fulfilled': 'fulfilled', - 'First Response Due': 'first_response_due', - 'Resolution Due': 'resolution_due' + "SLA Failed": "failed", + "SLA Fulfilled": "fulfilled", + "First Response Due": "first_response_due", + "Resolution Due": "resolution_due", } for label, fieldname in self.sla_status_map.items(): - self.columns.append({ - 'label': _(label), - 'fieldname': fieldname, - 'fieldtype': 'Int', - 'width': 100 - }) + 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'] + 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 - }) + self.columns.append( + {"label": _(metric), "fieldname": scrub(metric), "fieldtype": "Duration", "width": 170} + ) def get_data(self): self.get_issues() @@ -112,26 +108,37 @@ class IssueSummary(object): def get_issues(self): filters = self.get_common_filters() self.field_map = { - 'Customer': 'customer', - 'Issue Type': 'issue_type', - 'Issue Priority': 'priority', - 'Assigned To': '_assign' + "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 + 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]) + 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') + '%') + if self.filters.get("assigned_to"): + filters["_assign"] = ("like", "%" + self.filters.get("assigned_to") + "%") - for entry in ['company', 'status', 'priority', 'customer', 'project']: + for entry in ["company", "status", "priority", "customer", "project"]: if self.filters.get(entry): filters[entry] = self.filters.get(entry) @@ -142,20 +149,20 @@ class IssueSummary(object): self.get_summary_data() for entity, data in self.issue_summary_data.items(): - 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} + 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) + 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) @@ -174,36 +181,41 @@ class IssueSummary(object): status = d.status agreement_status = scrub(d.agreement_status) - if self.filters.based_on == 'Assigned To': + 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.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 + 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') + 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.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.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'] + 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) @@ -211,7 +223,7 @@ class IssueSummary(object): field = self.field_map.get(self.filters.based_on) if issues: - if self.filters.based_on == 'Assigned To': + if self.filters.based_on == "Assigned To": assignment_map = frappe._dict() for d in self.entries: if d._assign: @@ -219,11 +231,15 @@ class IssueSummary(object): 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 + 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 @@ -234,7 +250,8 @@ class IssueSummary(object): self.issue_summary_data[entry][metric] /= flt(assignment_map.get(entry)) else: - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT {0}, AVG(first_response_time) as avg_frt, AVG(avg_response_time) as avg_resp_time, @@ -245,21 +262,30 @@ class IssueSummary(object): WHERE name IN %(issues)s GROUP BY {0} - """.format(field), {'issues': issues}, as_dict=1) + """.format( + field + ), + {"issues": issues}, + as_dict=1, + ) for entry in data: value = entry.get(field) if not value: - value = _('Not Specified') + 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 + 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): self.chart = [] @@ -273,47 +299,30 @@ class IssueSummary(object): entity = self.filters.based_on entity_field = self.field_map.get(entity) - if entity == 'Assigned To': - entity_field = 'user' + 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')) - on_hold_issues.append(entry.get('on_hold')) - resolved_issues.append(entry.get('resolved')) - closed_issues.append(entry.get('closed')) + open_issues.append(entry.get("open")) + replied_issues.append(entry.get("replied")) + on_hold_issues.append(entry.get("on_hold")) + 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': 'On Hold', - 'values': on_hold_issues[:30] - }, - { - 'name': 'Resolved', - 'values': resolved_issues[:30] - }, - { - 'name': 'Closed', - 'values': closed_issues[:30] - } - ] + "data": { + "labels": labels[:30], + "datasets": [ + {"name": "Open", "values": open_issues[:30]}, + {"name": "Replied", "values": replied_issues[:30]}, + {"name": "On Hold", "values": on_hold_issues[:30]}, + {"name": "Resolved", "values": resolved_issues[:30]}, + {"name": "Closed", "values": closed_issues[:30]}, + ], }, - 'type': 'bar', - 'barOptions': { - 'stacked': True - } + "type": "bar", + "barOptions": {"stacked": True}, } def get_report_summary(self): @@ -326,41 +335,41 @@ class IssueSummary(object): closed = 0 for entry in self.data: - open_issues += entry.get('open') - replied += entry.get('replied') - on_hold += entry.get('on_hold') - resolved += entry.get('resolved') - closed += entry.get('closed') + open_issues += entry.get("open") + replied += entry.get("replied") + on_hold += entry.get("on_hold") + resolved += entry.get("resolved") + closed += entry.get("closed") self.report_summary = [ { - 'value': open_issues, - 'indicator': 'Red', - 'label': _('Open'), - 'datatype': 'Int', + "value": open_issues, + "indicator": "Red", + "label": _("Open"), + "datatype": "Int", }, { - 'value': replied, - 'indicator': 'Grey', - 'label': _('Replied'), - 'datatype': 'Int', + "value": replied, + "indicator": "Grey", + "label": _("Replied"), + "datatype": "Int", }, { - 'value': on_hold, - 'indicator': 'Grey', - 'label': _('On Hold'), - 'datatype': 'Int', + "value": on_hold, + "indicator": "Grey", + "label": _("On Hold"), + "datatype": "Int", }, { - 'value': resolved, - 'indicator': 'Green', - 'label': _('Resolved'), - 'datatype': 'Int', + "value": resolved, + "indicator": "Green", + "label": _("Resolved"), + "datatype": "Int", }, { - 'value': closed, - 'indicator': 'Green', - 'label': _('Closed'), - 'datatype': 'Int', - } + "value": closed, + "indicator": "Green", + "label": _("Closed"), + "datatype": "Int", + }, ] diff --git a/erpnext/support/report/support_hour_distribution/support_hour_distribution.py b/erpnext/support/report/support_hour_distribution/support_hour_distribution.py index 6b2098f4a8..54967213af 100644 --- a/erpnext/support/report/support_hour_distribution/support_hour_distribution.py +++ b/erpnext/support/report/support_hour_distribution/support_hour_distribution.py @@ -7,34 +7,36 @@ from frappe import _ from frappe.utils import add_to_date, get_datetime, getdate time_slots = { - '12AM - 3AM': '00:00:00-03:00:00', - '3AM - 6AM': '03:00:00-06:00:00', - '6AM - 9AM': '06:00:00-09:00:00', - '9AM - 12PM': '09:00:00-12:00:00', - '12PM - 3PM': '12:00:00-15:00:00', - '3PM - 6PM': '15:00:00-18:00:00', - '6PM - 9PM': '18:00:00-21:00:00', - '9PM - 12AM': '21:00:00-23:00:00' + "12AM - 3AM": "00:00:00-03:00:00", + "3AM - 6AM": "03:00:00-06:00:00", + "6AM - 9AM": "06:00:00-09:00:00", + "9AM - 12PM": "09:00:00-12:00:00", + "12PM - 3PM": "12:00:00-15:00:00", + "3PM - 6PM": "15:00:00-18:00:00", + "6PM - 9PM": "18:00:00-21:00:00", + "9PM - 12AM": "21:00:00-23:00:00", } + def execute(filters=None): columns, data = [], [] - if not filters.get('periodicity'): - filters['periodicity'] = 'Daily' + if not filters.get("periodicity"): + filters["periodicity"] = "Daily" columns = get_columns() data, timeslot_wise_count = get_data(filters) chart = get_chart_data(timeslot_wise_count) return columns, data, None, chart + def get_data(filters): start_date = getdate(filters.from_date) data = [] time_slot_wise_total_count = {} - while(start_date <= getdate(filters.to_date)): - hours_count = {'date': start_date} + while start_date <= getdate(filters.to_date): + hours_count = {"date": start_date} for key, value in time_slots.items(): - start_time, end_time = value.split('-') + start_time, end_time = value.split("-") start_time = get_datetime("{0} {1}".format(start_date.strftime("%Y-%m-%d"), start_time)) end_time = get_datetime("{0} {1}".format(start_date.strftime("%Y-%m-%d"), end_time)) hours_count[key] = get_hours_count(start_time, end_time) @@ -47,49 +49,57 @@ def get_data(filters): return data, time_slot_wise_total_count + def get_hours_count(start_time, end_time): - data = frappe.db.sql(""" select count(*) from `tabIssue` where creation - between %(start_time)s and %(end_time)s""", { - 'start_time': start_time, - 'end_time': end_time - }, as_list=1) or [] + data = ( + frappe.db.sql( + """ select count(*) from `tabIssue` where creation + between %(start_time)s and %(end_time)s""", + {"start_time": start_time, "end_time": end_time}, + as_list=1, + ) + or [] + ) return data[0][0] if len(data) > 0 else 0 -def get_columns(): - columns = [{ - "fieldname": "date", - "label": _("Date"), - "fieldtype": "Date", - "width": 100 - }] - for label in ['12AM - 3AM', '3AM - 6AM', '6AM - 9AM', - '9AM - 12PM', '12PM - 3PM', '3PM - 6PM', '6PM - 9PM', '9PM - 12AM']: - columns.append({ - "fieldname": label, - "label": _(label), - "fieldtype": "Data", - "width": 120 - }) +def get_columns(): + columns = [{"fieldname": "date", "label": _("Date"), "fieldtype": "Date", "width": 100}] + + for label in [ + "12AM - 3AM", + "3AM - 6AM", + "6AM - 9AM", + "9AM - 12PM", + "12PM - 3PM", + "3PM - 6PM", + "6PM - 9PM", + "9PM - 12AM", + ]: + columns.append({"fieldname": label, "label": _(label), "fieldtype": "Data", "width": 120}) return columns + def get_chart_data(timeslot_wise_count): total_count = [] - timeslots = ['12AM - 3AM', '3AM - 6AM', '6AM - 9AM', - '9AM - 12PM', '12PM - 3PM', '3PM - 6PM', '6PM - 9PM', '9PM - 12AM'] + timeslots = [ + "12AM - 3AM", + "3AM - 6AM", + "6AM - 9AM", + "9AM - 12PM", + "12PM - 3PM", + "3PM - 6PM", + "6PM - 9PM", + "9PM - 12AM", + ] datasets = [] for data in timeslots: total_count.append(timeslot_wise_count.get(data, 0)) - datasets.append({'values': total_count}) + datasets.append({"values": total_count}) - chart = { - "data": { - 'labels': timeslots, - 'datasets': datasets - } - } + chart = {"data": {"labels": timeslots, "datasets": datasets}} chart["type"] = "line" return chart diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 0c24484bdf..1c88883abc 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -11,8 +11,8 @@ from frappe.model.document import Document from erpnext.crm.doctype.lead.lead import get_lead_with_phone_number from erpnext.crm.doctype.utils import get_scheduled_employees_for_popup, strip_number -END_CALL_STATUSES = ['No Answer', 'Completed', 'Busy', 'Failed'] -ONGOING_CALL_STATUSES = ['Ringing', 'In Progress'] +END_CALL_STATUSES = ["No Answer", "Completed", "Busy", "Failed"] +ONGOING_CALL_STATUSES = ["Ringing", "In Progress"] class CallLog(Document): @@ -20,18 +20,17 @@ class CallLog(Document): deduplicate_dynamic_links(self) def before_insert(self): - """Add lead(third party person) links to the document. - """ - lead_number = self.get('from') if self.is_incoming_call() else self.get('to') + """Add lead(third party person) links to the document.""" + lead_number = self.get("from") if self.is_incoming_call() else self.get("to") lead_number = strip_number(lead_number) contact = get_contact_with_phone_number(strip_number(lead_number)) if contact: - self.add_link(link_type='Contact', link_name=contact) + self.add_link(link_type="Contact", link_name=contact) lead = get_lead_with_phone_number(lead_number) if lead: - self.add_link(link_type='Lead', link_name=lead) + self.add_link(link_type="Lead", link_name=lead) def after_insert(self): self.trigger_call_popup() @@ -39,29 +38,29 @@ class CallLog(Document): def on_update(self): def _is_call_missed(doc_before_save, doc_after_save): # FIXME: This works for Exotel but not for all telepony providers - return doc_before_save.to != doc_after_save.to and doc_after_save.status not in END_CALL_STATUSES + return ( + doc_before_save.to != doc_after_save.to and doc_after_save.status not in END_CALL_STATUSES + ) def _is_call_ended(doc_before_save, doc_after_save): return doc_before_save.status not in END_CALL_STATUSES and self.status in END_CALL_STATUSES doc_before_save = self.get_doc_before_save() - if not doc_before_save: return + if not doc_before_save: + return if _is_call_missed(doc_before_save, self): - frappe.publish_realtime('call_{id}_missed'.format(id=self.id), self) + frappe.publish_realtime("call_{id}_missed".format(id=self.id), self) self.trigger_call_popup() if _is_call_ended(doc_before_save, self): - frappe.publish_realtime('call_{id}_ended'.format(id=self.id), self) + frappe.publish_realtime("call_{id}_ended".format(id=self.id), self) def is_incoming_call(self): - return self.type == 'Incoming' + return self.type == "Incoming" def add_link(self, link_type, link_name): - self.append('links', { - 'link_doctype': link_type, - 'link_name': link_name - }) + self.append("links", {"link_doctype": link_type, "link_name": link_name}) def trigger_call_popup(self): if self.is_incoming_call(): @@ -72,53 +71,63 @@ class CallLog(Document): emails = set(scheduled_employees).intersection(employee_emails) if frappe.conf.developer_mode: - self.add_comment(text=f""" + self.add_comment( + text=f""" Scheduled Employees: {scheduled_employees} Matching Employee: {employee_emails} Show Popup To: {emails} - """) + """ + ) if employee_emails and not emails: self.add_comment(text=_("No employee was scheduled for call popup")) for email in emails: - frappe.publish_realtime('show_call_popup', self, user=email) + frappe.publish_realtime("show_call_popup", self, user=email) @frappe.whitelist() def add_call_summary(call_log, summary): - doc = frappe.get_doc('Call Log', call_log) - doc.add_comment('Comment', frappe.bold(_('Call Summary')) + '

    ' + summary) + doc = frappe.get_doc("Call Log", call_log) + doc.add_comment("Comment", frappe.bold(_("Call Summary")) + "

    " + summary) + def get_employees_with_number(number): number = strip_number(number) - if not number: return [] + if not number: + return [] - employee_emails = frappe.cache().hget('employees_with_number', number) - if employee_emails: return employee_emails + employee_emails = frappe.cache().hget("employees_with_number", number) + if employee_emails: + return employee_emails - employees = frappe.get_all('Employee', filters={ - 'cell_number': ['like', '%{}%'.format(number)], - 'user_id': ['!=', ''] - }, fields=['user_id']) + employees = frappe.get_all( + "Employee", + filters={"cell_number": ["like", "%{}%".format(number)], "user_id": ["!=", ""]}, + fields=["user_id"], + ) employee_emails = [employee.user_id for employee in employees] - frappe.cache().hset('employees_with_number', number, employee_emails) + frappe.cache().hset("employees_with_number", number, employee_emails) return employee_emails + def link_existing_conversations(doc, state): """ Called from hooks on creation of Contact or Lead to link all the existing conversations. """ - if doc.doctype != 'Contact': return + if doc.doctype != "Contact": + return try: numbers = [d.phone for d in doc.phone_nos] for number in numbers: number = strip_number(number) - if not number: continue - logs = frappe.db.sql_list(""" + if not number: + continue + logs = frappe.db.sql_list( + """ SELECT cl.name FROM `tabCall Log` cl LEFT JOIN `tabDynamic Link` dl ON cl.name = dl.parent @@ -131,44 +140,42 @@ def link_existing_conversations(doc, state): ELSE 0 END )=0 - """, dict( - phone_number='%{}'.format(number), - docname=doc.name, - doctype = doc.doctype - ) + """, + dict(phone_number="%{}".format(number), docname=doc.name, doctype=doc.doctype), ) for log in logs: - call_log = frappe.get_doc('Call Log', log) + call_log = frappe.get_doc("Call Log", log) call_log.add_link(link_type=doc.doctype, link_name=doc.name) call_log.save(ignore_permissions=True) frappe.db.commit() except Exception: - frappe.log_error(title=_('Error during caller information update')) + frappe.log_error(title=_("Error during caller information update")) + def get_linked_call_logs(doctype, docname): # content will be shown in timeline - logs = frappe.get_all('Dynamic Link', fields=['parent'], filters={ - 'parenttype': 'Call Log', - 'link_doctype': doctype, - 'link_name': docname - }) + logs = frappe.get_all( + "Dynamic Link", + fields=["parent"], + filters={"parenttype": "Call Log", "link_doctype": doctype, "link_name": docname}, + ) logs = set([log.parent for log in logs]) - logs = frappe.get_all('Call Log', fields=['*'], filters={ - 'name': ['in', logs] - }) + logs = frappe.get_all("Call Log", fields=["*"], filters={"name": ["in", logs]}) timeline_contents = [] for log in logs: log.show_call_button = 0 - timeline_contents.append({ - 'icon': 'call', - 'is_card': True, - 'creation': log.creation, - 'template': 'call_link', - 'template_data': log - }) + timeline_contents.append( + { + "icon": "call", + "is_card": True, + "creation": log.creation, + "template": "call_link", + "template_data": log, + } + ) return timeline_contents diff --git a/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py index 08e244d889..5edf81df73 100644 --- a/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py +++ b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py @@ -20,35 +20,38 @@ class IncomingCallSettings(Document): self.validate_call_schedule_overlaps(self.call_handling_schedule) def validate_call_schedule_timeslot(self, schedule: list): - """ Make sure that to time slot is ahead of from time slot. - """ + """Make sure that to time slot is ahead of from time slot.""" errors = [] for record in schedule: from_time = self.time_to_seconds(record.from_time) to_time = self.time_to_seconds(record.to_time) if from_time >= to_time: errors.append( - _('Call Schedule Row {0}: To time slot should always be ahead of From time slot.').format(record.idx) + _("Call Schedule Row {0}: To time slot should always be ahead of From time slot.").format( + record.idx + ) ) if errors: - frappe.throw('
    '.join(errors)) + frappe.throw("
    ".join(errors)) def validate_call_schedule_overlaps(self, schedule: list): - """Check if any time slots are overlapped in a day schedule. - """ + """Check if any time slots are overlapped in a day schedule.""" week_days = set([each.day_of_week for each in schedule]) for day in week_days: - timeslots = [(record.from_time, record.to_time) for record in schedule if record.day_of_week==day] + timeslots = [ + (record.from_time, record.to_time) for record in schedule if record.day_of_week == day + ] # convert time in timeslot into an integer represents number of seconds timeslots = sorted(map(lambda seq: tuple(map(self.time_to_seconds, seq)), timeslots)) - if len(timeslots) < 2: continue + if len(timeslots) < 2: + continue for i in range(1, len(timeslots)): - if self.check_timeslots_overlap(timeslots[i-1], timeslots[i]): - frappe.throw(_('Please fix overlapping time slots for {0}.').format(day)) + if self.check_timeslots_overlap(timeslots[i - 1], timeslots[i]): + frappe.throw(_("Please fix overlapping time slots for {0}.").format(day)) @staticmethod def check_timeslots_overlap(ts1: Tuple[int, int], ts2: Tuple[int, int]) -> bool: @@ -58,7 +61,6 @@ class IncomingCallSettings(Document): @staticmethod def time_to_seconds(time: str) -> int: - """Convert time string of format HH:MM:SS into seconds - """ + """Convert time string of format HH:MM:SS into seconds""" date_time = datetime.strptime(time, "%H:%M:%S") return date_time - datetime(1900, 1, 1) diff --git a/erpnext/templates/pages/courses.py b/erpnext/templates/pages/courses.py index 6051e60aa3..fb1af387d2 100644 --- a/erpnext/templates/pages/courses.py +++ b/erpnext/templates/pages/courses.py @@ -6,13 +6,13 @@ import frappe def get_context(context): - course = frappe.get_doc('Course', frappe.form_dict.course) + course = frappe.get_doc("Course", frappe.form_dict.course) sidebar_title = course.name context.no_cache = 1 context.show_sidebar = True - course = frappe.get_doc('Course', frappe.form_dict.course) - course.has_permission('read') + course = frappe.get_doc("Course", frappe.form_dict.course) + course.has_permission("read") context.doc = course context.sidebar_title = sidebar_title context.intro = course.course_intro diff --git a/erpnext/templates/pages/help.py b/erpnext/templates/pages/help.py index 6a83fc8b57..19993ee9b1 100644 --- a/erpnext/templates/pages/help.py +++ b/erpnext/templates/pages/help.py @@ -25,21 +25,19 @@ def get_context(context): else: context.issues = [] + def get_forum_posts(s): - response = requests.get(s.forum_url + '/' + s.get_latest_query) + response = requests.get(s.forum_url + "/" + s.get_latest_query) response.raise_for_status() response_json = response.json() - topics_data = {} # it will actually be an array - key_list = s.response_key_list.split(',') + topics_data = {} # it will actually be an array + key_list = s.response_key_list.split(",") for key in key_list: topics_data = response_json.get(key) if not topics_data else topics_data.get(key) for topic in topics_data: - topic["link"] = s.forum_url + '/' + s.post_route_string + '/' + str(topic.get(s.post_route_key)) + topic["link"] = s.forum_url + "/" + s.post_route_string + "/" + str(topic.get(s.post_route_key)) - post_params = { - "title": s.post_title_key, - "description": s.post_description_key - } + post_params = {"title": s.post_title_key, "description": s.post_description_key} return topics_data, post_params diff --git a/erpnext/templates/pages/home.py b/erpnext/templates/pages/home.py index d08e81b9e6..bca3e56053 100644 --- a/erpnext/templates/pages/home.py +++ b/erpnext/templates/pages/home.py @@ -6,46 +6,51 @@ import frappe no_cache = 1 + def get_context(context): - homepage = frappe.get_doc('Homepage') + homepage = frappe.get_doc("Homepage") for item in homepage.products: - route = frappe.db.get_value('Website Item', {"item_code": item.item_code}, 'route') + route = frappe.db.get_value("Website Item", {"item_code": item.item_code}, "route") if route: - item.route = '/' + route + item.route = "/" + route homepage.title = homepage.title or homepage.company context.title = homepage.title context.homepage = homepage - if homepage.hero_section_based_on == 'Homepage Section' and homepage.hero_section: - homepage.hero_section_doc = frappe.get_doc('Homepage Section', homepage.hero_section) + if homepage.hero_section_based_on == "Homepage Section" and homepage.hero_section: + homepage.hero_section_doc = frappe.get_doc("Homepage Section", homepage.hero_section) if homepage.slideshow: - doc = frappe.get_doc('Website Slideshow', homepage.slideshow) + doc = frappe.get_doc("Website Slideshow", homepage.slideshow) context.slideshow = homepage.slideshow context.slideshow_header = doc.header context.slides = doc.slideshow_items - context.blogs = frappe.get_all('Blog Post', - fields=['title', 'blogger', 'blog_intro', 'route'], - filters={ - 'published': 1 - }, - order_by='modified desc', - limit=3 + context.blogs = frappe.get_all( + "Blog Post", + fields=["title", "blogger", "blog_intro", "route"], + filters={"published": 1}, + order_by="modified desc", + limit=3, ) # filter out homepage section which is used as hero section - homepage_hero_section = homepage.hero_section_based_on == 'Homepage Section' and homepage.hero_section - homepage_sections = frappe.get_all('Homepage Section', - filters=[['name', '!=', homepage_hero_section]] if homepage_hero_section else None, - order_by='section_order asc' + homepage_hero_section = ( + homepage.hero_section_based_on == "Homepage Section" and homepage.hero_section ) - context.homepage_sections = [frappe.get_doc('Homepage Section', name) for name in homepage_sections] + homepage_sections = frappe.get_all( + "Homepage Section", + filters=[["name", "!=", homepage_hero_section]] if homepage_hero_section else None, + order_by="section_order asc", + ) + context.homepage_sections = [ + frappe.get_doc("Homepage Section", name) for name in homepage_sections + ] context.metatags = context.metatags or frappe._dict({}) context.metatags.image = homepage.hero_image or None context.metatags.description = homepage.description or None - context.explore_link = '/all-products' + context.explore_link = "/all-products" diff --git a/erpnext/templates/pages/integrations/gocardless_checkout.py b/erpnext/templates/pages/integrations/gocardless_checkout.py index bbdbf1ddf9..280f67f16b 100644 --- a/erpnext/templates/pages/integrations/gocardless_checkout.py +++ b/erpnext/templates/pages/integrations/gocardless_checkout.py @@ -14,8 +14,18 @@ from erpnext.erpnext_integrations.doctype.gocardless_settings.gocardless_setting no_cache = 1 -expected_keys = ('amount', 'title', 'description', 'reference_doctype', 'reference_docname', - 'payer_name', 'payer_email', 'order_id', 'currency') +expected_keys = ( + "amount", + "title", + "description", + "reference_doctype", + "reference_docname", + "payer_name", + "payer_email", + "order_id", + "currency", +) + def get_context(context): context.no_cache = 1 @@ -25,17 +35,22 @@ def get_context(context): for key in expected_keys: context[key] = frappe.form_dict[key] - context['amount'] = flt(context['amount']) + context["amount"] = flt(context["amount"]) gateway_controller = get_gateway_controller(context.reference_docname) - context['header_img'] = frappe.db.get_value("GoCardless Settings", gateway_controller, "header_img") + context["header_img"] = frappe.db.get_value( + "GoCardless Settings", gateway_controller, "header_img" + ) else: - frappe.redirect_to_message(_('Some information is missing'), - _('Looks like someone sent you to an incomplete URL. Please ask them to look into it.')) + frappe.redirect_to_message( + _("Some information is missing"), + _("Looks like someone sent you to an incomplete URL. Please ask them to look into it."), + ) frappe.local.flags.redirect_location = frappe.local.response.location raise frappe.Redirect + @frappe.whitelist(allow_guest=True) def check_mandate(data, reference_doctype, reference_docname): data = json.loads(data) @@ -59,23 +74,27 @@ def check_mandate(data, reference_doctype, reference_docname): prefilled_customer.update({"email": frappe.session.user}) else: - prefilled_customer = { - "company_name": payer.name, - "email": frappe.session.user - } + prefilled_customer = {"company_name": payer.name, "email": frappe.session.user} - success_url = get_url("./integrations/gocardless_confirmation?reference_doctype=" + reference_doctype + "&reference_docname=" + reference_docname) + success_url = get_url( + "./integrations/gocardless_confirmation?reference_doctype=" + + reference_doctype + + "&reference_docname=" + + reference_docname + ) try: - redirect_flow = client.redirect_flows.create(params={ - "description": _("Pay {0} {1}").format(data['amount'], data['currency']), - "session_token": frappe.session.user, - "success_redirect_url": success_url, - "prefilled_customer": prefilled_customer - }) + redirect_flow = client.redirect_flows.create( + params={ + "description": _("Pay {0} {1}").format(data["amount"], data["currency"]), + "session_token": frappe.session.user, + "success_redirect_url": success_url, + "prefilled_customer": prefilled_customer, + } + ) return {"redirect_to": redirect_flow.redirect_url} except Exception as e: frappe.log_error(e, "GoCardless Payment Error") - return {"redirect_to": '/integrations/payment-failed'} + return {"redirect_to": "/integrations/payment-failed"} diff --git a/erpnext/templates/pages/integrations/gocardless_confirmation.py b/erpnext/templates/pages/integrations/gocardless_confirmation.py index a6c3e71494..cab532a530 100644 --- a/erpnext/templates/pages/integrations/gocardless_confirmation.py +++ b/erpnext/templates/pages/integrations/gocardless_confirmation.py @@ -11,7 +11,8 @@ from erpnext.erpnext_integrations.doctype.gocardless_settings.gocardless_setting no_cache = 1 -expected_keys = ('redirect_flow_id', 'reference_doctype', 'reference_docname') +expected_keys = ("redirect_flow_id", "reference_doctype", "reference_docname") + def get_context(context): context.no_cache = 1 @@ -22,11 +23,14 @@ def get_context(context): context[key] = frappe.form_dict[key] else: - frappe.redirect_to_message(_('Some information is missing'), - _('Looks like someone sent you to an incomplete URL. Please ask them to look into it.')) + frappe.redirect_to_message( + _("Some information is missing"), + _("Looks like someone sent you to an incomplete URL. Please ask them to look into it."), + ) frappe.local.flags.redirect_location = frappe.local.response.location raise frappe.Redirect + @frappe.whitelist(allow_guest=True) def confirm_payment(redirect_flow_id, reference_doctype, reference_docname): @@ -34,15 +38,15 @@ def confirm_payment(redirect_flow_id, reference_doctype, reference_docname): try: redirect_flow = client.redirect_flows.complete( - redirect_flow_id, - params={ - "session_token": frappe.session.user - }) + redirect_flow_id, params={"session_token": frappe.session.user} + ) confirmation_url = redirect_flow.confirmation_url - gocardless_success_page = frappe.get_hooks('gocardless_success_page') + gocardless_success_page = frappe.get_hooks("gocardless_success_page") if gocardless_success_page: - confirmation_url = frappe.get_attr(gocardless_success_page[-1])(reference_doctype, reference_docname) + confirmation_url = frappe.get_attr(gocardless_success_page[-1])( + reference_doctype, reference_docname + ) data = { "mandate": redirect_flow.links.mandate, @@ -50,7 +54,7 @@ def confirm_payment(redirect_flow_id, reference_doctype, reference_docname): "redirect_to": confirmation_url, "redirect_message": "Mandate successfully created", "reference_doctype": reference_doctype, - "reference_docname": reference_docname + "reference_docname": reference_docname, } try: @@ -65,29 +69,38 @@ def confirm_payment(redirect_flow_id, reference_doctype, reference_docname): except Exception as e: frappe.log_error(e, "GoCardless Payment Error") - return {"redirect_to": '/integrations/payment-failed'} + return {"redirect_to": "/integrations/payment-failed"} def create_mandate(data): data = frappe._dict(data) frappe.logger().debug(data) - mandate = data.get('mandate') + mandate = data.get("mandate") if frappe.db.exists("GoCardless Mandate", mandate): return else: - reference_doc = frappe.db.get_value(data.get('reference_doctype'), data.get('reference_docname'), ["reference_doctype", "reference_name"], as_dict=1) - erpnext_customer = frappe.db.get_value(reference_doc.reference_doctype, reference_doc.reference_name, ["customer_name"], as_dict=1) + reference_doc = frappe.db.get_value( + data.get("reference_doctype"), + data.get("reference_docname"), + ["reference_doctype", "reference_name"], + as_dict=1, + ) + erpnext_customer = frappe.db.get_value( + reference_doc.reference_doctype, reference_doc.reference_name, ["customer_name"], as_dict=1 + ) try: - frappe.get_doc({ - "doctype": "GoCardless Mandate", - "mandate": mandate, - "customer": erpnext_customer.customer_name, - "gocardless_customer": data.get('customer') - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "GoCardless Mandate", + "mandate": mandate, + "customer": erpnext_customer.customer_name, + "gocardless_customer": data.get("customer"), + } + ).insert(ignore_permissions=True) except Exception: frappe.log_error(frappe.get_traceback()) diff --git a/erpnext/templates/pages/material_request_info.py b/erpnext/templates/pages/material_request_info.py index 65d4427e11..301ca01cfc 100644 --- a/erpnext/templates/pages/material_request_info.py +++ b/erpnext/templates/pages/material_request_info.py @@ -20,17 +20,23 @@ def get_context(context): if not frappe.has_website_permission(context.doc): frappe.throw(_("Not Permitted"), frappe.PermissionError) - default_print_format = frappe.db.get_value('Property Setter', dict(property='default_print_format', doc_type=frappe.form_dict.doctype), "value") + default_print_format = frappe.db.get_value( + "Property Setter", + dict(property="default_print_format", doc_type=frappe.form_dict.doctype), + "value", + ) if default_print_format: context.print_format = default_print_format else: context.print_format = "Standard" context.doc.items = get_more_items_info(context.doc.items, context.doc.name) + def get_more_items_info(items, material_request): for item in items: - item.customer_provided = frappe.get_value('Item', item.item_code, 'is_customer_provided_item') - item.work_orders = frappe.db.sql(""" + item.customer_provided = frappe.get_value("Item", item.item_code, "is_customer_provided_item") + item.work_orders = frappe.db.sql( + """ select wo.name, wo.status, wo_item.consumed_qty from @@ -41,9 +47,16 @@ def get_more_items_info(items, material_request): and wo_item.parent=wo.name and wo.status not in ('Completed', 'Cancelled', 'Stopped') order by - wo.name asc""", item.item_code, as_dict=1) - item.delivered_qty = flt(frappe.db.sql("""select sum(transfer_qty) + wo.name asc""", + item.item_code, + as_dict=1, + ) + item.delivered_qty = flt( + frappe.db.sql( + """select sum(transfer_qty) from `tabStock Entry Detail` where material_request = %s and item_code = %s and docstatus = 1""", - (material_request, item.item_code))[0][0]) + (material_request, item.item_code), + )[0][0] + ) return items diff --git a/erpnext/templates/pages/order.py b/erpnext/templates/pages/order.py index 712b141def..3e6d57a02b 100644 --- a/erpnext/templates/pages/order.py +++ b/erpnext/templates/pages/order.py @@ -19,12 +19,17 @@ def get_context(context): context.parents = frappe.form_dict.parents context.title = frappe.form_dict.name - context.payment_ref = frappe.db.get_value("Payment Request", - {"reference_name": frappe.form_dict.name}, "name") + context.payment_ref = frappe.db.get_value( + "Payment Request", {"reference_name": frappe.form_dict.name}, "name" + ) context.enabled_checkout = frappe.get_doc("E Commerce Settings").enable_checkout - default_print_format = frappe.db.get_value('Property Setter', dict(property='default_print_format', doc_type=frappe.form_dict.doctype), "value") + default_print_format = frappe.db.get_value( + "Property Setter", + dict(property="default_print_format", doc_type=frappe.form_dict.doctype), + "value", + ) if default_print_format: context.print_format = default_print_format else: @@ -34,15 +39,23 @@ def get_context(context): frappe.throw(_("Not Permitted"), frappe.PermissionError) # check for the loyalty program of the customer - customer_loyalty_program = frappe.db.get_value("Customer", context.doc.customer, "loyalty_program") + customer_loyalty_program = frappe.db.get_value( + "Customer", context.doc.customer, "loyalty_program" + ) if customer_loyalty_program: from erpnext.accounts.doctype.loyalty_program.loyalty_program import ( get_loyalty_program_details_with_points, ) - loyalty_program_details = get_loyalty_program_details_with_points(context.doc.customer, customer_loyalty_program) + + loyalty_program_details = get_loyalty_program_details_with_points( + context.doc.customer, customer_loyalty_program + ) context.available_loyalty_points = int(loyalty_program_details.get("loyalty_points")) + def get_attachments(dt, dn): - return frappe.get_all("File", - fields=["name", "file_name", "file_url", "is_private"], - filters = {"attached_to_name": dn, "attached_to_doctype": dt, "is_private":0}) + return frappe.get_all( + "File", + fields=["name", "file_name", "file_url", "is_private"], + filters={"attached_to_name": dn, "attached_to_doctype": dt, "is_private": 0}, + ) diff --git a/erpnext/templates/pages/partners.py b/erpnext/templates/pages/partners.py index e4043ea8b9..8a49504ff0 100644 --- a/erpnext/templates/pages/partners.py +++ b/erpnext/templates/pages/partners.py @@ -6,11 +6,12 @@ import frappe page_title = "Partners" -def get_context(context): - partners = frappe.db.sql("""select * from `tabSales Partner` - where show_in_website=1 order by name asc""", as_dict=True) - return { - "partners": partners, - "title": page_title - } +def get_context(context): + partners = frappe.db.sql( + """select * from `tabSales Partner` + where show_in_website=1 order by name asc""", + as_dict=True, + ) + + return {"partners": partners, "title": page_title} diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py index 237adf99f5..77a749ef9e 100644 --- a/erpnext/templates/pages/product_search.py +++ b/erpnext/templates/pages/product_search.py @@ -17,9 +17,11 @@ from erpnext.setup.doctype.item_group.item_group import get_item_for_list_in_htm no_cache = 1 + def get_context(context): context.show_search = True + @frappe.whitelist(allow_guest=True) def get_product_list(search=None, start=0, limit=12): data = get_product_data(search, start, limit) @@ -29,6 +31,7 @@ def get_product_list(search=None, start=0, limit=12): return [get_item_for_list_in_html(r) for r in data] + def get_product_data(search=None, start=0, limit=12): # limit = 12 because we show 12 items in the grid view # base query @@ -53,7 +56,8 @@ def get_product_data(search=None, start=0, limit=12): # order by query += """ ORDER BY ranking desc, modified desc limit %s, %s""" % (cint(start), cint(limit)) - return frappe.db.sql(query, {"search": search}, as_dict=1) # nosemgrep + return frappe.db.sql(query, {"search": search}, as_dict=1) # nosemgrep + @frappe.whitelist(allow_guest=True) def search(query): @@ -62,9 +66,10 @@ def search(query): return { "product_results": product_results.get("results") or [], - "category_results": category_results.get("results") or [] + "category_results": category_results.get("results") or [], } + @frappe.whitelist(allow_guest=True) def product_search(query, limit=10, fuzzy_search=True): search_results = {"from_redisearch": True, "results": []} @@ -84,9 +89,7 @@ def product_search(query, limit=10, fuzzy_search=True): ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=red) client = Client(make_key(WEBSITE_ITEM_INDEX), conn=red) suggestions = ac.get_suggestions( - query, - num=limit, - fuzzy= fuzzy_search and len(query) > 3 # Fuzzy on length < 3 can be real slow + query, num=limit, fuzzy=fuzzy_search and len(query) > 3 # Fuzzy on length < 3 can be real slow ) # Build a query @@ -98,17 +101,22 @@ def product_search(query, limit=10, fuzzy_search=True): q = Query(query_string) results = client.search(q) - search_results['results'] = list(map(convert_to_dict, results.docs)) - search_results['results'] = sorted(search_results['results'], key=lambda k: frappe.utils.cint(k['ranking']), reverse=True) + search_results["results"] = list(map(convert_to_dict, results.docs)) + search_results["results"] = sorted( + search_results["results"], key=lambda k: frappe.utils.cint(k["ranking"]), reverse=True + ) return search_results + def clean_up_query(query): - return ''.join(c for c in query if c.isalnum() or c.isspace()) + return "".join(c for c in query if c.isalnum() or c.isspace()) + def convert_to_dict(redis_search_doc): return redis_search_doc.__dict__ + @frappe.whitelist(allow_guest=True) def get_category_suggestions(query): search_results = {"results": []} @@ -117,13 +125,10 @@ def get_category_suggestions(query): # Redisearch module not loaded, query db categories = frappe.db.get_all( "Item Group", - filters={ - "name": ["like", "%{0}%".format(query)], - "show_in_website": 1 - }, - fields=["name", "route"] + filters={"name": ["like", "%{0}%".format(query)], "show_in_website": 1}, + fields=["name", "route"], ) - search_results['results'] = categories + search_results["results"] = categories return search_results if not query: @@ -132,6 +137,6 @@ def get_category_suggestions(query): ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=frappe.cache()) suggestions = ac.get_suggestions(query, num=10) - search_results['results'] = [s.string for s in suggestions] + search_results["results"] = [s.string for s in suggestions] - return search_results \ No newline at end of file + return search_results diff --git a/erpnext/templates/pages/projects.py b/erpnext/templates/pages/projects.py index 16aa4390f1..4b3089b291 100644 --- a/erpnext/templates/pages/projects.py +++ b/erpnext/templates/pages/projects.py @@ -6,21 +6,28 @@ import frappe def get_context(context): - project_user = frappe.db.get_value("Project User", {"parent": frappe.form_dict.project, "user": frappe.session.user} , ["user", "view_attachments"], as_dict= True) - if frappe.session.user != 'Administrator' and (not project_user or frappe.session.user == 'Guest'): + project_user = frappe.db.get_value( + "Project User", + {"parent": frappe.form_dict.project, "user": frappe.session.user}, + ["user", "view_attachments"], + as_dict=True, + ) + if frappe.session.user != "Administrator" and ( + not project_user or frappe.session.user == "Guest" + ): raise frappe.PermissionError context.no_cache = 1 context.show_sidebar = True - project = frappe.get_doc('Project', frappe.form_dict.project) + project = frappe.get_doc("Project", frappe.form_dict.project) - project.has_permission('read') + project.has_permission("read") - project.tasks = get_tasks(project.name, start=0, item_status='open', - search=frappe.form_dict.get("search")) + project.tasks = get_tasks( + project.name, start=0, item_status="open", search=frappe.form_dict.get("search") + ) - project.timesheets = get_timesheets(project.name, start=0, - search=frappe.form_dict.get("search")) + project.timesheets = get_timesheets(project.name, start=0, search=frappe.form_dict.get("search")) if project_user and project_user.view_attachments: project.attachments = get_attachments(project.name) @@ -32,9 +39,22 @@ def get_tasks(project, start=0, search=None, item_status=None): filters = {"project": project} if search: filters["subject"] = ("like", "%{0}%".format(search)) - tasks = frappe.get_all("Task", filters=filters, - fields=["name", "subject", "status", "modified", "_assign", "exp_end_date", "is_group", "parent_task"], - limit_start=start, limit_page_length=10) + tasks = frappe.get_all( + "Task", + filters=filters, + fields=[ + "name", + "subject", + "status", + "modified", + "_assign", + "exp_end_date", + "is_group", + "parent_task", + ], + limit_start=start, + limit_page_length=10, + ) task_nest = [] for task in tasks: if task.is_group: @@ -44,36 +64,59 @@ def get_tasks(project, start=0, search=None, item_status=None): task_nest.append(task) return list(filter(lambda x: not x.parent_task, tasks)) + @frappe.whitelist() def get_task_html(project, start=0, item_status=None): - return frappe.render_template("erpnext/templates/includes/projects/project_tasks.html", - {"doc": { - "name": project, - "project_name": project, - "tasks": get_tasks(project, start, item_status=item_status)} - }, is_path=True) + return frappe.render_template( + "erpnext/templates/includes/projects/project_tasks.html", + { + "doc": { + "name": project, + "project_name": project, + "tasks": get_tasks(project, start, item_status=item_status), + } + }, + is_path=True, + ) + def get_timesheets(project, start=0, search=None): filters = {"project": project} if search: filters["activity_type"] = ("like", "%{0}%".format(search)) - timesheets = frappe.get_all('Timesheet Detail', filters=filters, - fields=['project','activity_type','from_time','to_time','parent'], - limit_start=start, limit_page_length=10) + timesheets = frappe.get_all( + "Timesheet Detail", + filters=filters, + fields=["project", "activity_type", "from_time", "to_time", "parent"], + limit_start=start, + limit_page_length=10, + ) for timesheet in timesheets: - info = frappe.get_all('Timesheet', filters={"name": timesheet.parent}, - fields=['name','status','modified','modified_by'], - limit_start=start, limit_page_length=10) + info = frappe.get_all( + "Timesheet", + filters={"name": timesheet.parent}, + fields=["name", "status", "modified", "modified_by"], + limit_start=start, + limit_page_length=10, + ) if len(info): timesheet.update(info[0]) return timesheets + @frappe.whitelist() def get_timesheet_html(project, start=0): - return frappe.render_template("erpnext/templates/includes/projects/project_timesheets.html", - {"doc": {"timesheets": get_timesheets(project, start)}}, is_path=True) + return frappe.render_template( + "erpnext/templates/includes/projects/project_timesheets.html", + {"doc": {"timesheets": get_timesheets(project, start)}}, + is_path=True, + ) + def get_attachments(project): - return frappe.get_all('File', filters= {"attached_to_name": project, "attached_to_doctype": 'Project', "is_private":0}, - fields=['file_name','file_url', 'file_size']) + return frappe.get_all( + "File", + filters={"attached_to_name": project, "attached_to_doctype": "Project", "is_private": 0}, + fields=["file_name", "file_url", "file_size"], + ) diff --git a/erpnext/templates/pages/regional/india/update_gstin.py b/erpnext/templates/pages/regional/india/update_gstin.py index 95b8f72d88..6939fe41a8 100644 --- a/erpnext/templates/pages/regional/india/update_gstin.py +++ b/erpnext/templates/pages/regional/india/update_gstin.py @@ -11,12 +11,12 @@ def get_context(context): except frappe.ValidationError: context.invalid_gstin = 1 - party_type = 'Customer' - party_name = frappe.db.get_value('Customer', party) + party_type = "Customer" + party_name = frappe.db.get_value("Customer", party) if not party_name: - party_type = 'Supplier' - party_name = frappe.db.get_value('Supplier', party) + party_type = "Supplier" + party_name = frappe.db.get_value("Supplier", party) if not party_name: context.not_found = 1 @@ -29,10 +29,10 @@ def get_context(context): def update_gstin(context): dirty = False for key, value in frappe.form_dict.items(): - if key != 'party': - address_name = frappe.get_value('Address', key) + if key != "party": + address_name = frappe.get_value("Address", key) if address_name: - address = frappe.get_doc('Address', address_name) + address = frappe.get_doc("Address", address_name) address.gstin = value.upper() address.save(ignore_permissions=True) dirty = True diff --git a/erpnext/templates/pages/rfq.py b/erpnext/templates/pages/rfq.py index 0afd46cac9..4b83642491 100644 --- a/erpnext/templates/pages/rfq.py +++ b/erpnext/templates/pages/rfq.py @@ -20,40 +20,59 @@ def get_context(context): update_supplier_details(context) context["title"] = frappe.form_dict.name + def get_supplier(): doctype = frappe.form_dict.doctype - parties_doctype = 'Request for Quotation Supplier' if doctype == 'Request for Quotation' else doctype + parties_doctype = ( + "Request for Quotation Supplier" if doctype == "Request for Quotation" else doctype + ) customers, suppliers = get_customers_suppliers(parties_doctype, frappe.session.user) - return suppliers[0] if suppliers else '' + return suppliers[0] if suppliers else "" + def check_supplier_has_docname_access(supplier): status = True - if frappe.form_dict.name not in frappe.db.sql_list("""select parent from `tabRequest for Quotation Supplier` - where supplier = %s""", (supplier,)): + if frappe.form_dict.name not in frappe.db.sql_list( + """select parent from `tabRequest for Quotation Supplier` + where supplier = %s""", + (supplier,), + ): status = False return status + def unauthorized_user(supplier): status = check_supplier_has_docname_access(supplier) or False if status == False: frappe.throw(_("Not Permitted"), frappe.PermissionError) + def update_supplier_details(context): supplier_doc = frappe.get_doc("Supplier", context.doc.supplier) - context.doc.currency = supplier_doc.default_currency or frappe.get_cached_value('Company', context.doc.company, "default_currency") - context.doc.currency_symbol = frappe.db.get_value("Currency", context.doc.currency, "symbol", cache=True) - context.doc.number_format = frappe.db.get_value("Currency", context.doc.currency, "number_format", cache=True) - context.doc.buying_price_list = supplier_doc.default_price_list or '' + context.doc.currency = supplier_doc.default_currency or frappe.get_cached_value( + "Company", context.doc.company, "default_currency" + ) + context.doc.currency_symbol = frappe.db.get_value( + "Currency", context.doc.currency, "symbol", cache=True + ) + context.doc.number_format = frappe.db.get_value( + "Currency", context.doc.currency, "number_format", cache=True + ) + context.doc.buying_price_list = supplier_doc.default_price_list or "" + def get_link_quotation(supplier, rfq): - quotation = frappe.db.sql(""" select distinct `tabSupplier Quotation Item`.parent as name, + quotation = frappe.db.sql( + """ select distinct `tabSupplier Quotation Item`.parent as name, `tabSupplier Quotation`.status, `tabSupplier Quotation`.transaction_date from `tabSupplier Quotation Item`, `tabSupplier Quotation` where `tabSupplier Quotation`.docstatus < 2 and `tabSupplier Quotation Item`.request_for_quotation =%(name)s and `tabSupplier Quotation Item`.parent = `tabSupplier Quotation`.name and `tabSupplier Quotation`.supplier = %(supplier)s order by `tabSupplier Quotation`.creation desc""", - {'name': rfq, 'supplier': supplier}, as_dict=1) + {"name": rfq, "supplier": supplier}, + as_dict=1, + ) for data in quotation: data.transaction_date = formatdate(data.transaction_date) diff --git a/erpnext/templates/pages/search_help.py b/erpnext/templates/pages/search_help.py index 1ef3942cbc..a6877ce9ab 100644 --- a/erpnext/templates/pages/search_help.py +++ b/erpnext/templates/pages/search_help.py @@ -11,17 +11,18 @@ def get_context(context): context.no_cache = 1 if frappe.form_dict.q: query = str(utils.escape(sanitize_html(frappe.form_dict.q))) - context.title = _('Help Results for') + context.title = _("Help Results for") context.query = query - context.route = '/search_help' + context.route = "/search_help" d = frappe._dict() d.results_sections = get_help_results_sections(query) context.update(d) else: - context.title = _('Docs Search') + context.title = _("Docs Search") -@frappe.whitelist(allow_guest = True) + +@frappe.whitelist(allow_guest=True) def get_help_results_sections(text): out = [] settings = frappe.get_doc("Support Settings", "Support Settings") @@ -40,63 +41,72 @@ def get_help_results_sections(text): if results: # Add section - out.append({ - "title": api.source_name, - "results": results - }) + out.append({"title": api.source_name, "results": results}) return out + def get_response(api, text): - response = requests.get(api.base_url + '/' + api.query_route, data={ - api.search_term_param_name: text - }) + response = requests.get( + api.base_url + "/" + api.query_route, data={api.search_term_param_name: text} + ) response.raise_for_status() return response.json() + def get_topics_data(api, response_json): if not response_json: response_json = {} - topics_data = {} # it will actually be an array - key_list = api.response_result_key_path.split(',') + topics_data = {} # it will actually be an array + key_list = api.response_result_key_path.split(",") for key in key_list: topics_data = response_json.get(key) if not topics_data else topics_data.get(key) return topics_data or [] + def prepare_api_results(api, topics_data): if not topics_data: topics_data = [] results = [] for topic in topics_data: - route = api.base_url + '/' + (api.post_route + '/' if api.post_route else "") - for key in api.post_route_key_list.split(','): + route = api.base_url + "/" + (api.post_route + "/" if api.post_route else "") + for key in api.post_route_key_list.split(","): route += str(topic[key]) - results.append(frappe._dict({ - 'title': topic[api.post_title_key], - 'preview': html2text(topic[api.post_description_key]), - 'route': route - })) + results.append( + frappe._dict( + { + "title": topic[api.post_title_key], + "preview": html2text(topic[api.post_description_key]), + "route": route, + } + ) + ) return results[:5] + def prepare_doctype_results(api, raw): results = [] for r in raw: prepared_result = {} - parts = r["content"].split(' ||| ') + parts = r["content"].split(" ||| ") for part in parts: - pair = part.split(' : ', 1) + pair = part.split(" : ", 1) prepared_result[pair[0]] = pair[1] - results.append(frappe._dict({ - 'title': prepared_result[api.result_title_field], - 'preview': prepared_result[api.result_preview_field], - 'route': prepared_result[api.result_route_field] - })) + results.append( + frappe._dict( + { + "title": prepared_result[api.result_title_field], + "preview": prepared_result[api.result_preview_field], + "route": prepared_result[api.result_route_field], + } + ) + ) return results diff --git a/erpnext/templates/pages/task_info.py b/erpnext/templates/pages/task_info.py index d1a70e14c3..66b775a917 100644 --- a/erpnext/templates/pages/task_info.py +++ b/erpnext/templates/pages/task_info.py @@ -4,9 +4,12 @@ import frappe def get_context(context): context.no_cache = 1 - task = frappe.get_doc('Task', frappe.form_dict.task) + task = frappe.get_doc("Task", frappe.form_dict.task) - context.comments = frappe.get_all('Communication', filters={'reference_name': task.name, 'comment_type': 'comment'}, - fields=['subject', 'sender_full_name', 'communication_date']) + context.comments = frappe.get_all( + "Communication", + filters={"reference_name": task.name, "comment_type": "comment"}, + fields=["subject", "sender_full_name", "communication_date"], + ) context.doc = task diff --git a/erpnext/templates/pages/timelog_info.py b/erpnext/templates/pages/timelog_info.py index db61e7e44b..3f0ec3738b 100644 --- a/erpnext/templates/pages/timelog_info.py +++ b/erpnext/templates/pages/timelog_info.py @@ -4,6 +4,6 @@ import frappe def get_context(context): context.no_cache = 1 - timelog = frappe.get_doc('Time Log', frappe.form_dict.timelog) + timelog = frappe.get_doc("Time Log", frappe.form_dict.timelog) context.doc = timelog diff --git a/erpnext/templates/pages/wishlist.py b/erpnext/templates/pages/wishlist.py index 72ee34e157..d70f27c9d9 100644 --- a/erpnext/templates/pages/wishlist.py +++ b/erpnext/templates/pages/wishlist.py @@ -23,31 +23,33 @@ def get_context(context): context.settings = settings context.no_cache = 1 + def get_stock_availability(item_code, warehouse): stock_qty = frappe.utils.flt( - frappe.db.get_value("Bin", - { - "item_code": item_code, - "warehouse": warehouse - }, - "actual_qty") + frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty") ) return bool(stock_qty) + def get_wishlist_items(): if not frappe.db.exists("Wishlist", frappe.session.user): return [] return frappe.db.get_all( "Wishlist Item", - filters={ - "parent": frappe.session.user - }, + filters={"parent": frappe.session.user}, fields=[ - "web_item_name", "item_code", "item_name", - "website_item", "warehouse", - "image", "item_group", "route" - ]) + "web_item_name", + "item_code", + "item_name", + "website_item", + "warehouse", + "image", + "item_group", + "route", + ], + ) + def set_stock_price_details(items, settings, selling_price_list): for item in items: @@ -55,17 +57,15 @@ def set_stock_price_details(items, settings, selling_price_list): item.available = get_stock_availability(item.item_code, item.get("warehouse")) price_details = get_price( - item.item_code, - selling_price_list, - settings.default_customer_group, - settings.company + item.item_code, selling_price_list, settings.default_customer_group, settings.company ) if price_details: - item.formatted_price = price_details.get('formatted_price') - item.formatted_mrp = price_details.get('formatted_mrp') + item.formatted_price = price_details.get("formatted_price") + item.formatted_mrp = price_details.get("formatted_mrp") if item.formatted_mrp: - item.discount = price_details.get('formatted_discount_percent') or \ - price_details.get('formatted_discount_rate') + item.discount = price_details.get("formatted_discount_percent") or price_details.get( + "formatted_discount_rate" + ) - return items \ No newline at end of file + return items diff --git a/erpnext/templates/utils.py b/erpnext/templates/utils.py index 9f46e6a99e..4295188dc0 100644 --- a/erpnext/templates/utils.py +++ b/erpnext/templates/utils.py @@ -8,31 +8,35 @@ import frappe @frappe.whitelist(allow_guest=True) def send_message(subject="Website Query", message="", sender="", status="Open"): from frappe.www.contact import send_message as website_send_message + lead = customer = None website_send_message(subject, message, sender) - customer = frappe.db.sql("""select distinct dl.link_name from `tabDynamic Link` dl + customer = frappe.db.sql( + """select distinct dl.link_name from `tabDynamic Link` dl left join `tabContact` c on dl.parent=c.name where dl.link_doctype='Customer' - and c.email_id = %s""", sender) + and c.email_id = %s""", + sender, + ) if not customer: - lead = frappe.db.get_value('Lead', dict(email_id=sender)) + lead = frappe.db.get_value("Lead", dict(email_id=sender)) if not lead: - new_lead = frappe.get_doc(dict( - doctype='Lead', - email_id = sender, - lead_name = sender.split('@')[0].title() - )).insert(ignore_permissions=True) + new_lead = frappe.get_doc( + dict(doctype="Lead", email_id=sender, lead_name=sender.split("@")[0].title()) + ).insert(ignore_permissions=True) - opportunity = frappe.get_doc(dict( - doctype ='Opportunity', - opportunity_from = 'Customer' if customer else 'Lead', - status = 'Open', - title = subject, - contact_email = sender, - to_discuss = message - )) + opportunity = frappe.get_doc( + dict( + doctype="Opportunity", + opportunity_from="Customer" if customer else "Lead", + status="Open", + title=subject, + contact_email=sender, + to_discuss=message, + ) + ) if customer: opportunity.party_name = customer[0][0] @@ -43,15 +47,17 @@ def send_message(subject="Website Query", message="", sender="", status="Open"): opportunity.insert(ignore_permissions=True) - comm = frappe.get_doc({ - "doctype":"Communication", - "subject": subject, - "content": message, - "sender": sender, - "sent_or_received": "Received", - 'reference_doctype': 'Opportunity', - 'reference_name': opportunity.name - }) + comm = frappe.get_doc( + { + "doctype": "Communication", + "subject": subject, + "content": message, + "sender": sender, + "sent_or_received": "Received", + "reference_doctype": "Opportunity", + "reference_name": opportunity.name, + } + ) comm.insert(ignore_permissions=True) return "okay" diff --git a/erpnext/tests/__init__.py b/erpnext/tests/__init__.py index a504340d40..dc37472c4c 100644 --- a/erpnext/tests/__init__.py +++ b/erpnext/tests/__init__.py @@ -1 +1 @@ -global_test_dependencies = ['User', 'Company', 'Item'] +global_test_dependencies = ["User", "Company", "Item"] diff --git a/erpnext/tests/test_init.py b/erpnext/tests/test_init.py index 61849726ef..6fbfbf4689 100644 --- a/erpnext/tests/test_init.py +++ b/erpnext/tests/test_init.py @@ -4,7 +4,8 @@ import frappe from erpnext import encode_company_abbr -test_records = frappe.get_test_records('Company') +test_records = frappe.get_test_records("Company") + class TestInit(unittest.TestCase): def test_encode_company_abbr(self): @@ -12,23 +13,30 @@ class TestInit(unittest.TestCase): abbr = "NFECT" names = [ - "Warehouse Name", "ERPNext Foundation India", "Gold - Member - {a}".format(a=abbr), - " - {a}".format(a=abbr), "ERPNext - Foundation - India", + "Warehouse Name", + "ERPNext Foundation India", + "Gold - Member - {a}".format(a=abbr), + " - {a}".format(a=abbr), + "ERPNext - Foundation - India", "ERPNext Foundation India - {a}".format(a=abbr), - "No-Space-{a}".format(a=abbr), "- Warehouse" + "No-Space-{a}".format(a=abbr), + "- Warehouse", ] expected_names = [ - "Warehouse Name - {a}".format(a=abbr), "ERPNext Foundation India - {a}".format(a=abbr), - "Gold - Member - {a}".format(a=abbr), " - {a}".format(a=abbr), + "Warehouse Name - {a}".format(a=abbr), + "ERPNext Foundation India - {a}".format(a=abbr), + "Gold - Member - {a}".format(a=abbr), + " - {a}".format(a=abbr), "ERPNext - Foundation - India - {a}".format(a=abbr), - "ERPNext Foundation India - {a}".format(a=abbr), "No-Space-{a} - {a}".format(a=abbr), - "- Warehouse - {a}".format(a=abbr) + "ERPNext Foundation India - {a}".format(a=abbr), + "No-Space-{a} - {a}".format(a=abbr), + "- Warehouse - {a}".format(a=abbr), ] for i in range(len(names)): enc_name = encode_company_abbr(names[i], abbr=abbr) self.assertTrue( enc_name == expected_names[i], - "{enc} is not same as {exp}".format(enc=enc_name, exp=expected_names[i]) + "{enc} is not same as {exp}".format(enc=enc_name, exp=expected_names[i]), ) diff --git a/erpnext/tests/test_notifications.py b/erpnext/tests/test_notifications.py index 669bf6f3d9..0f39195630 100644 --- a/erpnext/tests/test_notifications.py +++ b/erpnext/tests/test_notifications.py @@ -10,9 +10,9 @@ from frappe.desk import notifications class TestNotifications(unittest.TestCase): def test_get_notifications_for_targets(self): - ''' - Test notification config entries for targets as percentages - ''' + """ + Test notification config entries for targets as percentages + """ company = frappe.get_all("Company")[0] frappe.db.set_value("Company", company.name, "monthly_sales_target", 10000) @@ -21,7 +21,7 @@ class TestNotifications(unittest.TestCase): config = notifications.get_notification_config() doc_target_percents = notifications.get_notifications_for_targets(config, {}) - self.assertEqual(doc_target_percents['Company'][company.name], 10) + self.assertEqual(doc_target_percents["Company"][company.name], 10) frappe.db.set_value("Company", company.name, "monthly_sales_target", 2000) frappe.db.set_value("Company", company.name, "total_monthly_sales", 0) @@ -29,4 +29,4 @@ class TestNotifications(unittest.TestCase): config = notifications.get_notification_config() doc_target_percents = notifications.get_notifications_for_targets(config, {}) - self.assertEqual(doc_target_percents['Company'][company.name], 0) + self.assertEqual(doc_target_percents["Company"][company.name], 0) diff --git a/erpnext/tests/test_point_of_sale.py b/erpnext/tests/test_point_of_sale.py index 38f2c16d93..7267d4a1d5 100644 --- a/erpnext/tests/test_point_of_sale.py +++ b/erpnext/tests/test_point_of_sale.py @@ -14,11 +14,11 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry class TestPointOfSale(unittest.TestCase): @classmethod def setUpClass(cls) -> None: - frappe.db.savepoint('before_test_point_of_sale') + frappe.db.savepoint("before_test_point_of_sale") @classmethod def tearDownClass(cls) -> None: - frappe.db.rollback(save_point='before_test_point_of_sale') + frappe.db.rollback(save_point="before_test_point_of_sale") def test_item_search(self): """ diff --git a/erpnext/tests/test_regional.py b/erpnext/tests/test_regional.py index 10d62ce69f..abeecee4e7 100644 --- a/erpnext/tests/test_regional.py +++ b/erpnext/tests/test_regional.py @@ -7,15 +7,16 @@ import erpnext @erpnext.allow_regional def test_method(): - return 'original' + return "original" + class TestInit(unittest.TestCase): def test_regional_overrides(self): - frappe.flags.country = 'India' - self.assertEqual(test_method(), 'overridden') + frappe.flags.country = "India" + self.assertEqual(test_method(), "overridden") - frappe.flags.country = 'Maldives' - self.assertEqual(test_method(), 'original') + frappe.flags.country = "Maldives" + self.assertEqual(test_method(), "original") - frappe.flags.country = 'France' - self.assertEqual(test_method(), 'overridden') + frappe.flags.country = "France" + self.assertEqual(test_method(), "overridden") diff --git a/erpnext/tests/test_search.py b/erpnext/tests/test_search.py index c169458b8c..ffe9a5ae54 100644 --- a/erpnext/tests/test_search.py +++ b/erpnext/tests/test_search.py @@ -8,12 +8,11 @@ class TestSearch(unittest.TestCase): # Search for the word "cond", part of the word "conduire" (Lead) in french. def test_contact_search_in_foreign_language(self): try: - frappe.local.lang = 'fr' - output = filter_dynamic_link_doctypes("DocType", "cond", "name", 0, 20, { - 'fieldtype': 'HTML', - 'fieldname': 'contact_html' - }) - result = [['found' for x in y if x=="Lead"] for y in output] - self.assertTrue(['found'] in result) + frappe.local.lang = "fr" + output = filter_dynamic_link_doctypes( + "DocType", "cond", "name", 0, 20, {"fieldtype": "HTML", "fieldname": "contact_html"} + ) + result = [["found" for x in y if x == "Lead"] for y in output] + self.assertTrue(["found"] in result) finally: - frappe.local.lang = 'en' + frappe.local.lang = "en" diff --git a/erpnext/tests/test_subcontracting.py b/erpnext/tests/test_subcontracting.py index fccfd0d1cf..07291e851b 100644 --- a/erpnext/tests/test_subcontracting.py +++ b/erpnext/tests/test_subcontracting.py @@ -25,35 +25,43 @@ class TestSubcontracting(unittest.TestCase): make_bom_for_subcontracted_items() def test_po_with_bom(self): - ''' - - Set backflush based on BOM - - Create subcontracted PO for the item Subcontracted Item SA1 and add same item two times. - - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. - - Create purchase receipt against the PO and check serial nos and batch no. - ''' + """ + - Set backflush based on BOM + - Create subcontracted PO for the item Subcontracted Item SA1 and add same item two times. + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Create purchase receipt against the PO and check serial nos and batch no. + """ - set_backflush_based_on('BOM') - item_code = 'Subcontracted Item SA1' - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 5, 'rate': 100}, - {'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 6, 'rate': 100}] + set_backflush_based_on("BOM") + item_code = "Subcontracted Item SA1" + items = [ + {"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 5, "rate": 100}, + {"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 6, "rate": 100}, + ] - rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 5}, - {'item_code': 'Subcontracted SRM Item 2', 'qty': 5}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 5}, - {'item_code': 'Subcontracted SRM Item 1', 'qty': 6}, - {'item_code': 'Subcontracted SRM Item 2', 'qty': 6}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 6} + rm_items = [ + {"item_code": "Subcontracted SRM Item 1", "qty": 5}, + {"item_code": "Subcontracted SRM Item 2", "qty": 5}, + {"item_code": "Subcontracted SRM Item 3", "qty": 5}, + {"item_code": "Subcontracted SRM Item 1", "qty": 6}, + {"item_code": "Subcontracted SRM Item 2", "qty": 6}, + {"item_code": "Subcontracted SRM Item 3", "qty": 6}, ] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name if d.get('qty') == 5 else po.items[1].name + d["po_detail"] = po.items[0].name if d.get("qty") == 5 else po.items[1].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_receipt(po.name) pr1.submit() @@ -61,43 +69,58 @@ class TestSubcontracting(unittest.TestCase): for key, value in get_supplied_items(pr1).items(): transferred_detais = itemwise_details.get(key) - for field in ['qty', 'serial_no', 'batch_no']: + for field in ["qty", "serial_no", "batch_no"]: if value.get(field): transfer, consumed = (transferred_detais.get(field), value.get(field)) - if field == 'serial_no': + if field == "serial_no": transfer, consumed = (sorted(transfer), sorted(consumed)) self.assertEqual(transfer, consumed) def test_po_with_material_transfer(self): - ''' - - Set backflush based on Material Transfer - - Create subcontracted PO for the item Subcontracted Item SA1 and Subcontracted Item SA5. - - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. - - Transfer extra item Subcontracted SRM Item 4 for the subcontract item Subcontracted Item SA5. - - Create partial purchase receipt against the PO and check serial nos and batch no. - ''' + """ + - Set backflush based on Material Transfer + - Create subcontracted PO for the item Subcontracted Item SA1 and Subcontracted Item SA5. + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer extra item Subcontracted SRM Item 4 for the subcontract item Subcontracted Item SA5. + - Create partial purchase receipt against the PO and check serial nos and batch no. + """ - set_backflush_based_on('Material Transferred for Subcontract') - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA1', 'qty': 5, 'rate': 100}, - {'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA5', 'qty': 6, 'rate': 100}] + set_backflush_based_on("Material Transferred for Subcontract") + items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Item SA1", + "qty": 5, + "rate": 100, + }, + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Item SA5", + "qty": 6, + "rate": 100, + }, + ] - rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 5, 'main_item_code': 'Subcontracted Item SA1'}, - {'item_code': 'Subcontracted SRM Item 2', 'qty': 5, 'main_item_code': 'Subcontracted Item SA1'}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 5, 'main_item_code': 'Subcontracted Item SA1'}, - {'item_code': 'Subcontracted SRM Item 5', 'qty': 6, 'main_item_code': 'Subcontracted Item SA5'}, - {'item_code': 'Subcontracted SRM Item 4', 'qty': 6, 'main_item_code': 'Subcontracted Item SA5'} + rm_items = [ + {"item_code": "Subcontracted SRM Item 1", "qty": 5, "main_item_code": "Subcontracted Item SA1"}, + {"item_code": "Subcontracted SRM Item 2", "qty": 5, "main_item_code": "Subcontracted Item SA1"}, + {"item_code": "Subcontracted SRM Item 3", "qty": 5, "main_item_code": "Subcontracted Item SA1"}, + {"item_code": "Subcontracted SRM Item 5", "qty": 6, "main_item_code": "Subcontracted Item SA5"}, + {"item_code": "Subcontracted SRM Item 4", "qty": 6, "main_item_code": "Subcontracted Item SA5"}, ] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name if d.get('qty') == 5 else po.items[1].name + d["po_detail"] = po.items[0].name if d.get("qty") == 5 else po.items[1].name - make_stock_transfer_entry(po_no = po.name, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details) + ) pr1 = make_purchase_receipt(po.name) pr1.remove(pr1.items[1]) @@ -106,7 +129,7 @@ class TestSubcontracting(unittest.TestCase): for key, value in get_supplied_items(pr1).items(): transferred_detais = itemwise_details.get(key) - for field in ['qty', 'serial_no', 'batch_no']: + for field in ["qty", "serial_no", "batch_no"]: if value.get(field): self.assertEqual(value.get(field), transferred_detais.get(field)) @@ -116,36 +139,51 @@ class TestSubcontracting(unittest.TestCase): for key, value in get_supplied_items(pr2).items(): transferred_detais = itemwise_details.get(key) - for field in ['qty', 'serial_no', 'batch_no']: + for field in ["qty", "serial_no", "batch_no"]: if value.get(field): self.assertEqual(value.get(field), transferred_detais.get(field)) def test_subcontract_with_same_components_different_fg(self): - ''' - - Set backflush based on Material Transfer - - Create subcontracted PO for the item Subcontracted Item SA2 and Subcontracted Item SA3. - - Transfer the components from Stores to Supplier warehouse with serial nos. - - Transfer extra qty of components for the item Subcontracted Item SA2. - - Create partial purchase receipt against the PO and check serial nos and batch no. - ''' + """ + - Set backflush based on Material Transfer + - Create subcontracted PO for the item Subcontracted Item SA2 and Subcontracted Item SA3. + - Transfer the components from Stores to Supplier warehouse with serial nos. + - Transfer extra qty of components for the item Subcontracted Item SA2. + - Create partial purchase receipt against the PO and check serial nos and batch no. + """ - set_backflush_based_on('Material Transferred for Subcontract') - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA2', 'qty': 5, 'rate': 100}, - {'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA3', 'qty': 6, 'rate': 100}] + set_backflush_based_on("Material Transferred for Subcontract") + items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Item SA2", + "qty": 5, + "rate": 100, + }, + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Item SA3", + "qty": 6, + "rate": 100, + }, + ] - rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 6, 'main_item_code': 'Subcontracted Item SA2'}, - {'item_code': 'Subcontracted SRM Item 2', 'qty': 6, 'main_item_code': 'Subcontracted Item SA3'} + rm_items = [ + {"item_code": "Subcontracted SRM Item 2", "qty": 6, "main_item_code": "Subcontracted Item SA2"}, + {"item_code": "Subcontracted SRM Item 2", "qty": 6, "main_item_code": "Subcontracted Item SA3"}, ] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name if d.get('qty') == 5 else po.items[1].name + d["po_detail"] = po.items[0].name if d.get("qty") == 5 else po.items[1].name - make_stock_transfer_entry(po_no = po.name, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details) + ) pr1 = make_purchase_receipt(po.name) pr1.items[0].qty = 3 @@ -155,7 +193,7 @@ class TestSubcontracting(unittest.TestCase): for key, value in get_supplied_items(pr1).items(): transferred_detais = itemwise_details.get(key) self.assertEqual(value.qty, 4) - self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[0:4])) + self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[0:4])) pr2 = make_purchase_receipt(po.name) pr2.items[0].qty = 2 @@ -166,7 +204,7 @@ class TestSubcontracting(unittest.TestCase): transferred_detais = itemwise_details.get(key) self.assertEqual(value.qty, 2) - self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[4:6])) + self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[4:6])) pr3 = make_purchase_receipt(po.name) pr3.submit() @@ -174,85 +212,104 @@ class TestSubcontracting(unittest.TestCase): transferred_detais = itemwise_details.get(key) self.assertEqual(value.qty, 6) - self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[6:12])) + self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[6:12])) def test_return_non_consumed_materials(self): - ''' - - Set backflush based on Material Transfer - - Create subcontracted PO for the item Subcontracted Item SA2. - - Transfer the components from Stores to Supplier warehouse with serial nos. - - Transfer extra qty of component for the subcontracted item Subcontracted Item SA2. - - Create purchase receipt for full qty against the PO and change the qty of raw material. - - After that return the non consumed material back to the store from supplier's warehouse. - ''' + """ + - Set backflush based on Material Transfer + - Create subcontracted PO for the item Subcontracted Item SA2. + - Transfer the components from Stores to Supplier warehouse with serial nos. + - Transfer extra qty of component for the subcontracted item Subcontracted Item SA2. + - Create purchase receipt for full qty against the PO and change the qty of raw material. + - After that return the non consumed material back to the store from supplier's warehouse. + """ - set_backflush_based_on('Material Transferred for Subcontract') - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA2', 'qty': 5, 'rate': 100}] - rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 6, 'main_item_code': 'Subcontracted Item SA2'}] + set_backflush_based_on("Material Transferred for Subcontract") + items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Item SA2", + "qty": 5, + "rate": 100, + } + ] + rm_items = [ + {"item_code": "Subcontracted SRM Item 2", "qty": 6, "main_item_code": "Subcontracted Item SA2"} + ] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details) + ) pr1 = make_purchase_receipt(po.name) pr1.save() pr1.supplied_items[0].consumed_qty = 5 - pr1.supplied_items[0].serial_no = '\n'.join(sorted( - itemwise_details.get('Subcontracted SRM Item 2').get('serial_no')[0:5] - )) + pr1.supplied_items[0].serial_no = "\n".join( + sorted(itemwise_details.get("Subcontracted SRM Item 2").get("serial_no")[0:5]) + ) pr1.submit() for key, value in get_supplied_items(pr1).items(): transferred_detais = itemwise_details.get(key) self.assertEqual(value.qty, 5) - self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[0:5])) + self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[0:5])) po.load_from_db() self.assertEqual(po.supplied_items[0].consumed_qty, 5) doc = get_materials_from_supplier(po.name, [d.name for d in po.supplied_items]) self.assertEqual(doc.items[0].qty, 1) - self.assertEqual(doc.items[0].s_warehouse, '_Test Warehouse 1 - _TC') - self.assertEqual(doc.items[0].t_warehouse, '_Test Warehouse - _TC') - self.assertEqual(get_serial_nos(doc.items[0].serial_no), - itemwise_details.get(doc.items[0].item_code)['serial_no'][5:6]) + self.assertEqual(doc.items[0].s_warehouse, "_Test Warehouse 1 - _TC") + self.assertEqual(doc.items[0].t_warehouse, "_Test Warehouse - _TC") + self.assertEqual( + get_serial_nos(doc.items[0].serial_no), + itemwise_details.get(doc.items[0].item_code)["serial_no"][5:6], + ) def test_item_with_batch_based_on_bom(self): - ''' - - Set backflush based on BOM - - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no). - - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. - - Transfer the components in multiple batches. - - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches. - - Keep the qty as 2 for Subcontracted Item in the purchase receipt. - ''' + """ + - Set backflush based on BOM + - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no). + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer the components in multiple batches. + - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches. + - Keep the qty as 2 for Subcontracted Item in the purchase receipt. + """ - set_backflush_based_on('BOM') - item_code = 'Subcontracted Item SA4' - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + set_backflush_based_on("BOM") + item_code = "Subcontracted Item SA4" + items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}] - rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10}, - {'item_code': 'Subcontracted SRM Item 2', 'qty': 10}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 1} + rm_items = [ + {"item_code": "Subcontracted SRM Item 1", "qty": 10}, + {"item_code": "Subcontracted SRM Item 2", "qty": 10}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, + {"item_code": "Subcontracted SRM Item 3", "qty": 1}, ] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_receipt(po.name) pr1.items[0].qty = 2 @@ -281,37 +338,43 @@ class TestSubcontracting(unittest.TestCase): self.assertEqual(value.qty, 2) def test_item_with_batch_based_on_material_transfer(self): - ''' - - Set backflush based on Material Transferred for Subcontract - - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no). - - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. - - Transfer the components in multiple batches with extra 2 qty for the batched item. - - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches. - - Keep the qty as 2 for Subcontracted Item in the purchase receipt. - - In the first purchase receipt the batched raw materials will be consumed 2 extra qty. - ''' + """ + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no). + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer the components in multiple batches with extra 2 qty for the batched item. + - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches. + - Keep the qty as 2 for Subcontracted Item in the purchase receipt. + - In the first purchase receipt the batched raw materials will be consumed 2 extra qty. + """ - set_backflush_based_on('Material Transferred for Subcontract') - item_code = 'Subcontracted Item SA4' - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + set_backflush_based_on("Material Transferred for Subcontract") + item_code = "Subcontracted Item SA4" + items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}] - rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10}, - {'item_code': 'Subcontracted SRM Item 2', 'qty': 10}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3} + rm_items = [ + {"item_code": "Subcontracted SRM Item 1", "qty": 10}, + {"item_code": "Subcontracted SRM Item 2", "qty": 10}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, ] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_receipt(po.name) pr1.items[0].qty = 2 @@ -320,7 +383,7 @@ class TestSubcontracting(unittest.TestCase): pr1.submit() for key, value in get_supplied_items(pr1).items(): - qty = 4 if key != 'Subcontracted SRM Item 3' else 6 + qty = 4 if key != "Subcontracted SRM Item 3" else 6 self.assertEqual(value.qty, qty) pr1 = make_purchase_receipt(po.name) @@ -341,30 +404,35 @@ class TestSubcontracting(unittest.TestCase): self.assertEqual(value.qty, 2) def test_partial_transfer_serial_no_components_based_on_material_transfer(self): - ''' - - Set backflush based on Material Transferred for Subcontract - - Create subcontracted PO for the item Subcontracted Item SA2. - - Transfer the partial components from Stores to Supplier warehouse with serial nos. - - Create partial purchase receipt against the PO and change the qty manually. - - Transfer the remaining components from Stores to Supplier warehouse with serial nos. - - Create purchase receipt for remaining qty against the PO and change the qty manually. - ''' + """ + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA2. + - Transfer the partial components from Stores to Supplier warehouse with serial nos. + - Create partial purchase receipt against the PO and change the qty manually. + - Transfer the remaining components from Stores to Supplier warehouse with serial nos. + - Create purchase receipt for remaining qty against the PO and change the qty manually. + """ - set_backflush_based_on('Material Transferred for Subcontract') - item_code = 'Subcontracted Item SA2' - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + set_backflush_based_on("Material Transferred for Subcontract") + item_code = "Subcontracted Item SA2" + items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}] - rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 5}] + rm_items = [{"item_code": "Subcontracted SRM Item 2", "qty": 5}] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_receipt(po.name) pr1.items[0].qty = 5 @@ -377,7 +445,9 @@ class TestSubcontracting(unittest.TestCase): pr1.load_from_db() pr1.supplied_items[0].consumed_qty = 5 - pr1.supplied_items[0].serial_no = '\n'.join(itemwise_details[pr1.supplied_items[0].rm_item_code]['serial_no']) + pr1.supplied_items[0].serial_no = "\n".join( + itemwise_details[pr1.supplied_items[0].rm_item_code]["serial_no"] + ) pr1.save() pr1.submit() @@ -388,10 +458,14 @@ class TestSubcontracting(unittest.TestCase): itemwise_details = make_stock_in_entry(rm_items=rm_items) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_receipt(po.name) pr1.submit() @@ -402,67 +476,77 @@ class TestSubcontracting(unittest.TestCase): self.assertEqual(sorted(value.serial_no), sorted(details.serial_no)) def test_incorrect_serial_no_components_based_on_material_transfer(self): - ''' - - Set backflush based on Material Transferred for Subcontract - - Create subcontracted PO for the item Subcontracted Item SA2. - - Transfer the serialized componenets to the supplier. - - Create purchase receipt and change the serial no which is not transferred. - - System should throw the error and not allowed to save the purchase receipt. - ''' + """ + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA2. + - Transfer the serialized componenets to the supplier. + - Create purchase receipt and change the serial no which is not transferred. + - System should throw the error and not allowed to save the purchase receipt. + """ - set_backflush_based_on('Material Transferred for Subcontract') - item_code = 'Subcontracted Item SA2' - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + set_backflush_based_on("Material Transferred for Subcontract") + item_code = "Subcontracted Item SA2" + items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}] - rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 10}] + rm_items = [{"item_code": "Subcontracted SRM Item 2", "qty": 10}] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_receipt(po.name) pr1.save() - pr1.supplied_items[0].serial_no = 'ABCD' + pr1.supplied_items[0].serial_no = "ABCD" self.assertRaises(frappe.ValidationError, pr1.save) pr1.delete() def test_partial_transfer_batch_based_on_material_transfer(self): - ''' - - Set backflush based on Material Transferred for Subcontract - - Create subcontracted PO for the item Subcontracted Item SA6. - - Transfer the partial components from Stores to Supplier warehouse with batch. - - Create partial purchase receipt against the PO and change the qty manually. - - Transfer the remaining components from Stores to Supplier warehouse with batch. - - Create purchase receipt for remaining qty against the PO and change the qty manually. - ''' + """ + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA6. + - Transfer the partial components from Stores to Supplier warehouse with batch. + - Create partial purchase receipt against the PO and change the qty manually. + - Transfer the remaining components from Stores to Supplier warehouse with batch. + - Create purchase receipt for remaining qty against the PO and change the qty manually. + """ - set_backflush_based_on('Material Transferred for Subcontract') - item_code = 'Subcontracted Item SA6' - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + set_backflush_based_on("Material Transferred for Subcontract") + item_code = "Subcontracted Item SA6" + items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}] - rm_items = [{'item_code': 'Subcontracted SRM Item 3', 'qty': 5}] + rm_items = [{"item_code": "Subcontracted SRM Item 3", "qty": 5}] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_receipt(po.name) pr1.items[0].qty = 5 pr1.save() - transferred_batch_no = '' + transferred_batch_no = "" for key, value in get_supplied_items(pr1).items(): details = itemwise_details.get(key) self.assertEqual(value.qty, 3) @@ -482,10 +566,14 @@ class TestSubcontracting(unittest.TestCase): itemwise_details = make_stock_in_entry(rm_items=rm_items) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_receipt(po.name) pr1.submit() @@ -495,55 +583,60 @@ class TestSubcontracting(unittest.TestCase): self.assertEqual(value.qty, details.qty) self.assertEqual(value.batch_no, details.batch_no) - def test_item_with_batch_based_on_material_transfer_for_purchase_invoice(self): - ''' - - Set backflush based on Material Transferred for Subcontract - - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no). - - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. - - Transfer the components in multiple batches with extra 2 qty for the batched item. - - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches. - - Keep the qty as 2 for Subcontracted Item in the purchase receipt. - - In the first purchase receipt the batched raw materials will be consumed 2 extra qty. - ''' + """ + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no). + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer the components in multiple batches with extra 2 qty for the batched item. + - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches. + - Keep the qty as 2 for Subcontracted Item in the purchase receipt. + - In the first purchase receipt the batched raw materials will be consumed 2 extra qty. + """ - set_backflush_based_on('Material Transferred for Subcontract') - item_code = 'Subcontracted Item SA4' - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + set_backflush_based_on("Material Transferred for Subcontract") + item_code = "Subcontracted Item SA4" + items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}] - rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10}, - {'item_code': 'Subcontracted SRM Item 2', 'qty': 10}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3} + rm_items = [ + {"item_code": "Subcontracted SRM Item 1", "qty": 10}, + {"item_code": "Subcontracted SRM Item 2", "qty": 10}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, ] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_invoice(po.name) pr1.update_stock = 1 pr1.items[0].qty = 2 - pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.items[0].expense_account = "Stock Adjustment - _TC" add_second_row_in_pr(pr1) pr1.save() pr1.submit() for key, value in get_supplied_items(pr1).items(): - qty = 4 if key != 'Subcontracted SRM Item 3' else 6 + qty = 4 if key != "Subcontracted SRM Item 3" else 6 self.assertEqual(value.qty, qty) pr1 = make_purchase_invoice(po.name) pr1.update_stock = 1 - pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.items[0].expense_account = "Stock Adjustment - _TC" pr1.items[0].qty = 2 add_second_row_in_pr(pr1) pr1.save() @@ -555,43 +648,50 @@ class TestSubcontracting(unittest.TestCase): pr1 = make_purchase_invoice(po.name) pr1.update_stock = 1 pr1.items[0].qty = 2 - pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.items[0].expense_account = "Stock Adjustment - _TC" pr1.save() pr1.submit() for key, value in get_supplied_items(pr1).items(): self.assertEqual(value.qty, 2) - def test_partial_transfer_serial_no_components_based_on_material_transfer_for_purchase_invoice(self): - ''' - - Set backflush based on Material Transferred for Subcontract - - Create subcontracted PO for the item Subcontracted Item SA2. - - Transfer the partial components from Stores to Supplier warehouse with serial nos. - - Create partial purchase receipt against the PO and change the qty manually. - - Transfer the remaining components from Stores to Supplier warehouse with serial nos. - - Create purchase receipt for remaining qty against the PO and change the qty manually. - ''' + def test_partial_transfer_serial_no_components_based_on_material_transfer_for_purchase_invoice( + self, + ): + """ + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA2. + - Transfer the partial components from Stores to Supplier warehouse with serial nos. + - Create partial purchase receipt against the PO and change the qty manually. + - Transfer the remaining components from Stores to Supplier warehouse with serial nos. + - Create purchase receipt for remaining qty against the PO and change the qty manually. + """ - set_backflush_based_on('Material Transferred for Subcontract') - item_code = 'Subcontracted Item SA2' - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + set_backflush_based_on("Material Transferred for Subcontract") + item_code = "Subcontracted Item SA2" + items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}] - rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 5}] + rm_items = [{"item_code": "Subcontracted SRM Item 2", "qty": 5}] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_invoice(po.name) pr1.update_stock = 1 pr1.items[0].qty = 5 - pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.items[0].expense_account = "Stock Adjustment - _TC" pr1.save() for key, value in get_supplied_items(pr1).items(): @@ -601,7 +701,9 @@ class TestSubcontracting(unittest.TestCase): pr1.load_from_db() pr1.supplied_items[0].consumed_qty = 5 - pr1.supplied_items[0].serial_no = '\n'.join(itemwise_details[pr1.supplied_items[0].rm_item_code]['serial_no']) + pr1.supplied_items[0].serial_no = "\n".join( + itemwise_details[pr1.supplied_items[0].rm_item_code]["serial_no"] + ) pr1.save() pr1.submit() @@ -612,14 +714,18 @@ class TestSubcontracting(unittest.TestCase): itemwise_details = make_stock_in_entry(rm_items=rm_items) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_invoice(po.name) pr1.update_stock = 1 - pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.items[0].expense_account = "Stock Adjustment - _TC" pr1.submit() for key, value in get_supplied_items(pr1).items(): @@ -628,38 +734,43 @@ class TestSubcontracting(unittest.TestCase): self.assertEqual(sorted(value.serial_no), sorted(details.serial_no)) def test_partial_transfer_batch_based_on_material_transfer_for_purchase_invoice(self): - ''' - - Set backflush based on Material Transferred for Subcontract - - Create subcontracted PO for the item Subcontracted Item SA6. - - Transfer the partial components from Stores to Supplier warehouse with batch. - - Create partial purchase receipt against the PO and change the qty manually. - - Transfer the remaining components from Stores to Supplier warehouse with batch. - - Create purchase receipt for remaining qty against the PO and change the qty manually. - ''' + """ + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA6. + - Transfer the partial components from Stores to Supplier warehouse with batch. + - Create partial purchase receipt against the PO and change the qty manually. + - Transfer the remaining components from Stores to Supplier warehouse with batch. + - Create purchase receipt for remaining qty against the PO and change the qty manually. + """ - set_backflush_based_on('Material Transferred for Subcontract') - item_code = 'Subcontracted Item SA6' - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + set_backflush_based_on("Material Transferred for Subcontract") + item_code = "Subcontracted Item SA6" + items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}] - rm_items = [{'item_code': 'Subcontracted SRM Item 3', 'qty': 5}] + rm_items = [{"item_code": "Subcontracted SRM Item 3", "qty": 5}] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_invoice(po.name) pr1.update_stock = 1 pr1.items[0].qty = 5 - pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.items[0].expense_account = "Stock Adjustment - _TC" pr1.save() - transferred_batch_no = '' + transferred_batch_no = "" for key, value in get_supplied_items(pr1).items(): details = itemwise_details.get(key) self.assertEqual(value.qty, 3) @@ -679,14 +790,18 @@ class TestSubcontracting(unittest.TestCase): itemwise_details = make_stock_in_entry(rm_items=rm_items) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_invoice(po.name) pr1.update_stock = 1 - pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.items[0].expense_account = "Stock Adjustment - _TC" pr1.submit() for key, value in get_supplied_items(pr1).items(): @@ -695,41 +810,47 @@ class TestSubcontracting(unittest.TestCase): self.assertEqual(value.batch_no, details.batch_no) def test_item_with_batch_based_on_bom_for_purchase_invoice(self): - ''' - - Set backflush based on BOM - - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no). - - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. - - Transfer the components in multiple batches. - - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches. - - Keep the qty as 2 for Subcontracted Item in the purchase receipt. - ''' + """ + - Set backflush based on BOM + - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no). + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer the components in multiple batches. + - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches. + - Keep the qty as 2 for Subcontracted Item in the purchase receipt. + """ - set_backflush_based_on('BOM') - item_code = 'Subcontracted Item SA4' - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + set_backflush_based_on("BOM") + item_code = "Subcontracted Item SA4" + items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}] - rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10}, - {'item_code': 'Subcontracted SRM Item 2', 'qty': 10}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 1} + rm_items = [ + {"item_code": "Subcontracted SRM Item 1", "qty": 10}, + {"item_code": "Subcontracted SRM Item 2", "qty": 10}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, + {"item_code": "Subcontracted SRM Item 3", "qty": 1}, ] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_invoice(po.name) pr1.update_stock = 1 pr1.items[0].qty = 2 - pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.items[0].expense_account = "Stock Adjustment - _TC" add_second_row_in_pr(pr1) pr1.save() pr1.submit() @@ -740,7 +861,7 @@ class TestSubcontracting(unittest.TestCase): pr1 = make_purchase_invoice(po.name) pr1.update_stock = 1 pr1.items[0].qty = 2 - pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.items[0].expense_account = "Stock Adjustment - _TC" add_second_row_in_pr(pr1) pr1.save() pr1.submit() @@ -751,34 +872,50 @@ class TestSubcontracting(unittest.TestCase): pr1 = make_purchase_invoice(po.name) pr1.update_stock = 1 pr1.items[0].qty = 2 - pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.items[0].expense_account = "Stock Adjustment - _TC" pr1.save() pr1.submit() for key, value in get_supplied_items(pr1).items(): self.assertEqual(value.qty, 2) + def add_second_row_in_pr(pr): item_dict = {} - for column in ['item_code', 'item_name', 'qty', 'uom', 'warehouse', 'stock_uom', - 'purchase_order', 'purchase_order_item', 'conversion_factor', 'rate', 'expense_account', 'po_detail']: + for column in [ + "item_code", + "item_name", + "qty", + "uom", + "warehouse", + "stock_uom", + "purchase_order", + "purchase_order_item", + "conversion_factor", + "rate", + "expense_account", + "po_detail", + ]: item_dict[column] = pr.items[0].get(column) - pr.append('items', item_dict) + pr.append("items", item_dict) pr.set_missing_values() + def get_supplied_items(pr_doc): supplied_items = {} - for row in pr_doc.get('supplied_items'): + for row in pr_doc.get("supplied_items"): if row.rm_item_code not in supplied_items: - supplied_items.setdefault(row.rm_item_code, - frappe._dict({'qty': 0, 'serial_no': [], 'batch_no': defaultdict(float)})) + supplied_items.setdefault( + row.rm_item_code, frappe._dict({"qty": 0, "serial_no": [], "batch_no": defaultdict(float)}) + ) details = supplied_items[row.rm_item_code] update_item_details(row, details) return supplied_items + def make_stock_in_entry(**args): args = frappe._dict(args) @@ -786,11 +923,17 @@ def make_stock_in_entry(**args): for row in args.rm_items: row = frappe._dict(row) - doc = make_stock_entry(target=row.warehouse or '_Test Warehouse - _TC', - item_code=row.item_code, qty=row.qty or 1, basic_rate=row.rate or 100) + doc = make_stock_entry( + target=row.warehouse or "_Test Warehouse - _TC", + item_code=row.item_code, + qty=row.qty or 1, + basic_rate=row.rate or 100, + ) if row.item_code not in items: - items.setdefault(row.item_code, frappe._dict({'qty': 0, 'serial_no': [], 'batch_no': defaultdict(float)})) + items.setdefault( + row.item_code, frappe._dict({"qty": 0, "serial_no": [], "batch_no": defaultdict(float)}) + ) child_row = doc.items[0] details = items[child_row.item_code] @@ -798,15 +941,20 @@ def make_stock_in_entry(**args): return items + def update_item_details(child_row, details): - details.qty += (child_row.get('qty') if child_row.doctype == 'Stock Entry Detail' - else child_row.get('consumed_qty')) + details.qty += ( + child_row.get("qty") + if child_row.doctype == "Stock Entry Detail" + else child_row.get("consumed_qty") + ) if child_row.serial_no: details.serial_no.extend(get_serial_nos(child_row.serial_no)) if child_row.batch_no: - details.batch_no[child_row.batch_no] += (child_row.get('qty') or child_row.get('consumed_qty')) + details.batch_no[child_row.batch_no] += child_row.get("qty") or child_row.get("consumed_qty") + def make_stock_transfer_entry(**args): args = frappe._dict(args) @@ -815,21 +963,27 @@ def make_stock_transfer_entry(**args): for row in args.rm_items: row = frappe._dict(row) - item = {'item_code': row.main_item_code or args.main_item_code, 'rm_item_code': row.item_code, - 'qty': row.qty or 1, 'item_name': row.item_code, 'rate': row.rate or 100, - 'stock_uom': row.stock_uom or 'Nos', 'warehouse': row.warehuose or '_Test Warehouse - _TC'} + item = { + "item_code": row.main_item_code or args.main_item_code, + "rm_item_code": row.item_code, + "qty": row.qty or 1, + "item_name": row.item_code, + "rate": row.rate or 100, + "stock_uom": row.stock_uom or "Nos", + "warehouse": row.warehuose or "_Test Warehouse - _TC", + } item_details = args.itemwise_details.get(row.item_code) if item_details and item_details.serial_no: - serial_nos = item_details.serial_no[0:cint(row.qty)] - item['serial_no'] = '\n'.join(serial_nos) + serial_nos = item_details.serial_no[0 : cint(row.qty)] + item["serial_no"] = "\n".join(serial_nos) item_details.serial_no = list(set(item_details.serial_no) - set(serial_nos)) if item_details and item_details.batch_no: for batch_no, batch_qty in item_details.batch_no.items(): if batch_qty >= row.qty: - item['batch_no'] = batch_no + item["batch_no"] = batch_no item_details.batch_no[batch_no] -= row.qty break @@ -842,42 +996,70 @@ def make_stock_transfer_entry(**args): return doc + def make_subcontract_items(): - sub_contracted_items = {'Subcontracted Item SA1': {}, 'Subcontracted Item SA2': {}, 'Subcontracted Item SA3': {}, - 'Subcontracted Item SA4': {'has_batch_no': 1, 'create_new_batch': 1, 'batch_number_series': 'SBAT.####'}, - 'Subcontracted Item SA5': {}, 'Subcontracted Item SA6': {}} + sub_contracted_items = { + "Subcontracted Item SA1": {}, + "Subcontracted Item SA2": {}, + "Subcontracted Item SA3": {}, + "Subcontracted Item SA4": { + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "SBAT.####", + }, + "Subcontracted Item SA5": {}, + "Subcontracted Item SA6": {}, + } for item, properties in sub_contracted_items.items(): - if not frappe.db.exists('Item', item): - properties.update({'is_stock_item': 1, 'is_sub_contracted_item': 1}) + if not frappe.db.exists("Item", item): + properties.update({"is_stock_item": 1, "is_sub_contracted_item": 1}) make_item(item, properties) + def make_raw_materials(): - raw_materials = {'Subcontracted SRM Item 1': {}, - 'Subcontracted SRM Item 2': {'has_serial_no': 1, 'serial_no_series': 'SRI.####'}, - 'Subcontracted SRM Item 3': {'has_batch_no': 1, 'create_new_batch': 1, 'batch_number_series': 'BAT.####'}, - 'Subcontracted SRM Item 4': {'has_serial_no': 1, 'serial_no_series': 'SRII.####'}, - 'Subcontracted SRM Item 5': {'has_serial_no': 1, 'serial_no_series': 'SRII.####'}} + raw_materials = { + "Subcontracted SRM Item 1": {}, + "Subcontracted SRM Item 2": {"has_serial_no": 1, "serial_no_series": "SRI.####"}, + "Subcontracted SRM Item 3": { + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BAT.####", + }, + "Subcontracted SRM Item 4": {"has_serial_no": 1, "serial_no_series": "SRII.####"}, + "Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRII.####"}, + } for item, properties in raw_materials.items(): - if not frappe.db.exists('Item', item): - properties.update({'is_stock_item': 1}) + if not frappe.db.exists("Item", item): + properties.update({"is_stock_item": 1}) make_item(item, properties) + def make_bom_for_subcontracted_items(): boms = { - 'Subcontracted Item SA1': ['Subcontracted SRM Item 1', 'Subcontracted SRM Item 2', 'Subcontracted SRM Item 3'], - 'Subcontracted Item SA2': ['Subcontracted SRM Item 2'], - 'Subcontracted Item SA3': ['Subcontracted SRM Item 2'], - 'Subcontracted Item SA4': ['Subcontracted SRM Item 1', 'Subcontracted SRM Item 2', 'Subcontracted SRM Item 3'], - 'Subcontracted Item SA5': ['Subcontracted SRM Item 5'], - 'Subcontracted Item SA6': ['Subcontracted SRM Item 3'] + "Subcontracted Item SA1": [ + "Subcontracted SRM Item 1", + "Subcontracted SRM Item 2", + "Subcontracted SRM Item 3", + ], + "Subcontracted Item SA2": ["Subcontracted SRM Item 2"], + "Subcontracted Item SA3": ["Subcontracted SRM Item 2"], + "Subcontracted Item SA4": [ + "Subcontracted SRM Item 1", + "Subcontracted SRM Item 2", + "Subcontracted SRM Item 3", + ], + "Subcontracted Item SA5": ["Subcontracted SRM Item 5"], + "Subcontracted Item SA6": ["Subcontracted SRM Item 3"], } for item_code, raw_materials in boms.items(): - if not frappe.db.exists('BOM', {'item': item_code}): + if not frappe.db.exists("BOM", {"item": item_code}): make_bom(item=item_code, raw_materials=raw_materials, rate=100) + def set_backflush_based_on(based_on): - frappe.db.set_value('Buying Settings', None, - 'backflush_raw_materials_of_subcontract_based_on', based_on) + frappe.db.set_value( + "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", based_on + ) diff --git a/erpnext/tests/test_webform.py b/erpnext/tests/test_webform.py index 19255db33c..202467b545 100644 --- a/erpnext/tests/test_webform.py +++ b/erpnext/tests/test_webform.py @@ -7,132 +7,143 @@ from erpnext.buying.doctype.purchase_order.test_purchase_order import create_pur class TestWebsite(unittest.TestCase): def test_permission_for_custom_doctype(self): - create_user('Supplier 1', 'supplier1@gmail.com') - create_user('Supplier 2', 'supplier2@gmail.com') - create_supplier_with_contact('Supplier1', 'All Supplier Groups', 'Supplier 1', 'supplier1@gmail.com') - create_supplier_with_contact('Supplier2', 'All Supplier Groups', 'Supplier 2', 'supplier2@gmail.com') - po1 = create_purchase_order(supplier='Supplier1') - po2 = create_purchase_order(supplier='Supplier2') + create_user("Supplier 1", "supplier1@gmail.com") + create_user("Supplier 2", "supplier2@gmail.com") + create_supplier_with_contact( + "Supplier1", "All Supplier Groups", "Supplier 1", "supplier1@gmail.com" + ) + create_supplier_with_contact( + "Supplier2", "All Supplier Groups", "Supplier 2", "supplier2@gmail.com" + ) + po1 = create_purchase_order(supplier="Supplier1") + po2 = create_purchase_order(supplier="Supplier2") create_custom_doctype() create_webform() - create_order_assignment(supplier='Supplier1', po = po1.name) - create_order_assignment(supplier='Supplier2', po = po2.name) + create_order_assignment(supplier="Supplier1", po=po1.name) + create_order_assignment(supplier="Supplier2", po=po2.name) frappe.set_user("Administrator") # checking if data consist of all order assignment of Supplier1 and Supplier2 - self.assertTrue('Supplier1' and 'Supplier2' in [data.supplier for data in get_data()]) + self.assertTrue("Supplier1" and "Supplier2" in [data.supplier for data in get_data()]) frappe.set_user("supplier1@gmail.com") # checking if data only consist of order assignment of Supplier1 - self.assertTrue('Supplier1' in [data.supplier for data in get_data()]) - self.assertFalse([data.supplier for data in get_data() if data.supplier != 'Supplier1']) + self.assertTrue("Supplier1" in [data.supplier for data in get_data()]) + self.assertFalse([data.supplier for data in get_data() if data.supplier != "Supplier1"]) frappe.set_user("supplier2@gmail.com") # checking if data only consist of order assignment of Supplier2 - self.assertTrue('Supplier2' in [data.supplier for data in get_data()]) - self.assertFalse([data.supplier for data in get_data() if data.supplier != 'Supplier2']) + self.assertTrue("Supplier2" in [data.supplier for data in get_data()]) + self.assertFalse([data.supplier for data in get_data() if data.supplier != "Supplier2"]) frappe.set_user("Administrator") + def get_data(): - webform_list_contexts = frappe.get_hooks('webform_list_context') + webform_list_contexts = frappe.get_hooks("webform_list_context") if webform_list_contexts: - context = frappe._dict(frappe.get_attr(webform_list_contexts[0])('Buying') or {}) - kwargs = dict(doctype='Order Assignment', order_by = 'modified desc') + context = frappe._dict(frappe.get_attr(webform_list_contexts[0])("Buying") or {}) + kwargs = dict(doctype="Order Assignment", order_by="modified desc") return context.get_list(**kwargs) + def create_user(name, email): - frappe.get_doc({ - 'doctype': 'User', - 'send_welcome_email': 0, - 'user_type': 'Website User', - 'first_name': name, - 'email': email, - 'roles': [{"doctype": "Has Role", "role": "Supplier"}] - }).insert(ignore_if_duplicate = True) + frappe.get_doc( + { + "doctype": "User", + "send_welcome_email": 0, + "user_type": "Website User", + "first_name": name, + "email": email, + "roles": [{"doctype": "Has Role", "role": "Supplier"}], + } + ).insert(ignore_if_duplicate=True) + def create_supplier_with_contact(name, group, contact_name, contact_email): - supplier = frappe.get_doc({ - 'doctype': 'Supplier', - 'supplier_name': name, - 'supplier_group': group - }).insert(ignore_if_duplicate = True) + supplier = frappe.get_doc( + {"doctype": "Supplier", "supplier_name": name, "supplier_group": group} + ).insert(ignore_if_duplicate=True) - if not frappe.db.exists('Contact', contact_name+'-1-'+name): + if not frappe.db.exists("Contact", contact_name + "-1-" + name): new_contact = frappe.new_doc("Contact") new_contact.first_name = contact_name - new_contact.is_primary_contact = True, - new_contact.append('links', { - "link_doctype": "Supplier", - "link_name": supplier.name - }) - new_contact.append('email_ids', { - "email_id": contact_email, - "is_primary": 1 - }) + new_contact.is_primary_contact = (True,) + new_contact.append("links", {"link_doctype": "Supplier", "link_name": supplier.name}) + new_contact.append("email_ids", {"email_id": contact_email, "is_primary": 1}) new_contact.insert(ignore_mandatory=True) + def create_custom_doctype(): - frappe.get_doc({ - 'doctype': 'DocType', - 'name': 'Order Assignment', - 'module': 'Buying', - 'custom': 1, - 'autoname': 'field:po', - 'fields': [ - {'label': 'PO', 'fieldname': 'po', 'fieldtype': 'Link', 'options': 'Purchase Order'}, - {'label': 'Supplier', 'fieldname': 'supplier', 'fieldtype': 'Data', "fetch_from": "po.supplier"} - ], - 'permissions': [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - }, - { - "read": 1, - "role": "Supplier" - } - ] - }).insert(ignore_if_duplicate = True) + frappe.get_doc( + { + "doctype": "DocType", + "name": "Order Assignment", + "module": "Buying", + "custom": 1, + "autoname": "field:po", + "fields": [ + {"label": "PO", "fieldname": "po", "fieldtype": "Link", "options": "Purchase Order"}, + { + "label": "Supplier", + "fieldname": "supplier", + "fieldtype": "Data", + "fetch_from": "po.supplier", + }, + ], + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1, + }, + {"read": 1, "role": "Supplier"}, + ], + } + ).insert(ignore_if_duplicate=True) + def create_webform(): - frappe.get_doc({ - 'doctype': 'Web Form', - 'module': 'Buying', - 'title': 'SO Schedule', - 'route': 'so-schedule', - 'doc_type': 'Order Assignment', - 'web_form_fields': [ - { - 'doctype': 'Web Form Field', - 'fieldname': 'po', - 'fieldtype': 'Link', - 'options': 'Purchase Order', - 'label': 'PO' - }, - { - 'doctype': 'Web Form Field', - 'fieldname': 'supplier', - 'fieldtype': 'Data', - 'label': 'Supplier' - } - ] + frappe.get_doc( + { + "doctype": "Web Form", + "module": "Buying", + "title": "SO Schedule", + "route": "so-schedule", + "doc_type": "Order Assignment", + "web_form_fields": [ + { + "doctype": "Web Form Field", + "fieldname": "po", + "fieldtype": "Link", + "options": "Purchase Order", + "label": "PO", + }, + { + "doctype": "Web Form Field", + "fieldname": "supplier", + "fieldtype": "Data", + "label": "Supplier", + }, + ], + } + ).insert(ignore_if_duplicate=True) - }).insert(ignore_if_duplicate = True) def create_order_assignment(supplier, po): - frappe.get_doc({ - 'doctype': 'Order Assignment', - 'po': po, - 'supplier': supplier, - }).insert(ignore_if_duplicate = True) \ No newline at end of file + frappe.get_doc( + { + "doctype": "Order Assignment", + "po": po, + "supplier": supplier, + } + ).insert(ignore_if_duplicate=True) diff --git a/erpnext/tests/test_woocommerce.py b/erpnext/tests/test_woocommerce.py index 4a451aba4d..663464b6b7 100644 --- a/erpnext/tests/test_woocommerce.py +++ b/erpnext/tests/test_woocommerce.py @@ -25,32 +25,126 @@ class TestWoocommerce(unittest.TestCase): woo_settings.save(ignore_permissions=True) def test_sales_order_for_woocommerce(self): - frappe.flags.woocomm_test_order_data = {"id":75,"parent_id":0,"number":"74","order_key":"wc_order_5aa1281c2dacb","created_via":"checkout","version":"3.3.3","status":"processing","currency":"INR","date_created":"2018-03-08T12:10:04","date_created_gmt":"2018-03-08T12:10:04","date_modified":"2018-03-08T12:10:04","date_modified_gmt":"2018-03-08T12:10:04","discount_total":"0.00","discount_tax":"0.00","shipping_total":"150.00","shipping_tax":"0.00","cart_tax":"0.00","total":"649.00","total_tax":"0.00","prices_include_tax":False,"customer_id":12,"customer_ip_address":"103.54.99.5","customer_user_agent":"mozilla\\/5.0 (x11; linux x86_64) applewebkit\\/537.36 (khtml, like gecko) chrome\\/64.0.3282.186 safari\\/537.36","customer_note":"","billing":{"first_name":"Tony","last_name":"Stark","company":"_Test Company","address_1":"Mumbai","address_2":"","city":"Dadar","state":"MH","postcode":"123","country":"IN","email":"tony@gmail.com","phone":"123457890"},"shipping":{"first_name":"Tony","last_name":"Stark","company":"","address_1":"Mumbai","address_2":"","city":"Dadar","state":"MH","postcode":"123","country":"IN"},"payment_method":"cod","payment_method_title":"Cash on delivery","transaction_id":"","date_paid":"","date_paid_gmt":"","date_completed":"","date_completed_gmt":"","cart_hash":"8e76b020d5790066496f244860c4703f","meta_data":[],"line_items":[{"id":80,"name":"Marvel","product_id":56,"variation_id":0,"quantity":1,"tax_class":"","subtotal":"499.00","subtotal_tax":"0.00","total":"499.00","total_tax":"0.00","taxes":[],"meta_data":[],"sku":"","price":499}],"tax_lines":[],"shipping_lines":[{"id":81,"method_title":"Flat rate","method_id":"flat_rate:1","total":"150.00","total_tax":"0.00","taxes":[],"meta_data":[{"id":623,"key":"Items","value":"Marvel × 1"}]}],"fee_lines":[],"coupon_lines":[],"refunds":[]} + frappe.flags.woocomm_test_order_data = { + "id": 75, + "parent_id": 0, + "number": "74", + "order_key": "wc_order_5aa1281c2dacb", + "created_via": "checkout", + "version": "3.3.3", + "status": "processing", + "currency": "INR", + "date_created": "2018-03-08T12:10:04", + "date_created_gmt": "2018-03-08T12:10:04", + "date_modified": "2018-03-08T12:10:04", + "date_modified_gmt": "2018-03-08T12:10:04", + "discount_total": "0.00", + "discount_tax": "0.00", + "shipping_total": "150.00", + "shipping_tax": "0.00", + "cart_tax": "0.00", + "total": "649.00", + "total_tax": "0.00", + "prices_include_tax": False, + "customer_id": 12, + "customer_ip_address": "103.54.99.5", + "customer_user_agent": "mozilla\\/5.0 (x11; linux x86_64) applewebkit\\/537.36 (khtml, like gecko) chrome\\/64.0.3282.186 safari\\/537.36", + "customer_note": "", + "billing": { + "first_name": "Tony", + "last_name": "Stark", + "company": "_Test Company", + "address_1": "Mumbai", + "address_2": "", + "city": "Dadar", + "state": "MH", + "postcode": "123", + "country": "IN", + "email": "tony@gmail.com", + "phone": "123457890", + }, + "shipping": { + "first_name": "Tony", + "last_name": "Stark", + "company": "", + "address_1": "Mumbai", + "address_2": "", + "city": "Dadar", + "state": "MH", + "postcode": "123", + "country": "IN", + }, + "payment_method": "cod", + "payment_method_title": "Cash on delivery", + "transaction_id": "", + "date_paid": "", + "date_paid_gmt": "", + "date_completed": "", + "date_completed_gmt": "", + "cart_hash": "8e76b020d5790066496f244860c4703f", + "meta_data": [], + "line_items": [ + { + "id": 80, + "name": "Marvel", + "product_id": 56, + "variation_id": 0, + "quantity": 1, + "tax_class": "", + "subtotal": "499.00", + "subtotal_tax": "0.00", + "total": "499.00", + "total_tax": "0.00", + "taxes": [], + "meta_data": [], + "sku": "", + "price": 499, + } + ], + "tax_lines": [], + "shipping_lines": [ + { + "id": 81, + "method_title": "Flat rate", + "method_id": "flat_rate:1", + "total": "150.00", + "total_tax": "0.00", + "taxes": [], + "meta_data": [{"id": 623, "key": "Items", "value": "Marvel × 1"}], + } + ], + "fee_lines": [], + "coupon_lines": [], + "refunds": [], + } order() - self.assertTrue(frappe.get_value("Customer",{"woocommerce_email":"tony@gmail.com"})) - self.assertTrue(frappe.get_value("Item",{"woocommerce_id": 56})) - self.assertTrue(frappe.get_value("Sales Order",{"woocommerce_id":75})) + self.assertTrue(frappe.get_value("Customer", {"woocommerce_email": "tony@gmail.com"})) + self.assertTrue(frappe.get_value("Item", {"woocommerce_id": 56})) + self.assertTrue(frappe.get_value("Sales Order", {"woocommerce_id": 75})) frappe.flags.woocomm_test_order_data = {} + def emulate_request(): # Emulate Woocommerce Request headers = { - "X-Wc-Webhook-Event":"created", - "X-Wc-Webhook-Signature":"h1SjzQMPwd68MF5bficeFq20/RkQeRLsb9AVCUz/rLs=" + "X-Wc-Webhook-Event": "created", + "X-Wc-Webhook-Signature": "h1SjzQMPwd68MF5bficeFq20/RkQeRLsb9AVCUz/rLs=", } # Emulate Request Data data = """{"id":74,"parent_id":0,"number":"74","order_key":"wc_order_5aa1281c2dacb","created_via":"checkout","version":"3.3.3","status":"processing","currency":"INR","date_created":"2018-03-08T12:10:04","date_created_gmt":"2018-03-08T12:10:04","date_modified":"2018-03-08T12:10:04","date_modified_gmt":"2018-03-08T12:10:04","discount_total":"0.00","discount_tax":"0.00","shipping_total":"150.00","shipping_tax":"0.00","cart_tax":"0.00","total":"649.00","total_tax":"0.00","prices_include_tax":false,"customer_id":12,"customer_ip_address":"103.54.99.5","customer_user_agent":"mozilla\\/5.0 (x11; linux x86_64) applewebkit\\/537.36 (khtml, like gecko) chrome\\/64.0.3282.186 safari\\/537.36","customer_note":"","billing":{"first_name":"Tony","last_name":"Stark","company":"Woocommerce","address_1":"Mumbai","address_2":"","city":"Dadar","state":"MH","postcode":"123","country":"IN","email":"tony@gmail.com","phone":"123457890"},"shipping":{"first_name":"Tony","last_name":"Stark","company":"","address_1":"Mumbai","address_2":"","city":"Dadar","state":"MH","postcode":"123","country":"IN"},"payment_method":"cod","payment_method_title":"Cash on delivery","transaction_id":"","date_paid":null,"date_paid_gmt":null,"date_completed":null,"date_completed_gmt":null,"cart_hash":"8e76b020d5790066496f244860c4703f","meta_data":[],"line_items":[{"id":80,"name":"Marvel","product_id":56,"variation_id":0,"quantity":1,"tax_class":"","subtotal":"499.00","subtotal_tax":"0.00","total":"499.00","total_tax":"0.00","taxes":[],"meta_data":[],"sku":"","price":499}],"tax_lines":[],"shipping_lines":[{"id":81,"method_title":"Flat rate","method_id":"flat_rate:1","total":"150.00","total_tax":"0.00","taxes":[],"meta_data":[{"id":623,"key":"Items","value":"Marvel × 1"}]}],"fee_lines":[],"coupon_lines":[],"refunds":[]}""" # Build URL - port = frappe.get_site_config().webserver_port or '8000' + port = frappe.get_site_config().webserver_port or "8000" - if os.environ.get('CI'): - host = 'localhost' + if os.environ.get("CI"): + host = "localhost" else: host = frappe.local.site - url = "http://{site}:{port}/api/method/erpnext.erpnext_integrations.connectors.woocommerce_connection.order".format(site=host, port=port) + url = "http://{site}:{port}/api/method/erpnext.erpnext_integrations.connectors.woocommerce_connection.order".format( + site=host, port=port + ) r = requests.post(url=url, headers=headers, data=data) diff --git a/erpnext/tests/test_zform_loads.py b/erpnext/tests/test_zform_loads.py index 5b82c7bbdd..26e60c001a 100644 --- a/erpnext/tests/test_zform_loads.py +++ b/erpnext/tests/test_zform_loads.py @@ -7,11 +7,14 @@ from frappe.www.printview import get_html_and_style class TestFormLoads(FrappeTestCase): - @change_settings("Print Settings", {"allow_print_for_cancelled": 1}) def test_load(self): erpnext_modules = frappe.get_all("Module Def", filters={"app_name": "erpnext"}, pluck="name") - doctypes = frappe.get_all("DocType", {"istable": 0, "issingle": 0, "is_virtual": 0, "module": ("in", erpnext_modules)}, pluck="name") + doctypes = frappe.get_all( + "DocType", + {"istable": 0, "issingle": 0, "is_virtual": 0, "module": ("in", erpnext_modules)}, + pluck="name", + ) for doctype in doctypes: last_doc = frappe.db.get_value(doctype, {}, "name", order_by="modified desc") @@ -23,7 +26,7 @@ class TestFormLoads(FrappeTestCase): def assertFormLoad(self, doctype, docname): # reset previous response - frappe.response = frappe._dict({"docs":[]}) + frappe.response = frappe._dict({"docs": []}) frappe.response.docinfo = None try: @@ -31,8 +34,12 @@ class TestFormLoads(FrappeTestCase): except Exception as e: self.fail(f"Failed to load {doctype}-{docname}: {e}") - self.assertTrue(frappe.response.docs, msg=f"expected document in reponse, found: {frappe.response.docs}") - self.assertTrue(frappe.response.docinfo, msg=f"expected docinfo in reponse, found: {frappe.response.docinfo}") + self.assertTrue( + frappe.response.docs, msg=f"expected document in reponse, found: {frappe.response.docs}" + ) + self.assertTrue( + frappe.response.docinfo, msg=f"expected docinfo in reponse, found: {frappe.response.docinfo}" + ) def assertDocPrint(self, doctype, docname): doc = frappe.get_doc(doctype, docname) @@ -44,9 +51,8 @@ class TestFormLoads(FrappeTestCase): messages_after = frappe.get_message_log() if len(messages_after) > len(messages_before): - new_messages = messages_after[len(messages_before):] - self.fail("Print view showing error/warnings: \n" - + "\n".join(str(msg) for msg in new_messages)) + new_messages = messages_after[len(messages_before) :] + self.fail("Print view showing error/warnings: \n" + "\n".join(str(msg) for msg in new_messages)) # html should exist self.assertTrue(bool(ret["html"])) diff --git a/erpnext/tests/ui_test_bulk_transaction_processing.py b/erpnext/tests/ui_test_bulk_transaction_processing.py index d78689eb5b..a0dc76d54f 100644 --- a/erpnext/tests/ui_test_bulk_transaction_processing.py +++ b/erpnext/tests/ui_test_bulk_transaction_processing.py @@ -18,4 +18,4 @@ def create_records(): gd.set("default_company", "Test Bulk") gd.save() frappe.clear_cache() - create_so() \ No newline at end of file + create_so() diff --git a/erpnext/tests/ui_test_helpers.py b/erpnext/tests/ui_test_helpers.py index 9c8c371e05..44834c8a77 100644 --- a/erpnext/tests/ui_test_helpers.py +++ b/erpnext/tests/ui_test_helpers.py @@ -9,54 +9,67 @@ def create_employee_records(): frappe.db.sql("DELETE FROM tabEmployee WHERE company='Test Org Chart'") - emp1 = create_employee('Test Employee 1', 'CEO') - emp2 = create_employee('Test Employee 2', 'CTO') - emp3 = create_employee('Test Employee 3', 'Head of Marketing and Sales', emp1) - emp4 = create_employee('Test Employee 4', 'Project Manager', emp2) - emp5 = create_employee('Test Employee 5', 'Engineer', emp2) - emp6 = create_employee('Test Employee 6', 'Analyst', emp3) - emp7 = create_employee('Test Employee 7', 'Software Developer', emp4) + emp1 = create_employee("Test Employee 1", "CEO") + emp2 = create_employee("Test Employee 2", "CTO") + emp3 = create_employee("Test Employee 3", "Head of Marketing and Sales", emp1) + emp4 = create_employee("Test Employee 4", "Project Manager", emp2) + emp5 = create_employee("Test Employee 5", "Engineer", emp2) + emp6 = create_employee("Test Employee 6", "Analyst", emp3) + emp7 = create_employee("Test Employee 7", "Software Developer", emp4) employees = [emp1, emp2, emp3, emp4, emp5, emp6, emp7] return employees + @frappe.whitelist() def get_employee_records(): - return frappe.db.get_list('Employee', filters={ - 'company': 'Test Org Chart' - }, pluck='name', order_by='name') + return frappe.db.get_list( + "Employee", filters={"company": "Test Org Chart"}, pluck="name", order_by="name" + ) + def create_company(): - company = frappe.db.exists('Company', 'Test Org Chart') + company = frappe.db.exists("Company", "Test Org Chart") if not company: - company = frappe.get_doc({ - 'doctype': 'Company', - 'company_name': 'Test Org Chart', - 'country': 'India', - 'default_currency': 'INR' - }).insert().name + company = ( + frappe.get_doc( + { + "doctype": "Company", + "company_name": "Test Org Chart", + "country": "India", + "default_currency": "INR", + } + ) + .insert() + .name + ) return company + def create_employee(first_name, designation, reports_to=None): - employee = frappe.db.exists('Employee', {'first_name': first_name, 'designation': designation}) + employee = frappe.db.exists("Employee", {"first_name": first_name, "designation": designation}) if not employee: - employee = frappe.get_doc({ - 'doctype': 'Employee', - 'first_name': first_name, - 'company': 'Test Org Chart', - 'gender': 'Female', - 'date_of_birth': getdate('08-12-1998'), - 'date_of_joining': getdate('01-01-2021'), - 'designation': designation, - 'reports_to': reports_to - }).insert().name + employee = ( + frappe.get_doc( + { + "doctype": "Employee", + "first_name": first_name, + "company": "Test Org Chart", + "gender": "Female", + "date_of_birth": getdate("08-12-1998"), + "date_of_joining": getdate("01-01-2021"), + "designation": designation, + "reports_to": reports_to, + } + ) + .insert() + .name + ) return employee + def create_missing_designation(): - if not frappe.db.exists('Designation', 'CTO'): - frappe.get_doc({ - 'doctype': 'Designation', - 'designation_name': 'CTO' - }).insert() + if not frappe.db.exists("Designation", "CTO"): + frappe.get_doc({"doctype": "Designation", "designation_name": "CTO"}).insert() diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index d795253665..159ce7078e 100644 --- a/erpnext/tests/utils.py +++ b/erpnext/tests/utils.py @@ -9,83 +9,77 @@ from frappe.core.doctype.report.report import get_report_module_dotted_path ReportFilters = Dict[str, Any] ReportName = NewType("ReportName", str) + def create_test_contact_and_address(): - frappe.db.sql('delete from tabContact') - frappe.db.sql('delete from `tabContact Email`') - frappe.db.sql('delete from `tabContact Phone`') - frappe.db.sql('delete from tabAddress') - frappe.db.sql('delete from `tabDynamic Link`') + frappe.db.sql("delete from tabContact") + frappe.db.sql("delete from `tabContact Email`") + frappe.db.sql("delete from `tabContact Phone`") + frappe.db.sql("delete from tabAddress") + frappe.db.sql("delete from `tabDynamic Link`") - frappe.get_doc({ - "doctype": "Address", - "address_title": "_Test Address for Customer", - "address_type": "Office", - "address_line1": "Station Road", - "city": "_Test City", - "state": "Test State", - "country": "India", - "links": [ - { - "link_doctype": "Customer", - "link_name": "_Test Customer" - } - ] - }).insert() + frappe.get_doc( + { + "doctype": "Address", + "address_title": "_Test Address for Customer", + "address_type": "Office", + "address_line1": "Station Road", + "city": "_Test City", + "state": "Test State", + "country": "India", + "links": [{"link_doctype": "Customer", "link_name": "_Test Customer"}], + } + ).insert() - contact = frappe.get_doc({ - "doctype": 'Contact', - "first_name": "_Test Contact for _Test Customer", - "links": [ - { - "link_doctype": "Customer", - "link_name": "_Test Customer" - } - ] - }) + contact = frappe.get_doc( + { + "doctype": "Contact", + "first_name": "_Test Contact for _Test Customer", + "links": [{"link_doctype": "Customer", "link_name": "_Test Customer"}], + } + ) contact.add_email("test_contact_customer@example.com", is_primary=True) contact.add_phone("+91 0000000000", is_primary_phone=True) contact.insert() - contact_two = frappe.get_doc({ - "doctype": 'Contact', - "first_name": "_Test Contact 2 for _Test Customer", - "links": [ - { - "link_doctype": "Customer", - "link_name": "_Test Customer" - } - ] - }) + contact_two = frappe.get_doc( + { + "doctype": "Contact", + "first_name": "_Test Contact 2 for _Test Customer", + "links": [{"link_doctype": "Customer", "link_name": "_Test Customer"}], + } + ) contact_two.add_email("test_contact_two_customer@example.com", is_primary=True) contact_two.add_phone("+92 0000000000", is_primary_phone=True) contact_two.insert() def execute_script_report( - report_name: ReportName, - module: str, - filters: ReportFilters, - default_filters: Optional[ReportFilters] = None, - optional_filters: Optional[ReportFilters] = None - ): + report_name: ReportName, + module: str, + filters: ReportFilters, + default_filters: Optional[ReportFilters] = None, + optional_filters: Optional[ReportFilters] = None, +): """Util for testing execution of a report with specified filters. Tests the execution of report with default_filters + filters. Tests the execution using optional_filters one at a time. Args: - report_name: Human readable name of report (unscrubbed) - module: module to which report belongs to - filters: specific values for filters - default_filters: default values for filters such as company name. - optional_filters: filters which should be tested one at a time in addition to default filters. + report_name: Human readable name of report (unscrubbed) + module: module to which report belongs to + filters: specific values for filters + default_filters: default values for filters such as company name. + optional_filters: filters which should be tested one at a time in addition to default filters. """ if default_filters is None: default_filters = {} test_filters = [] - report_execute_fn = frappe.get_attr(get_report_module_dotted_path(module, report_name) + ".execute") + report_execute_fn = frappe.get_attr( + get_report_module_dotted_path(module, report_name) + ".execute" + ) report_filters = frappe._dict(default_filters).copy().update(filters) test_filters.append(report_filters) diff --git a/erpnext/utilities/__init__.py b/erpnext/utilities/__init__.py index 3749cdeaa8..c2b4229f17 100644 --- a/erpnext/utilities/__init__.py +++ b/erpnext/utilities/__init__.py @@ -7,9 +7,12 @@ from erpnext.utilities.activation import get_level def update_doctypes(): - for d in frappe.db.sql("""select df.parent, df.fieldname + for d in frappe.db.sql( + """select df.parent, df.fieldname from tabDocField df, tabDocType dt where df.fieldname - like "%description%" and df.parent = dt.name and dt.istable = 1""", as_dict=1): + like "%description%" and df.parent = dt.name and dt.istable = 1""", + as_dict=1, + ): dt = frappe.get_doc("DocType", d.parent) for f in dt.fields: @@ -18,20 +21,17 @@ def update_doctypes(): dt.save() break + def get_site_info(site_info): # called via hook - company = frappe.db.get_single_value('Global Defaults', 'default_company') + company = frappe.db.get_single_value("Global Defaults", "default_company") domain = None if not company: - company = frappe.db.sql('select name from `tabCompany` order by creation asc') + company = frappe.db.sql("select name from `tabCompany` order by creation asc") company = company[0][0] if company else None if company: - domain = frappe.get_cached_value('Company', cstr(company), 'domain') + domain = frappe.get_cached_value("Company", cstr(company), "domain") - return { - 'company': company, - 'domain': domain, - 'activation': get_level() - } + return {"company": company, "domain": domain, "activation": get_level()} diff --git a/erpnext/utilities/activation.py b/erpnext/utilities/activation.py index faf3fd4a20..43af0dc3c1 100644 --- a/erpnext/utilities/activation.py +++ b/erpnext/utilities/activation.py @@ -41,7 +41,7 @@ def get_level(): "Supplier": 5, "Task": 5, "User": 5, - "Work Order": 5 + "Work Order": 5, } for doctype, min_count in doctypes.items(): @@ -50,111 +50,118 @@ def get_level(): activation_level += 1 sales_data.append({doctype: count}) - if frappe.db.get_single_value('System Settings', 'setup_complete'): + if frappe.db.get_single_value("System Settings", "setup_complete"): activation_level += 1 - communication_number = frappe.db.count('Communication', dict(communication_medium='Email')) + communication_number = frappe.db.count("Communication", dict(communication_medium="Email")) if communication_number > 10: activation_level += 1 sales_data.append({"Communication": communication_number}) # recent login - if frappe.db.sql('select name from tabUser where last_login > date_sub(now(), interval 2 day) limit 1'): + if frappe.db.sql( + "select name from tabUser where last_login > date_sub(now(), interval 2 day) limit 1" + ): activation_level += 1 level = {"activation_level": activation_level, "sales_data": sales_data} return level + def get_help_messages(): - '''Returns help messages to be shown on Desktop''' + """Returns help messages to be shown on Desktop""" if get_level() > 6: return [] - domain = frappe.get_cached_value('Company', erpnext.get_default_company(), 'domain') + domain = frappe.get_cached_value("Company", erpnext.get_default_company(), "domain") messages = [] message_settings = [ frappe._dict( - doctype='Lead', - title=_('Create Leads'), - description=_('Leads help you get business, add all your contacts and more as your leads'), - action=_('Create Lead'), - route='List/Lead', - domain=('Manufacturing', 'Retail', 'Services', 'Distribution'), - target=3 + doctype="Lead", + title=_("Create Leads"), + description=_("Leads help you get business, add all your contacts and more as your leads"), + action=_("Create Lead"), + route="List/Lead", + domain=("Manufacturing", "Retail", "Services", "Distribution"), + target=3, ), frappe._dict( - doctype='Quotation', - title=_('Create customer quotes'), - description=_('Quotations are proposals, bids you have sent to your customers'), - action=_('Create Quotation'), - route='List/Quotation', - domain=('Manufacturing', 'Retail', 'Services', 'Distribution'), - target=3 + doctype="Quotation", + title=_("Create customer quotes"), + description=_("Quotations are proposals, bids you have sent to your customers"), + action=_("Create Quotation"), + route="List/Quotation", + domain=("Manufacturing", "Retail", "Services", "Distribution"), + target=3, ), frappe._dict( - doctype='Sales Order', - title=_('Manage your orders'), - description=_('Create Sales Orders to help you plan your work and deliver on-time'), - action=_('Create Sales Order'), - route='List/Sales Order', - domain=('Manufacturing', 'Retail', 'Services', 'Distribution'), - target=3 + doctype="Sales Order", + title=_("Manage your orders"), + description=_("Create Sales Orders to help you plan your work and deliver on-time"), + action=_("Create Sales Order"), + route="List/Sales Order", + domain=("Manufacturing", "Retail", "Services", "Distribution"), + target=3, ), frappe._dict( - doctype='Purchase Order', - title=_('Create Purchase Orders'), - description=_('Purchase orders help you plan and follow up on your purchases'), - action=_('Create Purchase Order'), - route='List/Purchase Order', - domain=('Manufacturing', 'Retail', 'Services', 'Distribution'), - target=3 + doctype="Purchase Order", + title=_("Create Purchase Orders"), + description=_("Purchase orders help you plan and follow up on your purchases"), + action=_("Create Purchase Order"), + route="List/Purchase Order", + domain=("Manufacturing", "Retail", "Services", "Distribution"), + target=3, ), frappe._dict( - doctype='User', - title=_('Create Users'), - description=_('Add the rest of your organization as your users. You can also add invite Customers to your portal by adding them from Contacts'), - action=_('Create User'), - route='List/User', - domain=('Manufacturing', 'Retail', 'Services', 'Distribution'), - target=3 + doctype="User", + title=_("Create Users"), + description=_( + "Add the rest of your organization as your users. You can also add invite Customers to your portal by adding them from Contacts" + ), + action=_("Create User"), + route="List/User", + domain=("Manufacturing", "Retail", "Services", "Distribution"), + target=3, ), frappe._dict( - doctype='Timesheet', - title=_('Add Timesheets'), - description=_('Timesheets help keep track of time, cost and billing for activites done by your team'), - action=_('Create Timesheet'), - route='List/Timesheet', - domain=('Services',), - target=5 + doctype="Timesheet", + title=_("Add Timesheets"), + description=_( + "Timesheets help keep track of time, cost and billing for activites done by your team" + ), + action=_("Create Timesheet"), + route="List/Timesheet", + domain=("Services",), + target=5, ), frappe._dict( - doctype='Student', - title=_('Add Students'), - description=_('Students are at the heart of the system, add all your students'), - action=_('Create Student'), - route='List/Student', - domain=('Education',), - target=5 + doctype="Student", + title=_("Add Students"), + description=_("Students are at the heart of the system, add all your students"), + action=_("Create Student"), + route="List/Student", + domain=("Education",), + target=5, ), frappe._dict( - doctype='Student Batch', - title=_('Group your students in batches'), - description=_('Student Batches help you track attendance, assessments and fees for students'), - action=_('Create Student Batch'), - route='List/Student Batch', - domain=('Education',), - target=3 + doctype="Student Batch", + title=_("Group your students in batches"), + description=_("Student Batches help you track attendance, assessments and fees for students"), + action=_("Create Student Batch"), + route="List/Student Batch", + domain=("Education",), + target=3, ), frappe._dict( - doctype='Employee', - title=_('Create Employee Records'), - description=_('Create Employee records to manage leaves, expense claims and payroll'), - action=_('Create Employee'), - route='List/Employee', - target=3 - ) + doctype="Employee", + title=_("Create Employee Records"), + description=_("Create Employee records to manage leaves, expense claims and payroll"), + action=_("Create Employee"), + route="List/Employee", + target=3, + ), ] for m in message_settings: diff --git a/erpnext/utilities/bot.py b/erpnext/utilities/bot.py index 87a350864f..5c2e576dd2 100644 --- a/erpnext/utilities/bot.py +++ b/erpnext/utilities/bot.py @@ -9,13 +9,16 @@ from frappe.utils.bot import BotParser class FindItemBot(BotParser): def get_reply(self): - if self.startswith('where is', 'find item', 'locate'): - if not frappe.has_permission('Warehouse'): + if self.startswith("where is", "find item", "locate"): + if not frappe.has_permission("Warehouse"): raise frappe.PermissionError - item = '%{0}%'.format(self.strip_words(self.query, 'where is', 'find item', 'locate')) - items = frappe.db.sql('''select name from `tabItem` where item_code like %(txt)s - or item_name like %(txt)s or description like %(txt)s''', dict(txt=item)) + item = "%{0}%".format(self.strip_words(self.query, "where is", "find item", "locate")) + items = frappe.db.sql( + """select name from `tabItem` where item_code like %(txt)s + or item_name like %(txt)s or description like %(txt)s""", + dict(txt=item), + ) if items: out = [] @@ -23,14 +26,19 @@ class FindItemBot(BotParser): for item in items: found = False for warehouse in warehouses: - qty = frappe.db.get_value("Bin", {'item_code': item[0], 'warehouse': warehouse.name}, 'actual_qty') + qty = frappe.db.get_value( + "Bin", {"item_code": item[0], "warehouse": warehouse.name}, "actual_qty" + ) if qty: - out.append(_('{0} units of [{1}](/app/Form/Item/{1}) found in [{2}](/app/Form/Warehouse/{2})').format(qty, - item[0], warehouse.name)) + out.append( + _("{0} units of [{1}](/app/Form/Item/{1}) found in [{2}](/app/Form/Warehouse/{2})").format( + qty, item[0], warehouse.name + ) + ) found = True if not found: - out.append(_('[{0}](/app/Form/Item/{0}) is out of stock').format(item[0])) + out.append(_("[{0}](/app/Form/Item/{0}) is out of stock").format(item[0])) return "\n\n".join(out) diff --git a/erpnext/utilities/bulk_transaction.py b/erpnext/utilities/bulk_transaction.py index 64e2ff4218..bfcba0766e 100644 --- a/erpnext/utilities/bulk_transaction.py +++ b/erpnext/utilities/bulk_transaction.py @@ -45,9 +45,13 @@ def job(deserialized_data, from_doctype, to_doctype): frappe.db.rollback(save_point="before_creation_state") failed_history.append(e) failed.append(e) - update_logger(doc_name, e, from_doctype, to_doctype, status="Failed", log_date=str(date.today())) + update_logger( + doc_name, e, from_doctype, to_doctype, status="Failed", log_date=str(date.today()) + ) if not failed: - update_logger(doc_name, None, from_doctype, to_doctype, status="Success", log_date=str(date.today())) + update_logger( + doc_name, None, from_doctype, to_doctype, status="Success", log_date=str(date.today()) + ) show_job_status(failed_history, deserialized_data, to_doctype) @@ -96,7 +100,7 @@ def task(doc_name, from_doctype, to_doctype): }, "Purchase Receipt": {"Purchase Invoice": purchase_receipt.make_purchase_invoice}, } - if to_doctype in ['Advance Payment', 'Payment']: + if to_doctype in ["Advance Payment", "Payment"]: obj = mapper[from_doctype][to_doctype](from_doctype, doc_name) else: obj = mapper[from_doctype][to_doctype](doc_name) @@ -142,9 +146,7 @@ def update_logger(doc_name, e, from_doctype, to_doctype, status, log_date=None, else: log_doc = get_logger_doc(log_date) if record_exists(log_doc, doc_name, status): - append_data_to_logger( - log_doc, doc_name, e, from_doctype, to_doctype, status, restarted - ) + append_data_to_logger(log_doc, doc_name, e, from_doctype, to_doctype, status, restarted) log_doc.save() @@ -158,20 +160,20 @@ def show_job_status(failed_history, deserialized_data, to_doctype): if len(failed_history) != 0 and len(failed_history) < len(deserialized_data): frappe.msgprint( - _("""Creation of {0} partially successful. - Check Bulk Transaction Log""").format( - to_doctype - ), + _( + """Creation of {0} partially successful. + Check Bulk Transaction Log""" + ).format(to_doctype), title="Partially successful", indicator="orange", ) if len(failed_history) == len(deserialized_data): frappe.msgprint( - _("""Creation of {0} failed. - Check Bulk Transaction Log""").format( - to_doctype - ), + _( + """Creation of {0} failed. + Check Bulk Transaction Log""" + ).format(to_doctype), title="Failed", indicator="red", ) @@ -198,4 +200,4 @@ def mark_retrired_transaction(log_doc, doc_name): log_doc.save() - return record \ No newline at end of file + return record diff --git a/erpnext/utilities/doctype/rename_tool/rename_tool.py b/erpnext/utilities/doctype/rename_tool/rename_tool.py index 74de54ac31..b31574cdc8 100644 --- a/erpnext/utilities/doctype/rename_tool/rename_tool.py +++ b/erpnext/utilities/doctype/rename_tool/rename_tool.py @@ -12,14 +12,19 @@ from frappe.model.rename_doc import bulk_rename class RenameTool(Document): pass + @frappe.whitelist() def get_doctypes(): - return frappe.db.sql_list("""select name from tabDocType - where allow_rename=1 and module!='Core' order by name""") + return frappe.db.sql_list( + """select name from tabDocType + where allow_rename=1 and module!='Core' order by name""" + ) + @frappe.whitelist() def upload(select_doctype=None, rows=None): from frappe.utils.csvutils import read_csv_content_from_attached_file + if not select_doctype: select_doctype = frappe.form_dict.select_doctype diff --git a/erpnext/utilities/doctype/sms_log/test_sms_log.py b/erpnext/utilities/doctype/sms_log/test_sms_log.py index 5f7abdc1a8..3ff0202388 100644 --- a/erpnext/utilities/doctype/sms_log/test_sms_log.py +++ b/erpnext/utilities/doctype/sms_log/test_sms_log.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('SMS Log') + class TestSMSLog(unittest.TestCase): pass diff --git a/erpnext/utilities/doctype/video/video.py b/erpnext/utilities/doctype/video/video.py index ae13952bc7..330812dc27 100644 --- a/erpnext/utilities/doctype/video/video.py +++ b/erpnext/utilities/doctype/video/video.py @@ -28,16 +28,17 @@ class Video(Document): try: video = api.get_video_by_id(video_id=self.youtube_video_id) - video_stats = video.items[0].to_dict().get('statistics') + video_stats = video.items[0].to_dict().get("statistics") - self.like_count = video_stats.get('likeCount') - self.view_count = video_stats.get('viewCount') - self.dislike_count = video_stats.get('dislikeCount') - self.comment_count = video_stats.get('commentCount') + self.like_count = video_stats.get("likeCount") + self.view_count = video_stats.get("viewCount") + self.dislike_count = video_stats.get("dislikeCount") + self.comment_count = video_stats.get("commentCount") except Exception: title = "Failed to Update YouTube Statistics for Video: {0}".format(self.name) - frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title) + frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title) + def is_tracking_enabled(): return frappe.db.get_single_value("Video Settings", "enable_youtube_tracking") @@ -54,7 +55,9 @@ def get_frequency(value): def update_youtube_data(): # Called every 30 minutes via hooks - enable_youtube_tracking, frequency = frappe.db.get_value("Video Settings", "Video Settings", ["enable_youtube_tracking", "frequency"]) + enable_youtube_tracking, frequency = frappe.db.get_value( + "Video Settings", "Video Settings", ["enable_youtube_tracking", "frequency"] + ) if not enable_youtube_tracking: return @@ -77,19 +80,21 @@ def get_formatted_ids(video_list): for video in video_list: ids.append(video.youtube_video_id) - return ','.join(ids) + return ",".join(ids) @frappe.whitelist() def get_id_from_url(url): """ - Returns video id from url - :param youtube url: String URL + Returns video id from url + :param youtube url: String URL """ if not isinstance(url, str): frappe.throw(_("URL can only be a string"), title=_("Invalid URL")) - pattern = re.compile(r'[a-z\:\//\.]+(youtube|youtu)\.(com|be)/(watch\?v=|embed/|.+\?v=)?([^"&?\s]{11})?') + pattern = re.compile( + r'[a-z\:\//\.]+(youtube|youtu)\.(com|be)/(watch\?v=|embed/|.+\?v=)?([^"&?\s]{11})?' + ) id = pattern.match(url) return id.groups()[-1] @@ -105,7 +110,7 @@ def batch_update_youtube_data(): return video_stats except Exception: title = "Failed to Update YouTube Statistics" - frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title) + frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title) def prepare_and_set_data(video_list): video_ids = get_formatted_ids(video_list) @@ -114,24 +119,27 @@ def batch_update_youtube_data(): def set_youtube_data(entries): for entry in entries: - video_stats = entry.to_dict().get('statistics') - video_id = entry.to_dict().get('id') + video_stats = entry.to_dict().get("statistics") + video_id = entry.to_dict().get("id") stats = { - 'like_count' : video_stats.get('likeCount'), - 'view_count' : video_stats.get('viewCount'), - 'dislike_count' : video_stats.get('dislikeCount'), - 'comment_count' : video_stats.get('commentCount'), - 'video_id': video_id + "like_count": video_stats.get("likeCount"), + "view_count": video_stats.get("viewCount"), + "dislike_count": video_stats.get("dislikeCount"), + "comment_count": video_stats.get("commentCount"), + "video_id": video_id, } - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabVideo` SET like_count = %(like_count)s, view_count = %(view_count)s, dislike_count = %(dislike_count)s, comment_count = %(comment_count)s - WHERE youtube_video_id = %(video_id)s""", stats) + WHERE youtube_video_id = %(video_id)s""", + stats, + ) video_list = frappe.get_all("Video", fields=["youtube_video_id"]) if len(video_list) > 50: diff --git a/erpnext/utilities/doctype/video_settings/video_settings.py b/erpnext/utilities/doctype/video_settings/video_settings.py index 6f1e2bba16..97fbc41934 100644 --- a/erpnext/utilities/doctype/video_settings/video_settings.py +++ b/erpnext/utilities/doctype/video_settings/video_settings.py @@ -18,5 +18,5 @@ class VideoSettings(Document): build("youtube", "v3", developerKey=self.api_key) except Exception: title = _("Failed to Authenticate the API key.") - frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title) + frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title) frappe.throw(title + " Please check the error logs.", title=_("Invalid Credentials")) diff --git a/erpnext/utilities/hierarchy_chart.py b/erpnext/utilities/hierarchy_chart.py index c18ce107ad..4bf4353cdf 100644 --- a/erpnext/utilities/hierarchy_chart.py +++ b/erpnext/utilities/hierarchy_chart.py @@ -8,11 +8,11 @@ from frappe import _ @frappe.whitelist() def get_all_nodes(method, company): - '''Recursively gets all data from nodes''' + """Recursively gets all data from nodes""" method = frappe.get_attr(method) if method not in frappe.whitelisted: - frappe.throw(_('Not Permitted'), frappe.PermissionError) + frappe.throw(_("Not Permitted"), frappe.PermissionError) root_nodes = method(company=company) result = [] @@ -21,14 +21,16 @@ def get_all_nodes(method, company): for root in root_nodes: data = method(root.id, company) result.append(dict(parent=root.id, parent_name=root.name, data=data)) - nodes_to_expand.extend([{'id': d.get('id'), 'name': d.get('name')} for d in data if d.get('expandable')]) + nodes_to_expand.extend( + [{"id": d.get("id"), "name": d.get("name")} for d in data if d.get("expandable")] + ) while nodes_to_expand: parent = nodes_to_expand.pop(0) - data = method(parent.get('id'), company) - result.append(dict(parent=parent.get('id'), parent_name=parent.get('name'), data=data)) + data = method(parent.get("id"), company) + result.append(dict(parent=parent.get("id"), parent_name=parent.get("name"), data=data)) for d in data: - if d.get('expandable'): - nodes_to_expand.append({'id': d.get('id'), 'name': d.get('name')}) + if d.get("expandable"): + nodes_to_expand.append({"id": d.get("id"), "name": d.get("name")}) return result diff --git a/erpnext/utilities/product.py b/erpnext/utilities/product.py index 0a45002422..04ee0b3b1e 100644 --- a/erpnext/utilities/product.py +++ b/erpnext/utilities/product.py @@ -9,32 +9,41 @@ from erpnext.stock.doctype.batch.batch import get_batch_qty def get_web_item_qty_in_stock(item_code, item_warehouse_field, warehouse=None): - in_stock, stock_qty = 0, '' - template_item_code, is_stock_item = frappe.db.get_value("Item", item_code, ["variant_of", "is_stock_item"]) + in_stock, stock_qty = 0, "" + template_item_code, is_stock_item = frappe.db.get_value( + "Item", item_code, ["variant_of", "is_stock_item"] + ) if not warehouse: warehouse = frappe.db.get_value("Website Item", {"item_code": item_code}, item_warehouse_field) if not warehouse and template_item_code and template_item_code != item_code: - warehouse = frappe.db.get_value("Website Item", {"item_code": template_item_code}, item_warehouse_field) + warehouse = frappe.db.get_value( + "Website Item", {"item_code": template_item_code}, item_warehouse_field + ) if warehouse: - stock_qty = frappe.db.sql(""" + stock_qty = frappe.db.sql( + """ select GREATEST(S.actual_qty - S.reserved_qty - S.reserved_qty_for_production - S.reserved_qty_for_sub_contract, 0) / IFNULL(C.conversion_factor, 1) from tabBin S inner join `tabItem` I on S.item_code = I.Item_code left join `tabUOM Conversion Detail` C on I.sales_uom = C.uom and C.parent = I.Item_code - where S.item_code=%s and S.warehouse=%s""", (item_code, warehouse)) + where S.item_code=%s and S.warehouse=%s""", + (item_code, warehouse), + ) if stock_qty: stock_qty = adjust_qty_for_expired_items(item_code, stock_qty, warehouse) in_stock = stock_qty[0][0] > 0 and 1 or 0 - return frappe._dict({"in_stock": in_stock, "stock_qty": stock_qty, "is_stock_item": is_stock_item}) + return frappe._dict( + {"in_stock": in_stock, "stock_qty": stock_qty, "is_stock_item": is_stock_item} + ) def adjust_qty_for_expired_items(item_code, stock_qty, warehouse): - batches = frappe.get_all('Batch', filters=[{'item': item_code}], fields=['expiry_date', 'name']) + batches = frappe.get_all("Batch", filters=[{"item": item_code}], fields=["expiry_date", "name"]) expired_batches = get_expired_batches(batches) stock_qty = [list(item) for item in stock_qty] @@ -67,33 +76,42 @@ def qty_from_all_warehouses(batch_info): return qty + def get_price(item_code, price_list, customer_group, company, qty=1): from erpnext.e_commerce.shopping_cart.cart import get_party template_item_code = frappe.db.get_value("Item", item_code, "variant_of") if price_list: - price = frappe.get_all("Item Price", fields=["price_list_rate", "currency"], - filters={"price_list": price_list, "item_code": item_code}) + price = frappe.get_all( + "Item Price", + fields=["price_list_rate", "currency"], + filters={"price_list": price_list, "item_code": item_code}, + ) if template_item_code and not price: - price = frappe.get_all("Item Price", fields=["price_list_rate", "currency"], - filters={"price_list": price_list, "item_code": template_item_code}) + price = frappe.get_all( + "Item Price", + fields=["price_list_rate", "currency"], + filters={"price_list": price_list, "item_code": template_item_code}, + ) if price: party = get_party() - pricing_rule_dict = frappe._dict({ - "item_code": item_code, - "qty": qty, - "stock_qty": qty, - "transaction_type": "selling", - "price_list": price_list, - "customer_group": customer_group, - "company": company, - "conversion_rate": 1, - "for_shopping_cart": True, - "currency": frappe.db.get_value("Price List", price_list, "currency") - }) + pricing_rule_dict = frappe._dict( + { + "item_code": item_code, + "qty": qty, + "stock_qty": qty, + "transaction_type": "selling", + "price_list": price_list, + "customer_group": customer_group, + "company": company, + "conversion_rate": 1, + "for_shopping_cart": True, + "currency": frappe.db.get_value("Price List", price_list, "currency"), + } + ) if party and party.doctype == "Customer": pricing_rule_dict.update({"customer": party.name}) @@ -108,7 +126,9 @@ def get_price(item_code, price_list, customer_group, company, qty=1): if pricing_rule.pricing_rule_for == "Discount Percentage": price_obj.discount_percent = pricing_rule.discount_percentage price_obj.formatted_discount_percent = str(flt(pricing_rule.discount_percentage, 0)) + "%" - price_obj.price_list_rate = flt(price_obj.price_list_rate * (1.0 - (flt(pricing_rule.discount_percentage) / 100.0))) + price_obj.price_list_rate = flt( + price_obj.price_list_rate * (1.0 - (flt(pricing_rule.discount_percentage) / 100.0)) + ) if pricing_rule.pricing_rule_for == "Rate": rate_discount = flt(mrp) - flt(pricing_rule.price_list_rate) @@ -117,21 +137,33 @@ def get_price(item_code, price_list, customer_group, company, qty=1): price_obj.price_list_rate = pricing_rule.price_list_rate or 0 if price_obj: - price_obj["formatted_price"] = fmt_money(price_obj["price_list_rate"], currency=price_obj["currency"]) + price_obj["formatted_price"] = fmt_money( + price_obj["price_list_rate"], currency=price_obj["currency"] + ) if mrp != price_obj["price_list_rate"]: price_obj["formatted_mrp"] = fmt_money(mrp, currency=price_obj["currency"]) - price_obj["currency_symbol"] = not cint(frappe.db.get_default("hide_currency_symbol")) \ - and (frappe.db.get_value("Currency", price_obj.currency, "symbol", cache=True) or price_obj.currency) \ + price_obj["currency_symbol"] = ( + not cint(frappe.db.get_default("hide_currency_symbol")) + and ( + frappe.db.get_value("Currency", price_obj.currency, "symbol", cache=True) + or price_obj.currency + ) or "" + ) - uom_conversion_factor = frappe.db.sql("""select C.conversion_factor + uom_conversion_factor = frappe.db.sql( + """select C.conversion_factor from `tabUOM Conversion Detail` C inner join `tabItem` I on C.parent = I.name and C.uom = I.sales_uom - where I.name = %s""", item_code) + where I.name = %s""", + item_code, + ) uom_conversion_factor = uom_conversion_factor[0][0] if uom_conversion_factor else 1 - price_obj["formatted_price_sales_uom"] = fmt_money(price_obj["price_list_rate"] * uom_conversion_factor, currency=price_obj["currency"]) + price_obj["formatted_price_sales_uom"] = fmt_money( + price_obj["price_list_rate"] * uom_conversion_factor, currency=price_obj["currency"] + ) if not price_obj["price_list_rate"]: price_obj["price_list_rate"] = 0 @@ -144,11 +176,17 @@ def get_price(item_code, price_list, customer_group, company, qty=1): return price_obj + def get_non_stock_item_status(item_code, item_warehouse_field): # if item is a product bundle, check if its bundle items are in stock if frappe.db.exists("Product Bundle", item_code): items = frappe.get_doc("Product Bundle", item_code).get_all_children() - bundle_warehouse = frappe.db.get_value("Website Item", {"item_code": item_code}, item_warehouse_field) - return all(get_web_item_qty_in_stock(d.item_code, item_warehouse_field, bundle_warehouse).in_stock for d in items) + bundle_warehouse = frappe.db.get_value( + "Website Item", {"item_code": item_code}, item_warehouse_field + ) + return all( + get_web_item_qty_in_stock(d.item_code, item_warehouse_field, bundle_warehouse).in_stock + for d in items + ) else: return 1 diff --git a/erpnext/utilities/report/youtube_interactions/youtube_interactions.py b/erpnext/utilities/report/youtube_interactions/youtube_interactions.py index a185a7012c..a65a75f362 100644 --- a/erpnext/utilities/report/youtube_interactions/youtube_interactions.py +++ b/erpnext/utilities/report/youtube_interactions/youtube_interactions.py @@ -16,91 +16,51 @@ def execute(filters=None): chart_data, summary = get_chart_summary_data(data) return columns, data, None, chart_data, summary + def get_columns(): return [ - { - "label": _("Published Date"), - "fieldname": "publish_date", - "fieldtype": "Date", - "width": 100 - }, - { - "label": _("Title"), - "fieldname": "title", - "fieldtype": "Data", - "width": 200 - }, - { - "label": _("Duration"), - "fieldname": "duration", - "fieldtype": "Duration", - "width": 100 - }, - { - "label": _("Views"), - "fieldname": "view_count", - "fieldtype": "Float", - "width": 200 - }, - { - "label": _("Likes"), - "fieldname": "like_count", - "fieldtype": "Float", - "width": 200 - }, - { - "label": _("Dislikes"), - "fieldname": "dislike_count", - "fieldtype": "Float", - "width": 100 - }, - { - "label": _("Comments"), - "fieldname": "comment_count", - "fieldtype": "Float", - "width": 100 - } + {"label": _("Published Date"), "fieldname": "publish_date", "fieldtype": "Date", "width": 100}, + {"label": _("Title"), "fieldname": "title", "fieldtype": "Data", "width": 200}, + {"label": _("Duration"), "fieldname": "duration", "fieldtype": "Duration", "width": 100}, + {"label": _("Views"), "fieldname": "view_count", "fieldtype": "Float", "width": 200}, + {"label": _("Likes"), "fieldname": "like_count", "fieldtype": "Float", "width": 200}, + {"label": _("Dislikes"), "fieldname": "dislike_count", "fieldtype": "Float", "width": 100}, + {"label": _("Comments"), "fieldname": "comment_count", "fieldtype": "Float", "width": 100}, ] + def get_data(filters): - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT publish_date, title, provider, duration, view_count, like_count, dislike_count, comment_count FROM `tabVideo` WHERE view_count is not null and publish_date between %(from_date)s and %(to_date)s - ORDER BY view_count desc""", filters, as_dict=1) + ORDER BY view_count desc""", + filters, + as_dict=1, + ) + def get_chart_summary_data(data): labels, likes, views = [], [], [] total_views = 0 for row in data: - labels.append(row.get('title')) - likes.append(row.get('like_count')) - views.append(row.get('view_count')) - total_views += flt(row.get('view_count')) - + labels.append(row.get("title")) + likes.append(row.get("like_count")) + views.append(row.get("view_count")) + total_views += flt(row.get("view_count")) chart_data = { - "data" : { - "labels" : labels, - "datasets" : [ - { - "name" : "Likes", - "values" : likes - }, - { - "name" : "Views", - "values" : views - } - ] + "data": { + "labels": labels, + "datasets": [{"name": "Likes", "values": likes}, {"name": "Views", "values": views}], }, "type": "bar", - "barOptions": { - "stacked": 1 - }, + "barOptions": {"stacked": 1}, } summary = [ diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index feea2284b7..73cbcd4094 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -10,7 +10,9 @@ from frappe.utils import cint, cstr, flt, get_time, now_datetime from erpnext.controllers.status_updater import StatusUpdater -class UOMMustBeIntegerError(frappe.ValidationError): pass +class UOMMustBeIntegerError(frappe.ValidationError): + pass + class TransactionBase(StatusUpdater): def validate_posting_time(self): @@ -18,69 +20,79 @@ class TransactionBase(StatusUpdater): if frappe.flags.in_import and self.posting_date: self.set_posting_time = 1 - if not getattr(self, 'set_posting_time', None): + if not getattr(self, "set_posting_time", None): now = now_datetime() - self.posting_date = now.strftime('%Y-%m-%d') - self.posting_time = now.strftime('%H:%M:%S.%f') + self.posting_date = now.strftime("%Y-%m-%d") + self.posting_time = now.strftime("%H:%M:%S.%f") elif self.posting_time: try: get_time(self.posting_time) except ValueError: - frappe.throw(_('Invalid Posting Time')) + frappe.throw(_("Invalid Posting Time")) def add_calendar_event(self, opts, force=False): - if cstr(self.contact_by) != cstr(self._prev.contact_by) or \ - cstr(self.contact_date) != cstr(self._prev.contact_date) or force or \ - (hasattr(self, "ends_on") and cstr(self.ends_on) != cstr(self._prev.ends_on)): + if ( + cstr(self.contact_by) != cstr(self._prev.contact_by) + or cstr(self.contact_date) != cstr(self._prev.contact_date) + or force + or (hasattr(self, "ends_on") and cstr(self.ends_on) != cstr(self._prev.ends_on)) + ): self.delete_events() self._add_calendar_event(opts) def delete_events(self): - participations = frappe.get_all("Event Participants", filters={"reference_doctype": self.doctype, "reference_docname": self.name, - "parenttype": "Event"}, fields=["name", "parent"]) + participations = frappe.get_all( + "Event Participants", + filters={ + "reference_doctype": self.doctype, + "reference_docname": self.name, + "parenttype": "Event", + }, + fields=["name", "parent"], + ) if participations: for participation in participations: - total_participants = frappe.get_all("Event Participants", filters={"parenttype": "Event", "parent": participation.parent}) + total_participants = frappe.get_all( + "Event Participants", filters={"parenttype": "Event", "parent": participation.parent} + ) if len(total_participants) <= 1: frappe.db.sql("delete from `tabEvent` where name='%s'" % participation.parent) frappe.db.sql("delete from `tabEvent Participants` where name='%s'" % participation.name) - def _add_calendar_event(self, opts): opts = frappe._dict(opts) if self.contact_date: - event = frappe.get_doc({ - "doctype": "Event", - "owner": opts.owner or self.owner, - "subject": opts.subject, - "description": opts.description, - "starts_on": self.contact_date, - "ends_on": opts.ends_on, - "event_type": "Private" - }) - - event.append('event_participants', { - "reference_doctype": self.doctype, - "reference_docname": self.name + event = frappe.get_doc( + { + "doctype": "Event", + "owner": opts.owner or self.owner, + "subject": opts.subject, + "description": opts.description, + "starts_on": self.contact_date, + "ends_on": opts.ends_on, + "event_type": "Private", } ) + event.append( + "event_participants", {"reference_doctype": self.doctype, "reference_docname": self.name} + ) + event.insert(ignore_permissions=True) if frappe.db.exists("User", self.contact_by): - frappe.share.add("Event", event.name, self.contact_by, - flags={"ignore_share_permission": True}) + frappe.share.add("Event", event.name, self.contact_by, flags={"ignore_share_permission": True}) def validate_uom_is_integer(self, uom_field, qty_fields): validate_uom_is_integer(self, uom_field, qty_fields) def validate_with_previous_doc(self, ref): - self.exclude_fields = ["conversion_factor", "uom"] if self.get('is_return') else [] + self.exclude_fields = ["conversion_factor", "uom"] if self.get("is_return") else [] for key, val in ref.items(): is_child = val.get("is_child_table") @@ -105,8 +117,9 @@ class TransactionBase(StatusUpdater): def compare_values(self, ref_doc, fields, doc=None): for reference_doctype, ref_dn_list in ref_doc.items(): for reference_name in ref_dn_list: - prevdoc_values = frappe.db.get_value(reference_doctype, reference_name, - [d[0] for d in fields], as_dict=1) + prevdoc_values = frappe.db.get_value( + reference_doctype, reference_name, [d[0] for d in fields], as_dict=1 + ) if not prevdoc_values: frappe.throw(_("Invalid reference {0} {1}").format(reference_doctype, reference_name)) @@ -115,7 +128,6 @@ class TransactionBase(StatusUpdater): if prevdoc_values[field] is not None and field not in self.exclude_fields: self.validate_value(field, condition, prevdoc_values[field], doc) - def validate_rate_with_reference_doc(self, ref_details): buying_doctypes = ["Purchase Order", "Purchase Invoice", "Purchase Receipt"] @@ -131,17 +143,26 @@ class TransactionBase(StatusUpdater): if d.get(ref_link_field): ref_rate = frappe.db.get_value(ref_dt + " Item", d.get(ref_link_field), "rate") - if abs(flt(d.rate - ref_rate, d.precision("rate"))) >= .01: + if abs(flt(d.rate - ref_rate, d.precision("rate"))) >= 0.01: if action == "Stop": - role_allowed_to_override = frappe.db.get_single_value(settings_doc, 'role_to_override_stop_action') + role_allowed_to_override = frappe.db.get_single_value( + settings_doc, "role_to_override_stop_action" + ) if role_allowed_to_override not in frappe.get_roles(): - frappe.throw(_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format( - d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate)) + frappe.throw( + _("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format( + d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate + ) + ) else: - frappe.msgprint(_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format( - d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate), title=_("Warning"), indicator="orange") - + frappe.msgprint( + _("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format( + d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate + ), + title=_("Warning"), + indicator="orange", + ) def get_link_filters(self, for_doctype): if hasattr(self, "prev_link_mapper") and self.prev_link_mapper.get(for_doctype): @@ -150,11 +171,7 @@ class TransactionBase(StatusUpdater): values = filter(None, tuple(item.as_dict()[fieldname] for item in self.items)) if values: - ret = { - for_doctype : { - "filters": [[for_doctype, "name", "in", values]] - } - } + ret = {for_doctype: {"filters": [[for_doctype, "name", "in", values]]}} else: ret = None else: @@ -163,17 +180,17 @@ class TransactionBase(StatusUpdater): return ret def reset_default_field_value(self, default_field: str, child_table: str, child_table_field: str): - """ Reset "Set default X" fields on forms to avoid confusion. + """Reset "Set default X" fields on forms to avoid confusion. - example: - doc = { - "set_from_warehouse": "Warehouse A", - "items": [{"from_warehouse": "warehouse B"}, {"from_warehouse": "warehouse A"}], - } - Since this has dissimilar values in child table, the default field will be erased. + example: + doc = { + "set_from_warehouse": "Warehouse A", + "items": [{"from_warehouse": "warehouse B"}, {"from_warehouse": "warehouse A"}], + } + Since this has dissimilar values in child table, the default field will be erased. - doc.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") - """ + doc.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") + """ child_table_values = set() for row in self.get(child_table): @@ -182,8 +199,11 @@ class TransactionBase(StatusUpdater): if len(child_table_values) > 1: self.set(default_field, None) + def delete_events(ref_type, ref_name): - events = frappe.db.sql_list(""" SELECT + events = ( + frappe.db.sql_list( + """ SELECT distinct `tabEvent`.name from `tabEvent`, `tabEvent Participants` @@ -191,18 +211,27 @@ def delete_events(ref_type, ref_name): `tabEvent`.name = `tabEvent Participants`.parent and `tabEvent Participants`.reference_doctype = %s and `tabEvent Participants`.reference_docname = %s - """, (ref_type, ref_name)) or [] + """, + (ref_type, ref_name), + ) + or [] + ) if events: frappe.delete_doc("Event", events, for_reload=True) + def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None): if isinstance(qty_fields, str): qty_fields = [qty_fields] distinct_uoms = list(set(d.get(uom_field) for d in doc.get_all_children())) - integer_uoms = list(filter(lambda uom: frappe.db.get_value("UOM", uom, - "must_be_whole_number", cache=True) or None, distinct_uoms)) + integer_uoms = list( + filter( + lambda uom: frappe.db.get_value("UOM", uom, "must_be_whole_number", cache=True) or None, + distinct_uoms, + ) + ) if not integer_uoms: return @@ -213,6 +242,11 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None): qty = d.get(f) if qty: if abs(cint(qty) - flt(qty)) > 0.0000001: - frappe.throw(_("Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}.") \ - .format(qty, d.idx, frappe.bold(_("Must be Whole Number")), frappe.bold(d.get(uom_field))), - UOMMustBeIntegerError) + frappe.throw( + _( + "Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}." + ).format( + qty, d.idx, frappe.bold(_("Must be Whole Number")), frappe.bold(d.get(uom_field)) + ), + UOMMustBeIntegerError, + ) diff --git a/erpnext/www/all-products/index.py b/erpnext/www/all-products/index.py index ffaead64f6..fbf0dce059 100644 --- a/erpnext/www/all-products/index.py +++ b/erpnext/www/all-products/index.py @@ -5,15 +5,18 @@ from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder sitemap = 1 + def get_context(context): # Add homepage as parent context.body_class = "product-page" - context.parents = [{"name": frappe._("Home"), "route":"/"}] + context.parents = [{"name": frappe._("Home"), "route": "/"}] filter_engine = ProductFiltersBuilder() context.field_filters = filter_engine.get_field_filters() context.attribute_filters = filter_engine.get_attribute_filters() - context.page_length = cint(frappe.db.get_single_value('E Commerce Settings', 'products_per_page'))or 20 + context.page_length = ( + cint(frappe.db.get_single_value("E Commerce Settings", "products_per_page")) or 20 + ) - context.no_cache = 1 \ No newline at end of file + context.no_cache = 1 diff --git a/erpnext/www/book_appointment/index.py b/erpnext/www/book_appointment/index.py index 8cda3c1b90..06e99da3f9 100644 --- a/erpnext/www/book_appointment/index.py +++ b/erpnext/www/book_appointment/index.py @@ -11,38 +11,46 @@ no_cache = 1 def get_context(context): - is_enabled = frappe.db.get_single_value('Appointment Booking Settings', 'enable_scheduling') + is_enabled = frappe.db.get_single_value("Appointment Booking Settings", "enable_scheduling") if is_enabled: return context else: - frappe.redirect_to_message(_("Appointment Scheduling Disabled"), _("Appointment Scheduling has been disabled for this site"), - http_status_code=302, indicator_color="red") + frappe.redirect_to_message( + _("Appointment Scheduling Disabled"), + _("Appointment Scheduling has been disabled for this site"), + http_status_code=302, + indicator_color="red", + ) raise frappe.Redirect + @frappe.whitelist(allow_guest=True) def get_appointment_settings(): - settings = frappe.get_doc('Appointment Booking Settings') - settings.holiday_list = frappe.get_doc('Holiday List', settings.holiday_list) + settings = frappe.get_doc("Appointment Booking Settings") + settings.holiday_list = frappe.get_doc("Holiday List", settings.holiday_list) return settings + @frappe.whitelist(allow_guest=True) def get_timezones(): import pytz + return pytz.all_timezones + @frappe.whitelist(allow_guest=True) def get_appointment_slots(date, timezone): # Convert query to local timezones - format_string = '%Y-%m-%d %H:%M:%S' - query_start_time = datetime.datetime.strptime(date + ' 00:00:00', format_string) - query_end_time = datetime.datetime.strptime(date + ' 23:59:59', format_string) + format_string = "%Y-%m-%d %H:%M:%S" + query_start_time = datetime.datetime.strptime(date + " 00:00:00", format_string) + query_end_time = datetime.datetime.strptime(date + " 23:59:59", format_string) query_start_time = convert_to_system_timezone(timezone, query_start_time) query_end_time = convert_to_system_timezone(timezone, query_end_time) now = convert_to_guest_timezone(timezone, datetime.datetime.now()) # Database queries - settings = frappe.get_doc('Appointment Booking Settings') - holiday_list = frappe.get_doc('Holiday List', settings.holiday_list) + settings = frappe.get_doc("Appointment Booking Settings") + holiday_list = frappe.get_doc("Holiday List", settings.holiday_list) timeslots = get_available_slots_between(query_start_time, query_end_time, settings) # Filter and convert timeslots @@ -58,15 +66,15 @@ def get_appointment_slots(date, timezone): converted_timeslots.append(dict(time=converted_timeslot, availability=True)) else: converted_timeslots.append(dict(time=converted_timeslot, availability=False)) - date_required = datetime.datetime.strptime(date + ' 00:00:00', format_string).date() + date_required = datetime.datetime.strptime(date + " 00:00:00", format_string).date() converted_timeslots = filter_timeslots(date_required, converted_timeslots) return converted_timeslots + def get_available_slots_between(query_start_time, query_end_time, settings): records = _get_records(query_start_time, query_end_time, settings) timeslots = [] - appointment_duration = datetime.timedelta( - minutes=settings.appointment_duration) + appointment_duration = datetime.timedelta(minutes=settings.appointment_duration) for record in records: if record.day_of_week == WEEKDAYS[query_start_time.weekday()]: current_time = _deltatime_to_datetime(query_start_time, record.from_time) @@ -82,33 +90,35 @@ def get_available_slots_between(query_start_time, query_end_time, settings): @frappe.whitelist(allow_guest=True) def create_appointment(date, time, tz, contact): - format_string = '%Y-%m-%d %H:%M:%S' + format_string = "%Y-%m-%d %H:%M:%S" scheduled_time = datetime.datetime.strptime(date + " " + time, format_string) # Strip tzinfo from datetime objects since it's handled by the doctype - scheduled_time = scheduled_time.replace(tzinfo = None) + scheduled_time = scheduled_time.replace(tzinfo=None) scheduled_time = convert_to_system_timezone(tz, scheduled_time) - scheduled_time = scheduled_time.replace(tzinfo = None) + scheduled_time = scheduled_time.replace(tzinfo=None) # Create a appointment document from form - appointment = frappe.new_doc('Appointment') + appointment = frappe.new_doc("Appointment") appointment.scheduled_time = scheduled_time contact = json.loads(contact) - appointment.customer_name = contact.get('name', None) - appointment.customer_phone_number = contact.get('number', None) - appointment.customer_skype = contact.get('skype', None) - appointment.customer_details = contact.get('notes', None) - appointment.customer_email = contact.get('email', None) - appointment.status = 'Open' + appointment.customer_name = contact.get("name", None) + appointment.customer_phone_number = contact.get("number", None) + appointment.customer_skype = contact.get("skype", None) + appointment.customer_details = contact.get("notes", None) + appointment.customer_email = contact.get("email", None) + appointment.status = "Open" appointment.insert() return appointment + # Helper Functions def filter_timeslots(date, timeslots): filtered_timeslots = [] for timeslot in timeslots: - if(timeslot['time'].date() == date): + if timeslot["time"].date() == date: filtered_timeslots.append(timeslot) return filtered_timeslots + def convert_to_guest_timezone(guest_tz, datetimeobject): guest_tz = pytz.timezone(guest_tz) local_timezone = pytz.timezone(frappe.utils.get_time_zone()) @@ -116,15 +126,18 @@ def convert_to_guest_timezone(guest_tz, datetimeobject): datetimeobject = datetimeobject.astimezone(guest_tz) return datetimeobject -def convert_to_system_timezone(guest_tz,datetimeobject): + +def convert_to_system_timezone(guest_tz, datetimeobject): guest_tz = pytz.timezone(guest_tz) datetimeobject = guest_tz.localize(datetimeobject) system_tz = pytz.timezone(frappe.utils.get_time_zone()) datetimeobject = datetimeobject.astimezone(system_tz) return datetimeobject + def check_availabilty(timeslot, settings): - return frappe.db.count('Appointment', {'scheduled_time': timeslot}) < settings.number_of_agents + return frappe.db.count("Appointment", {"scheduled_time": timeslot}) < settings.number_of_agents + def _is_holiday(date, holiday_list): for holiday in holiday_list.holidays: @@ -136,7 +149,10 @@ def _is_holiday(date, holiday_list): def _get_records(start_time, end_time, settings): records = [] for record in settings.availability_of_slots: - if record.day_of_week == WEEKDAYS[start_time.weekday()] or record.day_of_week == WEEKDAYS[end_time.weekday()]: + if ( + record.day_of_week == WEEKDAYS[start_time.weekday()] + or record.day_of_week == WEEKDAYS[end_time.weekday()] + ): records.append(record) return records @@ -148,4 +164,4 @@ def _deltatime_to_datetime(date, deltatime): def _datetime_to_deltatime(date_time): midnight = datetime.datetime.combine(date_time.date(), datetime.time.min) - return (date_time-midnight) + return date_time - midnight diff --git a/erpnext/www/book_appointment/verify/index.py b/erpnext/www/book_appointment/verify/index.py index dc36f4fc97..1a5ba9de7e 100644 --- a/erpnext/www/book_appointment/verify/index.py +++ b/erpnext/www/book_appointment/verify/index.py @@ -8,11 +8,11 @@ def get_context(context): context.success = False return context - email = frappe.form_dict['email'] - appointment_name = frappe.form_dict['appointment'] + email = frappe.form_dict["email"] + appointment_name = frappe.form_dict["appointment"] if email and appointment_name: - appointment = frappe.get_doc('Appointment',appointment_name) + appointment = frappe.get_doc("Appointment", appointment_name) appointment.set_verified(email) context.success = True return context diff --git a/erpnext/www/lms/content.py b/erpnext/www/lms/content.py index b187a78865..99462ceeee 100644 --- a/erpnext/www/lms/content.py +++ b/erpnext/www/lms/content.py @@ -4,28 +4,27 @@ import erpnext.education.utils as utils no_cache = 1 + def get_context(context): # Load Query Parameters try: - program = frappe.form_dict['program'] - content = frappe.form_dict['content'] - content_type = frappe.form_dict['type'] - course = frappe.form_dict['course'] - topic = frappe.form_dict['topic'] + program = frappe.form_dict["program"] + content = frappe.form_dict["content"] + content_type = frappe.form_dict["type"] + course = frappe.form_dict["course"] + topic = frappe.form_dict["topic"] except KeyError: - frappe.local.flags.redirect_location = '/lms' + frappe.local.flags.redirect_location = "/lms" raise frappe.Redirect - # Check if user has access to the content has_program_access = utils.allowed_program_access(program) has_content_access = allowed_content_access(program, content, content_type) if frappe.session.user == "Guest" or not has_program_access or not has_content_access: - frappe.local.flags.redirect_location = '/lms' + frappe.local.flags.redirect_location = "/lms" raise frappe.Redirect - # Set context for content to be displayer context.content = frappe.get_doc(content_type, content).as_dict() context.content_type = content_type @@ -34,35 +33,43 @@ def get_context(context): context.topic = topic topic = frappe.get_doc("Topic", topic) - content_list = [{'content_type':item.content_type, 'content':item.content} for item in topic.topic_content] + content_list = [ + {"content_type": item.content_type, "content": item.content} for item in topic.topic_content + ] # Set context for progress numbers - context.position = content_list.index({'content': content, 'content_type': content_type}) + context.position = content_list.index({"content": content, "content_type": content_type}) context.length = len(content_list) # Set context for navigation context.previous = get_previous_content(content_list, context.position) context.next = get_next_content(content_list, context.position) + def get_next_content(content_list, current_index): try: return content_list[current_index + 1] except IndexError: return None + def get_previous_content(content_list, current_index): if current_index == 0: return None else: return content_list[current_index - 1] + def allowed_content_access(program, content, content_type): - contents_of_program = frappe.db.sql("""select `tabTopic Content`.content, `tabTopic Content`.content_type + contents_of_program = frappe.db.sql( + """select `tabTopic Content`.content, `tabTopic Content`.content_type from `tabCourse Topic`, `tabProgram Course`, `tabTopic Content` where `tabCourse Topic`.parent = `tabProgram Course`.course and `tabTopic Content`.parent = `tabCourse Topic`.topic - and `tabProgram Course`.parent = %(program)s""", {'program': program}) + and `tabProgram Course`.parent = %(program)s""", + {"program": program}, + ) return (content, content_type) in contents_of_program diff --git a/erpnext/www/lms/course.py b/erpnext/www/lms/course.py index 012e25ce52..840beee3ad 100644 --- a/erpnext/www/lms/course.py +++ b/erpnext/www/lms/course.py @@ -4,23 +4,25 @@ import erpnext.education.utils as utils no_cache = 1 + def get_context(context): try: - program = frappe.form_dict['program'] - course_name = frappe.form_dict['name'] + program = frappe.form_dict["program"] + course_name = frappe.form_dict["name"] except KeyError: - frappe.local.flags.redirect_location = '/lms' + frappe.local.flags.redirect_location = "/lms" raise frappe.Redirect context.education_settings = frappe.get_single("Education Settings") - course = frappe.get_doc('Course', course_name) + course = frappe.get_doc("Course", course_name) context.program = program context.course = course context.topics = course.get_topics() - context.has_access = utils.allowed_program_access(context.program) + context.has_access = utils.allowed_program_access(context.program) context.progress = get_topic_progress(context.topics, course, context.program) + def get_topic_progress(topics, course, program): progress = {topic.name: utils.get_topic_progress(topic, course.name, program) for topic in topics} return progress diff --git a/erpnext/www/lms/index.py b/erpnext/www/lms/index.py index 035f7d9f72..782ac481a0 100644 --- a/erpnext/www/lms/index.py +++ b/erpnext/www/lms/index.py @@ -4,10 +4,11 @@ import erpnext.education.utils as utils no_cache = 1 + def get_context(context): context.education_settings = frappe.get_single("Education Settings") if not context.education_settings.enable_lms: - frappe.local.flags.redirect_location = '/' + frappe.local.flags.redirect_location = "/" raise frappe.Redirect context.featured_programs = get_featured_programs() diff --git a/erpnext/www/lms/profile.py b/erpnext/www/lms/profile.py index 8cd2f24fdc..c4c1cd78eb 100644 --- a/erpnext/www/lms/profile.py +++ b/erpnext/www/lms/profile.py @@ -4,23 +4,34 @@ import erpnext.education.utils as utils no_cache = 1 + def get_context(context): if frappe.session.user == "Guest": - frappe.local.flags.redirect_location = '/lms' + frappe.local.flags.redirect_location = "/lms" raise frappe.Redirect context.student = utils.get_current_student() if not context.student: - context.student = frappe.get_doc('User', frappe.session.user) + context.student = frappe.get_doc("User", frappe.session.user) context.progress = get_program_progress(context.student.name) + def get_program_progress(student): - enrolled_programs = frappe.get_all("Program Enrollment", filters={'student':student}, fields=['program']) + enrolled_programs = frappe.get_all( + "Program Enrollment", filters={"student": student}, fields=["program"] + ) student_progress = [] for list_item in enrolled_programs: program = frappe.get_doc("Program", list_item.program) progress = utils.get_program_progress(program) completion = utils.get_program_completion(program) - student_progress.append({'program': program.program_name, 'name': program.name, 'progress':progress, 'completion': completion}) + student_progress.append( + { + "program": program.program_name, + "name": program.name, + "progress": progress, + "completion": completion, + } + ) return student_progress diff --git a/erpnext/www/lms/program.py b/erpnext/www/lms/program.py index db2653a6d5..1df2aa5bac 100644 --- a/erpnext/www/lms/program.py +++ b/erpnext/www/lms/program.py @@ -5,11 +5,12 @@ import erpnext.education.utils as utils no_cache = 1 + def get_context(context): try: - program = frappe.form_dict['program'] + program = frappe.form_dict["program"] except KeyError: - frappe.local.flags.redirect_location = '/lms' + frappe.local.flags.redirect_location = "/lms" raise frappe.Redirect context.education_settings = frappe.get_single("Education Settings") @@ -18,12 +19,14 @@ def get_context(context): context.has_access = utils.allowed_program_access(program) context.progress = get_course_progress(context.courses, context.program) + def get_program(program_name): try: - return frappe.get_doc('Program', program_name) + return frappe.get_doc("Program", program_name) except frappe.DoesNotExistError: frappe.throw(_("Program {0} does not exist.").format(program_name)) + def get_course_progress(courses, program): progress = {course.name: utils.get_course_progress(course, program) for course in courses} return progress or {} diff --git a/erpnext/www/lms/topic.py b/erpnext/www/lms/topic.py index 17fc8f7992..7783211a41 100644 --- a/erpnext/www/lms/topic.py +++ b/erpnext/www/lms/topic.py @@ -4,20 +4,22 @@ import erpnext.education.utils as utils no_cache = 1 + def get_context(context): try: - course = frappe.form_dict['course'] - program = frappe.form_dict['program'] - topic = frappe.form_dict['topic'] + course = frappe.form_dict["course"] + program = frappe.form_dict["program"] + topic = frappe.form_dict["topic"] except KeyError: - frappe.local.flags.redirect_location = '/lms' + frappe.local.flags.redirect_location = "/lms" raise frappe.Redirect context.program = program context.course = course context.topic = frappe.get_doc("Topic", topic) context.contents = get_contents(context.topic, course, program) - context.has_access = utils.allowed_program_access(program) + context.has_access = utils.allowed_program_access(program) + def get_contents(topic, course, program): student = utils.get_current_student() @@ -27,19 +29,29 @@ def get_contents(topic, course, program): progress = [] if contents: for content in contents: - if content.doctype in ('Article', 'Video'): + if content.doctype in ("Article", "Video"): if student: status = utils.check_content_completion(content.name, content.doctype, course_enrollment.name) else: status = True - progress.append({'content': content, 'content_type': content.doctype, 'completed': status}) - elif content.doctype == 'Quiz': + progress.append({"content": content, "content_type": content.doctype, "completed": status}) + elif content.doctype == "Quiz": if student: - status, score, result, time_taken = utils.check_quiz_completion(content, course_enrollment.name) + status, score, result, time_taken = utils.check_quiz_completion( + content, course_enrollment.name + ) else: status = False score = None result = None - progress.append({'content': content, 'content_type': content.doctype, 'completed': status, 'score': score, 'result': result}) + progress.append( + { + "content": content, + "content_type": content.doctype, + "completed": status, + "score": score, + "result": result, + } + ) return progress diff --git a/erpnext/www/payment_setup_certification.py b/erpnext/www/payment_setup_certification.py index c65cddb5ca..5d62d60f5e 100644 --- a/erpnext/www/payment_setup_certification.py +++ b/erpnext/www/payment_setup_certification.py @@ -2,18 +2,23 @@ import frappe no_cache = 1 + def get_context(context): - if frappe.session.user != 'Guest': + if frappe.session.user != "Guest": context.all_certifications = get_all_certifications_of_a_member() context.show_sidebar = True def get_all_certifications_of_a_member(): - '''Returns all certifications''' + """Returns all certifications""" all_certifications = [] - all_certifications = frappe.db.sql(""" select cc.name,cc.from_date,cc.to_date,ca.amount,ca.currency + all_certifications = frappe.db.sql( + """ select cc.name,cc.from_date,cc.to_date,ca.amount,ca.currency from `tabCertified Consultant` cc inner join `tabCertification Application` ca on cc.certification_application = ca.name - where paid = 1 and email = %(user)s order by cc.to_date desc""" ,{'user': frappe.session.user},as_dict=True) + where paid = 1 and email = %(user)s order by cc.to_date desc""", + {"user": frappe.session.user}, + as_dict=True, + ) return all_certifications diff --git a/erpnext/www/shop-by-category/index.py b/erpnext/www/shop-by-category/index.py index 09f97ba5ef..8a92418d25 100644 --- a/erpnext/www/shop-by-category/index.py +++ b/erpnext/www/shop-by-category/index.py @@ -3,6 +3,7 @@ from frappe import _ sitemap = 1 + def get_context(context): context.body_class = "product-page" @@ -18,13 +19,9 @@ def get_context(context): context.no_cache = 1 + def get_slideshow(slideshow): - values = { - 'show_indicators': 1, - 'show_controls': 1, - 'rounded': 1, - 'slider_name': "Categories" - } + values = {"show_indicators": 1, "show_controls": 1, "rounded": 1, "slider_name": "Categories"} slideshow = frappe.get_cached_doc("Website Slideshow", slideshow) slides = slideshow.get({"doctype": "Website Slideshow Item"}) for index, slide in enumerate(slides, start=1): @@ -37,9 +34,10 @@ def get_slideshow(slideshow): return values + def get_tabs(categories): tab_values = { - 'title': _("Shop by Category"), + "title": _("Shop by Category"), } categorical_data = get_category_records(categories) @@ -48,21 +46,19 @@ def get_tabs(categories): # pre-render cards for each tab tab_values[f"tab_{index + 1}_content"] = frappe.render_template( "erpnext/www/shop-by-category/category_card_section.html", - {"data": categorical_data[tab], "type": tab} + {"data": categorical_data[tab], "type": tab}, ) return tab_values + def get_category_records(categories): categorical_data = {} for category in categories: if category == "item_group": categorical_data["item_group"] = frappe.db.get_all( "Item Group", - filters={ - "parent_item_group": "All Item Groups", - "show_in_website": 1 - }, - fields=["name", "parent_item_group", "is_group", "image", "route"] + filters={"parent_item_group": "All Item Groups", "show_in_website": 1}, + fields=["name", "parent_item_group", "is_group", "image", "route"], ) else: doctype = frappe.unscrub(category) @@ -73,4 +69,3 @@ def get_category_records(categories): categorical_data[category] = frappe.db.get_all(doctype, fields=fields) return categorical_data - diff --git a/erpnext/www/support/index.py b/erpnext/www/support/index.py index 408ddf43a5..aa00e92880 100644 --- a/erpnext/www/support/index.py +++ b/erpnext/www/support/index.py @@ -3,7 +3,7 @@ import frappe def get_context(context): context.no_cache = 1 - context.align_greeting = '' + context.align_greeting = "" setting = frappe.get_doc("Support Settings") context.greeting_title = setting.greeting_title @@ -16,18 +16,22 @@ def get_context(context): if favorite_articles: for article in favorite_articles: name_list.append(article.name) - for record in (frappe.get_all("Help Article", + for record in frappe.get_all( + "Help Article", fields=["title", "content", "route", "category"], - filters={"name": ['not in', tuple(name_list)], "published": 1}, - order_by="creation desc", limit=(6-len(favorite_articles)))): + filters={"name": ["not in", tuple(name_list)], "published": 1}, + order_by="creation desc", + limit=(6 - len(favorite_articles)), + ): favorite_articles.append(record) context.favorite_article_list = get_favorite_articles(favorite_articles) context.help_article_list = get_help_article_list() + def get_favorite_articles_by_page_view(): return frappe.db.sql( - """ + """ SELECT t1.name as name, t1.title as title, @@ -43,32 +47,42 @@ def get_favorite_articles_by_page_view(): GROUP BY route ORDER BY count DESC LIMIT 6; - """, as_dict=True) + """, + as_dict=True, + ) + def get_favorite_articles(favorite_articles): - favorite_article_list=[] + favorite_article_list = [] for article in favorite_articles: description = frappe.utils.strip_html(article.content) if len(description) > 120: - description = description[:120] + '...' + description = description[:120] + "..." favorite_article_dict = { - 'title': article.title, - 'description': description, - 'route': article.route, - 'category': article.category, + "title": article.title, + "description": description, + "route": article.route, + "category": article.category, } favorite_article_list.append(favorite_article_dict) return favorite_article_list + def get_help_article_list(): - help_article_list=[] + help_article_list = [] category_list = frappe.get_all("Help Category", fields="name") for category in category_list: - help_articles = frappe.get_all("Help Article", fields="*", filters={"category": category.name, "published": 1}, order_by="modified desc", limit=5) + help_articles = frappe.get_all( + "Help Article", + fields="*", + filters={"category": category.name, "published": 1}, + order_by="modified desc", + limit=5, + ) if help_articles: help_aricles_per_caetgory = { - 'category': category, - 'articles': help_articles, + "category": category, + "articles": help_articles, } help_article_list.append(help_aricles_per_caetgory) return help_article_list diff --git a/setup.py b/setup.py index 8140700422..1faff0412f 100644 --- a/setup.py +++ b/setup.py @@ -2,23 +2,22 @@ from setuptools import setup, find_packages import re, ast # get version from __version__ variable in erpnext/__init__.py -_version_re = re.compile(r'__version__\s+=\s+(.*)') +_version_re = re.compile(r"__version__\s+=\s+(.*)") -with open('requirements.txt') as f: - install_requires = f.read().strip().split('\n') +with open("requirements.txt") as f: + install_requires = f.read().strip().split("\n") -with open('erpnext/__init__.py', 'rb') as f: - version = str(ast.literal_eval(_version_re.search( - f.read().decode('utf-8')).group(1))) +with open("erpnext/__init__.py", "rb") as f: + version = str(ast.literal_eval(_version_re.search(f.read().decode("utf-8")).group(1))) setup( - name='erpnext', + name="erpnext", version=version, - description='Open Source ERP', - author='Frappe Technologies', - author_email='info@erpnext.com', + description="Open Source ERP", + author="Frappe Technologies", + author_email="info@erpnext.com", packages=find_packages(), zip_safe=False, include_package_data=True, - install_requires=install_requires + install_requires=install_requires, ) From b0f7931b2bea268a0a89b8f67749aed0466e8047 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 28 Mar 2022 18:54:12 +0530 Subject: [PATCH 447/447] chore: ignore black formatting changes in blame --- .git-blame-ignore-revs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 88049be32d..e9cb6cf903 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -23,3 +23,6 @@ b147b85e6ac19a9220cd1e2958a6ebd99373283a # removing six compatibility layer 8fe5feb6a4372bf5f2dfaf65fca41bbcc25c8ce7 + +# bulk format python code with black +494bd9ef78313436f0424b918f200dab8fc7c20b