Merge branch 'develop' into copy-emails-to-customer
This commit is contained in:
commit
4c197c8dbd
@ -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:
|
||||
|
@ -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",
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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]
|
||||
)
|
||||
|
@ -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.
|
||||
|
@ -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"),
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -124,6 +124,7 @@ class Customer(TransactionBase):
|
||||
),
|
||||
title=_("Note"),
|
||||
indicator="yellow",
|
||||
alert=True,
|
||||
)
|
||||
|
||||
return new_customer_name
|
||||
|
@ -346,8 +346,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",
|
||||
@ -507,50 +507,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):") + "<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"))
|
||||
|
@ -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"],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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],
|
||||
]
|
||||
@ -1687,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)
|
||||
@ -1701,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)
|
||||
|
@ -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"""
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user