diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 8f9f7ce3bea..c8bf6644a51 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -702,7 +702,50 @@ class TestPaymentEntry(FrappeTestCase): pe2.submit() # create return entry against si1 - create_sales_invoice(is_return=1, return_against=si1.name, qty=-1) + cr_note = create_sales_invoice(is_return=1, return_against=si1.name, qty=-1) + si1_outstanding = frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount") + + # create JE(credit note) manually against si1 and cr_note + je = frappe.get_doc( + { + "doctype": "Journal Entry", + "company": si1.company, + "voucher_type": "Credit Note", + "posting_date": nowdate(), + } + ) + je.append( + "accounts", + { + "account": si1.debit_to, + "party_type": "Customer", + "party": si1.customer, + "debit": 0, + "credit": 100, + "debit_in_account_currency": 0, + "credit_in_account_currency": 100, + "reference_type": si1.doctype, + "reference_name": si1.name, + "cost_center": si1.items[0].cost_center, + }, + ) + je.append( + "accounts", + { + "account": cr_note.debit_to, + "party_type": "Customer", + "party": cr_note.customer, + "debit": 100, + "credit": 0, + "debit_in_account_currency": 100, + "credit_in_account_currency": 0, + "reference_type": cr_note.doctype, + "reference_name": cr_note.name, + "cost_center": cr_note.items[0].cost_center, + }, + ) + je.save().submit() + si1_outstanding = frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount") self.assertEqual(si1_outstanding, -100) diff --git a/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py index fc6dbba7e7f..ce9579ed613 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py +++ b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py @@ -294,7 +294,7 @@ class TestPaymentLedgerEntry(FrappeTestCase): cr_note1.return_against = si3.name cr_note1 = cr_note1.save().submit() - pl_entries = ( + pl_entries_si3 = ( qb.from_(ple) .select( ple.voucher_type, @@ -309,7 +309,24 @@ class TestPaymentLedgerEntry(FrappeTestCase): .run(as_dict=True) ) - expected_values = [ + pl_entries_cr_note1 = ( + qb.from_(ple) + .select( + ple.voucher_type, + ple.voucher_no, + ple.against_voucher_type, + ple.against_voucher_no, + ple.amount, + ple.delinked, + ) + .where( + (ple.against_voucher_type == cr_note1.doctype) & (ple.against_voucher_no == cr_note1.name) + ) + .orderby(ple.creation) + .run(as_dict=True) + ) + + expected_values_for_si3 = [ { "voucher_type": si3.doctype, "voucher_no": si3.name, @@ -317,18 +334,21 @@ class TestPaymentLedgerEntry(FrappeTestCase): "against_voucher_no": si3.name, "amount": amount, "delinked": 0, - }, + } + ] + # credit/debit notes post ledger entries against itself + expected_values_for_cr_note1 = [ { "voucher_type": cr_note1.doctype, "voucher_no": cr_note1.name, - "against_voucher_type": si3.doctype, - "against_voucher_no": si3.name, + "against_voucher_type": cr_note1.doctype, + "against_voucher_no": cr_note1.name, "amount": -amount, "delinked": 0, }, ] - self.assertEqual(pl_entries[0], expected_values[0]) - self.assertEqual(pl_entries[1], expected_values[1]) + self.assertEqual(pl_entries_si3, expected_values_for_si3) + self.assertEqual(pl_entries_cr_note1, expected_values_for_cr_note1) def test_je_against_inv_and_note(self): ple = self.ple diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index 2adc1238b70..7b7ce7a8920 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -163,6 +163,15 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo this.frm.refresh(); } + invoice_name() { + this.frm.trigger("get_unreconciled_entries"); + } + + payment_name() { + this.frm.trigger("get_unreconciled_entries"); + } + + clear_child_tables() { this.frm.clear_table("invoices"); this.frm.clear_table("payments"); diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json index 5f6c7034ed3..b88791d3f91 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -27,8 +27,10 @@ "bank_cash_account", "cost_center", "sec_break1", + "invoice_name", "invoices", "column_break_15", + "payment_name", "payments", "sec_break2", "allocation" @@ -137,6 +139,7 @@ "label": "Minimum Invoice Amount" }, { + "default": "50", "description": "System will fetch all the entries if limit value is zero.", "fieldname": "invoice_limit", "fieldtype": "Int", @@ -167,6 +170,7 @@ "label": "Maximum Payment Amount" }, { + "default": "50", "description": "System will fetch all the entries if limit value is zero.", "fieldname": "payment_limit", "fieldtype": "Int", @@ -194,13 +198,23 @@ "label": "Default Advance Account", "mandatory_depends_on": "doc.party_type", "options": "Account" + }, + { + "fieldname": "invoice_name", + "fieldtype": "Data", + "label": "Filter on Invoice" + }, + { + "fieldname": "payment_name", + "fieldtype": "Data", + "label": "Filter on Payment" } ], "hide_toolbar": 1, "icon": "icon-resize-horizontal", "issingle": 1, "links": [], - "modified": "2023-06-09 13:02:48.718362", + "modified": "2023-08-15 05:35:50.109290", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation", diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 0c62ba96acb..7ef5278d252 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -5,6 +5,7 @@ import frappe from frappe import _, msgprint, qb from frappe.model.document import Document +from frappe.query_builder import Criterion from frappe.query_builder.custom import ConstantColumn from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today @@ -74,6 +75,9 @@ class PaymentReconciliation(Document): } ) + if self.payment_name: + condition.update({"name": self.payment_name}) + payment_entries = get_advance_payment_entries( self.party_type, self.party, @@ -89,6 +93,9 @@ class PaymentReconciliation(Document): def get_jv_entries(self): condition = self.get_conditions() + if self.payment_name: + condition += f" and t1.name like '%%{self.payment_name}%%'" + if self.get("cost_center"): condition += f" and t2.cost_center = '{self.cost_center}' " @@ -146,6 +153,15 @@ class PaymentReconciliation(Document): def get_return_invoices(self): voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" doc = qb.DocType(voucher_type) + + conditions = [] + conditions.append(doc.docstatus == 1) + conditions.append(doc[frappe.scrub(self.party_type)] == self.party) + conditions.append(doc.is_return == 1) + + if self.payment_name: + conditions.append(doc.name.like(f"%{self.payment_name}%")) + self.return_invoices = ( qb.from_(doc) .select( @@ -153,11 +169,7 @@ class PaymentReconciliation(Document): doc.name.as_("voucher_no"), doc.return_against, ) - .where( - (doc.docstatus == 1) - & (doc[frappe.scrub(self.party_type)] == self.party) - & (doc.is_return == 1) - ) + .where(Criterion.all(conditions)) .run(as_dict=True) ) @@ -174,15 +186,12 @@ class PaymentReconciliation(Document): self.common_filter_conditions.append(ple.account == self.receivable_payable_account) self.get_return_invoices() - return_invoices = [ - x for x in self.return_invoices if x.return_against == None or x.return_against == "" - ] outstanding_dr_or_cr = [] - if return_invoices: + if self.return_invoices: ple_query = QueryPaymentLedger() return_outstanding = ple_query.get_voucher_outstandings( - vouchers=return_invoices, + vouchers=self.return_invoices, common_filter=self.common_filter_conditions, posting_date=self.ple_posting_date_filter, min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None, @@ -226,6 +235,8 @@ class PaymentReconciliation(Document): min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None, max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None, accounting_dimensions=self.accounting_dimension_filter_conditions, + limit=self.invoice_limit, + voucher_no=self.invoice_name, ) cr_dr_notes = ( diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 66438a7efab..efe97415a55 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -86,8 +86,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. } } - if(doc.docstatus == 1 && doc.outstanding_amount != 0 - && !(doc.is_return && doc.return_against) && !doc.on_hold) { + if(doc.docstatus == 1 && doc.outstanding_amount != 0 && !doc.on_hold) { this.frm.add_custom_button( __('Payment'), () => this.make_payment_entry(), diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index f33439989a8..9f1224d65e8 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -628,9 +628,7 @@ class PurchaseInvoice(BuyingController): "credit_in_account_currency": base_grand_total if self.party_account_currency == self.company_currency else grand_total, - "against_voucher": self.return_against - if cint(self.is_return) and self.return_against - else self.name, + "against_voucher": self.name, "against_voucher_type": self.doctype, "project": self.project, "cost_center": self.cost_center, @@ -1644,12 +1642,8 @@ class PurchaseInvoice(BuyingController): elif outstanding_amount > 0 and getdate(self.due_date) >= getdate(): self.status = "Unpaid" # Check if outstanding amount is 0 due to debit note issued against invoice - elif ( - outstanding_amount <= 0 - and self.is_return == 0 - and frappe.db.get_value( - "Purchase Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1} - ) + elif self.is_return == 0 and frappe.db.get_value( + "Purchase Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1} ): self.status = "Debit Note Issued" elif self.is_return == 1: diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index a4bcdb41db5..642e99cd58a 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -98,8 +98,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm); } - if (doc.docstatus == 1 && doc.outstanding_amount!=0 - && !(cint(doc.is_return) && doc.return_against)) { + if (doc.docstatus == 1 && doc.outstanding_amount!=0) { this.frm.add_custom_button( __('Payment'), () => this.make_payment_entry(), diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 0bc5aa2ed2d..fba2fa7552e 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1104,9 +1104,7 @@ class SalesInvoice(SellingController): "debit_in_account_currency": base_grand_total if self.party_account_currency == self.company_currency else grand_total, - "against_voucher": self.return_against - if cint(self.is_return) and self.return_against - else self.name, + "against_voucher": self.name, "against_voucher_type": self.doctype, "cost_center": self.cost_center, "project": self.project, @@ -1732,12 +1730,8 @@ class SalesInvoice(SellingController): elif outstanding_amount > 0 and getdate(self.due_date) >= getdate(): self.status = "Unpaid" # Check if outstanding amount is 0 due to credit note issued against invoice - elif ( - outstanding_amount <= 0 - and self.is_return == 0 - and frappe.db.get_value( - "Sales Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1} - ) + elif self.is_return == 0 and frappe.db.get_value( + "Sales Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1} ): self.status = "Credit Note Issued" elif self.is_return == 1: diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index f9cfe5a9200..21b39d73120 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1500,8 +1500,8 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(party_credited, 1000) # Check outstanding amount - self.assertFalse(si1.outstanding_amount) - self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 1500) + self.assertEqual(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount"), -1000) + self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 2500) def test_gle_made_when_asset_is_returned(self): create_asset_data() diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index a7b35a579a9..751063ad8e6 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -214,8 +214,8 @@ class ReceivablePayableReport(object): for party_type in self.party_type: if self.filters.get(scrub(party_type)): amount = ple.amount_in_account_currency - else: - amount = ple.amount + else: + amount = ple.amount amount_in_account_currency = ple.amount_in_account_currency # update voucher diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index bccf6f10b63..9d6d0f91fba 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -908,7 +908,9 @@ def get_outstanding_invoices( min_outstanding=None, max_outstanding=None, accounting_dimensions=None, - vouchers=None, + vouchers=None, # list of dicts [{'voucher_type': '', 'voucher_no': ''}] for filtering + limit=None, # passed by reconciliation tool + voucher_no=None, # filter passed by reconciliation tool ): ple = qb.DocType("Payment Ledger Entry") @@ -941,6 +943,8 @@ def get_outstanding_invoices( max_outstanding=max_outstanding, get_invoices=True, accounting_dimensions=accounting_dimensions or [], + limit=limit, + voucher_no=voucher_no, ) for d in invoice_list: @@ -1678,12 +1682,13 @@ class QueryPaymentLedger(object): self.voucher_posting_date = [] self.min_outstanding = None self.max_outstanding = None + self.limit = self.voucher_no = None def reset(self): # clear filters self.vouchers.clear() self.common_filter.clear() - self.min_outstanding = self.max_outstanding = None + self.min_outstanding = self.max_outstanding = self.limit = None # clear result self.voucher_outstandings.clear() @@ -1697,6 +1702,7 @@ class QueryPaymentLedger(object): filter_on_voucher_no = [] filter_on_against_voucher_no = [] + if self.vouchers: voucher_types = set([x.voucher_type for x in self.vouchers]) voucher_nos = set([x.voucher_no for x in self.vouchers]) @@ -1707,6 +1713,10 @@ class QueryPaymentLedger(object): filter_on_against_voucher_no.append(ple.against_voucher_type.isin(voucher_types)) filter_on_against_voucher_no.append(ple.against_voucher_no.isin(voucher_nos)) + if self.voucher_no: + filter_on_voucher_no.append(ple.voucher_no.like(f"%{self.voucher_no}%")) + filter_on_against_voucher_no.append(ple.against_voucher_no.like(f"%{self.voucher_no}%")) + # build outstanding amount filter filter_on_outstanding_amount = [] if self.min_outstanding: @@ -1822,6 +1832,11 @@ class QueryPaymentLedger(object): ) ) + if self.limit: + self.cte_query_voucher_amount_and_outstanding = ( + self.cte_query_voucher_amount_and_outstanding.limit(self.limit) + ) + # execute SQL self.voucher_outstandings = self.cte_query_voucher_amount_and_outstanding.run(as_dict=True) @@ -1835,6 +1850,8 @@ class QueryPaymentLedger(object): get_payments=False, get_invoices=False, accounting_dimensions=None, + limit=None, + voucher_no=None, ): """ Fetch voucher amount and outstanding amount from Payment Ledger using Database CTE @@ -1856,6 +1873,8 @@ class QueryPaymentLedger(object): self.max_outstanding = max_outstanding self.get_payments = get_payments self.get_invoices = get_invoices + self.limit = limit + self.voucher_no = voucher_no self.query_for_outstanding() return self.voucher_outstandings diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 0a2f61d23bd..962292b8eed 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -228,15 +228,19 @@ frappe.ui.form.on('Asset', { {name: __("Schedule Date"), editable: false, resizable: false, width: 270}, {name: __("Depreciation Amount"), editable: false, resizable: false, width: 164}, {name: __("Accumulated Depreciation Amount"), editable: false, resizable: false, width: 164}, - {name: __("Journal Entry"), editable: false, resizable: false, format: value => `${value}`, width: 312} + {name: __("Journal Entry"), editable: false, resizable: false, format: value => `${value}`, width: 304} ], data: data, + layout: "fluid", serialNoColumn: false, checkboxColumn: true, cellHeight: 35 }); - datatable.style.setStyle(`.dt-scrollable`, {'font-size': '0.75rem', 'margin-bottom': '1rem'}); + datatable.style.setStyle(`.dt-scrollable`, {'font-size': '0.75rem', 'margin-bottom': '1rem', 'margin-left': '0.35rem', 'margin-right': '0.35rem'}); + datatable.style.setStyle(`.dt-header`, {'margin-left': '0.35rem', 'margin-right': '0.35rem'}); + datatable.style.setStyle(`.dt-cell--header`, {'color': 'var(--text-muted)'}); + datatable.style.setStyle(`.dt-cell`, {'color': 'var(--text-color)'}); datatable.style.setStyle(`.dt-cell--col-1`, {'text-align': 'center'}); datatable.style.setStyle(`.dt-cell--col-2`, {'font-weight': 600}); datatable.style.setStyle(`.dt-cell--col-3`, {'font-weight': 600}); diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index ce894eb00d9..ddb09c1f44b 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -96,11 +96,14 @@ class Asset(AccountsController): "Asset Depreciation Schedules created:
{0}

Please check, edit if needed, and submit the Asset." ).format(asset_depr_schedules_links) ) - if not frappe.db.exists( - { - "doctype": "Asset Activity", - "asset": self.name, - } + if ( + not frappe.db.exists( + { + "doctype": "Asset Activity", + "asset": self.name, + } + ) + and not self.flags.asset_created_via_asset_capitalization ): add_asset_activity(self.name, _("Asset created")) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 324b7392a8b..0bf2fbb14b6 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -509,6 +509,7 @@ class AssetCapitalization(StockController): asset_doc.gross_purchase_amount = total_target_asset_value asset_doc.purchase_receipt_amount = total_target_asset_value asset_doc.flags.ignore_validate = True + asset_doc.flags.asset_created_via_asset_capitalization = True asset_doc.insert() self.target_asset = asset_doc.name diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py index 2b4b248a302..83350aaf0c9 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -779,9 +779,20 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones( def get_temp_asset_depr_schedule_doc( asset_doc, row, date_of_disposal=None, date_of_return=None, update_asset_finance_book_row=False ): - asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule") + current_asset_depr_schedule_doc = get_asset_depr_schedule_doc( + asset_doc.name, "Active", row.finance_book + ) - asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data( + if not current_asset_depr_schedule_doc: + frappe.throw( + _("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format( + asset_doc.name, row.finance_book + ) + ) + + temp_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) + + temp_asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data( asset_doc, row, date_of_disposal, @@ -789,7 +800,7 @@ def get_temp_asset_depr_schedule_doc( update_asset_finance_book_row, ) - return asset_depr_schedule_doc + return temp_asset_depr_schedule_doc @frappe.whitelist() diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 7c33056a91f..f6a19514392 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -185,8 +185,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e if(!in_list(["Closed", "Delivered"], doc.status)) { if(this.frm.doc.status !== 'Closed' && flt(this.frm.doc.per_received, 2) < 100 && flt(this.frm.doc.per_billed, 2) < 100) { - // Don't add Update Items button if the PO is following the new subcontracting flow. - if (!(this.frm.doc.is_subcontracted && !this.frm.doc.is_old_subcontracting_flow)) { + if (!this.frm.doc.__onload || this.frm.doc.__onload.can_update_items) { this.frm.add_custom_button(__('Update Items'), () => { erpnext.utils.update_child_items({ frm: this.frm, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 06b9d29e69c..3576cd426d6 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -52,6 +52,7 @@ class PurchaseOrder(BuyingController): def onload(self): supplier_tds = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category") self.set_onload("supplier_tds", supplier_tds) + self.set_onload("can_update_items", self.can_update_items()) def validate(self): super(PurchaseOrder, self).validate() @@ -450,6 +451,17 @@ class PurchaseOrder(BuyingController): else: self.db_set("per_received", 0, update_modified=False) + def can_update_items(self) -> bool: + result = True + + if self.is_subcontracted and not self.is_old_subcontracting_flow: + if frappe.db.exists( + "Subcontracting Order", {"purchase_order": self.name, "docstatus": ["!=", 2]} + ): + result = False + + return result + def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0): """get last purchase rate for an item""" diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 3edaffae2ad..55c01e85137 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -901,6 +901,71 @@ class TestPurchaseOrder(FrappeTestCase): self.assertRaises(frappe.ValidationError, po.save) + def test_update_items_for_subcontracting_purchase_order(self): + from erpnext.controllers.tests.test_subcontracting_controller import ( + get_subcontracting_order, + make_bom_for_subcontracted_items, + make_raw_materials, + make_service_items, + make_subcontracted_items, + ) + + def update_items(po, qty): + trans_items = [po.items[0].as_dict()] + trans_items[0]["qty"] = qty + trans_items[0]["fg_item_qty"] = qty + trans_items = json.dumps(trans_items, default=str) + + return update_child_qty_rate( + po.doctype, + trans_items, + po.name, + ) + + make_subcontracted_items() + make_raw_materials() + make_service_items() + make_bom_for_subcontracted_items() + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 7", + "qty": 10, + "rate": 100, + "fg_item": "Subcontracted Item SA7", + "fg_item_qty": 10, + }, + ] + po = create_purchase_order( + rm_items=service_items, + is_subcontracted=1, + supplier_warehouse="_Test Warehouse 1 - _TC", + ) + + update_items(po, qty=20) + po.reload() + + # Test - 1: Items should be updated as there is no Subcontracting Order against PO + self.assertEqual(po.items[0].qty, 20) + self.assertEqual(po.items[0].fg_item_qty, 20) + + sco = get_subcontracting_order(po_name=po.name, warehouse="_Test Warehouse - _TC") + + # Test - 2: ValidationError should be raised as there is Subcontracting Order against PO + self.assertRaises(frappe.ValidationError, update_items, po=po, qty=30) + + sco.reload() + sco.cancel() + po.reload() + + update_items(po, qty=30) + po.reload() + + # Test - 3: Items should be updated as the Subcontracting Order is cancelled + self.assertEqual(po.items[0].qty, 30) + self.assertEqual(po.items[0].fg_item_qty, 30) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py index 71019e80377..a7e03c08fac 100644 --- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py @@ -154,31 +154,35 @@ def get_data(filters): procurement_record = [] if procurement_record_against_mr: procurement_record += procurement_record_against_mr + for po in purchase_order_entry: # fetch material records linked to the purchase order item - mr_record = mr_records.get(po.material_request_item, [{}])[0] - procurement_detail = { - "material_request_date": mr_record.get("transaction_date"), - "cost_center": po.cost_center, - "project": po.project, - "requesting_site": po.warehouse, - "requestor": po.owner, - "material_request_no": po.material_request, - "item_code": po.item_code, - "quantity": flt(po.qty), - "unit_of_measurement": po.stock_uom, - "status": po.status, - "purchase_order_date": po.transaction_date, - "purchase_order": po.parent, - "supplier": po.supplier, - "estimated_cost": flt(mr_record.get("amount")), - "actual_cost": flt(pi_records.get(po.name)), - "purchase_order_amt": flt(po.amount), - "purchase_order_amt_in_company_currency": flt(po.base_amount), - "expected_delivery_date": po.schedule_date, - "actual_delivery_date": pr_records.get(po.name), - } - procurement_record.append(procurement_detail) + material_requests = mr_records.get(po.material_request_item, [{}]) + + for mr_record in material_requests: + procurement_detail = { + "material_request_date": mr_record.get("transaction_date"), + "cost_center": po.cost_center, + "project": po.project, + "requesting_site": po.warehouse, + "requestor": po.owner, + "material_request_no": po.material_request, + "item_code": po.item_code, + "quantity": flt(po.qty), + "unit_of_measurement": po.stock_uom, + "status": po.status, + "purchase_order_date": po.transaction_date, + "purchase_order": po.parent, + "supplier": po.supplier, + "estimated_cost": flt(mr_record.get("amount")), + "actual_cost": flt(pi_records.get(po.name)), + "purchase_order_amt": flt(po.amount), + "purchase_order_amt_in_company_currency": flt(po.base_amount), + "expected_delivery_date": po.schedule_date, + "actual_delivery_date": pr_records.get(po.name), + } + procurement_record.append(procurement_detail) + return procurement_record @@ -301,7 +305,7 @@ def get_po_entries(filters): & (parent.name == child.parent) & (parent.status.notin(("Closed", "Completed", "Cancelled"))) ) - .groupby(parent.name, child.item_code) + .groupby(parent.name, child.material_request_item) ) query = apply_filters_on_query(filters, parent, child, query) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 340ec01bee7..955ebef003a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2418,6 +2418,9 @@ def get_common_query( q = q.select((payment_entry.target_exchange_rate).as_("exchange_rate")) if condition: + if condition.get("name", None): + q = q.where(payment_entry.name.like(f"%{condition.get('name')}%")) + q = q.where(payment_entry.company == condition["company"]) q = ( q.where(payment_entry.posting_date >= condition["from_payment_date"]) @@ -2855,6 +2858,27 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil return update_supplied_items + def validate_fg_item_for_subcontracting(new_data, is_new): + if is_new: + if not new_data.get("fg_item"): + frappe.throw( + _("Finished Good Item is not specified for service item {0}").format(new_data["item_code"]) + ) + else: + is_sub_contracted_item, default_bom = frappe.db.get_value( + "Item", new_data["fg_item"], ["is_sub_contracted_item", "default_bom"] + ) + + if not is_sub_contracted_item: + frappe.throw( + _("Finished Good Item {0} must be a sub-contracted item").format(new_data["fg_item"]) + ) + elif not default_bom: + frappe.throw(_("Default BOM not found for FG Item {0}").format(new_data["fg_item"])) + + if not new_data.get("fg_item_qty"): + frappe.throw(_("Finished Good Item {0} Qty can not be zero").format(new_data["fg_item"])) + data = json.loads(trans_items) any_qty_changed = False # updated to true if any item's qty changes @@ -2886,6 +2910,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate")) prev_qty, new_qty = flt(child_item.get("qty")), flt(d.get("qty")) + prev_fg_qty, new_fg_qty = flt(child_item.get("fg_item_qty")), flt(d.get("fg_item_qty")) prev_con_fac, new_con_fac = flt(child_item.get("conversion_factor")), flt( d.get("conversion_factor") ) @@ -2898,6 +2923,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil rate_unchanged = prev_rate == new_rate qty_unchanged = prev_qty == new_qty + fg_qty_unchanged = prev_fg_qty == new_fg_qty uom_unchanged = prev_uom == new_uom conversion_factor_unchanged = prev_con_fac == new_con_fac any_conversion_factor_changed |= not conversion_factor_unchanged @@ -2907,6 +2933,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil if ( rate_unchanged and qty_unchanged + and fg_qty_unchanged and conversion_factor_unchanged and uom_unchanged and date_unchanged @@ -2917,6 +2944,17 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil if flt(child_item.get("qty")) != flt(d.get("qty")): any_qty_changed = True + if ( + parent.doctype == "Purchase Order" + and parent.is_subcontracted + and not parent.is_old_subcontracting_flow + ): + validate_fg_item_for_subcontracting(d, new_child_flag) + child_item.fg_item_qty = flt(d["fg_item_qty"]) + + if new_child_flag: + child_item.fg_item = d["fg_item"] + child_item.qty = flt(d.get("qty")) rate_precision = child_item.precision("rate") or 2 conv_fac_precision = child_item.precision("conversion_factor") or 2 @@ -3020,11 +3058,20 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil parent.update_ordered_qty() parent.update_ordered_and_reserved_qty() parent.update_receiving_percentage() - if parent.is_old_subcontracting_flow: - if should_update_supplied_items(parent): - parent.update_reserved_qty_for_subcontract() - parent.create_raw_materials_supplied() - parent.save() + + if parent.is_subcontracted: + if parent.is_old_subcontracting_flow: + if should_update_supplied_items(parent): + parent.update_reserved_qty_for_subcontract() + parent.create_raw_materials_supplied() + parent.save() + else: + if not parent.can_update_items(): + frappe.throw( + _( + "Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}." + ).format(frappe.bold(parent.name)) + ) else: # Sales Order parent.validate_warehouse() parent.update_reserved_qty() diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py index eeb35c4d964..6b61ae949da 100644 --- a/erpnext/controllers/tests/test_subcontracting_controller.py +++ b/erpnext/controllers/tests/test_subcontracting_controller.py @@ -1090,7 +1090,7 @@ def get_subcontracting_order(**args): po = frappe.get_doc("Purchase Order", args.get("po_name")) if po.is_subcontracted: - return create_subcontracting_order(po_name=po.name, **args) + return create_subcontracting_order(**args) if not args.service_items: service_items = [ diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js index 6ef82971f5c..0b485bb896c 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.js +++ b/erpnext/crm/doctype/opportunity/opportunity.js @@ -1,7 +1,7 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt frappe.provide("erpnext.crm"); -erpnext.pre_sales.set_as_lost("Quotation"); +erpnext.pre_sales.set_as_lost("Opportunity"); erpnext.sales_common.setup_selling_controller(); diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index f456e5e500c..c11d123982f 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -579,7 +579,9 @@ erpnext.utils.update_child_items = function(opts) { "conversion_factor": d.conversion_factor, "qty": d.qty, "rate": d.rate, - "uom": d.uom + "uom": d.uom, + "fg_item": d.fg_item, + "fg_item_qty": d.fg_item_qty, } }); @@ -678,6 +680,37 @@ erpnext.utils.update_child_items = function(opts) { }) } + if (frm.doc.doctype == 'Purchase Order' && frm.doc.is_subcontracted && !frm.doc.is_old_subcontracting_flow) { + fields.push({ + fieldtype:'Link', + fieldname:'fg_item', + options: 'Item', + reqd: 1, + in_list_view: 0, + read_only: 0, + disabled: 0, + label: __('Finished Good Item'), + get_query: () => { + return { + filters: { + 'is_stock_item': 1, + 'is_sub_contracted_item': 1, + 'default_bom': ['!=', ''] + } + } + }, + }, { + fieldtype:'Float', + fieldname:'fg_item_qty', + reqd: 1, + default: 0, + read_only: 0, + in_list_view: 0, + label: __('Finished Good Item Qty'), + precision: get_precision('fg_item_qty') + }) + } + let dialog = new frappe.ui.Dialog({ title: __("Update Items"), size: "extra-large", diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 1139c4b83a5..9efae6a9d60 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -656,7 +656,10 @@ def make_stock_entry(source_name, target_doc=None): "job_card_item": "job_card_item", }, "postprocess": update_item, - "condition": lambda doc: doc.ordered_qty < doc.stock_qty, + "condition": lambda doc: ( + flt(doc.ordered_qty, doc.precision("ordered_qty")) + < flt(doc.stock_qty, doc.precision("ordered_qty")) + ), }, }, target_doc,