Merge branch 'develop' into copy-emails-to-customer
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index ca031f0..1a7eef6 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.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 @@
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 @@
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/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index f85fc87..a48f5ea 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",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index cc19650..c381b8a 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -421,7 +421,8 @@
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 @@
):
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 @@
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 @@
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(
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 3a70afc..e3fa5e8 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 @@
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 @@
(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 @@
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 @@
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(
diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
index 976935b..6ff81be 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 @@
# 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]
+ )
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 7c63518..11e9f9f 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -6,7 +6,7 @@
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 @@
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 @@
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/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py
index 9555902..65d0872 100644
--- a/erpnext/controllers/subcontracting_controller.py
+++ b/erpnext/controllers/subcontracting_controller.py
@@ -881,7 +881,9 @@
"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/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index d44532a..415216c 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -124,6 +124,7 @@
),
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 654f297..9827b56 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -346,8 +346,8 @@
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",
@@ -507,50 +507,51 @@
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):") + "<br>"
- )
- message += "<br><ul><li>" + "</li><li>".join(mandatory_fields) + "</li></ul>"
- 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):") + "<br>"
+ )
+ message += "<br><ul><li>" + "</li><li>".join(mandatory_fields) + "</li></ul>"
+ message += _("Please create Customer from Lead {0}.").format(get_link_to_form("Lead", lead_name))
+
+ frappe.throw(message, title=_("Mandatory Missing"))
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py
index d4a574d..2440701 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 @@
"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 @@
{"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/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 3fdda2c..0f12f38 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 @@
{
"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 57ba5bb..ee5a176 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 @@
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 @@
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 @@
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 @@
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 @@
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],
]
@@ -1687,7 +1706,7 @@
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)
@@ -1701,6 +1720,7 @@
to_warehouse=target_warehouse,
)
+ pr.reload()
pr.submit()
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0)
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index f29e7ea..76af5d7 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -289,6 +289,21 @@
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 @@
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"""