From 2010b1b6e847911cda7491c5c218d8a074b5d895 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Thu, 8 Dec 2022 16:26:07 -0500 Subject: [PATCH 01/38] fix: use highest precision for exchange rate. --- erpnext/accounts/doctype/payment_entry/payment_entry.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 3fc1adff2d3..4a7a57b6275 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -305,6 +305,7 @@ "fieldname": "source_exchange_rate", "fieldtype": "Float", "label": "Exchange Rate", + "precision": "9", "print_hide": 1, "reqd": 1 }, @@ -334,6 +335,7 @@ "fieldname": "target_exchange_rate", "fieldtype": "Float", "label": "Exchange Rate", + "precision": "9", "print_hide": 1, "reqd": 1 }, @@ -731,7 +733,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-02-23 20:08:39.559814", + "modified": "2022-12-08 16:25:43.824051", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", From d512919f5c95605e98495d5b2f1b11f838622b79 Mon Sep 17 00:00:00 2001 From: artykbasar <78363421+artykbasar@users.noreply.github.com> Date: Sun, 11 Dec 2022 08:51:34 +0000 Subject: [PATCH 02/38] Subscription Cost center value is fixed to Default value(Bug fix) self.cost_center field value keeps changing to erpnext.get_default_cost_center(self.get("company")) after validation of this doctype. This overrides the user input. simple fix, it will first check if the field is empty, if empty then puts the company's default cost center value if not then user input will be saved. --- erpnext/accounts/doctype/subscription/subscription.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 9dab4e91fba..8708342b118 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -280,7 +280,8 @@ class Subscription(Document): self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval()) self.validate_end_date() self.validate_to_follow_calendar_months() - self.cost_center = erpnext.get_default_cost_center(self.get("company")) + if not self.cost_center: + self.cost_center = erpnext.get_default_cost_center(self.get("company")) def validate_trial_period(self): """ From 15e3b7f218cd881d28f616a6286dca27a5223744 Mon Sep 17 00:00:00 2001 From: kunhi Date: Tue, 13 Dec 2022 12:41:56 +0400 Subject: [PATCH 03/38] fix: at create_customer_or_supplier on session creation --- erpnext/portal/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/portal/utils.py b/erpnext/portal/utils.py index 7be8c5df180..c8b03e678bc 100644 --- a/erpnext/portal/utils.py +++ b/erpnext/portal/utils.py @@ -102,7 +102,7 @@ def create_party_contact(doctype, fullname, user, party_name): contact = frappe.new_doc("Contact") contact.update({"first_name": fullname, "email_id": user}) contact.append("links", dict(link_doctype=doctype, link_name=party_name)) - contact.append("email_ids", dict(email_id=user)) + contact.append("email_ids", dict(email_id=user, is_primary=True)) contact.flags.ignore_mandatory = True contact.insert(ignore_permissions=True) From 6d9d730759af258eadf93bc105a0bd159f2b7ba2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 14 Dec 2022 16:05:15 +0530 Subject: [PATCH 04/38] fix: cost_center filter gives incorrect output filtering on cost center gives invoices that are reconciled as having outstanding --- .../doctype/payment_reconciliation/payment_reconciliation.py | 4 +++- erpnext/accounts/utils.py | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 52efd33fefa..ff212f2a35f 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -23,6 +23,7 @@ class PaymentReconciliation(Document): def __init__(self, *args, **kwargs): super(PaymentReconciliation, self).__init__(*args, **kwargs) self.common_filter_conditions = [] + self.accounting_dimension_filter_conditions = [] self.ple_posting_date_filter = [] @frappe.whitelist() @@ -193,6 +194,7 @@ class PaymentReconciliation(Document): posting_date=self.ple_posting_date_filter, min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None, max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None, + accounting_dimensions=self.accounting_dimension_filter_conditions, ) if self.invoice_limit: @@ -381,7 +383,7 @@ class PaymentReconciliation(Document): self.common_filter_conditions.append(ple.company == self.company) if self.get("cost_center") and (get_invoices or get_return_invoices): - self.common_filter_conditions.append(ple.cost_center == self.cost_center) + self.accounting_dimension_filter_conditions.append(ple.cost_center == self.cost_center) if get_invoices: if self.from_invoice_date: diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 41702d65b49..1e573b01bad 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -836,6 +836,7 @@ def get_outstanding_invoices( posting_date=None, min_outstanding=None, max_outstanding=None, + accounting_dimensions=None, ): ple = qb.DocType("Payment Ledger Entry") @@ -866,6 +867,7 @@ def get_outstanding_invoices( min_outstanding=min_outstanding, max_outstanding=max_outstanding, get_invoices=True, + accounting_dimensions=accounting_dimensions or [], ) for d in invoice_list: @@ -1615,6 +1617,7 @@ class QueryPaymentLedger(object): .where(ple.delinked == 0) .where(Criterion.all(filter_on_voucher_no)) .where(Criterion.all(self.common_filter)) + .where(Criterion.all(self.dimensions_filter)) .where(Criterion.all(self.voucher_posting_date)) .groupby(ple.voucher_type, ple.voucher_no, ple.party_type, ple.party) ) @@ -1702,6 +1705,7 @@ class QueryPaymentLedger(object): max_outstanding=None, get_payments=False, get_invoices=False, + accounting_dimensions=None, ): """ Fetch voucher amount and outstanding amount from Payment Ledger using Database CTE @@ -1717,6 +1721,7 @@ class QueryPaymentLedger(object): self.reset() self.vouchers = vouchers self.common_filter = common_filter or [] + self.dimensions_filter = accounting_dimensions or [] self.voucher_posting_date = posting_date or [] self.min_outstanding = min_outstanding self.max_outstanding = max_outstanding From 8eb93004f7f5947ab535cb42c377f2d795dac255 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 14 Dec 2022 16:16:22 +0530 Subject: [PATCH 05/38] fix: cost_center filter fix for 'Get Outstanding Invoice' in PE --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 9354e44d249..e7e7ce5c9d1 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1188,6 +1188,7 @@ def get_outstanding_reference_documents(args): ple = qb.DocType("Payment Ledger Entry") common_filter = [] + accounting_dimensions_filter = [] posting_and_due_date = [] # confirm that Supplier is not blocked @@ -1217,7 +1218,7 @@ def get_outstanding_reference_documents(args): # Add cost center condition if args.get("cost_center"): condition += " and cost_center='%s'" % args.get("cost_center") - common_filter.append(ple.cost_center == args.get("cost_center")) + accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center")) date_fields_dict = { "posting_date": ["from_posting_date", "to_posting_date"], @@ -1243,6 +1244,7 @@ def get_outstanding_reference_documents(args): posting_date=posting_and_due_date, min_outstanding=args.get("outstanding_amt_greater_than"), max_outstanding=args.get("outstanding_amt_less_than"), + accounting_dimensions=accounting_dimensions_filter, ) outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices) From ce9626fead61278085f681673feedcb0f45ff5b2 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 14 Dec 2022 14:03:36 +0100 Subject: [PATCH 06/38] feat: more control when printing RFQ --- .../request_for_quotation.js | 119 ++++++++++++------ .../request_for_quotation.py | 15 ++- 2 files changed, 95 insertions(+), 39 deletions(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index 98c7dc9bd32..744dd70d625 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -57,44 +57,93 @@ frappe.ui.form.on("Request for Quotation",{ }); }, __("Tools")); - frm.add_custom_button(__('Download PDF'), () => { - var suppliers = []; - const fields = [{ - fieldtype: 'Link', - label: __('Select a Supplier'), - fieldname: 'supplier', - options: 'Supplier', - reqd: 1, - get_query: () => { - return { - filters: [ - ["Supplier", "name", "in", frm.doc.suppliers.map((row) => {return row.supplier;})] - ] - } - } - }]; - - frappe.prompt(fields, data => { - var child = locals[cdt][cdn] - - var w = window.open( - frappe.urllib.get_full_url("/api/method/erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_pdf?" - +"doctype="+encodeURIComponent(frm.doc.doctype) - +"&name="+encodeURIComponent(frm.doc.name) - +"&supplier="+encodeURIComponent(data.supplier) - +"&no_letterhead=0")); - if(!w) { - frappe.msgprint(__("Please enable pop-ups")); return; - } + frm.add_custom_button( + __("Download PDF"), + () => { + frappe.prompt( + [ + { + fieldtype: "Link", + label: "Select a Supplier", + fieldname: "supplier", + options: "Supplier", + reqd: 1, + default: frm.doc.suppliers?.length == 1 ? frm.doc.suppliers[0].supplier : "", + get_query: () => { + return { + filters: [ + [ + "Supplier", + "name", + "in", + frm.doc.suppliers.map((row) => { + return row.supplier; + }), + ], + ], + }; + }, + }, + { + fieldtype: "Section Break", + label: "Print Settings", + fieldname: "print_settings", + collapsible: 1, + }, + { + fieldtype: "Link", + label: "Print Format", + fieldname: "print_format", + options: "Print Format", + get_query: () => { + return { + filters: { + doc_type: "Request for Quotation", + }, + }; + }, + }, + { + fieldtype: "Link", + label: "Language", + fieldname: "language", + options: "Language", + }, + { + fieldtype: "Link", + label: "Letter Head", + fieldname: "letter_head", + options: "Letter Head", + }, + ], + (data) => { + var w = window.open( + frappe.urllib.get_full_url( + "/api/method/erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_pdf?" + + new URLSearchParams({ + doctype: frm.doc.doctype, + name: frm.doc.name, + supplier: data.supplier, + print_format: data.print_format || "Standard", + language: data.language || frappe.boot.lang, + letter_head: data.letter_head || "", + }).toString() + ) + ); + if (!w) { + frappe.msgprint(__("Please enable pop-ups")); + return; + } + }, + "Download PDF for Supplier", + "Download" + ); }, - 'Download PDF for Supplier', - 'Download'); - }, - __("Tools")); + __("Tools") + ); - frm.page.set_inner_btn_group_as_primary(__('Create')); + frm.page.set_inner_btn_group_as_primary(__("Create")); } - }, make_supplier_quotation: function(frm) { diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index bdbc9ce0b73..dbc36449570 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -389,10 +389,17 @@ def create_rfq_items(sq_doc, supplier, data): @frappe.whitelist() -def get_pdf(doctype, name, supplier): - doc = get_rfq_doc(doctype, name, supplier) - if doc: - download_pdf(doctype, name, doc=doc) +def get_pdf(doctype, name, supplier, print_format=None, language=None, letter_head=None): + # permissions get checked in `download_pdf` + if doc := get_rfq_doc(doctype, name, supplier): + download_pdf( + doctype, + name, + print_format, + doc=doc, + language=language, + letter_head=letter_head or None, + ) def get_rfq_doc(doctype, name, supplier): From 8717148d9bb96451162d83cc5fd7297aa45a4dc1 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 14 Dec 2022 14:14:29 +0100 Subject: [PATCH 07/38] feat: improve visibility of default values --- .../doctype/request_for_quotation/request_for_quotation.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index 744dd70d625..a9f5afb2e98 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -95,6 +95,7 @@ frappe.ui.form.on("Request for Quotation",{ label: "Print Format", fieldname: "print_format", options: "Print Format", + placeholder: "Standard", get_query: () => { return { filters: { @@ -108,12 +109,14 @@ frappe.ui.form.on("Request for Quotation",{ label: "Language", fieldname: "language", options: "Language", + default: frappe.boot.lang, }, { fieldtype: "Link", label: "Letter Head", fieldname: "letter_head", options: "Letter Head", + default: frm.doc.letter_head, }, ], (data) => { @@ -126,7 +129,7 @@ frappe.ui.form.on("Request for Quotation",{ supplier: data.supplier, print_format: data.print_format || "Standard", language: data.language || frappe.boot.lang, - letter_head: data.letter_head || "", + letter_head: data.letter_head || frm.doc.letter_head || "", }).toString() ) ); From 973ef33eb5ba0e173978a9c7452bb0861b272077 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 14 Dec 2022 20:41:00 +0530 Subject: [PATCH 08/38] fix: Cost Center for tax withholding invoices --- .../tax_withholding_category.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 30ed91b9744..a9d870da74a 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -121,12 +121,24 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None): else: tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted) + cost_center = get_cost_center(inv) + tax_row.update({"cost_center": cost_center}) + if inv.doctype == "Purchase Invoice": return tax_row, tax_deducted_on_advances, voucher_wise_amount else: return tax_row +def get_cost_center(inv): + cost_center = frappe.get_cached_value("Company", inv.company, "cost_center") + + if len(inv.get("taxes")) > 0: + cost_center = inv.get("taxes")[0].cost_center + + return cost_center + + def get_tax_withholding_details(tax_withholding_category, posting_date, company): tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category) From 26277cfcf3e7bbb6b14ceb42ee77e740f2825f20 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 14 Dec 2022 21:22:48 +0530 Subject: [PATCH 09/38] chore: resolve errors in test --- .../tax_withholding_category/tax_withholding_category.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index a9d870da74a..b834d1404d0 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -133,7 +133,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None): def get_cost_center(inv): cost_center = frappe.get_cached_value("Company", inv.company, "cost_center") - if len(inv.get("taxes")) > 0: + if len(inv.get("taxes", [])) > 0: cost_center = inv.get("taxes")[0].cost_center return cost_center From dc178984aefc3ae01e37989d9e7755749072ce82 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 15 Dec 2022 14:15:20 +0530 Subject: [PATCH 10/38] fix: Payment Request flow fixes from Order to Payment Entry --- .../accounts/doctype/payment_entry/payment_entry.py | 2 +- .../doctype/payment_request/payment_request.js | 2 +- .../doctype/payment_request/payment_request.py | 11 +++++------ .../buying/doctype/purchase_order/purchase_order.js | 4 ++-- erpnext/public/js/controllers/transaction.js | 4 ++-- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 9354e44d249..052b1df6f51 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1639,7 +1639,7 @@ def get_payment_entry( ): reference_doc = None doc = frappe.get_doc(dt, dn) - if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0: + if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= 99.99: frappe.throw(_("Can only make payment against unbilled {0}").format(dt)) if not party_type: diff --git a/erpnext/accounts/doctype/payment_request/payment_request.js b/erpnext/accounts/doctype/payment_request/payment_request.js index 901ef1987b4..e9139120283 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.js +++ b/erpnext/accounts/doctype/payment_request/payment_request.js @@ -42,7 +42,7 @@ frappe.ui.form.on("Payment Request", "refresh", function(frm) { }); } - if(!frm.doc.payment_gateway_account && frm.doc.status == "Initiated") { + if((!frm.doc.payment_gateway_account || frm.doc.payment_request_type == "Outward") && frm.doc.status == "Initiated") { frm.add_custom_button(__('Create Payment Entry'), function(){ frappe.call({ method: "erpnext.accounts.doctype.payment_request.payment_request.make_payment_entry", diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index d82083cea05..2cc487d9fe5 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -261,6 +261,7 @@ class PaymentRequest(Document): payment_entry.update( { + "mode_of_payment": self.mode_of_payment, "reference_no": self.name, "reference_date": nowdate(), "remarks": "Payment Entry against {0} {1} via Payment Request {2}".format( @@ -410,12 +411,10 @@ def make_payment_request(**args): else "" ) - existing_payment_request = None - if args.order_type == "Shopping Cart": - existing_payment_request = frappe.db.get_value( - "Payment Request", - {"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": ("!=", 2)}, - ) + existing_payment_request = frappe.db.get_value( + "Payment Request", + {"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": ("!=", 2)}, + ) if existing_payment_request: frappe.db.set_value( diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 06fdea030c8..47089f7d850 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -235,11 +235,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e cur_frm.add_custom_button(__('Purchase Invoice'), this.make_purchase_invoice, __('Create')); - if(flt(doc.per_billed)==0 && doc.status != "Delivered") { + if(flt(doc.per_billed) < 100 && doc.status != "Delivered") { cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create')); } - if(flt(doc.per_billed)==0) { + if(flt(doc.per_billed) < 100) { this.frm.add_custom_button(__('Payment Request'), function() { me.make_payment_request() }, __('Create')); } diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 46ac80895cf..cf05d06a690 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -298,7 +298,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } make_payment_request() { - var me = this; + let me = this; const payment_request_type = (in_list(['Sales Order', 'Sales Invoice'], this.frm.doc.doctype)) ? "Inward" : "Outward"; @@ -314,7 +314,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }, callback: function(r) { if(!r.exc){ - var doc = frappe.model.sync(r.message); + frappe.model.sync(r.message); frappe.set_route("Form", r.message.doctype, r.message.name); } } From a998a8a2dad20e2671de5dbf70816c31c09e4c0e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 15 Dec 2022 12:57:53 +0530 Subject: [PATCH 11/38] test: cost center should not affect outstanding calculation --- .../test_payment_reconciliation.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index dae029b4084..6030134fff2 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -8,6 +8,8 @@ from frappe import qb from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, nowdate +from erpnext import get_default_cost_center +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.party import get_party_account @@ -20,6 +22,7 @@ class TestPaymentReconciliation(FrappeTestCase): self.create_item() self.create_customer() self.create_account() + self.create_cost_center() self.clear_old_entries() def tearDown(self): @@ -216,6 +219,22 @@ class TestPaymentReconciliation(FrappeTestCase): ) return je + def create_cost_center(self): + # Setup cost center + cc_name = "Sub" + + self.main_cc = frappe.get_doc("Cost Center", get_default_cost_center(self.company)) + + cc_exists = frappe.db.get_list("Cost Center", filters={"cost_center_name": cc_name}) + if cc_exists: + self.sub_cc = frappe.get_doc("Cost Center", cc_exists[0].name) + else: + sub_cc = frappe.new_doc("Cost Center") + sub_cc.cost_center_name = "Sub" + sub_cc.parent_cost_center = self.main_cc.parent_cost_center + sub_cc.company = self.main_cc.company + self.sub_cc = sub_cc.save() + def test_filter_min_max(self): # check filter condition minimum and maximum amount self.create_sales_invoice(qty=1, rate=300) @@ -578,3 +597,24 @@ class TestPaymentReconciliation(FrappeTestCase): self.assertEqual(len(pr.payments), 1) self.assertEqual(pr.payments[0].amount, amount) self.assertEqual(pr.payments[0].currency, "EUR") + + def test_differing_cost_center_on_invoice_and_payment(self): + """ + Cost Center filter should not affect outstanding amount calculation + """ + + si = self.create_sales_invoice(qty=1, rate=100, do_not_submit=True) + si.cost_center = self.main_cc.name + si.submit() + pr = get_payment_entry(si.doctype, si.name) + pr.cost_center = self.sub_cc.name + pr = pr.save().submit() + + pr = self.create_payment_reconciliation() + pr.cost_center = self.main_cc.name + + pr.get_unreconciled_entries() + + # check PR tool output + self.assertEqual(len(pr.get("invoices")), 0) + self.assertEqual(len(pr.get("payments")), 0) From 0f28074e5a8b39773ccadf4427f83eaa1ed8821e Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 15 Dec 2022 16:34:06 +0530 Subject: [PATCH 12/38] fix: unsupported operand type(s) for +: 'int' and 'NoneType' --- erpnext/stock/get_item_details.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 31dccf6944d..1741d654601 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -828,9 +828,9 @@ def insert_item_price(args): ): if frappe.has_permission("Item Price", "write"): price_list_rate = ( - (args.rate + args.discount_amount) / args.get("conversion_factor") + (flt(args.rate) + flt(args.discount_amount)) / args.get("conversion_factor") if args.get("conversion_factor") - else (args.rate + args.discount_amount) + else (flt(args.rate) + flt(args.discount_amount)) ) item_price = frappe.db.get_value( From ae8dd2b2bc86cf36162e905ccce9b371b7c123d2 Mon Sep 17 00:00:00 2001 From: Daizy Modi Date: Thu, 15 Dec 2022 16:47:27 +0530 Subject: [PATCH 13/38] fix: remove guest role from doctype --- erpnext/crm/doctype/appointment/appointment.json | 13 ++----------- .../appointment_booking_settings.json | 12 ++++-------- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json index fe7b4e17f0b..c26b064c4c5 100644 --- a/erpnext/crm/doctype/appointment/appointment.json +++ b/erpnext/crm/doctype/appointment/appointment.json @@ -102,7 +102,7 @@ } ], "links": [], - "modified": "2021-06-30 13:09:14.228756", + "modified": "2022-12-15 11:11:02.131986", "modified_by": "Administrator", "module": "CRM", "name": "Appointment", @@ -121,16 +121,6 @@ "share": 1, "write": 1 }, - { - "create": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Guest", - "share": 1 - }, { "create": 1, "delete": 1, @@ -170,5 +160,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json index 4b26e4901bd..436eb10c888 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2019-08-27 10:56:48.309824", "doctype": "DocType", "editable_grid": 1, @@ -101,7 +102,8 @@ } ], "issingle": 1, - "modified": "2019-11-26 12:14:17.669366", + "links": [], + "modified": "2022-12-15 11:10:13.517742", "modified_by": "Administrator", "module": "CRM", "name": "Appointment Booking Settings", @@ -117,13 +119,6 @@ "share": 1, "write": 1 }, - { - "email": 1, - "print": 1, - "read": 1, - "role": "Guest", - "share": 1 - }, { "create": 1, "email": 1, @@ -147,5 +142,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From 25f0b26a17e98aa8d01533f0a183e030e2be82f7 Mon Sep 17 00:00:00 2001 From: Daizy Modi Date: Thu, 15 Dec 2022 16:49:23 +0530 Subject: [PATCH 14/38] fix: remove unused page `book-appointment` --- erpnext/www/book-appointment/__init__.py | 0 erpnext/www/book-appointment/verify/__init__.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 erpnext/www/book-appointment/__init__.py delete mode 100644 erpnext/www/book-appointment/verify/__init__.py diff --git a/erpnext/www/book-appointment/__init__.py b/erpnext/www/book-appointment/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/www/book-appointment/verify/__init__.py b/erpnext/www/book-appointment/verify/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 From 9a00b3bdbd15f4ab83c496e7ee4a35e447b4cfe6 Mon Sep 17 00:00:00 2001 From: Daizy Modi Date: Thu, 15 Dec 2022 16:51:00 +0530 Subject: [PATCH 15/38] fix: format html of verify email for book appointment --- erpnext/www/book_appointment/verify/index.html | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/erpnext/www/book_appointment/verify/index.html b/erpnext/www/book_appointment/verify/index.html index 58c07e85ccc..eaaf8ecfc73 100644 --- a/erpnext/www/book_appointment/verify/index.html +++ b/erpnext/www/book_appointment/verify/index.html @@ -5,14 +5,12 @@ {% endblock%} {% block page_content %} - - {% if success==True %} -
+ {%- set alert_class = 'alert-success' if success else 'alert-danger' -%} +
+ {% if success==True %} {{ _("Your email has been verified and your appointment has been scheduled") }} -
- {% else %} -
+ {% else %} {{ _("Verification failed please check the link") }} -
- {% endif %} + {% endif %} +
{% endblock%} From ac51c2750043d8fe5e2be329eded7a7f6eef2c57 Mon Sep 17 00:00:00 2001 From: Daizy Modi Date: Thu, 15 Dec 2022 16:53:51 +0530 Subject: [PATCH 16/38] fix: fetch required details from appointment booking settings --- erpnext/www/book_appointment/index.py | 38 ++++++++++++-------- erpnext/www/book_appointment/verify/index.py | 4 +-- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/erpnext/www/book_appointment/index.py b/erpnext/www/book_appointment/index.py index 06e99da3f94..8cb66275e05 100644 --- a/erpnext/www/book_appointment/index.py +++ b/erpnext/www/book_appointment/index.py @@ -26,8 +26,12 @@ def get_context(context): @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_cached_value( + "Appointment Booking Settings", + None, + ["holiday_list", "advance_booking_days", "appointment_duration", "success_redirect_url"], + as_dict=True, + ) return settings @@ -90,23 +94,27 @@ 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" - scheduled_time = datetime.datetime.strptime(date + " " + time, format_string) + contact = json.loads(contact) + datetime_obj = datetime.datetime.strptime(date + " " + time, "%Y-%m-%d %H:%M:%S") # Strip tzinfo from datetime objects since it's handled by the doctype + scheduled_time_obj = datetime_obj.replace(tzinfo=None) + scheduled_time = convert_to_system_timezone(tz, scheduled_time_obj) scheduled_time = scheduled_time.replace(tzinfo=None) - scheduled_time = convert_to_system_timezone(tz, scheduled_time) - scheduled_time = scheduled_time.replace(tzinfo=None) + # Create a appointment document from form 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.insert() + appointment.update( + { + "scheduled_time": scheduled_time, + "customer_name": contact.get("name", None), + "customer_phone_number": contact.get("number", None), + "customer_skype": contact.get("skype", None), + "customer_details": contact.get("notes", None), + "customer_email": contact.get("email", None), + "status": "Open", + } + ) + appointment.insert(ignore_permissions=True) return appointment diff --git a/erpnext/www/book_appointment/verify/index.py b/erpnext/www/book_appointment/verify/index.py index 1a5ba9de7e5..4a1ed75ee90 100644 --- a/erpnext/www/book_appointment/verify/index.py +++ b/erpnext/www/book_appointment/verify/index.py @@ -2,7 +2,6 @@ import frappe from frappe.utils.verified_command import verify_request -@frappe.whitelist(allow_guest=True) def get_context(context): if not verify_request(): context.success = False @@ -15,7 +14,6 @@ def get_context(context): appointment = frappe.get_doc("Appointment", appointment_name) appointment.set_verified(email) context.success = True - return context else: context.success = False - return context + return context From ae31ff1c48cb4a9baab98f61bfe9ed85914a9034 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 15 Dec 2022 17:05:53 +0530 Subject: [PATCH 17/38] fix: disabled items showing in the report 'Itemwise Recommended Reorder Level ' --- .../itemwise_recommended_reorder_level.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a6fc049cbde..c4358b809fc 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 @@ -82,7 +82,7 @@ def get_item_info(filters): item.safety_stock, item.lead_time_days, ) - .where(item.is_stock_item == 1) + .where((item.is_stock_item == 1) & (item.disabled == 0)) ) if brand := filters.get("brand"): From 4bfe2ea572c32d1ad8c8d927bf4f43ad5be86df4 Mon Sep 17 00:00:00 2001 From: Daizy Modi Date: Thu, 15 Dec 2022 17:19:28 +0530 Subject: [PATCH 18/38] fix: agent assignment and permissions for appointment --- .../crm/doctype/appointment/appointment.py | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 6e7ba1fd5bc..29d092245b4 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -6,7 +6,9 @@ from collections import Counter import frappe from frappe import _ +from frappe.desk.form.assign_to import add as add_assignment from frappe.model.document import Document +from frappe.share import add_docshare from frappe.utils import get_url, getdate, now from frappe.utils.verified_command import get_signed_params @@ -130,13 +132,13 @@ class Appointment(Document): self.party = lead.name def auto_assign(self): - from frappe.desk.form.assign_to import add as add_assignemnt - existing_assignee = self.get_assignee_from_latest_opportunity() if existing_assignee: # If the latest opportunity is assigned to someone # Assign the appointment to the same - add_assignemnt({"doctype": self.doctype, "name": self.name, "assign_to": [existing_assignee]}) + add_agent_assignment( + {"doctype": self.doctype, "name": self.name, "assign_to": [existing_assignee]} + ) return if self._assign: return @@ -144,7 +146,7 @@ class Appointment(Document): for agent in available_agents: if _check_agent_availability(agent, self.scheduled_time): agent = agent[0] - add_assignemnt({"doctype": self.doctype, "name": self.name, "assign_to": [agent]}) + add_agent_assignment({"doctype": self.doctype, "name": self.name, "assign_to": [agent]}) break def get_assignee_from_latest_opportunity(self): @@ -201,7 +203,7 @@ class Appointment(Document): def _get_agents_sorted_by_asc_workload(date): - appointments = frappe.db.get_list("Appointment", fields="*") + appointments = frappe.get_all("Appointment", fields="*") agent_list = _get_agent_list_as_strings() if not appointments: return agent_list @@ -226,7 +228,7 @@ def _get_agent_list_as_strings(): def _check_agent_availability(agent_email, scheduled_time): - appointemnts_at_scheduled_time = frappe.get_list( + appointemnts_at_scheduled_time = frappe.get_all( "Appointment", filters={"scheduled_time": scheduled_time} ) for appointment in appointemnts_at_scheduled_time: @@ -240,3 +242,12 @@ def _get_employee_from_user(user): if employee_docname: return frappe.get_doc("Employee", employee_docname) return None + + +def add_agent_assignment(args): + doc = frappe.get_cached_doc(args.get("doctype"), args.get("name")) + for assign_to in args.get("assign_to"): + if not frappe.has_permission(doc=doc, user=assign_to): + add_docshare(doc.doctype, doc.name, assign_to, flags={"ignore_share_permission": True}) + + add_assignment(args) From 56f3ac15d8e043080ab0050694d7d627e5a3cfad Mon Sep 17 00:00:00 2001 From: Daizy Modi Date: Thu, 15 Dec 2022 18:02:14 +0530 Subject: [PATCH 19/38] fix: removed unncessary changes --- erpnext/crm/doctype/appointment/appointment.py | 8 +++++--- erpnext/www/book_appointment/verify/index.html | 14 ++++++++------ erpnext/www/book_appointment/verify/index.py | 3 ++- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 29d092245b4..8480f2ca44a 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -245,9 +245,11 @@ def _get_employee_from_user(user): def add_agent_assignment(args): - doc = frappe.get_cached_doc(args.get("doctype"), args.get("name")) + doctype = args.get("doctype") + docname = args.get("name") + for assign_to in args.get("assign_to"): - if not frappe.has_permission(doc=doc, user=assign_to): - add_docshare(doc.doctype, doc.name, assign_to, flags={"ignore_share_permission": True}) + if not frappe.has_permission(doctype=doctype, doc=docname, user=assign_to): + add_docshare(doctype, docname, assign_to, flags={"ignore_share_permission": True}) add_assignment(args) diff --git a/erpnext/www/book_appointment/verify/index.html b/erpnext/www/book_appointment/verify/index.html index eaaf8ecfc73..58c07e85ccc 100644 --- a/erpnext/www/book_appointment/verify/index.html +++ b/erpnext/www/book_appointment/verify/index.html @@ -5,12 +5,14 @@ {% endblock%} {% block page_content %} - {%- set alert_class = 'alert-success' if success else 'alert-danger' -%} -
- {% if success==True %} + + {% if success==True %} +
{{ _("Your email has been verified and your appointment has been scheduled") }} - {% else %} +
+ {% else %} +
{{ _("Verification failed please check the link") }} - {% endif %} -
+
+ {% endif %} {% endblock%} diff --git a/erpnext/www/book_appointment/verify/index.py b/erpnext/www/book_appointment/verify/index.py index 4a1ed75ee90..3beb8667ae7 100644 --- a/erpnext/www/book_appointment/verify/index.py +++ b/erpnext/www/book_appointment/verify/index.py @@ -14,6 +14,7 @@ def get_context(context): appointment = frappe.get_doc("Appointment", appointment_name) appointment.set_verified(email) context.success = True + return context else: context.success = False - return context + return context From 1a40c04b72986232d40be1620788ed5b6b68ffab Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 15 Dec 2022 18:51:58 +0530 Subject: [PATCH 20/38] feat: Ignore company related doctype for other apps via hooks --- .../transaction_deletion_record.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 c18a4b2214b..4256a7d8312 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -204,7 +204,7 @@ class TransactionDeletionRecord(Document): @frappe.whitelist() def get_doctypes_to_be_ignored(): - doctypes_to_be_ignored_list = [ + doctypes_to_be_ignored = [ "Account", "Cost Center", "Warehouse", @@ -223,4 +223,7 @@ def get_doctypes_to_be_ignored(): "Customer", "Supplier", ] - return doctypes_to_be_ignored_list + + doctypes_to_be_ignored.extend(frappe.get_hooks("company_data_to_be_ignored") or []) + + return doctypes_to_be_ignored From 5d0b5c8d2a0564fedef0683971391863a70915a4 Mon Sep 17 00:00:00 2001 From: Daizy Modi Date: Fri, 16 Dec 2022 10:10:29 +0530 Subject: [PATCH 21/38] fix: pass necessary params instead of args --- .../crm/doctype/appointment/appointment.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 8480f2ca44a..a4e00d6c47b 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -136,9 +136,7 @@ class Appointment(Document): if existing_assignee: # If the latest opportunity is assigned to someone # Assign the appointment to the same - add_agent_assignment( - {"doctype": self.doctype, "name": self.name, "assign_to": [existing_assignee]} - ) + assign_agents(self.doctype, self.name, [existing_assignee]) return if self._assign: return @@ -146,7 +144,7 @@ class Appointment(Document): for agent in available_agents: if _check_agent_availability(agent, self.scheduled_time): agent = agent[0] - add_agent_assignment({"doctype": self.doctype, "name": self.name, "assign_to": [agent]}) + assign_agents(self.doctype, self.name, [agent]) break def get_assignee_from_latest_opportunity(self): @@ -244,12 +242,9 @@ def _get_employee_from_user(user): return None -def add_agent_assignment(args): - doctype = args.get("doctype") - docname = args.get("name") +def assign_agents(doctype: str, name: str, agents: list[str]) -> None: + for agent in agents: + if not frappe.has_permission(doctype=doctype, doc=name, user=agent): + add_docshare(doctype, name, agent, flags={"ignore_share_permission": True}) - for assign_to in args.get("assign_to"): - if not frappe.has_permission(doctype=doctype, doc=docname, user=assign_to): - add_docshare(doctype, docname, assign_to, flags={"ignore_share_permission": True}) - - add_assignment(args) + add_assignment({"doctype": doctype, "name": name, "assign_to": agents}) From 4802d719edf62e558b623b5dceaa75991df3a607 Mon Sep 17 00:00:00 2001 From: Daizy Modi Date: Fri, 16 Dec 2022 15:55:58 +0530 Subject: [PATCH 22/38] fix: removed unused data and minor changes --- .../crm/doctype/appointment/appointment.py | 19 +++++------- erpnext/www/book_appointment/index.js | 3 -- erpnext/www/book_appointment/index.py | 30 ++++++++----------- 3 files changed, 21 insertions(+), 31 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index a4e00d6c47b..bd49bdc925c 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -136,15 +136,14 @@ class Appointment(Document): if existing_assignee: # If the latest opportunity is assigned to someone # Assign the appointment to the same - assign_agents(self.doctype, self.name, [existing_assignee]) + self.assign_agent(existing_assignee) return if self._assign: return available_agents = _get_agents_sorted_by_asc_workload(getdate(self.scheduled_time)) for agent in available_agents: if _check_agent_availability(agent, self.scheduled_time): - agent = agent[0] - assign_agents(self.doctype, self.name, [agent]) + self.assign_agent(agent[0]) break def get_assignee_from_latest_opportunity(self): @@ -199,6 +198,12 @@ class Appointment(Document): params = {"email": self.customer_email, "appointment": self.name} return get_url(verify_route + "?" + get_signed_params(params)) + def assign_agent(self, agent): + if not frappe.has_permission(doc=self, user=agent): + add_docshare(self.doctype, self.name, agent, flags={"ignore_share_permission": True}) + + add_assignment({"doctype": self.doctype, "name": self.name, "assign_to": [agent]}) + def _get_agents_sorted_by_asc_workload(date): appointments = frappe.get_all("Appointment", fields="*") @@ -240,11 +245,3 @@ def _get_employee_from_user(user): if employee_docname: return frappe.get_doc("Employee", employee_docname) return None - - -def assign_agents(doctype: str, name: str, agents: list[str]) -> None: - for agent in agents: - if not frappe.has_permission(doctype=doctype, doc=name, user=agent): - add_docshare(doctype, name, agent, flags={"ignore_share_permission": True}) - - add_assignment({"doctype": doctype, "name": name, "assign_to": agents}) diff --git a/erpnext/www/book_appointment/index.js b/erpnext/www/book_appointment/index.js index 46ac15518c7..d02cdad4cf4 100644 --- a/erpnext/www/book_appointment/index.js +++ b/erpnext/www/book_appointment/index.js @@ -2,8 +2,6 @@ frappe.ready(async () => { initialise_select_date(); }) -window.holiday_list = []; - async function initialise_select_date() { navigate_to_page(1); await get_global_variables(); @@ -20,7 +18,6 @@ async function get_global_variables() { window.timezones = (await frappe.call({ method:'erpnext.www.book_appointment.index.get_timezones' })).message; - window.holiday_list = window.appointment_settings.holiday_list; } function setup_timezone_selector() { diff --git a/erpnext/www/book_appointment/index.py b/erpnext/www/book_appointment/index.py index 8cb66275e05..dfca9465ed1 100644 --- a/erpnext/www/book_appointment/index.py +++ b/erpnext/www/book_appointment/index.py @@ -29,7 +29,7 @@ def get_appointment_settings(): settings = frappe.get_cached_value( "Appointment Booking Settings", None, - ["holiday_list", "advance_booking_days", "appointment_duration", "success_redirect_url"], + ["advance_booking_days", "appointment_duration", "success_redirect_url"], as_dict=True, ) return settings @@ -94,26 +94,22 @@ def get_available_slots_between(query_start_time, query_end_time, settings): @frappe.whitelist(allow_guest=True) def create_appointment(date, time, tz, contact): - contact = json.loads(contact) - datetime_obj = datetime.datetime.strptime(date + " " + time, "%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_obj = datetime_obj.replace(tzinfo=None) - scheduled_time = convert_to_system_timezone(tz, scheduled_time_obj) scheduled_time = scheduled_time.replace(tzinfo=None) - + scheduled_time = convert_to_system_timezone(tz, scheduled_time) + scheduled_time = scheduled_time.replace(tzinfo=None) # Create a appointment document from form appointment = frappe.new_doc("Appointment") - appointment.update( - { - "scheduled_time": scheduled_time, - "customer_name": contact.get("name", None), - "customer_phone_number": contact.get("number", None), - "customer_skype": contact.get("skype", None), - "customer_details": contact.get("notes", None), - "customer_email": contact.get("email", None), - "status": "Open", - } - ) + 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.insert(ignore_permissions=True) return appointment From 3b66920342430d44996b41b1334f4588a7030a85 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 16 Dec 2022 19:22:29 +0530 Subject: [PATCH 23/38] fix: Unable to import COA through importer --- .../chart_of_accounts_importer/chart_of_accounts_importer.py | 4 ++++ erpnext/setup/doctype/company/company.py | 3 --- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index 325a356c3ce..220b74727b9 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -485,6 +485,10 @@ def set_default_accounts(company): "default_payable_account": frappe.db.get_value( "Account", {"company": company.name, "account_type": "Payable", "is_group": 0} ), + "default_provisional_account": frappe.db.get_value( + "Account", + {"company": company.name, "account_type": "Service Received But Not Billed", "is_group": 0}, + ), } ) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index d6f23780942..07ee2890c46 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -70,9 +70,6 @@ class Company(NestedSet): self.abbr = self.abbr.strip() - # if self.get('__islocal') and len(self.abbr) > 5: - # frappe.throw(_("Abbreviation cannot have more than 5 characters")) - if not self.abbr.strip(): frappe.throw(_("Abbreviation is mandatory")) From 4ecce242a80a3cba7213ab7111ded8de7bb92081 Mon Sep 17 00:00:00 2001 From: Gokulnath <95605271+Gokulnath17@users.noreply.github.com> Date: Sat, 17 Dec 2022 20:06:01 +0530 Subject: [PATCH 24/38] feat: adding warehouse filter for sales order ananlysis report feat: adding warehouse filter for sales order ananlysis report --- .../report/sales_order_analysis/sales_order_analysis.js | 6 ++++++ .../report/sales_order_analysis/sales_order_analysis.py | 3 +++ 2 files changed, 9 insertions(+) diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js index 91748bc7be2..f3f931edfdc 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js @@ -44,6 +44,12 @@ frappe.query_reports["Sales Order Analysis"] = { } } }, + { + "fieldname": "warehouse", + "label": __("Warehouse"), + "fieldtype": "Link", + "options": "Warehouse" + }, { "fieldname": "status", "label": __("Status"), 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 720aa41982a..63d339a839d 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -53,6 +53,9 @@ def get_conditions(filters): if filters.get("status"): conditions += " and so.status in %(status)s" + if filters.get("warehouse"): + conditions += " and soi.warehouse = %(warehouse)s" + return conditions From b09eade3e4b41833e507410410f63b93d8d17de7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 16 Dec 2022 21:58:22 +0530 Subject: [PATCH 25/38] fix: ERR journals reported in AR/AP Exchange Rate Revaluation on Receivable/Payable will included in AR/AP report --- .../accounts_receivable.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index a195c575866..9d9684340c7 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -99,6 +99,9 @@ class ReceivablePayableReport(object): # Get return entries self.get_return_entries() + # Get Exchange Rate Revaluations + self.get_exchange_rate_revaluations() + self.data = [] for ple in self.ple_entries: @@ -251,7 +254,8 @@ class ReceivablePayableReport(object): row.invoice_grand_total = row.invoiced if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and ( - abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision + (abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision) + or (row.voucher_no in self.err_journals) ): # non-zero oustanding, we must consider this row @@ -1028,3 +1032,17 @@ class ReceivablePayableReport(object): "data": {"labels": self.ageing_column_labels, "datasets": rows}, "type": "percentage", } + + def get_exchange_rate_revaluations(self): + je = qb.DocType("Journal Entry") + results = ( + qb.from_(je) + .select(je.name) + .where( + (je.company == self.filters.company) + & (je.posting_date.lte(self.filters.report_date)) + & (je.voucher_type == "Exchange Rate Revaluation") + ) + .run() + ) + self.err_journals = [x[0] for x in results] if results else [] From 2ed86760d7696b0129a0d01cc4287cfe001f3a66 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 18 Dec 2022 12:39:54 +0530 Subject: [PATCH 26/38] test: err for party should be in AR/AP report --- .../test_accounts_receivable.py | 134 +++++++++++++++--- 1 file changed, 116 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index bac8beed2e2..97a9c15fc76 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -1,9 +1,10 @@ import unittest import frappe -from frappe.tests.utils import FrappeTestCase -from frappe.utils import add_days, getdate, today +from frappe.tests.utils import FrappeTestCase, change_settings +from frappe.utils import add_days, flt, getdate, today +from erpnext import get_default_cost_center from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute @@ -17,10 +18,37 @@ class TestAccountsReceivable(FrappeTestCase): frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company 2'") frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'") frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'") + frappe.db.sql("delete from `tabJournal Entry` where company='_Test Company 2'") + frappe.db.sql("delete from `tabExchange Rate Revaluation` where company='_Test Company 2'") + + self.create_usd_account() def tearDown(self): frappe.db.rollback() + def create_usd_account(self): + name = "Debtors USD" + exists = frappe.db.get_list( + "Account", filters={"company": "_Test Company 2", "account_name": "Debtors USD"} + ) + if exists: + self.debtors_usd = exists[0].name + else: + debtors = frappe.get_doc( + "Account", + frappe.db.get_list( + "Account", filters={"company": "_Test Company 2", "account_name": "Debtors"} + )[0].name, + ) + + debtors_usd = frappe.new_doc("Account") + debtors_usd.company = debtors.company + debtors_usd.account_name = "Debtors USD" + debtors_usd.account_currency = "USD" + debtors_usd.parent_account = debtors.parent_account + debtors_usd.account_type = debtors.account_type + self.debtors_usd = debtors_usd.save().name + def test_accounts_receivable(self): filters = { "company": "_Test Company 2", @@ -33,7 +61,7 @@ class TestAccountsReceivable(FrappeTestCase): } # check invoice grand total and invoiced column's value for 3 payment terms - name = make_sales_invoice() + name = make_sales_invoice().name report = execute(filters) expected_data = [[100, 30], [100, 50], [100, 20]] @@ -118,8 +146,74 @@ class TestAccountsReceivable(FrappeTestCase): ], ) + @change_settings( + "Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1} + ) + def test_exchange_revaluation_for_party(self): + """ + Exchange Revaluation for party on Receivable/Payable shoule be included + """ -def make_sales_invoice(): + company = "_Test Company 2" + customer = "_Test Customer 2" + + # Using Exchange Gain/Loss account for unrealized as well. + company_doc = frappe.get_doc("Company", company) + company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account + company_doc.save() + + si = make_sales_invoice(no_payment_schedule=True, do_not_submit=True) + si.currency = "USD" + si.conversion_rate = 0.90 + si.debit_to = self.debtors_usd + si = si.save().submit() + + # Exchange Revaluation + err = frappe.new_doc("Exchange Rate Revaluation") + err.company = company + err.posting_date = today() + accounts = err.get_accounts_data() + err.extend("accounts", accounts) + err.accounts[0].new_exchange_rate = 0.95 + row = err.accounts[0] + row.new_balance_in_base_currency = flt( + row.new_exchange_rate * flt(row.balance_in_account_currency) + ) + row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency) + err.set_total_gain_loss() + err = err.save().submit() + + # Submit JV for ERR + jv = frappe.get_doc(err.make_jv_entry()) + jv = jv.save() + for x in jv.accounts: + x.cost_center = get_default_cost_center(jv.company) + jv.submit() + + filters = { + "company": company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + report = execute(filters) + + expected_data_for_err = [0, -5, 0, 5] + row = [x for x in report[1] if x.voucher_type == jv.doctype and x.voucher_no == jv.name][0] + self.assertEqual( + expected_data_for_err, + [ + row.invoiced, + row.paid, + row.credit_note, + row.outstanding, + ], + ) + + +def make_sales_invoice(no_payment_schedule=False, do_not_submit=False): frappe.set_user("Administrator") si = create_sales_invoice( @@ -134,22 +228,26 @@ def make_sales_invoice(): do_not_save=1, ) - si.append( - "payment_schedule", - dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30), - ) - si.append( - "payment_schedule", - dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50), - ) - si.append( - "payment_schedule", - dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20), - ) + if not no_payment_schedule: + si.append( + "payment_schedule", + dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30), + ) + si.append( + "payment_schedule", + dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50), + ) + si.append( + "payment_schedule", + dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20), + ) - si.submit() + si = si.save() - return si.name + if not do_not_submit: + si = si.submit() + + return si def make_payment(docname): From 2b4eae5f8405cb3ec536cf5c8740eaaa0edf5496 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 19 Dec 2022 16:24:55 +0530 Subject: [PATCH 27/38] fix: unsupported operand type(s) for +=: 'int' and 'NoneType' --- erpnext/stock/doctype/pick_list/pick_list.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 5de7fedc4e0..aff5e0539c7 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -192,13 +192,13 @@ class PickList(Document): if item_map.get(key): item_map[key].qty += item.qty - item_map[key].stock_qty += item.stock_qty + item_map[key].stock_qty += flt(item.stock_qty, item.precision("stock_qty")) else: item_map[key] = item # maintain count of each item (useful to limit get query) self.item_count_map.setdefault(item_code, 0) - self.item_count_map[item_code] += item.stock_qty + self.item_count_map[item_code] += flt(item.stock_qty, item.precision("stock_qty")) return item_map.values() From 88ce11f03df30634ec9ee1c0e50ccb424452c82c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 19 Dec 2022 16:44:19 +0530 Subject: [PATCH 28/38] fix: incorrect type hints (#33381) [skip ci] --- erpnext/stock/doctype/serial_no/serial_no.py | 4 ++-- .../doctype/stock_reconciliation/stock_reconciliation.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index a2748d0d09a..541d4d17e18 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -766,13 +766,13 @@ def get_delivery_note_serial_no(item_code, qty, delivery_note): @frappe.whitelist() def auto_fetch_serial_number( - qty: float, + qty: int, 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, + exclude_sr_nos=None, ) -> List[str]: filters = frappe._dict({"item_code": item_code, "warehouse": warehouse}) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 3a0b38a0fcd..398b3c98e38 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -715,8 +715,8 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None): def get_stock_balance_for( item_code: str, warehouse: str, - posting_date: str, - posting_time: str, + posting_date, + posting_time, batch_no: Optional[str] = None, with_valuation_rate: bool = True, ): From b1721b79cebc7c4c438f00a1deaebaa5b39f7e0b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 19 Dec 2022 23:24:34 +0530 Subject: [PATCH 29/38] fix: daily scheduler to identify and fix stock transfer entries having incorrect valuation --- erpnext/hooks.py | 1 + .../stock/doctype/stock_entry/stock_entry.py | 73 ++++++++++++++++++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index fd19d2585cc..e84a32d46f7 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -393,6 +393,7 @@ scheduler_events = { ], "daily": [ "erpnext.support.doctype.issue.issue.auto_close_tickets", + "erpnext.stock.doctype.stock_entry.stock_entry.audit_incorrect_valuation_entries", "erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity", "erpnext.controllers.accounts_controller.update_invoice_status", "erpnext.accounts.doctype.fiscal_year.fiscal_year.auto_create_fiscal_year", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index d401f818c68..a047a9b8142 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -4,12 +4,24 @@ import json from collections import defaultdict +from typing import Dict import frappe from frappe import _ from frappe.model.mapper import get_mapped_doc from frappe.query_builder.functions import Sum -from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate +from frappe.utils import ( + add_days, + cint, + comma_or, + cstr, + flt, + format_time, + formatdate, + getdate, + nowdate, + today, +) import erpnext from erpnext.accounts.general_ledger import process_gl_map @@ -2700,3 +2712,62 @@ def get_stock_entry_data(work_order): ) .orderby(stock_entry.creation, stock_entry_detail.item_code, stock_entry_detail.idx) ).run(as_dict=1) + + +def audit_incorrect_valuation_entries(): + # Audit of stock transfer entries having incorrect valuation + from erpnext.controllers.stock_controller import create_repost_item_valuation_entry + + stock_entries = get_incorrect_stock_entries() + + for stock_entry, values in stock_entries.items(): + reposting_data = frappe._dict( + { + "posting_date": values.posting_date, + "posting_time": values.posting_time, + "voucher_type": "Stock Entry", + "voucher_no": stock_entry, + "company": values.company, + } + ) + + create_repost_item_valuation_entry(reposting_data) + + +def get_incorrect_stock_entries() -> Dict: + stock_entry = frappe.qb.DocType("Stock Entry") + stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry") + transfer_purposes = [ + "Material Transfer", + "Material Transfer for Manufacture", + "Send to Subcontractor", + ] + + query = ( + frappe.qb.from_(stock_entry) + .inner_join(stock_ledger_entry) + .on(stock_entry.name == stock_ledger_entry.voucher_no) + .select( + stock_entry.name, + stock_entry.company, + stock_entry.posting_date, + stock_entry.posting_time, + Sum(stock_ledger_entry.stock_value_difference).as_("stock_value"), + ) + .where( + (stock_entry.docstatus == 1) + & (stock_entry.purpose.isin(transfer_purposes)) + & (stock_ledger_entry.modified > add_days(today(), -2)) + ) + .groupby(stock_ledger_entry.voucher_detail_no) + .having(Sum(stock_ledger_entry.stock_value_difference) != 0) + ) + + data = query.run(as_dict=True) + stock_entries = {} + + for row in data: + if abs(row.stock_value) > 0.1 and row.name not in stock_entries: + stock_entries.setdefault(row.name, row) + + return stock_entries From f31612376af60e9b7ed5bce10edba49e9dc27b0d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 20 Dec 2022 00:14:41 +0530 Subject: [PATCH 30/38] test: added test case to validate audit for incorrect entries --- erpnext/hooks.py | 2 +- .../doctype/stock_entry/test_stock_entry.py | 42 ++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index e84a32d46f7..7d72c76b819 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -393,7 +393,6 @@ scheduler_events = { ], "daily": [ "erpnext.support.doctype.issue.issue.auto_close_tickets", - "erpnext.stock.doctype.stock_entry.stock_entry.audit_incorrect_valuation_entries", "erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity", "erpnext.controllers.accounts_controller.update_invoice_status", "erpnext.accounts.doctype.fiscal_year.fiscal_year.auto_create_fiscal_year", @@ -421,6 +420,7 @@ scheduler_events = { "erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall", "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans", "erpnext.crm.utils.open_leads_opportunities_based_on_todays_event", + "erpnext.stock.doctype.stock_entry.stock_entry.audit_incorrect_valuation_entries", ], "monthly_long": [ "erpnext.accounts.deferred_revenue.process_deferred_accounting", diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index b574b718fe1..680d209735e 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -5,7 +5,7 @@ import frappe from frappe.permissions import add_user_permission, remove_user_permission from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, flt, nowdate, nowtime, today +from frappe.utils import add_days, flt, now, nowdate, nowtime, today from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.stock.doctype.item.test_item import ( @@ -17,6 +17,8 @@ from erpnext.stock.doctype.item.test_item import ( from erpnext.stock.doctype.serial_no.serial_no import * # noqa from erpnext.stock.doctype.stock_entry.stock_entry import ( FinishedGoodError, + audit_incorrect_valuation_entries, + get_incorrect_stock_entries, move_sample_to_retention_warehouse, ) from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry @@ -1614,6 +1616,44 @@ class TestStockEntry(FrappeTestCase): self.assertRaises(BatchExpiredError, se.save) + def test_audit_incorrect_stock_entries(self): + item_code = "Test Incorrect Valuation Rate Item - 001" + create_item(item_code=item_code, is_stock_item=1) + + make_stock_entry( + item_code=item_code, + purpose="Material Receipt", + posting_date=add_days(nowdate(), -10), + qty=2, + rate=500, + to_warehouse="_Test Warehouse - _TC", + ) + + transfer_entry = make_stock_entry( + item_code=item_code, + purpose="Material Transfer", + qty=2, + rate=500, + from_warehouse="_Test Warehouse - _TC", + to_warehouse="_Test Warehouse 1 - _TC", + ) + + sle_name = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": transfer_entry.name, "actual_qty": (">", 0)}, "name" + ) + + frappe.db.set_value( + "Stock Ledger Entry", sle_name, {"modified": add_days(now(), -1), "stock_value_difference": 10} + ) + + stock_entries = get_incorrect_stock_entries() + self.assertTrue(transfer_entry.name in stock_entries) + + audit_incorrect_valuation_entries() + + stock_entries = get_incorrect_stock_entries() + self.assertFalse(transfer_entry.name in stock_entries) + def make_serialized_item(**args): args = frappe._dict(args) From d0dbfec052dd9f5ef32e4872c59f3cd985a0ff2b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 20 Dec 2022 09:59:27 +0530 Subject: [PATCH 31/38] fix: Cost center filter not working in cash flow report --- .../accounts/report/cash_flow/cash_flow.py | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py index d4f2011aa8c..12a6bc11999 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.py +++ b/erpnext/accounts/report/cash_flow/cash_flow.py @@ -8,6 +8,7 @@ from frappe.utils import cint, cstr from erpnext.accounts.report.financial_statements import ( get_columns, + get_cost_centers_with_children, get_data, get_filtered_list_for_consolidated_report, get_period_list, @@ -160,10 +161,11 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_ total = 0 for period in period_list: start_date = get_start_date(period, accumulated_values, company) + filters.start_date = start_date + filters.end_date = period["to_date"] + filters.account_type = account_type - amount = get_account_type_based_gl_data( - company, start_date, period["to_date"], account_type, filters - ) + amount = get_account_type_based_gl_data(company, filters) if amount and account_type == "Depreciation": amount *= -1 @@ -175,7 +177,7 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_ return data -def get_account_type_based_gl_data(company, start_date, end_date, account_type, filters=None): +def get_account_type_based_gl_data(company, filters=None): cond = "" filters = frappe._dict(filters or {}) @@ -191,19 +193,25 @@ def get_account_type_based_gl_data(company, start_date, end_date, account_type, frappe.db.escape(cstr(filters.finance_book)) ) + if filters.get("cost_center"): + filters.cost_center = get_cost_centers_with_children(filters.cost_center) + cond += " and cost_center in %(cost_center)s" + gl_sum = frappe.db.sql_list( """ select sum(credit) - sum(debit) from `tabGL Entry` - where company=%s and posting_date >= %s and posting_date <= %s + where company=%(company)s and posting_date >= %(start_date)s and posting_date <= %(end_date)s and voucher_type != 'Period Closing Voucher' - and account in ( SELECT name FROM tabAccount WHERE account_type = %s) {cond} + and account in ( SELECT name FROM tabAccount WHERE account_type = %(account_type)s) {cond} """.format( cond=cond ), - (company, start_date, end_date, account_type), + filters, ) + print(gl_sum) + return gl_sum[0] if gl_sum and gl_sum[0] else 0 From 068df9f815723db32e673dafc14a85cdc6fcc313 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 20 Dec 2022 10:00:53 +0530 Subject: [PATCH 32/38] chore: remove print statement --- erpnext/accounts/report/cash_flow/cash_flow.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py index 12a6bc11999..cb3c78a2b0b 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.py +++ b/erpnext/accounts/report/cash_flow/cash_flow.py @@ -210,8 +210,6 @@ def get_account_type_based_gl_data(company, filters=None): filters, ) - print(gl_sum) - return gl_sum[0] if gl_sum and gl_sum[0] else 0 From e25b98b620114ba233a48d3e3a402f40001281b8 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 20 Dec 2022 11:42:05 +0530 Subject: [PATCH 33/38] chore: Update test case --- .../payment_request/payment_request.py | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 2cc487d9fe5..085ff980d56 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -411,44 +411,44 @@ def make_payment_request(**args): else "" ) - existing_payment_request = frappe.db.get_value( + draft_payment_request = frappe.db.get_value( "Payment Request", - {"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": ("!=", 2)}, + {"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0}, ) - if existing_payment_request: + existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn) + + if existing_payment_request_amount: + grand_total -= existing_payment_request_amount + + if draft_payment_request: frappe.db.set_value( - "Payment Request", existing_payment_request, "grand_total", grand_total, update_modified=False + "Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False ) - pr = frappe.get_doc("Payment Request", existing_payment_request) + pr = frappe.get_doc("Payment Request", draft_payment_request) else: if args.order_type != "Shopping Cart": - existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn) - - if existing_payment_request_amount: - grand_total -= existing_payment_request_amount - - pr = frappe.new_doc("Payment Request") - pr.update( - { - "payment_gateway_account": gateway_account.get("name"), - "payment_gateway": gateway_account.get("payment_gateway"), - "payment_account": gateway_account.get("payment_account"), - "payment_channel": gateway_account.get("payment_channel"), - "payment_request_type": args.get("payment_request_type"), - "currency": ref_doc.currency, - "grand_total": grand_total, - "mode_of_payment": args.mode_of_payment, - "email_to": args.recipient_id or ref_doc.owner, - "subject": _("Payment Request for {0}").format(args.dn), - "message": gateway_account.get("message") or get_dummy_message(ref_doc), - "reference_doctype": args.dt, - "reference_name": args.dn, - "party_type": args.get("party_type") or "Customer", - "party": args.get("party") or ref_doc.get("customer"), - "bank_account": bank_account, - } - ) + pr = frappe.new_doc("Payment Request") + pr.update( + { + "payment_gateway_account": gateway_account.get("name"), + "payment_gateway": gateway_account.get("payment_gateway"), + "payment_account": gateway_account.get("payment_account"), + "payment_channel": gateway_account.get("payment_channel"), + "payment_request_type": args.get("payment_request_type"), + "currency": ref_doc.currency, + "grand_total": grand_total, + "mode_of_payment": args.mode_of_payment, + "email_to": args.recipient_id or ref_doc.owner, + "subject": _("Payment Request for {0}").format(args.dn), + "message": gateway_account.get("message") or get_dummy_message(ref_doc), + "reference_doctype": args.dt, + "reference_name": args.dn, + "party_type": args.get("party_type") or "Customer", + "party": args.get("party") or ref_doc.get("customer"), + "bank_account": bank_account, + } + ) if args.order_type == "Shopping Cart" or args.mute_email: pr.flags.mute_email = True From 31c95deb88b376ff86746bc07b07590465594fd0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 20 Dec 2022 11:45:51 +0530 Subject: [PATCH 34/38] chore: More fixes --- .../payment_request/payment_request.py | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 085ff980d56..fc938014b33 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -427,28 +427,27 @@ def make_payment_request(**args): ) pr = frappe.get_doc("Payment Request", draft_payment_request) else: - if args.order_type != "Shopping Cart": - pr = frappe.new_doc("Payment Request") - pr.update( - { - "payment_gateway_account": gateway_account.get("name"), - "payment_gateway": gateway_account.get("payment_gateway"), - "payment_account": gateway_account.get("payment_account"), - "payment_channel": gateway_account.get("payment_channel"), - "payment_request_type": args.get("payment_request_type"), - "currency": ref_doc.currency, - "grand_total": grand_total, - "mode_of_payment": args.mode_of_payment, - "email_to": args.recipient_id or ref_doc.owner, - "subject": _("Payment Request for {0}").format(args.dn), - "message": gateway_account.get("message") or get_dummy_message(ref_doc), - "reference_doctype": args.dt, - "reference_name": args.dn, - "party_type": args.get("party_type") or "Customer", - "party": args.get("party") or ref_doc.get("customer"), - "bank_account": bank_account, - } - ) + pr = frappe.new_doc("Payment Request") + pr.update( + { + "payment_gateway_account": gateway_account.get("name"), + "payment_gateway": gateway_account.get("payment_gateway"), + "payment_account": gateway_account.get("payment_account"), + "payment_channel": gateway_account.get("payment_channel"), + "payment_request_type": args.get("payment_request_type"), + "currency": ref_doc.currency, + "grand_total": grand_total, + "mode_of_payment": args.mode_of_payment, + "email_to": args.recipient_id or ref_doc.owner, + "subject": _("Payment Request for {0}").format(args.dn), + "message": gateway_account.get("message") or get_dummy_message(ref_doc), + "reference_doctype": args.dt, + "reference_name": args.dn, + "party_type": args.get("party_type") or "Customer", + "party": args.get("party") or ref_doc.get("customer"), + "bank_account": bank_account, + } + ) if args.order_type == "Shopping Cart" or args.mute_email: pr.flags.mute_email = True From ade83cbab3882129970edb94050cd29fdbdf38dc Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 20 Dec 2022 16:45:46 +0530 Subject: [PATCH 35/38] fix: Consolidated financial report --- .../consolidated_financial_statement.py | 8 +++++--- 1 file changed, 5 insertions(+), 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 d269e1f7945..ddee9fc1e17 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -268,10 +268,12 @@ def get_cash_flow_data(fiscal_year, companies, filters): def get_account_type_based_data(account_type, companies, fiscal_year, filters): data = {} total = 0 + filters.account_type = account_type + filters.start_date = fiscal_year.year_start_date + filters.end_date = fiscal_year.year_end_date + for company in companies: - amount = get_account_type_based_gl_data( - company, fiscal_year.year_start_date, fiscal_year.year_end_date, account_type, filters - ) + amount = get_account_type_based_gl_data(company, filters) if amount and account_type == "Depreciation": amount *= -1 From e684eb32d0cf62f67f2b1de30ec7368d36708321 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 21 Dec 2022 12:11:49 +0530 Subject: [PATCH 36/38] fix: typerror on multi warehouse in Packed Items DN(with bundled item with varying warehouses)-> Sales Invoice. --- erpnext/accounts/report/gross_profit/gross_profit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index dacc809da0d..ba947f392f8 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -607,6 +607,7 @@ class GrossProfitGenerator(object): return abs(previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty)) else: return flt(row.qty) * self.get_average_buying_rate(row, item_code) + return 0.0 def get_buying_amount(self, row, item_code): # IMP NOTE From 5918bb03f7db388a27cb9319530b56c383304242 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 21 Dec 2022 12:37:13 +0530 Subject: [PATCH 37/38] test: type error on bundled products with different warehouses --- .../report/gross_profit/test_gross_profit.py | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/erpnext/accounts/report/gross_profit/test_gross_profit.py b/erpnext/accounts/report/gross_profit/test_gross_profit.py index 0ea6b5c8a44..fa11a41df4a 100644 --- a/erpnext/accounts/report/gross_profit/test_gross_profit.py +++ b/erpnext/accounts/report/gross_profit/test_gross_profit.py @@ -6,6 +6,8 @@ from frappe.utils import add_days, flt, nowdate from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.report.gross_profit.gross_profit import execute +from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice +from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry @@ -14,6 +16,7 @@ class TestGrossProfit(FrappeTestCase): def setUp(self): self.create_company() self.create_item() + self.create_bundle() self.create_customer() self.create_sales_invoice() self.clear_old_entries() @@ -42,6 +45,7 @@ class TestGrossProfit(FrappeTestCase): self.company = company.name self.cost_center = company.cost_center self.warehouse = "Stores - " + abbr + self.finished_warehouse = "Finished Goods - " + abbr self.income_account = "Sales - " + abbr self.expense_account = "Cost of Goods Sold - " + abbr self.debit_to = "Debtors - " + abbr @@ -53,6 +57,23 @@ class TestGrossProfit(FrappeTestCase): ) self.item = item if isinstance(item, str) else item.item_code + def create_bundle(self): + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + + item2 = create_item( + item_code="_Test GP Item 2", is_stock_item=1, company=self.company, warehouse=self.warehouse + ) + self.item2 = item2 if isinstance(item2, str) else item2.item_code + + # This will be parent item + bundle = create_item( + item_code="_Test GP bundle", is_stock_item=0, company=self.company, warehouse=self.warehouse + ) + self.bundle = bundle if isinstance(bundle, str) else bundle.item_code + + # Create Product Bundle + self.product_bundle = make_product_bundle(parent=self.bundle, items=[self.item, self.item2]) + def create_customer(self): name = "_Test GP Customer" if frappe.db.exists("Customer", name): @@ -93,6 +114,28 @@ class TestGrossProfit(FrappeTestCase): ) return sinv + def create_delivery_note( + self, item=None, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False + ): + """ + Helper function to populate default values in Delivery Note + """ + dnote = create_delivery_note( + company=self.company, + customer=self.customer, + currency="INR", + item=item or self.item, + qty=qty, + rate=rate, + cost_center=self.cost_center, + warehouse=self.warehouse, + return_against=None, + expense_account=self.expense_account, + do_not_save=do_not_save, + do_not_submit=do_not_submit, + ) + return dnote + def clear_old_entries(self): doctype_list = [ "Sales Invoice", @@ -207,3 +250,55 @@ class TestGrossProfit(FrappeTestCase): } gp_entry = [x for x in data if x.parent_invoice == sinv.name] self.assertDictContainsSubset(expected_entry_with_dn, gp_entry[0]) + + def test_bundled_delivery_note_with_different_warehouses(self): + """ + Test Delivery Note with bundled item. Packed Item from the bundle having different warehouses + """ + se = make_stock_entry( + company=self.company, + item_code=self.item, + target=self.warehouse, + qty=1, + basic_rate=100, + do_not_submit=True, + ) + item = se.items[0] + se.append( + "items", + { + "item_code": self.item2, + "s_warehouse": "", + "t_warehouse": self.finished_warehouse, + "qty": 1, + "basic_rate": 100, + "conversion_factor": item.conversion_factor or 1.0, + "transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0), + "serial_no": item.serial_no, + "batch_no": item.batch_no, + "cost_center": item.cost_center, + "expense_account": item.expense_account, + }, + ) + se = se.save().submit() + + # Make a Delivery note with Product bundle + # Packed Items will have different warehouses + dnote = self.create_delivery_note(item=self.bundle, qty=1, rate=200, do_not_submit=True) + dnote.packed_items[1].warehouse = self.finished_warehouse + dnote = dnote.submit() + + # make Sales Invoice for above delivery note + sinv = make_sales_invoice(dnote.name) + sinv = sinv.save().submit() + + filters = frappe._dict( + company=self.company, + from_date=nowdate(), + to_date=nowdate(), + group_by="Invoice", + sales_invoice=sinv.name, + ) + + columns, data = execute(filters=filters) + self.assertGreater(len(data), 0) From 13c4420f42bb483bbbed7eddf168f4cb62554ab6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 22 Dec 2022 10:55:22 +0530 Subject: [PATCH 38/38] fix: payment terms and sales partner filter issue in AR/AP report --- .../accounts_receivable/accounts_receivable.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index a195c575866..e73d5ba390d 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -794,19 +794,19 @@ class ReceivablePayableReport(object): if self.filters.get("payment_terms_template"): self.qb_selection_filter.append( - self.ple.party_isin( - qb.from_(self.customer).where( - self.customer.payment_terms == self.filters.get("payment_terms_template") - ) + self.ple.party.isin( + qb.from_(self.customer) + .select(self.customer.name) + .where(self.customer.payment_terms == self.filters.get("payment_terms_template")) ) ) if self.filters.get("sales_partner"): self.qb_selection_filter.append( - self.ple.party_isin( - qb.from_(self.customer).where( - self.customer.default_sales_partner == self.filters.get("payment_terms_template") - ) + self.ple.party.isin( + qb.from_(self.customer) + .select(self.customer.name) + .where(self.customer.default_sales_partner == self.filters.get("payment_terms_template")) ) )