|
| 1 | +# Copyright 2025 Binhex - Simple Solutions |
| 2 | +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). |
| 3 | + |
| 4 | +import logging |
| 5 | +from unittest.mock import patch |
| 6 | + |
| 7 | +from odoo import fields |
| 8 | +from odoo.exceptions import UserError |
| 9 | +from odoo.tests import tagged |
| 10 | + |
| 11 | +from .common import BillcomTestCommon |
| 12 | + |
| 13 | +_logger = logging.getLogger(__name__) |
| 14 | + |
| 15 | + |
| 16 | +@tagged("post_install", "-at_install", "billcom") |
| 17 | +class TestAccountPayment(BillcomTestCommon): |
| 18 | + """Test account.payment Bill.com integration""" |
| 19 | + |
| 20 | + @classmethod |
| 21 | + def setUpClass(cls): |
| 22 | + super().setUpClass() |
| 23 | + |
| 24 | + # Create a payment for testing |
| 25 | + cls.payment = cls.env["account.payment"].create( |
| 26 | + { |
| 27 | + "payment_type": "outbound", |
| 28 | + "partner_type": "supplier", |
| 29 | + "partner_id": cls.vendor_billcom.id, |
| 30 | + "amount": 100.0, |
| 31 | + "journal_id": cls.bank_journal.id, |
| 32 | + "date": fields.Date.today(), |
| 33 | + } |
| 34 | + ) |
| 35 | + |
| 36 | + def test_payment_billcom_fields(self): |
| 37 | + """Test that payments have all required Bill.com fields""" |
| 38 | + required_fields = [ |
| 39 | + "billcom", |
| 40 | + "billcom_id", |
| 41 | + "is_sync_to_billcom", |
| 42 | + "billcom_sync_status", |
| 43 | + ] |
| 44 | + |
| 45 | + for field in required_fields: |
| 46 | + self.assertTrue( |
| 47 | + hasattr(self.payment, field), f"Payment should have field: {field}" |
| 48 | + ) |
| 49 | + |
| 50 | + @patch( |
| 51 | + "odoo.addons.billcom_integration.models.billcom_service.BillcomService._make_request" |
| 52 | + ) |
| 53 | + def test_sync_payment_to_billcom(self, mock_request): |
| 54 | + """Test syncing payment to Bill.com""" |
| 55 | + mock_request.return_value = { |
| 56 | + "id": "payment_abc123", |
| 57 | + "amount": 100.0, |
| 58 | + "processDate": fields.Date.today().isoformat(), |
| 59 | + "singleStatus": "SCHEDULED", |
| 60 | + } |
| 61 | + |
| 62 | + # Trigger sync |
| 63 | + self.payment.sync_to_billcom() |
| 64 | + |
| 65 | + # Verify billcom_id was updated |
| 66 | + self.assertEqual(self.payment.billcom_id, "payment_abc123") |
| 67 | + |
| 68 | + @patch( |
| 69 | + "odoo.addons.billcom_integration.models.billcom_service.BillcomService._make_request" |
| 70 | + ) |
| 71 | + def test_payment_status_update_from_webhook(self, mock_request): |
| 72 | + """Test payment status update from Bill.com webhook""" |
| 73 | + self.payment.billcom_id = "payment_webhook_001" |
| 74 | + |
| 75 | + mock_request.return_value = { |
| 76 | + "id": "payment_webhook_001", |
| 77 | + "singleStatus": "PAID", |
| 78 | + "paidDate": fields.Date.today().isoformat(), |
| 79 | + } |
| 80 | + |
| 81 | + # Sync from Bill.com |
| 82 | + self.payment.sync_from_billcom_by_id("payment_webhook_001") |
| 83 | + |
| 84 | + # Verify payment was synced |
| 85 | + self.assertEqual(self.payment.billcom_id, "payment_webhook_001") |
| 86 | + |
| 87 | + def test_payment_for_vendor_bill(self): |
| 88 | + """Test creating payment for vendor bill""" |
| 89 | + # Post the vendor bill |
| 90 | + self.vendor_bill.action_post() |
| 91 | + |
| 92 | + # Create payment from bill |
| 93 | + payment = ( |
| 94 | + self.env["account.payment"] |
| 95 | + .with_context(active_ids=[self.vendor_bill.id], active_model="account.move") |
| 96 | + .create( |
| 97 | + { |
| 98 | + "payment_type": "outbound", |
| 99 | + "partner_type": "supplier", |
| 100 | + "partner_id": self.vendor_billcom.id, |
| 101 | + "amount": self.vendor_bill.amount_total, |
| 102 | + "journal_id": self.bank_journal.id, |
| 103 | + } |
| 104 | + ) |
| 105 | + ) |
| 106 | + |
| 107 | + self.assertEqual(payment.partner_id, self.vendor_billcom) |
| 108 | + self.assertEqual(payment.amount, self.vendor_bill.amount_total) |
| 109 | + |
| 110 | + def test_payment_multi_currency(self): |
| 111 | + """Test payment in different currency""" |
| 112 | + eur = self.env["res.currency"].search([("name", "=", "EUR")], limit=1) |
| 113 | + if not eur: |
| 114 | + eur = self.env["res.currency"].create({"name": "EUR", "symbol": "€"}) |
| 115 | + |
| 116 | + payment = self.env["account.payment"].create( |
| 117 | + { |
| 118 | + "payment_type": "outbound", |
| 119 | + "partner_type": "supplier", |
| 120 | + "partner_id": self.vendor_billcom.id, |
| 121 | + "amount": 100.0, |
| 122 | + "currency_id": eur.id, |
| 123 | + "journal_id": self.bank_journal.id, |
| 124 | + } |
| 125 | + ) |
| 126 | + |
| 127 | + self.assertEqual(payment.currency_id, eur) |
| 128 | + |
| 129 | + def test_payment_funding_account(self): |
| 130 | + """Test payment with Bill.com funding account""" |
| 131 | + # Create funding account |
| 132 | + funding_account = self.env["billcom.funding.account"].create( |
| 133 | + { |
| 134 | + "name": "Test Bank Account", |
| 135 | + "billcom_id": "funding_001", |
| 136 | + "account_type": "Checking", |
| 137 | + "last_four_digits": "1234", |
| 138 | + } |
| 139 | + ) |
| 140 | + |
| 141 | + # Payment with funding account (if field exists) |
| 142 | + if hasattr(self.payment, "billcom_funding_account_id"): |
| 143 | + self.payment.billcom_funding_account_id = funding_account |
| 144 | + self.assertEqual(self.payment.billcom_funding_account_id, funding_account) |
| 145 | + |
| 146 | + @patch( |
| 147 | + "odoo.addons.billcom_integration.models.billcom_service.BillcomService._make_request" |
| 148 | + ) |
| 149 | + def test_payment_sync_error_handling(self, mock_request): |
| 150 | + """Test error handling during payment sync""" |
| 151 | + mock_request.side_effect = UserError("Payment sync failed") |
| 152 | + |
| 153 | + with self.assertRaises(UserError): |
| 154 | + self.payment.sync_to_billcom() |
| 155 | + |
| 156 | + def test_payment_date_validation(self): |
| 157 | + """Test payment date is required""" |
| 158 | + payment = self.env["account.payment"].create( |
| 159 | + { |
| 160 | + "payment_type": "outbound", |
| 161 | + "partner_type": "supplier", |
| 162 | + "partner_id": self.vendor_billcom.id, |
| 163 | + "amount": 100.0, |
| 164 | + "journal_id": self.bank_journal.id, |
| 165 | + "date": fields.Date.today(), |
| 166 | + } |
| 167 | + ) |
| 168 | + |
| 169 | + self.assertTrue(payment.date) |
| 170 | + |
| 171 | + def test_customer_payment_not_synced_to_billcom(self): |
| 172 | + """Test customer payments (inbound) sync behavior""" |
| 173 | + customer_payment = self.env["account.payment"].create( |
| 174 | + { |
| 175 | + "payment_type": "inbound", |
| 176 | + "partner_type": "customer", |
| 177 | + "partner_id": self.customer_billcom.id, |
| 178 | + "amount": 100.0, |
| 179 | + "journal_id": self.bank_journal.id, |
| 180 | + } |
| 181 | + ) |
| 182 | + |
| 183 | + self.assertEqual(customer_payment.payment_type, "inbound") |
| 184 | + |
| 185 | + @patch( |
| 186 | + "odoo.addons.billcom_integration.models.billcom_service_abstract.BillcomServiceAbstract._make_request" # noqa B950 |
| 187 | + ) |
| 188 | + def test_update_billcom_payment_status(self, mock_request): |
| 189 | + """Test updating payment status from Bill.com""" |
| 190 | + # Setup a payment that needs update |
| 191 | + self.payment.billcom_id = "payment_to_update" |
| 192 | + self.payment.billcom_payment_status = "scheduled" |
| 193 | + self.payment.last_sync_date = fields.Datetime.now() - fields.timedelta(hours=2) |
| 194 | + |
| 195 | + # Mock response |
| 196 | + mock_request.return_value = { |
| 197 | + "id": "payment_to_update", |
| 198 | + "singleStatus": "PROCESSED", |
| 199 | + "confirmationNumber": "CONF123", |
| 200 | + } |
| 201 | + |
| 202 | + # Run update |
| 203 | + self.env["account.payment"].update_billcom_payment_status() |
| 204 | + |
| 205 | + # Verify status updated |
| 206 | + self.assertEqual(self.payment.billcom_payment_status, "processed") |
| 207 | + self.assertEqual(self.payment.billcom_confirmation_number, "CONF123") |
| 208 | + |
| 209 | + @patch( |
| 210 | + "odoo.addons.billcom_integration.models.billcom_service_abstract.BillcomServiceAbstract._make_request" # noqa B950 |
| 211 | + ) |
| 212 | + def test_action_cancel_billcom_payment(self, mock_request): |
| 213 | + """Test canceling payment in Bill.com""" |
| 214 | + self.payment.billcom_id = "payment_to_cancel" |
| 215 | + self.payment.billcom_payment_status = "scheduled" |
| 216 | + |
| 217 | + mock_request.return_value = {"id": "payment_to_cancel", "status": "CANCELED"} |
| 218 | + |
| 219 | + self.payment.action_cancel_billcom_payment() |
| 220 | + |
| 221 | + self.assertEqual(self.payment.billcom_payment_status, "canceled") |
| 222 | + self.assertEqual(self.payment.state, "cancel") |
| 223 | + |
| 224 | + @patch( |
| 225 | + "odoo.addons.billcom_integration.models.billcom_service_abstract.BillcomServiceAbstract._make_request" # noqa B950 |
| 226 | + ) |
| 227 | + def test_action_get_payment_status(self, mock_request): |
| 228 | + """Test manual status fetch""" |
| 229 | + self.payment.billcom_id = "payment_status_check" |
| 230 | + |
| 231 | + mock_request.return_value = { |
| 232 | + "id": "payment_status_check", |
| 233 | + "singleStatus": "SENT", |
| 234 | + } |
| 235 | + |
| 236 | + self.payment.action_get_payment_status() |
| 237 | + |
| 238 | + self.assertEqual(self.payment.billcom_payment_status, "sent") |
| 239 | + |
| 240 | + def test_prepare_payment_data_wallet(self): |
| 241 | + """Test payment data preparation for WALLET funding type""" |
| 242 | + self.payment.billcom_funding_account_type = "WALLET" |
| 243 | + # Wallet requires process date |
| 244 | + self.payment.billcom_process_date = fields.Date.today() + fields.timedelta( |
| 245 | + days=1 |
| 246 | + ) |
| 247 | + |
| 248 | + data = self.payment._prepare_payment_data() |
| 249 | + |
| 250 | + self.assertEqual(data["fundingAccount"]["type"], "WALLET") |
| 251 | + self.assertIn("processDate", data) |
0 commit comments