Source code for transaction

# -*- coding: utf-8 -*-
'''

    Payment Gateway Transaction

    :copyright: (c) 2013-2014 by Openlabs Technologies & Consulting (P) Ltd.
    :license: BSD, see LICENSE for more details

'''
from uuid import uuid4
from decimal import Decimal
from datetime import datetime

import yaml
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval, If, Bool
from trytond.wizard import Wizard, StateView, StateTransition, \
    Button
from trytond.transaction import Transaction
from trytond.exceptions import UserError
from trytond.model import ModelSQL, ModelView, Workflow, fields


__all__ = [
    'PaymentGateway', 'PaymentGatewaySelf', 'PaymentTransaction',
    'TransactionLog', 'PaymentProfile', 'AddPaymentProfileView',
    'AddPaymentProfile', 'BaseCreditCardViewMixin', 'Party',
    'TransactionUseCardView', 'TransactionUseCard',
]
__metaclass__ = PoolMeta

READONLY_IF_NOT_DRAFT = {'readonly': Eval('state') != 'draft'}


[docs]class PaymentGateway(ModelSQL, ModelView): """ Payment Gateway Payment gateway record is a specific configuration for a `provider` """ __name__ = 'payment_gateway.gateway' name = fields.Char('Name', required=True, select=True) journal = fields.Many2One('account.journal', 'Journal', required=True) provider = fields.Selection('get_providers', 'Provider', required=True) method = fields.Selection( 'get_methods', 'Method', required=True, selection_change_with=['provider'] ) test = fields.Boolean('Test Account') @staticmethod def default_provider(): return 'self' @classmethod
[docs] def get_providers(cls): """ Downstream modules can add to the list """ return []
def get_methods(self): """ Downstream modules can override the method and add entries to this """ return []
class PaymentGatewaySelf: "COD, Cheque and Bank Transfer Implementation" __name__ = 'payment_gateway.gateway' @classmethod def get_providers(cls, values=None): """ Downstream modules can add to the list """ rv = super(PaymentGatewaySelf, cls).get_providers() self_record = ('self', 'Self') if self_record not in rv: rv.append(self_record) return rv def get_methods(self): if self.provider == 'self': return [ ('cod', 'Cash On Delivery'), ('cheque', 'Cheque'), ] return super(PaymentGatewaySelf, self).get_methods()
[docs]class PaymentTransaction(Workflow, ModelSQL, ModelView): '''Gateway Transaction''' __name__ = 'payment_gateway.transaction' uuid = fields.Char('UUID', required=True, readonly=True) provider_reference = fields.Char( 'Provider Reference', readonly=True, states={ 'invisible': Eval('state') == 'draft' }, depends=['state'] ) date = fields.Date( 'Date', required=True, states=READONLY_IF_NOT_DRAFT, depends=['state'] ) company = fields.Many2One( 'company.company', 'Company', required=True, states=READONLY_IF_NOT_DRAFT, select=True, domain=[ ('id', If(Eval('context', {}).contains('company'), '=', '!='), Eval('context', {}).get('company', -1)), ], depends=['state'] ) party = fields.Many2One( 'party.party', 'Party', required=True, on_change=['party'], ondelete='RESTRICT', depends=['state'], states=READONLY_IF_NOT_DRAFT, ) payment_profile = fields.Many2One( 'party.payment_profile', 'Payment Profile', domain=[ ('party', '=', Eval('party')), ('gateway', '=', Eval('gateway')), ], on_change=['payment_profile'], ondelete='RESTRICT', depends=['state', 'party', 'gateway'], states=READONLY_IF_NOT_DRAFT, ) address = fields.Many2One( 'party.address', 'Address', required=True, domain=[('party', '=', Eval('party'))], depends=['state', 'party'], states=READONLY_IF_NOT_DRAFT, ondelete='RESTRICT' ) amount = fields.Numeric( 'Amount', digits=(16, Eval('currency_digits', 2)), required=True, depends=['state', 'currency_digits'], states=READONLY_IF_NOT_DRAFT, ) currency = fields.Many2One( 'currency.currency', 'Currency', required=True, depends=['state'], states=READONLY_IF_NOT_DRAFT, ) currency_digits = fields.Function( fields.Integer( 'Currency Digits', on_change_with=['currency'] ), 'on_change_with_currency_digits' ) gateway = fields.Many2One( 'payment_gateway.gateway', 'Gateway', required=True, states=READONLY_IF_NOT_DRAFT, depends=['state'], ondelete='RESTRICT', on_change=['gateway'] ) provider = fields.Function( fields.Char('Provider'), 'get_provider' ) method = fields.Function( fields.Char('Payment Gateway Method'), 'get_method' ) move = fields.Many2One( 'account.move', 'Move', readonly=True, ondelete='RESTRICT' ) logs = fields.One2Many( 'payment_gateway.transaction.log', 'transaction', 'Logs', depends=['state'], states={ 'readonly': Eval('state') in ('done', 'cancel') } ) state = fields.Selection([ ('draft', 'Draft'), ('in-progress', 'In Progress'), ('failed', 'Failed'), ('authorized', 'Authorized'), ('completed', 'Completed'), ('posted', 'Posted'), ('cancel', 'Canceled'), ], 'State', readonly=True) def get_rec_name(self, name=None): """ Return the most meaningful rec_name """ if self.state == 'draft': return self.uuid if not self.payment_profile: return '%s/%s' % (self.gateway.name, self.provider_reference) return '%s/%s' % ( self.payment_profile.rec_name, self.provider_reference ) @classmethod def __setup__(cls): super(PaymentTransaction, cls).__setup__() cls._order.insert(0, ('date', 'DESC')) cls._error_messages.update({ 'feature_not_available': 'The feature %s is not avaialable ' 'for provider %s', }) cls._transitions |= set(( ('draft', 'in-progress'), ('draft', 'authorized'), ('in-progress', 'failed'), ('in-progress', 'authorized'), ('in-progress', 'completed'), ('in-progress', 'cancel'), ('failed', 'in-progress'), ('authorized', 'cancel'), ('authorized', 'completed'), ('completed', 'posted'), )) cls._buttons.update({ 'cancel': { 'invisible': ~Eval('state').in_(['in-progress', 'authorized']), }, 'authorize': { 'invisible': ~( (Eval('state') == 'draft') & Eval('payment_profile', True) ), }, 'settle': { 'invisible': ~(Eval('state') == 'authorized'), }, 'retry': { 'invisible': ~(Eval('state') == 'failed'), }, 'capture': { 'invisible': ~( (Eval('state') == 'draft') & Eval('payment_profile', True) ), }, 'post': { 'invisible': ~(Eval('state') == 'completed'), }, 'use_card': { 'invisible': ~( (Eval('state') == 'draft') & ~Bool(Eval('payment_profile')) & (Eval('method') == 'credit_card') ), }, 'update_status': { 'invisible': ~Eval('state').in_(['in-progress']) } }) @staticmethod def default_uuid(): return unicode(uuid4()) @staticmethod def default_date(): Date = Pool().get('ir.date') return Date.today() @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_currency(): Company = Pool().get('company.company') if Transaction().context.get('company'): company = Company(Transaction().context['company']) return company.currency.id @staticmethod def default_state(): return 'draft' @classmethod def copy(cls, records, default=None): if default is None: default = {} default.update({ 'uuid': cls.default_uuid(), 'provider_reference': None, 'move': None, 'logs': None, }) return super(PaymentTransaction, cls).copy(records, default) def on_change_with_currency_digits(self, name=None): if self.currency: return self.currency.digits return 2 def on_change_party(self): res = { 'address': None, } if self.party: try: address = self.party.address_get(type='invoice') except AttributeError: # account_invoice module is not installed pass else: res['address'] = address.id res['address.rec_name'] = address.rec_name return res def on_change_payment_profile(self): res = {} if self.payment_profile: res['address'] = self.payment_profile.address.id res['address.rec_name'] = self.payment_profile.address.rec_name return res def get_provider(self): """ Return the gateway provider based on the gateway """ return self.gateway.provider def get_method(self, name=None): """ Return the method based on the gateway """ return self.gateway.method def on_change_gateway(self): if self.gateway: return { 'provider': self.gateway.provider, 'method': self.gateway.method, } def on_change_with_provider(self): return self.get_provider() @classmethod @ModelView.button @Workflow.transition('cancel') def cancel(cls, transactions): for transaction in transactions: method_name = 'cancel_%s' % transaction.gateway.provider if not hasattr(transaction, method_name): cls.raise_user_error( 'feature_not_available', ('cancellation', transaction.gateway.provider), ) getattr(transaction, method_name)() @classmethod @ModelView.button @Workflow.transition('in-progress') def authorize(cls, transactions): for transaction in transactions: method_name = 'authorize_%s' % transaction.gateway.provider if not hasattr(transaction, method_name): cls.raise_user_error( 'feature_not_available', ('authorization', transaction.gateway.provider), ) getattr(transaction, method_name)() @classmethod @ModelView.button @Workflow.transition('in-progress') def retry(cls, transactions): for transaction in transactions: method_name = 'retry_%s' % transaction.gateway.provider if not hasattr(transaction, method_name): cls.raise_user_error( 'feature_not_available', ('retry', transaction.gateway.provider) ) getattr(transaction, method_name)() @classmethod @ModelView.button @Workflow.transition('completed') def settle(cls, transactions): for transaction in transactions: method_name = 'settle_%s' % transaction.gateway.provider if not hasattr(transaction, method_name): cls.raise_user_error( 'feature_not_available', ('settle', transaction.gateway.provider) ) getattr(transaction, method_name)() @classmethod @ModelView.button @Workflow.transition('in-progress') def capture(cls, transactions): for transaction in transactions: method_name = 'capture_%s' % transaction.gateway.provider if not hasattr(transaction, method_name): cls.raise_user_error( 'feature_not_available', ('capture', transaction.gateway.provider) ) getattr(transaction, method_name)() @classmethod @ModelView.button @Workflow.transition('posted') def post(cls, transactions): """ Complete the transactions by creating account moves and post them. This method is likely to end in failure if the initial configuration of the journal and fiscal periods have not been done. You could alternatively use the safe_post instance method to try to post the record, but ignore the error silently. """ for transaction in transactions: if not transaction.move: transaction.create_move() @classmethod @ModelView.button def update_status(cls, transactions): """ Check the status with the payment gateway provider and update the status of this transaction accordingly. """ for transaction in transactions: method_name = 'update_%s' % transaction.gateway.provider if not hasattr(transaction, method_name): cls.raise_user_error( 'feature_not_available' ('update status', transaction.gateway.provider) ) getattr(transaction, method_name)()
[docs] def safe_post(self): """ If the initial configuration including defining a period and journal is not completed, marking as done could fail. In such cases, just mark as in-progress and let the user to manually mark as done. Failing would otherwise rollback transaction but its not possible to rollback the payment """ try: self.post([self]) except UserError, exc: log = 'Could not mark as done\n' log += unicode(exc) TransactionLog.create([{ 'transaction': self, 'log': log }])
def create_move(self, date=None): """ Create the account move for the payment :param date: Optional date for the account move :return: Active record of the created move """ Currency = Pool().get('currency.currency') Period = Pool().get('account.period') Move = Pool().get('account.move') journal = self.gateway.journal date = date or self.date if not journal.debit_account: self.raise_user_error('missing_debit_account', (journal.rec_name,)) period_id = Period.find(self.company.id, date=date) amount_second_currency = second_currency = None amount = self.amount if self.currency != self.company.currency: amount = Currency.compute( self.currency, self.amount, self.company.currency ) amount_second_currency = self.amount second_currency = self.currency lines = [{ 'description': self.rec_name, 'account': self.party.account_receivable.id, 'party': self.party.id, 'debit': Decimal('0.0'), 'credit': amount, 'amount_second_currency': amount_second_currency, 'second_currency': second_currency, }, { 'description': self.rec_name, 'account': journal.debit_account.id, 'party': self.party.id, 'debit': amount, 'credit': Decimal('0.0'), 'amount_second_currency': amount_second_currency, 'second_currency': second_currency, }] move, = Move.create([{ 'journal': journal.id, 'period': period_id, 'date': date, 'lines': [('create', lines)], }]) Move.post([move]) # Set the move as the move of this transaction self.move = move self.save() return move @classmethod @ModelView.button_action('payment_gateway.wizard_transaction_use_card') def use_card(cls, transactions): pass
[docs]class TransactionLog(ModelSQL, ModelView): "Transaction Log" __name__ = 'payment_gateway.transaction.log' timestamp = fields.DateTime('Event Timestamp', readonly=True) transaction = fields.Many2One( 'payment_gateway.transaction', 'Transaction', required=True, readonly=True, ) is_system_generated = fields.Boolean('Is System Generated') log = fields.Text( 'Log', required=True, depends=['is_system_generated'], states={'readonly': Eval('is_system_generated', True)} ) @staticmethod def default_is_system_generated(): return False @staticmethod def default_timestamp(): return datetime.utcnow() @classmethod
[docs] def serialize_and_create(cls, transaction, data): """ Serialise a given object and then save it as a log :param transaction: The transaction against which the log needs to be saved :param data: The data object that needs to be saved """ return cls.create([{ 'transaction': transaction, 'log': yaml.dump(data, default_flow_style=False), }])[0]
class BaseCreditCardViewMixin(object): """ A Reusable Mixin class to get Credit Card view """ owner = fields.Char('Card Owner', required=True) number = fields.Char('Card Number', required=True) expiry_month = fields.Selection([ ('01', '01-January'), ('02', '02-February'), ('03', '03-March'), ('04', '04-April'), ('05', '05-May'), ('06', '06-June'), ('07', '07-July'), ('08', '08-August'), ('09', '09-September'), ('10', '10-October'), ('11', '11-November'), ('12', '12-December'), ], 'Expiry Month', required=True) expiry_year = fields.Integer('Expiry Year', required=True) csc = fields.Integer('Card Security Code (CVV/CVD)', help='CVD/CVV/CVN') @staticmethod def default_owner(): """ If a party is provided in the context fill up this instantly """ Party = Pool().get('party.party') party_id = Transaction().context.get('party') if party_id: return Party(party_id).name class Party: __name__ = 'party.party' payment_profiles = fields.One2Many( 'party.payment_profile', 'party', 'Payment Profiles' ) @classmethod def __setup__(cls): super(Party, cls).__setup__() cls._buttons.update({ 'add_payment_profile': {} }) @classmethod @ModelView.button_action('payment_gateway.wizard_add_payment_profile') def add_payment_profile(cls, parties): pass
[docs]class PaymentProfile(ModelSQL, ModelView): """ Secure Payment Profile Several payment gateway service providers offer a secure way to store confidential customer credit card insformation on their server. Transactions can then be processed against these profiles without the need to recollect payment information from the customer, and without the need to store confidential credit card information in Tryton. This model represents a profile thus stored with any of the third party providers. """ __name__ = 'party.payment_profile' party = fields.Many2One('party.party', 'Party', required=True) address = fields.Many2One( 'party.address', 'Address', required=True, domain=[('party', '=', Eval('party'))], depends=['party'] ) gateway = fields.Many2One( 'payment_gateway.gateway', 'Gateway', required=True, ondelete='RESTRICT', readonly=True, ) provider_reference = fields.Char( 'Provider Reference', required=True, readonly=True ) last_4_digits = fields.Char('Last 4 digits', readonly=True) expiry_month = fields.Selection([ ('01', '01-January'), ('02', '02-February'), ('03', '03-March'), ('04', '04-April'), ('05', '05-May'), ('06', '06-June'), ('07', '07-July'), ('08', '08-August'), ('09', '09-September'), ('10', '10-October'), ('11', '11-November'), ('12', '12-December'), ], 'Expiry Month', required=True) expiry_year = fields.Integer('Expiry Year', required=True) def get_rec_name(self, name=None): if self.last_4_digits: return self.gateway.name + ('xxxx ' * 3) + self.last_4_digits return 'Incomplete Card'
class AddPaymentProfileView(BaseCreditCardViewMixin, ModelView): """ View for adding a payment profile """ __name__ = 'party.payment_profile.add_view' party = fields.Many2One( 'party.party', 'Party', required=True, states={'invisible': Eval('party_invisible', False)} ) address = fields.Many2One( 'party.address', 'Address', required=True, domain=[('party', '=', Eval('party'))], depends=['party'] ) provider = fields.Selection('get_providers', 'Provider', required=True) gateway = fields.Many2One( 'payment_gateway.gateway', 'Gateway', required=True, domain=[('provider', '=', Eval('provider'))], depends=['provider'] ) @classmethod def get_providers(cls): """ Return the list of providers who support credit card profiles. """ return []
[docs]class AddPaymentProfile(Wizard): """ Add a payment profile """ __name__ = 'party.party.payment_profile.add' start_state = 'card_info' card_info = StateView( 'party.payment_profile.add_view', 'payment_gateway.payment_profile_add_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Add', 'add', 'tryton-ok', default=True) ] ) add = StateTransition() def default_card_info(self, fields): Party = Pool().get('party.party') party = Party(Transaction().context.get('active_id')) res = {'party': party.id} try: address = self.party.address_get(type='invoice') except AttributeError: # account_invoice module is not installed pass else: res['address'] = address.id return res
[docs] def create_profile(self, provider_reference): """ A helper function that creates a profile from the card information that was entered into the View of the wizard. This helper could be called by the method which implement the API and wants to create the profile with provider_reference. :param provider_reference: Value for the provider_reference field. :return: Active record of the created profile """ Profile = Pool().get('party.payment_profile') profile = Profile( party=self.card_info.party.id, address=self.card_info.address.id, gateway=self.card_info.gateway.id, last_4_digits=self.card_info.number[-4:], expiry_month=self.card_info.expiry_month, expiry_year=self.card_info.expiry_year, provider_reference=provider_reference, ) profile.save() return profile
def transition_add(self): """ Downstream module implementing the functionality should check for the provider type and handle it accordingly. To handle, name your method transition_add_<provider_name>. For example if your proivder internal name is paypal, then the method name should be `transition_add_paypal` Once validated, the payment profile must be created by the method. A helper function is provided in this class itself which fills in most of the information automatically and the only additional information required is the reference from the payment provider. """ method_name = 'transition_add_%s' % self.card_info.provider return getattr(self, method_name)()
class TransactionUseCardView(BaseCreditCardViewMixin, ModelView): """ View for putting in credit card information """ __name__ = 'payment_gateway.transaction.use_card.view' class TransactionUseCard(Wizard): """ Add a payment profile """ __name__ = 'payment_gateway.transaction.use_card' start_state = 'card_info' card_info = StateView( 'payment_gateway.transaction.use_card.view', 'payment_gateway.transaction_use_card_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Authorize', 'authorize', 'tryton-go-next'), Button('Capture', 'capture', 'tryton-ok', default=True), ] ) capture = StateTransition() authorize = StateTransition() def transition_capture(self): """ Delegates to the capture method for the provider in payment_gateway.transaction """ PaymentTransaction = Pool().get('payment_gateway.transaction') transaction = PaymentTransaction( Transaction().context.get('active_id') ) getattr(transaction, 'capture_%s' % transaction.gateway.provider)( self.card_info ) return 'end' def transition_authorize(self): """ Delegates to the authorize method for the provider in payment_gateway.transaction """ PaymentTransaction = Pool().get('payment_gateway.transaction') transaction = PaymentTransaction( Transaction().context.get('active_id') ) getattr(transaction, 'authorize_%s' % transaction.gateway.provider)( self.card_info ) return 'end'
Read the Docs v: latest
Versions
latest
Downloads
On Read the Docs
Project Home
Builds

Free document hosting provided by Read the Docs.