diff --git a/__init__.py b/__init__.py index 76f7b19904..228177666a 100644 --- a/__init__.py +++ b/__init__.py @@ -21,6 +21,7 @@ def register(): payment.Payment, payment.CustomerSourceDetachAsk, party.Party, + party.PartyReceptionDirectDebit, ir.Cron, module='account_payment_stripe', type_='model') Pool.register( diff --git a/common.py b/common.py new file mode 100644 index 0000000000..10afd5c1d6 --- /dev/null +++ b/common.py @@ -0,0 +1,144 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. +from trytond.model import fields +from trytond.pyson import Eval + + +class StripeCustomerMethodMixin: + __slots__ = () + + stripe_account = fields.Function(fields.Many2One( + 'account.payment.stripe.account', "Stripe Account"), + 'on_change_with_stripe_account') + + stripe_customer = fields.Many2One( + 'account.payment.stripe.customer', "Stripe Customer", + domain=[ + ('party', '=', Eval('party', -1)), + ('stripe_account', '=', Eval('stripe_account', -1)), + ], + states={ + 'invisible': Eval('process_method') != 'stripe', + 'readonly': ( + ~Eval('party') | (Eval('party', -1) < 0) + | ~Eval('stripe_account') | (Eval('stripe_account', -1) < 0)), + }, + depends=['party', 'stripe_account', 'process_method']) + + stripe_customer_source = fields.Char( + "Stripe Customer Source", + states={ + 'invisible': ( + (Eval('process_method') != 'stripe') + | ~Eval('stripe_customer') + | Eval('stripe_customer_payment_method')), + }, + depends=[ + 'process_method', 'stripe_customer', + 'stripe_customer_payment_method']) + # Use Function field with selection to avoid to query Stripe + # to validate the value + stripe_customer_source_selection = fields.Function(fields.Selection( + 'get_stripe_customer_sources', "Stripe Customer Source", + states={ + 'invisible': ( + (Eval('process_method') != 'stripe') + | ~Eval('stripe_customer') + | Eval('stripe_customer_payment_method')), + }, + depends=[ + 'process_method', 'stripe_customer', + 'stripe_customer_payment_method']), + 'get_stripe_customer_source', setter='set_stripe_customer_source') + + stripe_customer_payment_method = fields.Char( + "Stripe Payment Method", + states={ + 'invisible': ( + (Eval('process_method') != 'stripe') + | ~Eval('stripe_customer') + | Eval('stripe_customer_source')), + }, + depends=[ + 'process_method', 'stripe_customer', 'stripe_customer_source']) + # Use Function field with selection to avoid to query Stripe + # to validate the value + stripe_customer_payment_method_selection = fields.Function( + fields.Selection( + 'get_stripe_customer_payment_methods', + "Stripe Customer Payment Method", + states={ + 'invisible': ( + (Eval('process_method') != 'stripe') + | ~Eval('stripe_customer') + | Eval('stripe_customer_source')), + }, + depends=[ + 'process_method', 'stripe_customer', + 'stripe_customer_source']), + 'get_stripe_customer_payment_method', + setter='set_stripe_customer_payment_method') + + def on_change_party(self): + try: + super().on_change_party() + except AttributeError: + pass + self.stripe_customer = None + self.stripe_customer_source = None + self.stripe_customer_source_selection = None + + @fields.depends('journal') + def on_change_with_stripe_account(self, name=None): + if self.journal and self.journal.process_method == 'stripe': + return self.journal.stripe_account.id + + @fields.depends('stripe_customer', 'stripe_customer_source') + def get_stripe_customer_sources(self): + sources = [('', '')] + if self.stripe_customer: + sources.extend(self.stripe_customer.sources()) + if (self.stripe_customer_source + and self.stripe_customer_source not in dict(sources)): + sources.append( + (self.stripe_customer_source, self.stripe_customer_source)) + return sources + + @fields.depends( + 'stripe_customer_source_selection', + 'stripe_customer_source') + def on_change_stripe_customer_source_selection(self): + self.stripe_customer_source = self.stripe_customer_source_selection + + def get_stripe_customer_source(self, name): + return self.stripe_customer_source + + @classmethod + def set_stripe_customer_source(cls, payments, name, value): + pass + + @fields.depends('stripe_customer', 'stripe_customer_payment_method') + def get_stripe_customer_payment_methods(self): + methods = [('', '')] + if self.stripe_customer: + methods.extend(self.stripe_customer.payment_methods()) + if (self.stripe_customer_payment_method + and self.stripe_customer_payment_method not in dict(methods)): + methods.append( + (self.stripe_customer_payment_method, + self.stripe_customer_payment_method)) + return methods + + @fields.depends( + 'stripe_customer_payment_method_selection', + 'stripe_customer_payment_method') + def on_change_stripe_customer_payment_method_selection(self): + self.stripe_customer_payment_method = ( + self.stripe_customer_payment_method_selection) + + def get_stripe_customer_payment_method(self, name): + return self.stripe_customer_payment_method + + @classmethod + def set_stripe_customer_payment_method(cls, payments, name, value): + pass diff --git a/party.py b/party.py index ea0d6aee4b..09addafc0f 100644 --- a/party.py +++ b/party.py @@ -3,6 +3,8 @@ from trytond.pool import PoolMeta, Pool from trytond.model import fields +from .common import StripeCustomerMethodMixin + class Party(metaclass=PoolMeta): __name__ = 'party.party' @@ -29,6 +31,19 @@ class Party(metaclass=PoolMeta): Customer.__queue__.stripe_update(to_update) +class PartyReceptionDirectDebit( + StripeCustomerMethodMixin, metaclass=PoolMeta): + __name__ = 'party.party.reception_direct_debit' + + def _get_payment(self, line, date, amount): + payment = super()._get_payment(line, date, amount) + self.stripe_customer = self.stripe_customer + self.stripe_customer_source = self.stripe_customer_source + self.stripe_customer_payment_method = ( + self.stripe_customer_payment_method) + return payment + + class Replace(metaclass=PoolMeta): __name__ = 'party.replace' diff --git a/party.xml b/party.xml new file mode 100644 index 0000000000..ac6b0b98c8 --- /dev/null +++ b/party.xml @@ -0,0 +1,12 @@ + + + + + + party.party.reception_direct_debit + + party_reception_direct_debit_form + + + diff --git a/payment.py b/payment.py index d665b72bad..783e9429b0 100644 --- a/payment.py +++ b/payment.py @@ -16,7 +16,7 @@ from trytond.i18n import gettext from trytond.model import ( ModelSQL, ModelView, Workflow, DeactivableMixin, fields, dualmethod) from trytond.pool import PoolMeta, Pool -from trytond.pyson import Eval, Bool +from trytond.pyson import Eval from trytond.report import Report, get_email from trytond.rpc import RPC from trytond.sendmail import sendmail_transactional @@ -30,6 +30,8 @@ from trytond.modules.account_payment.exceptions import ( ProcessError, PaymentValidationError) from trytond.modules.currency.fields import Monetary +from .common import StripeCustomerMethodMixin + logger = logging.getLogger(__name__) stripe.max_network_retries = config.getint( 'account_payment_stripe', 'max_network_retries', default=3) @@ -118,7 +120,7 @@ class CheckoutMixin: } -class Payment(CheckoutMixin, metaclass=PoolMeta): +class Payment(StripeCustomerMethodMixin, CheckoutMixin, metaclass=PoolMeta): __name__ = 'account.payment' stripe_checkout_needed = fields.Function( @@ -192,76 +194,6 @@ class Payment(CheckoutMixin, metaclass=PoolMeta): states={ 'invisible': ~Eval('stripe_dispute_status'), }) - stripe_customer = fields.Many2One( - 'account.payment.stripe.customer', "Stripe Customer", - domain=[ - ('party', '=', Eval('party', -1)), - ('stripe_account', '=', Eval('stripe_account', -1)), - ], - states={ - 'invisible': Eval('process_method') != 'stripe', - 'required': Bool(Eval('stripe_customer_source')), - 'readonly': (~Eval('state').in_(['draft', 'approved']) - | Eval('stripe_token') | Eval('stripe_payment_intent_id')), - }, - depends=['party', 'stripe_account', 'process_method', - 'stripe_customer_source', 'stripe_token', - 'stripe_payment_intent_id', 'state']) - stripe_customer_source = fields.Char( - "Stripe Customer Source", - states={ - 'invisible': ((Eval('process_method') != 'stripe') - | Eval('stripe_token') - | Eval('stripe_payment_intent_id') - | ~Eval('stripe_customer')), - 'readonly': ~Eval('state').in_(['draft', 'approved']), - }, - depends=['process_method', 'stripe_token', 'stripe_payment_intent_id', - 'stripe_customer', 'state']) - # Use Function field with selection to avoid to query Stripe - # to validate the value - stripe_customer_source_selection = fields.Function(fields.Selection( - 'get_stripe_customer_sources', "Stripe Customer Source", - states={ - 'invisible': ((Eval('process_method') != 'stripe') - | Eval('stripe_token') - | Eval('stripe_payment_intent_id') - | ~Eval('stripe_customer')), - 'readonly': ~Eval('state').in_(['draft', 'approved']), - }, - depends=[ - 'process_method', 'stripe_token', 'stripe_customer', 'state']), - 'get_stripe_customer_source', setter='set_stripe_customer_source') - stripe_customer_payment_method = fields.Char( - "Stripe Payment Method", - states={ - 'invisible': ((Eval('process_method') != 'stripe') - | Eval('stripe_token') - | ~Eval('stripe_customer')), - 'readonly': (~Eval('state').in_(['draft', 'approved']) - | Eval('stripe_payment_intent_id')), - }, - depends=['process_method', 'stripe_token', 'stripe_customer', 'state']) - # Use Function field with selection to avoid to query Stripe - # to validate the value - stripe_customer_payment_method_selection = fields.Function( - fields.Selection( - 'get_stripe_customer_payment_methods', - "Stripe Customer Payment Method", - states={ - 'invisible': ((Eval('process_method') != 'stripe') - | Eval('stripe_token') - | ~Eval('stripe_customer')), - 'readonly': (~Eval('state').in_(['draft', 'approved']) - | Eval('stripe_payment_intent_id')), - }, - depends=[ - 'process_method', 'stripe_token', 'stripe_customer', 'state']), - 'get_stripe_customer_payment_method', - setter='set_stripe_customer_payment_method') - stripe_account = fields.Function(fields.Many2One( - 'account.payment.stripe.account', "Stripe Account"), - 'on_change_with_stripe_account') stripe_amount = fields.Function( fields.Integer("Stripe Amount"), 'get_stripe_amount', setter='set_stripe_amount') @@ -278,6 +210,44 @@ class Payment(CheckoutMixin, metaclass=PoolMeta): @classmethod def __setup__(cls): super(Payment, cls).__setup__() + + cls.stripe_customer.states['readonly'] = ( + ~Eval('state').in_(['draft', 'approved']) + | Eval('stripe_token') + | Eval('stripe_payment_intent_id')) + cls.stripe_customer.depends.extend( + ['state', 'stripe_token', 'stripe_payment_intent_id']) + + cls.stripe_customer_source.states['invisible'] |= ( + Eval('stripe_token') | Eval('stripe_payment_intent_id')) + cls.stripe_customer_source.states['readonly'] = ( + ~Eval('state').in_(['draft', 'approved'])) + cls.stripe_customer_source.depends.extend( + ['stripe_token', 'stripe_payment_intent_id', 'state']) + + cls.stripe_customer_source_selection.states['invisible'] |= ( + Eval('stripe_token') | Eval('stripe_payment_intent_id')) + cls.stripe_customer_source_selection.states['readonly'] = ( + ~Eval('state').in_(['draft', 'approved'])) + cls.stripe_customer_source_selection.depends.extend( + ['stripe_token', 'stripe_payment_intent_id', 'state']) + + cls.stripe_customer_payment_method.states['invisible'] |= ( + Eval('stripe_token')) + cls.stripe_customer_payment_method.states['readonly'] = ( + ~Eval('state').in_(['draft', 'approved']) + | Eval('stripe_payment_intent_id')) + cls.stripe_customer_payment_method.depends.extend( + ['stripe_token', 'state', 'stripe_payment_intent_id']) + + cls.stripe_customer_payment_method_selection.states['invisible'] |= ( + Eval('stripe_token')) + cls.stripe_customer_payment_method_selection.states['readonly'] = ( + ~Eval('state').in_(['draft', 'approved']) + | Eval('stripe_payment_intent_id')) + cls.stripe_customer_payment_method_selection.depends.extend( + ['stripe_token', 'state', 'stripe_payment_intent_id']) + cls.amount.states['readonly'] &= ~Eval('stripe_capture_needed') cls.amount.depends.append('stripe_capture_needed') cls.stripe_amount.states.update(cls.amount.states) @@ -330,63 +300,6 @@ class Payment(CheckoutMixin, metaclass=PoolMeta): def default_stripe_idempotency_key(cls): return uuid.uuid4().hex - @fields.depends('party') - def on_change_party(self): - super(Payment, self).on_change_party() - self.stripe_customer = None - self.stripe_customer_source = None - self.stripe_customer_source_selection = None - - @fields.depends('stripe_customer', 'stripe_customer_source') - def get_stripe_customer_sources(self): - sources = [('', '')] - if self.stripe_customer: - sources.extend(self.stripe_customer.sources()) - if (self.stripe_customer_source - and self.stripe_customer_source not in dict(sources)): - sources.append( - (self.stripe_customer_source, self.stripe_customer_source)) - return sources - - @fields.depends( - 'stripe_customer_source_selection', - 'stripe_customer_source') - def on_change_stripe_customer_source_selection(self): - self.stripe_customer_source = self.stripe_customer_source_selection - - def get_stripe_customer_source(self, name): - return self.stripe_customer_source - - @classmethod - def set_stripe_customer_source(cls, payments, name, value): - pass - - @fields.depends('stripe_customer', 'stripe_customer_payment_method') - def get_stripe_customer_payment_methods(self): - methods = [('', '')] - if self.stripe_customer: - methods.extend(self.stripe_customer.payment_methods()) - if (self.stripe_customer_payment_method - and self.stripe_customer_payment_method not in dict(methods)): - methods.append( - (self.stripe_customer_payment_method, - self.stripe_customer_payment_method)) - return methods - - @fields.depends( - 'stripe_customer_payment_method_selection', - 'stripe_customer_payment_method') - def on_change_stripe_customer_payment_method_selection(self): - self.stripe_customer_payment_method = ( - self.stripe_customer_payment_method_selection) - - def get_stripe_customer_payment_method(self, name): - return self.stripe_customer_payment_method - - @classmethod - def set_stripe_customer_payment_method(cls, payments, name, value): - pass - @fields.depends('process_method', 'stripe_token', 'stripe_payment_intent_id', 'stripe_customer_source', 'stripe_customer_source_selection', @@ -406,11 +319,6 @@ class Payment(CheckoutMixin, metaclass=PoolMeta): and not self.stripe_capture and not self.stripe_captured) - @fields.depends('journal') - def on_change_with_stripe_account(self, name=None): - if self.journal and self.journal.process_method == 'stripe': - return self.journal.stripe_account.id - def get_stripe_amount(self, name): return int(self.amount * 10 ** self.currency.digits) diff --git a/tryton.cfg b/tryton.cfg index cf6f5e08fa..9b6b48e55f 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -7,4 +7,5 @@ depends: party xml: payment.xml + party.xml message.xml diff --git a/view/party_reception_direct_debit_form.xml b/view/party_reception_direct_debit_form.xml new file mode 100644 index 0000000000..23e70d38bf --- /dev/null +++ b/view/party_reception_direct_debit_form.xml @@ -0,0 +1,18 @@ + + + + + +