From dcdd3e8cc452bd31c4707ea93bf176d585e5822f Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 24 Jan 2024 01:48:10 +0100 Subject: [PATCH 01/41] fix: don't overwrite existing terms in transaction (cherry picked from commit 77b044f1a6e7273b21ce5a884429875b10d3bb2e) # Conflicts: # erpnext/public/js/controllers/transaction.js --- erpnext/public/js/controllers/transaction.js | 34 ++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index a3d4d880b29..3e731759838 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -809,6 +809,40 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe me.frm.set_value("tc_name", company_doc.default_buying_terms); } } +<<<<<<< HEAD +======= + + if ( + company_doc.default_selling_terms && + frappe.meta.has_field(me.frm.doc.doctype, "tc_name") && + [ + "Sales Invoice", + "Quotation", + "Sales Order", + "Delivery Note", + ].includes(me.frm.doc.doctype) && + !me.frm.doc.tc_name + ) { + me.frm.set_value("tc_name", company_doc.default_selling_terms); + } + + if ( + company_doc.default_buying_terms && + frappe.meta.has_field(me.frm.doc.doctype, "tc_name") && + [ + "Request for Quotation", + "Supplier Quotation", + "Purchase Order", + // Purchase Invoice is excluded as per issue #3345 + "Material Request", + "Purchase Receipt", + ].includes(me.frm.doc.doctype) && + !me.frm.doc.tc_name + ) { + me.frm.set_value("tc_name", company_doc.default_buying_terms); + } + +>>>>>>> 77b044f1a6 (fix: don't overwrite existing terms in transaction) frappe.run_serially([ () => me.frm.script_manager.trigger("currency"), () => me.update_item_tax_map(), From aebb0c79797b44f73d056fffac3d05ae7f439c72 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 31 Jan 2024 12:38:37 +0530 Subject: [PATCH 02/41] chore: cleanup doctype descriptions (backport #39637) (#39652) * chore: cleanup doctype descriptions (#39637) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> (cherry picked from commit 6b8f046fb4cfef201464976f5b4c95dc79f96731) # Conflicts: # erpnext/accounts/doctype/accounts_settings/accounts_settings.json # erpnext/accounts/doctype/fiscal_year/fiscal_year.json # erpnext/stock/doctype/item_price/item_price.json # erpnext/stock/doctype/stock_settings/stock_settings.json * chore: fix conflicts --------- Co-authored-by: Rucha Mahabal --- .../accounts_settings/accounts_settings.json | 3 +- .../doctype/fiscal_year/fiscal_year.json | 8 +- .../monthly_distribution.json | 226 ++++------- .../purchase_taxes_and_charges_template.json | 4 +- .../sales_taxes_and_charges_template.json | 4 +- .../buying_settings/buying_settings.json | 3 +- .../product_bundle/product_bundle.json | 4 +- .../setup/doctype/item_group/item_group.json | 4 +- .../doctype/sales_person/sales_person.json | 8 +- .../terms_and_conditions.json | 4 +- .../stock/doctype/item_price/item_price.json | 4 +- .../stock/doctype/price_list/price_list.json | 364 ++---------------- .../stock_settings/stock_settings.json | 4 +- 13 files changed, 123 insertions(+), 517 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index fd052d04760..0e238e08f62 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -1,7 +1,6 @@ { "actions": [], "creation": "2013-06-24 15:49:57", - "description": "Settings for Accounts", "doctype": "DocType", "document_type": "Other", "editable_grid": 1, @@ -462,7 +461,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-11-20 09:37:47.650347", + "modified": "2024-01-30 14:04:26.553554", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json index 5ab91f2506c..da7f08fe3d4 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json @@ -3,7 +3,7 @@ "allow_import": 1, "autoname": "field:year", "creation": "2013-01-22 16:50:25", - "description": "**Fiscal Year** represents a Financial Year. All accounting entries and other major transactions are tracked against **Fiscal Year**.", + "description": "Represents a Financial Year. All accounting entries and other major transactions are tracked against the Fiscal Year.", "doctype": "DocType", "document_type": "Setup", "engine": "InnoDB", @@ -82,10 +82,11 @@ "icon": "fa fa-calendar", "idx": 1, "links": [], - "modified": "2020-11-05 12:16:53.081573", + "modified": "2024-01-30 12:35:38.645968", "modified_by": "Administrator", "module": "Accounts", "name": "Fiscal Year", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -122,5 +123,6 @@ ], "show_name_in_global_search": 1, "sort_field": "name", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.json b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.json index 14f2d802505..488e8b26207 100644 --- a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.json +++ b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.json @@ -1,173 +1,77 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:distribution_id", - "beta": 0, - "creation": "2013-01-10 16:34:05", - "custom": 0, - "description": "**Monthly Distribution** helps you distribute the Budget/Target across months if you have seasonality in your business.", - "docstatus": 0, - "doctype": "DocType", - "editable_grid": 0, - "engine": "InnoDB", + "actions": [], + "autoname": "field:distribution_id", + "creation": "2013-01-10 16:34:05", + "description": "Helps you distribute the Budget/Target across months if you have seasonality in your business.", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "distribution_id", + "fiscal_year", + "percentages" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Name of the Monthly Distribution", - "fieldname": "distribution_id", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Distribution Name", - "length": 0, - "no_copy": 0, - "oldfieldname": "distribution_id", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "description": "Name of the Monthly Distribution", + "fieldname": "distribution_id", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Distribution Name", + "oldfieldname": "distribution_id", + "oldfieldtype": "Data", + "reqd": 1, + "unique": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "fiscal_year", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Fiscal Year", - "length": 0, - "no_copy": 0, - "oldfieldname": "fiscal_year", - "oldfieldtype": "Select", - "options": "Fiscal Year", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "fiscal_year", + "fieldtype": "Link", + "in_filter": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Fiscal Year", + "oldfieldname": "fiscal_year", + "oldfieldtype": "Select", + "options": "Fiscal Year", + "search_index": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "percentages", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Monthly Distribution Percentages", - "length": 0, - "no_copy": 0, - "oldfieldname": "budget_distribution_details", - "oldfieldtype": "Table", - "options": "Monthly Distribution Percentage", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "percentages", + "fieldtype": "Table", + "label": "Monthly Distribution Percentages", + "oldfieldname": "budget_distribution_details", + "oldfieldtype": "Table", + "options": "Monthly Distribution Percentage" } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-bar-chart", - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2016-11-21 14:54:35.998761", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Monthly Distribution", - "name_case": "Title Case", - "owner": "Administrator", + ], + "icon": "fa fa-bar-chart", + "idx": 1, + "links": [], + "modified": "2024-01-30 13:57:55.802744", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Monthly Distribution", + "naming_rule": "By fieldname", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 2, - "print": 0, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "permlevel": 2, + "read": 1, + "report": 1, + "role": "Accounts Manager" } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_seen": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json index c36efb89a30..2ff6a45d1eb 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json @@ -3,7 +3,7 @@ "allow_import": 1, "allow_rename": 1, "creation": "2013-01-10 16:34:08", - "description": "Standard tax template that can be applied to all Purchase Transactions. This template can contain list of tax heads and also other expense heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Consider Tax or Charge for: In this section you can specify if the tax / charge is only for valuation (not a part of total) or only for total (does not add value to the item) or for both.\n10. Add or Deduct: Whether you want to add or deduct the tax.", + "description": "Standard tax template that can be applied to all Purchase Transactions. This template can contain a list of tax heads and also other expense heads like \"Shipping\", \"Insurance\", \"Handling\", etc.", "doctype": "DocType", "document_type": "Setup", "engine": "InnoDB", @@ -77,7 +77,7 @@ "icon": "fa fa-money", "idx": 1, "links": [], - "modified": "2022-05-16 16:15:29.059370", + "modified": "2024-01-30 13:08:09.537242", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Taxes and Charges Template", diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json index 408ecbf36dc..736d283cdbd 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json @@ -3,7 +3,7 @@ "allow_import": 1, "allow_rename": 1, "creation": "2013-01-10 16:34:09", - "description": "Standard tax template that can be applied to all Sales Transactions. This template can contain list of tax heads and also other expense / income heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Is this Tax included in Basic Rate?: If you check this, it means that this tax will not be shown below the item table, but will be included in the Basic Rate in your main item table. This is useful where you want give a flat price (inclusive of all taxes) price to customers.", + "description": "Standard tax template that can be applied to all Sales Transactions. This template can contain a list of tax heads and also other expense/income heads like \"Shipping\", \"Insurance\", \"Handling\" etc.", "doctype": "DocType", "document_type": "Setup", "engine": "InnoDB", @@ -79,7 +79,7 @@ "icon": "fa fa-money", "idx": 1, "links": [], - "modified": "2022-05-16 16:14:52.061672", + "modified": "2024-01-30 13:07:28.801104", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Taxes and Charges Template", diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index ddcbd555ae8..77ce5f7d193 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -1,7 +1,6 @@ { "actions": [], "creation": "2013-06-25 11:04:03", - "description": "Settings for Buying Module", "doctype": "DocType", "document_type": "Other", "engine": "InnoDB", @@ -214,7 +213,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-01-12 16:42:01.894346", + "modified": "2024-01-30 14:04:43.177427", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.json b/erpnext/selling/doctype/product_bundle/product_bundle.json index c4f21b61b9e..1c37b854b9a 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.json +++ b/erpnext/selling/doctype/product_bundle/product_bundle.json @@ -2,7 +2,7 @@ "actions": [], "allow_import": 1, "creation": "2013-06-20 11:53:21", - "description": "Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have \"Is Stock Item\" as \"No\" and \"Is Sales Item\" as \"Yes\".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials", + "description": "Aggregate a group of Items into another Item. This is useful if you are maintaining the stock of the packed items and not the bundled item", "doctype": "DocType", "engine": "InnoDB", "field_order": [ @@ -77,7 +77,7 @@ "icon": "fa fa-sitemap", "idx": 1, "links": [], - "modified": "2023-11-22 15:20:46.805114", + "modified": "2024-01-30 13:57:04.951788", "modified_by": "Administrator", "module": "Selling", "name": "Product Bundle", diff --git a/erpnext/setup/doctype/item_group/item_group.json b/erpnext/setup/doctype/item_group/item_group.json index dfa5a8ed0a7..7c9233fa1f3 100644 --- a/erpnext/setup/doctype/item_group/item_group.json +++ b/erpnext/setup/doctype/item_group/item_group.json @@ -4,7 +4,7 @@ "allow_rename": 1, "autoname": "field:item_group_name", "creation": "2013-03-28 10:35:29", - "description": "Item Classification", + "description": "An Item Group is a way to classify items based on types.", "doctype": "DocType", "document_type": "Setup", "engine": "InnoDB", @@ -135,7 +135,7 @@ "is_tree": 1, "links": [], "max_attachments": 3, - "modified": "2023-10-12 13:44:13.611287", + "modified": "2024-01-30 14:08:38.485616", "modified_by": "Administrator", "module": "Setup", "name": "Item Group", diff --git a/erpnext/setup/doctype/sales_person/sales_person.json b/erpnext/setup/doctype/sales_person/sales_person.json index e526ac42ba8..79bd8411ee2 100644 --- a/erpnext/setup/doctype/sales_person/sales_person.json +++ b/erpnext/setup/doctype/sales_person/sales_person.json @@ -4,7 +4,7 @@ "allow_rename": 1, "autoname": "field:sales_person_name", "creation": "2013-01-10 16:34:24", - "description": "All Sales Transactions can be tagged against multiple **Sales Persons** so that you can set and monitor targets.", + "description": "All Sales Transactions can be tagged against multiple Sales Persons so that you can set and monitor targets.", "doctype": "DocType", "document_type": "Setup", "engine": "InnoDB", @@ -145,10 +145,11 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2020-03-18 18:11:13.968024", + "modified": "2024-01-30 13:57:26.436991", "modified_by": "Administrator", "module": "Setup", "name": "Sales Person", + "naming_rule": "By fieldname", "nsm_parent_field": "parent_sales_person", "owner": "Administrator", "permissions": [ @@ -181,5 +182,6 @@ "search_fields": "parent_sales_person", "show_name_in_global_search": 1, "sort_field": "modified", - "sort_order": "ASC" + "sort_order": "ASC", + "states": [] } \ No newline at end of file diff --git a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json index f884864acfa..76e52aefeba 100644 --- a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json +++ b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json @@ -4,7 +4,7 @@ "allow_rename": 1, "autoname": "field:title", "creation": "2013-01-10 16:34:24", - "description": "Standard Terms and Conditions that can be added to Sales and Purchases.\n\nExamples:\n\n1. Validity of the offer.\n1. Payment Terms (In Advance, On Credit, part advance etc).\n1. What is extra (or payable by the Customer).\n1. Safety / usage warning.\n1. Warranty if any.\n1. Returns Policy.\n1. Terms of shipping, if applicable.\n1. Ways of addressing disputes, indemnity, liability, etc.\n1. Address and Contact of your Company.", + "description": "Standard Terms and Conditions that can be added to Sales and Purchases. Examples: Validity of the offer, Payment Terms, Safety and Usage, etc.", "doctype": "DocType", "document_type": "Setup", "engine": "InnoDB", @@ -77,7 +77,7 @@ "icon": "icon-legal", "idx": 1, "links": [], - "modified": "2023-02-01 14:33:39.246532", + "modified": "2024-01-30 12:47:52.325531", "modified_by": "Administrator", "module": "Setup", "name": "Terms and Conditions", diff --git a/erpnext/stock/doctype/item_price/item_price.json b/erpnext/stock/doctype/item_price/item_price.json index f4d9bb0742d..b92026dc97b 100644 --- a/erpnext/stock/doctype/item_price/item_price.json +++ b/erpnext/stock/doctype/item_price/item_price.json @@ -3,7 +3,7 @@ "allow_import": 1, "autoname": "hash", "creation": "2013-05-02 16:29:48", - "description": "Multiple Item prices.", + "description": "Log the selling and buying rate of an Item", "doctype": "DocType", "document_type": "Setup", "engine": "InnoDB", @@ -220,7 +220,7 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-11-15 08:26:04.041861", + "modified": "2024-01-30 14:02:19.304854", "modified_by": "Administrator", "module": "Stock", "name": "Item Price", diff --git a/erpnext/stock/doctype/price_list/price_list.json b/erpnext/stock/doctype/price_list/price_list.json index 56340fb05ca..38cd1ee0c66 100644 --- a/erpnext/stock/doctype/price_list/price_list.json +++ b/erpnext/stock/doctype/price_list/price_list.json @@ -1,434 +1,134 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, + "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "field:price_list_name", - "beta": 0, "creation": "2013-01-25 11:35:09", - "custom": 0, - "description": "Price List Master", - "docstatus": 0, + "description": "A Price List is a collection of Item Prices either Selling, Buying, or both", "doctype": "DocType", "document_type": "Setup", - "editable_grid": 0, "engine": "InnoDB", + "field_order": [ + "enabled", + "sb_1", + "price_list_name", + "currency", + "buying", + "selling", + "price_not_uom_dependent", + "column_break_3", + "countries" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", - "fetch_if_empty": 0, "fieldname": "enabled", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Enabled", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Enabled" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "sb_1", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "price_list_name", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Price List Name", - "length": 0, "no_copy": 1, "oldfieldname": "price_list_name", "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, "unique": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "currency", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 1, "label": "Currency", - "length": 0, - "no_copy": 0, "options": "Currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "buying", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, - "label": "Buying", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Buying" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "selling", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, - "label": "Selling", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Selling" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "price_not_uom_dependent", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Price Not UOM Dependent", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Price Not UOM Dependent" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "countries", "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Applicable for Countries", - "length": 0, - "no_copy": 0, - "options": "Price List Country", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Price List Country" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, "icon": "fa fa-tags", "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, + "links": [], "max_attachments": 1, - "modified": "2019-06-24 17:16:28.027302", + "modified": "2024-01-30 14:39:26.328837", "modified_by": "Administrator", "module": "Stock", "name": "Price List", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, "read": 1, "report": 1, - "role": "Sales User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "role": "Sales User" }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, - "email": 0, "export": 1, - "if_owner": 0, "import": 1, - "permlevel": 0, - "print": 0, "read": 1, "report": 1, "role": "Sales Master Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, "read": 1, "report": 1, - "role": "Purchase User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "role": "Purchase User" }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, "read": 1, "report": 1, "role": "Purchase Master Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, "read": 1, - "report": 0, - "role": "Manufacturing User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "role": "Manufacturing User" } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, "search_fields": "currency", "show_name_in_global_search": 1, + "sort_field": "modified", "sort_order": "ASC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + "states": [] } \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 122829032de..40fac4113d6 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -1,7 +1,7 @@ { "actions": [], "creation": "2013-06-24 16:37:54", - "description": "Settings", + "description": "Default settings for your stock-related transactions", "doctype": "DocType", "engine": "InnoDB", "field_order": [ @@ -427,7 +427,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-10-18 12:35:30.068799", + "modified": "2024-01-30 14:03:52.143457", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", From 29eb09052867de15b5a95ba22c45c7e3037aebb9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 31 Jan 2024 12:49:48 +0530 Subject: [PATCH 03/41] fix: Exchange rate on MR to PO creation for muticurrency POs (#39646) fix: Exchange rate on MR to PO creation for muticurrency POs (#39646) (cherry picked from commit cfd1666181ffdff8ae79bbbb7863e1b12b3c6090) Co-authored-by: Deepesh Garg --- erpnext/stock/doctype/material_request/material_request.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 7e34f66c2b9..2487497efc9 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -462,6 +462,7 @@ def make_purchase_order(source_name, target_doc=None, args=None): postprocess, ) + doclist.set_onload("load_after_mapping", False) return doclist From cea4ed6f88af9ab226aa8bdcfdecf9b08cecc664 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 30 Jan 2024 17:24:59 +0530 Subject: [PATCH 04/41] fix(portal): show PO pay button if payments installed (cherry picked from commit ae7be84d873cee0b0ea35614804a249ea56c05bb) --- erpnext/templates/pages/order.html | 12 ++++++++++++ erpnext/templates/pages/order.py | 5 ++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/erpnext/templates/pages/order.html b/erpnext/templates/pages/order.html index 97bf48727cf..6c59a9688dc 100644 --- a/erpnext/templates/pages/order.html +++ b/erpnext/templates/pages/order.html @@ -34,6 +34,18 @@ + {% if show_pay_button %} + + {% endif %} {% endblock %} diff --git a/erpnext/templates/pages/order.py b/erpnext/templates/pages/order.py index d0968bf88a2..21d4b860d1f 100644 --- a/erpnext/templates/pages/order.py +++ b/erpnext/templates/pages/order.py @@ -48,7 +48,10 @@ def get_context(context): ) context.available_loyalty_points = int(loyalty_program_details.get("loyalty_points")) - context.show_pay_button = frappe.db.get_single_value("Buying Settings", "show_pay_button") + context.show_pay_button = ( + "payments" in frappe.get_installed_apps() + and frappe.db.get_single_value("Buying Settings", "show_pay_button") + ) context.show_make_pi_button = False if context.doc.get("supplier"): # show Make Purchase Invoice button based on permission From e9314325cc3e2e2a8665f36220d658856b8e802a Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 31 Jan 2024 13:37:01 +0530 Subject: [PATCH 05/41] fix: conditionally display show btn setting (cherry picked from commit 0c9572bb489c7b1cc12b38e3308f987dd61f162f) # Conflicts: # erpnext/buying/doctype/buying_settings/buying_settings.json --- erpnext/buying/doctype/buying_settings/buying_settings.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 77ce5f7d193..7e57bbf2df6 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -151,6 +151,7 @@ }, { "default": "1", + "depends_on": "eval: frappe.boot.versions && frappe.boot.versions.payments", "fieldname": "show_pay_button", "fieldtype": "Check", "label": "Show Pay Button in Purchase Order Portal" @@ -213,7 +214,11 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], +<<<<<<< HEAD "modified": "2024-01-30 14:04:43.177427", +======= + "modified": "2024-01-31 13:34:18.101256", +>>>>>>> 0c9572bb48 (fix: conditionally display show btn setting) "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", From 510ab769f15f41d7773ec70efad30c7f4b084e92 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 31 Jan 2024 15:23:14 +0530 Subject: [PATCH 06/41] chore: fix conflicts --- erpnext/buying/doctype/buying_settings/buying_settings.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 7e57bbf2df6..ae854f29343 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -214,11 +214,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], -<<<<<<< HEAD - "modified": "2024-01-30 14:04:43.177427", -======= "modified": "2024-01-31 13:34:18.101256", ->>>>>>> 0c9572bb48 (fix: conditionally display show btn setting) "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", @@ -268,4 +264,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} From ba55d0ede778f79ec2f24e462e30c31166594165 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 31 Jan 2024 13:19:19 +0100 Subject: [PATCH 07/41] chore: resolve conflicts --- erpnext/public/js/controllers/transaction.js | 38 ++------------------ 1 file changed, 2 insertions(+), 36 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 3e731759838..05f512f5c4a 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -798,51 +798,17 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } let selling_doctypes_for_tc = ["Sales Invoice", "Quotation", "Sales Order", "Delivery Note"]; if (company_doc.default_selling_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") && - selling_doctypes_for_tc.indexOf(me.frm.doc.doctype) != -1) { + selling_doctypes_for_tc.includes(me.frm.doc.doctype) && !me.frm.doc.tc_name) { me.frm.set_value("tc_name", company_doc.default_selling_terms); } let buying_doctypes_for_tc = ["Request for Quotation", "Supplier Quotation", "Purchase Order", "Material Request", "Purchase Receipt"]; // Purchase Invoice is excluded as per issue #3345 if (company_doc.default_buying_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") && - buying_doctypes_for_tc.indexOf(me.frm.doc.doctype) != -1) { + buying_doctypes_for_tc.includes(me.frm.doc.doctype) && !me.frm.doc.tc_name) { me.frm.set_value("tc_name", company_doc.default_buying_terms); } } -<<<<<<< HEAD -======= - - if ( - company_doc.default_selling_terms && - frappe.meta.has_field(me.frm.doc.doctype, "tc_name") && - [ - "Sales Invoice", - "Quotation", - "Sales Order", - "Delivery Note", - ].includes(me.frm.doc.doctype) && - !me.frm.doc.tc_name - ) { - me.frm.set_value("tc_name", company_doc.default_selling_terms); - } - - if ( - company_doc.default_buying_terms && - frappe.meta.has_field(me.frm.doc.doctype, "tc_name") && - [ - "Request for Quotation", - "Supplier Quotation", - "Purchase Order", - // Purchase Invoice is excluded as per issue #3345 - "Material Request", - "Purchase Receipt", - ].includes(me.frm.doc.doctype) && - !me.frm.doc.tc_name - ) { - me.frm.set_value("tc_name", company_doc.default_buying_terms); - } - ->>>>>>> 77b044f1a6 (fix: don't overwrite existing terms in transaction) frappe.run_serially([ () => me.frm.script_manager.trigger("currency"), () => me.update_item_tax_map(), From 7f4cd3cd156e5d9df196d9635eaf19569d9ca7c6 Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Thu, 1 Feb 2024 10:13:29 +0530 Subject: [PATCH 08/41] fix: correctly calculate diff amount for included taxes (#39655) (cherry picked from commit 772f540bef28117c008512ead6558db801d395cd) --- .../doctype/payment_entry/payment_entry.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index cd2ad394763..8af0713b0b4 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -946,19 +946,19 @@ class PaymentEntry(AccountsController): ) base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount) - - if self.payment_type == "Receive": - self.difference_amount = base_party_amount - self.base_received_amount - elif self.payment_type == "Pay": - self.difference_amount = self.base_paid_amount - base_party_amount - else: - self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) - - total_deductions = sum(flt(d.amount) for d in self.get("deductions")) included_taxes = self.get_included_taxes() + if self.payment_type == "Receive": + self.difference_amount = base_party_amount - self.base_received_amount + included_taxes + elif self.payment_type == "Pay": + self.difference_amount = self.base_paid_amount - base_party_amount - included_taxes + else: + self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) - included_taxes + + total_deductions = sum(flt(d.amount) for d in self.get("deductions")) + self.difference_amount = flt( - self.difference_amount - total_deductions - included_taxes, self.precision("difference_amount") + self.difference_amount - total_deductions, self.precision("difference_amount") ) def get_included_taxes(self): From 0a235584ad1ff1eb238e79b8dfcb34b6ee21e4ff Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 1 Feb 2024 14:53:01 +0530 Subject: [PATCH 09/41] refactor: move ignore ERR filters from SOA to General Ledger (cherry picked from commit c077eda64e8b57011c42fbf7b4fe41af9ab2bc2f) --- .../process_statement_of_accounts.py | 16 ++-------------- .../report/general_ledger/general_ledger.js | 6 ++++++ .../report/general_ledger/general_ledger.py | 13 +++++++++++++ 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index c03b18a8716..083c8fce18b 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -120,18 +120,6 @@ def get_statement_dict(doc, get_statement_dict=False): statement_dict = {} ageing = "" - err_journals = None - if doc.report == "General Ledger" and doc.ignore_exchange_rate_revaluation_journals: - err_journals = frappe.db.get_all( - "Journal Entry", - filters={ - "company": doc.company, - "docstatus": 1, - "voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]), - }, - as_list=True, - ) - for entry in doc.customers: if doc.include_ageing: ageing = set_ageing(doc, entry) @@ -144,8 +132,8 @@ def get_statement_dict(doc, get_statement_dict=False): ) filters = get_common_filters(doc) - if err_journals: - filters.update({"voucher_no_not_in": [x[0] for x in err_journals]}) + if doc.ignore_exchange_rate_revaluation_journals: + filters.update({"ignore_err": True}) if doc.report == "General Ledger": filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency)) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 79b5e4d9ec8..b7b9d34e00f 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -203,8 +203,14 @@ frappe.query_reports["General Ledger"] = { "fieldname": "show_remarks", "label": __("Show Remarks"), "fieldtype": "Check" + }, + { + "fieldname": "ignore_err", + "label": __("Ignore Exchange Rate Revaluation Journals"), + "fieldtype": "Check" } + ] } diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index ff6cd9f4b25..8fccc14873e 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -241,6 +241,19 @@ def get_conditions(filters): if filters.get("against_voucher_no"): conditions.append("against_voucher=%(against_voucher_no)s") + if filters.get("ignore_err"): + err_journals = frappe.db.get_all( + "Journal Entry", + filters={ + "company": filters.get("company"), + "docstatus": 1, + "voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]), + }, + as_list=True, + ) + if err_journals: + filters.update({"voucher_no_not_in": [x[0] for x in err_journals]}) + if filters.get("voucher_no_not_in"): conditions.append("voucher_no not in %(voucher_no_not_in)s") From 1ee15f65a8280f865c3497d7f00808c7744258bc Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 1 Feb 2024 15:03:02 +0530 Subject: [PATCH 10/41] test: ignore_err filter out in General Ledger (cherry picked from commit affca3a519c626a2c3273a82f00ee1579853b0e1) --- .../general_ledger/test_general_ledger.py | 104 +++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/general_ledger/test_general_ledger.py b/erpnext/accounts/report/general_ledger/test_general_ledger.py index a8c362e78c1..c3ed7f2a23f 100644 --- a/erpnext/accounts/report/general_ledger/test_general_ledger.py +++ b/erpnext/accounts/report/general_ledger/test_general_ledger.py @@ -3,7 +3,7 @@ import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import today +from frappe.utils import flt, today from erpnext.accounts.report.general_ledger.general_ledger import execute @@ -148,3 +148,105 @@ class TestGeneralLedger(FrappeTestCase): self.assertEqual(data[2]["credit"], 900) self.assertEqual(data[3]["debit"], 100) self.assertEqual(data[3]["credit"], 100) + + def test_ignore_exchange_rate_journals_filter(self): + # create a new account with USD currency + account_name = "Test Debtors USD" + company = "_Test Company" + account = frappe.get_doc( + { + "account_name": account_name, + "is_group": 0, + "company": company, + "root_type": "Asset", + "report_type": "Balance Sheet", + "account_currency": "USD", + "parent_account": "Accounts Receivable - _TC", + "account_type": "Receivable", + "doctype": "Account", + } + ) + account.insert(ignore_if_duplicate=True) + # create a JV to debit 1000 USD at 75 exchange rate + jv = frappe.new_doc("Journal Entry") + jv.posting_date = today() + jv.company = company + jv.multi_currency = 1 + jv.cost_center = "_Test Cost Center - _TC" + jv.set( + "accounts", + [ + { + "account": account.name, + "party_type": "Customer", + "party": "_Test Customer", + "debit_in_account_currency": 1000, + "credit_in_account_currency": 0, + "exchange_rate": 75, + "cost_center": "_Test Cost Center - _TC", + }, + { + "account": "Cash - _TC", + "debit_in_account_currency": 0, + "credit_in_account_currency": 75000, + "cost_center": "_Test Cost Center - _TC", + }, + ], + ) + jv.save() + jv.submit() + + revaluation = frappe.new_doc("Exchange Rate Revaluation") + revaluation.posting_date = today() + revaluation.company = company + accounts = revaluation.get_accounts_data() + revaluation.extend("accounts", accounts) + row = revaluation.accounts[0] + row.new_exchange_rate = 83 + row.new_balance_in_base_currency = flt( + row.new_exchange_rate * flt(row.balance_in_account_currency) + ) + row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency) + revaluation.set_total_gain_loss() + revaluation = revaluation.save().submit() + + # post journal entry for Revaluation doc + frappe.db.set_value( + "Company", company, "unrealized_exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC" + ) + revaluation_jv = revaluation.make_jv_for_revaluation() + revaluation_jv.cost_center = "_Test Cost Center - _TC" + for acc in revaluation_jv.get("accounts"): + acc.cost_center = "_Test Cost Center - _TC" + revaluation_jv.save() + revaluation_jv.submit() + + # With ignore_err enabled + columns, data = execute( + frappe._dict( + { + "company": company, + "from_date": today(), + "to_date": today(), + "account": [account.name], + "group_by": "Group by Voucher (Consolidated)", + "ignore_err": True, + } + ) + ) + self.assertNotIn(revaluation_jv.name, set([x.voucher_no for x in data])) + + # Without ignore_err enabled + columns, data = execute( + frappe._dict( + { + "company": company, + "from_date": today(), + "to_date": today(), + "account": [account.name], + "group_by": "Group by Voucher (Consolidated)", + "ignore_err": False, + } + ) + ) + self.assertIn(revaluation_jv.name, set([x.voucher_no for x in data])) From 72c95d31d1df152fcd91d3494148f9bdea007aed Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 1 Feb 2024 15:46:57 +0530 Subject: [PATCH 11/41] refactor(test): use party with USD billing currency (cherry picked from commit beff566c8267104b97f7e4b80c36715e4eb91832) --- erpnext/accounts/report/general_ledger/test_general_ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/general_ledger/test_general_ledger.py b/erpnext/accounts/report/general_ledger/test_general_ledger.py index c3ed7f2a23f..75f94309bcc 100644 --- a/erpnext/accounts/report/general_ledger/test_general_ledger.py +++ b/erpnext/accounts/report/general_ledger/test_general_ledger.py @@ -179,7 +179,7 @@ class TestGeneralLedger(FrappeTestCase): { "account": account.name, "party_type": "Customer", - "party": "_Test Customer", + "party": "_Test Customer USD", "debit_in_account_currency": 1000, "credit_in_account_currency": 0, "exchange_rate": 75, From e2d041f51c08c6763bbf8d97ebb0613bbc6fe0a9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 1 Feb 2024 16:05:27 +0530 Subject: [PATCH 12/41] refactor: use pop up to inform of possible data issue and leave a comment in communcation trail as well (cherry picked from commit 78483e2ee6fdef9e36b63408f6796f0a1e78dac1) --- erpnext/accounts/doctype/account/account.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 82af85d7024..5f0faf79b5b 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -118,6 +118,7 @@ class Account(NestedSet): self.validate_balance_must_be_debit_or_credit() self.validate_account_currency() self.validate_root_company_and_sync_account_to_children() + self.validate_receivable_payable_account_type() def validate_parent_child_account_type(self): if self.parent_account: @@ -188,6 +189,24 @@ class Account(NestedSet): "Balance Sheet" if self.root_type in ("Asset", "Liability", "Equity") else "Profit and Loss" ) + def validate_receivable_payable_account_type(self): + doc_before_save = self.get_doc_before_save() + receivable_payable_types = ["Receivable", "Payable"] + if ( + doc_before_save + and doc_before_save.account_type in receivable_payable_types + and doc_before_save.account_type != self.account_type + ): + # check for ledger entries + if frappe.db.get_all("GL Entry", filters={"account": self.name, "is_cancelled": 0}, limit=1): + msg = _( + "There are ledger entries against this account. Changing {0} to non-{1} in live system will cause incorrect output in 'Accounts {2}' report" + ).format( + frappe.bold("Account Type"), doc_before_save.account_type, doc_before_save.account_type + ) + frappe.msgprint(msg) + self.add_comment("Comment", msg) + def validate_root_details(self): doc_before_save = self.get_doc_before_save() From 41e46326c04bc7939bc94fa57a82259ecaf5c7ee Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 2 Feb 2024 15:24:45 +0530 Subject: [PATCH 13/41] fix: out of range for valuation_rate column in SE (backport #39687) (#39691) fix: out of range for valuation_rate column in SE (cherry picked from commit 1e15a3cc151d750f6b00a52c4a9a595436e465ba) Co-authored-by: s-aga-r --- erpnext/stock/stock_ledger.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 0a6a686d8e3..379320237e6 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -897,9 +897,12 @@ class update_entries_after(object): self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount) - self.wh_data.qty_after_transaction += doc.total_qty + precision = doc.precision("total_qty") + self.wh_data.qty_after_transaction += flt(doc.total_qty, precision) if self.wh_data.qty_after_transaction: - self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction + self.wh_data.valuation_rate = flt(self.wh_data.stock_value, precision) / flt( + self.wh_data.qty_after_transaction, precision + ) def validate_negative_stock(self, sle): """ From e76f59803cebd7ba19a2d0618dce959dfce263b2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 2 Feb 2024 20:27:36 +0530 Subject: [PATCH 14/41] fix: Percentage handling in queries (#39692) fix: Percentage handling in queries (#39692) * fix: Percentage handling in queries * test: Account with percent sign * chore: add test records (cherry picked from commit 6d87cfeb8d87b6faed8a7238cd8cf97df2acd99d) Co-authored-by: Deepesh Garg --- erpnext/accounts/doctype/account/test_account.py | 14 ++++++++++++++ erpnext/accounts/utils.py | 10 +++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py index 30eebef7fba..a26eba1aba2 100644 --- a/erpnext/accounts/doctype/account/test_account.py +++ b/erpnext/accounts/doctype/account/test_account.py @@ -6,6 +6,7 @@ import unittest import frappe from frappe.test_runner import make_test_records +from frappe.utils import nowdate from erpnext.accounts.doctype.account.account import ( InvalidAccountMergeError, @@ -324,6 +325,19 @@ class TestAccount(unittest.TestCase): acc.account_currency = "USD" self.assertRaises(frappe.ValidationError, acc.save) + def test_account_balance(self): + from erpnext.accounts.utils import get_balance_on + + if not frappe.db.exists("Account", "Test Percent Account %5 - _TC"): + acc = frappe.new_doc("Account") + acc.account_name = "Test Percent Account %5" + acc.parent_account = "Tax Assets - _TC" + acc.company = "_Test Company" + acc.insert() + + balance = get_balance_on(account="Test Percent Account %5 - _TC", date=nowdate()) + self.assertEqual(balance, 0) + def _make_test_records(verbose=None): from frappe.test_runner import make_test_objects diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index a61b09e1e8f..d7b13bf7eda 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -237,7 +237,7 @@ def get_balance_on( ) else: - cond.append("""gle.cost_center = %s """ % (frappe.db.escape(cost_center, percent=False),)) + cond.append("""gle.cost_center = %s """ % (frappe.db.escape(cost_center),)) if account: if not (frappe.flags.ignore_account_permission or ignore_account_permission): @@ -258,7 +258,7 @@ def get_balance_on( if acc.account_currency == frappe.get_cached_value("Company", acc.company, "default_currency"): in_account_currency = False else: - cond.append("""gle.account = %s """ % (frappe.db.escape(account, percent=False),)) + cond.append("""gle.account = %s """ % (frappe.db.escape(account),)) if account_type: accounts = frappe.db.get_all( @@ -278,11 +278,11 @@ def get_balance_on( if party_type and party: cond.append( """gle.party_type = %s and gle.party = %s """ - % (frappe.db.escape(party_type), frappe.db.escape(party, percent=False)) + % (frappe.db.escape(party_type), frappe.db.escape(party)) ) if company: - cond.append("""gle.company = %s """ % (frappe.db.escape(company, percent=False))) + cond.append("""gle.company = %s """ % (frappe.db.escape(company))) if account or (party_type and party) or account_type: precision = get_currency_precision() @@ -348,7 +348,7 @@ def get_count_on(account, fieldname, date): % (acc.lft, acc.rgt) ) else: - cond.append("""gle.account = %s """ % (frappe.db.escape(account, percent=False),)) + cond.append("""gle.account = %s """ % (frappe.db.escape(account),)) entries = frappe.db.sql( """ From f31070741afd9a8d12aebda024a0537fa2dcbe8b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 3 Feb 2024 08:49:50 +0530 Subject: [PATCH 15/41] fix: Pricing rule application/removal on qty change (#39084) fix: Pricing rule application/removal on qty change (cherry picked from commit f52d7c7665d4097b19a8bb6c5f99acf38cd8adcf) Co-authored-by: Deepesh Garg --- .../doctype/pricing_rule/pricing_rule.py | 5 +++ erpnext/public/js/controllers/transaction.js | 41 ++++++++++++++++--- erpnext/stock/get_item_details.py | 4 +- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index b41cf53665e..4ccfc192ac3 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -579,12 +579,17 @@ def apply_price_discount_rule(pricing_rule, item_details, args): item_details[field] += pricing_rule.get(field, 0) if pricing_rule else args.get(field, 0) +@frappe.whitelist() def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, rate=None): from erpnext.accounts.doctype.pricing_rule.utils import ( get_applied_pricing_rules, get_pricing_rule_items, ) + if isinstance(item_details, str): + item_details = json.loads(item_details) + item_details = frappe._dict(item_details) + for d in get_applied_pricing_rules(pricing_rules): if not d or not frappe.db.exists("Pricing Rule", d): continue diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 05f512f5c4a..40f7e555ed5 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1200,8 +1200,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe let item = frappe.get_doc(cdt, cdn); // item.pricing_rules = '' frappe.run_serially([ - () => this.remove_pricing_rule(item), + () => this.remove_pricing_rule_for_item(item), () => this.conversion_factor(doc, cdt, cdn, true), + () => this.apply_price_list(item, true), //reapply price list before applying pricing rule () => this.calculate_stock_uom_rate(doc, cdt, cdn), () => this.apply_pricing_rule(item, true) ]); @@ -1448,8 +1449,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe ignore_pricing_rule() { if(this.frm.doc.ignore_pricing_rule) { - var me = this; - var item_list = []; + let me = this; + let item_list = []; $.each(this.frm.doc["items"] || [], function(i, d) { if (d.item_code) { @@ -1488,6 +1489,34 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } } + remove_pricing_rule_for_item(item) { + let me = this; + return this.frm.call({ + method: "erpnext.accounts.doctype.pricing_rule.pricing_rule.remove_pricing_rule_for_item", + args: { + pricing_rules: item.pricing_rules, + item_details: { + "doctype": item.doctype, + "name": item.name, + "item_code": item.item_code, + "pricing_rules": item.pricing_rules, + "parenttype": item.parenttype, + "parent": item.parent, + "price_list_rate": item.price_list_rate + }, + item_code: item.item_code, + rate: item.price_list_rate, + }, + callback: function(r) { + if (!r.exc && r.message) { + me.remove_pricing_rule(r.message); + me.calculate_taxes_and_totals(); + if(me.frm.doc.apply_discount_on) me.frm.trigger("apply_discount_on"); + } + } + }); + } + apply_pricing_rule(item, calculate_taxes_and_totals) { var me = this; var args = this._get_args(item); @@ -1712,8 +1741,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe this.frm.set_value("plc_conversion_rate", ""); } - var me = this; - var args = this._get_args(item); + let me = this; + let args = this._get_args(item); if (!((args.items && args.items.length) || args.price_list)) { return; } @@ -1755,7 +1784,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe "discount_amount", "margin_rate_or_amount", "rate_with_margin"]; if(item.remove_free_item) { - var items = []; + let items = []; me.frm.doc.items.forEach(d => { if(d.item_code != item.remove_free_item || !d.is_free_item) { diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index e7465959212..ebcdd11bf15 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -497,8 +497,8 @@ def update_barcode_value(out): def get_barcode_data(items_list): - # get itemwise batch no data - # exmaple: {'LED-GRE': [Batch001, Batch002]} + # get item-wise batch no data + # example: {'LED-GRE': [Batch001, Batch002]} # where LED-GRE is item code, SN0001 is serial no and Pune is warehouse itemwise_barcode = {} From d7c7748c03b66880d92d3d0450cf437d9ec51fa4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 3 Feb 2024 09:09:59 +0530 Subject: [PATCH 16/41] fix: fetch/change tax template on basis of base_net_rate instead of net_rate (#39448) fix: fetch/change tax template on basis of base_net_rate instead of net_rate (#39448) fix: change tax template on basis of base_net_rate instead of net_rate Co-authored-by: Deepesh Garg (cherry picked from commit e9fe10c6f13424630a2bfc7cfef1e1faeebab30d) Co-authored-by: Divyam Mistry <73271406+divyam-mistry@users.noreply.github.com> --- erpnext/controllers/taxes_and_totals.py | 1 + erpnext/public/js/controllers/transaction.js | 3 ++- erpnext/stock/get_item_details.py | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index f9f68a119b3..de942b7474f 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -97,6 +97,7 @@ class calculate_taxes_and_totals(object): item_doc = frappe.get_cached_doc("Item", item.item_code) args = { "net_rate": item.net_rate or item.rate, + "base_net_rate": item.base_net_rate or item.base_rate, "tax_category": self.doc.get("tax_category"), "posting_date": self.doc.get("posting_date"), "bill_date": self.doc.get("bill_date"), diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 40f7e555ed5..5da6d7ec610 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -502,6 +502,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe project: item.project || me.frm.doc.project, qty: item.qty || 1, net_rate: item.rate, + base_net_rate: item.base_net_rate, stock_qty: item.stock_qty, conversion_factor: item.conversion_factor, weight_per_unit: item.weight_per_unit, @@ -1902,7 +1903,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe if (item.item_code) { // Use combination of name and item code in case same item is added multiple times item_codes.push([item.item_code, item.name]); - item_rates[item.name] = item.net_rate; + item_rates[item.name] = item.base_net_rate; item_tax_templates[item.name] = item.item_tax_template; } }); diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index ebcdd11bf15..bed5285f2df 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -543,7 +543,7 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_t args = { "company": company, "tax_category": tax_category, - "net_rate": item_rates.get(item_code[1]), + "base_net_rate": item_rates.get(item_code[1]), } if item_tax_templates: @@ -635,7 +635,7 @@ def is_within_valid_range(args, tax): if not flt(tax.maximum_net_rate): # No range specified, just ignore return True - elif flt(tax.minimum_net_rate) <= flt(args.get("net_rate")) <= flt(tax.maximum_net_rate): + elif flt(tax.minimum_net_rate) <= flt(args.get("base_net_rate")) <= flt(tax.maximum_net_rate): return True return False From 4ee8cf390793c0ec7d059fcdc3f5c30c3c47e6e9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 4 Feb 2024 17:54:56 +0530 Subject: [PATCH 17/41] feat: reference for POS SI payments (#39523) feat: reference for POS SI payments (#39523) * feat: reference field in SI payment * fix: document link for pos si * refactor: pos invoice queries (cherry picked from commit d9a72c1e614a8d103ecb8eb13db26cff18981a52) Co-authored-by: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> --- .../doctype/bank_clearance/bank_clearance.py | 89 ++++++++++++------- .../sales_invoice_payment.json | 11 ++- .../sales_invoice_payment.py | 1 + 3 files changed, 67 insertions(+), 34 deletions(-) diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 4b97619f29f..8a505a8dee2 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -5,7 +5,9 @@ import frappe from frappe import _, msgprint from frappe.model.document import Document +from frappe.query_builder.custom import ConstantColumn from frappe.utils import flt, fmt_money, getdate +from pypika import Order import erpnext @@ -179,39 +181,62 @@ def get_payment_entries_for_bank_clearance( pos_sales_invoices, pos_purchase_invoices = [], [] if include_pos_transactions: - pos_sales_invoices = frappe.db.sql( - """ - select - "Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit, - si.posting_date, si.customer as against_account, sip.clearance_date, - account.account_currency, 0 as credit - from `tabSales Invoice Payment` sip, `tabSales Invoice` si, `tabAccount` account - where - sip.account=%(account)s and si.docstatus=1 and sip.parent = si.name - and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s - order by - si.posting_date ASC, si.name DESC - """, - {"account": account, "from": from_date, "to": to_date}, - as_dict=1, - ) + si_payment = frappe.qb.DocType("Sales Invoice Payment") + si = frappe.qb.DocType("Sales Invoice") + acc = frappe.qb.DocType("Account") - pos_purchase_invoices = frappe.db.sql( - """ - select - "Purchase Invoice" as payment_document, pi.name as payment_entry, pi.paid_amount as credit, - pi.posting_date, pi.supplier as against_account, pi.clearance_date, - account.account_currency, 0 as debit - from `tabPurchase Invoice` pi, `tabAccount` account - where - pi.cash_bank_account=%(account)s and pi.docstatus=1 and account.name = pi.cash_bank_account - and pi.posting_date >= %(from)s and pi.posting_date <= %(to)s - order by - pi.posting_date ASC, pi.name DESC - """, - {"account": account, "from": from_date, "to": to_date}, - as_dict=1, - ) + pos_sales_invoices = ( + frappe.qb.from_(si_payment) + .inner_join(si) + .on(si_payment.parent == si.name) + .inner_join(acc) + .on(si_payment.account == acc.name) + .select( + ConstantColumn("Sales Invoice").as_("payment_document"), + si.name.as_("payment_entry"), + si_payment.reference_no.as_("cheque_number"), + si_payment.amount.as_("debit"), + si.posting_date, + si.customer.as_("against_account"), + si_payment.clearance_date, + acc.account_currency, + ConstantColumn(0).as_("credit"), + ) + .where( + (si.docstatus == 1) + & (si_payment.account == account) + & (si.posting_date >= from_date) + & (si.posting_date <= to_date) + ) + .orderby(si.posting_date) + .orderby(si.name, order=Order.desc) + ).run(as_dict=True) + + pi = frappe.qb.DocType("Purchase Invoice") + + pos_purchase_invoices = ( + frappe.qb.from_(pi) + .inner_join(acc) + .on(pi.cash_bank_account == acc.name) + .select( + ConstantColumn("Purchase Invoice").as_("payment_document"), + pi.name.as_("payment_entry"), + pi.paid_amount.as_("credit"), + pi.posting_date, + pi.supplier.as_("against_account"), + pi.clearance_date, + acc.account_currency, + ConstantColumn(0).as_("debit"), + ) + .where( + (pi.docstatus == 1) + & (pi.cash_bank_account == account) + & (pi.posting_date >= from_date) + & (pi.posting_date <= to_date) + ) + .orderby(pi.posting_date) + .orderby(pi.name, order=Order.desc) + ).run(as_dict=True) entries = ( list(payment_entries) diff --git a/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.json b/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.json index 5ab46b7fd5c..bd59f65dd4c 100644 --- a/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.json +++ b/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.json @@ -8,6 +8,7 @@ "default", "mode_of_payment", "amount", + "reference_no", "column_break_3", "account", "type", @@ -75,11 +76,16 @@ "hidden": 1, "label": "Default", "read_only": 1 + }, + { + "fieldname": "reference_no", + "fieldtype": "Data", + "label": "Reference No" } ], "istable": 1, "links": [], - "modified": "2020-08-03 12:45:39.986598", + "modified": "2024-01-23 16:20:06.436979", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Payment", @@ -87,5 +93,6 @@ "permissions": [], "quick_entry": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.py b/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.py index 57d01424063..e460a01155e 100644 --- a/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.py +++ b/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.py @@ -23,6 +23,7 @@ class SalesInvoicePayment(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data + reference_no: DF.Data | None type: DF.ReadOnly | None # end: auto-generated types From 5fe40ac0856d07903d1fbbe291c668435d07a77b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 4 Feb 2024 22:59:17 +0530 Subject: [PATCH 18/41] fix(work order): resolve type error during job card creation (backport #39713) (#39714) fix(work order): resolve type error during job card creation (#39713) fix: type error (cherry picked from commit c81d597ca510706cf3ca7230f21b1630e7515bf5) Co-authored-by: Vishnu VS --- erpnext/manufacturing/doctype/work_order/work_order.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 78bfc767d6f..02a1d27432c 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1511,14 +1511,14 @@ def get_serial_nos_for_work_order(work_order, production_item): def validate_operation_data(row): - if row.get("qty") <= 0: + if flt(row.get("qty")) <= 0: frappe.throw( _("Quantity to Manufacture can not be zero for the operation {0}").format( frappe.bold(row.get("operation")) ) ) - if row.get("qty") > row.get("pending_qty"): + if flt(row.get("qty")) > flt(row.get("pending_qty")): frappe.throw( _("For operation {0}: Quantity ({1}) can not be greter than pending quantity({2})").format( frappe.bold(row.get("operation")), From 3eac436e7bbbe7df45db3d9555f93cea14e6252b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 10:14:42 +0530 Subject: [PATCH 19/41] fix: production plan date filters for orders (backport #39702) (#39720) fix: production plan date filters for orders (#39702) (cherry picked from commit 407045a1dee4d5d1e4f08f0e78871942ded9e709) Co-authored-by: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> --- .../doctype/production_plan/production_plan.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 51658a03a7c..d460108d7b4 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1334,10 +1334,10 @@ def get_sales_orders(self): ) date_field_mapper = { - "from_date": self.from_date >= so.transaction_date, - "to_date": self.to_date <= so.transaction_date, - "from_delivery_date": self.from_delivery_date >= so_item.delivery_date, - "to_delivery_date": self.to_delivery_date <= so_item.delivery_date, + "from_date": so.transaction_date >= self.from_date, + "to_date": so.transaction_date <= self.to_date, + "from_delivery_date": so_item.delivery_date >= self.from_delivery_date, + "to_delivery_date": so_item.delivery_date <= self.to_delivery_date, } for field, value in date_field_mapper.items(): From a40864dedeb9f17061a04a57451086b24b56a9ab Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 1 Feb 2024 19:29:06 +0530 Subject: [PATCH 20/41] fix: incorrect landed cost voucher amount (cherry picked from commit d78a1e78148a3702990ae347a2b1c31b580c308e) --- .../doctype/landed_cost_voucher/landed_cost_voucher.py | 7 +++++++ .../stock/doctype/purchase_receipt/purchase_receipt.py | 10 +++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index c6518b45cd7..aa5b2793c58 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -149,6 +149,13 @@ class LandedCostVoucher(Document): self.get("items")[item_count - 1].applicable_charges += diff def validate_applicable_charges_for_item(self): + if self.distribute_charges_based_on == "Distribute Manually" and len(self.taxes) > 1: + frappe.throw( + _( + "Please keep one Applicable Charges, when 'Distribute Charges Based On' is 'Distribute Manually'. For more charges, please create another Landed Cost Voucher." + ) + ) + based_on = self.distribute_charges_based_on.lower() if based_on != "distribute manually": diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 970949106c2..8da05966317 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1358,16 +1358,16 @@ def get_item_account_wise_additional_cost(purchase_document): for lcv in landed_cost_vouchers: landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", lcv.parent) + based_on_field = None # Use amount field for total item cost for manually cost distributed LCVs - if landed_cost_voucher_doc.distribute_charges_based_on == "Distribute Manually": - based_on_field = "amount" - else: + if landed_cost_voucher_doc.distribute_charges_based_on != "Distribute Manually": based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on) total_item_cost = 0 - for item in landed_cost_voucher_doc.items: - total_item_cost += item.get(based_on_field) + if based_on_field: + for item in landed_cost_voucher_doc.items: + total_item_cost += item.get(based_on_field) for item in landed_cost_voucher_doc.items: if item.receipt_document == purchase_document: From 1b2831bdfe0b650c7c285f17907ba241f163dabb Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 31 Jan 2024 11:32:17 +0530 Subject: [PATCH 21/41] perf: timeout for auto material request through reorder level (cherry picked from commit 951023f434a36ef03f874b3dcbd4f995168b7b5a) --- erpnext/controllers/selling_controller.py | 2 +- .../doctype/stock_entry/test_stock_entry.py | 42 +++++ erpnext/stock/get_item_details.py | 7 +- erpnext/stock/reorder_item.py | 163 +++++++++++++----- 4 files changed, 172 insertions(+), 42 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 22b0d08c92a..0556c3c2a23 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -602,7 +602,7 @@ class SellingController(StockController): if self.doctype in ["Sales Order", "Quotation"]: for item in self.items: item.gross_profit = flt( - ((item.base_rate - item.valuation_rate) * item.stock_qty), self.precision("amount", item) + ((item.base_rate - flt(item.valuation_rate)) * item.stock_qty), self.precision("amount", item) ) def set_customer_address(self): diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 420afe8c4f7..4e3214ebeca 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -1776,6 +1776,48 @@ class TestStockEntry(FrappeTestCase): self.assertRaises(frappe.ValidationError, se1.cancel) + def test_auto_reorder_level(self): + from erpnext.stock.reorder_item import reorder_item + + item_doc = make_item( + "Test Auto Reorder Item - 001", + properties={"stock_uom": "Kg", "purchase_uom": "Nos", "is_stock_item": 1}, + uoms=[{"uom": "Nos", "conversion_factor": 5}], + ) + + if not frappe.db.exists("Item Reorder", {"parent": item_doc.name}): + item_doc.append( + "reorder_levels", + { + "warehouse_reorder_level": 0, + "warehouse_reorder_qty": 10, + "warehouse": "_Test Warehouse - _TC", + "material_request_type": "Purchase", + }, + ) + + item_doc.save(ignore_permissions=True) + + frappe.db.set_single_value("Stock Settings", "auto_indent", 1) + + mr_list = reorder_item() + + frappe.db.set_single_value("Stock Settings", "auto_indent", 0) + mrs = frappe.get_all( + "Material Request Item", + fields=["qty", "stock_uom", "stock_qty"], + filters={"item_code": item_doc.name, "uom": "Nos"}, + ) + + for mri in mrs: + self.assertEqual(mri.stock_uom, "Kg") + self.assertEqual(mri.stock_qty, 10) + self.assertEqual(mri.qty, 2) + + for mr in mr_list: + mr.cancel() + mr.delete() + def make_serialized_item(**args): args = frappe._dict(args) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index bed5285f2df..1cb10575cd1 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -86,7 +86,8 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru get_party_item_code(args, item, out) - set_valuation_rate(out, args) + if args.get("doctype") in ["Sales Order", "Quotation"]: + set_valuation_rate(out, args) update_party_blanket_order(args, out) @@ -269,7 +270,9 @@ def get_basic_details(args, item, overwrite_warehouse=True): if not item: item = frappe.get_doc("Item", args.get("item_code")) - if item.variant_of and not item.taxes: + if ( + item.variant_of and not item.taxes and frappe.db.exists("Item Tax", {"parent": item.variant_of}) + ): item.update_template_tables() item_defaults = get_item_defaults(item.name, args.company) diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py index caa3d66ac39..51913005afd 100644 --- a/erpnext/stock/reorder_item.py +++ b/erpnext/stock/reorder_item.py @@ -34,73 +34,157 @@ def _reorder_item(): erpnext.get_default_company() or frappe.db.sql("""select name from tabCompany limit 1""")[0][0] ) - items_to_consider = frappe.db.sql_list( - """select name from `tabItem` item - where is_stock_item=1 and has_variants=0 - and disabled=0 - and (end_of_life is null or end_of_life='0000-00-00' or end_of_life > %(today)s) - and (exists (select name from `tabItem Reorder` ir where ir.parent=item.name) - or (variant_of is not null and variant_of != '' - and exists (select name from `tabItem Reorder` ir where ir.parent=item.variant_of)) - )""", - {"today": nowdate()}, - ) + items_to_consider = get_items_for_reorder() if not items_to_consider: return item_warehouse_projected_qty = get_item_warehouse_projected_qty(items_to_consider) - def add_to_material_request( - item_code, warehouse, reorder_level, reorder_qty, material_request_type, warehouse_group=None - ): - if warehouse not in warehouse_company: + def add_to_material_request(**kwargs): + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + if kwargs.warehouse not in warehouse_company: # a disabled warehouse return - reorder_level = flt(reorder_level) - reorder_qty = flt(reorder_qty) + reorder_level = flt(kwargs.reorder_level) + reorder_qty = flt(kwargs.reorder_qty) # projected_qty will be 0 if Bin does not exist - if warehouse_group: - projected_qty = flt(item_warehouse_projected_qty.get(item_code, {}).get(warehouse_group)) + if kwargs.warehouse_group: + projected_qty = flt( + item_warehouse_projected_qty.get(kwargs.item_code, {}).get(kwargs.warehouse_group) + ) else: - projected_qty = flt(item_warehouse_projected_qty.get(item_code, {}).get(warehouse)) + projected_qty = flt( + item_warehouse_projected_qty.get(kwargs.item_code, {}).get(kwargs.warehouse) + ) if (reorder_level or reorder_qty) and projected_qty <= reorder_level: deficiency = reorder_level - projected_qty if deficiency > reorder_qty: reorder_qty = deficiency - company = warehouse_company.get(warehouse) or default_company + company = warehouse_company.get(kwargs.warehouse) or default_company - material_requests[material_request_type].setdefault(company, []).append( - {"item_code": item_code, "warehouse": warehouse, "reorder_qty": reorder_qty} + material_requests[kwargs.material_request_type].setdefault(company, []).append( + { + "item_code": kwargs.item_code, + "warehouse": kwargs.warehouse, + "reorder_qty": reorder_qty, + "item_details": kwargs.item_details, + } ) - for item_code in items_to_consider: - item = frappe.get_doc("Item", item_code) + for item_code, reorder_levels in items_to_consider.items(): + for d in reorder_levels: + if d.has_variants: + continue - if item.variant_of and not item.get("reorder_levels"): - item.update_template_tables() - - if item.get("reorder_levels"): - for d in item.get("reorder_levels"): - add_to_material_request( - item_code, - d.warehouse, - d.warehouse_reorder_level, - d.warehouse_reorder_qty, - d.material_request_type, - warehouse_group=d.warehouse_group, - ) + add_to_material_request( + item_code=item_code, + warehouse=d.warehouse, + reorder_level=d.warehouse_reorder_level, + reorder_qty=d.warehouse_reorder_qty, + material_request_type=d.material_request_type, + warehouse_group=d.warehouse_group, + item_details=frappe._dict( + { + "item_code": item_code, + "name": item_code, + "item_name": d.item_name, + "item_group": d.item_group, + "brand": d.brand, + "description": d.description, + "stock_uom": d.stock_uom, + "purchase_uom": d.purchase_uom, + } + ), + ) if material_requests: return create_material_request(material_requests) +def get_items_for_reorder() -> dict[str, list]: + reorder_table = frappe.qb.DocType("Item Reorder") + item_table = frappe.qb.DocType("Item") + + query = ( + frappe.qb.from_(reorder_table) + .inner_join(item_table) + .on(reorder_table.parent == item_table.name) + .select( + reorder_table.warehouse, + reorder_table.warehouse_group, + reorder_table.material_request_type, + reorder_table.warehouse_reorder_level, + reorder_table.warehouse_reorder_qty, + item_table.name, + item_table.stock_uom, + item_table.purchase_uom, + item_table.description, + item_table.item_name, + item_table.item_group, + item_table.brand, + item_table.variant_of, + item_table.has_variants, + ) + .where( + (item_table.disabled == 0) + & (item_table.is_stock_item == 1) + & ( + (item_table.end_of_life.isnull()) + | (item_table.end_of_life > nowdate()) + | (item_table.end_of_life == "0000-00-00") + ) + ) + ) + + data = query.run(as_dict=True) + itemwise_reorder = frappe._dict({}) + for d in data: + itemwise_reorder.setdefault(d.name, []).append(d) + + itemwise_reorder = get_reorder_levels_for_variants(itemwise_reorder) + + return itemwise_reorder + + +def get_reorder_levels_for_variants(itemwise_reorder): + item_table = frappe.qb.DocType("Item") + + query = ( + frappe.qb.from_(item_table) + .select( + item_table.name, + item_table.variant_of, + ) + .where( + (item_table.disabled == 0) + & (item_table.is_stock_item == 1) + & ( + (item_table.end_of_life.isnull()) + | (item_table.end_of_life > nowdate()) + | (item_table.end_of_life == "0000-00-00") + ) + & (item_table.variant_of.notnull()) + ) + ) + + variants_item = query.run(as_dict=True) + for row in variants_item: + if not itemwise_reorder.get(row.name) and itemwise_reorder.get(row.variant_of): + itemwise_reorder.setdefault(row.name, []).extend(itemwise_reorder.get(row.variant_of, [])) + + return itemwise_reorder + + def get_item_warehouse_projected_qty(items_to_consider): item_warehouse_projected_qty = {} + items_to_consider = list(items_to_consider.keys()) for item_code, warehouse, projected_qty in frappe.db.sql( """select item_code, warehouse, projected_qty @@ -164,7 +248,7 @@ def create_material_request(material_requests): for d in items: d = frappe._dict(d) - item = frappe.get_doc("Item", d.item_code) + item = d.get("item_details") uom = item.stock_uom conversion_factor = 1.0 @@ -190,6 +274,7 @@ def create_material_request(material_requests): "item_code": d.item_code, "schedule_date": add_days(nowdate(), cint(item.lead_time_days)), "qty": qty, + "conversion_factor": conversion_factor, "uom": uom, "stock_uom": item.stock_uom, "warehouse": d.warehouse, From 5e29aab83baece451af0636118da3112ef8f963b Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 5 Feb 2024 11:46:39 +0530 Subject: [PATCH 22/41] perf: memory consumption for the stock balance report (#39626) (cherry picked from commit b70f3de16be169a723842a3a99c046a3809d0768) --- .../report/stock_balance/stock_balance.py | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index ed84a5c2d5a..269323810b4 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -90,8 +90,7 @@ class StockBalanceReport(object): self.opening_data.setdefault(group_by_key, entry) def prepare_new_data(self): - if not self.sle_entries: - return + self.item_warehouse_map = self.get_item_warehouse_map() if self.filters.get("show_stock_ageing_data"): self.filters["show_warehouse_wise_stock"] = True @@ -99,7 +98,8 @@ class StockBalanceReport(object): _func = itemgetter(1) - self.item_warehouse_map = self.get_item_warehouse_map() + del self.sle_entries + sre_details = self.get_sre_reserved_qty_details() variant_values = {} @@ -143,15 +143,22 @@ class StockBalanceReport(object): item_warehouse_map = {} self.opening_vouchers = self.get_opening_vouchers() - for entry in self.sle_entries: - group_by_key = self.get_group_by_key(entry) - if group_by_key not in item_warehouse_map: - self.initialize_data(item_warehouse_map, group_by_key, entry) + if self.filters.get("show_stock_ageing_data"): + self.sle_entries = self.sle_query.run(as_dict=True) - self.prepare_item_warehouse_map(item_warehouse_map, entry, group_by_key) + with frappe.db.unbuffered_cursor(): + if not self.filters.get("show_stock_ageing_data"): + self.sle_entries = self.sle_query.run(as_dict=True, as_iterator=True) - if self.opening_data.get(group_by_key): - del self.opening_data[group_by_key] + for entry in self.sle_entries: + group_by_key = self.get_group_by_key(entry) + if group_by_key not in item_warehouse_map: + self.initialize_data(item_warehouse_map, group_by_key, entry) + + self.prepare_item_warehouse_map(item_warehouse_map, entry, group_by_key) + + if self.opening_data.get(group_by_key): + del self.opening_data[group_by_key] for group_by_key, entry in self.opening_data.items(): if group_by_key not in item_warehouse_map: @@ -252,7 +259,8 @@ class StockBalanceReport(object): .where( (table.docstatus == 1) & (table.company == self.filters.company) - & ((table.to_date <= self.from_date)) + & (table.to_date <= self.from_date) + & (table.status == "Completed") ) .orderby(table.to_date, order=Order.desc) .limit(1) @@ -305,7 +313,7 @@ class StockBalanceReport(object): if self.filters.get("company"): query = query.where(sle.company == self.filters.get("company")) - self.sle_entries = query.run(as_dict=True) + self.sle_query = query def apply_inventory_dimensions_filters(self, query, sle) -> str: inventory_dimension_fields = self.get_inventory_dimension_fields() From 986273b6d31e803c81516e3f1a1fde5b2a283531 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 14:05:44 +0530 Subject: [PATCH 23/41] feat: copy emails from lead to customer (#38647) feat: copy emails from lead to customer (cherry picked from commit 906ac093e37694c4616cffb961b131ce9002315b) Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com> --- erpnext/selling/doctype/customer/customer.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 835b545cbed..75341feeba5 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -230,6 +230,7 @@ class Customer(TransactionBase): if self.flags.is_new_doc: self.link_lead_address_and_contact() + self.copy_communication() self.update_customer_groups() @@ -287,6 +288,17 @@ class Customer(TransactionBase): linked_doc.append("links", dict(link_doctype="Customer", link_name=self.name)) linked_doc.save(ignore_permissions=self.flags.ignore_permissions) + def copy_communication(self): + if not self.lead_name or not frappe.db.get_single_value( + "CRM Settings", "carry_forward_communication_and_comments" + ): + return + + from erpnext.crm.utils import copy_comments, link_communications + + copy_comments("Lead", self.lead_name, self) + link_communications("Lead", self.lead_name, self) + def validate_name_with_customer_group(self): if frappe.db.exists("Customer Group", self.name): frappe.throw( From 1822325f340829b904676d621146a68c6f84b59c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 14:34:33 +0530 Subject: [PATCH 24/41] feat: Period-wise closing entries for TB (#39712) feat: Period-wise closing entries for TB (#39712) (cherry picked from commit 6e6c818084ee3fbbb2d44a58acb5c78c9f5b61ac) Co-authored-by: Deepesh Garg --- erpnext/accounts/report/trial_balance/trial_balance.js | 10 ++++++++-- erpnext/accounts/report/trial_balance/trial_balance.py | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/trial_balance/trial_balance.js b/erpnext/accounts/report/trial_balance/trial_balance.js index 2c4c7620736..5374ac16d11 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.js +++ b/erpnext/accounts/report/trial_balance/trial_balance.js @@ -78,8 +78,14 @@ frappe.query_reports["Trial Balance"] = { "options": erpnext.get_presentation_currency_list() }, { - "fieldname": "with_period_closing_entry", - "label": __("Period Closing Entry"), + "fieldname": "with_period_closing_entry_for_opening", + "label": __("With Period Closing Entry For Opening Balances"), + "fieldtype": "Check", + "default": 1 + }, + { + "fieldname": "with_period_closing_entry_for_current_period", + "label": __("Period Closing Entry For Current Period"), "fieldtype": "Check", "default": 1 }, diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 8b7f0bbc006..2ff0eff662d 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -116,7 +116,7 @@ def get_data(filters): max_rgt, filters, gl_entries_by_account, - ignore_closing_entries=not flt(filters.with_period_closing_entry), + ignore_closing_entries=not flt(filters.with_period_closing_entry_for_current_period), ignore_opening_entries=True, ) @@ -249,7 +249,7 @@ def get_opening_balance( ): opening_balance = opening_balance.where(closing_balance.posting_date >= filters.year_start_date) - if not flt(filters.with_period_closing_entry): + if not flt(filters.with_period_closing_entry_for_opening): if doctype == "Account Closing Balance": opening_balance = opening_balance.where(closing_balance.is_period_closing_voucher_entry == 0) else: From f2466846946b83b5a03fa381cae56edd6573f73a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 16:09:00 +0530 Subject: [PATCH 25/41] fix: remove applied pricing rule on qty change (backport #39688) (#39737) fix: remove pricing rule (cherry picked from commit 7c6a5a0f23b948953815870b726c30b0fd076338) Co-authored-by: s-aga-r --- erpnext/controllers/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 03c3cd2f9df..81a7a101e5e 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -693,7 +693,7 @@ class AccountsController(TransactionBase): if self.get("is_subcontracted"): args["is_subcontracted"] = self.is_subcontracted - ret = get_item_details(args, self, for_validate=True, overwrite_warehouse=False) + ret = get_item_details(args, self, for_validate=for_validate, overwrite_warehouse=False) for fieldname, value in ret.items(): if item.meta.get_field(fieldname) and value is not None: From b12672082673418c40cebc22b8f7224c0cbe2757 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 16:31:20 +0530 Subject: [PATCH 26/41] fix: Blanket Order Ordered Quantity (backport #39725) (#39739) * fix: disable no-copy for blanket order in PO (cherry picked from commit 5ce5c352e4ab2293b3f5b5dac9bc1a2a1912e620) * fix: update BO Ordered Quantity on PO Close/Open (cherry picked from commit 61ded697a7384d8ef133a42424d8a14763bb6061) * test: BO on PO Close/Open (cherry picked from commit 27d6c8b6d52ada292eef5c42506e95bcf933eec8) --------- Co-authored-by: s-aga-r --- .../doctype/purchase_order/purchase_order.py | 1 + .../purchase_order/test_purchase_order.py | 25 +++++++++++++++++++ .../purchase_order_item.json | 4 +-- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index a28a310306f..22860dfdbf5 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -453,6 +453,7 @@ class PurchaseOrder(BuyingController): self.update_ordered_qty() self.update_reserved_qty_for_subcontract() self.update_subcontracting_order_status() + self.update_blanket_order() self.notify_update() clear_doctype_notifications(self) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index f80a00a95f6..1216fb92658 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -814,6 +814,30 @@ class TestPurchaseOrder(FrappeTestCase): # To test if the PO does NOT have a Blanket Order self.assertEqual(po_doc.items[0].blanket_order, None) + def test_blanket_order_on_po_close_and_open(self): + # Step - 1: Create Blanket Order + bo = make_blanket_order(blanket_order_type="Purchasing", quantity=10, rate=10) + + # Step - 2: Create Purchase Order + po = create_purchase_order( + item_code="_Test Item", qty=5, against_blanket_order=1, against_blanket=bo.name + ) + + bo.load_from_db() + self.assertEqual(bo.items[0].ordered_qty, 5) + + # Step - 3: Close Purchase Order + po.update_status("Closed") + + bo.load_from_db() + self.assertEqual(bo.items[0].ordered_qty, 0) + + # Step - 4: Re-Open Purchase Order + po.update_status("Re-open") + + bo.load_from_db() + self.assertEqual(bo.items[0].ordered_qty, 5) + def test_payment_terms_are_fetched_when_creating_purchase_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_terms_template, @@ -1113,6 +1137,7 @@ def create_purchase_order(**args): "schedule_date": add_days(nowdate(), 1), "include_exploded_items": args.get("include_exploded_items", 1), "against_blanket_order": args.against_blanket_order, + "against_blanket": args.against_blanket, "material_request": args.material_request, "material_request_item": args.material_request_item, }, diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 5a24cc2e92d..e3e8def7ffd 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -545,7 +545,6 @@ "fieldname": "blanket_order", "fieldtype": "Link", "label": "Blanket Order", - "no_copy": 1, "options": "Blanket Order" }, { @@ -553,7 +552,6 @@ "fieldname": "blanket_order_rate", "fieldtype": "Currency", "label": "Blanket Order Rate", - "no_copy": 1, "print_hide": 1, "read_only": 1 }, @@ -917,7 +915,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-24 13:24:41.298416", + "modified": "2024-02-05 11:23:24.859435", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", From 75bd1e6b65ceceb91db4b9494c77a528bbe9994c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 6 Oct 2023 17:45:26 +0530 Subject: [PATCH 27/41] feat: visual plant floor (cherry picked from commit 68c997aa067f342f6e432e9f9c84416a0a4cc1bf) --- .../doctype/plant_floor/__init__.py | 0 .../doctype/plant_floor/plant_floor.js | 19 ++ .../doctype/plant_floor/plant_floor.json | 81 ++++++++ .../doctype/plant_floor/plant_floor.py | 21 ++ .../doctype/plant_floor/test_plant_floor.py | 9 + .../doctype/workstation/workstation.js | 61 ++++++ .../doctype/workstation/workstation.json | 112 +++++++++- .../doctype/workstation/workstation.py | 150 ++++++++++++++ .../workstation/workstation_job_card.html | 97 +++++++++ .../doctype/workstation/workstation_list.js | 15 +- .../workstation_working_hour.json | 192 +++++------------- .../page/visual_plant_floor/__init__.py | 0 .../visual_plant_floor/visual_plant_floor.js | 13 ++ .../visual_plant_floor.json | 29 +++ .../manufacturing/manufacturing.json | 19 +- erpnext/public/js/erpnext.bundle.js | 2 + .../js/plant_floor_visual/visual_plant.js | 157 ++++++++++++++ .../visual_plant_floor_template.html | 19 ++ erpnext/public/scss/erpnext.scss | 51 +++++ 19 files changed, 896 insertions(+), 151 deletions(-) create mode 100644 erpnext/manufacturing/doctype/plant_floor/__init__.py create mode 100644 erpnext/manufacturing/doctype/plant_floor/plant_floor.js create mode 100644 erpnext/manufacturing/doctype/plant_floor/plant_floor.json create mode 100644 erpnext/manufacturing/doctype/plant_floor/plant_floor.py create mode 100644 erpnext/manufacturing/doctype/plant_floor/test_plant_floor.py create mode 100644 erpnext/manufacturing/doctype/workstation/workstation_job_card.html create mode 100644 erpnext/manufacturing/page/visual_plant_floor/__init__.py create mode 100644 erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.js create mode 100644 erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.json create mode 100644 erpnext/public/js/plant_floor_visual/visual_plant.js create mode 100644 erpnext/public/js/templates/visual_plant_floor_template.html diff --git a/erpnext/manufacturing/doctype/plant_floor/__init__.py b/erpnext/manufacturing/doctype/plant_floor/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/doctype/plant_floor/plant_floor.js b/erpnext/manufacturing/doctype/plant_floor/plant_floor.js new file mode 100644 index 00000000000..427893743af --- /dev/null +++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.js @@ -0,0 +1,19 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Plant Floor", { + refresh(frm) { + frm.trigger('prepare_dashboard') + }, + + prepare_dashboard(frm) { + let wrapper = $(frm.fields_dict["plant_dashboard"].wrapper); + wrapper.empty(); + + frappe.visual_plant_floor = new frappe.ui.VisualPlantFloor({ + wrapper: wrapper, + skip_filters: true, + plant_floor: frm.doc.name, + }); + }, +}); diff --git a/erpnext/manufacturing/doctype/plant_floor/plant_floor.json b/erpnext/manufacturing/doctype/plant_floor/plant_floor.json new file mode 100644 index 00000000000..aa6eb1dd40f --- /dev/null +++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.json @@ -0,0 +1,81 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:floor_name", + "creation": "2023-10-06 15:06:07.976066", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "workstations_tab", + "plant_dashboard", + "details_tab", + "column_break_mvbx", + "floor_name", + "section_break_cczv", + "volumetric_weight" + ], + "fields": [ + { + "fieldname": "floor_name", + "fieldtype": "Data", + "label": "Floor Name", + "unique": 1 + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "workstations_tab", + "fieldtype": "Tab Break", + "label": "Dashboard" + }, + { + "fieldname": "plant_dashboard", + "fieldtype": "HTML", + "label": "Plant Dashboard" + }, + { + "fieldname": "details_tab", + "fieldtype": "Tab Break", + "label": "Details" + }, + { + "fieldname": "column_break_mvbx", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_cczv", + "fieldtype": "Section Break" + }, + { + "fieldname": "volumetric_weight", + "fieldtype": "Float", + "label": "Volumetric Weight" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-12-04 15:36:09.641203", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Plant Floor", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/plant_floor/plant_floor.py b/erpnext/manufacturing/doctype/plant_floor/plant_floor.py new file mode 100644 index 00000000000..729cc3337a9 --- /dev/null +++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.py @@ -0,0 +1,21 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class PlantFloor(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + floor_name: DF.Data | None + volumetric_weight: DF.Float + # end: auto-generated types + + pass diff --git a/erpnext/manufacturing/doctype/plant_floor/test_plant_floor.py b/erpnext/manufacturing/doctype/plant_floor/test_plant_floor.py new file mode 100644 index 00000000000..2fac2113366 --- /dev/null +++ b/erpnext/manufacturing/doctype/plant_floor/test_plant_floor.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestPlantFloor(FrappeTestCase): + pass diff --git a/erpnext/manufacturing/doctype/workstation/workstation.js b/erpnext/manufacturing/doctype/workstation/workstation.js index f830b170ed0..4ffc506f52e 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.js +++ b/erpnext/manufacturing/doctype/workstation/workstation.js @@ -2,6 +2,28 @@ // License: GNU General Public License v3. See license.txt frappe.ui.form.on("Workstation", { + set_illustration_image(frm) { + let status_image_field = frm.doc.status == "Production" ? frm.doc.on_status_image : frm.doc.off_status_image; + if (status_image_field) { + frm.sidebar.image_wrapper.find(".sidebar-image").attr("src", status_image_field); + } + }, + + refresh(frm) { + frm.trigger("set_illustration_image"); + frm.trigger("prepapre_dashboard"); + }, + + prepapre_dashboard(frm) { + let $parent = $(frm.fields_dict["workstation_dashboard"].wrapper); + $parent.empty(); + + let workstation_dashboard = new WorkstationDashboard({ + wrapper: $parent, + frm: frm + }); + }, + onload(frm) { if(frm.is_new()) { @@ -54,3 +76,42 @@ frappe.tour['Workstation'] = [ ]; + + +class WorkstationDashboard { + constructor({ wrapper, frm }) { + this.$wrapper = $(wrapper); + this.frm = frm; + + this.prepapre_dashboard(); + } + + prepapre_dashboard() { + frappe.call({ + method: "erpnext.manufacturing.doctype.workstation.workstation.get_job_cards", + args: { + workstation: this.frm.doc.name + }, + callback: (r) => { + if (r.message) { + this.render_job_cards(r.message); + } + } + }); + } + + render_job_cards(job_cards) { + let template = frappe.render_template("workstation_job_card", { + data: job_cards + }); + + this.$wrapper.html(template); + this.$wrapper.find(".collapse-indicator-job").on("click", (e) => { + $(e.currentTarget).closest(".form-dashboard-section").find(".section-body-job-card").toggleClass("hide") + if ($(e.currentTarget).closest(".form-dashboard-section").find(".section-body-job-card").hasClass("hide")) + $(e.currentTarget).html(frappe.utils.icon("es-line-down", "sm", "mb-1")) + else + $(e.currentTarget).html(frappe.utils.icon("es-line-up", "sm", "mb-1")) + }); + } +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/workstation/workstation.json b/erpnext/manufacturing/doctype/workstation/workstation.json index 881cba0cce0..5912714052b 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.json +++ b/erpnext/manufacturing/doctype/workstation/workstation.json @@ -8,10 +8,24 @@ "document_type": "Setup", "engine": "InnoDB", "field_order": [ + "dashboard_tab", + "workstation_dashboard", + "details_tab", "workstation_name", - "production_capacity", - "column_break_3", "workstation_type", + "plant_floor", + "column_break_3", + "production_capacity", + "warehouse", + "production_capacity_section", + "parts_per_hour", + "workstation_status_tab", + "status", + "column_break_glcv", + "illustration_section", + "on_status_image", + "column_break_etmc", + "off_status_image", "over_heads", "hour_rate_electricity", "hour_rate_consumable", @@ -24,7 +38,9 @@ "description", "working_hours_section", "holiday_list", - "working_hours" + "working_hours", + "total_working_hours", + "connections_tab" ], "fields": [ { @@ -120,9 +136,10 @@ }, { "default": "1", + "description": "Run parallel job cards in a workstation", "fieldname": "production_capacity", "fieldtype": "Int", - "label": "Production Capacity", + "label": "Job Capacity", "reqd": 1 }, { @@ -145,12 +162,97 @@ { "fieldname": "section_break_11", "fieldtype": "Section Break" + }, + { + "fieldname": "plant_floor", + "fieldtype": "Link", + "label": "Plant Floor", + "options": "Plant Floor" + }, + { + "fieldname": "workstation_status_tab", + "fieldtype": "Tab Break", + "label": "Workstation Status" + }, + { + "fieldname": "illustration_section", + "fieldtype": "Section Break", + "label": "Status Illustration" + }, + { + "fieldname": "column_break_etmc", + "fieldtype": "Column Break" + }, + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "options": "Production\nOff\nIdle\nProblem\nMaintenance\nSetup" + }, + { + "fieldname": "column_break_glcv", + "fieldtype": "Column Break" + }, + { + "fieldname": "on_status_image", + "fieldtype": "Attach Image", + "label": "Active Status" + }, + { + "fieldname": "off_status_image", + "fieldtype": "Attach Image", + "label": "Inactive Status" + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "label": "Warehouse", + "options": "Warehouse" + }, + { + "fieldname": "production_capacity_section", + "fieldtype": "Section Break", + "label": "Production Capacity" + }, + { + "fieldname": "parts_per_hour", + "fieldtype": "Float", + "label": "Parts Per Hour" + }, + { + "fieldname": "total_working_hours", + "fieldtype": "Float", + "label": "Total Working Hours" + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "dashboard_tab", + "fieldtype": "Tab Break", + "label": "Job Cards" + }, + { + "fieldname": "details_tab", + "fieldtype": "Tab Break", + "label": "Details" + }, + { + "fieldname": "connections_tab", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 + }, + { + "fieldname": "workstation_dashboard", + "fieldtype": "HTML", + "label": "Workstation Dashboard" } ], "icon": "icon-wrench", "idx": 1, + "image_field": "on_status_image", "links": [], - "modified": "2022-11-04 17:39:01.549346", + "modified": "2023-11-30 12:43:35.808845", "modified_by": "Administrator", "module": "Manufacturing", "name": "Workstation", diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index 0a247fc431d..c2b1e63067c 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -11,7 +11,11 @@ from frappe.utils import ( comma_and, flt, formatdate, + get_link_to_form, + get_time, + get_url_to_form, getdate, + time_diff_in_hours, time_diff_in_seconds, to_timedelta, ) @@ -60,6 +64,23 @@ class Workstation(Document): def before_save(self): self.set_data_based_on_workstation_type() self.set_hour_rate() + self.set_total_working_hours() + + def set_total_working_hours(self): + self.total_working_hours = 0.0 + for row in self.working_hours: + self.validate_working_hours(row) + + if row.start_time and row.end_time: + row.hours = flt(time_diff_in_hours(row.end_time, row.start_time), row.precision("hours")) + self.total_working_hours += row.hours + + def validate_working_hours(self, row): + if not (row.start_time and row.end_time): + frappe.throw(_("Row #{0}: Start Time and End Time are required").format(row.idx)) + + if get_time(row.start_time) >= get_time(row.end_time): + frappe.throw(_("Row #{0}: Start Time must be before End Time").format(row.idx)) def set_hour_rate(self): self.hour_rate = ( @@ -144,6 +165,86 @@ class Workstation(Document): return schedule_date +@frappe.whitelist() +def get_job_cards(workstation): + if frappe.has_permission("Job Card", "read"): + jc_data = frappe.get_all( + "Job Card", + fields=[ + "name", + "production_item", + "work_order", + "operation", + "total_completed_qty", + "for_quantity", + "status", + "expected_start_date", + "expected_end_date", + "time_required", + "wip_warehouse", + ], + filters={ + "workstation": workstation, + "docstatus": ("<", 2), + "status": ["not in", ["Completed", "Stopped"]], + }, + order_by="expected_start_date, expected_end_date", + ) + + job_cards = [row.name for row in jc_data] + raw_materials = get_raw_materials(job_cards) + + for row in jc_data: + row.progress_percent = ( + flt(row.total_completed_qty / row.for_quantity * 100, 2) if row.for_quantity else 0 + ) + row.progress_title = _("Total completed quantity: {0}").format(row.total_completed_qty) + row.status_color = get_status_color(row.status) + row.job_card_link = get_link_to_form("Job Card", row.name) + row.work_order_link = get_link_to_form("Work Order", row.work_order) + + row.raw_materials = raw_materials.get(row.name, []) + + return jc_data + + +def get_status_color(status): + colos_map = { + "Pending": "var(--bg-blue)", + "In Process": "var(--bg-yellow)", + "Submitted": "var(--bg-blue)", + "Open": "var(--bg-gray)", + "Closed": "var(--bg-green)", + "Work In Progress": "var(--bg-orange)", + } + + return colos_map.get(status, "var(--bg-blue)") + + +def get_raw_materials(job_cards): + raw_materials = {} + + data = frappe.get_all( + "Job Card Item", + fields=[ + "parent", + "item_code", + "item_group", + "uom", + "item_name", + "source_warehouse", + "required_qty", + "transferred_qty", + ], + filters={"parent": ["in", job_cards]}, + ) + + for row in data: + raw_materials.setdefault(row.parent, []).append(row) + + return raw_materials + + @frappe.whitelist() def get_default_holiday_list(): return frappe.get_cached_value( @@ -201,3 +302,52 @@ def check_workstation_for_holiday(workstation, from_datetime, to_datetime): + "\n".join(applicable_holidays), WorkstationHolidayError, ) + + +@frappe.whitelist() +def get_workstations(**kwargs): + kwargs = frappe._dict(kwargs) + _workstation = frappe.qb.DocType("Workstation") + + query = ( + frappe.qb.from_(_workstation) + .select( + _workstation.name, + _workstation.description, + _workstation.status, + _workstation.on_status_image, + _workstation.off_status_image, + ) + .orderby(_workstation.workstation_type, _workstation.name) + .where(_workstation.plant_floor == kwargs.plant_floor) + ) + + if kwargs.workstation: + query = query.where(_workstation.name == kwargs.workstation) + + if kwargs.workstation_type: + query = query.where(_workstation.workstation_type == kwargs.workstation_type) + + if kwargs.workstation_status: + query = query.where(_workstation.status == kwargs.workstation_status) + + data = query.run(as_dict=True) + + color_map = { + "Production": "var(--green-600)", + "Off": "var(--gray-600)", + "Idle": "var(--gray-600)", + "Problem": "var(--red-600)", + "Maintenance": "var(--yellow-600)", + "Setup": "var(--blue-600)", + } + + for d in data: + d.workstation_name = get_link_to_form("Workstation", d.name) + d.status_image = d.on_status_image + d.background_color = color_map.get(d.status, "var(--red-600)") + d.workstation_link = get_url_to_form("Workstation", d.name) + if d.status != "Production": + d.status_image = d.off_status_image + + return data diff --git a/erpnext/manufacturing/doctype/workstation/workstation_job_card.html b/erpnext/manufacturing/doctype/workstation/workstation_job_card.html new file mode 100644 index 00000000000..3c0ef6d837d --- /dev/null +++ b/erpnext/manufacturing/doctype/workstation/workstation_job_card.html @@ -0,0 +1,97 @@ + + +
+{% $.each(data, (idx, d) => { %} + +{% }); %} +
\ No newline at end of file diff --git a/erpnext/manufacturing/doctype/workstation/workstation_list.js b/erpnext/manufacturing/doctype/workstation/workstation_list.js index 61f2062ec0b..86928cafcb2 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation_list.js +++ b/erpnext/manufacturing/doctype/workstation/workstation_list.js @@ -1,5 +1,16 @@ frappe.listview_settings['Workstation'] = { - // add_fields: ["status"], - // filters:[["status","=", "Open"]] + add_fields: ["status"], + get_indicator: function(doc) { + let color_map = { + "Production": "green", + "Off": "gray", + "Idle": "gray", + "Problem": "red", + "Maintenance": "yellow", + "Setup": "blue", + } + + return [__(doc.status), color_map[doc.status], true]; + } }; diff --git a/erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json b/erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json index a79182fb31b..b185f7d29de 100644 --- a/erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json +++ b/erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json @@ -1,150 +1,58 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2014-12-24 14:46:40.678236", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2014-12-24 14:46:40.678236", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "start_time", + "hours", + "column_break_2", + "end_time", + "enabled" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "start_time", - "fieldtype": "Time", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Start Time", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "start_time", + "fieldtype": "Time", + "in_list_view": 1, + "label": "Start Time", + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "end_time", - "fieldtype": "Time", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "End Time", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "end_time", + "fieldtype": "Time", + "in_list_view": 1, + "label": "End Time", + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "1", - "fieldname": "enabled", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Enabled", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "default": "1", + "fieldname": "enabled", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Enabled" + }, + { + "fieldname": "hours", + "fieldtype": "Float", + "label": "Hours", + "read_only": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2016-12-13 05:02:36.754145", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Workstation Working Hour", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_seen": 0 + ], + "istable": 1, + "links": [], + "modified": "2023-10-25 14:48:29.697498", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Workstation Working Hour", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/manufacturing/page/visual_plant_floor/__init__.py b/erpnext/manufacturing/page/visual_plant_floor/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.js b/erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.js new file mode 100644 index 00000000000..38667e8d795 --- /dev/null +++ b/erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.js @@ -0,0 +1,13 @@ + + +frappe.pages['visual-plant-floor'].on_page_load = function(wrapper) { + var page = frappe.ui.make_app_page({ + parent: wrapper, + title: 'Visual Plant Floor', + single_column: true + }); + + frappe.visual_plant_floor = new frappe.ui.VisualPlantFloor( + {wrapper: $(wrapper).find('.layout-main-section')}, wrapper.page + ); +} \ No newline at end of file diff --git a/erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.json b/erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.json new file mode 100644 index 00000000000..a907e973e34 --- /dev/null +++ b/erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.json @@ -0,0 +1,29 @@ +{ + "content": null, + "creation": "2023-10-06 15:17:39.215300", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2023-10-06 15:18:00.622073", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "visual-plant-floor", + "owner": "Administrator", + "page_name": "visual-plant-floor", + "roles": [ + { + "role": "Manufacturing User" + }, + { + "role": "Manufacturing Manager" + }, + { + "role": "Operator" + } + ], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 0, + "title": "Visual Plant Floor" +} \ No newline at end of file diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json index 8e0785074fa..e3b632dba2e 100644 --- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json +++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"YHCQG3wAGv\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Creator\",\"col\":3}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", + "content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"YHCQG3wAGv\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Creator\",\"col\":3}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"Bw3jwRMiei\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Plant Floor\",\"col\":3}},{\"id\":\"4hPVRQke_x\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Visual Plant Floor\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", "creation": "2020-03-02 17:11:37.032604", "custom_blocks": [], "docstatus": 0, @@ -316,7 +316,7 @@ "type": "Link" } ], - "modified": "2023-08-08 22:28:39.633891", + "modified": "2023-11-30 15:21:14.577990", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing", @@ -336,6 +336,14 @@ "type": "URL", "url": "https://frappe.school/courses/manufacturing?utm_source=in_app" }, + { + "color": "Grey", + "doc_view": "List", + "label": "Plant Floor", + "link_to": "Plant Floor", + "stats_filter": "[]", + "type": "DocType" + }, { "color": "Grey", "doc_view": "List", @@ -343,6 +351,13 @@ "link_to": "BOM Creator", "type": "DocType" }, + { + "color": "Grey", + "doc_view": "List", + "label": "Visual Plant Floor", + "link_to": "visual-plant-floor", + "type": "Page" + }, { "color": "Grey", "doc_view": "List", diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js index dee9a06f052..b847e5729f5 100644 --- a/erpnext/public/js/erpnext.bundle.js +++ b/erpnext/public/js/erpnext.bundle.js @@ -5,6 +5,8 @@ import "./sms_manager"; import "./utils/party"; import "./controllers/stock_controller"; import "./payment/payments"; +import "./templates/visual_plant_floor_template.html"; +import "./plant_floor_visual/visual_plant"; import "./controllers/taxes_and_totals"; import "./controllers/transaction"; import "./templates/item_selector.html"; diff --git a/erpnext/public/js/plant_floor_visual/visual_plant.js b/erpnext/public/js/plant_floor_visual/visual_plant.js new file mode 100644 index 00000000000..b1d120fd934 --- /dev/null +++ b/erpnext/public/js/plant_floor_visual/visual_plant.js @@ -0,0 +1,157 @@ +class VisualPlantFloor { + constructor({wrapper, skip_filters=false, plant_floor=null}, page=null) { + this.wrapper = wrapper; + this.plant_floor = plant_floor; + this.skip_filters = skip_filters; + + this.make(); + if (!this.skip_filters) { + this.page = page; + this.add_filter(); + this.prepare_menu(); + } + } + + make() { + this.wrapper.append(` +
+
+
+
+
+
+ `); + + if (!this.skip_filters) { + this.filter_wrapper = this.wrapper.find('.plant-floor-filter'); + this.visualization_wrapper = this.wrapper.find('.plant-floor-visualization'); + } else if(this.plant_floor) { + this.prepare_data(); + } + } + + prepare_data() { + frappe.call({ + method: 'erpnext.manufacturing.doctype.workstation.workstation.get_workstations', + args: { + plant_floor: this.plant_floor, + }, + callback: (r) => { + this.workstations = r.message; + this.render_workstations(); + } + }); + } + + add_filter() { + this.plant_floor = frappe.ui.form.make_control({ + df: { + fieldtype: 'Link', + options: 'Plant Floor', + fieldname: 'plant_floor', + label: __('Plant Floor'), + reqd: 1, + onchange: () => { + this.render_plant_visualization(); + } + }, + parent: this.filter_wrapper, + render_input: true, + }); + + this.plant_floor.$wrapper.addClass('form-column col-sm-2'); + + this.workstation_type = frappe.ui.form.make_control({ + df: { + fieldtype: 'Link', + options: 'Workstation Type', + fieldname: 'workstation_type', + label: __('Machine Type'), + onchange: () => { + this.render_plant_visualization(); + } + }, + parent: this.filter_wrapper, + render_input: true, + }); + + this.workstation_type.$wrapper.addClass('form-column col-sm-2'); + + this.workstation = frappe.ui.form.make_control({ + df: { + fieldtype: 'Link', + options: 'Workstation', + fieldname: 'workstation', + label: __('Machine'), + onchange: () => { + this.render_plant_visualization(); + }, + get_query: () => { + if (this.workstation_type.get_value()) { + return { + filters: { + 'workstation_type': this.workstation_type.get_value() || '' + } + } + } + } + }, + parent: this.filter_wrapper, + render_input: true, + }); + + this.workstation.$wrapper.addClass('form-column col-sm-2'); + + this.workstation_status = frappe.ui.form.make_control({ + df: { + fieldtype: 'Select', + options: '\nProduction\nOff\nIdle\nProblem\nMaintenance\nSetup', + fieldname: 'workstation_status', + label: __('Status'), + onchange: () => { + this.render_plant_visualization(); + }, + }, + parent: this.filter_wrapper, + render_input: true, + }); + } + + render_plant_visualization() { + let plant_floor = this.plant_floor.get_value(); + + if (plant_floor) { + frappe.call({ + method: 'erpnext.manufacturing.doctype.workstation.workstation.get_workstations', + args: { + plant_floor: plant_floor, + workstation_type: this.workstation_type.get_value(), + workstation: this.workstation.get_value(), + workstation_status: this.workstation_status.get_value() + }, + callback: (r) => { + this.workstations = r.message; + this.render_workstations(); + } + }); + } + } + + render_workstations() { + console.log(this.wrapper.find('.plant-floor-container')) + this.wrapper.find('.plant-floor-container').empty(); + let template = frappe.render_template("visual_plant_floor_template", { + workstations: this.workstations + }); + + $(template).appendTo(this.wrapper.find('.plant-floor-container')); + } + + prepare_menu() { + this.page.add_menu_item(__('Refresh'), () => { + this.render_plant_visualization(); + }); + } +} + +frappe.ui.VisualPlantFloor = VisualPlantFloor; \ No newline at end of file diff --git a/erpnext/public/js/templates/visual_plant_floor_template.html b/erpnext/public/js/templates/visual_plant_floor_template.html new file mode 100644 index 00000000000..2e67085c022 --- /dev/null +++ b/erpnext/public/js/templates/visual_plant_floor_template.html @@ -0,0 +1,19 @@ +{% $.each(workstations, (idx, row) => { %} + +{% }); %} \ No newline at end of file diff --git a/erpnext/public/scss/erpnext.scss b/erpnext/public/scss/erpnext.scss index 8ab5973debd..ef09854c08b 100644 --- a/erpnext/public/scss/erpnext.scss +++ b/erpnext/public/scss/erpnext.scss @@ -490,3 +490,54 @@ body[data-route="pos"] { .exercise-col { padding: 10px; } + +.plant-floor, .workstation-wrapper, .workstation-card p { + border-radius: var(--border-radius-md); + border: 1px solid var(--border-color); + box-shadow: none; + background-color: var(--card-bg); + position: relative; +} + +.plant-floor { + padding-bottom: 25px; +} + +.plant-floor-filter { + padding-top: 10px; + display: flex; + flex-wrap: wrap; +} + +.plant-floor-container { + padding-top: 10px; + display: grid; + grid-template-columns: repeat(6,minmax(0,1fr)); + gap: var(--margin-xl); +} + +@media screen and (max-width: 620px) { + .plant-floor-container { + grid-template-columns: repeat(2,minmax(0,1fr)); + } +} + +.plant-floor-container .workstation-card { + padding: 5px; +} + +.plant-floor-container .workstation-image-link { + width: 100%; + font-size: 50px; + margin: var(--margin-sm); + min-height: 11rem; +} + +.workstation-abbr { + display: flex; + background-color: var(--control-bg); + height:100%; + width:100%; + align-items: center; + justify-content: center; +} \ No newline at end of file From 3bef12cb55fe381fc73a2e42f369651c8df82be3 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 5 Dec 2023 15:28:40 +0530 Subject: [PATCH 28/41] feat: make material request for job card from workstation dashboard (cherry picked from commit 6fea9d6dfe50513614e39a7b83ad4db59970e26e) --- .../doctype/job_card/job_card.py | 14 + .../doctype/plant_floor/plant_floor.js | 243 +++++++++++++++++- .../doctype/plant_floor/plant_floor.json | 36 ++- .../doctype/plant_floor/plant_floor.py | 114 +++++++- .../plant_floor/stock_summary_template.html | 61 +++++ .../doctype/workstation/workstation.js | 207 ++++++++++++++- .../doctype/workstation/workstation.py | 59 ++++- .../workstation/workstation_job_card.html | 38 ++- .../manufacturing/manufacturing.json | 12 +- .../js/plant_floor_visual/visual_plant.js | 2 +- erpnext/public/scss/erpnext.scss | 3 +- 11 files changed, 750 insertions(+), 39 deletions(-) create mode 100644 erpnext/manufacturing/doctype/plant_floor/stock_summary_template.html diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 23650b68736..079350b63b2 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -955,6 +955,14 @@ class JobCard(Document): if update_status: self.db_set("status", self.status) + if self.status in ["Completed", "Work In Progress"]: + status = { + "Completed": "Off", + "Work In Progress": "Production", + }.get(self.status) + + self.update_status_in_workstation(status) + def set_wip_warehouse(self): if not self.wip_warehouse: self.wip_warehouse = frappe.db.get_single_value( @@ -1035,6 +1043,12 @@ class JobCard(Document): return False + def update_status_in_workstation(self, status): + if not self.workstation: + return + + frappe.db.set_value("Workstation", self.workstation, "status", status) + @frappe.whitelist() def make_time_log(args): diff --git a/erpnext/manufacturing/doctype/plant_floor/plant_floor.js b/erpnext/manufacturing/doctype/plant_floor/plant_floor.js index 427893743af..67e5acd9da8 100644 --- a/erpnext/manufacturing/doctype/plant_floor/plant_floor.js +++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.js @@ -2,11 +2,31 @@ // For license information, please see license.txt frappe.ui.form.on("Plant Floor", { - refresh(frm) { - frm.trigger('prepare_dashboard') + setup(frm) { + frm.trigger("setup_queries"); }, - prepare_dashboard(frm) { + setup_queries(frm) { + frm.set_query("warehouse", (doc) => { + if (!doc.company) { + frappe.throw(__("Please select Company first")); + } + + return { + filters: { + "is_group": 0, + "company": doc.company + } + } + }); + }, + + refresh(frm) { + frm.trigger('prepare_stock_dashboard') + frm.trigger('prepare_workstation_dashboard') + }, + + prepare_workstation_dashboard(frm) { let wrapper = $(frm.fields_dict["plant_dashboard"].wrapper); wrapper.empty(); @@ -16,4 +36,221 @@ frappe.ui.form.on("Plant Floor", { plant_floor: frm.doc.name, }); }, + + prepare_stock_dashboard(frm) { + if (!frm.doc.warehouse) { + return; + } + + let wrapper = $(frm.fields_dict["stock_summary"].wrapper); + wrapper.empty(); + + frappe.visual_stock = new VisualStock({ + wrapper: wrapper, + frm: frm, + }); + }, }); + + +class VisualStock { + constructor(opts) { + Object.assign(this, opts); + this.make(); + } + + make() { + this.prepare_filters(); + this.prepare_stock_summary({ + start:0 + }); + } + + prepare_filters() { + this.wrapper.append(` +
+
+ +
+
+ `); + + this.item_filter = frappe.ui.form.make_control({ + df: { + fieldtype: "Link", + fieldname: "item_code", + placeholder: __("Item"), + options: "Item", + onchange: () => this.prepare_stock_summary({ + start:0, + item_code: this.item_filter.value + }) + }, + parent: this.wrapper.find('.filter-section'), + render_input: true, + }); + + this.item_filter.$wrapper.addClass('form-column col-sm-3'); + this.item_filter.$wrapper.find('.clearfix').hide(); + + this.item_group_filter = frappe.ui.form.make_control({ + df: { + fieldtype: "Link", + fieldname: "item_group", + placeholder: __("Item Group"), + options: "Item Group", + change: () => this.prepare_stock_summary({ + start:0, + item_group: this.item_group_filter.value + }) + }, + parent: this.wrapper.find('.filter-section'), + render_input: true, + }); + + this.item_group_filter.$wrapper.addClass('form-column col-sm-3'); + this.item_group_filter.$wrapper.find('.clearfix').hide(); + } + + prepare_stock_summary(args) { + let {start, item_code, item_group} = args; + + this.get_stock_summary(start, item_code, item_group).then(stock_summary => { + this.wrapper.find('.stock-summary-container').remove(); + this.wrapper.append(`
`); + this.stock_summary = stock_summary.message; + this.render_stock_summary(); + this.bind_events(); + }); + } + + async get_stock_summary(start, item_code, item_group) { + let stock_summary = await frappe.call({ + method: "erpnext.manufacturing.doctype.plant_floor.plant_floor.get_stock_summary", + args: { + warehouse: this.frm.doc.warehouse, + start: start, + item_code: item_code, + item_group: item_group + } + }); + + return stock_summary; + } + + render_stock_summary() { + let template = frappe.render_template("stock_summary_template", { + stock_summary: this.stock_summary + }); + + this.wrapper.find('.stock-summary-container').append(template); + } + + bind_events() { + this.wrapper.find('.btn-add').click((e) => { + this.item_code = decodeURI($(e.currentTarget).attr('data-item-code')); + + this.make_stock_entry([ + { + label: __("For Item"), + fieldname: "item_code", + fieldtype: "Data", + read_only: 1, + default: this.item_code + }, + { + label: __("Quantity"), + fieldname: "qty", + fieldtype: "Float", + reqd: 1 + } + ], __("Add Stock"), "Material Receipt") + }); + + this.wrapper.find('.btn-move').click((e) => { + this.item_code = decodeURI($(e.currentTarget).attr('data-item-code')); + + this.make_stock_entry([ + { + label: __("For Item"), + fieldname: "item_code", + fieldtype: "Data", + read_only: 1, + default: this.item_code + }, + { + label: __("Quantity"), + fieldname: "qty", + fieldtype: "Float", + reqd: 1 + }, + { + label: __("To Warehouse"), + fieldname: "to_warehouse", + fieldtype: "Link", + options: "Warehouse", + reqd: 1, + get_query: () => { + return { + filters: { + "is_group": 0, + "company": this.frm.doc.company + } + } + } + } + ], __("Move Stock"), "Material Transfer") + }); + } + + make_stock_entry(fields, title, stock_entry_type) { + frappe.prompt(fields, + (values) => { + this.values = values; + this.stock_entry_type = stock_entry_type; + this.update_values(); + + this.frm.call({ + method: "make_stock_entry", + doc: this.frm.doc, + args: { + kwargs: this.values, + }, + callback: (r) => { + if (!r.exc) { + var doc = frappe.model.sync(r.message); + frappe.set_route("Form", r.message.doctype, r.message.name); + } + } + }) + }, __(title), __("Create") + ); + } + + update_values() { + if (!this.values.qty) { + frappe.throw(__("Quantity is required")); + } + + let from_warehouse = ""; + let to_warehouse = ""; + + if (this.stock_entry_type == "Material Receipt") { + to_warehouse = this.frm.doc.warehouse; + } else { + from_warehouse = this.frm.doc.warehouse; + to_warehouse = this.values.to_warehouse; + } + + this.values = { + ...this.values, + ...{ + "company": this.frm.doc.company, + "item_code": this.item_code, + "from_warehouse": from_warehouse, + "to_warehouse": to_warehouse, + "purpose": this.stock_entry_type, + } + } + } +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/plant_floor/plant_floor.json b/erpnext/manufacturing/doctype/plant_floor/plant_floor.json index aa6eb1dd40f..be0052c47bf 100644 --- a/erpnext/manufacturing/doctype/plant_floor/plant_floor.json +++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.json @@ -10,11 +10,13 @@ "field_order": [ "workstations_tab", "plant_dashboard", + "stock_summary_tab", + "stock_summary", "details_tab", "column_break_mvbx", "floor_name", - "section_break_cczv", - "volumetric_weight" + "company", + "warehouse" ], "fields": [ { @@ -27,7 +29,7 @@ "depends_on": "eval:!doc.__islocal", "fieldname": "workstations_tab", "fieldtype": "Tab Break", - "label": "Dashboard" + "label": "Workstations" }, { "fieldname": "plant_dashboard", @@ -37,25 +39,39 @@ { "fieldname": "details_tab", "fieldtype": "Tab Break", - "label": "Details" + "label": "Floor" }, { "fieldname": "column_break_mvbx", "fieldtype": "Column Break" }, { - "fieldname": "section_break_cczv", - "fieldtype": "Section Break" + "fieldname": "warehouse", + "fieldtype": "Link", + "label": "Warehouse", + "options": "Warehouse" }, { - "fieldname": "volumetric_weight", - "fieldtype": "Float", - "label": "Volumetric Weight" + "depends_on": "eval:!doc.__islocal && doc.warehouse", + "fieldname": "stock_summary_tab", + "fieldtype": "Tab Break", + "label": "Stock Summary" + }, + { + "fieldname": "stock_summary", + "fieldtype": "HTML", + "label": "Stock Summary" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-12-04 15:36:09.641203", + "modified": "2024-01-30 11:59:07.508535", "modified_by": "Administrator", "module": "Manufacturing", "name": "Plant Floor", diff --git a/erpnext/manufacturing/doctype/plant_floor/plant_floor.py b/erpnext/manufacturing/doctype/plant_floor/plant_floor.py index 729cc3337a9..d30b7d10499 100644 --- a/erpnext/manufacturing/doctype/plant_floor/plant_floor.py +++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.py @@ -1,8 +1,10 @@ # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -# import frappe +import frappe from frappe.model.document import Document +from frappe.query_builder import Order +from frappe.utils import get_link_to_form, nowdate, nowtime class PlantFloor(Document): @@ -14,8 +16,114 @@ class PlantFloor(Document): if TYPE_CHECKING: from frappe.types import DF + company: DF.Link | None floor_name: DF.Data | None - volumetric_weight: DF.Float + warehouse: DF.Link | None # end: auto-generated types - pass + @frappe.whitelist() + def make_stock_entry(self, kwargs): + if isinstance(kwargs, str): + kwargs = frappe.parse_json(kwargs) + + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + stock_entry = frappe.new_doc("Stock Entry") + stock_entry.update( + { + "company": kwargs.company, + "from_warehouse": kwargs.from_warehouse, + "to_warehouse": kwargs.to_warehouse, + "purpose": kwargs.purpose, + "stock_entry_type": kwargs.purpose, + "posting_date": nowdate(), + "posting_time": nowtime(), + "items": self.get_item_details(kwargs), + } + ) + + stock_entry.set_missing_values() + + return stock_entry + + def get_item_details(self, kwargs) -> list[dict]: + item_details = frappe.db.get_value( + "Item", kwargs.item_code, ["item_name", "stock_uom", "item_group", "description"], as_dict=True + ) + item_details.update( + { + "qty": kwargs.qty, + "uom": item_details.stock_uom, + "item_code": kwargs.item_code, + "conversion_factor": 1, + "s_warehouse": kwargs.from_warehouse, + "t_warehouse": kwargs.to_warehouse, + } + ) + + return [item_details] + + +@frappe.whitelist() +def get_stock_summary(warehouse, start=0, item_code=None, item_group=None): + stock_details = get_stock_details( + warehouse, start=start, item_code=item_code, item_group=item_group + ) + + max_count = 0.0 + for d in stock_details: + d.actual_or_pending = ( + d.projected_qty + + d.reserved_qty + + d.reserved_qty_for_production + + d.reserved_qty_for_sub_contract + ) + d.pending_qty = 0 + d.total_reserved = ( + d.reserved_qty + d.reserved_qty_for_production + d.reserved_qty_for_sub_contract + ) + if d.actual_or_pending > d.actual_qty: + d.pending_qty = d.actual_or_pending - d.actual_qty + + d.max_count = max(d.actual_or_pending, d.actual_qty, d.total_reserved, max_count) + max_count = d.max_count + d.item_link = get_link_to_form("Item", d.item_code) + + return stock_details + + +def get_stock_details(warehouse, start=0, item_code=None, item_group=None): + item_table = frappe.qb.DocType("Item") + bin_table = frappe.qb.DocType("Bin") + + query = ( + frappe.qb.from_(bin_table) + .inner_join(item_table) + .on(bin_table.item_code == item_table.name) + .select( + bin_table.item_code, + bin_table.actual_qty, + bin_table.projected_qty, + bin_table.reserved_qty, + bin_table.reserved_qty_for_production, + bin_table.reserved_qty_for_sub_contract, + bin_table.reserved_qty_for_production_plan, + bin_table.reserved_stock, + item_table.item_name, + item_table.item_group, + item_table.image, + ) + .where(bin_table.warehouse == warehouse) + .limit(20) + .offset(start) + .orderby(bin_table.actual_qty, order=Order.desc) + ) + + if item_code: + query = query.where(bin_table.item_code == item_code) + + if item_group: + query = query.where(item_table.item_group == item_group) + + return query.run(as_dict=True) diff --git a/erpnext/manufacturing/doctype/plant_floor/stock_summary_template.html b/erpnext/manufacturing/doctype/plant_floor/stock_summary_template.html new file mode 100644 index 00000000000..8824c9811ba --- /dev/null +++ b/erpnext/manufacturing/doctype/plant_floor/stock_summary_template.html @@ -0,0 +1,61 @@ +{% $.each(stock_summary, (idx, row) => { %} +
+
+ {% if(row.image) { %} + + {% } else { %} +
{{frappe.get_abbr(row.item_code, 2)}}
+ {% } %} +
+
+ {% if (row.item_code === row.item_name) { %} + {{row.item_link}} + {% } else { %} + {{row.item_link}} +

+ {{row.item_name}} +

+ {% } %} + +
+
+ {{ frappe.format(row.actual_qty, { fieldtype: "Float"})}} +
+
+ {{ frappe.format(row.reserved_stock, { fieldtype: "Float"})}} +
+
+ + + {{ row.total_reserved }} + + + + + + + + {{ row.actual_qty }} {{ (row.pending_qty > 0) ? ("(" + row.pending_qty+ ")") : "" }} + + + + + {% if row.pending_qty > 0 %} + + + {% endif %} + + + +
+
+ +
+
+ +
+
+{% }); %} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/workstation/workstation.js b/erpnext/manufacturing/doctype/workstation/workstation.js index 4ffc506f52e..e3ad3fe3cce 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.js +++ b/erpnext/manufacturing/doctype/workstation/workstation.js @@ -94,18 +94,25 @@ class WorkstationDashboard { }, callback: (r) => { if (r.message) { - this.render_job_cards(r.message); + this.job_cards = r.message; + this.render_job_cards(); } } }); } - render_job_cards(job_cards) { + render_job_cards() { let template = frappe.render_template("workstation_job_card", { - data: job_cards + data: this.job_cards }); this.$wrapper.html(template); + this.prepare_timer(); + this.toggle_job_card(); + this.bind_events(); + } + + toggle_job_card() { this.$wrapper.find(".collapse-indicator-job").on("click", (e) => { $(e.currentTarget).closest(".form-dashboard-section").find(".section-body-job-card").toggleClass("hide") if ($(e.currentTarget).closest(".form-dashboard-section").find(".section-body-job-card").hasClass("hide")) @@ -114,4 +121,198 @@ class WorkstationDashboard { $(e.currentTarget).html(frappe.utils.icon("es-line-up", "sm", "mb-1")) }); } + + bind_events() { + this.$wrapper.find(".make-material-request").on("click", (e) => { + let job_card = $(e.currentTarget).attr("job-card"); + this.make_material_request(job_card); + }); + + this.$wrapper.find(".btn-start").on("click", (e) => { + let job_card = $(e.currentTarget).attr("job-card"); + this.start_job(job_card); + }); + + this.$wrapper.find(".btn-complete").on("click", (e) => { + let job_card = $(e.currentTarget).attr("job-card"); + let pending_qty = flt($(e.currentTarget).attr("pending-qty")); + this.complete_job(job_card, pending_qty); + }); + } + + start_job(job_card) { + let me = this; + frappe.prompt([ + { + fieldtype: 'Datetime', + label: __('Start Time'), + fieldname: 'start_time', + reqd: 1, + default: frappe.datetime.now_datetime() + }, + { + label: __('Operator'), + fieldname: 'employee', + fieldtype: 'Link', + options: 'Employee', + } + ], data => { + this.frm.call({ + method: "start_job", + doc: this.frm.doc, + args: { + job_card: job_card, + from_time: data.start_time, + employee: data.employee, + }, + callback(r) { + if (r.message) { + me.job_cards = [r.message]; + me.prepare_timer() + me.update_job_card_details(); + } + } + }); + }, __("Enter Value"), __("Start Job")); + } + + complete_job(job_card, qty_to_manufacture) { + let me = this; + let fields = [ + { + fieldtype: 'Float', + label: __('Completed Quantity'), + fieldname: 'qty', + reqd: 1, + default: flt(qty_to_manufacture || 0) + }, + { + fieldtype: 'Datetime', + label: __('End Time'), + fieldname: 'end_time', + default: frappe.datetime.now_datetime() + }, + ]; + + frappe.prompt(fields, data => { + if (data.qty <= 0) { + frappe.throw(__("Quantity should be greater than 0")); + } + + this.frm.call({ + method: "complete_job", + doc: this.frm.doc, + args: { + job_card: job_card, + qty: data.qty, + to_time: data.end_time, + }, + callback: function(r) { + if (r.message) { + me.job_cards = [r.message]; + me.prepare_timer() + me.update_job_card_details(); + } + } + }); + }, __("Enter Value"), __("Submit")); + } + + make_material_request(job_card) { + frappe.call({ + method: "erpnext.manufacturing.doctype.job_card.job_card.make_material_request", + args: { + source_name: job_card, + }, + callback: (r) => { + if (r.message) { + var doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + } + }); + } + + prepare_timer() { + this.job_cards.forEach((data) => { + if (data.time_logs?.length) { + data._current_time = this.get_current_time(data); + if (data.time_logs[cint(data.time_logs.length) - 1].to_time) { + this.updateStopwatch(data); + } else { + this.initialiseTimer(data); + } + } + }); + } + + update_job_card_details() { + let color_map = { + "Pending": "var(--bg-blue)", + "In Process": "var(--bg-yellow)", + "Submitted": "var(--bg-blue)", + "Open": "var(--bg-gray)", + "Closed": "var(--bg-green)", + "Work In Progress": "var(--bg-orange)", + } + + this.job_cards.forEach((data) => { + let job_card_selector = this.$wrapper.find(` + [data-name='${data.name}']` + ); + + $(job_card_selector).find(".job-card-status").text(data.status); + $(job_card_selector).find(".job-card-status").css("backgroundColor", color_map[data.status]); + + if (data.status === "Work In Progress") { + $(job_card_selector).find(".btn-start").addClass("hide"); + $(job_card_selector).find(".btn-complete").removeClass("hide"); + } else if (data.status === "Completed") { + $(job_card_selector).find(".btn-start").addClass("hide"); + $(job_card_selector).find(".btn-complete").addClass("hide"); + } + }); + } + + initialiseTimer(data) { + setInterval(() => { + data._current_time += 1; + this.updateStopwatch(data); + }, 1000); + } + + updateStopwatch(data) { + let increment = data._current_time; + let hours = Math.floor(increment / 3600); + let minutes = Math.floor((increment - (hours * 3600)) / 60); + let seconds = cint(increment - (hours * 3600) - (minutes * 60)); + + let job_card_selector = `[data-job-card='${data.name}']` + let timer_selector = this.$wrapper.find(job_card_selector) + + $(timer_selector).find(".hours").text(hours < 10 ? ("0" + hours.toString()) : hours.toString()); + $(timer_selector).find(".minutes").text(minutes < 10 ? ("0" + minutes.toString()) : minutes.toString()); + $(timer_selector).find(".seconds").text(seconds < 10 ? ("0" + seconds.toString()) : seconds.toString()); + } + + get_current_time(data) { + let current_time = 0.0; + data.time_logs.forEach(d => { + if (d.to_time) { + if (d.time_in_mins) { + current_time += flt(d.time_in_mins, 2) * 60; + } else { + current_time += this.get_seconds_diff(d.to_time, d.from_time); + } + } else { + current_time += this.get_seconds_diff(frappe.datetime.now_datetime(), d.from_time); + } + }); + + return current_time; + } + + get_seconds_diff(d1, d2) { + return moment(d1).diff(d2, "seconds"); + } } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index c2b1e63067c..90aa993d7e2 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -164,6 +164,28 @@ class Workstation(Document): return schedule_date + @frappe.whitelist() + def start_job(self, job_card, from_time, employee): + doc = frappe.get_doc("Job Card", job_card) + doc.append("time_logs", {"from_time": from_time, "employee": employee}) + doc.save(ignore_permissions=True) + + return doc + + @frappe.whitelist() + def complete_job(self, job_card, qty, to_time): + doc = frappe.get_doc("Job Card", job_card) + for row in doc.time_logs: + if not row.to_time: + row.to_time = to_time + row.time_in_mins = time_diff_in_hours(row.to_time, row.from_time) / 60 + row.completed_qty = qty + + doc.save(ignore_permissions=True) + doc.submit() + + return doc + @frappe.whitelist() def get_job_cards(workstation): @@ -177,6 +199,7 @@ def get_job_cards(workstation): "operation", "total_completed_qty", "for_quantity", + "transferred_qty", "status", "expected_start_date", "expected_end_date", @@ -193,6 +216,11 @@ def get_job_cards(workstation): job_cards = [row.name for row in jc_data] raw_materials = get_raw_materials(job_cards) + time_logs = get_time_logs(job_cards) + + allow_excess_transfer = frappe.db.get_single_value( + "Manufacturing Settings", "job_card_excess_transfer" + ) for row in jc_data: row.progress_percent = ( @@ -204,12 +232,16 @@ def get_job_cards(workstation): row.work_order_link = get_link_to_form("Work Order", row.work_order) row.raw_materials = raw_materials.get(row.name, []) + row.time_logs = time_logs.get(row.name, []) + row.make_material_request = False + if row.for_quantity > row.transferred_qty or allow_excess_transfer: + row.make_material_request = True return jc_data def get_status_color(status): - colos_map = { + color_map = { "Pending": "var(--bg-blue)", "In Process": "var(--bg-yellow)", "Submitted": "var(--bg-blue)", @@ -218,7 +250,7 @@ def get_status_color(status): "Work In Progress": "var(--bg-orange)", } - return colos_map.get(status, "var(--bg-blue)") + return color_map.get(status, "var(--bg-blue)") def get_raw_materials(job_cards): @@ -245,6 +277,29 @@ def get_raw_materials(job_cards): return raw_materials +def get_time_logs(job_cards): + time_logs = {} + + data = frappe.get_all( + "Job Card Time Log", + fields=[ + "parent", + "name", + "employee", + "from_time", + "to_time", + "time_in_mins", + ], + filters={"parent": ["in", job_cards], "parentfield": "time_logs"}, + order_by="parent, idx", + ) + + for row in data: + time_logs.setdefault(row.parent, []).append(row) + + return time_logs + + @frappe.whitelist() def get_default_holiday_list(): return frappe.get_cached_value( diff --git a/erpnext/manufacturing/doctype/workstation/workstation_job_card.html b/erpnext/manufacturing/doctype/workstation/workstation_job_card.html index 3c0ef6d837d..97707855db0 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation_job_card.html +++ b/erpnext/manufacturing/doctype/workstation/workstation_job_card.html @@ -27,13 +27,28 @@ {{ d.work_order_link }} -
-
- {{ frappe.format(d.expected_start_date, { fieldtype: 'Datetime' }) }} +
+
+ 00 + : + 00 + : + 00
+ + {% if(d.status === "Open") { %} +
+ {{ frappe.format(d.expected_start_date, { fieldtype: 'Datetime' }) }} +
+ {% } else { %} +
+ {{ frappe.format(d.expected_end_date, { fieldtype: 'Datetime' }) }} +
+ {% } %} +
-
+
{{ d.status }}
@@ -48,11 +63,24 @@ {{ d.for_quantity }} / {{ d.total_completed_qty }}
+
+ + +

- {{ __("Raw Materials") }} +
+
+ {{ __("Raw Materials") }} +
+ {% if(d.make_material_request) { %} +
+ +
+ {% } %} +
{% if(d.raw_materials) { %} diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json index e3b632dba2e..d2520d6b7eb 100644 --- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json +++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"YHCQG3wAGv\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Creator\",\"col\":3}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"Bw3jwRMiei\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Plant Floor\",\"col\":3}},{\"id\":\"4hPVRQke_x\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Visual Plant Floor\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", + "content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"YHCQG3wAGv\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Creator\",\"col\":3}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"Ubj6zXcmIQ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Plant Floor\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", "creation": "2020-03-02 17:11:37.032604", "custom_blocks": [], "docstatus": 0, @@ -316,7 +316,7 @@ "type": "Link" } ], - "modified": "2023-11-30 15:21:14.577990", + "modified": "2024-01-30 21:49:58.577218", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing", @@ -341,7 +341,6 @@ "doc_view": "List", "label": "Plant Floor", "link_to": "Plant Floor", - "stats_filter": "[]", "type": "DocType" }, { @@ -351,13 +350,6 @@ "link_to": "BOM Creator", "type": "DocType" }, - { - "color": "Grey", - "doc_view": "List", - "label": "Visual Plant Floor", - "link_to": "visual-plant-floor", - "type": "Page" - }, { "color": "Grey", "doc_view": "List", diff --git a/erpnext/public/js/plant_floor_visual/visual_plant.js b/erpnext/public/js/plant_floor_visual/visual_plant.js index b1d120fd934..8cd73adc574 100644 --- a/erpnext/public/js/plant_floor_visual/visual_plant.js +++ b/erpnext/public/js/plant_floor_visual/visual_plant.js @@ -26,6 +26,7 @@ class VisualPlantFloor { this.filter_wrapper = this.wrapper.find('.plant-floor-filter'); this.visualization_wrapper = this.wrapper.find('.plant-floor-visualization'); } else if(this.plant_floor) { + this.wrapper.find('.plant-floor').css('border', 'none'); this.prepare_data(); } } @@ -138,7 +139,6 @@ class VisualPlantFloor { } render_workstations() { - console.log(this.wrapper.find('.plant-floor-container')) this.wrapper.find('.plant-floor-container').empty(); let template = frappe.render_template("visual_plant_floor_template", { workstations: this.workstations diff --git a/erpnext/public/scss/erpnext.scss b/erpnext/public/scss/erpnext.scss index ef09854c08b..1626b7c894d 100644 --- a/erpnext/public/scss/erpnext.scss +++ b/erpnext/public/scss/erpnext.scss @@ -510,7 +510,6 @@ body[data-route="pos"] { } .plant-floor-container { - padding-top: 10px; display: grid; grid-template-columns: repeat(6,minmax(0,1fr)); gap: var(--margin-xl); @@ -530,7 +529,7 @@ body[data-route="pos"] { width: 100%; font-size: 50px; margin: var(--margin-sm); - min-height: 11rem; + min-height: 9rem; } .workstation-abbr { From 451c2880118cc0d09e38add2dc0b18fca5bb969a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 5 Feb 2024 14:05:01 +0530 Subject: [PATCH 29/41] perf: Move dimension validation out of GL Entry doctype (#39730) (cherry picked from commit b834ed10d6b6faa1f76e1343ca8b347ca393b7dd) --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 46 +------------------ erpnext/accounts/general_ledger.py | 42 +++++++++++++++++ 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 139f52696bc..17a97a05529 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -13,16 +13,9 @@ import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_checks_for_pl_and_bs_accounts, ) -from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import ( - get_dimension_filter_map, -) from erpnext.accounts.party import validate_party_frozen_disabled, validate_party_gle_currency from erpnext.accounts.utils import get_account_currency, get_fiscal_year -from erpnext.exceptions import ( - InvalidAccountCurrency, - InvalidAccountDimensionError, - MandatoryAccountDimensionError, -) +from erpnext.exceptions import InvalidAccountCurrency exclude_from_linked_with = True @@ -98,7 +91,6 @@ class GLEntry(Document): if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher": self.validate_account_details(adv_adj) self.validate_dimensions_for_pl_and_bs() - self.validate_allowed_dimensions() validate_balance_type(self.account, adv_adj) validate_frozen_account(self.account, adv_adj) @@ -208,42 +200,6 @@ class GLEntry(Document): ) ) - def validate_allowed_dimensions(self): - dimension_filter_map = get_dimension_filter_map() - for key, value in dimension_filter_map.items(): - dimension = key[0] - account = key[1] - - if self.account == account: - if value["is_mandatory"] and not self.get(dimension): - frappe.throw( - _("{0} is mandatory for account {1}").format( - frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account) - ), - MandatoryAccountDimensionError, - ) - - if value["allow_or_restrict"] == "Allow": - if self.get(dimension) and self.get(dimension) not in value["allowed_dimensions"]: - frappe.throw( - _("Invalid value {0} for {1} against account {2}").format( - frappe.bold(self.get(dimension)), - frappe.bold(frappe.unscrub(dimension)), - frappe.bold(self.account), - ), - InvalidAccountDimensionError, - ) - else: - if self.get(dimension) and self.get(dimension) in value["allowed_dimensions"]: - frappe.throw( - _("Invalid value {0} for {1} against account {2}").format( - frappe.bold(self.get(dimension)), - frappe.bold(frappe.unscrub(dimension)), - frappe.bold(self.account), - ), - InvalidAccountDimensionError, - ) - def check_pl_account(self): if ( self.is_opening == "Yes" diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 37ee5de1c9d..9f5c954fa48 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -13,9 +13,13 @@ import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, ) +from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import ( + get_dimension_filter_map, +) from erpnext.accounts.doctype.accounting_period.accounting_period import ClosedAccountingPeriod from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.utils import create_payment_ledger_entry +from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError def make_gl_entries( @@ -355,6 +359,7 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False): process_debit_credit_difference(gl_map) + dimension_filter_map = get_dimension_filter_map() if gl_map: check_freezing_date(gl_map[0]["posting_date"], adv_adj) is_opening = any(d.get("is_opening") == "Yes" for d in gl_map) @@ -362,6 +367,7 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False): validate_against_pcv(is_opening, gl_map[0]["posting_date"], gl_map[0]["company"]) for entry in gl_map: + validate_allowed_dimensions(entry, dimension_filter_map) make_entry(entry, adv_adj, update_outstanding, from_repost) @@ -700,3 +706,39 @@ def set_as_cancel(voucher_type, voucher_no): where voucher_type=%s and voucher_no=%s and is_cancelled = 0""", (now(), frappe.session.user, voucher_type, voucher_no), ) + + +def validate_allowed_dimensions(gl_entry, dimension_filter_map): + for key, value in dimension_filter_map.items(): + dimension = key[0] + account = key[1] + + if gl_entry.account == account: + if value["is_mandatory"] and not gl_entry.get(dimension): + frappe.throw( + _("{0} is mandatory for account {1}").format( + frappe.bold(frappe.unscrub(dimension)), frappe.bold(gl_entry.account) + ), + MandatoryAccountDimensionError, + ) + + if value["allow_or_restrict"] == "Allow": + if gl_entry.get(dimension) and gl_entry.get(dimension) not in value["allowed_dimensions"]: + frappe.throw( + _("Invalid value {0} for {1} against account {2}").format( + frappe.bold(gl_entry.get(dimension)), + frappe.bold(frappe.unscrub(dimension)), + frappe.bold(gl_entry.account), + ), + InvalidAccountDimensionError, + ) + else: + if gl_entry.get(dimension) and gl_entry.get(dimension) in value["allowed_dimensions"]: + frappe.throw( + _("Invalid value {0} for {1} against account {2}").format( + frappe.bold(gl_entry.get(dimension)), + frappe.bold(frappe.unscrub(dimension)), + frappe.bold(gl_entry.account), + ), + InvalidAccountDimensionError, + ) From 55a8326d065e5c439bfc69aa0e427158a60e328a Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 5 Feb 2024 19:26:31 +0530 Subject: [PATCH 30/41] perf: timeout while submitting the purchase receipt entry (cherry picked from commit 1fa62333773a59feb5a0a39d274ebcae362ed286) --- erpnext/buying/doctype/purchase_order/purchase_order.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 22860dfdbf5..9ca20a9ba2f 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -627,6 +627,7 @@ class PurchaseOrder(BuyingController): update_sco_status(sco, "Closed" if self.status == "Closed" else None) +@frappe.request_cache def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0): """get last purchase rate for an item""" From 00e86bf3186a01de99b596a840a8ee1bc526ee63 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 21:55:56 +0530 Subject: [PATCH 31/41] fix: update company in serial no doc (backport #39733) (#39747) fix: update company in serial no doc (cherry picked from commit 7a04f0f7bab1bc36774d41b236e8c4421a6305bd) Co-authored-by: s-aga-r --- erpnext/stock/serial_batch_bundle.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 4cfe5d817e6..78df755d747 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -283,6 +283,7 @@ class SerialBatchBundle: if (sn_table.purchase_document_no != self.sle.voucher_no and self.sle.is_cancelled != 1) else "Inactive", ) + .set(sn_table.company, self.sle.company) .where(sn_table.name.isin(serial_nos)) ).run() From 11d4382e19879ca7a58bca58caaf67254d39e10f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 2 Feb 2024 17:45:50 +0530 Subject: [PATCH 32/41] refactor: ensure unique accounts for each Bank Account's (cherry picked from commit 2caa2d677c09793f7097c3e76ebb4180a3f2a336) --- .../accounts/doctype/bank_account/bank_account.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py index 4b99b198def..22a59bfde08 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.py +++ b/erpnext/accounts/doctype/bank_account/bank_account.py @@ -9,6 +9,7 @@ from frappe.contacts.address_and_contact import ( load_address_and_contact, ) from frappe.model.document import Document +from frappe.utils import comma_and, get_link_to_form class BankAccount(Document): @@ -52,6 +53,17 @@ class BankAccount(Document): def validate(self): self.validate_company() self.validate_iban() + self.validate_account() + + def validate_account(self): + if self.account: + if accounts := frappe.db.get_all("Bank Account", filters={"account": self.account}, as_list=1): + frappe.throw( + _("'{0}' account is already used by {1}. Use another account.").format( + frappe.bold(self.account), + frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])), + ) + ) def validate_company(self): if self.is_company_account and not self.company: From 949f4c3790155da22ed50e0d3bef11a07b2b0bbe Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 3 Feb 2024 10:58:31 +0530 Subject: [PATCH 33/41] refactor(test): generate uniq GL acc and Bank acc for each test case (cherry picked from commit a9a2ec81de73cde0995c837e12cd5dc79a584841) --- .../bank_transaction/test_bank_transaction.py | 72 ++++++++++++------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index 7bb3f4183b4..1fe3608f566 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -32,8 +32,16 @@ class TestBankTransaction(FrappeTestCase): frappe.db.delete(dt) clear_loan_transactions() make_pos_profile() - add_transactions() - add_vouchers() + + # generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error + uniq_identifier = frappe.generate_hash(length=10) + gl_account = create_gl_account("_Test Bank " + uniq_identifier) + bank_account = create_bank_account( + gl_account=gl_account, bank_account_name="Checking Account " + uniq_identifier + ) + + add_transactions(bank_account=bank_account) + add_vouchers(gl_account=gl_account) # This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction. def test_linked_payments(self): @@ -219,7 +227,9 @@ def clear_loan_transactions(): frappe.db.delete("Loan Repayment") -def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"): +def create_bank_account( + bank_name="Citi Bank", gl_account="_Test Bank - _TC", bank_account_name="Checking Account" +): try: frappe.get_doc( { @@ -231,21 +241,35 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"): pass try: - frappe.get_doc( + bank_account = frappe.get_doc( { "doctype": "Bank Account", - "account_name": "Checking Account", + "account_name": bank_account_name, "bank": bank_name, - "account": account_name, + "account": gl_account, } ).insert(ignore_if_duplicate=True) except frappe.DuplicateEntryError: pass + return bank_account.name -def add_transactions(): - create_bank_account() +def create_gl_account(gl_account_name="_Test Bank - _TC"): + gl_account = frappe.get_doc( + { + "doctype": "Account", + "company": "_Test Company", + "parent_account": "Current Assets - _TC", + "account_type": "Bank", + "is_group": 0, + "account_name": gl_account_name, + } + ).insert() + return gl_account.name + + +def add_transactions(bank_account="_Test Bank - _TC"): doc = frappe.get_doc( { "doctype": "Bank Transaction", @@ -253,7 +277,7 @@ def add_transactions(): "date": "2018-10-23", "deposit": 1200, "currency": "INR", - "bank_account": "Checking Account - Citi Bank", + "bank_account": bank_account, } ).insert() doc.submit() @@ -265,7 +289,7 @@ def add_transactions(): "date": "2018-10-23", "deposit": 1700, "currency": "INR", - "bank_account": "Checking Account - Citi Bank", + "bank_account": bank_account, } ).insert() doc.submit() @@ -277,7 +301,7 @@ def add_transactions(): "date": "2018-10-26", "withdrawal": 690, "currency": "INR", - "bank_account": "Checking Account - Citi Bank", + "bank_account": bank_account, } ).insert() doc.submit() @@ -289,7 +313,7 @@ def add_transactions(): "date": "2018-10-27", "deposit": 3900, "currency": "INR", - "bank_account": "Checking Account - Citi Bank", + "bank_account": bank_account, } ).insert() doc.submit() @@ -301,13 +325,13 @@ def add_transactions(): "date": "2018-10-27", "withdrawal": 109080, "currency": "INR", - "bank_account": "Checking Account - Citi Bank", + "bank_account": bank_account, } ).insert() doc.submit() -def add_vouchers(): +def add_vouchers(gl_account="_Test Bank - _TC"): try: frappe.get_doc( { @@ -323,7 +347,7 @@ def add_vouchers(): pi = make_purchase_invoice(supplier="Conrad Electronic", qty=1, rate=690) - pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account) pe.reference_no = "Conrad Oct 18" pe.reference_date = "2018-10-24" pe.insert() @@ -342,14 +366,14 @@ def add_vouchers(): pass pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1200) - pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account) pe.reference_no = "Herr G Oct 18" pe.reference_date = "2018-10-24" pe.insert() pe.submit() pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1700) - pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account) pe.reference_no = "Herr G Nov 18" pe.reference_date = "2018-11-01" pe.insert() @@ -380,10 +404,10 @@ def add_vouchers(): pass pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900, is_paid=1, do_not_save=1) - pi.cash_bank_account = "_Test Bank - _TC" + pi.cash_bank_account = gl_account pi.insert() pi.submit() - pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account) pe.reference_no = "Poore Simon's Oct 18" pe.reference_date = "2018-10-28" pe.paid_amount = 690 @@ -392,7 +416,7 @@ def add_vouchers(): pe.submit() si = create_sales_invoice(customer="Poore Simon's", qty=1, rate=3900) - pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC") + pe = get_payment_entry("Sales Invoice", si.name, bank_account=gl_account) pe.reference_no = "Poore Simon's Oct 18" pe.reference_date = "2018-10-28" pe.insert() @@ -415,16 +439,12 @@ def add_vouchers(): if not frappe.db.get_value( "Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"} ): - mode_of_payment.append( - "accounts", {"company": "_Test Company", "default_account": "_Test Bank - _TC"} - ) + mode_of_payment.append("accounts", {"company": "_Test Company", "default_account": gl_account}) mode_of_payment.save() si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1) si.is_pos = 1 - si.append( - "payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 109080} - ) + si.append("payments", {"mode_of_payment": "Cash", "account": gl_account, "amount": 109080}) si.insert() si.submit() From 4235e08668a56deff9e78f74d9ca8f2976539350 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 3 Feb 2024 12:08:01 +0530 Subject: [PATCH 34/41] refactor(test): make use of test fixtures in Payment Order (cherry picked from commit 322cdbaccf0b8697000aae4e56efa659a34fa8e5) --- .../payment_order/test_payment_order.py | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/payment_order/test_payment_order.py b/erpnext/accounts/doctype/payment_order/test_payment_order.py index 0dcb1794b9a..60f288e1f07 100644 --- a/erpnext/accounts/doctype/payment_order/test_payment_order.py +++ b/erpnext/accounts/doctype/payment_order/test_payment_order.py @@ -4,9 +4,13 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import getdate -from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account +from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import ( + create_bank_account, + create_gl_account, +) from erpnext.accounts.doctype.payment_entry.payment_entry import ( get_payment_entry, make_payment_order, @@ -14,28 +18,32 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import ( from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice -class TestPaymentOrder(unittest.TestCase): +class TestPaymentOrder(FrappeTestCase): def setUp(self): - create_bank_account() + # generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error + uniq_identifier = frappe.generate_hash(length=10) + self.gl_account = create_gl_account("_Test Bank " + uniq_identifier) + self.bank_account = create_bank_account( + gl_account=self.gl_account, bank_account_name="Checking Account " + uniq_identifier + ) def tearDown(self): - for bt in frappe.get_all("Payment Order"): - doc = frappe.get_doc("Payment Order", bt.name) - doc.cancel() - doc.delete() + frappe.db.rollback() def test_payment_order_creation_against_payment_entry(self): purchase_invoice = make_purchase_invoice() payment_entry = get_payment_entry( - "Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC" + "Purchase Invoice", purchase_invoice.name, bank_account=self.gl_account ) payment_entry.reference_no = "_Test_Payment_Order" payment_entry.reference_date = getdate() - payment_entry.party_bank_account = "Checking Account - Citi Bank" + payment_entry.party_bank_account = self.bank_account payment_entry.insert() payment_entry.submit() - doc = create_payment_order_against_payment_entry(payment_entry, "Payment Entry") + doc = create_payment_order_against_payment_entry( + payment_entry, "Payment Entry", self.bank_account + ) reference_doc = doc.get("references")[0] self.assertEqual(reference_doc.reference_name, payment_entry.name) self.assertEqual(reference_doc.reference_doctype, "Payment Entry") @@ -43,13 +51,13 @@ class TestPaymentOrder(unittest.TestCase): self.assertEqual(reference_doc.amount, 250) -def create_payment_order_against_payment_entry(ref_doc, order_type): +def create_payment_order_against_payment_entry(ref_doc, order_type, bank_account): payment_order = frappe.get_doc( dict( doctype="Payment Order", company="_Test Company", payment_order_type=order_type, - company_bank_account="Checking Account - Citi Bank", + company_bank_account=bank_account, ) ) doc = make_payment_order(ref_doc.name, payment_order) From 53992deb10a2a4fd4f7c16c6d03d2bf8bd8ddf36 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 12:45:37 +0530 Subject: [PATCH 35/41] fix: show warehouse title field in sales docs (backport #39746) (#39755) fix: show warehouse title field in sales docs (cherry picked from commit ee14faaa39bcb0fb8d813aee7e00eedc8d3a38c1) Co-authored-by: s-aga-r --- erpnext/controllers/queries.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 604a25127cf..9904c75e98a 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -724,17 +724,24 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters): conditions, bin_conditions = [], [] filter_dict = get_doctype_wise_filters(filters) - query = """select `tabWarehouse`.name, + warehouse_field = "name" + meta = frappe.get_meta("Warehouse") + if meta.get("show_title_field_in_link") and meta.get("title_field"): + searchfield = meta.get("title_field") + warehouse_field = meta.get("title_field") + + query = """select `tabWarehouse`.`{warehouse_field}`, CONCAT_WS(' : ', 'Actual Qty', ifnull(round(`tabBin`.actual_qty, 2), 0 )) actual_qty from `tabWarehouse` left join `tabBin` on `tabBin`.warehouse = `tabWarehouse`.name {bin_conditions} where `tabWarehouse`.`{key}` like {txt} {fcond} {mcond} - order by ifnull(`tabBin`.actual_qty, 0) desc + order by ifnull(`tabBin`.actual_qty, 0) desc, `tabWarehouse`.`{warehouse_field}` asc limit {page_len} offset {start} """.format( + warehouse_field=warehouse_field, bin_conditions=get_filters_cond( doctype, filter_dict.get("Bin"), bin_conditions, ignore_permissions=True ), From 282c19e7e1992d46336a15a2228e2bbc6279851c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 4 Feb 2024 10:42:31 +0530 Subject: [PATCH 36/41] fix: use old serial / batch fields to make serial batch bundle (cherry picked from commit 9fafc83632c4e5c73186bda038b892b7a43d16b8) --- .../pos_invoice_item/pos_invoice_item.json | 33 ++- .../pos_invoice_item/pos_invoice_item.py | 1 + .../purchase_invoice/purchase_invoice.py | 1 + .../purchase_invoice_item.json | 49 ++-- .../purchase_invoice_item.py | 1 + .../doctype/sales_invoice/sales_invoice.py | 1 + .../sales_invoice_item.json | 35 ++- .../sales_invoice_item/sales_invoice_item.py | 1 + .../asset_capitalization.py | 1 + .../asset_capitalization_stock_item.json | 31 +- .../asset_capitalization_stock_item.py | 1 + erpnext/controllers/stock_controller.py | 48 +++ erpnext/public/js/controllers/transaction.js | 40 ++- erpnext/public/js/utils/barcode_scanner.js | 274 ++++-------------- .../doctype/sales_order/sales_order.py | 8 +- .../doctype/delivery_note/delivery_note.py | 2 + .../delivery_note_item.json | 30 +- .../delivery_note_item/delivery_note_item.py | 1 + .../doctype/packed_item/packed_item.json | 33 ++- .../stock/doctype/packed_item/packed_item.py | 1 + erpnext/stock/doctype/pick_list/pick_list.js | 1 - .../stock/doctype/pick_list/pick_list.json | 4 +- erpnext/stock/doctype/pick_list/pick_list.py | 150 +++++++--- .../pick_list_item/pick_list_item.json | 32 +- .../doctype/pick_list_item/pick_list_item.py | 1 + .../purchase_receipt/purchase_receipt.py | 1 + .../purchase_receipt_item.json | 21 +- .../purchase_receipt_item.py | 1 + .../serial_and_batch_bundle.py | 5 +- .../stock/doctype/stock_entry/stock_entry.py | 1 + .../stock_entry_detail.json | 32 +- .../stock_entry_detail/stock_entry_detail.py | 1 + .../stock_reconciliation.py | 1 + .../stock_reconciliation_item.json | 33 ++- .../stock_reconciliation_item.py | 2 + .../stock_settings/stock_settings.json | 9 +- .../doctype/stock_settings/stock_settings.py | 2 + erpnext/stock/serial_batch_bundle.py | 61 ++++ .../subcontracting_receipt.py | 1 + .../subcontracting_receipt_item.json | 36 ++- .../subcontracting_receipt_item.py | 1 + .../subcontracting_receipt_supplied_item.json | 33 ++- .../subcontracting_receipt_supplied_item.py | 1 + 43 files changed, 652 insertions(+), 370 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json index 5a281aaa4fd..ad2889d0a0a 100644 --- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json +++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json @@ -80,13 +80,16 @@ "target_warehouse", "quality_inspection", "serial_and_batch_bundle", - "batch_no", + "use_serial_batch_fields", "col_break5", "allow_zero_valuation_rate", - "serial_no", "item_tax_rate", "actual_batch_qty", "actual_qty", + "section_break_tlhi", + "serial_no", + "column_break_ciit", + "batch_no", "edit_references", "sales_order", "so_detail", @@ -628,13 +631,13 @@ "options": "Quality Inspection" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "batch_no", "fieldtype": "Link", "hidden": 1, "label": "Batch No", "options": "Batch", - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "fieldname": "col_break5", @@ -649,14 +652,14 @@ "print_hide": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "serial_no", "fieldtype": "Small Text", "hidden": 1, "in_list_view": 1, "label": "Serial No", "oldfieldname": "serial_no", - "oldfieldtype": "Small Text", - "read_only": 1 + "oldfieldtype": "Small Text" }, { "fieldname": "item_tax_rate", @@ -824,17 +827,33 @@ "read_only": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", "no_copy": 1, "options": "Serial and Batch Bundle", "print_hide": 1 + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial No / Batch Fields" + }, + { + "depends_on": "eval:doc.use_serial_batch_fields === 1", + "fieldname": "section_break_tlhi", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_ciit", + "fieldtype": "Column Break" } ], "istable": 1, "links": [], - "modified": "2023-11-14 18:33:22.585715", + "modified": "2024-02-04 16:36:25.665743", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice Item", diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py index e2a62f1336e..55a577b0c51 100644 --- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py +++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py @@ -82,6 +82,7 @@ class POSInvoiceItem(Document): target_warehouse: DF.Link | None total_weight: DF.Float uom: DF.Link + use_serial_batch_fields: DF.Check warehouse: DF.Link | None weight_per_unit: DF.Float weight_uom: DF.Link | None diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index cb0b8e1fb11..45b24826400 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -696,6 +696,7 @@ class PurchaseInvoice(BuyingController): # Updating stock ledger should always be called after updating prevdoc status, # because updating ordered qty in bin depends upon updated ordered qty in PO if self.update_stock == 1: + self.make_bundle_using_old_serial_batch_fields() self.update_stock_ledger() if self.is_old_subcontracting_flow: diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 26984d96efd..3ee4214ae71 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -62,16 +62,19 @@ "rm_supp_cost", "warehouse_section", "warehouse", - "from_warehouse", - "quality_inspection", "add_serial_batch_bundle", "serial_and_batch_bundle", - "serial_no", + "use_serial_batch_fields", "col_br_wh", + "from_warehouse", + "quality_inspection", "rejected_warehouse", "rejected_serial_and_batch_bundle", - "batch_no", + "section_break_rqbe", + "serial_no", "rejected_serial_no", + "column_break_vbbb", + "batch_no", "manufacture_details", "manufacturer", "column_break_13", @@ -440,13 +443,11 @@ "print_hide": 1 }, { - "depends_on": "eval:!doc.is_fixed_asset", + "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1", "fieldname": "batch_no", "fieldtype": "Link", - "hidden": 1, "label": "Batch No", "options": "Batch", - "read_only": 1, "search_index": 1 }, { @@ -454,21 +455,18 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval:!doc.is_fixed_asset", + "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1", "fieldname": "serial_no", "fieldtype": "Text", - "hidden": 1, - "label": "Serial No", - "read_only": 1 + "label": "Serial No" }, { - "depends_on": "eval:!doc.is_fixed_asset", + "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1", "fieldname": "rejected_serial_no", "fieldtype": "Text", "label": "Rejected Serial No", "no_copy": 1, - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "fieldname": "accounting", @@ -891,7 +889,7 @@ "label": "Apply TDS" }, { - "depends_on": "eval:parent.update_stock == 1", + "depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", @@ -901,7 +899,7 @@ "search_index": 1 }, { - "depends_on": "eval:parent.update_stock == 1", + "depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)", "fieldname": "rejected_serial_and_batch_bundle", "fieldtype": "Link", "label": "Rejected Serial and Batch Bundle", @@ -916,16 +914,31 @@ "options": "Asset" }, { - "depends_on": "eval:parent.update_stock === 1", + "depends_on": "eval:parent.update_stock === 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)", "fieldname": "add_serial_batch_bundle", "fieldtype": "Button", "label": "Add Serial / Batch No" + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial No / Batch Fields" + }, + { + "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1", + "fieldname": "section_break_rqbe", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_vbbb", + "fieldtype": "Column Break" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2024-01-21 19:46:25.537861", + "modified": "2024-02-04 14:11:52.742228", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py index e48d22379a6..ccbc34749d7 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py @@ -88,6 +88,7 @@ class PurchaseInvoiceItem(Document): stock_uom_rate: DF.Currency total_weight: DF.Float uom: DF.Link + use_serial_batch_fields: DF.Check valuation_rate: DF.Currency warehouse: DF.Link | None weight_per_unit: DF.Float diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 343f3033bfb..bbfe6a38d8d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -446,6 +446,7 @@ class SalesInvoice(SellingController): # Updating stock ledger should always be called after updating prevdoc status, # because updating reserved qty in bin depends upon updated delivered qty in SO if self.update_stock == 1: + self.make_bundle_using_old_serial_batch_fields() self.update_stock_ledger() # this sequence because outstanding may get -ve diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index ec9e792d7d4..d06c7861da7 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -83,14 +83,17 @@ "quality_inspection", "pick_serial_and_batch", "serial_and_batch_bundle", - "batch_no", - "incoming_rate", + "use_serial_batch_fields", "col_break5", "allow_zero_valuation_rate", - "serial_no", + "incoming_rate", "item_tax_rate", "actual_batch_qty", "actual_qty", + "section_break_eoec", + "serial_no", + "column_break_ytgd", + "batch_no", "edit_references", "sales_order", "so_detail", @@ -600,12 +603,11 @@ "options": "Quality Inspection" }, { + "depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1", "fieldname": "batch_no", "fieldtype": "Link", - "hidden": 1, "label": "Batch No", "options": "Batch", - "read_only": 1, "search_index": 1 }, { @@ -621,13 +623,12 @@ "print_hide": 1 }, { + "depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1", "fieldname": "serial_no", "fieldtype": "Small Text", - "hidden": 1, "label": "Serial No", "oldfieldname": "serial_no", - "oldfieldtype": "Small Text", - "read_only": 1 + "oldfieldtype": "Small Text" }, { "fieldname": "item_group", @@ -891,6 +892,7 @@ "read_only": 1 }, { + "depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", @@ -904,12 +906,27 @@ "fieldname": "pick_serial_and_batch", "fieldtype": "Button", "label": "Pick Serial / Batch No" + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial No / Batch Fields" + }, + { + "depends_on": "eval:doc.use_serial_batch_fields === 1 && parent.update_stock === 1", + "fieldname": "section_break_eoec", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_ytgd", + "fieldtype": "Column Break" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-12-29 13:03:14.121298", + "modified": "2024-02-04 11:52:16.106541", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py index 80f67748f40..c71d08e7f70 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py @@ -86,6 +86,7 @@ class SalesInvoiceItem(Document): target_warehouse: DF.Link | None total_weight: DF.Float uom: DF.Link + use_serial_batch_fields: DF.Check warehouse: DF.Link | None weight_per_unit: DF.Float weight_uom: DF.Link | None diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index cad74df51e1..66014904cc4 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -126,6 +126,7 @@ class AssetCapitalization(StockController): self.create_target_asset() def on_submit(self): + self.make_bundle_using_old_serial_batch_fields() self.update_stock_ledger() self.make_gl_entries() self.update_target_asset() diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json index 26e1c3c270f..d301454be88 100644 --- a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json +++ b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json @@ -18,9 +18,12 @@ "amount", "batch_and_serial_no_section", "serial_and_batch_bundle", + "use_serial_batch_fields", "column_break_13", - "batch_no", + "section_break_bfqc", "serial_no", + "column_break_mbuv", + "batch_no", "accounting_dimensions_section", "cost_center", "dimension_col_break" @@ -39,13 +42,13 @@ "reqd": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", "no_copy": 1, "options": "Batch", - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "fieldname": "section_break_6", @@ -102,12 +105,12 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "serial_no", "fieldtype": "Small Text", "hidden": 1, "label": "Serial No", - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "fieldname": "item_code", @@ -148,18 +151,34 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", "no_copy": 1, "options": "Serial and Batch Bundle", "print_hide": 1 + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial No / Batch Fields" + }, + { + "depends_on": "eval:doc.use_serial_batch_fields === 1", + "fieldname": "section_break_bfqc", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_mbuv", + "fieldtype": "Column Break" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-04-06 01:10:17.947952", + "modified": "2024-02-04 16:41:09.239762", "modified_by": "Administrator", "module": "Assets", "name": "Asset Capitalization Stock Item", diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py index 122cbb600d6..d2b075c3e68 100644 --- a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py +++ b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py @@ -27,6 +27,7 @@ class AssetCapitalizationStockItem(Document): serial_no: DF.SmallText | None stock_qty: DF.Float stock_uom: DF.Link + use_serial_batch_fields: DF.Check valuation_rate: DF.Currency warehouse: DF.Link # end: auto-generated types diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index c8516820ef3..626d341443f 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -126,6 +126,54 @@ class StockController(AccountsController): # remove extra whitespace and store one serial no on each line row.serial_no = clean_serial_no_string(row.serial_no) + def make_bundle_using_old_serial_batch_fields(self): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + from erpnext.stock.serial_batch_bundle import SerialBatchCreation + + for row in self.items: + if not row.serial_no and not row.batch_no and not row.get("rejected_serial_no"): + continue + + if not row.use_serial_batch_fields and ( + row.serial_no or row.batch_no or row.get("rejected_serial_no") + ): + frappe.throw(_("Please enable Use Old Serial / Batch Fields to make_bundle")) + + if row.use_serial_batch_fields and ( + not row.serial_and_batch_bundle or not row.get("rejected_serial_and_batch_bundle") + ): + sn_doc = SerialBatchCreation( + { + "item_code": row.item_code, + "warehouse": row.warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "voucher_type": self.doctype, + "voucher_no": self.name, + "voucher_detail_no": row.name, + "qty": row.stock_qty, + "type_of_transaction": "Inward" if row.stock_qty > 0 else "Outward", + "company": self.company, + "is_rejected": 1 if row.get("rejected_warehouse") else 0, + "serial_nos": get_serial_nos(row.serial_no) if row.serial_no else None, + "batches": frappe._dict({row.batch_no: row.stock_qty}) if row.batch_no else None, + "batch_no": row.batch_no, + "use_serial_batch_fields": row.use_serial_batch_fields, + } + ).make_serial_and_batch_bundle() + + if sn_doc.is_rejected: + row.rejected_serial_and_batch_bundle = sn_doc.name + row.db_set("rejected_serial_and_batch_bundle", sn_doc.name) + else: + row.serial_and_batch_bundle = sn_doc.name + row.db_set("serial_and_batch_bundle", sn_doc.name) + + def set_use_serial_batch_fields(self): + if frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields"): + for row in self.items: + row.use_serial_batch_fields = 1 + def get_gl_entries( self, warehouse_account=None, default_expense_account=None, default_cost_center=None ): diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 5da6d7ec610..846c071c432 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -7,6 +7,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe super.setup(); let me = this; + this.set_fields_onload_for_line_item(); this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle']; frappe.flags.hide_serial_batch_dialog = true; @@ -105,6 +106,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe frappe.ui.form.on(this.frm.doctype + " Item", { items_add: function(frm, cdt, cdn) { + debugger var item = frappe.get_doc(cdt, cdn); if (!item.warehouse && frm.doc.set_warehouse) { item.warehouse = frm.doc.set_warehouse; @@ -118,6 +120,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe item.from_warehouse = frm.doc.set_from_warehouse; } + if (item.docstatus === 0 + && frappe.meta.has_field(item.doctype, "use_serial_batch_fields") + && cint(frappe.user_defaults?.use_serial_batch_fields) === 1 + ) { + frappe.model.set_value(item.doctype, item.name, "use_serial_batch_fields", 1); + } + erpnext.accounts.dimensions.copy_dimension_from_first_row(frm, cdt, cdn, 'items'); } }); @@ -222,7 +231,19 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }; }); } + } + set_fields_onload_for_line_item() { + if (this.frm.is_new && this.frm.doc?.items) { + this.frm.doc.items.forEach(item => { + if (item.docstatus === 0 + && frappe.meta.has_field(item.doctype, "use_serial_batch_fields") + && cint(frappe.user_defaults?.use_serial_batch_fields) === 1 + ) { + frappe.model.set_value(item.doctype, item.name, "use_serial_batch_fields", 1); + } + }) + } } toggle_enable_for_stock_uom(field) { @@ -462,6 +483,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe this.frm.doc.doctype === 'Delivery Note') { show_batch_dialog = 1; } + + if (show_batch_dialog && item.use_serial_batch_fields === 1) { + show_batch_dialog = 0; + } + item.barcode = null; @@ -1242,20 +1268,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } } - sync_bundle_data() { - let doctypes = ["Sales Invoice", "Purchase Invoice", "Delivery Note", "Purchase Receipt"]; - - if (this.frm.is_new() && doctypes.includes(this.frm.doc.doctype)) { - const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm}); - barcode_scanner.sync_bundle_data(); - barcode_scanner.remove_item_from_localstorage(); - } - } - - before_save(doc) { - this.sync_bundle_data(); - } - service_start_date(frm, cdt, cdn) { var child = locals[cdt][cdn]; diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js index aacab0fe6c1..4d1c0c1ad3d 100644 --- a/erpnext/public/js/utils/barcode_scanner.js +++ b/erpnext/public/js/utils/barcode_scanner.js @@ -1,12 +1,15 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { constructor(opts) { this.frm = opts.frm; + // frappe.flags.trigger_from_barcode_scanner is used for custom scripts // field from which to capture input of scanned data this.scan_field_name = opts.scan_field_name || "scan_barcode"; this.scan_barcode_field = this.frm.fields_dict[this.scan_field_name]; this.barcode_field = opts.barcode_field || "barcode"; + this.serial_no_field = opts.serial_no_field || "serial_no"; + this.batch_no_field = opts.batch_no_field || "batch_no"; this.uom_field = opts.uom_field || "uom"; this.qty_field = opts.qty_field || "qty"; // field name on row which defines max quantity to be scanned e.g. picklist @@ -105,53 +108,52 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { this.frm.has_items = false; } - if (serial_no) { - this.is_duplicate_serial_no(row, item_code, serial_no) - .then((is_duplicate) => { - if (!is_duplicate) { - this.run_serially_tasks(row, data, resolve); - } else { - this.clean_up(); - reject(); - return; - } - }); - } else { - this.run_serially_tasks(row, data, resolve); + if (this.is_duplicate_serial_no(row, serial_no)) { + this.clean_up(); + reject(); + return; } - + frappe.run_serially([ + () => this.set_selector_trigger_flag(data), + () => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => { + this.show_scan_message(row.idx, row.item_code, qty); + }), + () => this.set_barcode_uom(row, uom), + () => this.set_serial_no(row, serial_no), + () => this.set_batch_no(row, batch_no), + () => this.set_barcode(row, barcode), + () => this.clean_up(), + () => this.revert_selector_flag(), + () => resolve(row) + ]); }); } - run_serially_tasks(row, data, resolve) { - const {item_code, barcode, batch_no, serial_no, uom} = data; + // batch and serial selector is reduandant when all info can be added by scan + // this flag on item row is used by transaction.js to avoid triggering selector + set_selector_trigger_flag(data) { + const {batch_no, serial_no, has_batch_no, has_serial_no} = data; - frappe.run_serially([ - () => this.set_serial_and_batch(row, item_code, serial_no, batch_no), - () => this.set_barcode(row, barcode), - () => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => { - this.show_scan_message(row.idx, row.item_code, qty); - }), - () => this.set_barcode_uom(row, uom), - () => this.clean_up(), - () => { - if (row.serial_and_batch_bundle && !this.frm.is_new()) { - this.frm.save(); - } + const require_selecting_batch = has_batch_no && !batch_no; + const require_selecting_serial = has_serial_no && !serial_no; - frappe.flags.trigger_from_barcode_scanner = false; - }, - () => resolve(row), - ]); + if (!(require_selecting_batch || require_selecting_serial)) { + frappe.flags.hide_serial_batch_dialog = true; + } + } + + revert_selector_flag() { + frappe.flags.hide_serial_batch_dialog = false; + frappe.flags.trigger_from_barcode_scanner = false; } set_item(row, item_code, barcode, batch_no, serial_no) { return new Promise(resolve => { const increment = async (value = 1) => { - const item_data = {item_code: item_code}; - item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value); + const item_data = {item_code: item_code, use_serial_batch_fields: 1.0}; frappe.flags.trigger_from_barcode_scanner = true; + item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value); await frappe.model.set_value(row.doctype, row.name, item_data); return value; }; @@ -160,6 +162,8 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { frappe.prompt(__("Please enter quantity for item {0}", [item_code]), ({value}) => { increment(value).then((value) => resolve(value)); }); + } else if (this.frm.has_items) { + this.prepare_item_for_scan(row, item_code, barcode, batch_no, serial_no); } else { increment().then((value) => resolve(value)); } @@ -182,8 +186,9 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { frappe.model.set_value(row.doctype, row.name, item_data); frappe.run_serially([ + () => this.set_batch_no(row, this.dialog.get_value("batch_no")), () => this.set_barcode(row, this.dialog.get_value("barcode")), - () => this.set_serial_and_batch(row, item_code, this.dialog.get_value("serial_no"), this.dialog.get_value("batch_no")), + () => this.set_serial_no(row, this.dialog.get_value("serial_no")), () => this.add_child_for_remaining_qty(row), () => this.clean_up() ]); @@ -337,144 +342,32 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { } } - async set_serial_and_batch(row, item_code, serial_no, batch_no) { - if (this.frm.is_new() || !row.serial_and_batch_bundle) { - this.set_bundle_in_localstorage(row, item_code, serial_no, batch_no); - } else if(row.serial_and_batch_bundle) { - frappe.call({ - method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.update_serial_or_batch", - args: { - bundle_id: row.serial_and_batch_bundle, - serial_no: serial_no, - batch_no: batch_no, - }, - }) - } - } + async set_serial_no(row, serial_no) { + if (serial_no && frappe.meta.has_field(row.doctype, this.serial_no_field)) { + const existing_serial_nos = row[this.serial_no_field]; + let new_serial_nos = ""; - get_key_for_localstorage() { - let parts = this.frm.doc.name.split("-"); - return parts[parts.length - 1] + this.frm.doc.doctype; - } - - update_localstorage_scanned_data() { - let docname = this.frm.doc.name - if (localStorage[docname]) { - let items = JSON.parse(localStorage[docname]); - let existing_items = this.frm.doc.items.map(d => d.item_code); - if (!existing_items.length) { - localStorage.removeItem(docname); - return; + if (!!existing_serial_nos) { + new_serial_nos = existing_serial_nos + "\n" + serial_no; + } else { + new_serial_nos = serial_no; } - - for (let item_code in items) { - if (!existing_items.includes(item_code)) { - delete items[item_code]; - } - } - - localStorage[docname] = JSON.stringify(items); + await frappe.model.set_value(row.doctype, row.name, this.serial_no_field, new_serial_nos); } } - async set_bundle_in_localstorage(row, item_code, serial_no, batch_no) { - let docname = this.frm.doc.name - - let entries = JSON.parse(localStorage.getItem(docname)); - if (!entries) { - entries = {}; - } - - let key = item_code; - if (!entries[key]) { - entries[key] = []; - } - - let existing_row = []; - if (!serial_no && batch_no) { - existing_row = entries[key].filter((e) => e.batch_no === batch_no); - if (existing_row.length) { - existing_row[0].qty += 1; - } - } else if (serial_no) { - existing_row = entries[key].filter((e) => e.serial_no === serial_no); - if (existing_row.length) { - frappe.throw(__("Serial No {0} has already scanned.", [serial_no])); - } - } - - if (!existing_row.length) { - entries[key].push({ - "serial_no": serial_no, - "batch_no": batch_no, - "qty": 1 - }); - } - - localStorage.setItem(docname, JSON.stringify(entries)); - - // Auto remove from localstorage after 1 hour - setTimeout(() => { - localStorage.removeItem(docname); - }, 3600000) - } - - remove_item_from_localstorage() { - let docname = this.frm.doc.name; - if (localStorage[docname]) { - localStorage.removeItem(docname); - } - } - - async sync_bundle_data() { - let docname = this.frm.doc.name; - - if (localStorage[docname]) { - let entries = JSON.parse(localStorage[docname]); - if (entries) { - for (let entry in entries) { - let row = this.frm.doc.items.filter((item) => { - if (item.item_code === entry) { - return true; - } - })[0]; - - if (row) { - this.create_serial_and_batch_bundle(row, entries, entry) - .then(() => { - if (!entries) { - localStorage.removeItem(docname); - } - }); - } - } - } - } - } - - async create_serial_and_batch_bundle(row, entries, key) { - frappe.call({ - method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.add_serial_batch_ledgers", - args: { - entries: entries[key], - child_row: row, - doc: this.frm.doc, - warehouse: row.warehouse, - do_not_save: 1 - }, - callback: function(r) { - row.serial_and_batch_bundle = r.message.name; - delete entries[key]; - } - }) - } - async set_barcode_uom(row, uom) { if (uom && frappe.meta.has_field(row.doctype, this.uom_field)) { await frappe.model.set_value(row.doctype, row.name, this.uom_field, uom); } } + async set_batch_no(row, batch_no) { + if (batch_no && frappe.meta.has_field(row.doctype, this.batch_no_field)) { + await frappe.model.set_value(row.doctype, row.name, this.batch_no_field, batch_no); + } + } + async set_barcode(row, barcode) { if (barcode && frappe.meta.has_field(row.doctype, this.barcode_field)) { await frappe.model.set_value(row.doctype, row.name, this.barcode_field, barcode); @@ -490,58 +383,13 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { } } - async is_duplicate_serial_no(row, item_code, serial_no) { - let is_duplicate = false; - const promise = new Promise((resolve, reject) => { - if (this.frm.is_new() || !row.serial_and_batch_bundle) { - is_duplicate = this.check_duplicate_serial_no_in_localstorage(item_code, serial_no); - if (is_duplicate) { - this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange"); - } + is_duplicate_serial_no(row, serial_no) { + const is_duplicate = row[this.serial_no_field]?.includes(serial_no); - resolve(is_duplicate); - } else if (row.serial_and_batch_bundle) { - this.check_duplicate_serial_no_in_db(row, serial_no, (r) => { - if (r.message) { - this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange"); - } - - is_duplicate = r.message; - resolve(is_duplicate); - }) - } - }); - - return await promise; - } - - check_duplicate_serial_no_in_db(row, serial_no, response) { - frappe.call({ - method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.is_duplicate_serial_no", - args: { - serial_no: serial_no, - bundle_id: row.serial_and_batch_bundle - }, - callback(r) { - response(r); - } - }); - } - - check_duplicate_serial_no_in_localstorage(item_code, serial_no) { - let docname = this.frm.doc.name - let entries = JSON.parse(localStorage.getItem(docname)); - - if (!entries) { - return false; + if (is_duplicate) { + this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange"); } - - let existing_row = []; - if (entries[item_code]) { - existing_row = entries[item_code].filter((e) => e.serial_no === serial_no); - } - - return existing_row.length; + return is_duplicate; } get_row_to_modify_on_scan(item_code, batch_no, uom, barcode) { @@ -587,4 +435,4 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { show_alert(msg, indicator, duration=3) { frappe.show_alert({message: msg, indicator: indicator}, duration); } -}; +}; \ No newline at end of file diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 5ef2c50146a..f00e6ac5122 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -904,6 +904,7 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None): target.run_method("set_missing_values") target.run_method("set_po_nos") target.run_method("calculate_taxes_and_totals") + target.run_method("set_use_serial_batch_fields") if source.company_address: target.update({"company_address": source.company_address}) @@ -1024,6 +1025,7 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): target.run_method("set_missing_values") target.run_method("set_po_nos") target.run_method("calculate_taxes_and_totals") + target.run_method("set_use_serial_batch_fields") if source.company_address: target.update({"company_address": source.company_address}) @@ -1606,7 +1608,11 @@ def create_pick_list(source_name, target_doc=None): "Sales Order", source_name, { - "Sales Order": {"doctype": "Pick List", "validation": {"docstatus": ["=", 1]}}, + "Sales Order": { + "doctype": "Pick List", + "field_map": {"set_warehouse": "parent_warehouse"}, + "validation": {"docstatus": ["=", 1]}, + }, "Sales Order Item": { "doctype": "Pick List Item", "field_map": {"parent": "sales_order", "name": "sales_order_item"}, diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 7d7b0cd4769..df45fdded89 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -398,6 +398,8 @@ class DeliveryNote(SellingController): self.check_credit_limit() elif self.issue_credit_note: self.make_return_invoice() + + self.make_bundle_using_old_serial_batch_fields() # Updating stock ledger should always be called after updating prevdoc status, # because updating reserved qty in bin depends upon updated delivered qty in SO self.update_stock_ledger() diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index a44b9ac44be..247672fe126 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -80,8 +80,11 @@ "section_break_40", "pick_serial_and_batch", "serial_and_batch_bundle", + "use_serial_batch_fields", "column_break_eaoe", + "section_break_qyjv", "serial_no", + "column_break_rxvc", "batch_no", "available_qty_section", "actual_batch_qty", @@ -850,6 +853,7 @@ "read_only": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", @@ -859,6 +863,7 @@ "search_index": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "pick_serial_and_batch", "fieldtype": "Button", "label": "Pick Serial / Batch No" @@ -874,27 +879,40 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "serial_no", "fieldtype": "Text", - "hidden": 1, - "label": "Serial No", - "read_only": 1 + "label": "Serial No" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "batch_no", "fieldtype": "Link", - "hidden": 1, "label": "Batch No", "options": "Batch", - "read_only": 1, "search_index": 1 + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial No / Batch Fields" + }, + { + "depends_on": "eval:doc.use_serial_batch_fields === 1", + "fieldname": "section_break_qyjv", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_rxvc", + "fieldtype": "Column Break" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:37:38.638144", + "modified": "2024-02-04 14:10:31.750340", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py index c11c4103e59..b76f7429728 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py @@ -82,6 +82,7 @@ class DeliveryNoteItem(Document): target_warehouse: DF.Link | None total_weight: DF.Float uom: DF.Link + use_serial_batch_fields: DF.Check warehouse: DF.Link | None weight_per_unit: DF.Float weight_uom: DF.Link | None diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index 5dd8934d43f..0b006ab363d 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -20,9 +20,12 @@ "uom", "section_break_9", "pick_serial_and_batch", - "serial_and_batch_bundle", - "serial_no", + "use_serial_batch_fields", "column_break_11", + "serial_and_batch_bundle", + "section_break_bgys", + "serial_no", + "column_break_qlha", "batch_no", "actual_batch_qty", "section_break_13", @@ -118,10 +121,10 @@ "fieldtype": "Section Break" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "serial_no", "fieldtype": "Text", - "label": "Serial No", - "read_only": 1 + "label": "Serial No" }, { "fieldname": "column_break_11", @@ -131,8 +134,7 @@ "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", - "options": "Batch", - "read_only": 1 + "options": "Batch" }, { "fieldname": "section_break_13", @@ -259,6 +261,7 @@ "read_only": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", @@ -267,16 +270,32 @@ "print_hide": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0", "fieldname": "pick_serial_and_batch", "fieldtype": "Button", "label": "Pick Serial / Batch No" + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial No / Batch Fields" + }, + { + "depends_on": "eval:doc.use_serial_batch_fields === 1", + "fieldname": "section_break_bgys", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_qlha", + "fieldtype": "Column Break" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-04-28 13:16:38.460806", + "modified": "2024-02-04 16:30:44.263964", "modified_by": "Administrator", "module": "Stock", "name": "Packed Item", diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index ed667c2b992..c115e33e171 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -47,6 +47,7 @@ class PackedItem(Document): serial_no: DF.Text | None target_warehouse: DF.Link | None uom: DF.Link | None + use_serial_batch_fields: DF.Check warehouse: DF.Link | None # end: auto-generated types diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index afd6ce81386..aa0e1254968 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -16,7 +16,6 @@ frappe.ui.form.on('Pick List', { frm.set_query('parent_warehouse', () => { return { filters: { - 'is_group': 1, 'company': frm.doc.company } }; diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json index 7259dc00a81..bd84aadef74 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.json +++ b/erpnext/stock/doctype/pick_list/pick_list.json @@ -51,7 +51,7 @@ "description": "Items under this warehouse will be suggested", "fieldname": "parent_warehouse", "fieldtype": "Link", - "label": "Parent Warehouse", + "label": "Warehouse", "options": "Warehouse" }, { @@ -188,7 +188,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2023-01-24 10:33:43.244476", + "modified": "2024-02-01 16:17:44.877426", "modified_by": "Administrator", "module": "Stock", "name": "Pick List", diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 758448af797..f2edeea56d1 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -13,7 +13,7 @@ from frappe.model.mapper import map_child_doc from frappe.query_builder import Case from frappe.query_builder.custom import GROUP_CONCAT from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum -from frappe.utils import cint, floor, flt +from frappe.utils import ceil, cint, floor, flt from frappe.utils.nestedset import get_descendants_of from erpnext.selling.doctype.sales_order.sales_order import ( @@ -122,11 +122,43 @@ class PickList(Document): def on_submit(self): self.validate_serial_and_batch_bundle() + self.make_bundle_using_old_serial_batch_fields() self.update_status() self.update_bundle_picked_qty() self.update_reference_qty() self.update_sales_order_picking_status() + def make_bundle_using_old_serial_batch_fields(self): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + for row in self.locations: + if not row.serial_no and not row.batch_no: + continue + + if not row.use_serial_batch_fields and (row.serial_no or row.batch_no): + frappe.throw(_("Please enable Use Old Serial / Batch Fields to make_bundle")) + + if row.use_serial_batch_fields and (not row.serial_and_batch_bundle): + sn_doc = SerialBatchCreation( + { + "item_code": row.item_code, + "warehouse": row.warehouse, + "voucher_type": self.doctype, + "voucher_no": self.name, + "voucher_detail_no": row.name, + "qty": row.stock_qty, + "type_of_transaction": "Inward" if row.stock_qty > 0 else "Outward", + "company": self.company, + "serial_nos": get_serial_nos(row.serial_no) if row.serial_no else None, + "batches": frappe._dict({row.batch_no: row.stock_qty}) if row.batch_no else None, + "batch_no": row.batch_no, + "use_serial_batch_fields": row.use_serial_batch_fields, + } + ).make_serial_and_batch_bundle() + + row.serial_and_batch_bundle = sn_doc.name + row.db_set("serial_and_batch_bundle", sn_doc.name) + def on_update_after_submit(self) -> None: if self.has_reserved_stock(): msg = _( @@ -317,8 +349,12 @@ class PickList(Document): self.item_location_map = frappe._dict() from_warehouses = None - if self.parent_warehouse: + if self.parent_warehouse and frappe.get_cached_value( + "Warehouse", self.parent_warehouse, "is_group" + ): from_warehouses = get_descendants_of("Warehouse", self.parent_warehouse) + elif self.parent_warehouse: + from_warehouses = [self.parent_warehouse] # Create replica before resetting, to handle empty table on update after submit. locations_replica = self.get("locations") @@ -351,13 +387,13 @@ class PickList(Document): for row in locations: location = item_doc.as_dict() location.update(row) + bundle = location.serial_and_batch_bundle or location.serial_no or location.batch_no key = ( location.item_code, location.warehouse, location.uom, - location.batch_no, - location.serial_no, location.sales_order_item or location.material_request_item, + bundle, ) if key not in updated_locations: @@ -645,7 +681,9 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus) "qty": qty, "stock_qty": stock_qty, "warehouse": item_location.warehouse, - "serial_and_batch_bundle": item_location.serial_and_batch_bundle, + "serial_no": item_location.serial_no, + "batch_no": item_location.batch_no, + "use_serial_batch_fields": 1, } ) ) @@ -673,6 +711,7 @@ def get_available_item_locations( company, ignore_validation=False, picked_item_details=None, + consider_rejected_warehouses=False, ): locations = [] total_picked_qty = ( @@ -681,7 +720,16 @@ def get_available_item_locations( has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no") has_batch_no = frappe.get_cached_value("Item", item_code, "has_batch_no") - if has_serial_no: + if has_batch_no and has_serial_no: + locations = get_available_item_locations_for_serial_and_batched_item( + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty, + consider_rejected_warehouses=consider_rejected_warehouses, + ) + elif has_serial_no: locations = get_available_item_locations_for_serialized_item( item_code, from_warehouses, required_qty, company, total_picked_qty ) @@ -724,6 +772,49 @@ def get_available_item_locations( return locations +def get_available_item_locations_for_serial_and_batched_item( + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty=0, + consider_rejected_warehouses=False, +): + # Get batch nos by FIFO + locations = get_available_item_locations_for_batched_item( + item_code, + from_warehouses, + required_qty, + company, + consider_rejected_warehouses=consider_rejected_warehouses, + ) + + if locations: + sn = frappe.qb.DocType("Serial No") + conditions = (sn.item_code == item_code) & (sn.company == company) + + for location in locations: + location.qty = ( + required_qty if location.qty > required_qty else location.qty + ) # if extra qty in batch + + serial_nos = ( + frappe.qb.from_(sn) + .select(sn.name) + .where( + (conditions) & (sn.batch_no == location.batch_no) & (sn.warehouse == location.warehouse) + ) + .orderby(sn.purchase_date) + .limit(ceil(location.qty + total_picked_qty)) + ).run(as_dict=True) + + serial_nos = [sn.name for sn in serial_nos] + location.serial_no = serial_nos + location.qty = len(serial_nos) + + return locations + + def get_available_item_locations_for_serialized_item( item_code, from_warehouses, required_qty, company, total_picked_qty=0 ): @@ -760,25 +851,12 @@ def get_available_item_locations_for_serialized_item( for warehouse, serial_nos in warehouse_serial_nos_map.items(): qty = len(serial_nos) - bundle_doc = SerialBatchCreation( - { - "item_code": item_code, - "warehouse": warehouse, - "voucher_type": "Pick List", - "total_qty": qty * -1, - "serial_nos": serial_nos, - "type_of_transaction": "Outward", - "company": company, - "do_not_submit": True, - } - ).make_serial_and_batch_bundle() - locations.append( { "qty": qty, "warehouse": warehouse, "item_code": item_code, - "serial_and_batch_bundle": bundle_doc.name, + "serial_nos": serial_nos, } ) @@ -808,29 +886,15 @@ def get_available_item_locations_for_batched_item( warehouse_wise_batches[d.warehouse][d.batch_no] += d.qty for warehouse, batches in warehouse_wise_batches.items(): - qty = sum(batches.values()) - - bundle_doc = SerialBatchCreation( - { - "item_code": item_code, - "warehouse": warehouse, - "voucher_type": "Pick List", - "total_qty": qty * -1, - "batches": batches, - "type_of_transaction": "Outward", - "company": company, - "do_not_submit": True, - } - ).make_serial_and_batch_bundle() - - locations.append( - { - "qty": qty, - "warehouse": warehouse, - "item_code": item_code, - "serial_and_batch_bundle": bundle_doc.name, - } - ) + for batch_no, qty in batches.items(): + locations.append( + { + "qty": qty, + "warehouse": warehouse, + "item_code": item_code, + "batch_no": batch_no, + } + ) return locations diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json index e8e4afc6e3f..c8001fd508d 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json @@ -24,8 +24,11 @@ "serial_no_and_batch_section", "pick_serial_and_batch", "serial_and_batch_bundle", - "serial_no", + "use_serial_batch_fields", "column_break_20", + "section_break_ecxc", + "serial_no", + "column_break_belw", "batch_no", "column_break_15", "sales_order", @@ -72,19 +75,17 @@ "read_only": 1 }, { - "depends_on": "serial_no", + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "serial_no", "fieldtype": "Small Text", - "label": "Serial No", - "read_only": 1 + "label": "Serial No" }, { - "depends_on": "batch_no", + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", "options": "Batch", - "read_only": 1, "search_index": 1 }, { @@ -195,6 +196,7 @@ "read_only": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", @@ -204,6 +206,7 @@ "search_index": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0", "fieldname": "pick_serial_and_batch", "fieldtype": "Button", "label": "Pick Serial / Batch No" @@ -218,11 +221,26 @@ "print_hide": 1, "read_only": 1, "report_hide": 1 + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial No / Batch Fields" + }, + { + "depends_on": "eval:doc.use_serial_batch_fields === 1", + "fieldname": "section_break_ecxc", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_belw", + "fieldtype": "Column Break" } ], "istable": 1, "links": [], - "modified": "2023-07-26 12:54:15.785962", + "modified": "2024-02-04 16:12:16.257951", "modified_by": "Administrator", "module": "Stock", "name": "Pick List Item", diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.py b/erpnext/stock/doctype/pick_list_item/pick_list_item.py index 6e5a94e4465..f3f6298a305 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.py +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.py @@ -37,6 +37,7 @@ class PickListItem(Document): stock_reserved_qty: DF.Float stock_uom: DF.Link | None uom: DF.Link | None + use_serial_batch_fields: DF.Check warehouse: DF.Link | None # end: auto-generated types diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 8da05966317..28d55f6ce3a 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -368,6 +368,7 @@ class PurchaseReceipt(BuyingController): else: self.db_set("status", "Completed") + self.make_bundle_using_old_serial_batch_fields() # Updating stock ledger should always be called after updating prevdoc status, # because updating ordered qty, reserved_qty_for_subcontract in bin # depends upon updated ordered qty in PO diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 9bd692ad618..6b01047f006 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -94,6 +94,7 @@ "section_break_45", "add_serial_batch_bundle", "serial_and_batch_bundle", + "use_serial_batch_fields", "col_break5", "add_serial_batch_for_rejected_qty", "rejected_serial_and_batch_bundle", @@ -1003,6 +1004,7 @@ "read_only": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", @@ -1020,24 +1022,22 @@ { "fieldname": "serial_no", "fieldtype": "Text", - "label": "Serial No", - "read_only": 1 + "label": "Serial No" }, { "fieldname": "rejected_serial_no", "fieldtype": "Text", - "label": "Rejected Serial No", - "read_only": 1 + "label": "Rejected Serial No" }, { "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", "options": "Batch", - "read_only": 1, "search_index": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "rejected_serial_and_batch_bundle", "fieldtype": "Link", "label": "Rejected Serial and Batch Bundle", @@ -1045,11 +1045,13 @@ "options": "Serial and Batch Bundle" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0", "fieldname": "add_serial_batch_for_rejected_qty", "fieldtype": "Button", "label": "Add Serial / Batch No (Rejected Qty)" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "section_break_3vxt", "fieldtype": "Section Break" }, @@ -1058,6 +1060,7 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0", "fieldname": "add_serial_batch_bundle", "fieldtype": "Button", "label": "Add Serial / Batch No" @@ -1098,12 +1101,18 @@ "read_only": 1, "report_hide": 1, "search_index": 1 + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial No / Batch Fields" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-12-25 22:32:09.801965", + "modified": "2024-02-04 11:48:06.653771", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py index aed8d21dae7..3c6dcdca488 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py @@ -99,6 +99,7 @@ class PurchaseReceiptItem(Document): supplier_part_no: DF.Data | None total_weight: DF.Float uom: DF.Link + use_serial_batch_fields: DF.Check valuation_rate: DF.Currency warehouse: DF.Link | None weight_per_unit: DF.Float diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 9cad8f62b88..ea33c54544d 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -1677,7 +1677,10 @@ def get_reserved_batches_for_sre(kwargs) -> dict: query = query.where(sb_entry.batch_no == kwargs.batch_no) if kwargs.warehouse: - query = query.where(sre.warehouse == kwargs.warehouse) + if isinstance(kwargs.warehouse, list): + query = query.where(sre.warehouse.isin(kwargs.warehouse)) + else: + query = query.where(sre.warehouse == kwargs.warehouse) if kwargs.ignore_voucher_nos: query = query.where(sre.name.notin(kwargs.ignore_voucher_nos)) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 00cc8be4bb8..4239191383d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -274,6 +274,7 @@ class StockEntry(StockController): def on_submit(self): self.validate_closed_subcontracting_order() + self.make_bundle_using_old_serial_batch_fields() self.update_stock_ledger() self.update_work_order() self.validate_subcontract_order() diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index bd84a2b0d99..bd11d0b861d 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -47,9 +47,12 @@ "amount", "serial_no_batch", "add_serial_batch_bundle", - "serial_and_batch_bundle", + "use_serial_batch_fields", "col_break4", + "serial_and_batch_bundle", + "section_break_rdtg", "serial_no", + "column_break_prps", "batch_no", "accounting", "expense_account", @@ -289,27 +292,27 @@ "no_copy": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "serial_no", "fieldtype": "Small Text", "label": "Serial No", "no_copy": 1, "oldfieldname": "serial_no", - "oldfieldtype": "Text", - "read_only": 1 + "oldfieldtype": "Text" }, { "fieldname": "col_break4", "fieldtype": "Column Break" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", "no_copy": 1, "oldfieldname": "batch_no", "oldfieldtype": "Link", - "options": "Batch", - "read_only": 1 + "options": "Batch" }, { "depends_on": "eval:parent.inspection_required && doc.t_warehouse", @@ -573,24 +576,41 @@ "read_only": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0", "fieldname": "add_serial_batch_bundle", "fieldtype": "Button", "label": "Add Serial / Batch No" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", "no_copy": 1, "options": "Serial and Batch Bundle", "print_hide": 1 + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial No / Batch Fields" + }, + { + "depends_on": "eval:doc.use_serial_batch_fields === 1", + "fieldname": "section_break_rdtg", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_prps", + "fieldtype": "Column Break" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-01-12 11:56:04.626103", + "modified": "2024-02-04 16:16:47.606270", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py index a6dd0faadfc..47c443c5194 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py @@ -63,6 +63,7 @@ class StockEntryDetail(Document): transfer_qty: DF.Float transferred_qty: DF.Float uom: DF.Link + use_serial_batch_fields: DF.Check valuation_rate: DF.Currency # end: auto-generated types diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 788ae0d3abc..cc8a7c57b37 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -99,6 +99,7 @@ class StockReconciliation(StockController): ) def on_submit(self): + self.make_bundle_using_old_serial_batch_fields() self.update_stock_ledger() self.make_gl_entries() self.repost_future_sle_and_gle() diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json index fc4ae6a5fab..27693d2f1bf 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json @@ -19,11 +19,14 @@ "allow_zero_valuation_rate", "serial_no_and_batch_section", "add_serial_batch_bundle", - "serial_and_batch_bundle", - "batch_no", + "use_serial_batch_fields", "column_break_11", + "serial_and_batch_bundle", "current_serial_and_batch_bundle", + "section_break_lypk", "serial_no", + "column_break_eefq", + "batch_no", "section_break_3", "current_qty", "current_amount", @@ -103,10 +106,10 @@ "label": "Serial No and Batch" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "serial_no", "fieldtype": "Long Text", - "label": "Serial No", - "read_only": 1 + "label": "Serial No" }, { "fieldname": "column_break_11", @@ -171,11 +174,11 @@ "read_only": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", "options": "Batch", - "read_only": 1, "search_index": 1 }, { @@ -195,6 +198,7 @@ "read_only": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial / Batch Bundle", @@ -204,6 +208,7 @@ "search_index": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0", "fieldname": "current_serial_and_batch_bundle", "fieldtype": "Link", "label": "Current Serial / Batch Bundle", @@ -212,6 +217,7 @@ "read_only": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0", "fieldname": "add_serial_batch_bundle", "fieldtype": "Button", "label": "Add Serial / Batch No" @@ -222,11 +228,26 @@ "fieldtype": "Link", "label": "Item Group", "options": "Item Group" + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial No / Batch Fields" + }, + { + "depends_on": "eval:doc.use_serial_batch_fields === 1", + "fieldname": "section_break_lypk", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_eefq", + "fieldtype": "Column Break" } ], "istable": 1, "links": [], - "modified": "2024-01-14 10:04:23.599951", + "modified": "2024-02-04 16:19:44.576022", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation Item", diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.py b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.py index c82cdf58de1..1938fec32b0 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.py +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.py @@ -26,6 +26,7 @@ class StockReconciliationItem(Document): current_valuation_rate: DF.Currency has_item_scanned: DF.Data | None item_code: DF.Link + item_group: DF.Link | None item_name: DF.Data | None parent: DF.Data parentfield: DF.Data @@ -34,6 +35,7 @@ class StockReconciliationItem(Document): quantity_difference: DF.ReadOnly | None serial_and_batch_bundle: DF.Link | None serial_no: DF.LongText | None + use_serial_batch_fields: DF.Check valuation_rate: DF.Currency warehouse: DF.Link # end: auto-generated types diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 40fac4113d6..32ef46915b2 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -50,6 +50,7 @@ "disable_serial_no_and_batch_selector", "use_naming_series", "naming_series_prefix", + "use_serial_batch_fields", "stock_planning_tab", "auto_material_request", "auto_indent", @@ -420,6 +421,12 @@ "fieldname": "auto_reserve_stock_for_sales_order_on_purchase", "fieldtype": "Check", "label": "Auto Reserve Stock for Sales Order on Purchase" + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial / Batch Fields" } ], "icon": "icon-cog", @@ -427,7 +434,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-01-30 14:03:52.143457", + "modified": "2024-02-04 12:01:31.931864", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 088c7cdfe1c..c4960aa67a8 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -57,6 +57,7 @@ class StockSettings(Document): stock_uom: DF.Link | None update_existing_price_list_rate: DF.Check use_naming_series: DF.Check + use_serial_batch_fields: DF.Check valuation_method: DF.Literal["FIFO", "Moving Average", "LIFO"] # end: auto-generated types @@ -68,6 +69,7 @@ class StockSettings(Document): "allow_negative_stock", "default_warehouse", "set_qty_in_transactions_based_on_serial_no_input", + "use_serial_batch_fields", ]: frappe.db.set_default(key, self.get(key, "")) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 78df755d747..d8b5b34d449 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -794,6 +794,9 @@ class SerialBatchCreation: setattr(self, "actual_qty", qty) self.__dict__["actual_qty"] = self.actual_qty + if not hasattr(self, "use_serial_batch_fields"): + setattr(self, "use_serial_batch_fields", 0) + def duplicate_package(self): if not self.serial_and_batch_bundle: return @@ -902,9 +905,14 @@ class SerialBatchCreation: self.batches = get_available_batches(kwargs) def set_auto_serial_batch_entries_for_inward(self): + print(self.get("serial_nos")) + if (self.get("batches") and self.has_batch_no) or ( self.get("serial_nos") and self.has_serial_no ): + if self.use_serial_batch_fields and self.get("serial_nos"): + self.make_serial_no_if_not_exists() + return self.batch_no = None @@ -916,6 +924,59 @@ class SerialBatchCreation: else: self.batches = frappe._dict({self.batch_no: abs(self.actual_qty)}) + def make_serial_no_if_not_exists(self): + non_exists_serial_nos = [] + for row in self.serial_nos: + if not frappe.db.exists("Serial No", row): + non_exists_serial_nos.append(row) + + if non_exists_serial_nos: + self.make_serial_nos(non_exists_serial_nos) + + def make_serial_nos(self, serial_nos): + serial_nos_details = [] + batch_no = None + if self.batches: + batch_no = list(self.batches.keys())[0] + + for serial_no in serial_nos: + serial_nos_details.append( + ( + serial_no, + serial_no, + now(), + now(), + frappe.session.user, + frappe.session.user, + self.warehouse, + self.company, + self.item_code, + self.item_name, + self.description, + "Active", + batch_no, + ) + ) + + if serial_nos_details: + fields = [ + "name", + "serial_no", + "creation", + "modified", + "owner", + "modified_by", + "warehouse", + "company", + "item_code", + "item_name", + "description", + "status", + "batch_no", + ] + + frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details)) + def set_serial_batch_entries(self, doc): if self.get("serial_nos"): serial_no_wise_batch = frappe._dict({}) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 475b6030780..8d82709e75f 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -149,6 +149,7 @@ class SubcontractingReceipt(SubcontractingController): self.update_prevdoc_status() self.set_subcontracting_order_status() self.set_consumed_qty_in_subcontract_order() + self.make_bundle_using_old_serial_batch_fields() self.update_stock_ledger() self.make_gl_entries() self.repost_future_sle_and_gle() diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json index 9bfc2fdb7a1..9d363599443 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -48,11 +48,14 @@ "reference_name", "section_break_45", "serial_and_batch_bundle", - "serial_no", + "use_serial_batch_fields", "col_break5", "rejected_serial_and_batch_bundle", - "batch_no", + "section_break_jshh", + "serial_no", "rejected_serial_no", + "column_break_henr", + "batch_no", "manufacture_details", "manufacturer", "column_break_16", @@ -311,22 +314,20 @@ "label": "Serial and Batch Details" }, { - "depends_on": "eval:!doc.is_fixed_asset", + "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1", "fieldname": "serial_no", "fieldtype": "Small Text", "label": "Serial No", - "no_copy": 1, - "read_only": 1 + "no_copy": 1 }, { - "depends_on": "eval:!doc.is_fixed_asset", + "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1", "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", "no_copy": 1, "options": "Batch", - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "depends_on": "eval: !parent.is_return", @@ -478,6 +479,7 @@ "label": "Accounting Details" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", @@ -486,6 +488,7 @@ "print_hide": 1 }, { + "depends_on": "eval:doc.use_serial_batch_fields === 0", "fieldname": "rejected_serial_and_batch_bundle", "fieldtype": "Link", "label": "Rejected Serial and Batch Bundle", @@ -546,12 +549,27 @@ "fieldtype": "Check", "label": "Include Exploded Items", "print_hide": 1 + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial No / Batch Fields" + }, + { + "depends_on": "eval:doc.use_serial_batch_fields === 1", + "fieldname": "section_break_jshh", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_henr", + "fieldtype": "Column Break" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-11-30 12:05:51.920705", + "modified": "2024-02-04 16:23:30.374865", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Item", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py index d02160ece45..1a4ce5b977a 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py @@ -58,6 +58,7 @@ class SubcontractingReceiptItem(Document): subcontracting_order: DF.Link | None subcontracting_order_item: DF.Data | None subcontracting_receipt_item: DF.Data | None + use_serial_batch_fields: DF.Check warehouse: DF.Link | None # end: auto-generated types diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json index 90bcf4e544e..1c8e9dd227b 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json @@ -26,10 +26,13 @@ "current_stock", "secbreak_3", "serial_and_batch_bundle", - "batch_no", + "use_serial_batch_fields", "col_break4", + "subcontracting_order", + "section_break_zwnh", "serial_no", - "subcontracting_order" + "column_break_qibi", + "batch_no" ], "fields": [ { @@ -60,19 +63,19 @@ "width": "300px" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", "no_copy": 1, - "options": "Batch", - "read_only": 1 + "options": "Batch" }, { + "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "serial_no", "fieldtype": "Text", "label": "Serial No", - "no_copy": 1, - "read_only": 1 + "no_copy": 1 }, { "fieldname": "col_break1", @@ -198,6 +201,7 @@ }, { "columns": 2, + "depends_on": "eval:doc.use_serial_batch_fields === 0", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "in_list_view": 1, @@ -205,12 +209,27 @@ "no_copy": 1, "options": "Serial and Batch Bundle", "print_hide": 1 + }, + { + "default": "0", + "fieldname": "use_serial_batch_fields", + "fieldtype": "Check", + "label": "Use Serial No / Batch Fields" + }, + { + "depends_on": "eval:doc.use_serial_batch_fields === 1", + "fieldname": "section_break_zwnh", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_qibi", + "fieldtype": "Column Break" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-03-15 13:55:08.132626", + "modified": "2024-02-04 16:32:17.534162", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Supplied Item", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py index 2ee55518d52..8f09197aa83 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py @@ -35,6 +35,7 @@ class SubcontractingReceiptSuppliedItem(Document): serial_no: DF.Text | None stock_uom: DF.Link | None subcontracting_order: DF.Link | None + use_serial_batch_fields: DF.Check # end: auto-generated types pass From a08b97e88654389966a6c00e201ced19326bf961 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 5 Feb 2024 12:40:26 +0530 Subject: [PATCH 37/41] test: fixed test cases (cherry picked from commit c1e869f040fd4c6ec2e234db71c3f80be50e4524) --- .../asset_capitalization_stock_item.json | 2 +- erpnext/controllers/stock_controller.py | 27 +++++- erpnext/public/js/controllers/transaction.js | 4 +- .../delivery_note/test_delivery_note.py | 5 +- .../doctype/packed_item/packed_item.json | 4 +- erpnext/stock/doctype/pick_list/pick_list.py | 44 +++++----- .../stock/doctype/pick_list/test_pick_list.py | 41 ++++----- .../pick_list_item/pick_list_item.json | 4 +- .../test_serial_and_batch_bundle.py | 6 ++ .../doctype/stock_entry/stock_entry_utils.py | 5 ++ .../doctype/stock_entry/test_stock_entry.py | 83 ++----------------- .../stock_entry_detail.json | 2 +- .../test_stock_ledger_entry.py | 4 + .../stock_reconciliation_item.json | 6 +- .../subcontracting_receipt_item.json | 4 +- .../subcontracting_receipt_supplied_item.json | 2 +- 16 files changed, 100 insertions(+), 143 deletions(-) diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json index d301454be88..8eda441781f 100644 --- a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json +++ b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json @@ -151,7 +151,7 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval:doc.use_serial_batch_fields === 0", + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 626d341443f..fb67f14ba01 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -130,7 +130,15 @@ class StockController(AccountsController): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.serial_batch_bundle import SerialBatchCreation - for row in self.items: + # To handle test cases + if frappe.flags.in_test and frappe.flags.use_serial_and_batch_fields: + return + + table_name = "items" + if self.doctype == "Asset Capitalization": + table_name = "stock_items" + + for row in self.get(table_name): if not row.serial_no and not row.batch_no and not row.get("rejected_serial_no"): continue @@ -140,7 +148,7 @@ class StockController(AccountsController): frappe.throw(_("Please enable Use Old Serial / Batch Fields to make_bundle")) if row.use_serial_batch_fields and ( - not row.serial_and_batch_bundle or not row.get("rejected_serial_and_batch_bundle") + not row.serial_and_batch_bundle and not row.get("rejected_serial_and_batch_bundle") ): sn_doc = SerialBatchCreation( { @@ -164,10 +172,21 @@ class StockController(AccountsController): if sn_doc.is_rejected: row.rejected_serial_and_batch_bundle = sn_doc.name - row.db_set("rejected_serial_and_batch_bundle", sn_doc.name) + row.db_set( + { + "rejected_serial_and_batch_bundle": sn_doc.name, + "rejected_serial_no": "", + } + ) else: row.serial_and_batch_bundle = sn_doc.name - row.db_set("serial_and_batch_bundle", sn_doc.name) + row.db_set( + { + "serial_and_batch_bundle": sn_doc.name, + "serial_no": "", + "batch_no": "", + } + ) def set_use_serial_batch_fields(self): if frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields"): diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 846c071c432..0241afcf030 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -732,10 +732,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe item.serial_no = item.serial_no.replace(/,/g, '\n'); item.conversion_factor = item.conversion_factor || 1; refresh_field("serial_no", item.name, item.parentfield); - if (!doc.is_return && cint(frappe.user_defaults.set_qty_in_transactions_based_on_serial_no_input)) { + if (!doc.is_return) { setTimeout(() => { me.update_qty(cdt, cdn); - }, 10000); + }, 3000); } } } diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index dae42895edb..4d15520013a 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -189,7 +189,6 @@ class TestDeliveryNote(FrappeTestCase): }, ) - frappe.flags.ignore_serial_batch_bundle_validation = True serial_nos = [ "OSN-1", "OSN-2", @@ -228,6 +227,8 @@ class TestDeliveryNote(FrappeTestCase): ) se_doc.items[0].serial_no = "\n".join(serial_nos) + + frappe.flags.use_serial_and_batch_fields = True se_doc.submit() self.assertEqual(sorted(get_serial_nos(se_doc.items[0].serial_no)), sorted(serial_nos)) @@ -283,6 +284,8 @@ class TestDeliveryNote(FrappeTestCase): self.assertTrue(serial_no in serial_nos) self.assertFalse(serial_no in returned_serial_nos1) + frappe.flags.use_serial_and_batch_fields = False + def test_sales_return_for_non_bundled_items_partial(self): company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index 0b006ab363d..1daf6791d40 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -261,7 +261,7 @@ "read_only": 1 }, { - "depends_on": "eval:doc.use_serial_batch_fields === 0", + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", @@ -270,7 +270,7 @@ "print_hide": 1 }, { - "depends_on": "eval:doc.use_serial_batch_fields === 0", + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "pick_serial_and_batch", "fieldtype": "Button", "label": "Pick Serial / Batch No" diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index f2edeea56d1..e2edb20510c 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -147,12 +147,11 @@ class PickList(Document): "voucher_no": self.name, "voucher_detail_no": row.name, "qty": row.stock_qty, - "type_of_transaction": "Inward" if row.stock_qty > 0 else "Outward", + "type_of_transaction": "Outward", "company": self.company, "serial_nos": get_serial_nos(row.serial_no) if row.serial_no else None, "batches": frappe._dict({row.batch_no: row.stock_qty}) if row.batch_no else None, "batch_no": row.batch_no, - "use_serial_batch_fields": row.use_serial_batch_fields, } ).make_serial_and_batch_bundle() @@ -188,6 +187,7 @@ class PickList(Document): {"is_cancelled": 1, "voucher_no": ""}, ) + frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle).cancel() row.db_set("serial_and_batch_bundle", None) def on_update(self): @@ -349,18 +349,13 @@ class PickList(Document): self.item_location_map = frappe._dict() from_warehouses = None - if self.parent_warehouse and frappe.get_cached_value( - "Warehouse", self.parent_warehouse, "is_group" - ): + if self.parent_warehouse: from_warehouses = get_descendants_of("Warehouse", self.parent_warehouse) - elif self.parent_warehouse: - from_warehouses = [self.parent_warehouse] # Create replica before resetting, to handle empty table on update after submit. locations_replica = self.get("locations") # reset - self.remove_serial_and_batch_bundle() self.delete_key("locations") updated_locations = frappe._dict() for item_doc in items: @@ -387,13 +382,13 @@ class PickList(Document): for row in locations: location = item_doc.as_dict() location.update(row) - bundle = location.serial_and_batch_bundle or location.serial_no or location.batch_no key = ( location.item_code, location.warehouse, location.uom, + location.batch_no, + location.serial_no, location.sales_order_item or location.material_request_item, - bundle, ) if key not in updated_locations: @@ -675,13 +670,17 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus) if not stock_qty: break + serial_nos = None + if item_location.serial_nos: + serial_nos = "\n".join(item_location.serial_nos[0 : cint(stock_qty)]) + locations.append( frappe._dict( { "qty": qty, "stock_qty": stock_qty, "warehouse": item_location.warehouse, - "serial_no": item_location.serial_no, + "serial_no": serial_nos, "batch_no": item_location.batch_no, "use_serial_batch_fields": 1, } @@ -711,7 +710,6 @@ def get_available_item_locations( company, ignore_validation=False, picked_item_details=None, - consider_rejected_warehouses=False, ): locations = [] total_picked_qty = ( @@ -727,7 +725,6 @@ def get_available_item_locations( required_qty, company, total_picked_qty, - consider_rejected_warehouses=consider_rejected_warehouses, ) elif has_serial_no: locations = get_available_item_locations_for_serialized_item( @@ -778,7 +775,6 @@ def get_available_item_locations_for_serial_and_batched_item( required_qty, company, total_picked_qty=0, - consider_rejected_warehouses=False, ): # Get batch nos by FIFO locations = get_available_item_locations_for_batched_item( @@ -786,7 +782,6 @@ def get_available_item_locations_for_serial_and_batched_item( from_warehouses, required_qty, company, - consider_rejected_warehouses=consider_rejected_warehouses, ) if locations: @@ -804,12 +799,12 @@ def get_available_item_locations_for_serial_and_batched_item( .where( (conditions) & (sn.batch_no == location.batch_no) & (sn.warehouse == location.warehouse) ) - .orderby(sn.purchase_date) + .orderby(sn.creation) .limit(ceil(location.qty + total_picked_qty)) ).run(as_dict=True) serial_nos = [sn.name for sn in serial_nos] - location.serial_no = serial_nos + location.serial_nos = serial_nos location.qty = len(serial_nos) return locations @@ -848,6 +843,7 @@ def get_available_item_locations_for_serialized_item( picked_qty -= 1 locations = [] + for warehouse, serial_nos in warehouse_serial_nos_map.items(): qty = len(serial_nos) @@ -888,12 +884,14 @@ def get_available_item_locations_for_batched_item( for warehouse, batches in warehouse_wise_batches.items(): for batch_no, qty in batches.items(): locations.append( - { - "qty": qty, - "warehouse": warehouse, - "item_code": item_code, - "batch_no": batch_no, - } + frappe._dict( + { + "qty": qty, + "warehouse": warehouse, + "item_code": item_code, + "batch_no": batch_no, + } + ) ) return locations diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 322b0b46baa..cffd0d2820f 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -217,6 +217,8 @@ class TestPickList(FrappeTestCase): ) pick_list.save() + pick_list.submit() + self.assertEqual(pick_list.locations[0].item_code, "_Test Serialized Item") self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse - _TC") self.assertEqual(pick_list.locations[0].qty, 5) @@ -239,7 +241,7 @@ class TestPickList(FrappeTestCase): pr1 = make_purchase_receipt(item_code="Batched Item", qty=1, rate=100.0) pr1.load_from_db() - oldest_batch_no = pr1.items[0].batch_no + oldest_batch_no = get_batch_from_bundle(pr1.items[0].serial_and_batch_bundle) pr2 = make_purchase_receipt(item_code="Batched Item", qty=2, rate=100.0) @@ -302,6 +304,8 @@ class TestPickList(FrappeTestCase): } ) pick_list.set_item_locations() + pick_list.submit() + pick_list.reload() self.assertEqual( get_batch_from_bundle(pick_list.locations[0].serial_and_batch_bundle), oldest_batch_no @@ -310,6 +314,7 @@ class TestPickList(FrappeTestCase): get_serial_nos_from_bundle(pick_list.locations[0].serial_and_batch_bundle), oldest_serial_nos ) + pick_list.cancel() pr1.cancel() pr2.cancel() @@ -671,29 +676,22 @@ class TestPickList(FrappeTestCase): so = make_sales_order(item_code=item, qty=25.0, rate=100) pl = create_pick_list(so.name) + pl.submit() # pick half the qty for loc in pl.locations: self.assertEqual(loc.qty, 25.0) self.assertTrue(loc.serial_and_batch_bundle) - data = frappe.get_all( - "Serial and Batch Entry", - fields=["qty", "batch_no"], - filters={"parent": loc.serial_and_batch_bundle}, - ) - - for d in data: - self.assertEqual(d.batch_no, "PICKLT-000001") - self.assertEqual(d.qty, 25.0 * -1) - pl.save() pl.submit() so1 = make_sales_order(item_code=item, qty=10.0, rate=100) - pl = create_pick_list(so1.name) + pl1 = create_pick_list(so1.name) + pl1.submit() + # pick half the qty - for loc in pl.locations: - self.assertEqual(loc.qty, 10.0) + for loc in pl1.locations: + self.assertEqual(loc.qty, 5.0) self.assertTrue(loc.serial_and_batch_bundle) data = frappe.get_all( @@ -709,8 +707,7 @@ class TestPickList(FrappeTestCase): elif d.batch_no == "PICKLT-000002": self.assertEqual(d.qty, 5.0 * -1) - pl.save() - pl.submit() + pl1.cancel() pl.cancel() def test_picklist_for_serial_item(self): @@ -723,6 +720,7 @@ class TestPickList(FrappeTestCase): so = make_sales_order(item_code=item, qty=25.0, rate=100) pl = create_pick_list(so.name) + pl.submit() picked_serial_nos = [] # pick half the qty for loc in pl.locations: @@ -736,13 +734,11 @@ class TestPickList(FrappeTestCase): picked_serial_nos = [d.serial_no for d in data] self.assertEqual(len(picked_serial_nos), 25) - pl.save() - pl.submit() - so1 = make_sales_order(item_code=item, qty=10.0, rate=100) - pl = create_pick_list(so1.name) + pl1 = create_pick_list(so1.name) + pl1.submit() # pick half the qty - for loc in pl.locations: + for loc in pl1.locations: self.assertEqual(loc.qty, 10.0) self.assertTrue(loc.serial_and_batch_bundle) @@ -756,8 +752,7 @@ class TestPickList(FrappeTestCase): for d in data: self.assertTrue(d.serial_no not in picked_serial_nos) - pl.save() - pl.submit() + pl1.cancel() pl.cancel() def test_picklist_with_bundles(self): diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json index c8001fd508d..962fa9f09de 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json @@ -196,7 +196,7 @@ "read_only": 1 }, { - "depends_on": "eval:doc.use_serial_batch_fields === 0", + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", @@ -206,7 +206,7 @@ "search_index": 1 }, { - "depends_on": "eval:doc.use_serial_batch_fields === 0", + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "pick_serial_and_batch", "fieldtype": "Button", "label": "Pick Serial / Batch No" diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index 0d453fb8418..f4309437086 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -136,6 +136,7 @@ class TestSerialandBatchBundle(FrappeTestCase): def test_old_batch_valuation(self): frappe.flags.ignore_serial_batch_bundle_validation = True + frappe.flags.use_serial_and_batch_fields = True batch_item_code = "Old Batch Item Valuation 1" make_item( batch_item_code, @@ -240,6 +241,7 @@ class TestSerialandBatchBundle(FrappeTestCase): bundle_doc.submit() frappe.flags.ignore_serial_batch_bundle_validation = False + frappe.flags.use_serial_and_batch_fields = False def test_old_serial_no_valuation(self): from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt @@ -259,6 +261,7 @@ class TestSerialandBatchBundle(FrappeTestCase): ) frappe.flags.ignore_serial_batch_bundle_validation = True + frappe.flags.use_serial_and_batch_fields = True serial_no_id = "Old Serial No 1" if not frappe.db.exists("Serial No", serial_no_id): @@ -320,6 +323,9 @@ class TestSerialandBatchBundle(FrappeTestCase): for row in bundle_doc.entries: self.assertEqual(flt(row.stock_value_difference, 2), -100.00) + frappe.flags.ignore_serial_batch_bundle_validation = False + frappe.flags.use_serial_and_batch_fields = False + def test_batch_not_belong_to_serial_no(self): from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py index 83bfaa0094c..0f67e47ad9a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py @@ -92,6 +92,9 @@ def make_stock_entry(**args): else: args.qty = cint(args.qty) + if args.serial_no or args.batch_no: + args.use_serial_batch_fields = True + # purpose if not args.purpose: if args.source and args.target: @@ -162,6 +165,7 @@ def make_stock_entry(**args): ) args.serial_no = serial_number + s.append( "items", { @@ -177,6 +181,7 @@ def make_stock_entry(**args): "batch_no": args.batch_no, "cost_center": args.cost_center, "expense_account": args.expense_account, + "use_serial_batch_fields": args.use_serial_batch_fields, }, ) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 4e3214ebeca..7ef2a0d5a0d 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -671,6 +671,7 @@ class TestStockEntry(FrappeTestCase): def test_serial_move(self): se = make_serialized_item() serial_no = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)[0] + frappe.flags.use_serial_and_batch_fields = True se = frappe.copy_doc(test_records[0]) se.purpose = "Material Transfer" @@ -691,6 +692,7 @@ class TestStockEntry(FrappeTestCase): self.assertTrue( frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse - _TC" ) + frappe.flags.use_serial_and_batch_fields = False def test_serial_cancel(self): se, serial_nos = self.test_serial_by_series() @@ -990,6 +992,8 @@ class TestStockEntry(FrappeTestCase): do_not_save=True, ) + frappe.flags.use_serial_and_batch_fields = True + cls_obj = SerialBatchCreation( { "type_of_transaction": "Inward", @@ -1026,84 +1030,7 @@ class TestStockEntry(FrappeTestCase): s2.submit() s2.cancel() - - # def test_retain_sample(self): - # from erpnext.stock.doctype.batch.batch import get_batch_qty - # from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse - - # create_warehouse("Test Warehouse for Sample Retention") - # frappe.db.set_value( - # "Stock Settings", - # None, - # "sample_retention_warehouse", - # "Test Warehouse for Sample Retention - _TC", - # ) - - # test_item_code = "Retain Sample Item" - # if not frappe.db.exists("Item", test_item_code): - # item = frappe.new_doc("Item") - # item.item_code = test_item_code - # item.item_name = "Retain Sample Item" - # item.description = "Retain Sample Item" - # item.item_group = "All Item Groups" - # item.is_stock_item = 1 - # item.has_batch_no = 1 - # item.create_new_batch = 1 - # item.retain_sample = 1 - # item.sample_quantity = 4 - # item.save() - - # receipt_entry = frappe.new_doc("Stock Entry") - # receipt_entry.company = "_Test Company" - # receipt_entry.purpose = "Material Receipt" - # receipt_entry.append( - # "items", - # { - # "item_code": test_item_code, - # "t_warehouse": "_Test Warehouse - _TC", - # "qty": 40, - # "basic_rate": 12, - # "cost_center": "_Test Cost Center - _TC", - # "sample_quantity": 4, - # }, - # ) - # receipt_entry.set_stock_entry_type() - # receipt_entry.insert() - # receipt_entry.submit() - - # retention_data = move_sample_to_retention_warehouse( - # receipt_entry.company, receipt_entry.get("items") - # ) - # retention_entry = frappe.new_doc("Stock Entry") - # retention_entry.company = retention_data.company - # retention_entry.purpose = retention_data.purpose - # retention_entry.append( - # "items", - # { - # "item_code": test_item_code, - # "t_warehouse": "Test Warehouse for Sample Retention - _TC", - # "s_warehouse": "_Test Warehouse - _TC", - # "qty": 4, - # "basic_rate": 12, - # "cost_center": "_Test Cost Center - _TC", - # "batch_no": get_batch_from_bundle(receipt_entry.get("items")[0].serial_and_batch_bundle), - # }, - # ) - # retention_entry.set_stock_entry_type() - # retention_entry.insert() - # retention_entry.submit() - - # qty_in_usable_warehouse = get_batch_qty( - # get_batch_from_bundle(receipt_entry.get("items")[0].serial_and_batch_bundle), "_Test Warehouse - _TC", "_Test Item" - # ) - # qty_in_retention_warehouse = get_batch_qty( - # get_batch_from_bundle(receipt_entry.get("items")[0].serial_and_batch_bundle), - # "Test Warehouse for Sample Retention - _TC", - # "_Test Item", - # ) - - # self.assertEqual(qty_in_usable_warehouse, 36) - # self.assertEqual(qty_in_retention_warehouse, 4) + frappe.flags.use_serial_and_batch_fields = False def test_quality_check(self): item_code = "_Test Item For QC" diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index bd11d0b861d..c7b3daab82a 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -582,7 +582,7 @@ "label": "Add Serial / Batch No" }, { - "depends_on": "eval:doc.use_serial_batch_fields === 0", + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index d8a3f2e33c1..c0999532d03 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -482,6 +482,8 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): (item, warehouses[0], batches[1], 1, 200), (item, warehouses[0], batches[0], 1, 200), ] + + frappe.flags.use_serial_and_batch_fields = True dns = create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list) sle_details = fetch_sle_details_for_doc_list(dns, ["stock_value_difference"]) svd_list = [-1 * d["stock_value_difference"] for d in sle_details] @@ -494,6 +496,8 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): "Incorrect 'Incoming Rate' values fetched for DN items", ) + frappe.flags.use_serial_and_batch_fields = False + def test_batchwise_item_valuation_stock_reco(self): item, warehouses, batches = setup_item_valuation_test() state = {"stock_value": 0.0, "qty": 0.0} diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json index 27693d2f1bf..734225972c7 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json @@ -198,7 +198,7 @@ "read_only": 1 }, { - "depends_on": "eval:doc.use_serial_batch_fields === 0", + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial / Batch Bundle", @@ -208,7 +208,7 @@ "search_index": 1 }, { - "depends_on": "eval:doc.use_serial_batch_fields === 0", + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "current_serial_and_batch_bundle", "fieldtype": "Link", "label": "Current Serial / Batch Bundle", @@ -217,7 +217,7 @@ "read_only": 1 }, { - "depends_on": "eval:doc.use_serial_batch_fields === 0", + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "add_serial_batch_bundle", "fieldtype": "Button", "label": "Add Serial / Batch No" diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json index 9d363599443..f9e0a0b591c 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -479,7 +479,7 @@ "label": "Accounting Details" }, { - "depends_on": "eval:doc.use_serial_batch_fields === 0", + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", @@ -488,7 +488,7 @@ "print_hide": 1 }, { - "depends_on": "eval:doc.use_serial_batch_fields === 0", + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "rejected_serial_and_batch_bundle", "fieldtype": "Link", "label": "Rejected Serial and Batch Bundle", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json index 1c8e9dd227b..957b6a2a654 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json @@ -201,7 +201,7 @@ }, { "columns": 2, - "depends_on": "eval:doc.use_serial_batch_fields === 0", + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "in_list_view": 1, From 73618f06054fa0ba14a21f77653ced39f822a414 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 6 Feb 2024 13:31:36 +0530 Subject: [PATCH 38/41] test: test case to check use serial / batch fields feature (cherry picked from commit 01650120d40babc0992a0673498b1eda689fd615) --- erpnext/controllers/stock_controller.py | 17 +++- .../delivery_note/test_delivery_note.py | 5 +- .../purchase_receipt/test_purchase_receipt.py | 92 ++++++++++++++++++- .../serial_and_batch_bundle.py | 6 +- erpnext/stock/doctype/serial_no/serial_no.py | 4 +- .../stock_reconciliation.js | 1 + .../stock_reconciliation.py | 74 ++++++++++++++- .../test_stock_reconciliation.py | 5 +- .../stock_settings/stock_settings.json | 2 +- erpnext/stock/utils.py | 51 ++++------ 10 files changed, 208 insertions(+), 49 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index fb67f14ba01..ba3cdc8e833 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -21,6 +21,9 @@ from erpnext.stock import get_warehouse_account_map from erpnext.stock.doctype.inventory_dimension.inventory_dimension import ( get_evaluated_inventory_dimension, ) +from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + get_type_of_transaction, +) from erpnext.stock.stock_ledger import get_items_to_be_repost @@ -150,6 +153,13 @@ class StockController(AccountsController): if row.use_serial_batch_fields and ( not row.serial_and_batch_bundle and not row.get("rejected_serial_and_batch_bundle") ): + if self.doctype == "Stock Reconciliation": + qty = row.qty + type_of_transaction = "Inward" + else: + qty = row.stock_qty + type_of_transaction = get_type_of_transaction(self, row) + sn_doc = SerialBatchCreation( { "item_code": row.item_code, @@ -159,14 +169,15 @@ class StockController(AccountsController): "voucher_type": self.doctype, "voucher_no": self.name, "voucher_detail_no": row.name, - "qty": row.stock_qty, - "type_of_transaction": "Inward" if row.stock_qty > 0 else "Outward", + "qty": qty, + "type_of_transaction": type_of_transaction, "company": self.company, "is_rejected": 1 if row.get("rejected_warehouse") else 0, "serial_nos": get_serial_nos(row.serial_no) if row.serial_no else None, - "batches": frappe._dict({row.batch_no: row.stock_qty}) if row.batch_no else None, + "batches": frappe._dict({row.batch_no: qty}) if row.batch_no else None, "batch_no": row.batch_no, "use_serial_batch_fields": row.use_serial_batch_fields, + "do_not_submit": True, } ).make_serial_and_batch_bundle() diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 4d15520013a..7889f95c605 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1555,7 +1555,7 @@ def create_delivery_note(**args): dn.return_against = args.return_against bundle_id = None - if args.get("batch_no") or args.get("serial_no"): + if not args.use_serial_batch_fields and (args.get("batch_no") or args.get("serial_no")): type_of_transaction = args.type_of_transaction or "Outward" if dn.is_return: @@ -1597,6 +1597,9 @@ def create_delivery_note(**args): "expense_account": args.expense_account or "Cost of Goods Sold - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC", "target_warehouse": args.target_warehouse, + "use_serial_batch_fields": args.use_serial_batch_fields, + "serial_no": args.serial_no if args.use_serial_batch_fields else None, + "batch_no": args.batch_no if args.use_serial_batch_fields else None, }, ) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index dd49eabeaf8..ff0300f9e96 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2193,6 +2193,93 @@ class TestPurchaseReceipt(FrappeTestCase): pr_doc.reload() self.assertFalse(pr_doc.items[0].from_warehouse) + def test_use_serial_batch_fields_for_serial_nos(self): + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + create_stock_reconciliation, + ) + + item_code = make_item( + "_Test Use Serial Fields Item Serial Item", + properties={"has_serial_no": 1, "serial_no_series": "SNU-TSFISI-.#####"}, + ).name + + serial_nos = [ + "SNU-TSFISI-000011", + "SNU-TSFISI-000012", + "SNU-TSFISI-000013", + "SNU-TSFISI-000014", + "SNU-TSFISI-000015", + ] + + pr = make_purchase_receipt( + item_code=item_code, + qty=5, + serial_no="\n".join(serial_nos), + use_serial_batch_fields=1, + rate=100, + ) + + self.assertEqual(pr.items[0].use_serial_batch_fields, 1) + self.assertFalse(pr.items[0].serial_no) + self.assertTrue(pr.items[0].serial_and_batch_bundle) + + sbb_doc = frappe.get_doc("Serial and Batch Bundle", pr.items[0].serial_and_batch_bundle) + + for row in sbb_doc.entries: + self.assertTrue(row.serial_no in serial_nos) + + serial_nos.remove("SNU-TSFISI-000015") + + sr = create_stock_reconciliation( + item_code=item_code, + serial_no="\n".join(serial_nos), + qty=4, + warehouse=pr.items[0].warehouse, + use_serial_batch_fields=1, + do_not_submit=True, + ) + sr.reload() + + serial_nos = get_serial_nos(sr.items[0].current_serial_no) + self.assertEqual(len(serial_nos), 5) + self.assertEqual(sr.items[0].current_qty, 5) + + new_serial_nos = get_serial_nos(sr.items[0].serial_no) + self.assertEqual(len(new_serial_nos), 4) + self.assertEqual(sr.items[0].qty, 4) + self.assertEqual(sr.items[0].use_serial_batch_fields, 1) + self.assertFalse(sr.items[0].current_serial_and_batch_bundle) + self.assertFalse(sr.items[0].serial_and_batch_bundle) + self.assertTrue(sr.items[0].current_serial_no) + sr.submit() + + sr.reload() + self.assertTrue(sr.items[0].current_serial_and_batch_bundle) + self.assertTrue(sr.items[0].serial_and_batch_bundle) + + serial_no_status = frappe.db.get_value("Serial No", "SNU-TSFISI-000015", "status") + + self.assertTrue(serial_no_status != "Active") + + dn = create_delivery_note( + item_code=item_code, + qty=4, + serial_no="\n".join(new_serial_nos), + use_serial_batch_fields=1, + ) + + self.assertTrue(dn.items[0].serial_and_batch_bundle) + self.assertEqual(dn.items[0].qty, 4) + doc = frappe.get_doc("Serial and Batch Bundle", dn.items[0].serial_and_batch_bundle) + for row in doc.entries: + self.assertTrue(row.serial_no in new_serial_nos) + + for sn in new_serial_nos: + serial_no_status = frappe.db.get_value("Serial No", sn, "status") + self.assertTrue(serial_no_status != "Active") + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier @@ -2361,7 +2448,7 @@ def make_purchase_receipt(**args): uom = args.uom or frappe.db.get_value("Item", item_code, "stock_uom") or "_Test UOM" bundle_id = None - if args.get("batch_no") or args.get("serial_no"): + if not args.use_serial_batch_fields and (args.get("batch_no") or args.get("serial_no")): batches = {} if args.get("batch_no"): batches = frappe._dict({args.batch_no: qty}) @@ -2403,6 +2490,9 @@ def make_purchase_receipt(**args): "cost_center": args.cost_center or frappe.get_cached_value("Company", pr.company, "cost_center"), "asset_location": args.location or "Test Location", + "use_serial_batch_fields": args.use_serial_batch_fields or 0, + "serial_no": args.serial_no if args.use_serial_batch_fields else "", + "batch_no": args.batch_no if args.use_serial_batch_fields else "", }, ) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index ea33c54544d..eb4df29db82 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -1117,7 +1117,7 @@ def parse_serial_nos(data): if isinstance(data, list): return data - return [s.strip() for s in cstr(data).strip().upper().replace(",", "\n").split("\n") if s.strip()] + return [s.strip() for s in cstr(data).strip().replace(",", "\n").split("\n") if s.strip()] @frappe.whitelist() @@ -1256,7 +1256,7 @@ def create_serial_batch_no_ledgers( def get_type_of_transaction(parent_doc, child_row): - type_of_transaction = child_row.type_of_transaction + type_of_transaction = child_row.get("type_of_transaction") if parent_doc.get("doctype") == "Stock Entry": type_of_transaction = "Outward" if child_row.s_warehouse else "Inward" @@ -1384,6 +1384,8 @@ def get_available_serial_nos(kwargs): filters = {"item_code": kwargs.item_code} + # ignore_warehouse is used for backdated stock transactions + # There might be chances that the serial no not exists in the warehouse during backdated stock transactions if not kwargs.get("ignore_warehouse"): filters["warehouse"] = ("is", "set") if kwargs.warehouse: diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 122664c2dde..5f4f3931a74 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -151,9 +151,7 @@ def get_serial_nos(serial_no): if isinstance(serial_no, list): return serial_no - return [ - s.strip() for s in cstr(serial_no).strip().upper().replace(",", "\n").split("\n") if s.strip() - ] + return [s.strip() for s in cstr(serial_no).strip().replace(",", "\n").split("\n") if s.strip()] def clean_serial_no_string(serial_no: str) -> str: diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 8e9dcb0fc52..ba7f9c58a8b 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -198,6 +198,7 @@ frappe.ui.form.on("Stock Reconciliation", { frappe.model.set_value(cdt, cdn, "current_amount", r.message.rate * r.message.qty); frappe.model.set_value(cdt, cdn, "amount", row.qty * row.valuation_rate); frappe.model.set_value(cdt, cdn, "current_serial_no", r.message.serial_nos); + frappe.model.set_value(cdt, cdn, "use_serial_batch_fields", r.message.use_serial_batch_fields); if (frm.doc.purpose == "Stock Reconciliation" && !frm.doc.scan_mode) { frappe.model.set_value(cdt, cdn, "serial_no", r.message.serial_nos); diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index cc8a7c57b37..ce08615ed5c 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -99,6 +99,7 @@ class StockReconciliation(StockController): ) def on_submit(self): + self.make_bundle_for_current_qty() self.make_bundle_using_old_serial_batch_fields() self.update_stock_ledger() self.make_gl_entries() @@ -117,9 +118,52 @@ class StockReconciliation(StockController): self.repost_future_sle_and_gle() self.delete_auto_created_batches() + def make_bundle_for_current_qty(self): + from erpnext.stock.serial_batch_bundle import SerialBatchCreation + + for row in self.items: + if not row.use_serial_batch_fields: + continue + + if row.current_serial_and_batch_bundle: + continue + + if row.current_qty and (row.current_serial_no or row.batch_no): + sn_doc = SerialBatchCreation( + { + "item_code": row.item_code, + "warehouse": row.warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "voucher_type": self.doctype, + "voucher_no": self.name, + "voucher_detail_no": row.name, + "qty": row.qty, + "type_of_transaction": "Outward", + "company": self.company, + "is_rejected": 0, + "serial_nos": get_serial_nos(row.current_serial_no) if row.current_serial_no else None, + "batches": frappe._dict({row.batch_no: row.qty}) if row.batch_no else None, + "batch_no": row.batch_no, + "do_not_submit": True, + } + ).make_serial_and_batch_bundle() + + row.current_serial_and_batch_bundle = sn_doc.name + row.db_set( + { + "current_serial_and_batch_bundle": sn_doc.name, + "current_serial_no": "", + "batch_no": "", + } + ) + def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False) -> None: """Set Serial and Batch Bundle for each item""" for item in self.items: + if not save and item.use_serial_batch_fields: + continue + if voucher_detail_no and voucher_detail_no != item.name: continue @@ -230,6 +274,9 @@ class StockReconciliation(StockController): def set_new_serial_and_batch_bundle(self): for item in self.items: + if item.use_serial_batch_fields: + continue + if not item.qty: continue @@ -292,8 +339,10 @@ class StockReconciliation(StockController): inventory_dimensions_dict=inventory_dimensions_dict, ) - if (item.qty is None or item.qty == item_dict.get("qty")) and ( - item.valuation_rate is None or item.valuation_rate == item_dict.get("rate") + if ( + (item.qty is None or item.qty == item_dict.get("qty")) + and (item.valuation_rate is None or item.valuation_rate == item_dict.get("rate")) + and (not item.serial_no or (item.serial_no == item_dict.get("serial_nos"))) ): return False else: @@ -304,6 +353,11 @@ class StockReconciliation(StockController): if item.valuation_rate is None: item.valuation_rate = item_dict.get("rate") + if item_dict.get("serial_nos"): + item.current_serial_no = item_dict.get("serial_nos") + if self.purpose == "Stock Reconciliation" and not item.serial_no and item.qty: + item.serial_no = item.current_serial_no + item.current_qty = item_dict.get("qty") item.current_valuation_rate = item_dict.get("rate") self.calculate_difference_amount(item, item_dict) @@ -1136,9 +1190,16 @@ def get_stock_balance_for( has_serial_no = bool(item_dict.get("has_serial_no")) has_batch_no = bool(item_dict.get("has_batch_no")) + use_serial_batch_fields = frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields") + if not batch_no and has_batch_no: # Not enough information to fetch data - return {"qty": 0, "rate": 0, "serial_nos": None} + return { + "qty": 0, + "rate": 0, + "serial_nos": None, + "use_serial_batch_fields": use_serial_batch_fields, + } # TODO: fetch only selected batch's values data = get_stock_balance( @@ -1161,7 +1222,12 @@ def get_stock_balance_for( get_batch_qty(batch_no, warehouse, posting_date=posting_date, posting_time=posting_time) or 0 ) - return {"qty": qty, "rate": rate, "serial_nos": serial_nos} + return { + "qty": qty, + "rate": rate, + "serial_nos": serial_nos, + "use_serial_batch_fields": use_serial_batch_fields, + } @frappe.whitelist() diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 0bbfed40d89..479a74af7a8 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -1094,7 +1094,7 @@ def create_stock_reconciliation(**args): ) bundle_id = None - if args.batch_no or args.serial_no: + if not args.use_serial_batch_fields and (args.batch_no or args.serial_no): batches = frappe._dict({}) if args.batch_no: batches[args.batch_no] = args.qty @@ -1125,7 +1125,10 @@ def create_stock_reconciliation(**args): "warehouse": args.warehouse or "_Test Warehouse - _TC", "qty": args.qty, "valuation_rate": args.rate, + "serial_no": args.serial_no if args.use_serial_batch_fields else None, + "batch_no": args.batch_no if args.use_serial_batch_fields else None, "serial_and_batch_bundle": bundle_id, + "use_serial_batch_fields": args.use_serial_batch_fields, }, ) diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 32ef46915b2..3f2c1142552 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -423,7 +423,7 @@ "label": "Auto Reserve Stock for Sales Order on Purchase" }, { - "default": "0", + "default": "1", "fieldname": "use_serial_batch_fields", "fieldtype": "Check", "label": "Use Serial / Batch Fields" diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 017db5d5505..54e0ab5acf8 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -11,6 +11,9 @@ from frappe.query_builder.functions import CombineDatetime, IfNull, Sum from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime import erpnext +from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + get_available_serial_nos, +) from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation from erpnext.stock.valuation import FIFOValuation, LIFOValuation @@ -125,7 +128,21 @@ def get_stock_balance( if with_valuation_rate: if with_serial_no: - serial_nos = get_serial_nos_data_after_transactions(args) + serial_no_details = get_available_serial_nos( + frappe._dict( + { + "item_code": item_code, + "warehouse": warehouse, + "posting_date": posting_date, + "posting_time": posting_time, + "ignore_warehouse": 1, + } + ) + ) + + serial_nos = "" + if serial_no_details: + serial_nos = "\n".join(d.serial_no for d in serial_no_details) return ( (last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos) @@ -140,38 +157,6 @@ def get_stock_balance( return last_entry.qty_after_transaction if last_entry else 0.0 -def get_serial_nos_data_after_transactions(args): - - serial_nos = set() - args = frappe._dict(args) - sle = frappe.qb.DocType("Stock Ledger Entry") - - stock_ledger_entries = ( - frappe.qb.from_(sle) - .select("serial_no", "actual_qty") - .where( - (sle.item_code == args.item_code) - & (sle.warehouse == args.warehouse) - & ( - CombineDatetime(sle.posting_date, sle.posting_time) - < CombineDatetime(args.posting_date, args.posting_time) - ) - & (sle.is_cancelled == 0) - ) - .orderby(sle.posting_date, sle.posting_time, sle.creation) - .run(as_dict=1) - ) - - for stock_ledger_entry in stock_ledger_entries: - changed_serial_no = get_serial_nos_data(stock_ledger_entry.serial_no) - if stock_ledger_entry.actual_qty > 0: - serial_nos.update(changed_serial_no) - else: - serial_nos.difference_update(changed_serial_no) - - return "\n".join(serial_nos) - - def get_serial_nos_data(serial_nos): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos From b582e9c42c30765f83821726976ce18120534167 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 6 Feb 2024 19:13:48 +0530 Subject: [PATCH 39/41] fix: remove file from the disk after the completion of reposting (cherry picked from commit fb330d1b5a8b2620d23c5a899c26ea240966b20c) --- .../repost_item_valuation/repost_item_valuation.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 79b8ee30cfe..4cb2fdcd4d2 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -286,6 +286,7 @@ def repost(doc): repost_gl_entries(doc) doc.set_status("Completed") + remove_attached_file(doc.name) except Exception as e: if frappe.flags.in_test: @@ -314,6 +315,13 @@ def repost(doc): frappe.db.commit() +def remove_attached_file(docname): + if file_name := frappe.db.get_value( + "File", {"attached_to_name": docname, "attached_to_doctype": "Repost Item Valuation"}, "name" + ): + frappe.delete_doc("File", file_name) + + def repost_sl_entries(doc): if doc.based_on == "Transaction": repost_future_sle( From 7833138c5799db4c0362f0501b0e799b8556076e Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 6 Feb 2024 19:22:56 +0530 Subject: [PATCH 40/41] test: test case to check removed attached file (cherry picked from commit 76b57a433886c3a38b9c4883db8652aabe8d32d8) --- .../repost_item_valuation.py | 2 +- .../test_repost_item_valuation.py | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 4cb2fdcd4d2..2715324ad0b 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -319,7 +319,7 @@ def remove_attached_file(docname): if file_name := frappe.db.get_value( "File", {"attached_to_name": docname, "attached_to_doctype": "Repost Item Valuation"}, "name" ): - frappe.delete_doc("File", file_name) + frappe.delete_doc("File", file_name, delete_permanently=True) def repost_sl_entries(doc): diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py index aa90ff03a82..ecf9d431342 100644 --- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -424,3 +424,38 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin): self.assertRaises(frappe.ValidationError, riv.save) doc.cancel() + + def test_remove_attached_file(self): + item_code = make_item("_Test Remove Attached File Item", properties={"is_stock_item": 1}) + + make_purchase_receipt( + item_code=item_code, + qty=1, + rate=100, + ) + + pr1 = make_purchase_receipt( + item_code=item_code, + qty=1, + rate=100, + posting_date=add_days(today(), days=-1), + ) + + if docname := frappe.db.exists("Repost Item Valuation", {"voucher_no": pr1.name}): + self.assertFalse( + frappe.db.get_value( + "File", + {"attached_to_doctype": "Repost Item Valuation", "attached_to_name": docname}, + "name", + ) + ) + else: + repost_entries = create_item_wise_repost_entries(pr1.doctype, pr1.name) + for entry in repost_entries: + self.assertFalse( + frappe.db.get_value( + "File", + {"attached_to_doctype": "Repost Item Valuation", "attached_to_name": entry.name}, + "name", + ) + ) From 02f56ee20ef32b5fef4e99b44c6d431042c60794 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 6 Feb 2024 22:48:13 +0530 Subject: [PATCH 41/41] fix: do not throw validation for cancelled sle (cherry picked from commit 32ccf3524ad973b1bc8c5c7eefa4c79f5d8b4444) --- erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 69db4f57726..75f1f2f2d98 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -93,6 +93,9 @@ class StockLedgerEntry(Document): self.validate_inventory_dimension_negative_stock() def validate_inventory_dimension_negative_stock(self): + if self.is_cancelled: + return + extra_cond = "" kwargs = {}