From 169ff5a0dd074bc197376d94a977a9ab3f4bb495 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 19 Jun 2022 21:18:12 +0530 Subject: [PATCH 01/26] feat: Cash and Non trade discounts in Sales Invoice --- .../doctype/sales_invoice/sales_invoice.js | 7 +++++++ .../doctype/sales_invoice/sales_invoice.json | 14 ++++++++++---- .../doctype/sales_invoice/sales_invoice.py | 18 +++++++++++++++++- erpnext/controllers/taxes_and_totals.py | 7 +++++++ 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index aefa9a59ddf..cdb187f2185 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -476,6 +476,13 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e this.frm.trigger("calculate_timesheet_totals"); } } + + is_cash_or_non_trade_discount() { + this.frm.set_df_property("additional_discount_account", "hidden", 1 - this.frm.doc.is_cash_or_non_trade_discount); + if (!this.frm.doc.is_cash_or_non_trade_discount) { + this.frm.set_value("additional_discount_account", ""); + } + } }; // for backward compatibility: combine new and previous states diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 327545aa54e..499377d4263 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -106,6 +106,7 @@ "loyalty_redemption_cost_center", "section_break_49", "apply_discount_on", + "is_cash_or_non_trade_discount", "base_discount_amount", "additional_discount_account", "column_break_51", @@ -1790,8 +1791,6 @@ "width": "50%" }, { - "fetch_from": "sales_partner.commission_rate", - "fetch_if_empty": 1, "fieldname": "commission_rate", "fieldtype": "Float", "hide_days": 1, @@ -1990,7 +1989,7 @@ { "fieldname": "additional_discount_account", "fieldtype": "Link", - "label": "Additional Discount Account", + "label": "Discount Account", "options": "Account" }, { @@ -2028,6 +2027,13 @@ "fieldtype": "Currency", "label": "Amount Eligible for Commission", "read_only": 1 + }, + { + "default": "0", + "depends_on": "eval: doc.apply_discount_on == \"Grand Total\"", + "fieldname": "is_cash_or_non_trade_discount", + "fieldtype": "Check", + "label": "Is Cash or Non Trade Discount" } ], "icon": "fa fa-file-text", @@ -2040,7 +2046,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2022-06-10 03:52:51.409913", + "modified": "2022-06-16 16:22:44.870575", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index a580d45accf..2fe6e1b6af1 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1029,7 +1029,7 @@ class SalesInvoice(SellingController): ) if grand_total and not self.is_internal_transfer(): - # Didnot use base_grand_total to book rounding loss gle + # Did not use base_grand_total to book rounding loss gle gl_entries.append( self.get_gl_dict( { @@ -1054,6 +1054,22 @@ class SalesInvoice(SellingController): ) ) + if self.apply_discount_on == "Grand Total" and self.get("is_cash_or_discount_account"): + gl_entries.append( + self.get_gl_dict( + { + "account": self.additional_discount_account, + "against": self.debit_to, + "debit": self.base_discount_amount, + "debit_in_account_currency": self.discount_amount, + "cost_center": self.cost_center, + "project": self.project, + }, + self.currency, + item=self, + ) + ) + def make_tax_gl_entries(self, gl_entries): enable_discount_accounting = cint( frappe.db.get_single_value("Selling Settings", "enable_discount_accounting") diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 2144055b343..a3a054cd29d 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -500,6 +500,9 @@ class calculate_taxes_and_totals(object): else: self.doc.grand_total = flt(self.doc.net_total) + if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"): + self.doc.grand_total -= self.doc.discount_amount + if self.doc.get("taxes"): self.doc.total_taxes_and_charges = flt( self.doc.grand_total - self.doc.net_total - flt(self.doc.rounding_adjustment), @@ -594,6 +597,10 @@ class calculate_taxes_and_totals(object): if not self.doc.apply_discount_on: frappe.throw(_("Please select Apply Discount On")) + if self.doc.apply_discount_on == "Grand Total" and self.doc.is_cash_or_non_trade_discount: + self.discount_amount_applied = True + return + self.doc.base_discount_amount = flt( self.doc.discount_amount * self.doc.conversion_rate, self.doc.precision("base_discount_amount") ) From f337213f33384aea9d80ef97d96d0097d436bd68 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 19 Jun 2022 21:19:02 +0530 Subject: [PATCH 02/26] fix(India): Discounts in E-Invoicing --- erpnext/regional/india/e_invoice/utils.py | 27 ++++++++++++++++------- erpnext/regional/india/utils.py | 12 ++++++++-- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 5eb14a5ddd3..44ba6351e7e 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -271,14 +271,18 @@ def get_item_list(invoice): item.description = sanitize_for_json(d.item_name) item.qty = abs(item.qty) - if flt(item.qty) != 0.0: - item.unit_rate = abs(item.taxable_value / item.qty) - else: - item.unit_rate = abs(item.taxable_value) - item.gross_amount = abs(item.taxable_value) - item.taxable_value = abs(item.taxable_value) - item.discount_amount = 0 + if invoice.get("apply_discount_on"): + item.discount_amount = item.base_amount - item.base_net_amount + elif item.discount_amount > 0: + item.discount_amount = item.discount_amount + else: + item.discount_amount = 0 + + item.unit_rate = abs(item.taxable_value - item.discount_amount) / item.qty + + item.gross_amount = abs(item.taxable_value) + item.discount_amount + item.taxable_value = abs(item.taxable_value) item.is_service_item = "Y" if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else "N" item.serial_no = "" @@ -352,7 +356,14 @@ def update_item_taxes(invoice, item): def get_invoice_value_details(invoice): invoice_value_details = frappe._dict(dict()) invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get("items")])) - invoice_value_details.invoice_discount_amt = 0 + if ( + invoice.apply_discount_on == "Grand Total" + and invoice.discount_amount + and invoice.get("is_cash_or_non_trade_discount") + ): + invoice_value_details.invoice_discount_amt = invoice.base_discount_amount + else: + invoice_value_details.invoice_discount_amt = 0 invoice_value_details.round_off = invoice.base_rounding_adjustment invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs( diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 02624695710..f1586fc3dc1 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -1060,8 +1060,16 @@ def update_taxable_values(doc, method): considered_rows.append(prev_row_id) 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 + if ( + doc.apply_discount_on == "Grand Total" + and doc.discount_amount + and doc.get("is_cash_or_non_trade_discount") + ): + proportionate_value = item.base_amount if doc.base_total else item.qty + total_value = doc.base_total if doc.base_total else doc.total_qty + else: + 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( From e2295b4e2fad0fdfb1afefa66b4e63d924c1434a Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 20 Jun 2022 18:52:40 +0200 Subject: [PATCH 03/26] fix: apply price list rate --- erpnext/public/js/controllers/transaction.js | 50 ++++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 01f72adf349..1f86718990e 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1483,48 +1483,46 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } _set_values_for_item_list(children) { - var me = this; - var items_rule_dict = {}; + const items_rule_dict = {}; - for(var i=0, l=children.length; i 0) { - me.apply_product_discount(d); + if (child.free_item_data.length > 0) { + this.apply_product_discount(child); } - if (d.apply_rule_on_other_items) { - items_rule_dict[d.name] = d; + if (child.apply_rule_on_other_items) { + items_rule_dict[child.name] = child; } } - me.frm.refresh_field('items'); - me.apply_rule_on_other_items(items_rule_dict); - - me.calculate_taxes_and_totals(); + this.apply_rule_on_other_items(items_rule_dict); + this.calculate_taxes_and_totals(); } apply_rule_on_other_items(args) { From 58fe220479d18e8b974f438b7ebfe0a2753ae5bd Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 24 Jun 2022 19:43:50 +0530 Subject: [PATCH 04/26] fix: Quotation and Sales Order item sync --- erpnext/selling/doctype/quotation/quotation.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 4fa4515a0f5..d775fa93be9 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -218,6 +218,15 @@ def make_sales_order(source_name, target_doc=None): def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): customer = _make_customer(source_name, ignore_permissions) + ordered_items = frappe._dict( + frappe.db.get_all( + "Sales Order Item", + {"prevdoc_docname": source_name, "docstatus": 1}, + ["item_code", "sum(qty)"], + group_by="item_code", + as_list=1, + ) + ) def set_missing_values(source, target): if customer: @@ -233,7 +242,9 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): target.run_method("calculate_taxes_and_totals") def update_item(obj, target, source_parent): - target.stock_qty = flt(obj.qty) * flt(obj.conversion_factor) + balance_qty = obj.qty - ordered_items.get(obj.item_code, 0.0) + target.qty = balance_qty if balance_qty > 0 else 0 + target.stock_qty = flt(target.qty) * flt(obj.conversion_factor) if obj.against_blanket_order: target.against_blanket_order = obj.against_blanket_order @@ -249,6 +260,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): "doctype": "Sales Order Item", "field_map": {"parent": "prevdoc_docname"}, "postprocess": update_item, + "condition": lambda doc: doc.qty > 0, }, "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, From 20dac08f5f729081e8fae2e60b3b3b95bbfda4d9 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 27 Jun 2022 15:54:54 +0530 Subject: [PATCH 05/26] refactor: clean up product bundle client side code (#31455) refactor: clean up product bundle cient side code - Remove deprecated CUR_FRM scripts - Remove client side fetches and move it to doctype schema --- .../doctype/product_bundle/product_bundle.js | 28 ++++++++----------- .../product_bundle_item.json | 7 ++++- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.js b/erpnext/selling/doctype/product_bundle/product_bundle.js index 7a04c6ab06d..3096b692a7e 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.js +++ b/erpnext/selling/doctype/product_bundle/product_bundle.js @@ -1,19 +1,13 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -cur_frm.cscript.refresh = function(doc, cdt, cdn) { - cur_frm.toggle_enable('new_item_code', doc.__islocal); -} - -cur_frm.fields_dict.new_item_code.get_query = function() { - return{ - query: "erpnext.selling.doctype.product_bundle.product_bundle.get_new_item_code" - } -} -cur_frm.fields_dict.new_item_code.query_description = __('Please select Item where "Is Stock Item" is "No" and "Is Sales Item" is "Yes" and there is no other Product Bundle'); - -cur_frm.cscript.onload = function() { - // set add fetch for item_code's item_name and description - cur_frm.add_fetch('item_code', 'stock_uom', 'uom'); - cur_frm.add_fetch('item_code', 'description', 'description'); -} +frappe.ui.form.on("Product Bundle", { + refresh: function (frm) { + frm.toggle_enable("new_item_code", frm.is_new()); + frm.set_query("new_item_code", () => { + return { + query: "erpnext.selling.doctype.product_bundle.product_bundle.get_new_item_code", + }; + }); + }, +}); diff --git a/erpnext/selling/doctype/product_bundle_item/product_bundle_item.json b/erpnext/selling/doctype/product_bundle_item/product_bundle_item.json index dc071e4d65e..fc8caeb31d9 100644 --- a/erpnext/selling/doctype/product_bundle_item/product_bundle_item.json +++ b/erpnext/selling/doctype/product_bundle_item/product_bundle_item.json @@ -33,6 +33,8 @@ "reqd": 1 }, { + "fetch_from": "item_code.description", + "fetch_if_empty": 1, "fieldname": "description", "fieldtype": "Text Editor", "in_list_view": 1, @@ -51,6 +53,8 @@ "print_hide": 1 }, { + "fetch_from": "item_code.stock_uom", + "fetch_if_empty": 1, "fieldname": "uom", "fieldtype": "Link", "in_list_view": 1, @@ -64,7 +68,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-02-28 14:06:05.725655", + "modified": "2022-06-27 05:30:18.475150", "modified_by": "Administrator", "module": "Selling", "name": "Product Bundle Item", @@ -72,5 +76,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From dd11f26eba45937b809047404ad453b8df2670ac Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 27 Jun 2022 15:55:08 +0530 Subject: [PATCH 06/26] fix: dont update RM items table if not required (#31408) Currently on PO update RM item table is auto computed again and again, if there was any transfer/consumption against that then it will be lost. This change: 1. Disables updating RM table if no change in qty of FG was made. Since RM table can't possibly be different with same FG qty. 2. Blocks update completely if qty is changed and RM items are already transferred. --- .../purchase_order/test_purchase_order.py | 37 +++++++++++++++++ erpnext/controllers/accounts_controller.py | 40 +++++++++++++++++-- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index d732b755fef..5f84de60d06 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -140,6 +140,43 @@ class TestPurchaseOrder(FrappeTestCase): # ordered qty decreases as ordered qty is 0 (deleted row) self.assertEqual(get_ordered_qty(), existing_ordered_qty - 10) # 0 + def test_supplied_items_validations_on_po_update_after_submit(self): + po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1, qty=5, rate=100) + item = po.items[0] + + original_supplied_items = {po.name: po.required_qty for po in po.supplied_items} + + # Just update rate + trans_item = [ + { + "item_code": "_Test FG Item", + "rate": 20, + "qty": 5, + "conversion_factor": 1.0, + "docname": item.name, + } + ] + update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name) + po.reload() + + new_supplied_items = {po.name: po.required_qty for po in po.supplied_items} + self.assertEqual(set(original_supplied_items.keys()), set(new_supplied_items.keys())) + + # Update qty to 2x + trans_item[0]["qty"] *= 2 + update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name) + po.reload() + + new_supplied_items = {po.name: po.required_qty for po in po.supplied_items} + self.assertEqual(2 * sum(original_supplied_items.values()), sum(new_supplied_items.values())) + + # Set transfer qty and attempt to update qty, shouldn't be allowed + po.supplied_items[0].supplied_qty = 2 + po.supplied_items[0].db_update() + trans_item[0]["qty"] *= 2 + with self.assertRaises(frappe.ValidationError): + update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name) + def test_update_child(self): mr = make_material_request(qty=10) po = make_purchase_order(mr.name) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index fc6fdcdeff6..ceac815bf4a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2440,7 +2440,7 @@ def update_bin_on_delete(row, doctype): update_bin_qty(row.item_code, row.warehouse, qty_dict) -def validate_and_delete_children(parent, data): +def validate_and_delete_children(parent, data) -> bool: deleted_children = [] updated_item_names = [d.get("docname") for d in data] for item in parent.items: @@ -2459,6 +2459,8 @@ def validate_and_delete_children(parent, data): for d in deleted_children: update_bin_on_delete(d, parent.doctype) + return bool(deleted_children) + @frappe.whitelist() def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"): @@ -2522,13 +2524,38 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil ): frappe.throw(_("Cannot set quantity less than received quantity")) + def should_update_supplied_items(doc) -> bool: + """Subcontracted PO can allow following changes *after submit*: + + 1. Change rate of subcontracting - regardless of other changes. + 2. Change qty and/or add new items and/or remove items + Exception: Transfer/Consumption is already made, qty change not allowed. + """ + + supplied_items_processed = any( + item.supplied_qty or item.consumed_qty or item.returned_qty for item in doc.supplied_items + ) + + update_supplied_items = ( + any_qty_changed or items_added_or_removed or any_conversion_factor_changed + ) + if update_supplied_items and supplied_items_processed: + frappe.throw(_("Item qty can not be updated as raw materials are already processed.")) + + return update_supplied_items + data = json.loads(trans_items) + any_qty_changed = False # updated to true if any item's qty changes + items_added_or_removed = False # updated to true if any new item is added or removed + any_conversion_factor_changed = False + sales_doctypes = ["Sales Order", "Sales Invoice", "Delivery Note", "Quotation"] parent = frappe.get_doc(parent_doctype, parent_doctype_name) check_doc_permissions(parent, "write") - validate_and_delete_children(parent, data) + _removed_items = validate_and_delete_children(parent, data) + items_added_or_removed |= _removed_items for d in data: new_child_flag = False @@ -2539,6 +2566,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil if not d.get("docname"): new_child_flag = True + items_added_or_removed = True check_doc_permissions(parent, "create") child_item = get_new_child_item(d) else: @@ -2561,6 +2589,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil qty_unchanged = prev_qty == new_qty uom_unchanged = prev_uom == new_uom conversion_factor_unchanged = prev_con_fac == new_con_fac + any_conversion_factor_changed |= not conversion_factor_unchanged date_unchanged = ( prev_date == getdate(new_date) if prev_date and new_date else False ) # in case of delivery note etc @@ -2574,6 +2603,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil continue validate_quantity(child_item, d) + if flt(child_item.get("qty")) != flt(d.get("qty")): + any_qty_changed = True child_item.qty = flt(d.get("qty")) rate_precision = child_item.precision("rate") or 2 @@ -2679,8 +2710,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil parent.update_ordered_and_reserved_qty() parent.update_receiving_percentage() if parent.is_subcontracted: - parent.update_reserved_qty_for_subcontract() - parent.create_raw_materials_supplied("supplied_items") + if should_update_supplied_items(parent): + parent.update_reserved_qty_for_subcontract() + parent.create_raw_materials_supplied("supplied_items") parent.save() else: # Sales Order parent.validate_warehouse() From 3cd34ebab65a030aae2b42d0fa0a5cbee934f1f6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 21 Jun 2022 11:15:19 +0530 Subject: [PATCH 07/26] fix: 'attribute error' on AR/AP report with delivery note filter --- .../accounts/report/accounts_receivable/accounts_receivable.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 1911152dec1..411b31371b2 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -128,6 +128,7 @@ class ReceivablePayableReport(object): credit_note_in_account_currency=0.0, outstanding_in_account_currency=0.0, ) + self.get_invoices(ple) if self.filters.get("group_by_party"): self.init_subtotal_row(ple.party) From 7921a1a60594deebc15208de1cad01ff5409ff94 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 27 Jun 2022 21:57:03 +0530 Subject: [PATCH 08/26] fix: Restored city, state and country fields --- erpnext/crm/doctype/lead/lead.json | 31 ++++++++++++++++--- .../crm/doctype/opportunity/opportunity.json | 30 ++++++++++++++---- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json index 9216c458c87..d47373fa612 100644 --- a/erpnext/crm/doctype/lead/lead.json +++ b/erpnext/crm/doctype/lead/lead.json @@ -46,6 +46,10 @@ "fax", "address_section", "address_html", + "column_break_38", + "city", + "state", + "country", "column_break2", "contact_html", "qualification_tab", @@ -333,9 +337,8 @@ }, { "fieldname": "no_of_employees", - "fieldtype": "Select", - "label": "No. of Employees", - "options": "1-10\n11-20\n21-30\n31-100\n11-50\n51-200\n201-500\n101-500\n500-1000\n501-1000\n>1000\n1000+" + "fieldtype": "Int", + "label": "No. of Employees" }, { "fieldname": "column_break_22", @@ -477,13 +480,33 @@ "fieldname": "disabled", "fieldtype": "Check", "label": "Disabled" + }, + { + "fieldname": "column_break_38", + "fieldtype": "Column Break" + }, + { + "fieldname": "city", + "fieldtype": "Data", + "label": "City" + }, + { + "fieldname": "state", + "fieldtype": "Data", + "label": "State" + }, + { + "fieldname": "country", + "fieldtype": "Link", + "label": "Country", + "options": "Country" } ], "icon": "fa fa-user", "idx": 5, "image_field": "image", "links": [], - "modified": "2022-06-21 15:10:06.613519", + "modified": "2022-06-27 21:56:17.392756", "modified_by": "Administrator", "module": "CRM", "name": "Lead", diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json index 8ddd4e36c2f..1a6f23bc7b2 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.json +++ b/erpnext/crm/doctype/opportunity/opportunity.json @@ -32,9 +32,12 @@ "column_break_23", "industry", "market_segment", - "column_break_31", - "territory", "website", + "column_break_31", + "city", + "state", + "country", + "territory", "section_break_14", "currency", "column_break_36", @@ -463,9 +466,8 @@ }, { "fieldname": "no_of_employees", - "fieldtype": "Select", - "label": "No of Employees", - "options": "1-10\n11-20\n21-30\n31-100\n11-50\n51-200\n201-500\n101-500\n500-1000\n501-1000\n>1000\n1000+" + "fieldtype": "Int", + "label": "No of Employees" }, { "fieldname": "annual_revenue", @@ -603,12 +605,28 @@ "label": "Notes", "no_copy": 1, "options": "CRM Note" + }, + { + "fieldname": "city", + "fieldtype": "Data", + "label": "City" + }, + { + "fieldname": "state", + "fieldtype": "Data", + "label": "State" + }, + { + "fieldname": "country", + "fieldtype": "Link", + "label": "Country", + "options": "Country" } ], "icon": "fa fa-info-sign", "idx": 195, "links": [], - "modified": "2022-06-21 15:04:34.363959", + "modified": "2022-06-27 18:44:32.858696", "modified_by": "Administrator", "module": "CRM", "name": "Opportunity", From 925b9d985e8f614d5a4de5f20218a7fca59f51ac Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 27 Jun 2022 21:58:19 +0530 Subject: [PATCH 09/26] fix: open lead and opportunities based on today's event --- erpnext/crm/utils.py | 24 +++++++++++++++++++++++- erpnext/hooks.py | 1 + 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/utils.py b/erpnext/crm/utils.py index 33441b166db..a2528c37034 100644 --- a/erpnext/crm/utils.py +++ b/erpnext/crm/utils.py @@ -1,6 +1,7 @@ import frappe from frappe.model.document import Document -from frappe.utils import cstr, now +from frappe.utils import cstr, now, today +from pypika import functions def update_lead_phone_numbers(contact, method): @@ -177,6 +178,27 @@ def get_open_events(ref_doctype, ref_docname): return data +def open_leads_opportunities_based_on_todays_event(): + event = frappe.qb.DocType("Event") + event_link = frappe.qb.DocType("Event Participants") + + query = ( + frappe.qb.from_(event) + .join(event_link) + .on(event_link.parent == event.name) + .select(event_link.reference_doctype, event_link.reference_docname) + .where( + (event_link.reference_doctype.isin(["Lead", "Opportunity"])) + & (event.status == "Open") + & (functions.Date(event.starts_on) == today()) + ) + ) + data = query.run(as_dict=True) + + for d in data: + frappe.db.set_value(d.reference_doctype, d.reference_docname, "status", "Open") + + class CRMNote(Document): @frappe.whitelist() def add_note(self, note): diff --git a/erpnext/hooks.py b/erpnext/hooks.py index b3c35cfe0bf..8abf65f4b5e 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -457,6 +457,7 @@ scheduler_events = { "erpnext.hr.utils.allocate_earned_leaves", "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", ], "weekly": ["erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_weekly"], "monthly": ["erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_monthly"], From bedb11ee670e3f19b3c6524ece78be5aba66d815 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 28 Jun 2022 10:50:44 +0530 Subject: [PATCH 10/26] fix: youtube stats background sync failures --- erpnext/utilities/doctype/video/video.py | 25 +++++++----------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/erpnext/utilities/doctype/video/video.py b/erpnext/utilities/doctype/video/video.py index a39d0a95ebf..15dbccde89d 100644 --- a/erpnext/utilities/doctype/video/video.py +++ b/erpnext/utilities/doctype/video/video.py @@ -9,6 +9,7 @@ import frappe import pytz from frappe import _ from frappe.model.document import Document +from frappe.utils import cint from pyyoutube import Api @@ -46,7 +47,7 @@ def is_tracking_enabled(): def get_frequency(value): # Return numeric value from frequency field, return 1 as fallback default value: 1 hour if value != "Daily": - return frappe.utils.cint(value[:2].strip()) + return cint(value[:2].strip()) elif value: return 24 return 1 @@ -120,24 +121,12 @@ def batch_update_youtube_data(): 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": cint(video_stats.get("likeCount")), + "view_count": cint(video_stats.get("viewCount")), + "dislike_count": cint(video_stats.get("dislikeCount")), + "comment_count": cint(video_stats.get("commentCount")), } - - 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, - ) + frappe.db.set_value("Video", video_id, stats) video_list = frappe.get_all("Video", fields=["youtube_video_id"]) if len(video_list) > 50: From 5d73697c647d5aeadd1b0738c1be8409a3ef7337 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 28 Jun 2022 12:22:17 +0530 Subject: [PATCH 11/26] fix: offset some scheduled jobs to avoid locks (#31466) If your site has multiple background workers then there's possibility that two jobs will execute in parallal, this creates problem when both are on operating on same data. This PR adds a separate section for hourly and daily jobs which have frequency offset from default frequency to avoid such conflicts. --- erpnext/hooks.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 8abf65f4b5e..816cb626449 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -402,6 +402,14 @@ scheduler_events = { "0/30 * * * *": [ "erpnext.utilities.doctype.video.video.update_youtube_data", ], + # Hourly but offset by 30 minutes + "30 * * * *": [ + "erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs", + ], + # Daily but offset by 45 minutes + "45 0 * * *": [ + "erpnext.stock.reorder_item.reorder_item", + ], }, "all": [ "erpnext.projects.doctype.project.project.project_status_update_reminder", @@ -411,7 +419,6 @@ scheduler_events = { "hourly": [ "erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails", "erpnext.accounts.doctype.subscription.subscription.process_all", - "erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs", "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization", "erpnext.projects.doctype.project.project.hourly_reminder", "erpnext.projects.doctype.project.project.collect_project_status", @@ -422,7 +429,6 @@ scheduler_events = { "erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction", ], "daily": [ - "erpnext.stock.reorder_item.reorder_item", "erpnext.support.doctype.issue.issue.auto_close_tickets", "erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity", "erpnext.controllers.accounts_controller.update_invoice_status", From 080fcb91f25af9f4174183b4fe662049ff710e0c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 28 Jun 2022 13:46:12 +0530 Subject: [PATCH 12/26] ci: pin semgrep to old version current version has problem with PRs originating from fork --- .github/workflows/linters.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index ebb88c9edac..af6d8f26a73 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -11,10 +11,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.8 + - name: Set up Python 3.10 uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: '3.10' - name: Install and Run Pre-commit uses: pre-commit/action@v2.0.3 @@ -22,10 +22,8 @@ jobs: - name: Download Semgrep rules run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules - - uses: returntocorp/semgrep-action@v1 - env: - SEMGREP_TIMEOUT: 120 - with: - config: >- - r/python.lang.correctness - ./frappe-semgrep-rules/rules + - name: Download semgrep + run: pip install semgrep==0.97.0 + + - name: Run Semgrep rules + run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness From cdf631b9de4b0351c4dc2613e1709e0d464ea69d Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Wed, 29 Jun 2022 02:36:47 -0400 Subject: [PATCH 13/26] fix: validate item dashboard material transfer entry (#31473) --- erpnext/stock/dashboard/item_dashboard.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js index c9d5f61f22b..6e7622c067f 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -260,6 +260,16 @@ erpnext.stock.move_item = function (item, source, target, actual_qty, rate, call } dialog.set_primary_action(__('Create Stock Entry'), function () { + if (source && (dialog.get_value("qty") == 0 || dialog.get_value("qty") > actual_qty)) { + frappe.msgprint(__("Quantity must be greater than zero, and less or equal to {0}", [actual_qty])); + return; + } + + if (dialog.get_value("source") === dialog.get_value("target")) { + frappe.msgprint(__("Source and target warehouse must be different")); + return; + } + frappe.model.with_doctype('Stock Entry', function () { let doc = frappe.model.get_new_doc('Stock Entry'); doc.from_warehouse = dialog.get_value('source'); From 2a619fd7890d542f9c6fa28594c0dae97cfb683a Mon Sep 17 00:00:00 2001 From: gn306029 Date: Wed, 29 Jun 2022 17:15:21 +0800 Subject: [PATCH 14/26] fix: Modify opts parameter misspell (#31476) Modify opts parameter misspell closes #31474 --- erpnext/buying/doctype/purchase_order/purchase_order.js | 2 +- erpnext/public/js/utils.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index da45610eaff..33dbe3f4683 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -425,7 +425,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e company: me.frm.doc.company }, allow_child_item_selection: true, - child_fielname: "items", + child_fieldname: "items", child_columns: ["item_code", "qty"] }) }, __("Get Items From")); diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 01710f1e41a..096175a68d1 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -713,7 +713,7 @@ erpnext.utils.map_current_doc = function(opts) { get_query: opts.get_query, add_filters_group: 1, allow_child_item_selection: opts.allow_child_item_selection, - child_fieldname: opts.child_fielname, + child_fieldname: opts.child_fieldname, child_columns: opts.child_columns, size: opts.size, action: function(selections, args) { From 8a13ddc2f2be2e4ad465e85d41e76f78167cd71f Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Wed, 29 Jun 2022 12:28:24 -0400 Subject: [PATCH 15/26] fix: gain/loss can be income or expense --- erpnext/setup/doctype/company/company.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 0de5b2d5a32..3dec303a926 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -219,8 +219,8 @@ erpnext.company.setup_queries = function(frm) { ["default_discount_account", {}], ["discount_allowed_account", {"root_type": "Expense"}], ["discount_received_account", {"root_type": "Income"}], - ["exchange_gain_loss_account", {"root_type": "Expense"}], - ["unrealized_exchange_gain_loss_account", {"root_type": "Expense"}], + ["exchange_gain_loss_account", {"root_type": ["in", ["Expense", "Income"]]}], + ["unrealized_exchange_gain_loss_account", {"root_type": ["in", ["Expense", "Income"]]}], ["accumulated_depreciation_account", {"root_type": "Asset", "account_type": "Accumulated Depreciation"}], ["depreciation_expense_account", {"root_type": "Expense", "account_type": "Depreciation"}], From 56c6a709cdc8f50b6a1b49b4b58685de1fd64eee Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 30 Jun 2022 11:35:45 +0530 Subject: [PATCH 16/26] ci: bump container count for unittests (#31490) [skip ci] --- .github/workflows/server-tests-mariadb.yml | 2 +- .github/workflows/server-tests-postgres.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml index cdb68499ffd..91a01140048 100644 --- a/.github/workflows/server-tests-mariadb.yml +++ b/.github/workflows/server-tests-mariadb.yml @@ -39,7 +39,7 @@ jobs: fail-fast: false matrix: - container: [1, 2, 3] + container: [1, 2, 3, 4] name: Python Unit Tests diff --git a/.github/workflows/server-tests-postgres.yml b/.github/workflows/server-tests-postgres.yml index 77d3c1ae61b..d3268e25b39 100644 --- a/.github/workflows/server-tests-postgres.yml +++ b/.github/workflows/server-tests-postgres.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - container: [1, 2, 3] + container: [1] name: Python Unit Tests From 20f85195f45f5b311e9ba02d82d08f0036928809 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 29 Jun 2022 12:23:17 +0530 Subject: [PATCH 17/26] build!: declarative builds --- .github/workflows/patch.yml | 2 +- .github/workflows/server-tests-mariadb.yml | 2 +- .github/workflows/server-tests-postgres.yml | 2 +- CODEOWNERS | 2 +- pyproject.toml | 29 +++++++++++++++++++++ requirements.txt | 11 -------- setup.py | 25 +++--------------- 7 files changed, 37 insertions(+), 36 deletions(-) delete mode 100644 requirements.txt diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index 2cf44444cc3..4d8c51d9429 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -52,7 +52,7 @@ jobs: uses: actions/cache@v2 with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }} restore-keys: | ${{ runner.os }}-pip- ${{ runner.os }}- diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml index 91a01140048..9dcc6560f9b 100644 --- a/.github/workflows/server-tests-mariadb.yml +++ b/.github/workflows/server-tests-mariadb.yml @@ -74,7 +74,7 @@ jobs: uses: actions/cache@v2 with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }} restore-keys: | ${{ runner.os }}-pip- ${{ runner.os }}- diff --git a/.github/workflows/server-tests-postgres.yml b/.github/workflows/server-tests-postgres.yml index d3268e25b39..f62c22b0df9 100644 --- a/.github/workflows/server-tests-postgres.yml +++ b/.github/workflows/server-tests-postgres.yml @@ -61,7 +61,7 @@ jobs: uses: actions/cache@v2 with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }} restore-keys: | ${{ runner.os }}-pip- ${{ runner.os }}- diff --git a/CODEOWNERS b/CODEOWNERS index bfc26010882..f0cb37919ad 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -32,4 +32,4 @@ erpnext/patches/ @deepeshgarg007 @nextchamp-saqib @marination @an erpnext/public/ @nextchamp-saqib @marination .github/ @ankush -requirements.txt @gavindsouza +pyproject.toml @gavindsouza @ankush diff --git a/pyproject.toml b/pyproject.toml index 8043dd99065..b39c4f072f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,32 @@ +[project] +name = "erpnext" +authors = [ + { name = "Frappe Technologies Pvt Ltd", email = "developers@frappe.io"} +] +description = "Open Source ERP" +requires-python = ">=3.8" +readme = "README.md" +dynamic = ["version"] +dependencies = [ + # Core dependencies + "pycountry~=20.7.3", + "python-stdnum~=1.16", + "Unidecode~=1.2.0", + "redisearch~=2.1.0", + + # integration dependencies + "gocardless-pro~=1.22.0", + "googlemaps", + "plaid-python~=7.2.1", + "python-youtube~=0.8.0", + "taxjar~=1.9.2", + "tweepy~=3.10.0", +] + +[build-system] +requires = ["flit_core >=3.4,<4"] +build-backend = "flit_core.buildapi" + [tool.black] line-length = 99 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 83e53758be2..00000000000 --- a/requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -# frappe # https://github.com/frappe/frappe is installed during bench-init -gocardless-pro~=1.22.0 -googlemaps -plaid-python~=7.2.1 -pycountry~=20.7.3 -python-stdnum~=1.16 -python-youtube~=0.8.0 -taxjar~=1.9.2 -tweepy~=3.10.0 -Unidecode~=1.2.0 -redisearch~=2.1.0 diff --git a/setup.py b/setup.py index 1faff0412f8..0ea4d073488 100644 --- a/setup.py +++ b/setup.py @@ -1,23 +1,6 @@ -from setuptools import setup, find_packages -import re, ast +# TODO: Remove this file when v15.0.0 is released +from setuptools import setup -# get version from __version__ variable in erpnext/__init__.py -_version_re = re.compile(r"__version__\s+=\s+(.*)") +name = "frappe" -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))) - -setup( - name="erpnext", - version=version, - 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, -) +setup() From 0e3872aceba70aa00941b535ec1a688b64c6fd3e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 29 Jun 2022 12:15:05 +0530 Subject: [PATCH 18/26] chore(meta): update CODEOWNERS Co-authored-by: Nabin Hait --- CODEOWNERS | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index f0cb37919ad..ecbae86d96b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -3,32 +3,29 @@ # These owners will be the default owners for everything in # the repo. Unless a later match takes precedence, -erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 -erpnext/assets/ @nextchamp-saqib @deepeshgarg007 -erpnext/erpnext_integrations/ @nextchamp-saqib +erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar +erpnext/assets/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007 -erpnext/regional @nextchamp-saqib @deepeshgarg007 -erpnext/selling @nextchamp-saqib @deepeshgarg007 +erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar +erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar erpnext/support/ @nextchamp-saqib @deepeshgarg007 pos* @nextchamp-saqib -erpnext/buying/ @marination @rohitwaghchaure @ankush +erpnext/buying/ @marination @rohitwaghchaure @s-aga-r erpnext/e_commerce/ @marination -erpnext/maintenance/ @marination @rohitwaghchaure -erpnext/manufacturing/ @marination @rohitwaghchaure @ankush +erpnext/maintenance/ @marination @rohitwaghchaure @s-aga-r +erpnext/manufacturing/ @marination @rohitwaghchaure @s-aga-r erpnext/portal/ @marination -erpnext/quality_management/ @marination @rohitwaghchaure +erpnext/quality_management/ @marination @rohitwaghchaure @s-aga-r erpnext/shopping_cart/ @marination -erpnext/stock/ @marination @rohitwaghchaure @ankush +erpnext/stock/ @marination @rohitwaghchaure @s-aga-r -erpnext/crm/ @ruchamahabal @pateljannat -erpnext/education/ @ruchamahabal @pateljannat -erpnext/hr/ @ruchamahabal @pateljannat -erpnext/payroll @ruchamahabal @pateljannat -erpnext/projects/ @ruchamahabal @pateljannat +erpnext/crm/ @NagariaHussain +erpnext/education/ @rutwikhdev +erpnext/projects/ @ruchamahabal -erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination @ankush -erpnext/patches/ @deepeshgarg007 @nextchamp-saqib @marination @ankush +erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination +erpnext/patches/ @deepeshgarg007 @nextchamp-saqib @marination erpnext/public/ @nextchamp-saqib @marination .github/ @ankush From cd0450b102367e1a5bb19561661049cced3f7727 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 29 Jun 2022 19:24:52 +0530 Subject: [PATCH 19/26] refactor: move dev-dependencies to pyproject --- dev-requirements.txt | 1 - pyproject.toml | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) delete mode 100644 dev-requirements.txt diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index 15545c0efa2..00000000000 --- a/dev-requirements.txt +++ /dev/null @@ -1 +0,0 @@ -hypothesis~=6.31.0 diff --git a/pyproject.toml b/pyproject.toml index b39c4f072f8..d446c9b39ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,9 @@ dependencies = [ requires = ["flit_core >=3.4,<4"] build-backend = "flit_core.buildapi" +[tool.bench.dev-dependencies] +hypothesis = "~=6.31.0" + [tool.black] line-length = 99 From 57d08b7cdf13f7c2c48098d3eb91004913d04f3e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 30 Jun 2022 15:49:43 +0530 Subject: [PATCH 20/26] build!: bump min python required to 3.10 --- .github/workflows/docs-checker.yml | 2 +- .github/workflows/patch.yml | 2 +- .github/workflows/server-tests-mariadb.yml | 2 +- .github/workflows/server-tests-postgres.yml | 2 +- pyproject.toml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docs-checker.yml b/.github/workflows/docs-checker.yml index b644568d5ea..722c1252ed9 100644 --- a/.github/workflows/docs-checker.yml +++ b/.github/workflows/docs-checker.yml @@ -12,7 +12,7 @@ jobs: - name: 'Setup Environment' uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: '3.10' - name: 'Clone repo' uses: actions/checkout@v2 diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index 4d8c51d9429..4e00870cab1 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -37,7 +37,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: '3.10' - name: Setup Node uses: actions/setup-node@v2 diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml index 9dcc6560f9b..f65cb460f19 100644 --- a/.github/workflows/server-tests-mariadb.yml +++ b/.github/workflows/server-tests-mariadb.yml @@ -59,7 +59,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: '3.10' - name: Setup Node uses: actions/setup-node@v2 diff --git a/.github/workflows/server-tests-postgres.yml b/.github/workflows/server-tests-postgres.yml index f62c22b0df9..53a94dbfacb 100644 --- a/.github/workflows/server-tests-postgres.yml +++ b/.github/workflows/server-tests-postgres.yml @@ -46,7 +46,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: '3.10' - name: Setup Node uses: actions/setup-node@v2 diff --git a/pyproject.toml b/pyproject.toml index d446c9b39ef..5acfd392726 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ authors = [ { name = "Frappe Technologies Pvt Ltd", email = "developers@frappe.io"} ] description = "Open Source ERP" -requires-python = ">=3.8" +requires-python = ">=3.10" readme = "README.md" dynamic = ["version"] dependencies = [ From b9f394a794cecde36d95740f812afe985c4adc9d Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 30 Jun 2022 16:20:59 +0530 Subject: [PATCH 21/26] ci: patch test w/ diff python versions Co-Authored-By: Gavin D'souza --- .github/workflows/patch.yml | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index 4e00870cab1..a71db7289ae 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -35,9 +35,9 @@ jobs: uses: actions/checkout@v2 - name: Setup Python - uses: actions/setup-python@v2 + uses: "gabrielfalcao/pyenv-action@v9" with: - python-version: '3.10' + versions: 3.10:latest, 3.7:latest - name: Setup Node uses: actions/setup-node@v2 @@ -82,7 +82,10 @@ jobs: ${{ runner.os }}-yarn- - name: Install - run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh + run: | + pip install frappe-bench + pyenv global $(pyenv versions | grep '3.10') + bash ${GITHUB_WORKSPACE}/.github/helper/install.sh env: DB: mariadb TYPE: server @@ -96,18 +99,23 @@ jobs: git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git + pyenv global $(pyenv versions | grep '3.7') for version in $(seq 12 13) do echo "Updating to v$version" branch_name="version-$version-hotfix" + git -C "apps/frappe" fetch --depth 1 upstream $branch_name:$branch_name git -C "apps/erpnext" fetch --depth 1 upstream $branch_name:$branch_name git -C "apps/frappe" checkout -q -f $branch_name git -C "apps/erpnext" checkout -q -f $branch_name - bench setup requirements --python + rm -rf ~/frappe-bench/env + bench setup env + bench pip install -e ./apps/erpnext + bench --site test_site migrate done @@ -115,5 +123,10 @@ jobs: echo "Updating to latest version" git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}" git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA" - bench setup requirements --python + + pyenv global $(pyenv versions | grep '3.10') + rm -rf ~/frappe-bench/env + bench -v setup env + bench pip install -e ./apps/erpnext + bench --site test_site migrate From 536e768ba96a20c519f19dfd4c6b65acedd03aef Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 30 Jun 2022 21:29:54 +0530 Subject: [PATCH 22/26] fix: Internal PI link in Sales Invoice --- .../doctype/sales_invoice/sales_invoice.py | 29 +++++++++++++++++++ .../sales_invoice/sales_invoice_dashboard.py | 2 ++ 2 files changed, 31 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 1a3164b0d94..657cd994fe5 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2117,6 +2117,8 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): source_document_warehouse_field = "from_warehouse" target_document_warehouse_field = "target_warehouse" + received_items = get_received_items(source_name, target_doctype, target_detail_field) + validate_inter_company_transaction(source_doc, doctype) details = get_inter_company_details(source_doc, doctype) @@ -2181,12 +2183,17 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): shipping_address_name=target_doc.shipping_address_name, ) + def update_item(source, target, source_parent): + target.qty = flt(source.qty) - received_items.get(source.name, 0.0) + item_field_map = { "doctype": target_doctype + " Item", "field_no_map": ["income_account", "expense_account", "cost_center", "warehouse"], "field_map": { "rate": "rate", }, + "postprocess": update_item, + "condition": lambda doc: doc.qty > 0, } if doctype in ["Sales Invoice", "Sales Order"]: @@ -2224,6 +2231,28 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): return doclist +def get_received_items(reference_name, doctype, reference_fieldname): + target_doctypes = frappe.get_all( + doctype, + filters={"inter_company_invoice_reference": reference_name, "docstatus": 1}, + as_list=True, + ) + + if target_doctypes: + target_doctypes = list(target_doctypes[0]) + + received_items_map = frappe._dict( + frappe.get_all( + doctype + " Item", + filters={"parent": ("in", target_doctypes)}, + fields=[reference_fieldname, "qty"], + as_list=1, + ) + ) + + return received_items_map + + def set_purchase_references(doc): # add internal PO or PR links if any if doc.is_internal_transfer(): diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py index c0005f78cfd..0a765f3f46f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py @@ -11,6 +11,7 @@ def get_data(): "Payment Request": "reference_name", "Sales Invoice": "return_against", "Auto Repeat": "reference_document", + "Purchase Invoice": "inter_company_invoice_reference", }, "internal_links": { "Sales Order": ["items", "sales_order"], @@ -30,5 +31,6 @@ def get_data(): {"label": _("Reference"), "items": ["Timesheet", "Delivery Note", "Sales Order"]}, {"label": _("Returns"), "items": ["Sales Invoice"]}, {"label": _("Subscription"), "items": ["Auto Repeat"]}, + {"label": _("Internal Transfers"), "items": ["Purchase Invoice"]}, ], } From 7b093e580386e260dadda3ebb335ea1384c7a679 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 1 Jul 2022 10:35:59 +0530 Subject: [PATCH 23/26] fix(Salary Slip): Components not updated when amount evaluates to 0 due to payment days (backport #31425) (#31432) Co-authored-by: Rucha Mahabal --- erpnext/payroll/doctype/salary_slip/salary_slip.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index e1ccc117e75..7b7d0571b68 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -624,7 +624,7 @@ class SalarySlip(TransactionBase): data = self.get_data_for_eval() for struct_row in self._salary_structure_doc.get(component_type): amount = self.eval_condition_and_formula(struct_row, data) - if amount and struct_row.statistical_component == 0: + if amount is not None and struct_row.statistical_component == 0: self.update_component_row(struct_row, amount, component_type) def get_data_for_eval(self): @@ -854,6 +854,10 @@ class SalarySlip(TransactionBase): component_row, joining_date, relieving_date )[0] + # remove 0 valued components that have been updated later + if component_row.amount == 0: + self.remove(component_row) + def set_precision_for_component_amounts(self): for component_type in ("earnings", "deductions"): for component_row in self.get(component_type): From 7e40c86c56c81abbbfa23ef79b7e14b9a6a47090 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 1 Jul 2022 20:08:16 +0530 Subject: [PATCH 24/26] fix(UX): dont apply price list when changing batch on mapped docs (#31503) fix(UX): dont apply price list batch change on mapped docs --- erpnext/public/js/controllers/transaction.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 01f72adf349..ee77b464939 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1107,9 +1107,25 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } } + is_a_mapped_document(item) { + const mapped_item_field_map = { + "Delivery Note Item": ["si_detail", "so_detail", "dn_detail"], + "Sales Invoice Item": ["dn_detail", "so_detail", "sales_invoice_item"], + "Purchase Receipt Item": ["purchase_order_item", "purchase_invoice_item", "purchase_receipt_item"], + "Purchase Invoice Item": ["purchase_order_item", "pr_detail", "po_detail"], + }; + const mappped_fields = mapped_item_field_map[item.doctype] || []; + + return mappped_fields + .map((field) => item[field]) + .filter(Boolean).length > 0; + } + batch_no(doc, cdt, cdn) { let item = frappe.get_doc(cdt, cdn); - this.apply_price_list(item, true); + if (!this.is_a_mapped_document(item)) { + this.apply_price_list(item, true); + } } toggle_conversion_factor(item) { From 38352b3e46fb18435c780e5775bbc886491eac96 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 2 Jul 2022 22:27:20 +0530 Subject: [PATCH 25/26] test: Add test for einvoice discounts --- .../sales_invoice/test_sales_invoice.py | 57 +++++++++++++++++++ erpnext/regional/india/e_invoice/utils.py | 6 +- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 448ec54b1ee..c2e82fb214d 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2731,6 +2731,63 @@ class TestSalesInvoice(unittest.TestCase): einvoice = make_einvoice(si) validate_totals(einvoice) + def test_einvoice_discounts(self): + from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals + + # Normal Itemized Discount + si = get_sales_invoice_for_e_invoice() + si.apply_discount_on = "" + si.items[0].discount_amount = 4000 + si.items[1].discount_amount = 300 + si.save() + + einvoice = make_einvoice(si) + validate_totals(einvoice) + + self.assertEqual(einvoice["ItemList"][0]["Discount"], 4000) + self.assertEqual(einvoice["ItemList"][1]["Discount"], 300) + self.assertEqual(einvoice["ValDtls"]["Discount"], 0) + + # Invoice Discount on net total + si = get_sales_invoice_for_e_invoice() + si.apply_discount_on = "Net Total" + si.discount_amount = 400 + si.save() + + einvoice = make_einvoice(si) + validate_totals(einvoice) + + self.assertEqual(einvoice["ItemList"][0]["Discount"], 316.83) + self.assertEqual(einvoice["ItemList"][1]["Discount"], 83.17) + self.assertEqual(einvoice["ValDtls"]["Discount"], 0) + + # Invoice Discount on grand total (Itemized Discount) + si = get_sales_invoice_for_e_invoice() + si.apply_discount_on = "Grand Total" + si.discount_amount = 400 + si.save() + + einvoice = make_einvoice(si) + validate_totals(einvoice) + + self.assertEqual(einvoice["ItemList"][0]["Discount"], 268.5) + self.assertEqual(einvoice["ItemList"][1]["Discount"], 70.48) + self.assertEqual(einvoice["ValDtls"]["Discount"], 0) + + # Invoice Discount on grand total (Cash/Non-Trade Discount) + si = get_sales_invoice_for_e_invoice() + si.apply_discount_on = "Grand Total" + si.is_cash_or_non_trade_discount = 1 + si.discount_amount = 400 + si.save() + + einvoice = make_einvoice(si) + validate_totals(einvoice) + + self.assertEqual(einvoice["ItemList"][0]["Discount"], 0) + self.assertEqual(einvoice["ItemList"][1]["Discount"], 0) + self.assertEqual(einvoice["ValDtls"]["Discount"], 400) + def test_item_tax_net_range(self): item = create_item("T Shirt") diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 44ba6351e7e..fb2779bb481 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -274,10 +274,6 @@ def get_item_list(invoice): if invoice.get("apply_discount_on"): item.discount_amount = item.base_amount - item.base_net_amount - elif item.discount_amount > 0: - item.discount_amount = item.discount_amount - else: - item.discount_amount = 0 item.unit_rate = abs(item.taxable_value - item.discount_amount) / item.qty @@ -361,7 +357,7 @@ def get_invoice_value_details(invoice): and invoice.discount_amount and invoice.get("is_cash_or_non_trade_discount") ): - invoice_value_details.invoice_discount_amt = invoice.base_discount_amount + invoice_value_details.invoice_discount_amt = invoice.discount_amount else: invoice_value_details.invoice_discount_amt = 0 From e54ec4b9b6280bf1d12ebd3f0fc8c704551f9652 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 3 Jul 2022 11:02:21 +0530 Subject: [PATCH 26/26] chore: use get instead of . operator --- erpnext/controllers/taxes_and_totals.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index a3a054cd29d..0d8cffe03f8 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -597,7 +597,9 @@ class calculate_taxes_and_totals(object): if not self.doc.apply_discount_on: frappe.throw(_("Please select Apply Discount On")) - if self.doc.apply_discount_on == "Grand Total" and self.doc.is_cash_or_non_trade_discount: + if self.doc.apply_discount_on == "Grand Total" and self.doc.get( + "is_cash_or_non_trade_discount" + ): self.discount_amount_applied = True return