mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-09 00:01:18 +00:00
Merge branch 'develop' into copy-emails-to-customer
This commit is contained in:
@@ -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.payment_request.payment_request import make_payment_request
|
||||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||||
SalesInvoice,
|
SalesInvoice,
|
||||||
get_bank_cash_account,
|
|
||||||
get_mode_of_payment_info,
|
get_mode_of_payment_info,
|
||||||
update_multi_mode_option,
|
update_multi_mode_option,
|
||||||
)
|
)
|
||||||
@@ -208,7 +207,6 @@ class POSInvoice(SalesInvoice):
|
|||||||
self.validate_stock_availablility()
|
self.validate_stock_availablility()
|
||||||
self.validate_return_items_qty()
|
self.validate_return_items_qty()
|
||||||
self.set_status()
|
self.set_status()
|
||||||
self.set_account_for_mode_of_payment()
|
|
||||||
self.validate_pos()
|
self.validate_pos()
|
||||||
self.validate_payment_amount()
|
self.validate_payment_amount()
|
||||||
self.validate_loyalty_transaction()
|
self.validate_loyalty_transaction()
|
||||||
@@ -643,11 +641,6 @@ class POSInvoice(SalesInvoice):
|
|||||||
update_multi_mode_option(self, pos_profile)
|
update_multi_mode_option(self, pos_profile)
|
||||||
self.paid_amount = 0
|
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()
|
@frappe.whitelist()
|
||||||
def create_payment_request(self):
|
def create_payment_request(self):
|
||||||
for pay in self.payments:
|
for pay in self.payments:
|
||||||
|
|||||||
@@ -1253,6 +1253,7 @@
|
|||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Status",
|
"label": "Status",
|
||||||
|
"no_copy": 1,
|
||||||
"options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nPartly Paid\nUnpaid\nOverdue\nCancelled\nInternal Transfer",
|
"options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nPartly Paid\nUnpaid\nOverdue\nCancelled\nInternal Transfer",
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
@@ -1612,7 +1613,7 @@
|
|||||||
"idx": 204,
|
"idx": 204,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-11-29 15:35:44.697496",
|
"modified": "2024-01-26 10:46:00.469053",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice",
|
"name": "Purchase Invoice",
|
||||||
|
|||||||
@@ -421,7 +421,8 @@ class SalesInvoice(SellingController):
|
|||||||
self.calculate_taxes_and_totals()
|
self.calculate_taxes_and_totals()
|
||||||
|
|
||||||
def before_save(self):
|
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):
|
def on_submit(self):
|
||||||
self.validate_pos_paid_amount()
|
self.validate_pos_paid_amount()
|
||||||
@@ -712,9 +713,6 @@ class SalesInvoice(SellingController):
|
|||||||
):
|
):
|
||||||
data.sales_invoice = sales_invoice
|
data.sales_invoice = sales_invoice
|
||||||
|
|
||||||
def on_update(self):
|
|
||||||
self.set_paid_amount()
|
|
||||||
|
|
||||||
def on_update_after_submit(self):
|
def on_update_after_submit(self):
|
||||||
if hasattr(self, "repost_required"):
|
if hasattr(self, "repost_required"):
|
||||||
fields_to_check = [
|
fields_to_check = [
|
||||||
@@ -745,6 +743,11 @@ class SalesInvoice(SellingController):
|
|||||||
self.paid_amount = paid_amount
|
self.paid_amount = paid_amount
|
||||||
self.base_paid_amount = base_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):
|
def validate_time_sheets_are_submitted(self):
|
||||||
for data in self.timesheets:
|
for data in self.timesheets:
|
||||||
if data.time_sheet:
|
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)
|
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):
|
def get_inter_company_details(doc, doctype):
|
||||||
if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"]:
|
if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"]:
|
||||||
parties = frappe.db.get_all(
|
parties = frappe.db.get_all(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, qb, scrub
|
from frappe import _, qb, query_builder, scrub
|
||||||
from frappe.query_builder import Criterion
|
from frappe.query_builder import Criterion
|
||||||
from frappe.query_builder.functions import Date, Substring, Sum
|
from frappe.query_builder.functions import Date, Substring, Sum
|
||||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||||
@@ -576,6 +576,8 @@ class ReceivablePayableReport(object):
|
|||||||
def get_future_payments_from_payment_entry(self):
|
def get_future_payments_from_payment_entry(self):
|
||||||
pe = frappe.qb.DocType("Payment Entry")
|
pe = frappe.qb.DocType("Payment Entry")
|
||||||
pe_ref = frappe.qb.DocType("Payment Entry Reference")
|
pe_ref = frappe.qb.DocType("Payment Entry Reference")
|
||||||
|
ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
frappe.qb.from_(pe)
|
frappe.qb.from_(pe)
|
||||||
.inner_join(pe_ref)
|
.inner_join(pe_ref)
|
||||||
@@ -587,6 +589,11 @@ class ReceivablePayableReport(object):
|
|||||||
(pe.posting_date).as_("future_date"),
|
(pe.posting_date).as_("future_date"),
|
||||||
(pe_ref.allocated_amount).as_("future_amount"),
|
(pe_ref.allocated_amount).as_("future_amount"),
|
||||||
(pe.reference_no).as_("future_ref"),
|
(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(
|
.where(
|
||||||
(pe.docstatus < 2)
|
(pe.docstatus < 2)
|
||||||
@@ -623,13 +630,24 @@ class ReceivablePayableReport(object):
|
|||||||
query = query.select(
|
query = query.select(
|
||||||
Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("future_amount")
|
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:
|
else:
|
||||||
query = query.select(
|
query = query.select(
|
||||||
Sum(jea.credit_in_account_currency - jea.debit_in_account_currency).as_("future_amount")
|
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:
|
else:
|
||||||
query = query.select(
|
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)
|
query = query.having(qb.Field("future_amount") > 0)
|
||||||
@@ -645,14 +663,19 @@ class ReceivablePayableReport(object):
|
|||||||
row.remaining_balance = row.outstanding
|
row.remaining_balance = row.outstanding
|
||||||
row.future_amount = 0.0
|
row.future_amount = 0.0
|
||||||
for future in self.future_payments.get((row.voucher_no, row.party), []):
|
for future in self.future_payments.get((row.voucher_no, row.party), []):
|
||||||
if row.remaining_balance > 0 and future.future_amount:
|
if self.filters.in_party_currency:
|
||||||
if future.future_amount > row.outstanding:
|
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
|
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
|
row.remaining_balance = 0
|
||||||
else:
|
else:
|
||||||
row.future_amount += future.future_amount
|
row.future_amount += future.get(future_amount_field)
|
||||||
future.future_amount = 0
|
future[future_amount_field] = 0
|
||||||
row.remaining_balance = row.outstanding - row.future_amount
|
row.remaining_balance = row.outstanding - row.future_amount
|
||||||
|
|
||||||
row.setdefault("future_ref", []).append(
|
row.setdefault("future_ref", []).append(
|
||||||
|
|||||||
@@ -772,3 +772,92 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
|||||||
# post sorting output should be [[Additional Debtors, ...], [Debtors, ...]]
|
# post sorting output should be [[Additional Debtors, ...], [Debtors, ...]]
|
||||||
report_output = sorted(report_output, key=lambda x: x[0])
|
report_output = sorted(report_output, key=lambda x: x[0])
|
||||||
self.assertEqual(expected_data, report_output)
|
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
|
from typing import List, Tuple
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _, bold
|
||||||
from frappe.utils import cint, flt, get_link_to_form, getdate
|
from frappe.utils import cint, flt, get_link_to_form, getdate
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
@@ -697,6 +697,9 @@ class StockController(AccountsController):
|
|||||||
self.validate_in_transit_warehouses()
|
self.validate_in_transit_warehouses()
|
||||||
self.validate_multi_currency()
|
self.validate_multi_currency()
|
||||||
self.validate_packed_items()
|
self.validate_packed_items()
|
||||||
|
|
||||||
|
if self.get("is_internal_supplier"):
|
||||||
|
self.validate_internal_transfer_qty()
|
||||||
else:
|
else:
|
||||||
self.validate_internal_transfer_warehouse()
|
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"):
|
if self.doctype in ("Sales Invoice", "Delivery Note Item") and self.get("packed_items"):
|
||||||
frappe.throw(_("Packed Items cannot be transferred internally"))
|
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):
|
def validate_putaway_capacity(self):
|
||||||
# if over receipt is attempted while 'apply putaway rule' is disabled
|
# if over receipt is attempted while 'apply putaway rule' is disabled
|
||||||
# and if rule was applied on the transaction, validate it.
|
# and if rule was applied on the transaction, validate it.
|
||||||
|
|||||||
@@ -881,7 +881,9 @@ class SubcontractingController(StockController):
|
|||||||
"posting_time": self.posting_time,
|
"posting_time": self.posting_time,
|
||||||
"qty": -1 * item.consumed_qty,
|
"qty": -1 * item.consumed_qty,
|
||||||
"voucher_detail_no": item.name,
|
"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"),
|
title=_("Note"),
|
||||||
indicator="yellow",
|
indicator="yellow",
|
||||||
|
alert=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
return new_customer_name
|
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)
|
return _make_sales_order(source_name, target_doc)
|
||||||
|
|
||||||
|
|
||||||
def _make_sales_order(source_name, target_doc=None, customer_group=None, ignore_permissions=False):
|
def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
||||||
customer = _make_customer(source_name, ignore_permissions, customer_group)
|
customer = _make_customer(source_name, ignore_permissions)
|
||||||
ordered_items = frappe._dict(
|
ordered_items = frappe._dict(
|
||||||
frappe.db.get_all(
|
frappe.db.get_all(
|
||||||
"Sales Order Item",
|
"Sales Order Item",
|
||||||
@@ -507,50 +507,51 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
|
|||||||
return doclist
|
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 = 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 quotation.quotation_to == "Customer":
|
||||||
if not frappe.db.exists("Customer", quotation.get("party_name")):
|
return frappe.get_doc("Customer", quotation.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
|
|
||||||
|
|
||||||
customer_doclist = _make_customer(lead_name, ignore_permissions=ignore_permissions)
|
# If the Quotation is not to a Customer, it must be to a Lead.
|
||||||
customer = frappe.get_doc(customer_doclist)
|
# Check if a Customer already exists for the Lead.
|
||||||
customer.flags.ignore_permissions = ignore_permissions
|
existing_customer_for_lead = frappe.db.get_value("Customer", {"lead_name": quotation.party_name})
|
||||||
customer.customer_group = customer_group
|
if existing_customer_for_lead:
|
||||||
|
return frappe.get_doc("Customer", existing_customer_for_lead)
|
||||||
|
|
||||||
try:
|
# If no Customer exists for the Lead, create a new Customer.
|
||||||
customer.insert()
|
return create_customer_from_lead(quotation.party_name, ignore_permissions=ignore_permissions)
|
||||||
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]
|
|
||||||
|
|
||||||
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"))
|
def create_customer_from_lead(lead_name, ignore_permissions=False):
|
||||||
else:
|
from erpnext.crm.doctype.lead.lead import _make_customer
|
||||||
return customer_name
|
|
||||||
else:
|
customer = _make_customer(lead_name, ignore_permissions=ignore_permissions)
|
||||||
return frappe.get_doc("Customer", quotation.get("party_name"))
|
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",
|
"Stock Entry": "delivery_note_no",
|
||||||
"Quality Inspection": "reference_name",
|
"Quality Inspection": "reference_name",
|
||||||
"Auto Repeat": "reference_document",
|
"Auto Repeat": "reference_document",
|
||||||
|
"Purchase Receipt": "inter_company_reference",
|
||||||
},
|
},
|
||||||
"internal_links": {
|
"internal_links": {
|
||||||
"Sales Order": ["items", "against_sales_order"],
|
"Sales Order": ["items", "against_sales_order"],
|
||||||
@@ -22,6 +23,9 @@ def get_data():
|
|||||||
{"label": _("Reference"), "items": ["Sales Order", "Shipment", "Quality Inspection"]},
|
{"label": _("Reference"), "items": ["Sales Order", "Shipment", "Quality Inspection"]},
|
||||||
{"label": _("Returns"), "items": ["Stock Entry"]},
|
{"label": _("Returns"), "items": ["Stock Entry"]},
|
||||||
{"label": _("Subscription"), "items": ["Auto Repeat"]},
|
{"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",
|
"item_code": args.item or args.item_code or "_Test Item",
|
||||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||||
"qty": args.qty if args.get("qty") is not None else 1,
|
"qty": args.get("qty", 1),
|
||||||
"rate": args.rate if args.get("rate") is not None else 100,
|
"rate": args.get("rate", 100),
|
||||||
"conversion_factor": 1.0,
|
"conversion_factor": 1.0,
|
||||||
"serial_and_batch_bundle": bundle_id,
|
"serial_and_batch_bundle": bundle_id,
|
||||||
"allow_zero_valuation_rate": args.allow_zero_valuation_rate or 1,
|
"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,
|
get_serial_nos_from_bundle,
|
||||||
make_serial_batch_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.doctype.warehouse.test_warehouse import create_warehouse
|
||||||
from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction
|
|
||||||
|
|
||||||
|
|
||||||
class TestPurchaseReceipt(FrappeTestCase):
|
class TestPurchaseReceipt(FrappeTestCase):
|
||||||
@@ -735,7 +733,6 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
po.cancel()
|
po.cancel()
|
||||||
|
|
||||||
def test_serial_no_against_purchase_receipt(self):
|
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"
|
item_code = "Test Manual Created Serial No"
|
||||||
if not frappe.db.exists("Item", item_code):
|
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):
|
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.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.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()
|
prepare_data_for_internal_transfer()
|
||||||
|
|
||||||
@@ -1034,6 +1036,22 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
company="_Test Company with perpetual inventory",
|
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"
|
customer = "_Test Internal Customer 2"
|
||||||
company = "_Test Company with perpetual inventory"
|
company = "_Test Company with perpetual inventory"
|
||||||
|
|
||||||
@@ -1071,7 +1089,8 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
sl_entries = get_sl_entries("Purchase Receipt", pr.name)
|
sl_entries = get_sl_entries("Purchase Receipt", pr.name)
|
||||||
|
|
||||||
expected_gle = [
|
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 Stock In Hand - TCP1", 0.0, 250.0],
|
||||||
["_Test Account Shipping Charges - TCP1", 0.0, 22.5],
|
["_Test Account Shipping Charges - TCP1", 0.0, 22.5],
|
||||||
]
|
]
|
||||||
@@ -1687,7 +1706,7 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
pr.items[0].rejected_warehouse = from_warehouse
|
pr.items[0].rejected_warehouse = from_warehouse
|
||||||
pr.save()
|
pr.save()
|
||||||
|
|
||||||
self.assertRaises(OverAllowanceError, pr.submit)
|
self.assertRaises(frappe.ValidationError, pr.submit)
|
||||||
|
|
||||||
# Step 5: Test Over Receipt Allowance
|
# Step 5: Test Over Receipt Allowance
|
||||||
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 50)
|
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 50)
|
||||||
@@ -1701,6 +1720,7 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
to_warehouse=target_warehouse,
|
to_warehouse=target_warehouse,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
pr.reload()
|
||||||
pr.submit()
|
pr.submit()
|
||||||
|
|
||||||
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0)
|
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()
|
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:
|
else:
|
||||||
valuation_method = get_valuation_method(args.get("item_code"))
|
valuation_method = get_valuation_method(args.get("item_code"))
|
||||||
previous_sle = get_previous_sle(args)
|
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)
|
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):
|
def get_avg_purchase_rate(serial_nos):
|
||||||
"""get average value of serial numbers"""
|
"""get average value of serial numbers"""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user