diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index 3b5698b118a..977cfe94f8a 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -192,7 +192,7 @@ class ExchangeRateRevaluation(Document): # round off balance based on currency precision # and consider debit-credit difference allowance currency_precision = get_currency_precision() - rounding_loss_allowance = float(rounding_loss_allowance) or 0.05 + rounding_loss_allowance = float(rounding_loss_allowance) for acc in account_details: acc.balance_in_account_currency = flt(acc.balance_in_account_currency, currency_precision) if abs(acc.balance_in_account_currency) <= rounding_loss_allowance: diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index cf1341b9c70..c59643280e8 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry"); frappe.ui.form.on("Journal Entry", { setup: function(frm) { frm.add_fetch("bank_account", "account", "account"); - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger', 'Asset', 'Asset Movement', 'Repost Accounting Ledger']; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"]; }, refresh: function(frm) { diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 4bf9501e5a0..82a9c0f1524 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -98,6 +98,8 @@ class JournalEntry(AccountsController): "Repost Payment Ledger Items", "Repost Accounting Ledger", "Repost Accounting Ledger Items", + "Unreconcile Payment", + "Unreconcile Payment Entries", ) self.make_gl_entries(1) self.update_advance_paid() diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 778a441b237..a3ebf045fc9 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -9,6 +9,8 @@ import frappe from frappe import ValidationError, _, qb, scrub, throw from frappe.utils import cint, comma_or, flt, getdate, nowdate from frappe.utils.data import comma_and, fmt_money +from pypika import Case +from pypika.functions import Coalesce, Sum import erpnext from erpnext.accounts.doctype.bank_account.bank_account import ( @@ -1566,12 +1568,13 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list if not split_rows: continue - frappe.msgprint( - _("Splitting {0} {1} into {2} rows as per Payment Terms").format( - _(entry.voucher_type), frappe.bold(entry.voucher_no), len(split_rows) - ), - alert=True, - ) + if len(split_rows) > 1: + frappe.msgprint( + _("Splitting {0} {1} into {2} rows as per Payment Terms").format( + _(entry.voucher_type), frappe.bold(entry.voucher_no), len(split_rows) + ), + alert=True, + ) outstanding_invoices_after_split += split_rows continue @@ -1853,18 +1856,24 @@ def get_company_defaults(company): def get_outstanding_on_journal_entry(name): - res = frappe.db.sql( - "SELECT " - 'CASE WHEN party_type IN ("Customer") ' - "THEN ifnull(sum(debit_in_account_currency - credit_in_account_currency), 0) " - "ELSE ifnull(sum(credit_in_account_currency - debit_in_account_currency), 0) " - "END as outstanding_amount " - "FROM `tabGL Entry` WHERE (voucher_no=%s OR against_voucher=%s) " - "AND party_type IS NOT NULL " - 'AND party_type != ""', - (name, name), - as_dict=1, - ) + gl = frappe.qb.DocType("GL Entry") + res = ( + frappe.qb.from_(gl) + .select( + Case() + .when( + gl.party_type == "Customer", + Coalesce(Sum(gl.debit_in_account_currency - gl.credit_in_account_currency), 0), + ) + .else_(Coalesce(Sum(gl.credit_in_account_currency - gl.debit_in_account_currency), 0)) + .as_("outstanding_amount") + ) + .where( + (Coalesce(gl.party_type, "") != "") + & (gl.is_cancelled == 0) + & ((gl.voucher_no == name) | (gl.against_voucher == name)) + ) + ).run(as_dict=True) outstanding_amount = res[0].get("outstanding_amount", 0) if res else 0 diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json index 0dc9c135b8c..e8985de4e1e 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -203,9 +203,10 @@ ], "hide_toolbar": 1, "icon": "icon-resize-horizontal", + "is_virtual": 1, "issingle": 1, "links": [], - "modified": "2023-08-15 05:35:50.109290", + "modified": "2023-11-17 17:33:55.701726", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation", @@ -230,6 +231,5 @@ ], "sort_field": "modified", "sort_order": "DESC", - "states": [], - "track_changes": 1 + "states": [] } diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index d2c75111edd..a907dc7acf5 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -29,6 +29,58 @@ class PaymentReconciliation(Document): self.accounting_dimension_filter_conditions = [] self.ple_posting_date_filter = [] + def load_from_db(self): + # 'modified' attribute is required for `run_doc_method` to work properly. + doc_dict = frappe._dict( + { + "modified": None, + "company": None, + "party": None, + "party_type": None, + "receivable_payable_account": None, + "default_advance_account": None, + "from_invoice_date": None, + "to_invoice_date": None, + "invoice_limit": 50, + "from_payment_date": None, + "to_payment_date": None, + "payment_limit": 50, + "minimum_invoice_amount": None, + "minimum_payment_amount": None, + "maximum_invoice_amount": None, + "maximum_payment_amount": None, + "bank_cash_account": None, + "cost_center": None, + "payment_name": None, + "invoice_name": None, + } + ) + super(Document, self).__init__(doc_dict) + + def save(self): + return + + @staticmethod + def get_list(args): + pass + + @staticmethod + def get_count(args): + pass + + @staticmethod + def get_stats(args): + pass + + def db_insert(self, *args, **kwargs): + pass + + def db_update(self, *args, **kwargs): + pass + + def delete(self): + pass + @frappe.whitelist() def get_unreconciled_entries(self): self.get_nonreconciled_payment_entries() diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json index 5b8556e7c83..491c67818df 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json +++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json @@ -159,9 +159,10 @@ "label": "Difference Posting Date" } ], + "is_virtual": 1, "istable": 1, "links": [], - "modified": "2023-10-23 10:44:56.066303", + "modified": "2023-11-17 17:33:38.612615", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Allocation", diff --git a/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json b/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json index c4dbd7e8441..7c9d49e7731 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json +++ b/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json @@ -71,9 +71,10 @@ "label": "Exchange Rate" } ], + "is_virtual": 1, "istable": 1, "links": [], - "modified": "2022-11-08 18:18:02.502149", + "modified": "2023-11-17 17:33:45.455166", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Invoice", diff --git a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json index 17f3900880c..d199236ae99 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json +++ b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json @@ -107,9 +107,10 @@ "options": "Cost Center" } ], + "is_virtual": 1, "istable": 1, "links": [], - "modified": "2023-09-03 07:43:29.965353", + "modified": "2023-11-17 17:33:34.818530", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Payment", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 7f0f04ea651..c2aa1d936e5 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -31,7 +31,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. super.onload(); // Ignore linked advances - this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger"]; + this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"]; if(!this.frm.doc.__islocal) { // show credit_to in print format diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 8bbea96e1d2..944a8fb2364 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1290,6 +1290,8 @@ class PurchaseInvoice(BuyingController): "Repost Payment Ledger Items", "Repost Accounting Ledger", "Repost Accounting Ledger Items", + "Unreconcile Payment", + "Unreconcile Payment Entries", "Payment Ledger Entry", "Tax Withheld Vouchers", ) @@ -1703,6 +1705,4 @@ def make_purchase_receipt(source_name, target_doc=None): target_doc, ) - doc.set_onload("ignore_price_list", True) - return doc 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 b62b4bab140..bc56132c60f 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -286,6 +286,7 @@ "oldfieldname": "import_rate", "oldfieldtype": "Currency", "options": "currency", + "read_only_depends_on": "eval: (!parent.is_return && doc.purchase_receipt && doc.pr_detail)", "reqd": 1 }, { @@ -893,7 +894,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:33:48.547297", + "modified": "2023-11-30 16:26:05.629780", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 34ea24dde49..434662c298b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1934,7 +1934,6 @@ def make_delivery_note(source_name, target_doc=None): set_missing_values, ) - doclist.set_onload("ignore_price_list", True) return doclist diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 037f0ec4a15..c24d28b3249 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -195,7 +195,7 @@ def set_address_details( company_address=None, shipping_address=None, *, - ignore_permissions=False + ignore_permissions=False, ): billing_address_field = ( "customer_address" if party_type == "Lead" else party_type.lower() + "_address" @@ -239,7 +239,7 @@ def set_address_details( shipping_address_display=render_address( shipping_address, check_permissions=not ignore_permissions ), - **get_fetch_values(doctype, "shipping_address", shipping_address) + **get_fetch_values(doctype, "shipping_address", shipping_address), ) if party_details.company_address: @@ -250,7 +250,7 @@ def set_address_details( party_details.company_address_display or render_address(party_details.company_address, check_permissions=False) ), - **get_fetch_values(doctype, "billing_address", party_details.company_address) + **get_fetch_values(doctype, "billing_address", party_details.company_address), ) # shipping address - if not already set @@ -258,7 +258,7 @@ def set_address_details( party_details.update( shipping_address=party_details.billing_address, shipping_address_display=party_details.billing_address_display, - **get_fetch_values(doctype, "shipping_address", party_details.billing_address) + **get_fetch_values(doctype, "shipping_address", party_details.billing_address), ) party_address, shipping_address = ( @@ -956,6 +956,9 @@ def get_partywise_advanced_payment_amount( if party: query = query.where(ple.party == party) + if invoice_doctypes := frappe.get_hooks("invoice_doctypes"): + query = query.where(ple.voucher_type.notin(invoice_doctypes)) + data = query.run() if data: return frappe._dict(data) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index ae3fa875e84..f1abc1d4ddb 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -1085,7 +1085,7 @@ class ReceivablePayableReport(object): ) if self.filters.show_remarks: - self.add_column(label=_("Remarks"), fieldname="remarks", fieldtype="Text", width=200), + self.add_column(label=_("Remarks"), fieldname="remarks", fieldtype="Text", width=200) def add_column(self, label, fieldname=None, fieldtype="Currency", options=None, width=120): if not fieldname: diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 2e0a9c5f738..759bb71ab24 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -282,7 +282,8 @@ def get_conditions(filters): if accounting_dimensions: for dimension in accounting_dimensions: - if not dimension.disabled: + # Ignore 'Finance Book' set up as dimension in below logic, as it is already handled in above section + if not dimension.disabled and dimension.document_type != "Finance Book": if filters.get(dimension.fieldname): if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): filters[dimension.fieldname] = get_dimension_with_children( diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 2836056b0c9..c98b8193538 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1809,6 +1809,8 @@ class QueryPaymentLedger(object): .where(ple.delinked == 0) .where(Criterion.all(filter_on_against_voucher_no)) .where(Criterion.all(self.common_filter)) + .where(Criterion.all(self.dimensions_filter)) + .where(Criterion.all(self.voucher_posting_date)) .groupby(ple.against_voucher_type, ple.against_voucher_no, ple.party_type, ple.party) .orderby(ple.posting_date, ple.voucher_no) .having(qb.Field("amount_in_account_currency") > 0) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index afe532d345d..37fbd184fe1 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -322,16 +322,16 @@ frappe.ui.form.on('Asset', { }, make_schedules_editable: function(frm) { - if (frm.doc.finance_books) { + if (frm.doc.finance_books.length) { var is_manual_hence_editable = frm.doc.finance_books.filter(d => d.depreciation_method == "Manual").length > 0 ? true : false; var is_shift_hence_editable = frm.doc.finance_books.filter(d => d.shift_based).length > 0 ? true : false; - frm.toggle_enable("depreciation_schedule", is_manual_hence_editable || is_shift_hence_editable); - frm.fields_dict["depreciation_schedule"].grid.toggle_enable("schedule_date", is_manual_hence_editable); - frm.fields_dict["depreciation_schedule"].grid.toggle_enable("depreciation_amount", is_manual_hence_editable); - frm.fields_dict["depreciation_schedule"].grid.toggle_enable("shift", is_shift_hence_editable); + frm.toggle_enable("schedules", is_manual_hence_editable || is_shift_hence_editable); + frm.fields_dict["schedules"].grid.toggle_enable("schedule_date", is_manual_hence_editable); + frm.fields_dict["schedules"].grid.toggle_enable("depreciation_amount", is_manual_hence_editable); + frm.fields_dict["schedules"].grid.toggle_enable("shift", is_shift_hence_editable); } }, diff --git a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py index 977a9b3714b..8fdcd0c14df 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py @@ -15,6 +15,9 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu class TestAssetValueAdjustment(unittest.TestCase): def setUp(self): create_asset_data() + frappe.db.set_value( + "Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC" + ) def test_current_asset_value(self): pr = make_purchase_receipt( diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 69f34c33b69..8131104b825 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -86,6 +86,10 @@ class PurchaseOrder(BuyingController): self.reset_default_field_value("set_warehouse", "items", "warehouse") def validate_with_previous_doc(self): + mri_compare_fields = [["project", "="], ["item_code", "="]] + if self.is_subcontracted: + mri_compare_fields = [["project", "="]] + super(PurchaseOrder, self).validate_with_previous_doc( { "Supplier Quotation": { @@ -108,7 +112,7 @@ class PurchaseOrder(BuyingController): }, "Material Request Item": { "ref_dn_field": "material_request_item", - "compare_fields": [["project", "="], ["item_code", "="]], + "compare_fields": mri_compare_fields, "is_child_table": True, }, } @@ -282,23 +286,6 @@ class PurchaseOrder(BuyingController): check_list.append(d.material_request) check_on_hold_or_closed_status("Material Request", d.material_request) - def update_requested_qty(self): - material_request_map = {} - for d in self.get("items"): - if d.material_request_item: - material_request_map.setdefault(d.material_request, []).append(d.material_request_item) - - for mr, mr_item_rows in material_request_map.items(): - if mr and mr_item_rows: - mr_obj = frappe.get_doc("Material Request", mr) - - if mr_obj.status in ["Stopped", "Cancelled"]: - frappe.throw( - _("Material Request {0} is cancelled or stopped").format(mr), frappe.InvalidStatusError - ) - - mr_obj.update_requested_qty(mr_item_rows) - def update_ordered_qty(self, po_item_rows=None): """update requested qty (before ordered_qty is updated)""" item_wh_list = [] @@ -340,7 +327,9 @@ class PurchaseOrder(BuyingController): self.update_status_updater() self.update_prevdoc_status() - self.update_requested_qty() + if not self.is_subcontracted or self.is_old_subcontracting_flow: + self.update_requested_qty() + self.update_ordered_qty() self.validate_budget() self.update_reserved_qty_for_subcontract() @@ -372,7 +361,9 @@ class PurchaseOrder(BuyingController): # Must be called after updating ordered qty in Material Request # bin uses Material Request Items to recalculate & update - self.update_requested_qty() + if not self.is_subcontracted or self.is_old_subcontracting_flow: + self.update_requested_qty() + self.update_ordered_qty() self.update_blanket_order() @@ -450,6 +441,20 @@ class PurchaseOrder(BuyingController): else: self.db_set("per_received", 0, update_modified=False) + def update_ordered_qty_in_so_for_removed_items(self, removed_items): + """ + Updates ordered_qty in linked SO when item rows are removed using Update Items + """ + if not self.is_against_so(): + return + for item in removed_items: + prev_ordered_qty = frappe.get_cached_value( + "Sales Order Item", item.get("sales_order_item"), "ordered_qty" + ) + frappe.db.set_value( + "Sales Order Item", item.get("sales_order_item"), "ordered_qty", prev_ordered_qty - item.qty + ) + def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0): """get last purchase rate for an item""" @@ -536,8 +541,6 @@ def make_purchase_receipt(source_name, target_doc=None): set_missing_values, ) - doc.set_onload("ignore_price_list", True) - return doc @@ -617,7 +620,6 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions postprocess, ignore_permissions=ignore_permissions, ) - doc.set_onload("ignore_price_list", True) return doc @@ -679,7 +681,10 @@ def get_mapped_subcontracting_order(source_name, target_doc=None): }, "Purchase Order Item": { "doctype": "Subcontracting Order Service Item", - "field_map": {}, + "field_map": { + "material_request": "material_request", + "material_request_item": "material_request_item", + }, "field_no_map": [], }, }, @@ -705,8 +710,8 @@ def get_mapped_subcontracting_order(source_name, target_doc=None): @frappe.whitelist() def is_subcontracting_order_created(po_name) -> bool: - count = frappe.db.count( - "Subcontracting Order", {"purchase_order": po_name, "status": ["not in", ["Draft", "Cancelled"]]} + return ( + True + if frappe.db.exists("Subcontracting Order", {"purchase_order": po_name, "docstatus": ["=", 1]}) + else False ) - - return True if count else False diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py index 2dd748bc19c..e27fbe8aaa2 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py @@ -168,7 +168,6 @@ def make_purchase_order(source_name, target_doc=None): set_missing_values, ) - doclist.set_onload("ignore_price_list", True) return doclist diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 4ef251f8add..393ad171d52 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2874,6 +2874,9 @@ def validate_and_delete_children(parent, data) -> bool: d.cancel() d.delete() + if parent.doctype == "Purchase Order": + parent.update_ordered_qty_in_so_for_removed_items(deleted_children) + # need to update ordered qty in Material Request first # bin uses Material Request Items to recalculate & update parent.update_prevdoc_status() @@ -3129,7 +3132,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil if parent_doctype == "Purchase Order": update_last_purchase_rate(parent, is_submit=1) - parent.update_prevdoc_status() + + if any_qty_changed or items_added_or_removed or any_conversion_factor_changed: + parent.update_prevdoc_status() + parent.update_requested_qty() parent.update_ordered_qty() parent.update_ordered_and_reserved_qty() diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 5a6c87c2169..efeedc14db6 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -530,8 +530,6 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): set_missing_values, ) - doclist.set_onload("ignore_price_list", True) - return doclist diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 6faddd2a8be..34d3e700ccc 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -789,6 +789,23 @@ class SubcontractingController(StockController): return self._sub_contracted_items + def update_requested_qty(self): + material_request_map = {} + for d in self.get("items"): + if d.material_request_item: + material_request_map.setdefault(d.material_request, []).append(d.material_request_item) + + for mr, mr_item_rows in material_request_map.items(): + if mr and mr_item_rows: + mr_obj = frappe.get_doc("Material Request", mr) + + if mr_obj.status in ["Stopped", "Cancelled"]: + frappe.throw( + _("Material Request {0} is cancelled or stopped").format(mr), frappe.InvalidStatusError + ) + + mr_obj.update_requested_qty(mr_item_rows) + def get_item_details(items): item = frappe.qb.DocType("Item") diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py index 4ea4fd11b4e..68af142afcb 100644 --- a/erpnext/controllers/tests/test_subcontracting_controller.py +++ b/erpnext/controllers/tests/test_subcontracting_controller.py @@ -938,6 +938,7 @@ def make_subcontracted_items(): "Subcontracted Item SA5": {}, "Subcontracted Item SA6": {}, "Subcontracted Item SA7": {}, + "Subcontracted Item SA8": {}, } for item, properties in sub_contracted_items.items(): @@ -957,6 +958,7 @@ def make_raw_materials(): }, "Subcontracted SRM Item 4": {"has_serial_no": 1, "serial_no_series": "SRII.####"}, "Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRII.####"}, + "Subcontracted SRM Item 8": {}, } for item, properties in raw_materials.items(): @@ -980,6 +982,7 @@ def make_service_items(): "Subcontracted Service Item 5": {}, "Subcontracted Service Item 6": {}, "Subcontracted Service Item 7": {}, + "Subcontracted Service Item 8": {}, } for item, properties in service_items.items(): @@ -1003,6 +1006,7 @@ def make_bom_for_subcontracted_items(): "Subcontracted Item SA5": ["Subcontracted SRM Item 5"], "Subcontracted Item SA6": ["Subcontracted SRM Item 3"], "Subcontracted Item SA7": ["Subcontracted SRM Item 1"], + "Subcontracted Item SA8": ["Subcontracted SRM Item 8"], } for item_code, raw_materials in boms.items(): diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index deddd17234e..2d5b3573ae4 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -40,7 +40,7 @@ class Lead(SellingController, CRMNote): if self.source == "Existing Customer" and self.customer: contact = frappe.db.get_value( "Dynamic Link", - {"link_doctype": "Customer", "link_name": self.customer}, + {"link_doctype": "Customer", "parenttype": "Contact", "link_name": self.customer}, "parent", ) if contact: diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 8d593d12963..6151af7e3ef 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1306,7 +1306,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): order_by = "idx desc, name, item_name" - fields = ["name", "item_group", "item_name", "description"] + fields = ["name", "item_name", "item_group", "description"] fields.extend( [field for field in searchfields if not field in ["name", "item_group", "description"]] ) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index a5779fb299c..455aa7e5766 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -213,29 +213,27 @@ class JobCard(Document): production_capacity = 1 query = query.where(jctl.employee == args.get("employee")) - existing = query.run(as_dict=True) + existing_time_logs = query.run(as_dict=True) - overlap_count = self.get_overlap_count(existing) - if existing and production_capacity > overlap_count: - return + if not self.has_overlap(production_capacity, existing_time_logs): + return {} if self.workstation_type: - if workstation := self.get_workstation_based_on_available_slot(existing): + if workstation := self.get_workstation_based_on_available_slot(existing_time_logs): self.workstation = workstation return None - return existing[0] if existing else None + return existing_time_logs[0] if existing_time_logs else None - @staticmethod - def get_overlap_count(time_logs): - count = 1 + def has_overlap(self, production_capacity, time_logs): + overlap = False + if production_capacity == 1 and len(time_logs) > 0: + return True # Check overlap exists or not between the overlapping time logs with the current Job Card - for idx, row in enumerate(time_logs): - next_idx = idx - if idx + 1 < len(time_logs): - next_idx = idx + 1 - next_row = time_logs[next_idx] + for row in time_logs: + count = 1 + for next_row in time_logs: if row.name == next_row.name: continue @@ -255,7 +253,10 @@ class JobCard(Document): ): count += 1 - return count + if count > production_capacity: + return True + + return overlap def get_workstation_based_on_available_slot(self, existing) -> Optional[str]: workstations = get_workstations(self.workstation_type) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 8d12ba92103..7778f060146 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1521,19 +1521,23 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company): ) locations = get_available_item_locations( - item.get("item_code"), warehouses, item.get("quantity"), company, ignore_validation=True + item.get("item_code"), + warehouses, + item.get("quantity") * item.get("conversion_factor"), + company, + ignore_validation=True, ) required_qty = item.get("quantity") + if item.get("conversion_factor") and item.get("purchase_uom") != item.get("stock_uom"): + # Convert qty to stock UOM + required_qty = required_qty * item.get("conversion_factor") + # get available material by transferring to production warehouse for d in locations: if required_qty <= 0: return - conversion_factor = 1.0 - if purchase_uom != stock_uom and purchase_uom == item["uom"]: - conversion_factor = get_uom_conversion_factor(item["item_code"], item["uom"]) - new_dict = copy.deepcopy(item) quantity = required_qty if d.get("qty") > required_qty else d.get("qty") @@ -1543,10 +1547,11 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company): "material_request_type": "Material Transfer", "uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM "from_warehouse": d.get("warehouse"), + "conversion_factor": 1.0, } ) - required_qty -= quantity / conversion_factor + required_qty -= quantity new_mr_items.append(new_dict) # raise purchase request for remaining qty @@ -1558,7 +1563,7 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company): if frappe.db.get_value("UOM", purchase_uom, "must_be_whole_number"): required_qty = ceil(required_qty) - item["quantity"] = required_qty + item["quantity"] = required_qty / item.get("conversion_factor") new_mr_items.append(item) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 4ad7d06c707..6a50a10d07a 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1272,12 +1272,14 @@ class TestProductionPlan(FrappeTestCase): for row in items: row = frappe._dict(row) if row.material_request_type == "Material Transfer": + self.assertTrue(row.uom == row.stock_uom) self.assertTrue(row.from_warehouse in [wh1, wh2]) self.assertEqual(row.quantity, 2) if row.material_request_type == "Purchase": + self.assertTrue(row.uom != row.stock_uom) self.assertTrue(row.warehouse == mrp_warhouse) - self.assertEqual(row.quantity, 12) + self.assertEqual(row.quantity, 12.0) def test_mr_qty_for_same_rm_with_different_sub_assemblies(self): from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom @@ -1393,6 +1395,58 @@ class TestProductionPlan(FrappeTestCase): self.assertEqual(after_qty, before_qty) + def test_material_request_qty_purchase_and_material_transfer(self): + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name + bom_item = make_item( + properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1", "purchase_uom": "Nos"} + ).name + + store_warehouse = create_warehouse("Store Warehouse", company="_Test Company") + rm_warehouse = create_warehouse("RM Warehouse", company="_Test Company") + + make_stock_entry( + item_code=bom_item, + qty=60, + target=store_warehouse, + rate=99, + ) + + if not frappe.db.exists("UOM Conversion Detail", {"parent": bom_item, "uom": "Nos"}): + doc = frappe.get_doc("Item", bom_item) + doc.append("uoms", {"uom": "Nos", "conversion_factor": 10}) + doc.save() + + make_bom(item=fg_item, raw_materials=[bom_item], source_warehouse="_Test Warehouse - _TC") + + pln = create_production_plan( + item_code=fg_item, planned_qty=10, stock_uom="_Test UOM 1", do_not_submit=1 + ) + + pln.for_warehouse = rm_warehouse + items = get_items_for_material_requests( + pln.as_dict(), warehouses=[{"warehouse": store_warehouse}] + ) + + for row in items: + self.assertEqual(row.get("quantity"), 10.0) + self.assertEqual(row.get("material_request_type"), "Material Transfer") + self.assertEqual(row.get("uom"), "_Test UOM 1") + self.assertEqual(row.get("from_warehouse"), store_warehouse) + self.assertEqual(row.get("conversion_factor"), 1.0) + + items = get_items_for_material_requests( + pln.as_dict(), warehouses=[{"warehouse": pln.for_warehouse}] + ) + + for row in items: + self.assertEqual(row.get("quantity"), 1.0) + self.assertEqual(row.get("material_request_type"), "Purchase") + self.assertEqual(row.get("uom"), "Nos") + self.assertEqual(row.get("conversion_factor"), 10.0) + def create_production_plan(**args): """ diff --git a/erpnext/patches.txt b/erpnext/patches.txt index b8cda748509..279610e56c6 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -270,6 +270,7 @@ erpnext.patches.v13_0.reset_corrupt_defaults erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair erpnext.patches.v14_0.update_reference_due_date_in_journal_entry erpnext.patches.v14_0.france_depreciation_warning +erpnext.patches.v14_0.clear_reconciliation_values_from_singles [post_model_sync] execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings') @@ -344,13 +345,12 @@ erpnext.patches.v14_0.migrate_deferred_accounts_to_item_defaults erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item erpnext.patches.v14_0.rename_over_order_allowance_field erpnext.patches.v14_0.migrate_delivery_stop_lock_field -execute:frappe.db.set_single_value("Payment Reconciliation", "invoice_limit", 50) -execute:frappe.db.set_single_value("Payment Reconciliation", "payment_limit", 50) erpnext.patches.v14_0.rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month erpnext.patches.v14_0.rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based erpnext.patches.v14_0.add_default_for_repost_settings erpnext.patches.v14_0.create_accounting_dimensions_in_supplier_quotation erpnext.patches.v14_0.update_zero_asset_quantity_field execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction") +erpnext.patches.v14_0.clear_reconciliation_values_from_singles # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger diff --git a/erpnext/patches/v14_0/clear_reconciliation_values_from_singles.py b/erpnext/patches/v14_0/clear_reconciliation_values_from_singles.py new file mode 100644 index 00000000000..c1f5b60a406 --- /dev/null +++ b/erpnext/patches/v14_0/clear_reconciliation_values_from_singles.py @@ -0,0 +1,17 @@ +from frappe import qb + + +def execute(): + """ + Clear `tabSingles` and Payment Reconciliation tables of values + """ + singles = qb.DocType("Singles") + qb.from_(singles).delete().where(singles.doctype == "Payment Reconciliation").run() + doctypes = [ + "Payment Reconciliation Invoice", + "Payment Reconciliation Payment", + "Payment Reconciliation Allocation", + ] + for x in doctypes: + dt = qb.DocType(x) + qb.from_(dt).delete().run() diff --git a/erpnext/public/js/communication.js b/erpnext/public/js/communication.js index 7ce8b0913c3..f205d889658 100644 --- a/erpnext/public/js/communication.js +++ b/erpnext/public/js/communication.js @@ -13,7 +13,7 @@ frappe.ui.form.on("Communication", { frappe.confirm(__(confirm_msg, [__("Issue")]), () => { frm.trigger('make_issue_from_communication'); }) - }, "Create"); + }, __("Create")); } if(!in_list(["Lead", "Opportunity"], frm.doc.reference_doctype)) { diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index fe24b18098a..050b9dcd3db 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -328,7 +328,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe onload_post_render() { if(this.frm.doc.__islocal && !(this.frm.doc.taxes || []).length - && !(this.frm.doc.__onload ? this.frm.doc.__onload.load_after_mapping : false)) { + && !this.frm.doc.__onload?.load_after_mapping) { frappe.after_ajax(() => this.apply_default_taxes()); } else if(this.frm.doc.__islocal && this.frm.doc.company && this.frm.doc["items"] && !this.frm.doc.is_pos) { @@ -918,9 +918,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe let me = this; this.set_dynamic_labels(); let company_currency = this.get_company_currency(); - // Added `ignore_price_list` to determine if document is loading after mapping from another doc + // Added `load_after_mapping` to determine if document is loading after mapping from another doc if(this.frm.doc.currency && this.frm.doc.currency !== company_currency - && !(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list)) { + && !this.frm.doc.__onload?.load_after_mapping) { this.get_exchange_rate(transaction_date, this.frm.doc.currency, company_currency, function(exchange_rate) { @@ -952,7 +952,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } if(flt(this.frm.doc.conversion_rate)>0.0) { - if(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list) { + if(this.frm.doc.__onload?.load_after_mapping) { this.calculate_taxes_and_totals(); } else if (!this.in_apply_price_list){ this.apply_price_list(); @@ -1039,9 +1039,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe this.set_dynamic_labels(); var company_currency = this.get_company_currency(); - // Added `ignore_price_list` to determine if document is loading after mapping from another doc + // Added `load_after_mapping` to determine if document is loading after mapping from another doc if(this.frm.doc.price_list_currency !== company_currency && - !(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list)) { + !this.frm.doc.__onload?.load_after_mapping) { this.get_exchange_rate(this.frm.doc.posting_date, this.frm.doc.price_list_currency, company_currency, function(exchange_rate) { me.frm.set_value("plc_conversion_rate", exchange_rate); @@ -1420,7 +1420,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } // Target doc created from a mapped doc - if (this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list) { + if (this.frm.doc.__onload?.load_after_mapping) { // Calculate totals even though pricing rule is not applied. // `apply_pricing_rule` is triggered due to change in data which most likely contributes to Total. if (calculate_taxes_and_totals) me.calculate_taxes_and_totals(); diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js new file mode 100644 index 00000000000..5514963c966 --- /dev/null +++ b/erpnext/public/js/utils/sales_common.js @@ -0,0 +1,431 @@ +// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// License: GNU General Public License v3. See license.txt + +frappe.provide("erpnext.selling"); + +erpnext.sales_common = { + setup_selling_controller:function() { + erpnext.selling.SellingController = class SellingController extends erpnext.TransactionController { + setup() { + super.setup(); + this.toggle_enable_for_stock_uom("allow_to_edit_stock_uom_qty_for_sales"); + this.frm.email_field = "contact_email"; + } + + onload() { + super.onload(); + this.setup_queries(); + this.frm.set_query('shipping_rule', function() { + return { + filters: { + "shipping_rule_type": "Selling" + } + }; + }); + } + + setup_queries() { + var me = this; + + $.each([["customer", "customer"], + ["lead", "lead"]], + function(i, opts) { + if(me.frm.fields_dict[opts[0]]) + me.frm.set_query(opts[0], erpnext.queries[opts[1]]); + }); + + me.frm.set_query('contact_person', erpnext.queries.contact_query); + me.frm.set_query('customer_address', erpnext.queries.address_query); + me.frm.set_query('shipping_address_name', erpnext.queries.address_query); + me.frm.set_query('dispatch_address_name', erpnext.queries.dispatch_address_query); + + erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype); + + if(this.frm.fields_dict.selling_price_list) { + this.frm.set_query("selling_price_list", function() { + return { filters: { selling: 1 } }; + }); + } + + if(this.frm.fields_dict.tc_name) { + this.frm.set_query("tc_name", function() { + return { filters: { selling: 1 } }; + }); + } + + if(!this.frm.fields_dict["items"]) { + return; + } + + if(this.frm.fields_dict["items"].grid.get_field('item_code')) { + this.frm.set_query("item_code", "items", function() { + return { + query: "erpnext.controllers.queries.item_query", + filters: {'is_sales_item': 1, 'customer': me.frm.doc.customer, 'has_variants': 0} + } + }); + } + + if(this.frm.fields_dict["packed_items"] && + this.frm.fields_dict["packed_items"].grid.get_field('batch_no')) { + this.frm.set_query("batch_no", "packed_items", function(doc, cdt, cdn) { + return me.set_query_for_batch(doc, cdt, cdn) + }); + } + + if(this.frm.fields_dict["items"].grid.get_field('item_code')) { + this.frm.set_query("item_tax_template", "items", function(doc, cdt, cdn) { + return me.set_query_for_item_tax_template(doc, cdt, cdn) + }); + } + + } + + refresh() { + super.refresh(); + + frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer'} + + this.frm.toggle_display("customer_name", + (this.frm.doc.customer_name && this.frm.doc.customer_name!==this.frm.doc.customer)); + + this.toggle_editable_price_list_rate(); + } + + customer() { + var me = this; + erpnext.utils.get_party_details(this.frm, null, null, function() { + me.apply_price_list(); + }); + } + + customer_address() { + erpnext.utils.get_address_display(this.frm, "customer_address"); + erpnext.utils.set_taxes_from_address(this.frm, "customer_address", "customer_address", "shipping_address_name"); + } + + shipping_address_name() { + erpnext.utils.get_address_display(this.frm, "shipping_address_name", "shipping_address"); + erpnext.utils.set_taxes_from_address(this.frm, "shipping_address_name", "customer_address", "shipping_address_name"); + } + + dispatch_address_name() { + erpnext.utils.get_address_display(this.frm, "dispatch_address_name", "dispatch_address"); + } + + sales_partner() { + this.apply_pricing_rule(); + } + + campaign() { + this.apply_pricing_rule(); + } + + selling_price_list() { + this.apply_price_list(); + this.set_dynamic_labels(); + } + + discount_percentage(doc, cdt, cdn) { + var item = frappe.get_doc(cdt, cdn); + item.discount_amount = 0.0; + this.apply_discount_on_item(doc, cdt, cdn, 'discount_percentage'); + } + + discount_amount(doc, cdt, cdn) { + + if(doc.name === cdn) { + return; + } + + var item = frappe.get_doc(cdt, cdn); + item.discount_percentage = 0.0; + this.apply_discount_on_item(doc, cdt, cdn, 'discount_amount'); + } + + commission_rate() { + this.calculate_commission(); + } + + total_commission() { + frappe.model.round_floats_in(this.frm.doc, ["amount_eligible_for_commission", "total_commission"]); + + const { amount_eligible_for_commission } = this.frm.doc; + if(!amount_eligible_for_commission) return; + + this.frm.set_value( + "commission_rate", flt( + this.frm.doc.total_commission * 100.0 / amount_eligible_for_commission + ) + ); + } + + allocated_percentage(doc, cdt, cdn) { + var sales_person = frappe.get_doc(cdt, cdn); + if(sales_person.allocated_percentage) { + + sales_person.allocated_percentage = flt(sales_person.allocated_percentage, + precision("allocated_percentage", sales_person)); + + sales_person.allocated_amount = flt(this.frm.doc.amount_eligible_for_commission * + sales_person.allocated_percentage / 100.0, + precision("allocated_amount", sales_person)); + refresh_field(["allocated_amount"], sales_person); + + this.calculate_incentive(sales_person); + refresh_field(["allocated_percentage", "allocated_amount", "commission_rate","incentives"], sales_person.name, + sales_person.parentfield); + } + } + + sales_person(doc, cdt, cdn) { + var row = frappe.get_doc(cdt, cdn); + this.calculate_incentive(row); + refresh_field("incentives",row.name,row.parentfield); + } + + toggle_editable_price_list_rate() { + var df = frappe.meta.get_docfield(this.frm.doc.doctype + " Item", "price_list_rate", this.frm.doc.name); + var editable_price_list_rate = cint(frappe.defaults.get_default("editable_price_list_rate")); + + if(df && editable_price_list_rate) { + const parent_field = frappe.meta.get_parentfield(this.frm.doc.doctype, this.frm.doc.doctype + " Item"); + if (!this.frm.fields_dict[parent_field]) return; + + this.frm.fields_dict[parent_field].grid.update_docfield_property( + 'price_list_rate', 'read_only', 0 + ); + } + } + + calculate_commission() { + if(!this.frm.fields_dict.commission_rate || this.frm.doc.docstatus === 1) return; + + if(this.frm.doc.commission_rate > 100) { + this.frm.set_value("commission_rate", 100); + frappe.throw(`${__(frappe.meta.get_label( + this.frm.doc.doctype, "commission_rate", this.frm.doc.name + ))} ${__("cannot be greater than 100")}`); + } + + this.frm.doc.amount_eligible_for_commission = this.frm.doc.items.reduce( + (sum, item) => item.grant_commission ? sum + item.base_net_amount : sum, 0 + ) + + this.frm.doc.total_commission = flt( + this.frm.doc.amount_eligible_for_commission * this.frm.doc.commission_rate / 100.0, + precision("total_commission") + ); + + refresh_field(["amount_eligible_for_commission", "total_commission"]); + } + + calculate_contribution() { + var me = this; + $.each(this.frm.doc.doctype.sales_team || [], function(i, sales_person) { + frappe.model.round_floats_in(sales_person); + if (!sales_person.allocated_percentage) return; + + sales_person.allocated_amount = flt( + me.frm.doc.amount_eligible_for_commission + * sales_person.allocated_percentage + / 100.0, + precision("allocated_amount", sales_person) + ); + }); + } + + calculate_incentive(row) { + if(row.allocated_amount) + { + row.incentives = flt( + row.allocated_amount * row.commission_rate / 100.0, + precision("incentives", row)); + } + } + + set_dynamic_labels() { + super.set_dynamic_labels(); + this.set_product_bundle_help(this.frm.doc); + } + + set_product_bundle_help(doc) { + if(!this.frm.fields_dict.packing_list) return; + if ((doc.packed_items || []).length) { + $(this.frm.fields_dict.packing_list.row.wrapper).toggle(true); + + if (in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) { + var help_msg = "
" + + __("For 'Product Bundle' items, Warehouse, Serial No and Batch No will be considered from the 'Packing List' table. If Warehouse and Batch No are same for all packing items for any 'Product Bundle' item, those values can be entered in the main Item table, values will be copied to 'Packing List' table.")+ + "
"; + frappe.meta.get_docfield(doc.doctype, 'product_bundle_help', doc.name).options = help_msg; + } + } else { + $(this.frm.fields_dict.packing_list.row.wrapper).toggle(false); + if (in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) { + frappe.meta.get_docfield(doc.doctype, 'product_bundle_help', doc.name).options = ''; + } + } + refresh_field('product_bundle_help'); + } + + company_address() { + var me = this; + if(this.frm.doc.company_address) { + frappe.call({ + method: "frappe.contacts.doctype.address.address.get_address_display", + args: {"address_dict": this.frm.doc.company_address }, + callback: function(r) { + if(r.message) { + me.frm.set_value("company_address_display", r.message) + } + } + }) + } else { + this.frm.set_value("company_address_display", ""); + } + } + + conversion_factor(doc, cdt, cdn, dont_fetch_price_list_rate) { + super.conversion_factor(doc, cdt, cdn, dont_fetch_price_list_rate); + } + + qty(doc, cdt, cdn) { + super.qty(doc, cdt, cdn); + } + + pick_serial_and_batch(doc, cdt, cdn) { + let item = locals[cdt][cdn]; + let me = this; + let path = "assets/erpnext/js/utils/serial_no_batch_selector.js"; + + frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]) + .then((r) => { + if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) { + item.has_serial_no = r.message.has_serial_no; + item.has_batch_no = r.message.has_batch_no; + item.type_of_transaction = item.qty > 0 ? "Outward":"Inward"; + + item.title = item.has_serial_no ? + __("Select Serial No") : __("Select Batch No"); + + if (item.has_serial_no && item.has_batch_no) { + item.title = __("Select Serial and Batch"); + } + + frappe.require(path, function() { + new erpnext.SerialBatchPackageSelector( + me.frm, item, (r) => { + if (r) { + frappe.model.set_value(item.doctype, item.name, { + "serial_and_batch_bundle": r.name, + "qty": Math.abs(r.total_qty) + }); + } + } + ); + }); + } + }); + } + + update_auto_repeat_reference(doc) { + if (doc.auto_repeat) { + frappe.call({ + method:"frappe.automation.doctype.auto_repeat.auto_repeat.update_reference", + args:{ + docname: doc.auto_repeat, + reference:doc.name + }, + callback: function(r){ + if (r.message=="success") { + frappe.show_alert({message:__("Auto repeat document updated"), indicator:'green'}); + } else { + frappe.show_alert({message:__("An error occurred during the update process"), indicator:'red'}); + } + } + }) + } + } + + project() { + let me = this; + if(in_list(["Delivery Note", "Sales Invoice", "Sales Order"], this.frm.doc.doctype)) { + if(this.frm.doc.project) { + frappe.call({ + method:'erpnext.projects.doctype.project.project.get_cost_center_name' , + args: {project: this.frm.doc.project}, + callback: function(r, rt) { + if(!r.exc) { + $.each(me.frm.doc["items"] || [], function(i, row) { + if(r.message) { + frappe.model.set_value(row.doctype, row.name, "cost_center", r.message); + frappe.msgprint(__("Cost Center For Item with Item Code {0} has been Changed to {1}", [row.item_name, r.message])); + } + }) + } + } + }) + } + } + } + + coupon_code() { + this.frm.set_value("discount_amount", 0); + this.frm.set_value("additional_discount_percentage", 0); + } + }; + } +} + +erpnext.pre_sales = { + set_as_lost: function(doctype) { + frappe.ui.form.on(doctype, { + set_as_lost_dialog: function(frm) { + var dialog = new frappe.ui.Dialog({ + title: __("Set as Lost"), + fields: [ + { + "fieldtype": "Table MultiSelect", + "label": __("Lost Reasons"), + "fieldname": "lost_reason", + "options": frm.doctype === 'Opportunity' ? 'Opportunity Lost Reason Detail': 'Quotation Lost Reason Detail', + "reqd": 1 + }, + { + "fieldtype": "Table MultiSelect", + "label": __("Competitors"), + "fieldname": "competitors", + "options": "Competitor Detail" + }, + { + "fieldtype": "Small Text", + "label": __("Detailed Reason"), + "fieldname": "detailed_reason" + }, + ], + primary_action: function() { + let values = dialog.get_values(); + + frm.call({ + doc: frm.doc, + method: 'declare_enquiry_lost', + args: { + 'lost_reasons_list': values.lost_reason, + 'competitors': values.competitors ? values.competitors : [], + 'detailed_reason': values.detailed_reason + }, + callback: function(r) { + dialog.hide(); + frm.reload_doc(); + }, + }); + }, + primary_action_label: __('Declare Lost') + }); + + dialog.show(); + } + }); + } +} \ No newline at end of file diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 87e5c5d0eec..c3c7766b9f5 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -651,7 +651,7 @@ "search_fields": "customer_name,customer_group,territory, mobile_no,primary_address", "show_name_in_global_search": 1, "sort_field": "modified", - "sort_order": "ASC", + "sort_order": "DESC", "states": [], "title_field": "customer_name", "track_changes": 1 diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 20d6268d739..de3c21e6eb8 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -582,7 +582,8 @@ def get_customer_outstanding( """ select sum(debit) - sum(credit) from `tabGL Entry` where party_type = 'Customer' - and party = %s and company=%s {0}""".format( + and is_cancelled = 0 and party = %s + and company=%s {0}""".format( cond ), (customer, company), diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 8ff681b0481..88e24ad542a 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -352,9 +352,6 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): ignore_permissions=ignore_permissions, ) - # postprocess: fetch shipping address, set missing values - doclist.set_onload("ignore_price_list", True) - return doclist @@ -423,8 +420,6 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): ignore_permissions=ignore_permissions, ) - doclist.set_onload("ignore_price_list", True) - return doclist diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index c7c7e526f4c..cbc11ed02af 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -716,8 +716,6 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): target_doc = get_mapped_doc("Sales Order", source_name, mapper, target_doc, set_missing_values) - target_doc.set_onload("ignore_price_list", True) - return target_doc @@ -806,8 +804,6 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): if automatically_fetch_payment_terms: doclist.set_payment_schedule() - doclist.set_onload("ignore_price_list", True) - return doclist diff --git a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py index cb6e8a1102f..9f3ba0da8bd 100644 --- a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py +++ b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py @@ -3,7 +3,8 @@ import frappe -from frappe import _, msgprint +from frappe import _, msgprint, qb +from frappe.query_builder import Criterion from erpnext import get_company_currency @@ -214,24 +215,33 @@ def get_conditions(filters, date_field): if items: conditions.append("dt_item.item_code in (%s)" % ", ".join(["%s"] * len(items))) values += items + else: + # return empty result, if no items are fetched after filtering on 'item group' and 'brand' + conditions.append("dt_item.item_code = Null") return " and ".join(conditions), values def get_items(filters): + item = qb.DocType("Item") + + item_query_conditions = [] if filters.get("item_group"): - key = "item_group" - elif filters.get("brand"): - key = "brand" - else: - key = "" - - items = [] - if key: - items = frappe.db.sql_list( - """select name from tabItem where %s = %s""" % (key, "%s"), (filters[key]) + # Handle 'Parent' nodes as well. + item_group = qb.DocType("Item Group") + lft, rgt = frappe.db.get_all( + "Item Group", filters={"name": filters.get("item_group")}, fields=["lft", "rgt"], as_list=True + )[0] + item_group_query = ( + qb.from_(item_group) + .select(item_group.name) + .where((item_group.lft >= lft) & (item_group.rgt <= rgt)) ) + item_query_conditions.append(item.item_group.isin(item_group_query)) + if filters.get("brand"): + item_query_conditions.append(item.brand == filters.get("brand")) + items = qb.from_(item).select(item.name).where(Criterion.all(item_query_conditions)).run() return items diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 571e0ea5d7a..d9d9a52482a 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -665,8 +665,6 @@ def make_sales_invoice(source_name, target_doc=None): if automatically_fetch_payment_terms: doc.set_payment_schedule() - doc.set_onload("ignore_price_list", True) - return doc diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index bb3cbd2fa6a..c12048077e7 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1057,7 +1057,6 @@ def make_purchase_invoice(source_name, target_doc=None): set_missing_values, ) - doclist.set_onload("ignore_price_list", True) return doclist 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 749dbf2111f..6d89e9897af 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -352,6 +352,7 @@ "oldfieldtype": "Currency", "options": "currency", "print_width": "100px", + "read_only_depends_on": "eval: (!parent.is_return && doc.purchase_order && doc.purchase_order_item)", "width": "100px" }, { @@ -1054,7 +1055,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:38:15.251994", + "modified": "2023-11-30 16:12:02.364608", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 6dd0a58645c..70771e77461 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -241,7 +241,7 @@ frappe.ui.form.on('Stock Entry', { } } - if (frm.doc.docstatus===0) { + if (frm.doc.docstatus === 0) { frm.add_custom_button(__('Purchase Invoice'), function() { erpnext.utils.map_current_doc({ method: "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_stock_entry", @@ -294,7 +294,8 @@ frappe.ui.form.on('Stock Entry', { }) }, __("Get Items From")); } - if (frm.doc.docstatus===0 && frm.doc.purpose == "Material Issue") { + + if (frm.doc.docstatus === 0 && frm.doc.purpose == "Material Issue") { frm.add_custom_button(__('Expired Batches'), function() { frappe.call({ method: "erpnext.stock.doctype.stock_entry.stock_entry.get_expired_batch_items", @@ -379,6 +380,10 @@ frappe.ui.form.on('Stock Entry', { frm.remove_custom_button('Bill of Materials', "Get Items From"); frm.events.show_bom_custom_button(frm); frm.trigger('add_to_transit'); + + frm.fields_dict.items.grid.update_docfield_property( + 'basic_rate', 'read_only', frm.doc.purpose == "Material Receipt" ? 0 : 1 + ); }, purpose: function(frm) { diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.js b/erpnext/stock/report/stock_analytics/stock_analytics.js index 78afe6d2642..071bfa22959 100644 --- a/erpnext/stock/report/stock_analytics/stock_analytics.js +++ b/erpnext/stock/report/stock_analytics/stock_analytics.js @@ -17,6 +17,7 @@ frappe.query_reports["Stock Analytics"] = { fieldtype: "Link", options:"Item", default: "", + get_query: () => ({filters: { 'is_stock_item': 1 }}), }, { fieldname: "value_quantity", diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.py b/erpnext/stock/report/stock_analytics/stock_analytics.py index 6c5b58c6e45..ab48181c48d 100644 --- a/erpnext/stock/report/stock_analytics/stock_analytics.py +++ b/erpnext/stock/report/stock_analytics/stock_analytics.py @@ -270,7 +270,7 @@ def get_items(filters): if item_code := filters.get("item_code"): return [item_code] else: - item_filters = {} + item_filters = {"is_stock_item": 1} if item_group := filters.get("item_group"): children = get_descendants_of("Item Group", item_group, ignore_permissions=True) item_filters["item_group"] = ("in", children + [item_group]) diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js index b1e4a74571e..bf3a397feef 100644 --- a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js +++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js @@ -13,10 +13,18 @@ const DIFFERENCE_FIELD_NAMES = [ frappe.query_reports["Stock Ledger Variance"] = { "filters": [ + { + "fieldname": "company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "reqd": 1, + "default": frappe.defaults.get_user_default("Company") + }, { "fieldname": "item_code", "fieldtype": "Link", - "label": "Item", + "label": __("Item"), "options": "Item", get_query: function() { return { @@ -27,7 +35,7 @@ frappe.query_reports["Stock Ledger Variance"] = { { "fieldname": "warehouse", "fieldtype": "Link", - "label": "Warehouse", + "label": __("Warehouse"), "options": "Warehouse", get_query: function() { return { @@ -38,7 +46,7 @@ frappe.query_reports["Stock Ledger Variance"] = { { "fieldname": "difference_in", "fieldtype": "Select", - "label": "Difference In", + "label": __("Difference In"), "options": [ "", "Qty", @@ -49,7 +57,7 @@ frappe.query_reports["Stock Ledger Variance"] = { { "fieldname": "include_disabled", "fieldtype": "Check", - "label": "Include Disabled", + "label": __("Include Disabled"), } ], diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py index 732f108ac41..189a90aa471 100644 --- a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py +++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py @@ -55,6 +55,11 @@ def get_columns(): "label": _("Warehouse"), "options": "Warehouse", }, + { + "fieldname": "valuation_method", + "fieldtype": "Data", + "label": _("Valuation Method"), + }, { "fieldname": "voucher_type", "fieldtype": "Link", @@ -194,6 +199,7 @@ def get_columns(): def get_data(filters=None): filters = frappe._dict(filters or {}) item_warehouse_map = get_item_warehouse_combinations(filters) + valuation_method = frappe.db.get_single_value("Stock Settings", "valuation_method") data = [] if item_warehouse_map: @@ -206,8 +212,17 @@ def get_data(filters=None): continue for row in report_data: - if has_difference(row, precision, filters.difference_in): - data.append(add_item_warehouse_details(row, item_warehouse)) + if has_difference( + row, precision, filters.difference_in, item_warehouse.valuation_method or valuation_method + ): + row.update( + { + "item_code": item_warehouse.item_code, + "warehouse": item_warehouse.warehouse, + "valuation_method": item_warehouse.valuation_method or valuation_method, + } + ) + data.append(row) break return data @@ -229,8 +244,14 @@ def get_item_warehouse_combinations(filters: dict = None) -> dict: .select( bin.item_code, bin.warehouse, + item.valuation_method, + ) + .where( + (item.is_stock_item == 1) + & (item.has_serial_no == 0) + & (warehouse.is_group == 0) + & (warehouse.company == filters.company) ) - .where((item.is_stock_item == 1) & (item.has_serial_no == 0) & (warehouse.is_group == 0)) ) if filters.item_code: @@ -243,37 +264,27 @@ def get_item_warehouse_combinations(filters: dict = None) -> dict: return query.run(as_dict=1) -def has_difference(row, precision, difference_in): - has_qty_difference = flt(row.difference_in_qty, precision) or flt(row.fifo_qty_diff, precision) - has_value_difference = ( - flt(row.diff_value_diff, precision) - or flt(row.fifo_value_diff, precision) - or flt(row.fifo_difference_diff, precision) - ) - has_valuation_difference = flt(row.valuation_diff, precision) or flt( - row.fifo_valuation_diff, precision - ) +def has_difference(row, precision, difference_in, valuation_method): + if valuation_method == "Moving Average": + qty_diff = flt(row.difference_in_qty, precision) + value_diff = flt(row.diff_value_diff, precision) + valuation_diff = flt(row.valuation_diff, precision) + else: + qty_diff = flt(row.difference_in_qty, precision) or flt(row.fifo_qty_diff, precision) + value_diff = ( + flt(row.diff_value_diff, precision) + or flt(row.fifo_value_diff, precision) + or flt(row.fifo_difference_diff, precision) + ) + valuation_diff = flt(row.valuation_diff, precision) or flt(row.fifo_valuation_diff, precision) - if difference_in == "Qty" and has_qty_difference: + if difference_in == "Qty" and qty_diff: return True - elif difference_in == "Value" and has_value_difference: + elif difference_in == "Value" and value_diff: return True - elif difference_in == "Valuation" and has_valuation_difference: + elif difference_in == "Valuation" and valuation_diff: return True elif difference_in not in ["Qty", "Value", "Valuation"] and ( - has_qty_difference or has_value_difference or has_valuation_difference + qty_diff or value_diff or valuation_diff ): return True - - return False - - -def add_item_warehouse_details(row, item_warehouse): - row.update( - { - "item_code": item_warehouse.item_code, - "warehouse": item_warehouse.warehouse, - } - ) - - return row diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 0b14d4d9f50..c0064996099 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -8,11 +8,28 @@ from frappe.utils import flt from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created from erpnext.controllers.subcontracting_controller import SubcontractingController -from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty +from erpnext.stock.stock_balance import update_bin_qty from erpnext.stock.utils import get_bin class SubcontractingOrder(SubcontractingController): + def __init__(self, *args, **kwargs): + super(SubcontractingOrder, self).__init__(*args, **kwargs) + + self.status_updater = [ + { + "source_dt": "Subcontracting Order Item", + "target_dt": "Material Request Item", + "join_field": "material_request_item", + "target_field": "ordered_qty", + "target_parent_dt": "Material Request", + "target_parent_field": "per_ordered", + "target_ref_field": "stock_qty", + "source_field": "qty", + "percent_join_field": "material_request", + } + ] + def before_validate(self): super(SubcontractingOrder, self).before_validate() @@ -26,11 +43,15 @@ class SubcontractingOrder(SubcontractingController): self.reset_default_field_value("set_warehouse", "items", "warehouse") def on_submit(self): + self.update_prevdoc_status() + self.update_requested_qty() self.update_ordered_qty_for_subcontracting() self.update_reserved_qty_for_subcontracting() self.update_status() def on_cancel(self): + self.update_prevdoc_status() + self.update_requested_qty() self.update_ordered_qty_for_subcontracting() self.update_reserved_qty_for_subcontracting() self.update_status() @@ -114,7 +135,32 @@ class SubcontractingOrder(SubcontractingController): ): item_wh_list.append([item.item_code, item.warehouse]) for item_code, warehouse in item_wh_list: - update_bin_qty(item_code, warehouse, {"ordered_qty": get_ordered_qty(item_code, warehouse)}) + update_bin_qty( + item_code, warehouse, {"ordered_qty": self.get_ordered_qty(item_code, warehouse)} + ) + + @staticmethod + def get_ordered_qty(item_code, warehouse): + table = frappe.qb.DocType("Subcontracting Order") + child = frappe.qb.DocType("Subcontracting Order Item") + + query = ( + frappe.qb.from_(table) + .inner_join(child) + .on(table.name == child.parent) + .select((child.qty - child.received_qty) * child.conversion_factor) + .where( + (table.docstatus == 1) + & (child.item_code == item_code) + & (child.warehouse == warehouse) + & (child.qty > child.received_qty) + & (table.status != "Completed") + ) + ) + + query = query.run() + + return flt(query[0][0]) if query else 0 def update_reserved_qty_for_subcontracting(self): for item in self.supplied_items: @@ -139,7 +185,9 @@ class SubcontractingOrder(SubcontractingController): "qty": si.fg_item_qty, "stock_uom": item.stock_uom, "bom": bom, - }, + "material_request": si.material_request, + "material_request_item": si.material_request_item, + } ) else: frappe.throw( diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py index 6a2983faaaf..fbdea6ddbee 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -6,6 +6,7 @@ from collections import defaultdict import frappe from frappe.tests.utils import FrappeTestCase +from frappe.utils import flt from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_subcontracting_order from erpnext.controllers.subcontracting_controller import ( @@ -566,6 +567,123 @@ class TestSubcontractingOrder(FrappeTestCase): self.assertEqual(sco.status, "Closed") self.assertEqual(sco.supplied_items[0].returned_qty, 5) + def test_ordered_qty_for_subcontracting_order(self): + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 8", + "qty": 10, + "rate": 100, + "fg_item": "Subcontracted Item SA8", + "fg_item_qty": 10, + }, + ] + + ordered_qty = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"}, + fieldname="ordered_qty", + ) + ordered_qty = flt(ordered_qty) + + sco = get_subcontracting_order(service_items=service_items) + sco.reload() + + new_ordered_qty = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"}, + fieldname="ordered_qty", + ) + new_ordered_qty = flt(new_ordered_qty) + + self.assertEqual(ordered_qty + 10, new_ordered_qty) + + for row in sco.supplied_items: + make_stock_entry( + target="_Test Warehouse 1 - _TC", + item_code=row.rm_item_code, + qty=row.required_qty, + basic_rate=100, + ) + + scr = make_subcontracting_receipt(sco.name) + scr.submit() + + new_ordered_qty = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"}, + fieldname="ordered_qty", + ) + + self.assertEqual(ordered_qty, new_ordered_qty) + + scr.reload() + scr.cancel() + + new_ordered_qty = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"}, + fieldname="ordered_qty", + ) + + self.assertEqual(ordered_qty + 10, new_ordered_qty) + + def test_requested_qty_for_subcontracting_order(self): + from erpnext.stock.doctype.material_request.material_request import make_purchase_order + from erpnext.stock.doctype.material_request.test_material_request import make_material_request + + requested_qty = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"}, + fieldname="indented_qty", + ) + requested_qty = flt(requested_qty) + + mr = make_material_request( + item_code="Subcontracted Item SA8", + material_request_type="Purchase", + qty=10, + ) + + self.assertTrue(mr.docstatus == 1) + + new_requested_qty = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"}, + fieldname="indented_qty", + ) + new_requested_qty = flt(new_requested_qty) + + self.assertEqual(requested_qty + 10, new_requested_qty) + + po = make_purchase_order(mr.name) + po.is_subcontracted = 1 + po.supplier = "_Test Supplier" + po.items[0].fg_item = "Subcontracted Item SA8" + po.items[0].fg_item_qty = 10 + po.items[0].item_code = "Subcontracted Service Item 8" + po.items[0].item_name = "Subcontracted Service Item 8" + po.items[0].qty = 10 + po.supplier_warehouse = "_Test Warehouse 1 - _TC" + po.save() + po.submit() + + self.assertTrue(po.items[0].material_request) + self.assertTrue(po.items[0].material_request_item) + + sco = create_subcontracting_order(po_name=po.name) + self.assertTrue(sco.items[0].material_request) + self.assertTrue(sco.items[0].material_request_item) + + new_requested_qty = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"}, + fieldname="indented_qty", + ) + new_requested_qty = flt(new_requested_qty) + + self.assertEqual(requested_qty, new_requested_qty) + def create_subcontracting_order(**args): args = frappe._dict(args) diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json index 46c229bfd37..58b4e32a123 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json @@ -40,6 +40,11 @@ "manufacture_section", "manufacturer", "manufacturer_part_no", + "column_break_impp", + "reference_section", + "material_request", + "column_break_fpyl", + "material_request_item", "accounting_dimensions_section", "cost_center", "dimension_col_break", @@ -332,13 +337,44 @@ "fieldtype": "Link", "label": "Project", "options": "Project" + }, + { + "fieldname": "column_break_impp", + "fieldtype": "Column Break" + }, + { + "fieldname": "material_request", + "fieldtype": "Link", + "label": "Material Request", + "no_copy": 1, + "options": "Material Request", + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "material_request_item", + "fieldtype": "Data", + "label": "Material Request Item", + "no_copy": 1, + "read_only": 1, + "search_index": 1 + }, + { + "collapsible": 1, + "fieldname": "reference_section", + "fieldtype": "Section Break", + "label": "Reference" + }, + { + "fieldname": "column_break_fpyl", + "fieldtype": "Column Break" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:38:37.640677", + "modified": "2023-11-30 15:29:43.744618", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Order Item", @@ -351,4 +387,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json b/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json index f213313ef6b..2fdcdab7fde 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json @@ -1,131 +1,160 @@ { - "actions": [], - "autoname": "hash", - "creation": "2022-04-01 19:23:05.728354", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "item_code", - "column_break_2", - "item_name", - "section_break_4", - "qty", - "column_break_6", - "rate", - "column_break_8", - "amount", - "section_break_10", - "fg_item", - "column_break_12", - "fg_item_qty" - ], - "fields": [ - { - "bold": 1, - "columns": 2, - "fieldname": "item_code", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Item Code", - "options": "Item", - "reqd": 1, - "search_index": 1 - }, - { - "fetch_from": "item_code.item_name", - "fieldname": "item_name", - "fieldtype": "Data", - "in_global_search": 1, - "in_list_view": 1, - "label": "Item Name", - "print_hide": 1, - "reqd": 1 - }, - { - "bold": 1, - "columns": 1, - "fieldname": "qty", - "fieldtype": "Float", - "in_list_view": 1, - "label": "Quantity", - "print_width": "60px", - "reqd": 1, - "width": "60px" - }, - { - "bold": 1, - "columns": 2, - "fetch_from": "item_code.standard_rate", - "fetch_if_empty": 1, - "fieldname": "rate", - "fieldtype": "Currency", - "in_list_view": 1, - "label": "Rate", - "options": "currency", - "reqd": 1 - }, - { - "columns": 2, - "fieldname": "amount", - "fieldtype": "Currency", - "in_list_view": 1, - "label": "Amount", - "options": "currency", - "read_only": 1, - "reqd": 1 - }, - { - "fieldname": "fg_item", - "fieldtype": "Link", - "label": "Finished Good Item", - "options": "Item", - "reqd": 1 - }, - { - "default": "1", - "fieldname": "fg_item_qty", - "fieldtype": "Float", - "label": "Finished Good Item Quantity", - "reqd": 1 - }, - { - "fieldname": "column_break_2", - "fieldtype": "Column Break" - }, - { - "fieldname": "section_break_4", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break_6", - "fieldtype": "Column Break" - }, - { - "fieldname": "column_break_8", - "fieldtype": "Column Break" - }, - { - "fieldname": "section_break_10", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break_12", - "fieldtype": "Column Break" - } - ], - "istable": 1, - "links": [], - "modified": "2022-04-07 11:43:43.094867", - "modified_by": "Administrator", - "module": "Subcontracting", - "name": "Subcontracting Order Service Item", - "naming_rule": "Random", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "search_fields": "item_name", - "sort_field": "modified", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file + "actions": [], + "autoname": "hash", + "creation": "2022-04-01 19:23:05.728354", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "column_break_2", + "item_name", + "section_break_4", + "qty", + "column_break_6", + "rate", + "column_break_8", + "amount", + "section_break_10", + "fg_item", + "column_break_12", + "fg_item_qty", + "section_break_kphn", + "material_request", + "column_break_piqi", + "material_request_item" + ], + "fields": [ + { + "bold": 1, + "columns": 2, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "reqd": 1, + "search_index": 1 + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "in_global_search": 1, + "in_list_view": 1, + "label": "Item Name", + "print_hide": 1, + "reqd": 1 + }, + { + "bold": 1, + "columns": 1, + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Quantity", + "print_width": "60px", + "reqd": 1, + "width": "60px" + }, + { + "bold": 1, + "columns": 2, + "fetch_from": "item_code.standard_rate", + "fetch_if_empty": 1, + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate", + "options": "currency", + "reqd": 1 + }, + { + "columns": 2, + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "currency", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "fg_item", + "fieldtype": "Link", + "label": "Finished Good Item", + "options": "Item", + "reqd": 1 + }, + { + "default": "1", + "fieldname": "fg_item_qty", + "fieldtype": "Float", + "label": "Finished Good Item Quantity", + "reqd": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_10", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, + { + "collapsible": 1, + "fieldname": "section_break_kphn", + "fieldtype": "Section Break", + "label": "Reference" + }, + { + "fieldname": "material_request", + "fieldtype": "Link", + "label": "Material Request", + "no_copy": 1, + "options": "Material Request", + "read_only": 1 + }, + { + "fieldname": "column_break_piqi", + "fieldtype": "Column Break" + }, + { + "fieldname": "material_request_item", + "fieldtype": "Data", + "label": "Material Request Item", + "no_copy": 1, + "read_only": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2023-11-30 13:29:31.017440", + "modified_by": "Administrator", + "module": "Subcontracting", + "name": "Subcontracting Order Service Item", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "search_fields": "item_name", + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index a83068f73fd..7b679d907e8 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -111,12 +111,12 @@ class SubcontractingReceipt(SubcontractingController): self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") self.update_status_updater_args() self.update_prevdoc_status() - self.update_stock_ledger() - self.make_gl_entries_on_cancel() - self.repost_future_sle_and_gle() self.delete_auto_created_batches() self.set_consumed_qty_in_subcontract_order() self.set_subcontracting_order_status() + self.update_stock_ledger() + self.make_gl_entries_on_cancel() + self.repost_future_sle_and_gle() self.update_status() @frappe.whitelist() 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 28bd84e5895..755142e5a97 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -45,6 +45,7 @@ "subcontracting_receipt_item", "section_break_45", "bom", + "include_exploded_items", "serial_no", "col_break5", "batch_no", @@ -465,12 +466,19 @@ "fieldname": "accounting_details_section", "fieldtype": "Section Break", "label": "Accounting Details" + }, + { + "default": "0", + "fieldname": "include_exploded_items", + "fieldtype": "Check", + "label": "Include Exploded Items", + "print_hide": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:38:26.459669", + "modified": "2023-11-30 12:05:51.920705", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Item", diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.js b/erpnext/support/doctype/warranty_claim/warranty_claim.js index 358768eb46c..10cb37f5124 100644 --- a/erpnext/support/doctype/warranty_claim/warranty_claim.js +++ b/erpnext/support/doctype/warranty_claim/warranty_claim.js @@ -4,93 +4,67 @@ frappe.provide("erpnext.support"); frappe.ui.form.on("Warranty Claim", { - setup: function(frm) { - frm.set_query('contact_person', erpnext.queries.contact_query); - frm.set_query('customer_address', erpnext.queries.address_query); - frm.set_query('customer', erpnext.queries.customer); + setup: (frm) => { + frm.set_query("contact_person", erpnext.queries.contact_query); + frm.set_query("customer_address", erpnext.queries.address_query); + frm.set_query("customer", erpnext.queries.customer); - frm.add_fetch('serial_no', 'item_code', 'item_code'); - frm.add_fetch('serial_no', 'item_name', 'item_name'); - frm.add_fetch('serial_no', 'description', 'description'); - frm.add_fetch('serial_no', 'maintenance_status', 'warranty_amc_status'); - frm.add_fetch('serial_no', 'warranty_expiry_date', 'warranty_expiry_date'); - frm.add_fetch('serial_no', 'amc_expiry_date', 'amc_expiry_date'); - frm.add_fetch('serial_no', 'customer', 'customer'); - frm.add_fetch('serial_no', 'customer_name', 'customer_name'); - frm.add_fetch('item_code', 'item_name', 'item_name'); - frm.add_fetch('item_code', 'description', 'description'); + frm.set_query("serial_no", () => { + let filters = { + company: frm.doc.company, + }; + + if (frm.doc.item_code) { + filters["item_code"] = frm.doc.item_code; + } + + return { filters: filters }; + }); + + frm.set_query("item_code", () => { + return { + filters: { + disabled: 0, + }, + }; + }); }, - onload: function(frm) { - if(!frm.doc.status) { - frm.set_value('status', 'Open'); + + onload: (frm) => { + if (!frm.doc.status) { + frm.set_value("status", "Open"); } }, - customer: function(frm) { + + refresh: (frm) => { + frappe.dynamic_link = { + doc: frm.doc, + fieldname: "customer", + doctype: "Customer", + }; + + if ( + !frm.doc.__islocal && + ["Open", "Work In Progress"].includes(frm.doc.status) + ) { + frm.add_custom_button(__("Maintenance Visit"), () => { + frappe.model.open_mapped_doc({ + method: "erpnext.support.doctype.warranty_claim.warranty_claim.make_maintenance_visit", + frm: frm, + }); + }); + } + }, + + customer: (frm) => { erpnext.utils.get_party_details(frm); }, - customer_address: function(frm) { + + customer_address: (frm) => { erpnext.utils.get_address_display(frm); }, - contact_person: function(frm) { + + contact_person: (frm) => { erpnext.utils.get_contact_details(frm); - } + }, }); - -erpnext.support.WarrantyClaim = class WarrantyClaim extends frappe.ui.form.Controller { - refresh() { - frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer'} - - if(!cur_frm.doc.__islocal && - (cur_frm.doc.status=='Open' || cur_frm.doc.status == 'Work In Progress')) { - cur_frm.add_custom_button(__('Maintenance Visit'), - this.make_maintenance_visit); - } - } - - make_maintenance_visit() { - frappe.model.open_mapped_doc({ - method: "erpnext.support.doctype.warranty_claim.warranty_claim.make_maintenance_visit", - frm: cur_frm - }) - } -}; - -extend_cscript(cur_frm.cscript, new erpnext.support.WarrantyClaim({frm: cur_frm})); - -cur_frm.fields_dict['serial_no'].get_query = function(doc, cdt, cdn) { - var cond = []; - var filter = [ - ['Serial No', 'docstatus', '!=', 2] - ]; - if(doc.item_code) { - cond = ['Serial No', 'item_code', '=', doc.item_code]; - filter.push(cond); - } - if(doc.customer) { - cond = ['Serial No', 'customer', '=', doc.customer]; - filter.push(cond); - } - return{ - filters:filter - } -} - -cur_frm.fields_dict['item_code'].get_query = function(doc, cdt, cdn) { - if(doc.serial_no) { - return{ - doctype: "Serial No", - fields: "item_code", - filters:{ - name: doc.serial_no - } - } - } - else{ - return{ - filters:[ - ['Item', 'docstatus', '!=', 2], - ['Item', 'disabled', '=', 0] - ] - } - } -}; diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.json b/erpnext/support/doctype/warranty_claim/warranty_claim.json index 45485ca2c2f..58ffd4a35c7 100644 --- a/erpnext/support/doctype/warranty_claim/warranty_claim.json +++ b/erpnext/support/doctype/warranty_claim/warranty_claim.json @@ -90,7 +90,8 @@ "fieldname": "serial_no", "fieldtype": "Link", "label": "Serial No", - "options": "Serial No" + "options": "Serial No", + "search_index": 1 }, { "fieldname": "customer", @@ -126,6 +127,8 @@ "options": "fa fa-ticket" }, { + "fetch_from": "serial_no.item_code", + "fetch_if_empty": 1, "fieldname": "item_code", "fieldtype": "Link", "in_list_view": 1, @@ -138,6 +141,7 @@ }, { "depends_on": "eval:doc.item_code", + "fetch_from": "item_code.item_name", "fieldname": "item_name", "fieldtype": "Data", "label": "Item Name", @@ -147,6 +151,7 @@ }, { "depends_on": "eval:doc.item_code", + "fetch_from": "item_code.description", "fieldname": "description", "fieldtype": "Small Text", "label": "Description", @@ -162,17 +167,24 @@ "width": "50%" }, { + "fetch_from": "serial_no.maintenance_status", + "fetch_if_empty": 1, "fieldname": "warranty_amc_status", "fieldtype": "Select", "label": "Warranty / AMC Status", - "options": "\nUnder Warranty\nOut of Warranty\nUnder AMC\nOut of AMC" + "options": "\nUnder Warranty\nOut of Warranty\nUnder AMC\nOut of AMC", + "search_index": 1 }, { + "fetch_from": "serial_no.warranty_expiry_date", + "fetch_if_empty": 1, "fieldname": "warranty_expiry_date", "fieldtype": "Date", "label": "Warranty Expiry Date" }, { + "fetch_from": "serial_no.amc_expiry_date", + "fetch_if_empty": 1, "fieldname": "amc_expiry_date", "fieldtype": "Date", "label": "AMC Expiry Date" @@ -223,6 +235,7 @@ { "bold": 1, "depends_on": "customer", + "fetch_from": "customer.customer_name", "fieldname": "customer_name", "fieldtype": "Data", "in_global_search": 1, @@ -362,7 +375,8 @@ ], "icon": "fa fa-bug", "idx": 1, - "modified": "2021-11-09 17:26:09.703215", + "links": [], + "modified": "2023-11-28 17:30:35.676410", "modified_by": "Administrator", "module": "Support", "name": "Warranty Claim",