Make payment group optional per process method

This commit is contained in:
Cédric Krier committed 2024-12-15 19:06:20 +01:00
1 parent f9e4940ed7
commit 001bfe69c6
7 files changed
+84 -65

No files matched your search

+1
View File
@@ -1,3 +1,4 @@
* Group is optional per process method
Version 7.4.0 - 2024-11-04
--------------------------
+1 -1
View File
@@ -72,7 +72,7 @@ Payment Group
=============
A *Payment Group* contains `Payments <model-account.payment>` processed
together at the same time.
together at the same time for methods that need it.
The payment group is used to follow the processing of the payments.
.. seealso::
+37 -21
View File
@@ -136,6 +136,9 @@ class Group(ModelSQL, ModelView):
def search_process_method(cls, name, clause):
return [('journal.' + clause[0],) + tuple(clause[1:])]
def process_manual(self):
pass
@classmethod
def create(cls, vlist):
pool = Pool()
@@ -340,6 +343,8 @@ class Payment(Workflow, ModelSQL, ModelView):
},
domain=[
('company', '=', Eval('company', -1)),
('journal', '=', Eval('journal', -1)),
('kind', '=', Eval('kind')),
])
process_method = fields.Function(
fields.Selection('get_process_methods', "Process Method"),
@@ -457,6 +462,8 @@ class Payment(Workflow, ModelSQL, ModelView):
'approve': RPC(
readonly=False, instantiate=0, fresh_session=True),
})
cls.group.states['required'] &= Eval('process_method').in_(
cls.process_method_with_group())
@classmethod
def __register__(cls, module):
@@ -652,31 +659,33 @@ class Payment(Workflow, ModelSQL, ModelView):
def process_wizard(cls, payments):
pass
@classmethod
def process_method_with_group(cls):
return ['manual']
@classmethod
@Workflow.transition('processing')
def process(cls, payments, group):
pool = Pool()
Group = pool.get('account.payment.group')
def process(cls, payments, group=None):
if payments:
group = group()
cls.write(payments, {
'group': group.id,
})
if group:
group = group()
cls.write(payments, {
'group': group.id,
})
# Set state before calling process method
# as it may set a different state directly
cls.proceed(payments)
process_method = getattr(Group,
'process_%s' % group.journal.process_method, None)
if process_method:
process_method(group)
group.save()
if group:
getattr(group, f'process_{group.process_method}')()
else:
for payment in payments:
getattr(payment, f'process_{payment.process_method}')()
return group
@classmethod
@ModelView.button
@Workflow.transition('processing')
def proceed(cls, payments):
assert all(p.group for p in payments)
cls._check_reconciled(
[p for p in payments if p.state not in {'succeeded', 'failed'}])
@@ -716,8 +725,8 @@ class ProcessPayment(Wizard):
def _group_payment_key(self, payment):
return (
('company', payment.company.id),
('journal', payment.journal.id),
('company', payment.company),
('journal', payment.journal),
('kind', payment.kind),
)
@@ -745,18 +754,25 @@ class ProcessPayment(Wizard):
payment=payment.rec_name,
line=payment.line.rec_name))
process_method_with_group = Payment.process_method_with_group()
groups = []
payments = sorted(
payments, key=sortable_values(self._group_payment_key))
for key, grouped_payments in groupby(payments,
key=self._group_payment_key):
def group():
group = self._new_group(dict(key))
group = self._new_group(key)
group.save()
groups.append(group)
return group
Payment.process(list(grouped_payments), group)
key = dict(key)
process_method = key['journal'].process_method
group = Payment.process(
list(grouped_payments),
group if process_method in process_method_with_group else None)
if group:
groups.append(group)
return action, {
'res_id': [g.id for g in groups],
}
if groups:
return action, {
'res_id': [g.id for g in groups],
}
+17 -19
View File
@@ -84,25 +84,6 @@ class PaymentGroup(metaclass=PoolMeta):
cls._buttons['succeed']['invisible'] |= (
Eval('process_method') == 'braintree')
def process_braintree(self):
pool = Pool()
Payment = pool.get('account.payment')
for payment in self.payments:
if not payment.braintree_nonce and not payment.braintree_customer:
account = payment.journal.braintree_account
for customer in payment.party.braintree_customers:
if (customer.braintree_account == account
and customer.braintree_customer_id):
payment.braintree_customer = customer
break
else:
raise ProcessError(
gettext(
'account_payment_braintree.msg_no_braintree_nonce',
payment=payment.rec_name))
Payment.save(self.payments)
Payment.__queue__.braintree_transact(self.payments)
class CheckoutMixin:
__slots__ = ()
@@ -322,6 +303,23 @@ class Payment(CheckoutMixin, BraintreeCustomerMethodMixin, metaclass=PoolMeta):
payment.braintree_device_data = None
cls.save(payments)
def process_braintree(self):
assert self.process_method == 'braintree'
if not self.braintree_nonce and not self.braintree_customer:
account = self.journal.braintree_account
for customer in self.party.braintree_customers:
if (customer.braintree_account == account
and customer.braintree_customer_id):
self.braintree_customer = customer
break
else:
raise ProcessError(
gettext(
'account_payment_braintree.msg_no_braintree_nonce',
self=self.rec_name))
self.save()
self.__class__.__queue__.braintree_transact([self])
@classmethod
def braintree_transact(cls, payments=None):
"""Create transactions for braintree payments
@@ -3,6 +3,7 @@
from collections import defaultdict
from functools import wraps
from sql import Null
from sql.aggregate import BoolAnd, Min
from sql.conditionals import Coalesce
@@ -417,6 +418,7 @@ class Group(metaclass=PoolMeta):
query = payment.select(
payment.group,
where=payment.group != Null,
having=Operator(column, value),
group_by=payment.group)
return [('id', 'in', query)]
+8 -4
View File
@@ -216,6 +216,7 @@ class Group(metaclass=PoolMeta):
pool = Pool()
Payment = pool.get('account.payment')
Mandate = pool.get('account.payment.sepa.mandate')
assert self.process_method == 'sepa'
if self.kind == 'receivable':
payments = list(self.payments)
mandates = Payment.get_sepa_mandates(payments)
@@ -259,11 +260,11 @@ class Group(metaclass=PoolMeta):
if to_write:
Payment.write(*to_write)
self.sepa_id = uuid.uuid4().hex
self.sepa_generate_message(_save=False)
self.sepa_generate_message()
@dualmethod
@ModelView.button
def sepa_generate_message(cls, groups, _save=True):
def sepa_generate_message(cls, groups):
pool = Pool()
Message = pool.get('account.payment.sepa.message')
for group in groups:
@@ -280,8 +281,7 @@ class Group(metaclass=PoolMeta):
message = Message(message=message, type='out', state='waiting',
company=group.company)
group.sepa_messages += (message,)
if _save:
cls.save(groups)
cls.save(groups)
@property
def sepa_initiating_party(self):
@@ -383,6 +383,10 @@ class Payment(metaclass=PoolMeta):
default.setdefault('sepa_mandate_sequence_type', None)
return super(Payment, cls).copy(payments, default=default)
@classmethod
def process_method_with_group(cls):
return super().process_method_with_group() + ['sepa']
@classmethod
def get_sepa_mandates(cls, payments):
mandates = []
+18 -20
View File
@@ -71,26 +71,6 @@ class Group(metaclass=PoolMeta):
cls._buttons['succeed']['invisible'] |= (
Eval('process_method') == 'stripe')
def process_stripe(self):
pool = Pool()
Payment = pool.get('account.payment')
for payment in self.payments:
if (not payment.stripe_token
and not payment.stripe_payment_intent_id
and not payment.stripe_customer):
account = payment.journal.stripe_account
for customer in payment.party.stripe_customers:
if (customer.stripe_account == account
and customer.stripe_customer_id):
payment.stripe_customer = customer
break
else:
raise ProcessError(
gettext('account_payment_stripe.msg_no_stripe_token',
payment=payment.rec_name))
Payment.save(self.payments)
Payment.__queue__.stripe_charge(self.payments)
class CheckoutMixin:
__slots__ = ()
@@ -382,6 +362,24 @@ class Payment(StripeCustomerMethodMixin, CheckoutMixin, metaclass=PoolMeta):
payment.stripe_payment_intent_id = None
cls.save(payments)
def process_stripe(self):
assert self.process_method == 'stripe'
if (not self.stripe_token
and not self.stripe_payment_intent_id
and not self.stripe_customer):
account = self.journal.stripe_account
for customer in self.party.stripe_customers:
if (customer.stripe_account == account
and customer.stripe_customer_id):
self.stripe_customer = customer
break
else:
raise ProcessError(
gettext('account_payment_stripe.msg_no_stripe_token',
self=self.rec_name))
self.save()
self.__class__.__queue__.stripe_charge([self])
@classmethod
def stripe_checkout(cls, payments):
for payment in payments: