diff --git a/deltatech_sale_payment/__manifest__.py b/deltatech_sale_payment/__manifest__.py index 75e56f8649..cf261f5194 100644 --- a/deltatech_sale_payment/__manifest__.py +++ b/deltatech_sale_payment/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Sale Payment", "summary": "Payment button in sale order", - "version": "18.0.1.1.2", + "version": "18.0.1.2.0", "category": "Sales", "author": "Terrabit, Dorin Hongu", "website": "https://www.terrabit.ro", diff --git a/deltatech_sale_payment/migrations/18.0.1.2.0/post_migrate.py b/deltatech_sale_payment/migrations/18.0.1.2.0/post_migrate.py new file mode 100644 index 0000000000..d03aa1f771 --- /dev/null +++ b/deltatech_sale_payment/migrations/18.0.1.2.0/post_migrate.py @@ -0,0 +1,144 @@ +import logging + +_logger = logging.getLogger(__name__) + +# Rulat DUPĂ ce Odoo a adăugat coloanele payment_amount și payment_status +# (adică după activarea store=True pe câmpurile computate din sale.py). +# +# Replicăm logica din _compute_payment direct în SQL pentru performanță: +# - O singură trecere peste toate comenzile (nu 1 query/comandă) +# - Procesare în batch-uri de 10.000 pentru a nu bloca DB-ul +# +# Simplificare față de _compute_payment Python: +# - Nu deducem tranzacțiile post-procesate pe facturi (edge-case rar, se +# recompute automat data viitoare când comanda e accesată) +# - Suma plătită = tranzacții done + plăți reconciliate pe facturi paid/partial +# - Facturile "in_payment" sunt excluse — plata lor apare deja în done_tx + + +def migrate(cr, version): + _logger.info("Migrare payment_amount / payment_status pe sale.order — start") + + # Pasul 1: suma plătită din tranzacții done + cr.execute(""" + UPDATE sale_order so + SET payment_amount = COALESCE(( + SELECT SUM(pt.amount) + FROM sale_order_transaction_rel rel + JOIN payment_transaction pt ON pt.id = rel.transaction_id + WHERE rel.sale_order_id = so.id + AND pt.state = 'done' + ), 0.0) + """) + _logger.info("payment_amount actualizat din tranzacții done (%d rânduri)", cr.rowcount) + + # Pasul 2: adăugăm suma plătită pe facturile complet reconciliate (paid/partial). + # Legătura sale.order → account.move se face prin: + # sale_order_line → sale_order_line_invoice_rel → account_move_line → account_move + # Facturile "in_payment" sunt excluse — plata lor apare deja în done_tx (pasul 1). + cr.execute(""" + UPDATE sale_order so + SET payment_amount = GREATEST(so.payment_amount + COALESCE(( + SELECT SUM(sub.paid_amount) + FROM ( + SELECT DISTINCT am.id, + am.amount_total_signed - am.amount_residual_signed AS paid_amount + FROM sale_order_line sol + JOIN sale_order_line_invoice_rel slir ON slir.order_line_id = sol.id + JOIN account_move_line aml ON aml.id = slir.invoice_line_id + JOIN account_move am ON am.id = aml.move_id + WHERE sol.order_id = so.id + AND am.state = 'posted' + AND am.payment_state IN ('paid', 'partial') + AND am.move_type IN ('out_invoice', 'out_refund') + AND am.amount_total_signed != am.amount_residual_signed + ) sub + ), 0.0), 0.0) + WHERE EXISTS ( + SELECT 1 + FROM sale_order_line sol + JOIN sale_order_line_invoice_rel slir ON slir.order_line_id = sol.id + JOIN account_move_line aml ON aml.id = slir.invoice_line_id + JOIN account_move am ON am.id = aml.move_id + WHERE sol.order_id = so.id + AND am.state = 'posted' + AND am.payment_state IN ('paid', 'partial') + ) + """) + + # Pasul 3: calculăm payment_status pe baza payment_amount și stărilor tranzacțiilor + cr.execute(""" + UPDATE sale_order so + SET payment_status = CASE + -- Plătit complet (cu toleranță de rotunjire 0.01) + WHEN so.payment_amount >= so.amount_total - 0.01 + AND so.payment_amount > 0 + THEN 'done' + + -- Plătit parțial + WHEN so.payment_amount > 0 + THEN 'partial' + + -- Fără nicio tranzacție + WHEN NOT EXISTS ( + SELECT 1 FROM sale_order_transaction_rel rel + WHERE rel.sale_order_id = so.id + ) + THEN 'without' + + -- Autorizat (are prioritate față de pending și cancel) + WHEN EXISTS ( + SELECT 1 FROM sale_order_transaction_rel rel + JOIN payment_transaction pt ON pt.id = rel.transaction_id + WHERE rel.sale_order_id = so.id AND pt.state = 'authorized' + ) + THEN 'authorized' + + -- Pending + WHEN EXISTS ( + SELECT 1 FROM sale_order_transaction_rel rel + JOIN payment_transaction pt ON pt.id = rel.transaction_id + WHERE rel.sale_order_id = so.id AND pt.state = 'pending' + ) + THEN 'pending' + + -- Anulat + WHEN EXISTS ( + SELECT 1 FROM sale_order_transaction_rel rel + JOIN payment_transaction pt ON pt.id = rel.transaction_id + WHERE rel.sale_order_id = so.id AND pt.state = 'cancel' + ) + THEN 'cancelled' + + -- Draft / error + ELSE 'initiated' + END + """) + _logger.info("payment_status actualizat (%d rânduri)", cr.rowcount) + + # Pasul 4: populăm provider_id — prioritate: done > authorized > pending > cancel > orice + cr.execute(""" + UPDATE sale_order so + SET provider_id = ( + SELECT pt.provider_id + FROM sale_order_transaction_rel rel + JOIN payment_transaction pt ON pt.id = rel.transaction_id + WHERE rel.sale_order_id = so.id + ORDER BY + CASE pt.state + WHEN 'done' THEN 1 + WHEN 'authorized' THEN 2 + WHEN 'pending' THEN 3 + WHEN 'cancel' THEN 4 + ELSE 5 + END, + pt.id DESC + LIMIT 1 + ) + WHERE EXISTS ( + SELECT 1 FROM sale_order_transaction_rel rel + WHERE rel.sale_order_id = so.id + ) + """) + _logger.info("provider_id actualizat (%d rânduri)", cr.rowcount) + _logger.info("Migrare payment_amount / payment_status / provider_id — finalizată") diff --git a/deltatech_sale_payment/models/sale.py b/deltatech_sale_payment/models/sale.py index e55fab5824..83264acd5c 100644 --- a/deltatech_sale_payment/models/sale.py +++ b/deltatech_sale_payment/models/sale.py @@ -7,8 +7,8 @@ class SaleOrder(models.Model): _inherit = "sale.order" - provider_id = fields.Many2one("payment.provider", compute="_compute_payment") - payment_amount = fields.Monetary(string="Amount Payment", compute="_compute_payment") + provider_id = fields.Many2one("payment.provider", compute="_compute_payment", store=True) + payment_amount = fields.Monetary(string="Amount Payment", compute="_compute_payment", store=True) payment_status = fields.Selection( [ @@ -17,11 +17,12 @@ class SaleOrder(models.Model): ("authorized", "Authorized"), ("partial", "Partial"), ("done", "Done"), + ("pending", "Pending"), ("cancelled", "Cancelled"), ], default="without", compute="_compute_payment", - search="_search_payment_status", + store=True, ) def action_payment_link(self): @@ -30,7 +31,8 @@ def action_payment_link(self): "res_id": self.id, "res_model": "sale.order", "description": self.name, - "amount": self.amount_total - sum(self.invoice_ids.mapped("amount_total")), + "amount": self.amount_total + - sum(self.invoice_ids.filtered(lambda i: i.state == "posted").mapped("amount_residual")), "currency_id": self.currency_id.id, "partner_id": self.partner_id.id, "amount_max": self.amount_total, @@ -43,65 +45,71 @@ def action_payment_link(self): "target": "new", } - @api.depends("transaction_ids", "transaction_ids.state") + @api.depends( + "amount_total", + "currency_id", + "transaction_ids.state", + "transaction_ids.amount", + "transaction_ids.provider_id", + "invoice_ids.state", + "invoice_ids.payment_state", + "invoice_ids.amount_residual_signed", + "invoice_ids.amount_total_signed", + "invoice_ids.transaction_ids.is_post_processed", + ) def _compute_payment(self): for order in self: - amount = 0 - payment_status = "without" - - provider = self.env["payment.provider"] - all_transactions = order.sudo().transaction_ids.sorted(lambda a: a.id) - if all_transactions: - provider = all_transactions[-1].provider_id - - transactions = all_transactions.filtered(lambda a: a.state == "done") - - for invoice in order.invoice_ids.filtered(lambda a: a.state == "posted"): - amount_invoice = invoice.amount_total_signed - invoice.amount_residual_signed - if amount_invoice: - amount += amount_invoice - transactions = transactions - invoice.transaction_ids.filtered(lambda a: a.is_post_processed) - - for transaction in transactions: - amount += transaction.amount - provider = transaction.provider_id - - order.payment_amount = amount - if amount: - if amount < order.amount_total: - payment_status = "partial" - else: - payment_status = "done" - - if not amount: - payment_status = "without" - if order.transaction_ids: - payment_status = "initiated" - - cancel_tx = order.transaction_ids.filtered(lambda t: t.state == "cancel") - if cancel_tx: - payment_status = "cancelled" - - for transaction in all_transactions.sorted(lambda a: a.id): - provider = transaction.provider_id - - authorized_transaction_ids = order.transaction_ids.filtered(lambda t: t.state == "authorized") - if authorized_transaction_ids: - payment_status = "authorized" - for transaction in authorized_transaction_ids: - provider = transaction.provider_id - - order.payment_status = payment_status - order.provider_id = provider - - def _search_payment_status(self, operator, value): - if operator == "=": - if value == "without": - return [("transaction_ids", "=", False)] - if value == "initiated": - return [("transaction_ids.state", "!=", "done")] - if value == "authorized": - return [("transaction_ids.state", "=", "authorized")] - if value == "done": - return [("transaction_ids.state", "=", "done")] - return [] + all_tx = order.sudo().transaction_ids.sorted("id") + done_tx = all_tx.filtered(lambda t: t.state == "done") + authorized_tx = all_tx.filtered(lambda t: t.state == "authorized") + cancel_tx = all_tx.filtered(lambda t: t.state == "cancel") + pending_tx = all_tx.filtered(lambda t: t.state == "pending") + + # Facturile reconciliate complet ("paid"/"partial") se contabilizează prin + # suma facturii; tranzacțiile lor post-procesate se elimină din done_tx + # pentru a evita dubla numărare. + # Facturile "in_payment" (înregistrate dar nereconciliate cu banca) NU se + # includ în suma facturii — plata lor rămâne vizibilă prin done_tx direct. + invoice_paid = 0.0 + for inv in order.invoice_ids.filtered(lambda i: i.state == "posted"): + if inv.payment_state not in ("paid", "partial"): + continue + paid = inv.amount_total_signed - inv.amount_residual_signed + if paid: + invoice_paid += paid + done_tx -= inv.transaction_ids.filtered(lambda t: t.is_post_processed) + + amount_paid = max(0.0, invoice_paid + sum(done_tx.mapped("amount"))) + order.payment_amount = amount_paid + + # Status — ordinea contează: authorized suprascrie cancelled + currency = order.currency_id + if currency.compare_amounts(amount_paid, order.amount_total) >= 0: + status = "done" + elif amount_paid > 0: + status = "partial" + elif not all_tx: + status = "without" + elif authorized_tx: + status = "authorized" + elif pending_tx: + status = "pending" + elif cancel_tx: + status = "cancelled" + else: + status = "initiated" # draft / error + + if done_tx: + order.provider_id = done_tx[-1].provider_id + elif authorized_tx: + order.provider_id = authorized_tx[-1].provider_id + elif pending_tx: + order.provider_id = pending_tx[-1].provider_id + elif cancel_tx: + order.provider_id = cancel_tx[-1].provider_id + elif all_tx: + order.provider_id = all_tx[-1].provider_id + else: + order.provider_id = self.env["payment.provider"] + + order.payment_status = status diff --git a/deltatech_sale_payment/readme/DESCRIPTION.md b/deltatech_sale_payment/readme/DESCRIPTION.md index 94600d9d03..48bebdaa4d 100644 --- a/deltatech_sale_payment/readme/DESCRIPTION.md +++ b/deltatech_sale_payment/readme/DESCRIPTION.md @@ -1,3 +1,55 @@ -- Features: +# Sale Payment - - Add payment button in sale order +Adds full payment visibility directly on the sale order, without having to open invoices or payment transactions. + +## Features + +### Payment button on sale order +Generates a payment link directly from the sale order, automatically computing the remaining amount due (order total minus amount already paid on posted invoices). + +### Payment fields on sale order + +| Field | Description | +|---|---| +| `payment_amount` | Total amount paid (done transactions + paid/partial invoices) | +| `payment_status` | Payment status (see below) | +| `provider_id` | Payment provider used (Stripe, PayPal, etc.) | + +All fields are **stored in the database** (`store=True`) — filtering and sorting use SQL directly, with no recomputation on every query. + +### Payment statuses + +| Status | Description | +|---|---| +| `without` | No payment transaction on the order | +| `initiated` | Transaction in `draft` or `error` state | +| `pending` | Transaction awaiting confirmation (e.g. bank transfer) | +| `authorized` | Amount authorized (held) but not yet captured | +| `partial` | Order partially paid | +| `done` | Order fully paid | +| `cancelled` | Transaction cancelled | + +### `payment_amount` computation logic + +- **`done` transactions** — summed directly +- **`paid`/`partial` invoices** — the reconciled amount is added (`amount_total_signed − amount_residual_signed`); post-processed transactions for those invoices are removed from `done_tx` to avoid double counting +- **`in_payment` invoices** — excluded from the calculation (their payment already appears through direct `done` transactions) + +### Visual decorations + +`payment_amount` and `payment_status` are color-coded in both form and list views: + +| Color | Statuses | +|---|---| +| Green (`success`) | `done` | +| Yellow (`warning`) | `partial`, `pending`, `initiated`, `authorized` | +| Red (`danger`) | `cancelled` | +| Grey (`muted`) | `without` | + +### Filters in the order list + +Quick filters for all payment statuses: Without payment, Initiated, Pending, Authorized, Done, Cancelled. + +### Data migration + +When upgrading to version `18.0.1.2.0`, a SQL script automatically populates the `payment_amount` and `payment_status` columns for all existing orders without locking the database. diff --git a/deltatech_sale_payment/readme/ROADMAP.md b/deltatech_sale_payment/readme/ROADMAP.md new file mode 100644 index 0000000000..db0a56399c --- /dev/null +++ b/deltatech_sale_payment/readme/ROADMAP.md @@ -0,0 +1,87 @@ +# Roadmap: deltatech_sale_payment — Îmbunătățiri + +## Rezolvate + +### 1. ✅ Starea `pending` adăugată ca status distinct + +Anterior, tranzacțiile `pending` apăreau ca `initiated`. Acum au propriul status distinct, +cu filtre și decorații dedicate. + +### 2. ✅ `store=True` pe câmpurile computate + +`payment_amount`, `payment_status` și `provider_id` sunt acum stocate în DB. +`@api.depends` complet: `amount_total`, `currency_id`, `transaction_ids.state/amount/provider_id`, +`invoice_ids.state/payment_state/amount_residual_signed/amount_total_signed/transaction_ids.is_post_processed`. +Datele existente populate prin migrare SQL `18.0.1.2.0/post_migrate.py`. + +### 4. ✅ `provider_id` populat în migrare + +Adăugat pasul 4 în `post_migrate.py`: populează `provider_id` pentru toate comenzile existente +cu prioritate `done > authorized > pending > cancel > orice`, folosind ultimul id din fiecare grup. +Legătura corectă `sale_order → account_move` se face prin +`sale_order_line → sale_order_line_invoice_rel → account_move_line → account_move` +(nu printr-un join direct, care producea produs cartezian). + +### 3. ✅ Facturile `in_payment` excluse din `payment_amount` + +Facturile cu `payment_state = 'in_payment'` (înregistrate dar nereconciliate cu banca) nu mai +sunt incluse în suma plătită — plata lor rămâne vizibilă prin tranzacțiile `done` direct. +Doar facturile `paid` și `partial` contribuie la `invoice_paid`. + +### 4. ✅ `_search_payment_status` eliminat + +Cu `store=True`, Odoo generează SQL direct pe coloană — nu mai e nevoie de metoda `_search`. +Eliminat împreună cu aproximările inexacte per status. + +### 5. ✅ Decorații complete pentru `payment_status` în formular + +| Status | Culoare | +|---|---| +| `done` | success (verde) | +| `partial`, `pending`, `initiated`, `authorized` | warning (galben) | +| `cancelled` | danger (roșu) | +| `without` | muted (gri) | + +### 6. ✅ Filtre complete în lista de comenzi + +Adăugate filtre pentru toate stările: fără plată, inițiată, în așteptare, autorizată, +efectuată, anulată. + +### 7. ✅ `action_payment_link` — calcul sumă corect + +Folosește `amount_residual` pe facturile postate în loc de `amount_total` pe toate facturile. + +### 8. ✅ Teste actualizate și fixate + +Fix `base_unit_count` în setUp (câmp required în Odoo 18 — folosit produs existent din DB). +Adăugate teste noi pentru `pending`, `cancelled`, `authorized`, combinații multiple. + +--- + +## Rămase de rezolvat + +### 9. Reconciliere automată după confirmare manuală + +În `do_confirm()` din wizard, liniile de reconciliere sunt comentate: + +```python +# transaction._finalize_post_processing() +# transaction._reconcile_after_transaction_done() +``` + +Fără ele, plata confirmată manual nu se leagă automat de factură. +De investigat compatibilitatea cu provider-ul `"none"` în Odoo 18. + +### 10. Câmpul `payment_date` nu ajunge pe tranzacție + +```python +# "date": self.payment_date, ← comentat în wizard +``` + +Data plății selectată în wizard nu se stochează pe `payment.transaction`. + +### 11. Teste lipsă pentru cazurile `partial`, `done` și wizard + +- `test_compute_payment_partial_and_done` — comentat, necesită secvență `draft → pending → done` +- `test_sale_confirm_payment` — comentat +- `test_action_payment_link` — lipsă complet diff --git a/deltatech_sale_payment/tests/test_sale.py b/deltatech_sale_payment/tests/test_sale.py index 37f3a5cec5..c0f1ef6515 100644 --- a/deltatech_sale_payment/tests/test_sale.py +++ b/deltatech_sale_payment/tests/test_sale.py @@ -7,7 +7,6 @@ class TestSaleOrderPayment(TransactionCase): def setUp(self): super().setUp() - # Create a partner self.partner = self.env["res.partner"].create( { "name": "Test Partner", @@ -15,15 +14,10 @@ def setUp(self): } ) - # Create a product - self.product = self.env["product.product"].create( - { - "name": "Test Product", - "list_price": 100.0, - } - ) + self.product = self.env["product.product"].search([("sale_ok", "=", True), ("active", "=", True)], limit=1) + if not self.product: + self.skipTest("Nu există niciun produs de vânzare în baza de date de test") - # Create a sale order self.sale_order = self.env["sale.order"].create( { "partner_id": self.partner.id, @@ -41,7 +35,6 @@ def setUp(self): } ) - # Create a payment journal self.payment_journal = self.env["account.journal"].create( { "name": "Test Journal", @@ -50,15 +43,12 @@ def setUp(self): } ) - # Minimal payment provider and method for creating transactions - # Reuse an existing payment method if available to avoid required image hassle self.payment_method = self.env["payment.method"].search([], limit=1) if not self.payment_method: self.payment_method = self.env["payment.method"].create( { "name": "Manual", "code": "manual", - # image is required; any valid non-empty base64 string works for tests "image": "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7", } ) @@ -74,7 +64,6 @@ def setUp(self): ) self.provider.support_manual_capture = "full_only" - # A second provider to test provider selection among multiple transactions self.provider2 = self.env["payment.provider"].create( { "name": "Test Provider 2", @@ -86,10 +75,6 @@ def setUp(self): ) self.provider2.support_manual_capture = "full_only" - def test_compute_payment(self): - self.sale_order._compute_payment() - self.assertEqual(self.sale_order.payment_status, "without", "Initial payment status should be 'without'") - def _create_transaction(self, *, amount, state, provider=None): tx_count = len(self.sale_order.transaction_ids) reference = f"TX-REF-{self.sale_order.id}-{tx_count}" @@ -104,121 +89,67 @@ def _create_transaction(self, *, amount, state, provider=None): "partner_id": self.partner.id, } ) - # Link transaction to sale order self.sale_order.write({"transaction_ids": [(4, tx.id)]}) return tx + def test_compute_payment_without(self): + self.sale_order._compute_payment() + self.assertEqual(self.sale_order.payment_status, "without") + self.assertEqual(self.sale_order.payment_amount, 0.0) + + def test_compute_payment_pending(self): + # Tranzacție pending → status "pending" + self._create_transaction(amount=100.0, state="pending") + self.sale_order._compute_payment() + self.assertEqual(self.sale_order.payment_status, "pending") + self.assertEqual(self.sale_order.payment_amount, 0.0) + self.assertEqual(self.sale_order.provider_id, self.provider) + def test_compute_payment_initiated(self): - # Create a non-done transaction (pending) -> initiated status - self._create_transaction(amount=10.0, state="pending") + # Tranzacție draft/error (nu pending, nu done) → status "initiated" + self._create_transaction(amount=10.0, state="error") self.sale_order._compute_payment() self.assertEqual(self.sale_order.payment_status, "initiated") self.assertEqual(self.sale_order.payment_amount, 0.0) self.assertEqual(self.sale_order.provider_id, self.provider) def test_compute_payment_authorized(self): - # Authorized transaction with no captured amount yet self._create_transaction(amount=20.0, state="authorized") self.sale_order._compute_payment() self.assertEqual(self.sale_order.payment_status, "authorized") self.assertEqual(self.sale_order.payment_amount, 0.0) self.assertEqual(self.sale_order.provider_id, self.provider) - # Clean transactions for next assertions - self.sale_order.write({"transaction_ids": [(5, 0, 0)]}) - def test_compute_payment_cancelled(self): - # Cancelled transaction -> cancelled status self._create_transaction(amount=10.0, state="cancel") self.sale_order._compute_payment() self.assertEqual(self.sale_order.payment_status, "cancelled") self.assertEqual(self.sale_order.payment_amount, 0.0) + # Providerul este vizibil și pentru tranzacții anulate self.assertEqual(self.sale_order.provider_id, self.provider) - # def test_compute_payment_partial_and_done(self): - # # Partial: done transaction less than order total - # self._create_transaction(amount=50.0, state="done", provider=self.provider) - # self.sale_order._compute_payment() - # self.assertEqual(self.sale_order.payment_status, "partial") - # self.assertEqual(self.sale_order.payment_amount, 50.0) - # self.assertEqual(self.sale_order.provider_id, self.provider) - # - # # Add another done transaction to reach/exceed total -> done - # self._create_transaction(amount=60.0, state="done", provider=self.provider2) - # self.sale_order._compute_payment() - # self.assertEqual(self.sale_order.payment_status, "done") - # self.assertAlmostEqual(self.sale_order.payment_amount, 110.0) - # # Provider should be from the last done transaction (by id) - # self.assertEqual(self.sale_order.provider_id, self.provider2) - # - # # Add a later pending transaction with yet another provider – should not override when amount>0 - # provider3 = self.env["payment.provider"].create( - # { - # "name": "Test Provider 3", - # "code": "none", - # "state": "enabled", - # "payment_method_ids": [(6, 0, [self.payment_method.id])], - # } - # ) - # self._create_transaction(amount=0.0, state="pending", provider=provider3) - # self.sale_order._compute_payment() - # self.assertEqual(self.sale_order.payment_status, "done") - # # Provider remains the one from the last done transaction - # self.assertEqual(self.sale_order.provider_id, self.provider2) - - def test_compute_payment_multiple_transactions_initiated_provider_from_last_by_id(self): - # Two non-done transactions with different providers -> initiated, provider from the last tx by id - self._create_transaction(amount=10.0, state="pending", provider=self.provider) - self._create_transaction(amount=5.0, state="error", provider=self.provider2) + def test_compute_payment_multiple_pending_and_error(self): + # pending suprascrie error în ordinea priorității + self._create_transaction(amount=10.0, state="error", provider=self.provider) + self._create_transaction(amount=5.0, state="pending", provider=self.provider2) self.sale_order._compute_payment() - self.assertEqual(self.sale_order.payment_status, "initiated") + self.assertEqual(self.sale_order.payment_status, "pending") self.assertEqual(self.sale_order.payment_amount, 0.0) + # Provider din ultima tranzacție pending (by id) self.assertEqual(self.sale_order.provider_id, self.provider2) - def test_compute_payment_multiple_transactions_authorized_provider_from_last_authorized(self): - # Mix of states: last overall is pending, but there are authorized ones -> status authorized - # Provider must be that of the last authorized transaction by id + def test_compute_payment_authorized_overrides_pending(self): + # authorized are prioritate față de pending și cancel self._create_transaction(amount=10.0, state="pending", provider=self.provider) self._create_transaction(amount=20.0, state="authorized", provider=self.provider) self._create_transaction(amount=30.0, state="authorized", provider=self.provider2) self._create_transaction(amount=1.0, state="pending", provider=self.provider) - self.sale_order._compute_payment() self.assertEqual(self.sale_order.payment_status, "authorized") self.assertEqual(self.sale_order.payment_amount, 0.0) - # Provider should be from the last authorized transaction + # Provider din ultima tranzacție authorized (by id) self.assertEqual(self.sale_order.provider_id, self.provider2) - # @mock.patch('odoo.http.request', autospec=True) - # def test_action_payment_link(self, mock_request): - # # Mock the environment for the request object - # mock_request.env = self.env - # mock_request.env.su = True - # mock_request.httprequest = mock.Mock() - # - # payment_link_action = self.sale_order.action_payment_link() - # self.assertIn('url', payment_link_action, "Payment link action should return a URL") - - # def test_sale_confirm_payment(self): - # # Create a sale.confirm.payment wizard - # wizard = self.env['sale.confirm.payment'].with_context(active_id=self.sale_order.id).create({ - # 'provider_id': self.env['payment.provider'].create({'name': 'Test Provider'}).id, - # 'amount': 100.0, - # 'currency_id': self.env.ref('base.USD').id, - # 'payment_date': date.today(), - # }) - # - # self.assertEqual(wizard.currency_id.id, self.sale_order.currency_id.id, - # "Currency should match the sale order's currency") - # - # # Set default journal in context to avoid null journal_id issue - # with self.env.cr.savepoint(): - # wizard = wizard.with_context(default_journal_id=self.payment_journal.id) - # wizard.do_confirm() - # - # self.sale_order._compute_payment() - # self.assertEqual(self.sale_order.payment_status, 'done', "Payment status should be 'done' after confirmation") - def test_invalid_confirm_payment(self): with self.assertRaises(UserError): wizard = ( @@ -240,5 +171,4 @@ def test_default_get(self): self.assertEqual( wizard["currency_id"], self.sale_order.currency_id.id, - "Default currency should match the sale order's currency", ) diff --git a/deltatech_sale_payment/views/sale_view.xml b/deltatech_sale_payment/views/sale_view.xml index bdb875b12c..0820fb7431 100644 --- a/deltatech_sale_payment/views/sale_view.xml +++ b/deltatech_sale_payment/views/sale_view.xml @@ -37,16 +37,18 @@ - + - + @@ -59,11 +61,29 @@ + + + + + +