Make payment group optional per process method
This commit is contained in:
1 parent
f9e4940ed7
commit
001bfe69c6
7 files changed
+84
-65
No files matched your search
@@ -1,3 +1,4 @@
|
||||
* Group is optional per process method
|
||||
|
||||
Version 7.4.0 - 2024-11-04
|
||||
--------------------------
|
||||
|
||||
@@ -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::
|
||||
|
||||
@@ -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],
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user