Merge branch 'version-15-hotfix' into mergify/bp/version-15-hotfix/pr-38234
This commit is contained in:
commit
c84c97577f
@ -14,6 +14,7 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
|
|||||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
from erpnext.accounts.party import get_party_account
|
from erpnext.accounts.party import get_party_account
|
||||||
|
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||||
from erpnext.stock.doctype.item.test_item import create_item
|
from erpnext.stock.doctype.item.test_item import create_item
|
||||||
|
|
||||||
test_dependencies = ["Item"]
|
test_dependencies = ["Item"]
|
||||||
@ -85,26 +86,44 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
self.customer5 = make_customer("_Test PR Customer 5", "EUR")
|
self.customer5 = make_customer("_Test PR Customer 5", "EUR")
|
||||||
|
|
||||||
def create_account(self):
|
def create_account(self):
|
||||||
account_name = "Debtors EUR"
|
accounts = [
|
||||||
if not frappe.db.get_value(
|
{
|
||||||
"Account", filters={"account_name": account_name, "company": self.company}
|
"attribute": "debtors_eur",
|
||||||
):
|
"account_name": "Debtors EUR",
|
||||||
acc = frappe.new_doc("Account")
|
"parent_account": "Accounts Receivable - _PR",
|
||||||
acc.account_name = account_name
|
"account_currency": "EUR",
|
||||||
acc.parent_account = "Accounts Receivable - _PR"
|
"account_type": "Receivable",
|
||||||
acc.company = self.company
|
},
|
||||||
acc.account_currency = "EUR"
|
{
|
||||||
acc.account_type = "Receivable"
|
"attribute": "creditors_usd",
|
||||||
acc.insert()
|
"account_name": "Payable USD",
|
||||||
else:
|
"parent_account": "Accounts Payable - _PR",
|
||||||
name = frappe.db.get_value(
|
"account_currency": "USD",
|
||||||
"Account",
|
"account_type": "Payable",
|
||||||
filters={"account_name": account_name, "company": self.company},
|
},
|
||||||
fieldname="name",
|
]
|
||||||
pluck=True,
|
|
||||||
)
|
for x in accounts:
|
||||||
acc = frappe.get_doc("Account", name)
|
x = frappe._dict(x)
|
||||||
self.debtors_eur = acc.name
|
if not frappe.db.get_value(
|
||||||
|
"Account", filters={"account_name": x.account_name, "company": self.company}
|
||||||
|
):
|
||||||
|
acc = frappe.new_doc("Account")
|
||||||
|
acc.account_name = x.account_name
|
||||||
|
acc.parent_account = x.parent_account
|
||||||
|
acc.company = self.company
|
||||||
|
acc.account_currency = x.account_currency
|
||||||
|
acc.account_type = x.account_type
|
||||||
|
acc.insert()
|
||||||
|
else:
|
||||||
|
name = frappe.db.get_value(
|
||||||
|
"Account",
|
||||||
|
filters={"account_name": x.account_name, "company": self.company},
|
||||||
|
fieldname="name",
|
||||||
|
pluck=True,
|
||||||
|
)
|
||||||
|
acc = frappe.get_doc("Account", name)
|
||||||
|
setattr(self, x.attribute, acc.name)
|
||||||
|
|
||||||
def create_sales_invoice(
|
def create_sales_invoice(
|
||||||
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||||
@ -151,6 +170,64 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
payment.posting_date = posting_date
|
payment.posting_date = posting_date
|
||||||
return payment
|
return payment
|
||||||
|
|
||||||
|
def create_purchase_invoice(
|
||||||
|
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Helper function to populate default values in sales invoice
|
||||||
|
"""
|
||||||
|
pinv = make_purchase_invoice(
|
||||||
|
qty=qty,
|
||||||
|
rate=rate,
|
||||||
|
company=self.company,
|
||||||
|
customer=self.supplier,
|
||||||
|
item_code=self.item,
|
||||||
|
item_name=self.item,
|
||||||
|
cost_center=self.cost_center,
|
||||||
|
warehouse=self.warehouse,
|
||||||
|
debit_to=self.debit_to,
|
||||||
|
parent_cost_center=self.cost_center,
|
||||||
|
update_stock=0,
|
||||||
|
currency="INR",
|
||||||
|
is_pos=0,
|
||||||
|
is_return=0,
|
||||||
|
return_against=None,
|
||||||
|
income_account=self.income_account,
|
||||||
|
expense_account=self.expense_account,
|
||||||
|
do_not_save=do_not_save,
|
||||||
|
do_not_submit=do_not_submit,
|
||||||
|
)
|
||||||
|
return pinv
|
||||||
|
|
||||||
|
def create_purchase_order(
|
||||||
|
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Helper function to populate default values in sales invoice
|
||||||
|
"""
|
||||||
|
pord = create_purchase_order(
|
||||||
|
qty=qty,
|
||||||
|
rate=rate,
|
||||||
|
company=self.company,
|
||||||
|
customer=self.supplier,
|
||||||
|
item_code=self.item,
|
||||||
|
item_name=self.item,
|
||||||
|
cost_center=self.cost_center,
|
||||||
|
warehouse=self.warehouse,
|
||||||
|
debit_to=self.debit_to,
|
||||||
|
parent_cost_center=self.cost_center,
|
||||||
|
update_stock=0,
|
||||||
|
currency="INR",
|
||||||
|
is_pos=0,
|
||||||
|
is_return=0,
|
||||||
|
return_against=None,
|
||||||
|
income_account=self.income_account,
|
||||||
|
expense_account=self.expense_account,
|
||||||
|
do_not_save=do_not_save,
|
||||||
|
do_not_submit=do_not_submit,
|
||||||
|
)
|
||||||
|
return pord
|
||||||
|
|
||||||
def clear_old_entries(self):
|
def clear_old_entries(self):
|
||||||
doctype_list = [
|
doctype_list = [
|
||||||
"GL Entry",
|
"GL Entry",
|
||||||
@ -163,13 +240,11 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
for doctype in doctype_list:
|
for doctype in doctype_list:
|
||||||
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
|
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
|
||||||
|
|
||||||
def create_payment_reconciliation(self):
|
def create_payment_reconciliation(self, party_is_customer=True):
|
||||||
pr = frappe.new_doc("Payment Reconciliation")
|
pr = frappe.new_doc("Payment Reconciliation")
|
||||||
pr.company = self.company
|
pr.company = self.company
|
||||||
pr.party_type = (
|
pr.party_type = "Customer" if party_is_customer else "Supplier"
|
||||||
self.party_type if hasattr(self, "party_type") and self.party_type else "Customer"
|
pr.party = self.customer if party_is_customer else self.supplier
|
||||||
)
|
|
||||||
pr.party = self.customer
|
|
||||||
pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
|
pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
|
||||||
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
|
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
|
||||||
return pr
|
return pr
|
||||||
@ -906,9 +981,13 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
self.assertEqual(pr.allocation[0].difference_amount, 0)
|
self.assertEqual(pr.allocation[0].difference_amount, 0)
|
||||||
|
|
||||||
def test_reconciliation_purchase_invoice_against_return(self):
|
def test_reconciliation_purchase_invoice_against_return(self):
|
||||||
pi = make_purchase_invoice(
|
self.supplier = "_Test Supplier USD"
|
||||||
supplier="_Test Supplier USD", currency="USD", conversion_rate=50
|
pi = self.create_purchase_invoice(qty=5, rate=50, do_not_submit=True)
|
||||||
).submit()
|
pi.supplier = self.supplier
|
||||||
|
pi.currency = "USD"
|
||||||
|
pi.conversion_rate = 50
|
||||||
|
pi.credit_to = self.creditors_usd
|
||||||
|
pi.save().submit()
|
||||||
|
|
||||||
pi_return = frappe.get_doc(pi.as_dict())
|
pi_return = frappe.get_doc(pi.as_dict())
|
||||||
pi_return.name = None
|
pi_return.name = None
|
||||||
@ -918,11 +997,12 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
pi_return.items[0].qty = -pi_return.items[0].qty
|
pi_return.items[0].qty = -pi_return.items[0].qty
|
||||||
pi_return.submit()
|
pi_return.submit()
|
||||||
|
|
||||||
self.company = "_Test Company"
|
pr = frappe.get_doc("Payment Reconciliation")
|
||||||
self.party_type = "Supplier"
|
pr.company = self.company
|
||||||
self.customer = "_Test Supplier USD"
|
pr.party_type = "Supplier"
|
||||||
|
pr.party = self.supplier
|
||||||
pr = self.create_payment_reconciliation()
|
pr.receivable_payable_account = self.creditors_usd
|
||||||
|
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
|
||||||
pr.get_unreconciled_entries()
|
pr.get_unreconciled_entries()
|
||||||
|
|
||||||
invoices = []
|
invoices = []
|
||||||
@ -931,6 +1011,7 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
if invoice.invoice_number == pi.name:
|
if invoice.invoice_number == pi.name:
|
||||||
invoices.append(invoice.as_dict())
|
invoices.append(invoice.as_dict())
|
||||||
break
|
break
|
||||||
|
|
||||||
for payment in pr.payments:
|
for payment in pr.payments:
|
||||||
if payment.reference_name == pi_return.name:
|
if payment.reference_name == pi_return.name:
|
||||||
payments.append(payment.as_dict())
|
payments.append(payment.as_dict())
|
||||||
@ -941,6 +1022,121 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
# Should not raise frappe.exceptions.ValidationError: Total Debit must be equal to Total Credit.
|
# Should not raise frappe.exceptions.ValidationError: Total Debit must be equal to Total Credit.
|
||||||
pr.reconcile()
|
pr.reconcile()
|
||||||
|
|
||||||
|
def test_reconciliation_from_purchase_order_to_multiple_invoices(self):
|
||||||
|
"""
|
||||||
|
Reconciling advance payment from PO/SO to multiple invoices should not cause overallocation
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.supplier = "_Test Supplier"
|
||||||
|
|
||||||
|
pi1 = self.create_purchase_invoice(qty=10, rate=100)
|
||||||
|
pi2 = self.create_purchase_invoice(qty=10, rate=100)
|
||||||
|
po = self.create_purchase_order(qty=20, rate=100)
|
||||||
|
pay = get_payment_entry(po.doctype, po.name)
|
||||||
|
# Overpay Puchase Order
|
||||||
|
pay.paid_amount = 3000
|
||||||
|
pay.save().submit()
|
||||||
|
# assert total allocated and unallocated before reconciliation
|
||||||
|
self.assertEqual(
|
||||||
|
(
|
||||||
|
pay.references[0].reference_doctype,
|
||||||
|
pay.references[0].reference_name,
|
||||||
|
pay.references[0].allocated_amount,
|
||||||
|
),
|
||||||
|
(po.doctype, po.name, 2000),
|
||||||
|
)
|
||||||
|
self.assertEqual(pay.total_allocated_amount, 2000)
|
||||||
|
self.assertEqual(pay.unallocated_amount, 1000)
|
||||||
|
self.assertEqual(pay.difference_amount, 0)
|
||||||
|
|
||||||
|
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
|
||||||
|
self.assertEqual(len(pr.invoices), 2)
|
||||||
|
self.assertEqual(len(pr.payments), 2)
|
||||||
|
|
||||||
|
for x in pr.payments:
|
||||||
|
self.assertEqual((x.reference_type, x.reference_name), (pay.doctype, pay.name))
|
||||||
|
|
||||||
|
invoices = [x.as_dict() for x in pr.invoices]
|
||||||
|
payments = [x.as_dict() for x in pr.payments]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
# partial allocation on pi1 and full allocate on pi2
|
||||||
|
pr.allocation[0].allocated_amount = 100
|
||||||
|
pr.reconcile()
|
||||||
|
|
||||||
|
# assert references and total allocated and unallocated amount
|
||||||
|
pay.reload()
|
||||||
|
self.assertEqual(len(pay.references), 3)
|
||||||
|
self.assertEqual(
|
||||||
|
(
|
||||||
|
pay.references[0].reference_doctype,
|
||||||
|
pay.references[0].reference_name,
|
||||||
|
pay.references[0].allocated_amount,
|
||||||
|
),
|
||||||
|
(po.doctype, po.name, 900),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
(
|
||||||
|
pay.references[1].reference_doctype,
|
||||||
|
pay.references[1].reference_name,
|
||||||
|
pay.references[1].allocated_amount,
|
||||||
|
),
|
||||||
|
(pi1.doctype, pi1.name, 100),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
(
|
||||||
|
pay.references[2].reference_doctype,
|
||||||
|
pay.references[2].reference_name,
|
||||||
|
pay.references[2].allocated_amount,
|
||||||
|
),
|
||||||
|
(pi2.doctype, pi2.name, 1000),
|
||||||
|
)
|
||||||
|
self.assertEqual(pay.total_allocated_amount, 2000)
|
||||||
|
self.assertEqual(pay.unallocated_amount, 1000)
|
||||||
|
self.assertEqual(pay.difference_amount, 0)
|
||||||
|
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
self.assertEqual(len(pr.invoices), 1)
|
||||||
|
self.assertEqual(len(pr.payments), 2)
|
||||||
|
|
||||||
|
invoices = [x.as_dict() for x in pr.invoices]
|
||||||
|
payments = [x.as_dict() for x in pr.payments]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
pr.reconcile()
|
||||||
|
|
||||||
|
# assert references and total allocated and unallocated amount
|
||||||
|
pay.reload()
|
||||||
|
self.assertEqual(len(pay.references), 3)
|
||||||
|
# PO references should be removed now
|
||||||
|
self.assertEqual(
|
||||||
|
(
|
||||||
|
pay.references[0].reference_doctype,
|
||||||
|
pay.references[0].reference_name,
|
||||||
|
pay.references[0].allocated_amount,
|
||||||
|
),
|
||||||
|
(pi1.doctype, pi1.name, 100),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
(
|
||||||
|
pay.references[1].reference_doctype,
|
||||||
|
pay.references[1].reference_name,
|
||||||
|
pay.references[1].allocated_amount,
|
||||||
|
),
|
||||||
|
(pi2.doctype, pi2.name, 1000),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
(
|
||||||
|
pay.references[2].reference_doctype,
|
||||||
|
pay.references[2].reference_name,
|
||||||
|
pay.references[2].allocated_amount,
|
||||||
|
),
|
||||||
|
(pi1.doctype, pi1.name, 900),
|
||||||
|
)
|
||||||
|
self.assertEqual(pay.total_allocated_amount, 2000)
|
||||||
|
self.assertEqual(pay.unallocated_amount, 1000)
|
||||||
|
self.assertEqual(pay.difference_amount, 0)
|
||||||
|
|
||||||
def test_rounding_of_unallocated_amount(self):
|
def test_rounding_of_unallocated_amount(self):
|
||||||
self.supplier = "_Test Supplier USD"
|
self.supplier = "_Test Supplier USD"
|
||||||
pi = self.create_purchase_invoice(qty=1, rate=10, do_not_submit=True)
|
pi = self.create_purchase_invoice(qty=1, rate=10, do_not_submit=True)
|
||||||
@ -975,7 +1171,6 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
# Should not raise frappe.exceptions.ValidationError: Payment Entry has been modified after you pulled it. Please pull it again.
|
# Should not raise frappe.exceptions.ValidationError: Payment Entry has been modified after you pulled it. Please pull it again.
|
||||||
pr.reconcile()
|
pr.reconcile()
|
||||||
|
|
||||||
|
|
||||||
def make_customer(customer_name, currency=None):
|
def make_customer(customer_name, currency=None):
|
||||||
if not frappe.db.exists("Customer", customer_name):
|
if not frappe.db.exists("Customer", customer_name):
|
||||||
customer = frappe.new_doc("Customer")
|
customer = frappe.new_doc("Customer")
|
||||||
|
@ -648,7 +648,7 @@ def update_reference_in_payment_entry(
|
|||||||
"outstanding_amount": d.outstanding_amount,
|
"outstanding_amount": d.outstanding_amount,
|
||||||
"allocated_amount": d.allocated_amount,
|
"allocated_amount": d.allocated_amount,
|
||||||
"exchange_rate": d.exchange_rate if d.exchange_gain_loss else payment_entry.get_exchange_rate(),
|
"exchange_rate": d.exchange_rate if d.exchange_gain_loss else payment_entry.get_exchange_rate(),
|
||||||
"exchange_gain_loss": d.exchange_gain_loss, # only populated from invoice in case of advance allocation
|
"exchange_gain_loss": d.exchange_gain_loss,
|
||||||
"account": d.account,
|
"account": d.account,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -661,22 +661,21 @@ def update_reference_in_payment_entry(
|
|||||||
existing_row.reference_doctype, existing_row.reference_name
|
existing_row.reference_doctype, existing_row.reference_name
|
||||||
).set_total_advance_paid()
|
).set_total_advance_paid()
|
||||||
|
|
||||||
original_row = existing_row.as_dict().copy()
|
if d.allocated_amount <= existing_row.allocated_amount:
|
||||||
existing_row.update(reference_details)
|
existing_row.allocated_amount -= d.allocated_amount
|
||||||
|
|
||||||
if d.allocated_amount < original_row.allocated_amount:
|
|
||||||
new_row = payment_entry.append("references")
|
new_row = payment_entry.append("references")
|
||||||
new_row.docstatus = 1
|
new_row.docstatus = 1
|
||||||
for field in list(reference_details):
|
for field in list(reference_details):
|
||||||
new_row.set(field, original_row[field])
|
new_row.set(field, reference_details[field])
|
||||||
|
|
||||||
new_row.allocated_amount = original_row.allocated_amount - d.allocated_amount
|
|
||||||
else:
|
else:
|
||||||
new_row = payment_entry.append("references")
|
new_row = payment_entry.append("references")
|
||||||
new_row.docstatus = 1
|
new_row.docstatus = 1
|
||||||
new_row.update(reference_details)
|
new_row.update(reference_details)
|
||||||
|
|
||||||
payment_entry.flags.ignore_validate_update_after_submit = True
|
payment_entry.flags.ignore_validate_update_after_submit = True
|
||||||
|
payment_entry.clear_unallocated_reference_document_rows()
|
||||||
payment_entry.setup_party_account_field()
|
payment_entry.setup_party_account_field()
|
||||||
payment_entry.set_missing_values()
|
payment_entry.set_missing_values()
|
||||||
if not skip_ref_details_update_for_pe:
|
if not skip_ref_details_update_for_pe:
|
||||||
|
@ -69,8 +69,14 @@ class Timesheet(Document):
|
|||||||
|
|
||||||
def update_billing_hours(self, args):
|
def update_billing_hours(self, args):
|
||||||
if args.is_billable:
|
if args.is_billable:
|
||||||
if flt(args.billing_hours) == 0.0 or flt(args.billing_hours) > flt(args.hours):
|
if flt(args.billing_hours) == 0.0:
|
||||||
args.billing_hours = args.hours
|
args.billing_hours = args.hours
|
||||||
|
elif flt(args.billing_hours) > flt(args.hours):
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Warning - Row {0}: Billing Hours are more than Actual Hours").format(args.idx),
|
||||||
|
indicator="orange",
|
||||||
|
alert=True,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
args.billing_hours = 0
|
args.billing_hours = 0
|
||||||
|
|
||||||
|
@ -1784,10 +1784,10 @@ class TestSalesOrder(FrappeTestCase):
|
|||||||
si.submit()
|
si.submit()
|
||||||
pe.load_from_db()
|
pe.load_from_db()
|
||||||
|
|
||||||
self.assertEqual(pe.references[0].reference_name, si.name)
|
self.assertEqual(pe.references[0].reference_name, so.name)
|
||||||
self.assertEqual(pe.references[0].allocated_amount, 200)
|
self.assertEqual(pe.references[0].allocated_amount, 300)
|
||||||
self.assertEqual(pe.references[1].reference_name, so.name)
|
self.assertEqual(pe.references[1].reference_name, si.name)
|
||||||
self.assertEqual(pe.references[1].allocated_amount, 300)
|
self.assertEqual(pe.references[1].allocated_amount, 200)
|
||||||
|
|
||||||
def test_delivered_item_material_request(self):
|
def test_delivered_item_material_request(self):
|
||||||
"SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO."
|
"SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO."
|
||||||
|
Loading…
x
Reference in New Issue
Block a user