From 001bfe69c666894b71bf67de8caaa97d75947053 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Sun, 15 Dec 2024 19:06:20 +0100 Subject: [PATCH] Make payment group optional per process method --- modules/account_payment/CHANGELOG | 1 + modules/account_payment/doc/design.rst | 2 +- modules/account_payment/payment.py | 58 +++++++++++++------- modules/account_payment_braintree/account.py | 36 ++++++------ modules/account_payment_clearing/payment.py | 2 + modules/account_payment_sepa/payment.py | 12 ++-- modules/account_payment_stripe/payment.py | 38 ++++++------- 7 files changed, 84 insertions(+), 65 deletions(-) diff --git a/modules/account_payment/CHANGELOG b/modules/account_payment/CHANGELOG index cbfd58b41a..378eedd36a 100644 --- a/modules/account_payment/CHANGELOG +++ b/modules/account_payment/CHANGELOG @@ -1,3 +1,4 @@ +* Group is optional per process method Version 7.4.0 - 2024-11-04 -------------------------- diff --git a/modules/account_payment/doc/design.rst b/modules/account_payment/doc/design.rst index 5f7957680b..82426e2c8c 100644 --- a/modules/account_payment/doc/design.rst +++ b/modules/account_payment/doc/design.rst @@ -72,7 +72,7 @@ Payment Group ============= A *Payment Group* contains `Payments ` 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:: diff --git a/modules/account_payment/payment.py b/modules/account_payment/payment.py index 36fc5dec55..be410faade 100644 --- a/modules/account_payment/payment.py +++ b/modules/account_payment/payment.py @@ -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], + } diff --git a/modules/account_payment_braintree/account.py b/modules/account_payment_braintree/account.py index 381061809c..cf3eb733d3 100644 --- a/modules/account_payment_braintree/account.py +++ b/modules/account_payment_braintree/account.py @@ -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 diff --git a/modules/account_payment_clearing/payment.py b/modules/account_payment_clearing/payment.py index d0a5957a23..ecb16481b8 100644 --- a/modules/account_payment_clearing/payment.py +++ b/modules/account_payment_clearing/payment.py @@ -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)] diff --git a/modules/account_payment_sepa/payment.py b/modules/account_payment_sepa/payment.py index 687fb3d335..96ce2c9a62 100644 --- a/modules/account_payment_sepa/payment.py +++ b/modules/account_payment_sepa/payment.py @@ -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 = [] diff --git a/modules/account_payment_stripe/payment.py b/modules/account_payment_stripe/payment.py index 758556a9fb..71fe49ae34 100644 --- a/modules/account_payment_stripe/payment.py +++ b/modules/account_payment_stripe/payment.py @@ -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: