From 8e0a7a8dbcbb56f65744bab2620495734768c5ef Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 8 Dec 2023 14:06:01 +0100 Subject: [PATCH 1/8] refactor: get/create customer for Sales Order --- erpnext/selling/doctype/customer/customer.py | 1 + .../selling/doctype/quotation/quotation.py | 85 ++++++++++--------- 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index efb9820016..2f6775f0cf 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -124,6 +124,7 @@ class Customer(TransactionBase): ), title=_("Note"), indicator="yellow", + alert=True, ) return new_customer_name diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 00b79e3aad..a149db9325 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -345,8 +345,8 @@ def make_sales_order(source_name: str, target_doc=None): return _make_sales_order(source_name, target_doc) -def _make_sales_order(source_name, target_doc=None, customer_group=None, ignore_permissions=False): - customer = _make_customer(source_name, ignore_permissions, customer_group) +def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): + customer = _make_customer(source_name, ignore_permissions) ordered_items = frappe._dict( frappe.db.get_all( "Sales Order Item", @@ -505,50 +505,51 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): return doclist -def _make_customer(source_name, ignore_permissions=False, customer_group=None): +def _make_customer(source_name, ignore_permissions=False): quotation = frappe.db.get_value( - "Quotation", source_name, ["order_type", "party_name", "customer_name"], as_dict=1 + "Quotation", + source_name, + ["order_type", "quotation_to", "party_name", "customer_name"], + as_dict=1, ) - if quotation and quotation.get("party_name"): - if not frappe.db.exists("Customer", quotation.get("party_name")): - lead_name = quotation.get("party_name") - customer_name = frappe.db.get_value( - "Customer", {"lead_name": lead_name}, ["name", "customer_name"], as_dict=True - ) - if not customer_name: - from erpnext.crm.doctype.lead.lead import _make_customer + if quotation.quotation_to == "Customer": + return frappe.get_doc("Customer", quotation.party_name) - customer_doclist = _make_customer(lead_name, ignore_permissions=ignore_permissions) - customer = frappe.get_doc(customer_doclist) - customer.flags.ignore_permissions = ignore_permissions - customer.customer_group = customer_group + # If the Quotation is not to a Customer, it must be to a Lead. + # Check if a Customer already exists for the Lead. + existing_customer_for_lead = frappe.db.get_value("Customer", {"lead_name": quotation.party_name}) + if existing_customer_for_lead: + return frappe.get_doc("Customer", existing_customer_for_lead) - try: - customer.insert() - return customer - except frappe.NameError: - if frappe.defaults.get_global_default("cust_master_name") == "Customer Name": - customer.run_method("autoname") - customer.name += "-" + lead_name - customer.insert() - return customer - else: - raise - except frappe.MandatoryError as e: - mandatory_fields = e.args[0].split(":")[1].split(",") - mandatory_fields = [customer.meta.get_label(field.strip()) for field in mandatory_fields] + # If no Customer exists for the Lead, create a new Customer. + return create_customer_from_lead(quotation.party_name, ignore_permissions=ignore_permissions) - frappe.local.message_log = [] - lead_link = frappe.utils.get_link_to_form("Lead", lead_name) - message = ( - _("Could not auto create Customer due to the following missing mandatory field(s):") + "
" - ) - message += "
" - message += _("Please create Customer from Lead {0}.").format(lead_link) - frappe.throw(message, title=_("Mandatory Missing")) - else: - return customer_name - else: - return frappe.get_doc("Customer", quotation.get("party_name")) +def create_customer_from_lead(lead_name, ignore_permissions=False): + from erpnext.crm.doctype.lead.lead import _make_customer + + customer = _make_customer(lead_name, ignore_permissions=ignore_permissions) + customer.flags.ignore_permissions = ignore_permissions + + try: + customer.insert() + return customer + except frappe.MandatoryError as e: + handle_mandatory_error(e, customer, lead_name) + + +def handle_mandatory_error(e, customer, lead_name): + from frappe.utils import get_link_to_form + + mandatory_fields = e.args[0].split(":")[1].split(",") + mandatory_fields = [customer.meta.get_label(field.strip()) for field in mandatory_fields] + + frappe.local.message_log = [] + message = ( + _("Could not auto create Customer due to the following missing mandatory field(s):") + "
" + ) + message += "
" + message += _("Please create Customer from Lead {0}.").format(get_link_to_form("Lead", lead_name)) + + frappe.throw(message, title=_("Mandatory Missing")) From 3815f07c33d9c84230304d53a735d8f580f3288e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 23 Jan 2024 13:11:06 +0100 Subject: [PATCH 2/8] refactor(Sales Invoice): set account for mode of payment --- .../accounts/doctype/pos_invoice/pos_invoice.py | 7 ------- .../doctype/sales_invoice/sales_invoice.py | 17 +++++++---------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index e542d3cc63..9b0b3ecfab 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -11,7 +11,6 @@ from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_lo from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( SalesInvoice, - get_bank_cash_account, get_mode_of_payment_info, update_multi_mode_option, ) @@ -208,7 +207,6 @@ class POSInvoice(SalesInvoice): self.validate_stock_availablility() self.validate_return_items_qty() self.set_status() - self.set_account_for_mode_of_payment() self.validate_pos() self.validate_payment_amount() self.validate_loyalty_transaction() @@ -643,11 +641,6 @@ class POSInvoice(SalesInvoice): update_multi_mode_option(self, pos_profile) self.paid_amount = 0 - def set_account_for_mode_of_payment(self): - for pay in self.payments: - if not pay.account: - pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account") - @frappe.whitelist() def create_payment_request(self): for pay in self.payments: diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index cc19650c38..c381b8a910 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -421,7 +421,8 @@ class SalesInvoice(SellingController): self.calculate_taxes_and_totals() def before_save(self): - set_account_for_mode_of_payment(self) + self.set_account_for_mode_of_payment() + self.set_paid_amount() def on_submit(self): self.validate_pos_paid_amount() @@ -712,9 +713,6 @@ class SalesInvoice(SellingController): ): data.sales_invoice = sales_invoice - def on_update(self): - self.set_paid_amount() - def on_update_after_submit(self): if hasattr(self, "repost_required"): fields_to_check = [ @@ -745,6 +743,11 @@ class SalesInvoice(SellingController): self.paid_amount = paid_amount self.base_paid_amount = base_paid_amount + def set_account_for_mode_of_payment(self): + for payment in self.payments: + if not payment.account: + payment.account = get_bank_cash_account(payment.mode_of_payment, self.company).get("account") + def validate_time_sheets_are_submitted(self): for data in self.timesheets: if data.time_sheet: @@ -2113,12 +2116,6 @@ def make_sales_return(source_name, target_doc=None): return make_return_doc("Sales Invoice", source_name, target_doc) -def set_account_for_mode_of_payment(self): - for data in self.payments: - if not data.account: - data.account = get_bank_cash_account(data.mode_of_payment, self.company).get("account") - - def get_inter_company_details(doc, doctype): if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"]: parties = frappe.db.get_all( From 99b839d2b6ed78a62677d264c9cd90a95ed0c45c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 26 Jan 2024 10:46:44 +0530 Subject: [PATCH 3/8] chore: failing ci tests --- .../accounts/doctype/purchase_invoice/purchase_invoice.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index f85fc878ab..a48f5ea16e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -1253,6 +1253,7 @@ "fieldtype": "Select", "in_standard_filter": 1, "label": "Status", + "no_copy": 1, "options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nPartly Paid\nUnpaid\nOverdue\nCancelled\nInternal Transfer", "print_hide": 1 }, @@ -1612,7 +1613,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2023-11-29 15:35:44.697496", + "modified": "2024-01-26 10:46:00.469053", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", From dbd4dae3d9b45dce8f6767cf7018822e1ab5d5e3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 26 Jan 2024 18:42:29 +0530 Subject: [PATCH 4/8] test: Internal transfer using purchase receipt --- .../delivery_note/test_delivery_note.py | 4 +-- .../purchase_receipt/test_purchase_receipt.py | 27 ++++++++++++++++--- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 3fdda2cc49..0f12f38195 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1597,8 +1597,8 @@ def create_delivery_note(**args): { "item_code": args.item or args.item_code or "_Test Item", "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty if args.get("qty") is not None else 1, - "rate": args.rate if args.get("rate") is not None else 100, + "qty": args.get("qty", 1), + "rate": args.get("rate", 100), "conversion_factor": 1.0, "serial_and_batch_bundle": bundle_id, "allow_zero_valuation_rate": args.allow_zero_valuation_rate or 1, diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 57ba5bb0a5..6f72684ae5 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -21,9 +21,7 @@ from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle get_serial_nos_from_bundle, make_serial_batch_bundle, ) -from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse -from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction class TestPurchaseReceipt(FrappeTestCase): @@ -735,7 +733,6 @@ class TestPurchaseReceipt(FrappeTestCase): po.cancel() def test_serial_no_against_purchase_receipt(self): - from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos item_code = "Test Manual Created Serial No" if not frappe.db.exists("Item", item_code): @@ -1020,6 +1017,11 @@ class TestPurchaseReceipt(FrappeTestCase): def test_stock_transfer_from_purchase_receipt_with_valuation(self): from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + create_stock_reconciliation, + ) + from erpnext.stock.get_item_details import get_valuation_rate + from erpnext.stock.utils import get_stock_balance prepare_data_for_internal_transfer() @@ -1034,6 +1036,22 @@ class TestPurchaseReceipt(FrappeTestCase): company="_Test Company with perpetual inventory", ) + if ( + get_valuation_rate( + pr1.items[0].item_code, "_Test Company with perpetual inventory", warehouse="Stores - TCP1" + ) + != 50 + ): + balance = get_stock_balance(item_code=pr1.items[0].item_code, warehouse="Stores - TCP1") + create_stock_reconciliation( + item_code=pr1.items[0].item_code, + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + qty=balance, + rate=50, + do_not_save=True, + ) + customer = "_Test Internal Customer 2" company = "_Test Company with perpetual inventory" @@ -1071,7 +1089,8 @@ class TestPurchaseReceipt(FrappeTestCase): sl_entries = get_sl_entries("Purchase Receipt", pr.name) expected_gle = [ - ["Stock In Hand - TCP1", 272.5, 0.0], + ["Stock In Hand - TCP1", 250.0, 0.0], + ["Cost of Goods Sold - TCP1", 22.5, 0.0], ["_Test Account Stock In Hand - TCP1", 0.0, 250.0], ["_Test Account Shipping Charges - TCP1", 0.0, 22.5], ] From 0de4197c886f6dfa500c0b3f1869c13cc3807aaf Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 27 Jan 2024 09:55:26 +0530 Subject: [PATCH 5/8] refactor: do currency conversion on future amount columns --- .../accounts_receivable.py | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 3a70afc3f0..e3fa5e878d 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -5,7 +5,7 @@ from collections import OrderedDict import frappe -from frappe import _, qb, scrub +from frappe import _, qb, query_builder, scrub from frappe.query_builder import Criterion from frappe.query_builder.functions import Date, Substring, Sum from frappe.utils import cint, cstr, flt, getdate, nowdate @@ -576,6 +576,8 @@ class ReceivablePayableReport(object): def get_future_payments_from_payment_entry(self): pe = frappe.qb.DocType("Payment Entry") pe_ref = frappe.qb.DocType("Payment Entry Reference") + ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"]) + return ( frappe.qb.from_(pe) .inner_join(pe_ref) @@ -587,6 +589,11 @@ class ReceivablePayableReport(object): (pe.posting_date).as_("future_date"), (pe_ref.allocated_amount).as_("future_amount"), (pe.reference_no).as_("future_ref"), + ifelse( + pe.payment_type == "Receive", + pe.source_exchange_rate * pe_ref.allocated_amount, + pe.target_exchange_rate * pe_ref.allocated_amount, + ).as_("future_amount_in_base_currency"), ) .where( (pe.docstatus < 2) @@ -623,13 +630,24 @@ class ReceivablePayableReport(object): query = query.select( Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("future_amount") ) + query = query.select(Sum(jea.debit - jea.credit).as_("future_amount_in_base_currency")) else: query = query.select( Sum(jea.credit_in_account_currency - jea.debit_in_account_currency).as_("future_amount") ) + query = query.select(Sum(jea.credit - jea.debit).as_("future_amount_in_base_currency")) else: query = query.select( - Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_("future_amount") + Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_( + "future_amount_in_base_currency" + ) + ) + query = query.select( + Sum( + jea.debit_in_account_currency + if self.account_type == "Payable" + else jea.credit_in_account_currency + ).as_("future_amount") ) query = query.having(qb.Field("future_amount") > 0) @@ -645,14 +663,19 @@ class ReceivablePayableReport(object): row.remaining_balance = row.outstanding row.future_amount = 0.0 for future in self.future_payments.get((row.voucher_no, row.party), []): - if row.remaining_balance > 0 and future.future_amount: - if future.future_amount > row.outstanding: + if self.filters.in_party_currency: + future_amount_field = "future_amount" + else: + future_amount_field = "future_amount_in_base_currency" + + if row.remaining_balance > 0 and future.get(future_amount_field): + if future.get(future_amount_field) > row.outstanding: row.future_amount = row.outstanding - future.future_amount = future.future_amount - row.outstanding + future[future_amount_field] = future.get(future_amount_field) - row.outstanding row.remaining_balance = 0 else: - row.future_amount += future.future_amount - future.future_amount = 0 + row.future_amount += future.get(future_amount_field) + future[future_amount_field] = 0 row.remaining_balance = row.outstanding - row.future_amount row.setdefault("future_ref", []).append( From 7b37389115cd10aac154c292e7b34fcefd4d7ba2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 27 Jan 2024 11:14:29 +0530 Subject: [PATCH 6/8] test: future payment with foreign currency --- .../test_accounts_receivable.py | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 976935b99f..6ff81be0ab 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -772,3 +772,92 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): # post sorting output should be [[Additional Debtors, ...], [Debtors, ...]] report_output = sorted(report_output, key=lambda x: x[0]) self.assertEqual(expected_data, report_output) + + def test_future_payments_on_foreign_currency(self): + self.customer2 = ( + frappe.get_doc( + { + "doctype": "Customer", + "customer_name": "Jane Doe", + "type": "Individual", + "default_currency": "USD", + } + ) + .insert() + .submit() + ) + + si = self.create_sales_invoice(do_not_submit=True) + si.posting_date = add_days(today(), -1) + si.customer = self.customer2 + si.currency = "USD" + si.conversion_rate = 80 + si.debit_to = self.debtors_usd + si.save().submit() + + # full payment in USD + pe = get_payment_entry(si.doctype, si.name) + pe.posting_date = add_days(today(), 1) + pe.base_received_amount = 7500 + pe.received_amount = 7500 + pe.source_exchange_rate = 75 + pe.save().submit() + + filters = frappe._dict( + { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "show_future_payments": True, + "in_party_currency": False, + } + ) + report = execute(filters)[1] + self.assertEqual(len(report), 1) + + expected_data = [8000.0, 8000.0, 500.0, 7500.0] + row = report[0] + self.assertEqual( + expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount] + ) + + filters.in_party_currency = True + report = execute(filters)[1] + self.assertEqual(len(report), 1) + expected_data = [100.0, 100.0, 0.0, 100.0] + row = report[0] + self.assertEqual( + expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount] + ) + + pe.cancel() + # partial payment in USD on a future date + pe = get_payment_entry(si.doctype, si.name) + pe.posting_date = add_days(today(), 1) + pe.base_received_amount = 6750 + pe.received_amount = 6750 + pe.source_exchange_rate = 75 + pe.paid_amount = 90 # in USD + pe.references[0].allocated_amount = 90 + pe.save().submit() + + filters.in_party_currency = False + report = execute(filters)[1] + self.assertEqual(len(report), 1) + expected_data = [8000.0, 8000.0, 1250.0, 6750.0] + row = report[0] + self.assertEqual( + expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount] + ) + + filters.in_party_currency = True + report = execute(filters)[1] + self.assertEqual(len(report), 1) + expected_data = [100.0, 100.0, 10.0, 90.0] + row = report[0] + self.assertEqual( + expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount] + ) From 67d828dab36d6f64fdb41e5f2cd203d508d2998c Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sat, 27 Jan 2024 12:00:06 +0530 Subject: [PATCH 7/8] fix: not able to save subcontracting purchase receipt (old flow) (#39590) --- .../controllers/subcontracting_controller.py | 4 +- erpnext/stock/utils.py | 47 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 9555902a74..65d087261f 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -881,7 +881,9 @@ class SubcontractingController(StockController): "posting_time": self.posting_time, "qty": -1 * item.consumed_qty, "voucher_detail_no": item.name, - "serial_and_batch_bundle": item.serial_and_batch_bundle, + "serial_and_batch_bundle": item.get("serial_and_batch_bundle"), + "serial_no": item.get("serial_no"), + "batch_no": item.get("batch_no"), } ) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index f29e7ea6a1..76af5d7e3e 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -289,6 +289,21 @@ def get_incoming_rate(args, raise_error_if_no_rate=True): in_rate = batch_obj.get_incoming_rate() + elif (args.get("serial_no") or "").strip() and not args.get("serial_and_batch_bundle"): + in_rate = get_avg_purchase_rate(args.get("serial_no")) + elif ( + args.get("batch_no") + and frappe.db.get_value("Batch", args.get("batch_no"), "use_batchwise_valuation", cache=True) + and not args.get("serial_and_batch_bundle") + ): + in_rate = get_batch_incoming_rate( + item_code=args.get("item_code"), + warehouse=args.get("warehouse"), + batch_no=args.get("batch_no"), + posting_date=args.get("posting_date"), + posting_time=args.get("posting_time"), + ) + else: valuation_method = get_valuation_method(args.get("item_code")) previous_sle = get_previous_sle(args) @@ -319,6 +334,38 @@ def get_incoming_rate(args, raise_error_if_no_rate=True): return flt(in_rate) +def get_batch_incoming_rate( + item_code, warehouse, batch_no, posting_date, posting_time, creation=None +): + + sle = frappe.qb.DocType("Stock Ledger Entry") + + timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime( + posting_date, posting_time + ) + if creation: + timestamp_condition |= ( + CombineDatetime(sle.posting_date, sle.posting_time) + == CombineDatetime(posting_date, posting_time) + ) & (sle.creation < creation) + + batch_details = ( + frappe.qb.from_(sle) + .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty")) + .where( + (sle.item_code == item_code) + & (sle.warehouse == warehouse) + & (sle.batch_no == batch_no) + & (sle.serial_and_batch_bundle.isnull()) + & (sle.is_cancelled == 0) + ) + .where(timestamp_condition) + ).run(as_dict=True) + + if batch_details and batch_details[0].batch_qty: + return batch_details[0].batch_value / batch_details[0].batch_qty + + def get_avg_purchase_rate(serial_nos): """get average value of serial numbers""" From 8fdc244e16c26a74944a3a67613f8b64009a69b0 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sat, 27 Jan 2024 21:37:58 +0530 Subject: [PATCH 8/8] fix: prevent extra transfer against inter transfer transaction (#39213) * fix: prevent extra transfer against inter transfer transaction * fix: internal transfer dashboard --- erpnext/controllers/stock_controller.py | 115 +++++++++++++++++- .../delivery_note/delivery_note_dashboard.py | 6 +- .../purchase_receipt/test_purchase_receipt.py | 3 +- 3 files changed, 121 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 7c63518553..11e9f9f441 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -6,7 +6,7 @@ from collections import defaultdict from typing import List, Tuple import frappe -from frappe import _ +from frappe import _, bold from frappe.utils import cint, flt, get_link_to_form, getdate import erpnext @@ -697,6 +697,9 @@ class StockController(AccountsController): self.validate_in_transit_warehouses() self.validate_multi_currency() self.validate_packed_items() + + if self.get("is_internal_supplier"): + self.validate_internal_transfer_qty() else: self.validate_internal_transfer_warehouse() @@ -735,6 +738,116 @@ class StockController(AccountsController): if self.doctype in ("Sales Invoice", "Delivery Note Item") and self.get("packed_items"): frappe.throw(_("Packed Items cannot be transferred internally")) + def validate_internal_transfer_qty(self): + if self.doctype not in ["Purchase Invoice", "Purchase Receipt"]: + return + + item_wise_transfer_qty = self.get_item_wise_inter_transfer_qty() + if not item_wise_transfer_qty: + return + + item_wise_received_qty = self.get_item_wise_inter_received_qty() + precision = frappe.get_precision(self.doctype + " Item", "qty") + + over_receipt_allowance = frappe.db.get_single_value( + "Stock Settings", "over_delivery_receipt_allowance" + ) + + parent_doctype = { + "Purchase Receipt": "Delivery Note", + "Purchase Invoice": "Sales Invoice", + }.get(self.doctype) + + for key, transferred_qty in item_wise_transfer_qty.items(): + recevied_qty = flt(item_wise_received_qty.get(key), precision) + if over_receipt_allowance: + transferred_qty = transferred_qty + flt( + transferred_qty * over_receipt_allowance / 100, precision + ) + + if recevied_qty > flt(transferred_qty, precision): + frappe.throw( + _("For Item {0} cannot be received more than {1} qty against the {2} {3}").format( + bold(key[1]), + bold(flt(transferred_qty, precision)), + bold(parent_doctype), + get_link_to_form(parent_doctype, self.get("inter_company_reference")), + ) + ) + + def get_item_wise_inter_transfer_qty(self): + reference_field = "inter_company_reference" + if self.doctype == "Purchase Invoice": + reference_field = "inter_company_invoice_reference" + + parent_doctype = { + "Purchase Receipt": "Delivery Note", + "Purchase Invoice": "Sales Invoice", + }.get(self.doctype) + + child_doctype = parent_doctype + " Item" + + parent_tab = frappe.qb.DocType(parent_doctype) + child_tab = frappe.qb.DocType(child_doctype) + + query = ( + frappe.qb.from_(parent_doctype) + .inner_join(child_tab) + .on(child_tab.parent == parent_tab.name) + .select( + child_tab.name, + child_tab.item_code, + child_tab.qty, + ) + .where((parent_tab.name == self.get(reference_field)) & (parent_tab.docstatus == 1)) + ) + + data = query.run(as_dict=True) + item_wise_transfer_qty = defaultdict(float) + for row in data: + item_wise_transfer_qty[(row.name, row.item_code)] += flt(row.qty) + + return item_wise_transfer_qty + + def get_item_wise_inter_received_qty(self): + child_doctype = self.doctype + " Item" + + parent_tab = frappe.qb.DocType(self.doctype) + child_tab = frappe.qb.DocType(child_doctype) + + query = ( + frappe.qb.from_(self.doctype) + .inner_join(child_tab) + .on(child_tab.parent == parent_tab.name) + .select( + child_tab.item_code, + child_tab.qty, + ) + .where(parent_tab.docstatus < 2) + ) + + if self.doctype == "Purchase Invoice": + query = query.select( + child_tab.sales_invoice_item.as_("name"), + ) + + query = query.where( + parent_tab.inter_company_invoice_reference == self.inter_company_invoice_reference + ) + else: + query = query.select( + child_tab.delivery_note_item.as_("name"), + ) + + query = query.where(parent_tab.inter_company_reference == self.inter_company_reference) + + data = query.run(as_dict=True) + item_wise_transfer_qty = defaultdict(float) + for row in data: + item_wise_transfer_qty[(row.name, row.item_code)] += flt(row.qty) + + return item_wise_transfer_qty + def validate_putaway_capacity(self): # if over receipt is attempted while 'apply putaway rule' is disabled # and if rule was applied on the transaction, validate it. diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py index d4a574da73..2440701af9 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py @@ -8,6 +8,7 @@ def get_data(): "Stock Entry": "delivery_note_no", "Quality Inspection": "reference_name", "Auto Repeat": "reference_document", + "Purchase Receipt": "inter_company_reference", }, "internal_links": { "Sales Order": ["items", "against_sales_order"], @@ -22,6 +23,9 @@ def get_data(): {"label": _("Reference"), "items": ["Sales Order", "Shipment", "Quality Inspection"]}, {"label": _("Returns"), "items": ["Stock Entry"]}, {"label": _("Subscription"), "items": ["Auto Repeat"]}, - {"label": _("Internal Transfer"), "items": ["Material Request", "Purchase Order"]}, + { + "label": _("Internal Transfer"), + "items": ["Material Request", "Purchase Order", "Purchase Receipt"], + }, ], } diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 6f72684ae5..ee5a17653d 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1706,7 +1706,7 @@ class TestPurchaseReceipt(FrappeTestCase): pr.items[0].rejected_warehouse = from_warehouse pr.save() - self.assertRaises(OverAllowanceError, pr.submit) + self.assertRaises(frappe.ValidationError, pr.submit) # Step 5: Test Over Receipt Allowance frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 50) @@ -1720,6 +1720,7 @@ class TestPurchaseReceipt(FrappeTestCase): to_warehouse=target_warehouse, ) + pr.reload() pr.submit() frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0)