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 aefa9a59dd..cdb187f218 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 327545aa54..499377d426 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 a580d45acc..2fe6e1b6af 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 2144055b34..a3a054cd29 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 5eb14a5ddd..44ba6351e7 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 0262469571..f1586fc3dc 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 01f72adf34..1f86718990 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 4fa4515a0f..d775fa93be 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 7a04c6ab06..3096b692a7 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 dc071e4d65..fc8caeb31d 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 d732b755fe..5f84de60d0 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 fc6fdcdeff..ceac815bf4 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 1911152dec..411b31371b 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 9216c458c8..d47373fa61 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 8ddd4e36c2..1a6f23bc7b 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 33441b166d..a2528c3703 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 b3c35cfe0b..8abf65f4b5 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 a39d0a95eb..15dbccde89 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 8abf65f4b5..816cb62644 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 ebb88c9eda..af6d8f26a7 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 c9d5f61f22..6e7622c067 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 da45610eaf..33dbe3f468 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 01710f1e41..096175a68d 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 0de5b2d5a3..3dec303a92 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 cdb68499ff..91a0114004 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 77d3c1ae61..d3268e25b3 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 2cf44444cc..4d8c51d942 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 91a0114004..9dcc6560f9 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 d3268e25b3..f62c22b0df 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 bfc2601088..f0cb37919a 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 8043dd9906..b39c4f072f 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 83e53758be..0000000000 --- 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 1faff0412f..0ea4d07348 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 f0cb37919a..ecbae86d96 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 15545c0efa..0000000000 --- a/dev-requirements.txt +++ /dev/null @@ -1 +0,0 @@ -hypothesis~=6.31.0 diff --git a/pyproject.toml b/pyproject.toml index b39c4f072f..d446c9b39e 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 b644568d5e..722c1252ed 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 4d8c51d942..4e00870cab 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 9dcc6560f9..f65cb460f1 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 f62c22b0df..53a94dbfac 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 d446c9b39e..5acfd39272 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 4e00870cab..a71db7289a 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 1a3164b0d9..657cd994fe 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 c0005f78cf..0a765f3f46 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 e1ccc117e7..7b7d0571b6 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 01f72adf34..ee77b46493 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 448ec54b1e..c2e82fb214 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 44ba6351e7..fb2779bb48 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 a3a054cd29..0d8cffe03f 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