feat: add Pending status for delivery/technician tasks

- New 'pending' status allows tasks to be created without a schedule,
  acting as a queue for unscheduled work that gets assigned later
- Pending group appears in the Delivery Map sidebar with amber color
- Other modules can create tasks in pending state for scheduling
- scheduled_date no longer required (null for pending tasks)
- New Pending Tasks menu item under Field Service
- Pending filter added to search view

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
gsinghpal
2026-02-24 04:21:05 -05:00
parent 84c009416e
commit 0e1aebe60b
26 changed files with 2735 additions and 9 deletions

View File

@@ -0,0 +1,386 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from werkzeug.urls import url_encode
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools.urls import urljoin as url_join
from odoo.addons.fusion_poynt import const
from odoo.addons.fusion_poynt import utils as poynt_utils
from odoo.addons.fusion_poynt.controllers.main import PoyntController
_logger = logging.getLogger(__name__)
class PaymentTransaction(models.Model):
_inherit = 'payment.transaction'
poynt_order_id = fields.Char(
string="Poynt Order ID",
readonly=True,
copy=False,
)
poynt_transaction_id = fields.Char(
string="Poynt Transaction ID",
readonly=True,
copy=False,
)
# === BUSINESS METHODS - PAYMENT FLOW === #
def _get_specific_processing_values(self, processing_values):
"""Override of payment to return Poynt-specific processing values.
For direct (online) payments we create a Poynt order upfront and return
identifiers plus the return URL so the frontend JS can complete the flow.
"""
if self.provider_code != 'poynt':
return super()._get_specific_processing_values(processing_values)
if self.operation == 'online_token':
return {}
poynt_data = self._poynt_create_order_and_authorize()
base_url = self.provider_id.get_base_url()
return_url = url_join(
base_url,
f'{PoyntController._return_url}?{url_encode({"reference": self.reference})}',
)
return {
'poynt_order_id': poynt_data.get('order_id', ''),
'poynt_transaction_id': poynt_data.get('transaction_id', ''),
'return_url': return_url,
'business_id': self.provider_id.poynt_business_id,
'is_test': self.provider_id.state == 'test',
}
def _send_payment_request(self):
"""Override of `payment` to send a payment request to Poynt."""
if self.provider_code != 'poynt':
return super()._send_payment_request()
if self.operation in ('online_token', 'offline'):
return self._poynt_process_token_payment()
poynt_data = self._poynt_create_order_and_authorize()
if poynt_data:
payment_data = {
'reference': self.reference,
'poynt_order_id': poynt_data.get('order_id'),
'poynt_transaction_id': poynt_data.get('transaction_id'),
'poynt_status': poynt_data.get('status', 'AUTHORIZED'),
'funding_source': poynt_data.get('funding_source', {}),
}
self._process('poynt', payment_data)
def _poynt_create_order_and_authorize(self):
"""Create a Poynt order and authorize the transaction.
:return: Dict with order_id, transaction_id, status, and funding_source.
:rtype: dict
"""
try:
order_payload = poynt_utils.build_order_payload(
self.reference, self.amount, self.currency_id,
)
order_result = self.provider_id._poynt_make_request(
'POST', 'orders', payload=order_payload,
)
order_id = order_result.get('id', '')
self.poynt_order_id = order_id
action = 'AUTHORIZE' if self.provider_id.capture_manually else 'SALE'
txn_payload = poynt_utils.build_transaction_payload(
action=action,
amount=self.amount,
currency=self.currency_id,
order_id=order_id,
reference=self.reference,
)
txn_result = self.provider_id._poynt_make_request(
'POST', 'transactions', payload=txn_payload,
)
transaction_id = txn_result.get('id', '')
self.poynt_transaction_id = transaction_id
self.provider_reference = transaction_id
return {
'order_id': order_id,
'transaction_id': transaction_id,
'status': txn_result.get('status', ''),
'funding_source': txn_result.get('fundingSource', {}),
}
except ValidationError as e:
self._set_error(str(e))
return {}
def _poynt_process_token_payment(self):
"""Process a payment using a stored token (card on file).
For token-based payments we send a SALE or AUTHORIZE using the
stored card ID from the payment token.
"""
try:
action = 'AUTHORIZE' if self.provider_id.capture_manually else 'SALE'
funding_source = {
'type': 'CREDIT_DEBIT',
'card': {
'cardId': self.token_id.poynt_card_id,
},
}
order_payload = poynt_utils.build_order_payload(
self.reference, self.amount, self.currency_id,
)
order_result = self.provider_id._poynt_make_request(
'POST', 'orders', payload=order_payload,
)
order_id = order_result.get('id', '')
self.poynt_order_id = order_id
txn_payload = poynt_utils.build_transaction_payload(
action=action,
amount=self.amount,
currency=self.currency_id,
order_id=order_id,
reference=self.reference,
funding_source=funding_source,
)
txn_result = self.provider_id._poynt_make_request(
'POST', 'transactions', payload=txn_payload,
)
transaction_id = txn_result.get('id', '')
self.poynt_transaction_id = transaction_id
self.provider_reference = transaction_id
payment_data = {
'reference': self.reference,
'poynt_order_id': order_id,
'poynt_transaction_id': transaction_id,
'poynt_status': txn_result.get('status', ''),
'funding_source': txn_result.get('fundingSource', {}),
}
self._process('poynt', payment_data)
except ValidationError as e:
self._set_error(str(e))
def _send_refund_request(self):
"""Override of `payment` to send a refund request to Poynt."""
if self.provider_code != 'poynt':
return super()._send_refund_request()
source_tx = self.source_transaction_id
refund_amount = abs(self.amount)
minor_amount = poynt_utils.format_poynt_amount(refund_amount, self.currency_id)
try:
refund_payload = {
'action': 'REFUND',
'parentId': source_tx.provider_reference,
'amounts': {
'transactionAmount': minor_amount,
'orderAmount': minor_amount,
'currency': self.currency_id.name,
},
'context': {
'source': 'WEB',
'sourceApp': 'odoo.fusion_poynt',
},
'notes': f'Refund for {source_tx.reference}',
}
result = self.provider_id._poynt_make_request(
'POST', 'transactions', payload=refund_payload,
)
self.provider_reference = result.get('id', '')
self.poynt_transaction_id = result.get('id', '')
payment_data = {
'reference': self.reference,
'poynt_transaction_id': result.get('id'),
'poynt_status': result.get('status', 'REFUNDED'),
}
self._process('poynt', payment_data)
except ValidationError as e:
self._set_error(str(e))
def _send_capture_request(self):
"""Override of `payment` to send a capture request to Poynt."""
if self.provider_code != 'poynt':
return super()._send_capture_request()
source_tx = self.source_transaction_id
minor_amount = poynt_utils.format_poynt_amount(self.amount, self.currency_id)
try:
capture_payload = {
'action': 'CAPTURE',
'parentId': source_tx.provider_reference,
'amounts': {
'transactionAmount': minor_amount,
'orderAmount': minor_amount,
'currency': self.currency_id.name,
},
'context': {
'source': 'WEB',
'sourceApp': 'odoo.fusion_poynt',
},
}
result = self.provider_id._poynt_make_request(
'POST', 'transactions', payload=capture_payload,
)
payment_data = {
'reference': self.reference,
'poynt_transaction_id': result.get('id'),
'poynt_status': result.get('status', 'CAPTURED'),
}
self._process('poynt', payment_data)
except ValidationError as e:
self._set_error(str(e))
def _send_void_request(self):
"""Override of `payment` to send a void request to Poynt."""
if self.provider_code != 'poynt':
return super()._send_void_request()
source_tx = self.source_transaction_id
try:
void_payload = {
'action': 'VOID',
'parentId': source_tx.provider_reference,
'context': {
'source': 'WEB',
'sourceApp': 'odoo.fusion_poynt',
},
}
result = self.provider_id._poynt_make_request(
'POST', 'transactions', payload=void_payload,
)
payment_data = {
'reference': self.reference,
'poynt_transaction_id': result.get('id'),
'poynt_status': result.get('status', 'VOIDED'),
}
self._process('poynt', payment_data)
except ValidationError as e:
self._set_error(str(e))
# === BUSINESS METHODS - NOTIFICATION PROCESSING === #
@api.model
def _search_by_reference(self, provider_code, payment_data):
"""Override of payment to find the transaction based on Poynt data."""
if provider_code != 'poynt':
return super()._search_by_reference(provider_code, payment_data)
reference = payment_data.get('reference')
if reference:
tx = self.search([
('reference', '=', reference),
('provider_code', '=', 'poynt'),
])
else:
poynt_txn_id = payment_data.get('poynt_transaction_id')
if poynt_txn_id:
tx = self.search([
('poynt_transaction_id', '=', poynt_txn_id),
('provider_code', '=', 'poynt'),
])
else:
_logger.warning("Received Poynt data with no reference or transaction ID")
tx = self
if not tx:
_logger.warning(
"No transaction found matching Poynt reference %s", reference,
)
return tx
def _apply_updates(self, payment_data):
"""Override of `payment` to update the transaction based on Poynt data."""
if self.provider_code != 'poynt':
return super()._apply_updates(payment_data)
poynt_txn_id = payment_data.get('poynt_transaction_id')
if poynt_txn_id:
self.provider_reference = poynt_txn_id
self.poynt_transaction_id = poynt_txn_id
poynt_order_id = payment_data.get('poynt_order_id')
if poynt_order_id:
self.poynt_order_id = poynt_order_id
funding_source = payment_data.get('funding_source', {})
if funding_source:
card_details = poynt_utils.extract_card_details(funding_source)
if card_details.get('brand'):
payment_method = self.env['payment.method']._get_from_code(
card_details['brand'],
mapping=const.CARD_BRAND_MAPPING,
)
if payment_method:
self.payment_method_id = payment_method
status = payment_data.get('poynt_status', '')
if not status:
self._set_error(_("Received data with missing transaction status."))
return
odoo_state = poynt_utils.get_poynt_status(status)
if odoo_state == 'authorized':
self._set_authorized()
elif odoo_state == 'done':
self._set_done()
if self.operation == 'refund':
self.env.ref('payment.cron_post_process_payment_tx')._trigger()
elif odoo_state == 'cancel':
self._set_canceled()
elif odoo_state == 'refund':
self._set_done()
self.env.ref('payment.cron_post_process_payment_tx')._trigger()
elif odoo_state == 'error':
error_msg = payment_data.get('error_message', _("Payment was declined by Poynt."))
self._set_error(error_msg)
else:
_logger.warning(
"Received unknown Poynt status (%s) for transaction %s.",
status, self.reference,
)
self._set_error(
_("Received data with unrecognized status: %s.", status)
)
def _extract_token_values(self, payment_data):
"""Override of `payment` to return token data based on Poynt data."""
if self.provider_code != 'poynt':
return super()._extract_token_values(payment_data)
funding_source = payment_data.get('funding_source', {})
card_details = poynt_utils.extract_card_details(funding_source)
if not card_details:
_logger.warning(
"Tokenization requested but no card data in payment response."
)
return {}
return {
'payment_details': card_details.get('last4', ''),
'poynt_card_id': card_details.get('card_id', ''),
}