260 lines
11 KiB
Python
260 lines
11 KiB
Python
from unittest.mock import patch
|
|
|
|
import odoo
|
|
|
|
from odoo.addons.point_of_sale.tests.common import TestPoSCommon
|
|
|
|
|
|
@odoo.tests.tagged('-at_install', 'post_install') # Only runs after installation, not at install time
|
|
class TestPOSCustomerAccountReconciliation(TestPoSCommon):
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.config = self.basic_config
|
|
self.order_data = [{
|
|
"pos_order_lines_ui_args": [(self.product_a, 1)],
|
|
"payments": [(self.pay_later_pm, 1150)],
|
|
"customer": self.partner_a,
|
|
"is_invoiced": True,
|
|
}, {
|
|
"pos_order_lines_ui_args": [(self.product_a, 1)],
|
|
"payments": [(self.pay_later_pm, 1150)],
|
|
"customer": self.partner_a,
|
|
"is_invoiced": False,
|
|
}, {
|
|
"pos_order_lines_ui_args": [(self.product_a, 1)],
|
|
"payments": [(self.pay_later_pm, 1150)],
|
|
"customer": self.partner_a,
|
|
"is_invoiced": False,
|
|
}, {
|
|
"pos_order_lines_ui_args": [(self.product_a, 1)],
|
|
"payments": [(self.pay_later_pm, 1150)],
|
|
"customer": self.partner_b,
|
|
"is_invoiced": True,
|
|
}, {
|
|
"pos_order_lines_ui_args": [(self.product_a, 1)],
|
|
"payments": [(self.pay_later_pm, 1150)],
|
|
"customer": self.partner_b,
|
|
"is_invoiced": False,
|
|
}]
|
|
|
|
def _create_paid_orders(self):
|
|
orders_dict = self._create_orders(self.order_data)
|
|
orders = self.env["pos.order"]
|
|
for order in orders_dict.values():
|
|
order.action_pos_order_paid()
|
|
orders |= order
|
|
return orders
|
|
|
|
def _get_unreconciled_moves(self, partner):
|
|
move_lines = self.env['account.move.line'].search([
|
|
('partner_id', '=', partner.id),
|
|
('account_id', '=', partner.property_account_receivable_id.id),
|
|
('reconciled', '=', False)
|
|
])
|
|
|
|
return move_lines
|
|
|
|
def _create_settle_order_lines_data(self, due_orders):
|
|
lines = []
|
|
for order in due_orders:
|
|
lines.append({'product': self.config.settle_due_product_id,
|
|
'quantity': 1,
|
|
'settled_order_id': order.id if not order.is_invoiced else False,
|
|
'settled_invoice_id': order.account_move.id if order.is_invoiced else False,
|
|
"qty": 0.0,
|
|
"price_unit": order.amount_paid,
|
|
"price_subtotal": 0.0,
|
|
"price_subtotal_incl": 0.0,
|
|
"price_type": "manual",
|
|
"discount": 0.0,
|
|
"refunded_qty": 0.0,
|
|
"price_extra": 0.0
|
|
})
|
|
return lines
|
|
|
|
def _create_settle_order(self, due_orders, is_invoiced=False):
|
|
result = []
|
|
order_data = []
|
|
for partner_id in due_orders.mapped('partner_id'):
|
|
partner_due_orders = due_orders.filtered(lambda o: o.partner_id.id == partner_id.id)
|
|
total_due = sum(order.amount_paid for order in partner_due_orders)
|
|
payments = [(self.bank_pm1, total_due)]
|
|
payments.append((self.pay_later_pm, -total_due))
|
|
order_data.append({
|
|
"pos_order_lines_ui_args": self._create_settle_order_lines_data(
|
|
partner_due_orders
|
|
),
|
|
"payments": payments,
|
|
"customer": partner_id,
|
|
"is_invoiced": is_invoiced,
|
|
})
|
|
order_data = [self.create_ui_order_data(**params) for params in order_data]
|
|
for data in order_data:
|
|
data['state'] = 'paid'
|
|
data['amount_paid'] = 0
|
|
|
|
order_ids = [order['id'] for order in self.env['pos.order'].sync_from_ui(order_data)['pos.order']]
|
|
for order_id in self.env["pos.order"].browse(order_ids):
|
|
result += order_id
|
|
|
|
return result
|
|
|
|
def _perform_test_customer_account_payment_is_reconciled(self, is_invoiced):
|
|
session1 = self.open_new_session()
|
|
orders = self._create_paid_orders()
|
|
session1.close_session_from_ui()
|
|
self.assertTrue(self._get_unreconciled_moves(self.partner_a))
|
|
self.assertTrue(self._get_unreconciled_moves(self.partner_b))
|
|
session2 = self.open_new_session()
|
|
self._create_settle_order(orders, is_invoiced)
|
|
session2.close_session_from_ui()
|
|
self.assertFalse(self._get_unreconciled_moves(self.partner_a))
|
|
self.assertFalse(self._get_unreconciled_moves(self.partner_b))
|
|
|
|
def test_customer_account_payment_is_reconciled_when_settlement_order_invoiced(self):
|
|
self._perform_test_customer_account_payment_is_reconciled(True)
|
|
|
|
def test_customer_account_payment_is_reconciled_when_settlement_order_not_invoiced(self):
|
|
self._perform_test_customer_account_payment_is_reconciled(False)
|
|
|
|
def test_customer_account_payment_is_reconciled_when_settlement_on_same_session(self):
|
|
session1 = self.open_new_session()
|
|
orders = self._create_paid_orders()
|
|
self._create_settle_order(orders, False)
|
|
session1.close_session_from_ui()
|
|
self.assertFalse(self._get_unreconciled_moves(self.partner_a))
|
|
self.assertFalse(self._get_unreconciled_moves(self.partner_b))
|
|
|
|
def test_different_customer_invoices_settlement_same_session_reconciled_separately(self):
|
|
self.partner_a.property_account_receivable_id = self.partner_b.property_account_receivable_id
|
|
invoice_a = self._create_invoice_one_line(partner_id=self.partner_a, price_unit=1000)
|
|
invoice_b = self._create_invoice_one_line(partner_id=self.partner_b, price_unit=2000)
|
|
(invoice_a + invoice_b).action_post()
|
|
session1 = self.open_new_session()
|
|
order_data = [
|
|
{
|
|
"pos_order_lines_ui_args": [{
|
|
'product': self.config.settle_invoice_product_id,
|
|
'quantity': 1,
|
|
'settled_order_id': False,
|
|
'settled_invoice_id': invoice_a.id,
|
|
"qty": 0.0,
|
|
"price_unit": 1000,
|
|
"price_subtotal": 0.0,
|
|
"price_subtotal_incl": 0.0,
|
|
"price_type": "manual",
|
|
"discount": 0.0,
|
|
"refunded_qty": 0.0,
|
|
"price_extra": 0.0,
|
|
}],
|
|
"payments": [(self.bank_pm1, 500)],
|
|
"customer": self.partner_a,
|
|
"is_invoiced": True,
|
|
},
|
|
{
|
|
"pos_order_lines_ui_args": [{
|
|
'product': self.config.settle_invoice_product_id,
|
|
'quantity': 1,
|
|
'settled_order_id': False,
|
|
'settled_invoice_id': invoice_b.id,
|
|
"qty": 0.0,
|
|
"price_unit": 2000,
|
|
"price_subtotal": 0.0,
|
|
"price_subtotal_incl": 0.0,
|
|
"price_type": "manual",
|
|
"discount": 0.0,
|
|
"refunded_qty": 0.0,
|
|
"price_extra": 0.0
|
|
}],
|
|
"payments": [(self.bank_pm1, 1000)],
|
|
"customer": self.partner_b,
|
|
"is_invoiced": True,
|
|
},
|
|
]
|
|
order_data = [self.create_ui_order_data(**params) for params in order_data]
|
|
order_data[0].update({
|
|
'state': 'paid',
|
|
'amount_total': '500',
|
|
})
|
|
order_data[1].update({
|
|
'state': 'paid',
|
|
'amount_total': '1000',
|
|
})
|
|
self.env['pos.order'].sync_from_ui(order_data)
|
|
session1.close_session_from_ui()
|
|
invoice_lines_matching = self.env['account.move.line'].search([('matching_number', '=', invoice_a.line_ids[-1].matching_number)])
|
|
self.assertEqual(len(invoice_lines_matching.partner_id), 1, "Only aml from same partner should be reconciled together")
|
|
|
|
def test_pos_amount_unsettled_after_session_close(self):
|
|
""" Partially settling an invoice in the POS must leave a correct
|
|
pos_amount_unsettled once the session is closed.
|
|
|
|
While the session is being closed, the settle payment is reconciled
|
|
with the invoice (lowering amount_residual_signed and marking
|
|
pos_amount_unsettled for recomputation) before the session state is
|
|
set to 'closed'. If any flush executes the pending recomputation in
|
|
that window, the settle line is deducted a second time
|
|
(residual - settled instead of residual) and, as the session state
|
|
was not a dependency, closing the session did not correct the value.
|
|
The flush after the reconciliation simulates that recomputation.
|
|
"""
|
|
invoice = self._create_invoice_one_line(partner_id=self.partner_a, price_unit=1000)
|
|
invoice.action_post()
|
|
self.assertEqual(invoice.pos_amount_unsettled, 1000)
|
|
|
|
session = self.open_new_session()
|
|
order_data = self.create_ui_order_data(
|
|
pos_order_lines_ui_args=[{
|
|
'product': self.config.settle_invoice_product_id,
|
|
'quantity': 1,
|
|
'settled_order_id': False,
|
|
'settled_invoice_id': invoice.id,
|
|
"qty": 0.0,
|
|
"price_unit": 700,
|
|
"price_subtotal": 0.0,
|
|
"price_subtotal_incl": 0.0,
|
|
"price_type": "manual",
|
|
"discount": 0.0,
|
|
"refunded_qty": 0.0,
|
|
"price_extra": 0.0,
|
|
}],
|
|
payments=[(self.bank_pm1, 700), (self.pay_later_pm, -700)],
|
|
customer=self.partner_a,
|
|
)
|
|
order_data['state'] = 'paid'
|
|
order_data['amount_paid'] = 0
|
|
self.env['pos.order'].sync_from_ui([order_data])
|
|
self.assertEqual(invoice.pos_amount_unsettled, 300)
|
|
|
|
PosSession = self.env.registry['pos.session']
|
|
original_reconcile = PosSession._reconcile_account_move_lines
|
|
|
|
def reconcile_and_flush(session_self, data):
|
|
result = original_reconcile(session_self, data)
|
|
session_self.env['account.move'].flush_model()
|
|
return result
|
|
|
|
with patch.object(PosSession, '_reconcile_account_move_lines', reconcile_and_flush):
|
|
session.close_session_from_ui()
|
|
|
|
self.assertEqual(invoice.amount_residual_signed, 300)
|
|
self.assertEqual(invoice.pos_amount_unsettled, 300)
|
|
self.assertEqual(self.partner_a.invoices_amount_due, 300)
|
|
|
|
def test_settlement_invoiced_later(self):
|
|
session = self.open_new_session()
|
|
order_dict = self._create_orders([{
|
|
"pos_order_lines_ui_args": [(self.product_a, 1)],
|
|
"payments": [(self.pay_later_pm, 1150)],
|
|
"customer": self.partner_a,
|
|
"is_invoiced": False,
|
|
}])
|
|
order = next(iter(order_dict.values()))
|
|
order.action_pos_order_paid()
|
|
self._create_settle_order(order, False)
|
|
session.close_session_from_ui()
|
|
order._generate_pos_order_invoice()
|
|
self.assertEqual(order.state, 'done')
|
|
self.assertEqual(order.account_move.payment_state, 'paid')
|