diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index d2d961f7c2a..b441af96600 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -46,7 +46,7 @@ class BankTransaction(StatusUpdater): def add_payment_entries(self, vouchers): "Add the vouchers with zero allocation. Save() will perform the allocations and clearance" if 0.0 >= self.unallocated_amount: - frappe.throw(frappe._(f"Bank Transaction {self.name} is already fully reconciled")) + frappe.throw(frappe._("Bank Transaction {0} is already fully reconciled").format(self.name)) added = False for voucher in vouchers: @@ -114,9 +114,7 @@ class BankTransaction(StatusUpdater): elif 0.0 > unallocated_amount: self.db_delete_payment_entry(payment_entry) - frappe.throw( - frappe._(f"Voucher {payment_entry.payment_entry} is over-allocated by {unallocated_amount}") - ) + frappe.throw(frappe._("Voucher {0} is over-allocated by {1}").format(unallocated_amount)) self.reload() @@ -178,7 +176,9 @@ def get_clearance_details(transaction, payment_entry): if gle["gl_account"] == gl_bank_account: if gle["amount"] <= 0.0: frappe.throw( - frappe._(f"Voucher {payment_entry.payment_entry} value is broken: {gle['amount']}") + frappe._("Voucher {0} value is broken: {1}").format( + payment_entry.payment_entry, gle["amount"] + ) ) unmatched_gles -= 1 diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 5c9168bf9c5..ab7884d5209 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -303,7 +303,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. apply_tds(frm) { var me = this; - + me.frm.set_value("tax_withheld_vouchers", []); if (!me.frm.doc.apply_tds) { me.frm.set_value("tax_withholding_category", ''); me.frm.set_df_property("tax_withholding_category", "hidden", 1); diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index c987231fe17..dd9c0736128 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -19,14 +19,19 @@ def execute(filters=None): return _execute(filters) -def _execute(filters=None, additional_table_columns=None, additional_query_columns=None): +def _execute( + filters=None, + additional_table_columns=None, + additional_query_columns=None, + additional_conditions=None, +): if not filters: filters = {} columns = get_columns(additional_table_columns, filters) company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency") - item_list = get_items(filters, additional_query_columns) + item_list = get_items(filters, additional_query_columns, additional_conditions) if item_list: itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency) @@ -328,7 +333,7 @@ def get_columns(additional_table_columns, filters): return columns -def get_conditions(filters): +def get_conditions(filters, additional_conditions=None): conditions = "" for opts in ( @@ -341,6 +346,9 @@ def get_conditions(filters): if filters.get(opts[0]): conditions += opts[1] + if additional_conditions: + conditions += additional_conditions + if filters.get("mode_of_payment"): conditions += """ and exists(select name from `tabSales Invoice Payment` where parent=`tabSales Invoice`.name @@ -376,8 +384,8 @@ def get_group_by_conditions(filters, doctype): return "ORDER BY `tab{0}`.{1}".format(doctype, frappe.scrub(filters.get("group_by"))) -def get_items(filters, additional_query_columns): - conditions = get_conditions(filters) +def get_items(filters, additional_query_columns, additional_conditions=None): + conditions = get_conditions(filters, additional_conditions) if additional_query_columns: additional_query_columns = ", " + ", ".join(additional_query_columns) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index d0ec6541623..78bb05671d8 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -392,6 +392,9 @@ class AccountsController(TransactionBase): ) def validate_inter_company_reference(self): + if self.get("is_return"): + return + if self.doctype not in ("Purchase Invoice", "Purchase Receipt"): return @@ -1679,6 +1682,9 @@ class AccountsController(TransactionBase): d.base_payment_amount = flt( d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount") ) + else: + self.fetch_payment_terms_from_order(po_or_so, doctype) + self.ignore_default_payment_terms_template = 1 def get_order_details(self): if self.doctype == "Sales Invoice": diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 590e00fb11e..3d47ee797da 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -171,7 +171,7 @@ class SellingController(StockController): self.round_floats_in(sales_person) sales_person.allocated_amount = flt( - self.amount_eligible_for_commission * sales_person.allocated_percentage / 100.0, + flt(self.amount_eligible_for_commission) * sales_person.allocated_percentage / 100.0, self.precision("allocated_amount", sales_person), ) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index a27e34819d4..befde71775a 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -442,7 +442,43 @@ class StockController(AccountsController): if not dimension: continue - if row.get(dimension.source_fieldname): + if self.doctype in [ + "Purchase Invoice", + "Purchase Receipt", + "Sales Invoice", + "Delivery Note", + "Stock Entry", + ]: + if ( + ( + sl_dict.actual_qty > 0 + and not self.get("is_return") + or sl_dict.actual_qty < 0 + and self.get("is_return") + ) + and self.doctype in ["Purchase Invoice", "Purchase Receipt"] + ) or ( + ( + sl_dict.actual_qty < 0 + and not self.get("is_return") + or sl_dict.actual_qty > 0 + and self.get("is_return") + ) + and self.doctype in ["Sales Invoice", "Delivery Note", "Stock Entry"] + ): + sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname) + else: + fieldname_start_with = "to" + if self.doctype in ["Purchase Invoice", "Purchase Receipt"]: + fieldname_start_with = "from" + + fieldname = f"{fieldname_start_with}_{dimension.source_fieldname}" + sl_dict[dimension.target_fieldname] = row.get(fieldname) + + if not sl_dict.get(dimension.target_fieldname): + sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname) + + elif row.get(dimension.source_fieldname): sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname) if not sl_dict.get(dimension.target_fieldname) and dimension.fetch_from_parent: @@ -734,6 +770,9 @@ class StockController(AccountsController): } ) + if self.docstatus == 2: + force = True + if force or future_sle_exists(args) or repost_required_for_queue(self): item_based_reposting = cint( frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting") diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 05754293b77..c3fa894e895 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -741,7 +741,7 @@ class SubcontractingController(StockController): sco_doc = frappe.get_doc("Subcontracting Order", sco) sco_doc.update_status() - def set_missing_values_in_additional_costs(self): + def calculate_additional_costs(self): self.total_additional_costs = sum(flt(item.amount) for item in self.get("additional_costs")) if self.total_additional_costs: diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py index 0e6fe95d45f..4ea4fd11b4e 100644 --- a/erpnext/controllers/tests/test_subcontracting_controller.py +++ b/erpnext/controllers/tests/test_subcontracting_controller.py @@ -36,7 +36,7 @@ class TestSubcontractingController(FrappeTestCase): sco.remove_empty_rows() self.assertEqual((len_before - 1), len(sco.service_items)) - def test_set_missing_values_in_additional_costs(self): + def test_calculate_additional_costs(self): sco = get_subcontracting_order(do_not_submit=1) rate_without_additional_cost = sco.items[0].rate diff --git a/erpnext/e_commerce/web_template/hero_slider/hero_slider.json b/erpnext/e_commerce/web_template/hero_slider/hero_slider.json index 2b1807c9651..39b2b3eaeb8 100644 --- a/erpnext/e_commerce/web_template/hero_slider/hero_slider.json +++ b/erpnext/e_commerce/web_template/hero_slider/hero_slider.json @@ -165,6 +165,7 @@ "fieldname": "slide_3_content_align", "fieldtype": "Select", "label": "Content Align", + "options": "Left\nCentre\nRight", "reqd": 0 }, { @@ -214,6 +215,7 @@ "fieldname": "slide_4_content_align", "fieldtype": "Select", "label": "Content Align", + "options": "Left\nCentre\nRight", "reqd": 0 }, { @@ -263,6 +265,7 @@ "fieldname": "slide_5_content_align", "fieldtype": "Select", "label": "Content Align", + "options": "Left\nCentre\nRight", "reqd": 0 }, { @@ -274,7 +277,7 @@ } ], "idx": 2, - "modified": "2021-02-24 15:57:05.889709", + "modified": "2023-05-12 15:03:57.604060", "modified_by": "Administrator", "module": "E-commerce", "name": "Hero Slider", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 877362dcba4..4a4046e47aa 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -562,6 +562,7 @@ class JobCard(Document): ) def update_work_order_data(self, for_quantity, time_in_mins, wo): + workstation_hour_rate = frappe.get_value("Workstation", self.workstation, "hour_rate") jc = frappe.qb.DocType("Job Card") jctl = frappe.qb.DocType("Job Card Time Log") @@ -587,6 +588,7 @@ class JobCard(Document): if data.get("workstation") != self.workstation: # workstations can change in a job card data.workstation = self.workstation + data.hour_rate = flt(workstation_hour_rate) wo.flags.ignore_validate_update_after_submit = True wo.update_operation_status() diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 449d461561a..e9a6cc385d3 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -264,7 +264,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex } } // payment request - if(flt(doc.per_billed)<100) { + if(flt(doc.per_billed, precision('per_billed', doc)) < 100 + frappe.boot.sysdefaults.over_billing_allowance) { this.frm.add_custom_button(__('Payment Request'), () => this.make_payment_request(), __('Create')); this.frm.add_custom_button(__('Payment'), () => this.make_payment_entry(), __('Create')); } diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index f1df3a11de4..e3de49c57d8 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -299,7 +299,8 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran } batch_no(doc, cdt, cdn) { - var me = this; + super.batch_no(doc, cdt, cdn); + var item = frappe.get_doc(cdt, cdn); if (item.serial_no) { @@ -378,10 +379,6 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran } } - batch_no(doc, cdt, cdn) { - super.batch_no(doc, cdt, cdn); - } - qty(doc, cdt, cdn) { super.qty(doc, cdt, cdn); diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 13a6f20db29..78f2e4935e7 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -281,7 +281,9 @@ def get_employee_email(employee_doc): def get_holiday_list_for_employee(employee, raise_exception=True): if employee: - holiday_list, company = frappe.db.get_value("Employee", employee, ["holiday_list", "company"]) + holiday_list, company = frappe.get_cached_value( + "Employee", employee, ["holiday_list", "company"] + ) else: holiday_list = "" company = frappe.db.get_single_value("Global Defaults", "default_company") diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.py b/erpnext/setup/doctype/holiday_list/holiday_list.py index fad827ad8ad..84d0d352871 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/holiday_list.py @@ -115,6 +115,8 @@ def is_holiday(holiday_list, date=None): if date is None: date = today() if holiday_list: - return bool(frappe.get_all("Holiday List", dict(name=holiday_list, holiday_date=date))) + return bool( + frappe.db.exists("Holiday", {"parent": holiday_list, "holiday_date": date}, cache=True) + ) else: return False diff --git a/erpnext/setup/module_onboarding/home/home.json b/erpnext/setup/module_onboarding/home/home.json index f02fc454c00..516f12229c5 100644 --- a/erpnext/setup/module_onboarding/home/home.json +++ b/erpnext/setup/module_onboarding/home/home.json @@ -22,24 +22,18 @@ "creation": "2021-11-22 12:19:15.888642", "docstatus": 0, "doctype": "Module Onboarding", - "documentation_url": "https://docs.erpnext.com/docs/v13/user/manual/en/setting-up/company-setup", + "documentation_url": "https://docs.erpnext.com/docs/v14/user/manual/en/setting-up/company-setup", "idx": 0, "is_complete": 0, - "modified": "2022-06-07 14:31:00.575193", + "modified": "2023-05-16 13:13:24.043792", "modified_by": "Administrator", "module": "Setup", "name": "Home", "owner": "Administrator", "steps": [ - { - "step": "Company Set Up" - }, { "step": "Navigation Help" }, - { - "step": "Data import" - }, { "step": "Create an Item" }, @@ -51,12 +45,9 @@ }, { "step": "Create a Quotation" - }, - { - "step": "Letterhead" } ], - "subtitle": "Company, Item, Customer, Supplier, Navigation Help, Data Import, Letter Head, Quotation", - "success_message": "Masters are all set up!", - "title": "Let's Set Up Some Masters" + "subtitle": "Item, Customer, Supplier, Navigation Help and Quotation", + "success_message": "You're ready to start your journey with ERPNext", + "title": "Let's begin your journey with ERPNext" } \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/company_set_up/company_set_up.json b/erpnext/setup/onboarding_step/company_set_up/company_set_up.json index 6f6583231f9..fae2de01129 100644 --- a/erpnext/setup/onboarding_step/company_set_up/company_set_up.json +++ b/erpnext/setup/onboarding_step/company_set_up/company_set_up.json @@ -5,11 +5,11 @@ "description": "# Set Up a Company\n\nA company is a legal entity for which you will set up your books of account and create accounting transactions. In ERPNext, you can create multiple companies, and establish relationships (group/subsidiary) among them.\n\nWithin the company master, you can capture various default accounts for that Company and set crucial settings related to the accounting methodology followed for a company.\n", "docstatus": 0, "doctype": "Onboarding Step", - "idx": 0, + "idx": 1, "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-12-15 14:22:18.317423", + "modified": "2023-05-15 09:18:42.895537", "modified_by": "Administrator", "name": "Company Set Up", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json b/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json index f74d745be9c..e1a8f908663 100644 --- a/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json +++ b/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json @@ -5,17 +5,17 @@ "description": "# Create a Customer\n\nThe Customer master is at the heart of your sales transactions. Customers are linked in Quotations, Sales Orders, Invoices, and Payments. Customers can be either numbered or identified by name (you would typically do this based on the number of customers you have).\n\nThrough Customer\u2019s master, you can effectively track essentials like:\n - Customer\u2019s multiple address and contacts\n - Account Receivables\n - Credit Limit and Credit Period\n", "docstatus": 0, "doctype": "Onboarding Step", - "idx": 0, + "idx": 1, "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-12-15 14:20:31.197564", + "modified": "2023-05-16 12:54:54.112364", "modified_by": "Administrator", "name": "Create a Customer", "owner": "Administrator", "reference_document": "Customer", "show_form_tour": 0, "show_full_form": 0, - "title": "Manage Customers", + "title": "Create a Customer", "validate_action": 1 } \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json b/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json index 8bdb621c0a5..92b45b4ff80 100644 --- a/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json +++ b/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json @@ -5,11 +5,11 @@ "description": "# Create a Quotation\n\nLet\u2019s get started with business transactions by creating your first Quotation. You can create a Quotation for an existing customer or a prospect. It will be an approved document, with items you sell and the proposed price + taxes applied. After completing the instructions, you will get a Quotation in a ready to share print format.", "docstatus": 0, "doctype": "Onboarding Step", - "idx": 0, + "idx": 1, "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-12-15 14:21:31.675330", + "modified": "2023-05-15 09:18:42.984170", "modified_by": "Administrator", "name": "Create a Quotation", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json b/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json index 9574141eaab..ef493fe00d1 100644 --- a/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json +++ b/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json @@ -5,17 +5,17 @@ "description": "# Create a Supplier\n\nAlso known as Vendor, is a master at the center of your purchase transactions. Suppliers are linked in Request for Quotation, Purchase Orders, Receipts, and Payments. Suppliers can be either numbered or identified by name.\n\nThrough Supplier\u2019s master, you can effectively track essentials like:\n - Supplier\u2019s multiple address and contacts\n - Account Receivables\n - Credit Limit and Credit Period\n", "docstatus": 0, "doctype": "Onboarding Step", - "idx": 0, + "idx": 1, "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-12-15 14:21:23.518301", + "modified": "2023-05-16 12:55:08.610113", "modified_by": "Administrator", "name": "Create a Supplier", "owner": "Administrator", "reference_document": "Supplier", "show_form_tour": 0, "show_full_form": 0, - "title": "Manage Suppliers", + "title": "Create a Supplier", "validate_action": 1 } \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/create_an_item/create_an_item.json b/erpnext/setup/onboarding_step/create_an_item/create_an_item.json index cd29683346c..15f36bec81d 100644 --- a/erpnext/setup/onboarding_step/create_an_item/create_an_item.json +++ b/erpnext/setup/onboarding_step/create_an_item/create_an_item.json @@ -6,18 +6,18 @@ "docstatus": 0, "doctype": "Onboarding Step", "form_tour": "Item General", - "idx": 0, + "idx": 1, "intro_video_url": "", "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-12-15 14:19:56.297772", + "modified": "2023-05-16 12:56:40.355878", "modified_by": "Administrator", "name": "Create an Item", "owner": "Administrator", "reference_document": "Item", "show_form_tour": 1, - "show_full_form": 1, - "title": "Manage Items", + "show_full_form": 0, + "title": "Create an Item", "validate_action": 1 } \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/data_import/data_import.json b/erpnext/setup/onboarding_step/data_import/data_import.json index 4999a368d37..e5dd7da29db 100644 --- a/erpnext/setup/onboarding_step/data_import/data_import.json +++ b/erpnext/setup/onboarding_step/data_import/data_import.json @@ -5,11 +5,11 @@ "description": "# Import Data from Spreadsheet\n\nIn ERPNext, you can easily migrate your historical data using spreadsheets. You can use it for migrating not just masters (like Customer, Supplier, Items), but also for transactions like (outstanding invoices, opening stock and accounting entries, etc).", "docstatus": 0, "doctype": "Onboarding Step", - "idx": 0, + "idx": 1, "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2022-06-07 14:28:51.390813", + "modified": "2023-05-15 09:18:42.962231", "modified_by": "Administrator", "name": "Data import", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/letterhead/letterhead.json b/erpnext/setup/onboarding_step/letterhead/letterhead.json index 8e1bb8ce827..584fd481ba1 100644 --- a/erpnext/setup/onboarding_step/letterhead/letterhead.json +++ b/erpnext/setup/onboarding_step/letterhead/letterhead.json @@ -5,11 +5,11 @@ "description": "# Create a Letter Head\n\nA Letter Head contains your organization's name, logo, address, etc which appears at the header and footer portion in documents. You can learn more about Setting up Letter Head in ERPNext here.\n", "docstatus": 0, "doctype": "Onboarding Step", - "idx": 0, + "idx": 1, "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-12-15 14:21:39.037742", + "modified": "2023-05-15 09:18:42.995184", "modified_by": "Administrator", "name": "Letterhead", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/navigation_help/navigation_help.json b/erpnext/setup/onboarding_step/navigation_help/navigation_help.json index cf07968bc7f..f57818b9f70 100644 --- a/erpnext/setup/onboarding_step/navigation_help/navigation_help.json +++ b/erpnext/setup/onboarding_step/navigation_help/navigation_help.json @@ -2,14 +2,14 @@ "action": "Watch Video", "action_label": "Learn about Navigation options", "creation": "2021-11-22 12:09:52.233872", - "description": "# Navigation in ERPNext\n\nEase of navigating and browsing around the ERPNext is one of our core strengths. In the following video, you will learn how to reach a specific feature in ERPNext via module page or awesome bar\u2019s shortcut.\n", + "description": "# Navigation in ERPNext\n\nEase of navigating and browsing around the ERPNext is one of our core strengths. In the following video, you will learn how to reach a specific feature in ERPNext via module page or AwesomeBar.", "docstatus": 0, "doctype": "Onboarding Step", - "idx": 0, + "idx": 1, "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2022-06-07 14:28:00.901082", + "modified": "2023-05-16 12:53:25.939908", "modified_by": "Administrator", "name": "Navigation Help", "owner": "Administrator", diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py index 62936fcfb89..db1cc494e0b 100644 --- a/erpnext/startup/boot.py +++ b/erpnext/startup/boot.py @@ -21,6 +21,10 @@ def boot_session(bootinfo): bootinfo.sysdefaults.allow_stale = cint( frappe.db.get_single_value("Accounts Settings", "allow_stale") ) + bootinfo.sysdefaults.over_billing_allowance = frappe.db.get_single_value( + "Accounts Settings", "over_billing_allowance" + ) + bootinfo.sysdefaults.quotation_valid_till = cint( frappe.db.get_single_value("CRM Settings", "default_valid_till") ) diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py index db2b5d0a6b6..8bff4d51470 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -75,7 +75,16 @@ class InventoryDimension(Document): self.delete_custom_fields() def delete_custom_fields(self): - filters = {"fieldname": self.source_fieldname} + filters = { + "fieldname": ( + "in", + [ + self.source_fieldname, + f"to_{self.source_fieldname}", + f"from_{self.source_fieldname}", + ], + ) + } if self.document_type: filters["dt"] = self.document_type @@ -88,6 +97,8 @@ class InventoryDimension(Document): def reset_value(self): if self.apply_to_all_doctypes: + self.type_of_transaction = "" + self.istable = 0 for field in ["document_type", "condition"]: self.set(field, None) @@ -111,12 +122,35 @@ class InventoryDimension(Document): def on_update(self): self.add_custom_fields() - def add_custom_fields(self): - dimension_fields = [ + @staticmethod + def get_insert_after_fieldname(doctype): + return frappe.get_all( + "DocField", + fields=["fieldname"], + filters={"parent": doctype}, + order_by="idx desc", + limit=1, + )[0].fieldname + + def get_dimension_fields(self, doctype=None): + if not doctype: + doctype = self.document_type + + label_start_with = "" + if doctype in ["Purchase Invoice Item", "Purchase Receipt Item"]: + label_start_with = "Target" + elif doctype in ["Sales Invoice Item", "Delivery Note Item", "Stock Entry Detail"]: + label_start_with = "Source" + + label = self.dimension_name + if label_start_with: + label = f"{label_start_with} {self.dimension_name}" + + return [ dict( fieldname="inventory_dimension", fieldtype="Section Break", - insert_after="warehouse", + insert_after=self.get_insert_after_fieldname(doctype), label="Inventory Dimension", collapsible=1, ), @@ -125,24 +159,37 @@ class InventoryDimension(Document): fieldtype="Link", insert_after="inventory_dimension", options=self.reference_document, - label=self.dimension_name, + label=label, reqd=self.reqd, mandatory_depends_on=self.mandatory_depends_on, ), ] + def add_custom_fields(self): custom_fields = {} + dimension_fields = [] if self.apply_to_all_doctypes: for doctype in get_inventory_documents(): - if not field_exists(doctype[0], self.source_fieldname): - custom_fields.setdefault(doctype[0], dimension_fields) + if field_exists(doctype[0], self.source_fieldname): + continue + + dimension_fields = self.get_dimension_fields(doctype[0]) + self.add_transfer_field(doctype[0], dimension_fields) + custom_fields.setdefault(doctype[0], dimension_fields) elif not field_exists(self.document_type, self.source_fieldname): + dimension_fields = self.get_dimension_fields() + + self.add_transfer_field(self.document_type, dimension_fields) custom_fields.setdefault(self.document_type, dimension_fields) - if not frappe.db.get_value( - "Custom Field", {"dt": "Stock Ledger Entry", "fieldname": self.target_fieldname} - ) and not field_exists("Stock Ledger Entry", self.target_fieldname): + if ( + dimension_fields + and not frappe.db.get_value( + "Custom Field", {"dt": "Stock Ledger Entry", "fieldname": self.target_fieldname} + ) + and not field_exists("Stock Ledger Entry", self.target_fieldname) + ): dimension_field = dimension_fields[1] dimension_field["mandatory_depends_on"] = "" dimension_field["reqd"] = 0 @@ -152,6 +199,53 @@ class InventoryDimension(Document): if custom_fields: create_custom_fields(custom_fields) + def add_transfer_field(self, doctype, dimension_fields): + if doctype not in [ + "Stock Entry Detail", + "Sales Invoice Item", + "Delivery Note Item", + "Purchase Invoice Item", + "Purchase Receipt Item", + ]: + return + + fieldname_start_with = "to" + label_start_with = "Target" + display_depends_on = "" + + if doctype in ["Purchase Invoice Item", "Purchase Receipt Item"]: + fieldname_start_with = "from" + label_start_with = "Source" + display_depends_on = "eval:parent.is_internal_supplier == 1" + elif doctype != "Stock Entry Detail": + display_depends_on = "eval:parent.is_internal_customer == 1" + elif doctype == "Stock Entry Detail": + display_depends_on = "eval:parent.purpose != 'Material Issue'" + + fieldname = f"{fieldname_start_with}_{self.source_fieldname}" + label = f"{label_start_with} {self.dimension_name}" + + if field_exists(doctype, fieldname): + return + + dimension_fields.extend( + [ + dict( + fieldname="inventory_dimension_col_break", + fieldtype="Column Break", + insert_after=self.source_fieldname, + ), + dict( + fieldname=fieldname, + fieldtype="Link", + insert_after="inventory_dimension_col_break", + options=self.reference_document, + label=label, + depends_on=display_depends_on, + ), + ] + ) + def field_exists(doctype, fieldname) -> str or None: return frappe.db.get_value("DocField", {"parent": doctype, "fieldname": fieldname}, "name") @@ -185,18 +279,19 @@ def get_evaluated_inventory_dimension(doc, sl_dict, parent_doc=None): dimensions = get_document_wise_inventory_dimensions(doc.doctype) filter_dimensions = [] for row in dimensions: - if ( - row.type_of_transaction == "Inward" - if doc.docstatus == 1 - else row.type_of_transaction != "Inward" - ) and sl_dict.actual_qty < 0: - continue - elif ( - row.type_of_transaction == "Outward" - if doc.docstatus == 1 - else row.type_of_transaction != "Outward" - ) and sl_dict.actual_qty > 0: - continue + if row.type_of_transaction: + if ( + row.type_of_transaction == "Inward" + if doc.docstatus == 1 + else row.type_of_transaction != "Inward" + ) and sl_dict.actual_qty < 0: + continue + elif ( + row.type_of_transaction == "Outward" + if doc.docstatus == 1 + else row.type_of_transaction != "Outward" + ) and sl_dict.actual_qty > 0: + continue evals = {"doc": doc} if parent_doc: diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index 28b1ed96f0d..b1d7f8f00c6 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -4,6 +4,7 @@ import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.tests.utils import FrappeTestCase +from frappe.utils import nowdate, nowtime from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.inventory_dimension.inventory_dimension import ( @@ -12,6 +13,7 @@ from erpnext.stock.doctype.inventory_dimension.inventory_dimension import ( DoNotChangeError, delete_dimension, ) +from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse @@ -20,6 +22,7 @@ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse class TestInventoryDimension(FrappeTestCase): def setUp(self): prepare_test_data() + create_store_dimension() def test_validate_inventory_dimension(self): # Can not be child doc @@ -73,6 +76,8 @@ class TestInventoryDimension(FrappeTestCase): self.assertFalse(custom_field) def test_inventory_dimension(self): + frappe.local.document_wise_inventory_dimensions = {} + warehouse = "Shelf Warehouse - _TC" item_code = "_Test Item" @@ -143,6 +148,8 @@ class TestInventoryDimension(FrappeTestCase): self.assertRaises(DoNotChangeError, inv_dim1.save) def test_inventory_dimension_for_purchase_receipt_and_delivery_note(self): + frappe.local.document_wise_inventory_dimensions = {} + inv_dimension = create_inventory_dimension( reference_document="Rack", dimension_name="Rack", apply_to_all_doctypes=1 ) @@ -250,6 +257,191 @@ class TestInventoryDimension(FrappeTestCase): ) ) + def test_for_purchase_sales_and_stock_transaction(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + create_inventory_dimension( + reference_document="Store", + type_of_transaction="Outward", + dimension_name="Store", + apply_to_all_doctypes=1, + ) + + item_code = "Test Inventory Dimension Item" + create_item(item_code) + warehouse = create_warehouse("Store Warehouse") + + # Purchase Receipt -> Inward in Store 1 + pr_doc = make_purchase_receipt( + item_code=item_code, warehouse=warehouse, qty=10, rate=100, do_not_submit=True + ) + + pr_doc.items[0].store = "Store 1" + pr_doc.save() + pr_doc.submit() + + entries = get_voucher_sl_entries(pr_doc.name, ["warehouse", "store", "incoming_rate"]) + + self.assertEqual(entries[0].warehouse, warehouse) + self.assertEqual(entries[0].store, "Store 1") + + # Stock Entry -> Transfer from Store 1 to Store 2 + se_doc = make_stock_entry( + item_code=item_code, qty=10, from_warehouse=warehouse, to_warehouse=warehouse, do_not_save=True + ) + + se_doc.items[0].store = "Store 1" + se_doc.items[0].to_store = "Store 2" + + se_doc.save() + se_doc.submit() + + entries = get_voucher_sl_entries( + se_doc.name, ["warehouse", "store", "incoming_rate", "actual_qty"] + ) + + for entry in entries: + self.assertEqual(entry.warehouse, warehouse) + if entry.actual_qty > 0: + self.assertEqual(entry.store, "Store 2") + self.assertEqual(entry.incoming_rate, 100.0) + else: + self.assertEqual(entry.store, "Store 1") + + # Delivery Note -> Outward from Store 2 + + dn_doc = create_delivery_note(item_code=item_code, qty=10, warehouse=warehouse, do_not_save=True) + + dn_doc.items[0].store = "Store 2" + dn_doc.save() + dn_doc.submit() + + entries = get_voucher_sl_entries(dn_doc.name, ["warehouse", "store", "actual_qty"]) + + self.assertEqual(entries[0].warehouse, warehouse) + self.assertEqual(entries[0].store, "Store 2") + self.assertEqual(entries[0].actual_qty, -10.0) + + return_dn = make_return_doc("Delivery Note", dn_doc.name) + return_dn.submit() + entries = get_voucher_sl_entries(return_dn.name, ["warehouse", "store", "actual_qty"]) + + self.assertEqual(entries[0].warehouse, warehouse) + self.assertEqual(entries[0].store, "Store 2") + self.assertEqual(entries[0].actual_qty, 10.0) + + se_doc = make_stock_entry( + item_code=item_code, qty=10, from_warehouse=warehouse, to_warehouse=warehouse, do_not_save=True + ) + + se_doc.items[0].store = "Store 2" + se_doc.items[0].to_store = "Store 1" + + se_doc.save() + se_doc.submit() + + return_pr = make_return_doc("Purchase Receipt", pr_doc.name) + return_pr.submit() + entries = get_voucher_sl_entries(return_pr.name, ["warehouse", "store", "actual_qty"]) + + self.assertEqual(entries[0].warehouse, warehouse) + self.assertEqual(entries[0].store, "Store 1") + self.assertEqual(entries[0].actual_qty, -10.0) + + def test_inter_transfer_return_against_inventory_dimension(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + + data = prepare_data_for_internal_transfer() + + dn_doc = create_delivery_note( + customer=data.customer, + company=data.company, + warehouse=data.from_warehouse, + target_warehouse=data.to_warehouse, + qty=5, + cost_center=data.cost_center, + expense_account=data.expense_account, + do_not_submit=True, + ) + + dn_doc.items[0].store = "Inter Transfer Store 1" + dn_doc.items[0].to_store = "Inter Transfer Store 2" + dn_doc.save() + dn_doc.submit() + + for d in get_voucher_sl_entries(dn_doc.name, ["store", "actual_qty"]): + if d.actual_qty > 0: + self.assertEqual(d.store, "Inter Transfer Store 2") + else: + self.assertEqual(d.store, "Inter Transfer Store 1") + + pr_doc = make_inter_company_purchase_receipt(dn_doc.name) + pr_doc.items[0].warehouse = data.store_warehouse + pr_doc.items[0].from_store = "Inter Transfer Store 2" + pr_doc.items[0].store = "Inter Transfer Store 3" + pr_doc.save() + pr_doc.submit() + + for d in get_voucher_sl_entries(pr_doc.name, ["store", "actual_qty"]): + if d.actual_qty > 0: + self.assertEqual(d.store, "Inter Transfer Store 3") + else: + self.assertEqual(d.store, "Inter Transfer Store 2") + + return_doc = make_return_doc("Purchase Receipt", pr_doc.name) + return_doc.submit() + + for d in get_voucher_sl_entries(return_doc.name, ["store", "actual_qty"]): + if d.actual_qty > 0: + self.assertEqual(d.store, "Inter Transfer Store 2") + else: + self.assertEqual(d.store, "Inter Transfer Store 3") + + dn_doc.load_from_db() + + return_doc1 = make_return_doc("Delivery Note", dn_doc.name) + return_doc1.posting_date = nowdate() + return_doc1.posting_time = nowtime() + return_doc1.items[0].target_warehouse = dn_doc.items[0].target_warehouse + return_doc1.items[0].warehouse = dn_doc.items[0].warehouse + return_doc1.save() + return_doc1.submit() + + for d in get_voucher_sl_entries(return_doc1.name, ["store", "actual_qty"]): + if d.actual_qty > 0: + self.assertEqual(d.store, "Inter Transfer Store 1") + else: + self.assertEqual(d.store, "Inter Transfer Store 2") + + +def get_voucher_sl_entries(voucher_no, fields): + return frappe.get_all( + "Stock Ledger Entry", filters={"voucher_no": voucher_no}, fields=fields, order_by="creation" + ) + + +def create_store_dimension(): + if not frappe.db.exists("DocType", "Store"): + frappe.get_doc( + { + "doctype": "DocType", + "name": "Store", + "module": "Stock", + "custom": 1, + "naming_rule": "By fieldname", + "autoname": "field:store_name", + "fields": [{"label": "Store Name", "fieldname": "store_name", "fieldtype": "Data"}], + "permissions": [ + {"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1} + ], + } + ).insert(ignore_permissions=True) + + for store in ["Store 1", "Store 2"]: + if not frappe.db.exists("Store", store): + frappe.get_doc({"doctype": "Store", "store_name": store}).insert(ignore_permissions=True) + def prepare_test_data(): if not frappe.db.exists("DocType", "Shelf"): @@ -326,3 +518,79 @@ def create_inventory_dimension(**args): doc.insert(ignore_permissions=True) return doc + + +def prepare_data_for_internal_transfer(): + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier + from erpnext.selling.doctype.customer.test_customer import create_internal_customer + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + company = "_Test Company with perpetual inventory" + + customer = create_internal_customer( + "_Test Internal Customer 2", + company, + company, + ) + + supplier = create_internal_supplier( + "_Test Internal Supplier 2", + company, + company, + ) + + for store in ["Inter Transfer Store 1", "Inter Transfer Store 2", "Inter Transfer Store 3"]: + if not frappe.db.exists("Store", store): + frappe.get_doc({"doctype": "Store", "store_name": store}).insert(ignore_permissions=True) + + warehouse = create_warehouse("_Test Internal Warehouse New A", company=company) + + to_warehouse = create_warehouse("_Test Internal Warehouse GIT A", company=company) + + pr_doc = make_purchase_receipt( + company=company, warehouse=warehouse, qty=10, rate=100, do_not_submit=True + ) + pr_doc.items[0].store = "Inter Transfer Store 1" + pr_doc.submit() + + if not frappe.db.get_value("Company", company, "unrealized_profit_loss_account"): + account = "Unrealized Profit and Loss - TCP1" + if not frappe.db.exists("Account", account): + frappe.get_doc( + { + "doctype": "Account", + "account_name": "Unrealized Profit and Loss", + "parent_account": "Direct Income - TCP1", + "company": company, + "is_group": 0, + "account_type": "Income Account", + } + ).insert() + + frappe.db.set_value("Company", company, "unrealized_profit_loss_account", account) + + cost_center = frappe.db.get_value("Company", company, "cost_center") or frappe.db.get_value( + "Cost Center", {"company": company}, "name" + ) + + expense_account = frappe.db.get_value( + "Company", company, "stock_adjustment_account" + ) or frappe.db.get_value( + "Account", {"company": company, "account_type": "Expense Account"}, "name" + ) + + return frappe._dict( + { + "from_warehouse": warehouse, + "to_warehouse": to_warehouse, + "customer": customer, + "supplier": supplier, + "company": company, + "cost_center": cost_center, + "expense_account": expense_account, + "store_warehouse": frappe.db.get_value( + "Warehouse", {"name": ("like", "Store%"), "company": company}, "name" + ), + } + ) diff --git a/erpnext/stock/doctype/item/item_list.js b/erpnext/stock/doctype/item/item_list.js index 534b3419203..22d38e88935 100644 --- a/erpnext/stock/doctype/item/item_list.js +++ b/erpnext/stock/doctype/item/item_list.js @@ -1,5 +1,5 @@ frappe.listview_settings['Item'] = { - add_fields: ["item_name", "stock_uom", "item_group", "image", "variant_of", + add_fields: ["item_name", "stock_uom", "item_group", "image", "has_variants", "end_of_life", "disabled"], filters: [["disabled", "=", "0"]], diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index bf3b5ddc54a..46d6e9e7578 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -172,8 +172,8 @@ class PickList(Document): if (row.picked_qty / row.stock_qty) * 100 > over_delivery_receipt_allowance: frappe.throw( _( - f"You are picking more than required quantity for the item {row.item_code}. Check if there is any other pick list created for the sales order {row.sales_order}." - ) + "You are picking more than required quantity for the item {0}. Check if there is any other pick list created for the sales order {1}." + ).format(row.item_code, row.sales_order) ) @frappe.whitelist() 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 96ac4352dcd..9c4d997b316 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 @@ -376,3 +376,19 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin): accounts_settings.acc_frozen_upto = "" accounts_settings.save() + + def test_create_repost_entry_for_cancelled_document(self): + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + get_multiple_items=True, + ) + + self.assertTrue(pr.docstatus == 1) + self.assertFalse(frappe.db.exists("Repost Item Valuation", {"voucher_no": pr.name})) + + pr.load_from_db() + + pr.cancel() + self.assertTrue(pr.docstatus == 2) + self.assertTrue(frappe.db.exists("Repost Item Valuation", {"voucher_no": pr.name})) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 103ed4ac3d0..6106809273f 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -781,13 +781,21 @@ class update_entries_after(object): d.db_update() def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate): - if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no): - frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "rate", outgoing_rate) + if frappe.db.exists("Subcontracting Receipt Item", sle.voucher_detail_no): + frappe.db.set_value("Subcontracting Receipt Item", sle.voucher_detail_no, "rate", outgoing_rate) else: frappe.db.set_value( - "Subcontracting Receipt Supplied Item", sle.voucher_detail_no, "rate", outgoing_rate + "Subcontracting Receipt Supplied Item", + sle.voucher_detail_no, + {"rate": outgoing_rate, "amount": abs(sle.actual_qty) * outgoing_rate}, ) + scr = frappe.get_doc("Subcontracting Receipt", sle.voucher_no, for_update=True) + scr.calculate_items_qty_and_amount() + scr.db_update() + for d in scr.items: + d.db_update() + def get_serialized_values(self, sle): incoming_rate = flt(sle.incoming_rate) actual_qty = flt(sle.actual_qty) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index e6de72d494d..39197332c1c 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -77,22 +77,22 @@ class SubcontractingOrder(SubcontractingController): frappe.throw(_(msg)) def set_missing_values(self): - self.set_missing_values_in_additional_costs() - self.set_missing_values_in_service_items() - self.set_missing_values_in_supplied_items() - self.set_missing_values_in_items() + self.calculate_additional_costs() + self.calculate_service_costs() + self.calculate_supplied_items_qty_and_amount() + self.calculate_items_qty_and_amount() - def set_missing_values_in_service_items(self): + def calculate_service_costs(self): for idx, item in enumerate(self.get("service_items")): self.items[idx].service_cost_per_qty = item.amount / self.items[idx].qty - def set_missing_values_in_supplied_items(self): + def calculate_supplied_items_qty_and_amount(self): for item in self.get("items"): bom = frappe.get_doc("BOM", item.bom) rm_cost = sum(flt(rm_item.amount) for rm_item in bom.items) item.rm_cost_per_qty = rm_cost / flt(bom.quantity) - def set_missing_values_in_items(self): + def calculate_items_qty_and_amount(self): total_qty = total = 0 for item in self.items: item.rate = item.rm_cost_per_qty + item.service_cost_per_qty + flt(item.additional_cost_per_qty) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 4f8e045d706..2c842622730 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -113,9 +113,9 @@ class SubcontractingReceipt(SubcontractingController): @frappe.whitelist() def set_missing_values(self): - self.set_missing_values_in_additional_costs() - self.set_missing_values_in_supplied_items() - self.set_missing_values_in_items() + self.calculate_additional_costs() + self.calculate_supplied_items_qty_and_amount() + self.calculate_items_qty_and_amount() def set_available_qty_for_consumption(self): supplied_items_details = {} @@ -147,13 +147,13 @@ class SubcontractingReceipt(SubcontractingController): item.rm_item_code, 0 ) - def set_missing_values_in_supplied_items(self): + def calculate_supplied_items_qty_and_amount(self): for item in self.get("supplied_items") or []: item.amount = item.rate * item.consumed_qty self.set_available_qty_for_consumption() - def set_missing_values_in_items(self): + def calculate_items_qty_and_amount(self): rm_supp_cost = {} for item in self.get("supplied_items") or []: if item.reference_name in rm_supp_cost: diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index 72ed4d4e2ef..dfb72c33567 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -6,7 +6,7 @@ import copy import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import cint, flt +from frappe.utils import add_days, cint, cstr, flt, today import erpnext from erpnext.accounts.doctype.account.test_account import get_inventory_account @@ -26,6 +26,9 @@ from erpnext.controllers.tests.test_subcontracting_controller import ( from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry +from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + create_stock_reconciliation, +) from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( make_subcontracting_receipt, ) @@ -528,6 +531,69 @@ class TestSubcontractingReceipt(FrappeTestCase): # consumed_qty should be (accepted_qty * qty_consumed_per_unit) = (6 * 1) = 6 self.assertEqual(scr.supplied_items[0].consumed_qty, 6) + def test_supplied_items_cost_after_reposting(self): + # Set Backflush Based On as "BOM" + set_backflush_based_on("BOM") + + # Create Material Receipt for RM's + make_stock_entry( + item_code="_Test Item", + qty=100, + target="_Test Warehouse 1 - _TC", + basic_rate=100, + posting_date=add_days(today(), -2), + ) + make_stock_entry( + item_code="_Test Item Home Desktop 100", + qty=100, + target="_Test Warehouse 1 - _TC", + basic_rate=100, + ) + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 10, + "rate": 100, + "fg_item": "_Test FG Item", + "fg_item_qty": 10, + }, + ] + + # Create Subcontracting Order + sco = get_subcontracting_order(service_items=service_items) + + # Transfer RM's + rm_items = get_rm_items(sco.supplied_items) + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + # Create Subcontracting Receipt + scr = make_subcontracting_receipt(sco.name) + scr.save() + scr.submit() + + # Create Backdated Stock Reconciliation + sr = create_stock_reconciliation( + item_code=rm_items[0].get("item_code"), + warehouse="_Test Warehouse 1 - _TC", + qty=100, + rate=50, + posting_date=add_days(today(), -1), + ) + + # Cost should be updated in Subcontracting Receipt after reposting + prev_cost = scr.supplied_items[0].rate + scr.load_from_db() + self.assertNotEqual(scr.supplied_items[0].rate, prev_cost) + self.assertEqual(scr.supplied_items[0].rate, sr.items[0].valuation_rate) + def make_return_subcontracting_receipt(**args): args = frappe._dict(args) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index e49f212f10f..7f4e9efa948 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -776,6 +776,9 @@ def on_communication_update(doc, status): if not parent.meta.has_field("service_level_agreement"): return + if not parent.get("service_level_agreement"): + return + if ( doc.sent_or_received == "Received" # a reply is received and parent.get("status") == "Open" # issue status is set as open from communication.py