banking-addons-team team mailing list archive
-
banking-addons-team team
-
Mailing list archive
-
Message #00872
[Merge] lp:~savoirfairelinux-openerp/banking-addons/loose-coupling into lp:banking-addons/bank-statement-reconcile-7.0
Virgil Dupras has proposed merging lp:~savoirfairelinux-openerp/banking-addons/loose-coupling into lp:banking-addons/bank-statement-reconcile-7.0.
Requested reviews:
Banking Addons Core Editors (banking-addons-team)
For more details, see:
https://code.launchpad.net/~savoirfairelinux-openerp/banking-addons/loose-coupling/+merge/185033
--
https://code.launchpad.net/~savoirfairelinux-openerp/banking-addons/loose-coupling/+merge/185033
Your team Banking Addons Core Editors is requested to review the proposed merge of lp:~savoirfairelinux-openerp/banking-addons/loose-coupling into lp:banking-addons/bank-statement-reconcile-7.0.
=== added directory 'customer_payment_request'
=== added file 'customer_payment_request/__init__.py'
--- customer_payment_request/__init__.py 1970-01-01 00:00:00 +0000
+++ customer_payment_request/__init__.py 2013-09-11 12:29:57 +0000
@@ -0,0 +1,2 @@
+import customer_payment_request
+import registry
=== added file 'customer_payment_request/__openerp__.py'
--- customer_payment_request/__openerp__.py 1970-01-01 00:00:00 +0000
+++ customer_payment_request/__openerp__.py 2013-09-11 12:29:57 +0000
@@ -0,0 +1,71 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 Savoir-faire Linux (<http://www.savoirfairelinux.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+{
+ "name" : "Customer Payment Requests",
+ "version" : "1.0",
+ "author" : "Savoir-faire Linux",
+ "website" : "http://www.savoirfairelinux.com",
+ "category" : "Accounting & Finance",
+ "description" : """
+Deals with mass payment requests
+--------------------------------
+
+When dealing with mass payments, we typically have a bunch of customers that owe us money and
+we want to charge their credit card, or bank account, or whatever. We have that information
+in one or more res.partner.bank record.
+
+The workflow here is that for every partner owing us money, we create a payment request in
+"waiting" mode, associating that payment with the first res.partner.bank associated with that
+partner.
+
+Then, we notify our registered payment handlers and they handle the payment request when they
+can handle the bank account associated with the request. The payment is then put in "sent" mode
+so that it isn't sent again until we receive the banks' responses.
+
+Then, we wait for the responses from banks by periodically polling for it. Upon receiving those
+responses, the bank handlers calls register_payment_refusal() or register_payment_acceptation().
+
+If accepted, the payment request is deleted and a validated voucher is created for the
+customer payment.
+
+If refused, the payment request is put back in waiting mode. If the partner has more than one
+bank account, we set the request's bank to the next in line.
+
+This module registers 3 cron jobs:
+
+1. send_customer_payment_requests(): Create a payment request for each partner owing us money and
+ tell bank handlers to send those requests.
+2. send_waiting_requests(): Retry every payment requests in "waiting" mode (being in this mode
+ because the previous payment attempt failed).
+3. receive_customer_payment_response(): Poll bank handlers for responses from banks. If there are
+ any, the bank handlers are responsible for registering payment acceptation or refusal.
+ """,
+ "depends" : ['account_voucher'],
+ "init_xml" : [],
+ 'data' : [
+ 'customer_payment_request_data.xml',
+ 'customer_payment_request_view.xml',
+ ],
+ "demo_xml" : [],
+ "installable" : True,
+ "certificate" : ''
+}
=== added file 'customer_payment_request/customer_payment_request.py'
--- customer_payment_request/customer_payment_request.py 1970-01-01 00:00:00 +0000
+++ customer_payment_request/customer_payment_request.py 2013-09-11 12:29:57 +0000
@@ -0,0 +1,188 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 Savoir-faire Linux (<http://www.savoirfairelinux.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import logging
+from datetime import date
+
+from openerp.osv import orm, fields
+from openerp.tools.translate import _
+
+import registry
+
+logger = logging.getLogger(__name__)
+
+class res_company_cprinfo(orm.Model):
+ _name = 'res.company.cprinfo'
+ _columns = {
+ 'process_payments': fields.boolean(
+ string=u"Process Payments",
+ help=u"Process partners with an outstanding payment",
+ ),
+ 'process_refunds': fields.boolean(
+ string=u"Process Refunds",
+ help=u"Process partners with an outstanding refund",
+ ),
+ }
+ _defaults = {
+ 'process_payments': True,
+ 'process_refunds': True,
+ }
+
+ def name_get(self, cr, uid, ids, context=None):
+ return dict.fromkeys(ids, _("Customer Payment Request Info"))
+
+class res_company(orm.Model):
+ _inherit = 'res.company'
+ _columns = {
+ 'customer_payment_request_info': fields.many2one('res.company.cprinfo', "Customer Payment Request Info"),
+ }
+
+ def _default_customer_payment_request_info(self, cr, uid, context=None):
+ res_company_cprinfo = self.pool.get('res.company.cprinfo')
+ res = res_company_cprinfo.create(cr, uid, {}, context=context)
+ return res
+
+ _defaults = {
+ 'customer_payment_request_info': _default_customer_payment_request_info,
+ }
+
+class customer_payment_request(orm.Model):
+ _name = 'customer.payment.request'
+
+ _columns = {
+ 'bank_account_id': fields.many2one(
+ 'res.partner.bank',
+ "Bank account the request was made at",
+ ),
+ 'partner_id': fields.related('bank_account_id', 'partner_id'),
+ 'amount': fields.float("Amount to charge"),
+ 'batch_ref': fields.char(size=64),
+ 'state': fields.selection([
+ ('waiting', "Waiting to be sent"),
+ ('sent', "Sent"),
+ ]),
+ }
+
+ def send_customer_payment_requests(self, cr, uid, ids=None, context=None):
+ partner_pool = self.pool.get('res.partner')
+ company_pool = self.pool.get('res.company')
+ # TODO: support multi-company
+ company = company_pool.browse(cr, uid, 1)
+ cprinfo = company.customer_payment_request_info
+ partner_ids = partner_pool.search(cr, uid, [('company_id', '=', company.id)])
+ partners = partner_pool.browse(cr, uid, partner_ids)
+
+ def should_charge(partner):
+ amount = p.credit - p.debit
+ if round(amount*100) == 0:
+ # Nothing to charge or refund
+ return False
+ if not cprinfo.process_payments and amount > 0:
+ return False
+ if not cprinfo.process_refunds and amount < 0:
+ return False
+ # Fetch the preferred payment type and see if it's a credit card. If it isn't, we
+ # don't process it here.
+ if not partner.bank_ids:
+ return False
+ if self.search(cr, uid, [('partner_id', '=', partner.id)]):
+ # We're already trying to charge this partner
+ return False
+ return True
+
+ toprocess = [p for p in partners if should_charge(p)]
+ if not toprocess:
+ return True
+
+ for p in toprocess:
+ bank_account = p.bank_ids[0]
+ amount = p.credit - p.debit
+ vals = {
+ 'bank_account_id': bank_account.id,
+ 'amount': amount,
+ 'state': 'waiting',
+ }
+ self.create(cr, uid, vals)
+
+ # Now that payment request records have been created, let's send them
+ self.send_waiting_requests(cr, uid)
+
+ def send_waiting_requests(self, cr, uid, ids=None, context=None):
+ # ping our registered handlers to process them.
+ for handler_func in registry.SEND_PAYMENT_FUNCS:
+ cpr_ids = self.search(cr, uid, [('state', '=', 'waiting')])
+ if not cpr_ids:
+ # everything is processed!
+ break
+ cprs = self.browse(cr, uid, cpr_ids)
+ handler_func(self, cr, uid, cprs)
+
+ cpr_ids = self.search(cr, uid, [('state', '=', 'waiting')])
+ if cpr_ids:
+ # We still have waiting requests! We have a problem because they should all have been
+ # handled. Eventually, we should have some kind of "problem review" UI, but for now, we
+ # just log a warning.
+ logger.warning("%d customer payment request(s) weren't processed by any handler", len(cpr_ids))
+
+ def receive_customer_payment_response(self, cr, uid, ids=None, context=None):
+ for handler_func in registry.RECEIVE_PAYMENT_FUNCS:
+ # handler_func will call register_payment_acceptation or register_payment_refusal
+ handler_func(self, cr, uid)
+
+ def register_payment_refusal(self, cr, uid, partner_id):
+ [cpr_id] = self.search(cr, uid, [('partner_id', '=', partner_id)])
+ cpr = self.browse(cr, uid, cpr_id)
+ partner = cpr.partner_id
+ if len(partner.bank_ids) > 1:
+ # We have multiple payment options, let's cycle through them
+ try:
+ index = [b.id for b in partner.bank_ids].index(cpr.bank_account_id.id) + 1
+ except ValueError:
+ # weird, our account isn't there. Maybe it has been removed. Let's go back to 0
+ index = 0
+ if index >= len(partner.bank_ids):
+ index = 0
+ cpr.write({'bank_account_id': partner.bank_ids[index].id})
+ cpr.write({'state': 'waiting'})
+
+ def register_payment_acceptation(self, cr, uid, partner_id, amount, account_id, journal_id, leave_draft=False):
+ logger.info("Importing payment of amount %d for partner %d", amount, partner_id)
+ account_voucher_pool = self.pool.get('account.voucher')
+ account_voucher_line_pool = self.pool.get('account.voucher.line')
+ vals = {
+ 'type': 'receipt',
+ 'partner_id': partner_id,
+ 'amount': amount,
+ 'account_id': account_id,
+ 'journal_id': journal_id,
+ }
+ voucher_id = account_voucher_pool.create(cr, uid, vals)
+ line_vals = account_voucher_pool.recompute_voucher_lines(
+ cr, uid, [voucher_id], partner_id, journal_id, amount, None, 'receipt', date.today()
+ )
+ all_lines = line_vals['value']['line_cr_ids'] + line_vals['value']['line_dr_ids']
+ for line in all_lines:
+ line['voucher_id'] = voucher_id
+ account_voucher_line_pool.create(cr, uid, line)
+ if not leave_draft:
+ account_voucher_pool.action_move_line_create(cr, uid, [voucher_id])
+ [cpr_id] = self.search(cr, uid, [('partner_id', '=', partner_id)])
+ self.unlink(cr, uid, cpr_id)
=== added file 'customer_payment_request/customer_payment_request_data.xml'
--- customer_payment_request/customer_payment_request_data.xml 1970-01-01 00:00:00 +0000
+++ customer_payment_request/customer_payment_request_data.xml 2013-09-11 12:29:57 +0000
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<openerp>
+ <data noupdate="1">
+ <record id="ir_cron_export_customer_payment_requests" model="ir.cron">
+ <field name="name">Export customer payment requests</field>
+ <field name="user_id" ref="base.user_root"/>
+ <field name="interval_number">1</field>
+ <field name="interval_type">hours</field>
+ <field name="numbercall">-1</field>
+ <field eval="False" name="active"/>
+ <field eval="False" name="doall"/>
+ <field eval="'customer.payment.request'" name="model"/>
+ <field eval="'send_customer_payment_requests'" name="function"/>
+ <field name="args">()</field>
+ </record>
+
+ <record id="ir_cron_retry_failed_payment_requests" model="ir.cron">
+ <field name="name">Retry failed payment requests</field>
+ <field name="user_id" ref="base.user_root"/>
+ <field name="interval_number">1</field>
+ <field name="interval_type">hours</field>
+ <field name="numbercall">-1</field>
+ <field eval="False" name="active"/>
+ <field eval="False" name="doall"/>
+ <field eval="'customer.payment.request'" name="model"/>
+ <field eval="'send_waiting_requests'" name="function"/>
+ <field name="args">()</field>
+ </record>
+
+ <record id="ir_cron_import_customer_payment_requests" model="ir.cron">
+ <field name="name">Import customer payment requests</field>
+ <field name="user_id" ref="base.user_root"/>
+ <field name="interval_number">1</field>
+ <field name="interval_type">hours</field>
+ <field name="numbercall">-1</field>
+ <field eval="False" name="active"/>
+ <field eval="False" name="doall"/>
+ <field eval="'customer.payment.request'" name="model"/>
+ <field eval="'receive_customer_payment_response'" name="function"/>
+ <field name="args">()</field>
+ </record>
+ </data>
+</openerp>
\ No newline at end of file
=== added file 'customer_payment_request/customer_payment_request_view.xml'
--- customer_payment_request/customer_payment_request_view.xml 1970-01-01 00:00:00 +0000
+++ customer_payment_request/customer_payment_request_view.xml 2013-09-11 12:29:57 +0000
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<openerp>
+ <data>
+ <record id="view_company_form" model="ir.ui.view">
+ <field name="name">res.company.form</field>
+ <field name="model">res.company</field>
+ <field name="inherit_id" ref="base.view_company_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//page[@string='General Information']/group/group" position="inside">
+ <field name="customer_payment_request_info"/>
+ </xpath>
+ </field>
+ </record>
+ </data>
+
+</openerp>
=== added directory 'customer_payment_request/docs'
=== added file 'customer_payment_request/docs/Makefile'
--- customer_payment_request/docs/Makefile 1970-01-01 00:00:00 +0000
+++ customer_payment_request/docs/Makefile 2013-09-11 12:29:57 +0000
@@ -0,0 +1,153 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ -rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/OpenERPBankPayments.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/OpenERPBankPayments.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/OpenERPBankPayments"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/OpenERPBankPayments"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
=== added file 'customer_payment_request/docs/conf.py'
--- customer_payment_request/docs/conf.py 1970-01-01 00:00:00 +0000
+++ customer_payment_request/docs/conf.py 2013-09-11 12:29:57 +0000
@@ -0,0 +1,242 @@
+# -*- coding: utf-8 -*-
+#
+# OpenERP Bank Payments documentation build configuration file, created by
+# sphinx-quickstart on Wed Sep 4 09:42:11 2013.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys, os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.insert(0, os.path.abspath('.'))
+
+# -- General configuration -----------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'OpenERP Bank Payments'
+copyright = u'2013, Savoir-faire Linux'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '1.0'
+# The full version, including alpha/beta/rc tags.
+release = '1.0'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'OpenERPBankPaymentsdoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+ ('index', 'OpenERPBankPayments.tex', u'OpenERP Bank Payments Documentation',
+ u'Savoir-faire Linux', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output --------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'openerpbankpayments', u'OpenERP Bank Payments Documentation',
+ [u'Savoir-faire Linux'], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output ------------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ ('index', 'OpenERPBankPayments', u'OpenERP Bank Payments Documentation',
+ u'Savoir-faire Linux', 'OpenERPBankPayments', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
=== added file 'customer_payment_request/docs/design.rst'
--- customer_payment_request/docs/design.rst 1970-01-01 00:00:00 +0000
+++ customer_payment_request/docs/design.rst 2013-09-11 12:29:57 +0000
@@ -0,0 +1,59 @@
+Module Design
+=============
+
+Modules in this project are organised in two groups, the :ref:`workflow` group and the
+:ref:`protocol` group. Each of them is as loosely coupled as possible to allow mix-and-matching of
+any of them together. This loose coupling is achieved through a :ref:`registry` mechanism.
+
+.. _workflow:
+
+Workflow modules
+----------------
+
+The role of workflow modules is to determine what warrants a communication with banks and how. For
+example, you have the :ref:`customer_payment_request` module which selects all ``partner`` owing us
+any money and, for that amount, asks (through the :ref:`registry`) that any :ref:`protocol` module
+that knows how to process payments with the bank associated with each of these partners to process
+them. Then, it waits for an answer from these protocol modules to confirm the payment and then
+create a ``voucher`` for it.
+
+But that's just one workflow. There could be another workflow which prefers to process payments by
+invoices, and only under certain conditions, and this hypothetical workflow module could reuse.
+Another workflow module could prefer avoiding vouchers and work directly with move lines. Another
+workflow module could be integrated with shipping, stocks and inventory.
+
+.. _protocol:
+
+Protocol modules
+----------------
+
+Protocol modules implement communication protocols with banks. The kind of transaction they support
+can vary and they don't have to support every workflow modules in the project (sometimes, they just
+can't). They broadcast their capabilities to the workflow modules through the :ref:`registry`.
+
+For example, :ref:`payment_management_visa_desjardins` can send payment requests for partner account
+of the ``credit_card`` type (supplied by :ref:`encrypted_credit_card`) and thus fit with the
+:ref:`customer_payment_request` workflow, so it registers with it through
+``customer_payment_request.registry``.
+
+.. _registry:
+
+Registry
+--------
+
+Registry mechanism allows for loose coupling between :ref:`workflow` and :ref:`protocol`. Its
+implementation is very low-tech and is simply a list of registered functions hosted by the workflow
+modules. When a protocol wants to support a particular workflow, it simply has to call that
+workflow's registry function with adapter functions as parameter. These adapter functions then
+adapt the information supplied by a particular workflow to the requirements of the communication
+protocol.
+
+For example, we have :ref:`payment_management_visa_desjardins` which implements the communication
+protocol for credit card processing. It hooks with :ref:`customer_payment_request`, which supplies
+a list of partners and the amount they each owe. Fine, we create a communication file from these and
+we're done.
+
+Later, let's say someone wants to create a new workflow which, instead of being partner-based, is
+invoice-based. Someone wanting to re-use :ref:`payment_management_visa_desjardins` would simply have
+to implement an additional adapter function which generates communication files from invoices (and
+maybe add invoice-based metadata to the communication for reconciliation) and we're done.
=== added file 'customer_payment_request/docs/index.rst'
--- customer_payment_request/docs/index.rst 1970-01-01 00:00:00 +0000
+++ customer_payment_request/docs/index.rst 2013-09-11 12:29:57 +0000
@@ -0,0 +1,25 @@
+Welcome to OpenERP Bank Payments's documentation!
+=================================================
+
+This project is a collection of modules to help manage automatic payments with banks. For now, it
+only handles customer payment requests and interfaces only with `Desjardins`_, but the project is
+designed to loosely couple :ref:`workflows <workflow>` and :ref:`bank protocols <protocol>`, so it
+should be possible to add stuff like supplier payments and bank protocols.
+
+Contents:
+
+.. toctree::
+ :maxdepth: 2
+
+ installation
+ design
+ modules
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
+.. _Desjardins: http://www.desjardins.com/en/
=== added file 'customer_payment_request/docs/installation.rst'
--- customer_payment_request/docs/installation.rst 1970-01-01 00:00:00 +0000
+++ customer_payment_request/docs/installation.rst 2013-09-11 12:29:57 +0000
@@ -0,0 +1,27 @@
+Installation and usage
+======================
+
+To use this project, you have to install at least one :ref:`workflow` and one :ref:`protocol`.
+
+The first step to do that, of course, is like any OpenERP project: Add the ``addons`` folder to your
+addons path and update your module list.
+
+Then, choose a workflow that you want to install in your system. For example, if you want to send
+requests to your bank for customers who owe you money to pay you, you'll install
+:ref:`customer_payment_request`.
+
+Worflow modules usually come with scheduled tasks (export payments, import bank response, etc.)
+which themselves are inactive by default. You'll have to go to your schedule task configuration
+and activate those newly added tasks and set appropriate schedules for them.
+
+Then, you'll need to install a :ref:`bank protocol <protocol>` module and configure it. Usually,
+bank protocol configuration is a by-company configuration, so you'll be able to access them through
+your company details form.
+
+Once this is done, you should be good to go. How things work, usually, is that payments are
+automatically sent and received through scheduled jobs and everything, such as payment vouchers, is
+reconciled automatically. Some modules, however, may give you more latitude in this regard and you
+might, for example, be able to tell them that you want to manually confirm payment vouchers.
+
+You should refer to the documentation specific to the modules you've installed for any details or
+extra configuration.
=== added file 'customer_payment_request/docs/modules.rst'
--- customer_payment_request/docs/modules.rst 1970-01-01 00:00:00 +0000
+++ customer_payment_request/docs/modules.rst 2013-09-11 12:29:57 +0000
@@ -0,0 +1,100 @@
+Module List
+===========
+
+.. _customer_payment_request:
+
+customer_payment_request
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+A :ref:`workflow module <workflow>` allowing to send mass payment requests to each partner owing us
+money (having a positive value in its "Total Receivable" field). Regularly, through scheduled
+actions, it scans for these partners and, for each of these, create a ``customer.payment.request``
+line in ``waiting`` mode.
+
+Then, it calls its ``send_payment_func()`` function. This function is called for each registered
+:ref:`protocol`, in order. The protocol modules are then expected to process every customer payment
+request that they can process (this usually depends on the type of the bank account associated with
+the partner) and then set the state of the request to ``sent``. If they can't process the request,
+they leave it to ``waiting``.
+
+Normally, when all protocols passed through the customer payment requests, all of them should be
+in ``sent`` mode. If there are any ``waiting`` leftovers, a warning will be logged on the server.
+
+Then, regularly, ``receive_payment_func()`` will be called to registered protocols to tell them to
+check for a response from the bank regarding the payment status. If there is any, the protocol
+modules are expected to call ``register_payment_acceptation()`` or ``register_payment_refusal()``,
+which will either remove the payment request and create a payment voucher or put it back in
+``waiting`` mode.
+
+The module will never create a payment request for the same partner at once to avoid
+double-charging.
+
+When ``register_payment_refusal()`` is called, this will enable a bank rotation mechanism and try
+again. So, if the partner has more than one registered bank account, it will retry, but with the
+next bank account in line. If it doesn't, the retry will be done on the same bank account.
+
+.. _payment_management_visa_desjardins:
+
+payment_management_visa_desjardins
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This :ref:`protocol module <protocol>` produces a text file in a format that, when sent to
+Desjardins, allows you to request payments from credit cards into your bank account.
+
+This module depends on :ref:`encrypted_credit_card` modules which supplies the ``credit_card``
+bank account type and manages encryption of credit card numbers (because it's illegal to hold
+credit card numbers in plain text into a database such as OpenERP's).
+
+The file that it produce, ``vdcoupon.txt`` can be sent to Desjardins through SSH with the help of
+``scripts/vdcoupon_watch_send.py`` and a response from Desjardins can be watched and managed with
+``scripts/vdcoupon_watch_recv.py``.
+
+This module supports the :ref:`customer_payment_request` workflow and processes all partners having
+a ``credit_card`` bank account.
+
+Using it requires Desjardins Merchant details to be set for your company, so you'll need to go fill
+it up after you've installed the module. Because this information is very similar to the one used by
+:ref:`payment_management_dd_desjardins`, there's a common module,
+:ref:`payment_management_desjardins` which defines these structures.
+
+.. _payment_management_dd_desjardins:
+
+payment_management_dd_desjardins
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This :ref:`protocol module <protocol>` produces a text file in a format that, when sent to
+Desjardins, allows you to request direct withdrawal from bank accounts.
+
+The file that it produce, ``ddcoupon.txt`` can be sent to Desjardins manually (for now).
+
+This module supports the :ref:`customer_payment_request` workflow and processes all partners having
+a ``bank`` bank account.
+
+Unlike credit card processing, direct withdrawal request have no automated response and are
+considered by the system to always succeed. For this reason payment requests are "accepted" right
+upon file creation and a voucher is created. For this reason, you might want to enable the
+"Leave vouchers as draft" option and manually confirm them.
+
+Nothing happens when the workflow module asks this protocol to poll for bank response because there
+aren't any.
+
+.. _payment_management_desjardins:
+
+payment_management_desjardins
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+A helper module defining common preferences for :ref:`payment_management_visa_desjardins` and
+:ref:`payment_management_dd_desjardins`.
+
+.. _encrypted_credit_card:
+
+encrypted_credit_card
+^^^^^^^^^^^^^^^^^^^^^
+
+A helper module adding the ``credit_card`` bank type, doing credit card number validation and adding
+logic for credit card number encryption.
+
+Encryption is done asynchronously through RSA keypairs. Whenever a new credit card number is
+entered, it is encrypted with a public key (which is set in company preferences) and then, another
+server which is hopefully very secure holds the private key and decrypts them before sendin them to
+banks.
=== added file 'customer_payment_request/registry.py'
--- customer_payment_request/registry.py 1970-01-01 00:00:00 +0000
+++ customer_payment_request/registry.py 2013-09-11 12:29:57 +0000
@@ -0,0 +1,53 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 Savoir-faire Linux (<http://www.savoirfairelinux.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+# def send_payment_func_prototype(cpr_obj, cr, uid, customer_payment_requests):
+# batch_file = SomeBatchFile()
+# processed = []
+# for cpr in customer_payment_requests:
+# if can_process_request(cpr):
+# batch_file.add_request(cpr)
+# processed.append(cpr)
+# if batch_file.send_to_bank() == success:
+# cpr_ids = [cpr.id for cpr in processed]
+# cpr_obj.write(cr, uid, cpr_ids, {'state': 'sent'})
+
+SEND_PAYMENT_FUNCS = []
+
+# def receive_payment_func_prototype(cpr_obj, cr, uid):
+# if not has_received_response_from_bank():
+# return
+# batch_response = parse_batch_response()
+# account_id = fetch_account_id_from_bank_prefs()
+# journal_id = fetch_journal_id_from_bank_prefs()
+# for line in batch_response:
+# if line.accepted:
+# cpr_obj.register_payment_acceptation(cr, uid, line.partner_id, line.amount, account_id, journal_id)
+# else:
+# cpr_obj.register_payment_refusal(cr, uid, line.partner_id)
+
+RECEIVE_PAYMENT_FUNCS = []
+
+def register(send_func, receive_func):
+ if send_func is not None:
+ SEND_PAYMENT_FUNCS.append(send_func)
+ if receive_func is not None:
+ RECEIVE_PAYMENT_FUNCS.append(receive_func)
=== added directory 'encrypted_credit_card'
=== added file 'encrypted_credit_card/__init__.py'
--- encrypted_credit_card/__init__.py 1970-01-01 00:00:00 +0000
+++ encrypted_credit_card/__init__.py 2013-09-11 12:29:57 +0000
@@ -0,0 +1,1 @@
+import encrypted_credit_card
\ No newline at end of file
=== added file 'encrypted_credit_card/__openerp__.py'
--- encrypted_credit_card/__openerp__.py 1970-01-01 00:00:00 +0000
+++ encrypted_credit_card/__openerp__.py 2013-09-11 12:29:57 +0000
@@ -0,0 +1,64 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 Savoir-faire Linux (<http://www.savoirfairelinux.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+{
+ "name" : "Encrypted Credit Cards",
+ "version" : "1.0",
+ "author" : "Savoir-faire Linux",
+ "website" : "http://www.savoirfairelinux.com",
+ "category" : "Accounting & Finance",
+ "description" : """
+Adds a new "Credit Card" bank type.
+
+This new bank type stores credit card numbers in an encrypted form, using a public key
+stored in res.company.
+
+To comply with PCI-DSS, we never ever store the credit card number in the DB, so the bank
+type record's 'acc_number' field contains stuff like "XXXX-XXXX-XXXX-1234". The actual
+credit card number is stored in "encrypted_cc_number", encrypted with the company's public
+key.
+
+The encryption method used is RSA. Because we store the encrypted CC number in a char()
+field and that encrypted data is binary, we encode that encrypted CC number in base64.
+
+This way, someone with full access to the DB is still unable to extract CC number unless he
+also has access to the private key, which hopefully is stored elsewhere, in a very secure
+place.
+
+We don't do any decryption here. It's up to another process to have access to the private
+key and decrypt those numbers.
+
+This module requires PyCrypto ( https://pypi.python.org/pypi/pycrypto )
+
+ """,
+ "depends" : ['sale'],
+ 'external_dependencies': {
+ 'python' : ['Crypto'],
+ },
+ "init_xml" : [],
+ 'data' : [
+ 'encrypted_credit_card_data.xml',
+ 'encrypted_credit_card_view.xml',
+ ],
+ "demo_xml" : [],
+ "installable" : True,
+ "certificate" : ''
+}
=== added file 'encrypted_credit_card/encrypted_credit_card.py'
--- encrypted_credit_card/encrypted_credit_card.py 1970-01-01 00:00:00 +0000
+++ encrypted_credit_card/encrypted_credit_card.py 2013-09-11 12:29:57 +0000
@@ -0,0 +1,216 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 Savoir-faire Linux (<http://www.savoirfairelinux.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+# About encryption in this module
+#
+# The goal of this module is to store CC numbers in an encrypted way using an assymetric encryption
+# method (we use RSA). This way, someone with full access to our DB won't be able to decrypt our
+# CC numbers unless he also has access to the private key, which of course isn't stored in the BD.
+#
+# Because the output of PyCrypto.PublicKey.RSA.encrypt() is binary data and that we store the
+# encrypted key in a char field, we encode that encrypted data with base64.
+
+import re
+import binascii
+from datetime import date
+from Crypto.PublicKey import RSA
+
+from openerp.tools.translate import _
+from openerp.osv import orm, fields
+
+def is_credit_card_number_valid(credit_card_number):
+ """Credit card number validation according to the MODULO-10 algorithm.
+ """
+ str_card_number = str(credit_card_number)
+ str_check_digit = str_card_number[-1]
+ str_validation_vect = ""
+ str_result_vect = []
+ result = 0
+ next_closest_ten = 10
+
+ # Make sure the credit card number consists of
+ # digits only
+ if not re.match(r"^[0-9]+$", str_card_number):
+ return False
+
+ # Build the validation vector '212121...'
+ for i in range(len(str_card_number)-1):
+ if i % 2 == 0:
+ str_validation_vect += '2'
+ else:
+ str_validation_vect += '1'
+
+ # Multiply each digit of the card number
+ # by the corresponding validation digit,
+ # except for the last digit
+ for i in range(len(str_validation_vect)):
+ res = int(str_card_number[i]) * int(str_validation_vect[i])
+ str_result_vect.append(res)
+
+ # Add the result of the above multiplication
+ # and consider a 2-digit number as 2 numbers
+ # of one digit
+ for number in str_result_vect:
+ if number < 10:
+ result += number
+ else:
+ str_number = str(number)
+ num_1 = int(str_number[0])
+ num_2 = int(str_number[1])
+ result += num_1 + num_2
+
+ # Compute the check digit and compare it
+ # with the last digit of the card number
+ while next_closest_ten < result:
+ next_closest_ten += 10
+
+ check_digit = next_closest_ten - result
+
+ if str(check_digit) == str_check_digit:
+ return True
+ else:
+ return False
+
+def fix_public_key(key):
+ # Copy/Pasting public key leads to formatting loss, and PyCrypto is sensitive on this matter.
+ # It wants all \n preserved, but in OpenERP's char field, those \n are going to be replaced
+ # with spaces. But don't try to naively replace spaces with newlines, because you're going to
+ # end up with BEGIN\nPUBLIC\KEY, which PyCrypto won't accept.
+ if key.strip().startswith('ssh-rsa'):
+ # This key is not of the type that starts with BEGIN PUBLIC KEY. Just return the stripped
+ # version
+ return key.strip()
+ stripped = re.sub(r'\s?-----[A-Z\s]+-----\s?', '', key)
+ stripped = stripped.replace(' ', '\n')
+ return '-----BEGIN PUBLIC KEY-----\n' + stripped + '\n-----END PUBLIC KEY-----'
+
+def encrypt_cc_number(cc_number, public_key):
+ key = RSA.importKey(fix_public_key(public_key))
+ encrypted_cc_number = key.encrypt(cc_number, 42)[0]
+ return binascii.b2a_base64(encrypted_cc_number).strip()
+
+def encrypt_cc_vals(partner_obj, vals):
+ if 'acc_number' not in vals:
+ # no acc_number, no problem
+ return
+ cc_number = vals['acc_number']
+ if cc_number.startswith('XXXX'):
+ # We're not actually changing the number, just remove it
+ del vals['acc_number']
+ elif is_credit_card_number_valid(cc_number):
+ # Ok, we're actually submitting a CC number here
+ # Never ever store CC number in clear
+ vals['acc_number'] = 'XXXX-XXXX-XXXX-' + cc_number[-4:]
+ vals['encrypted_cc_number'] = encrypt_cc_number(cc_number, partner_obj.company_id.cc_number_encrypt_key)
+
+class res_partner_bank(orm.Model):
+ _inherit = 'res.partner.bank'
+
+ _columns = {
+ # RSA-encrypted, bas64-encoded.
+ 'encrypted_cc_number': fields.char("Encrypted Credit Card Number", size=1024),
+ 'expiration_date': fields.char('Expiration date (YYMM)', size=4),
+ }
+
+ def check_credit_card_number(self, cr, uid, ids, context=None):
+ for bank_acc in self.browse(cr, uid, ids, context=context):
+ if bank_acc.state != 'credit_card':
+ continue
+ cc_number = bank_acc.acc_number
+ if cc_number.startswith('XXXX'):
+ # It's a hidden number, so we're not actually changing the encrypted CC number.
+ # Consider as valid
+ continue
+ if not is_credit_card_number_valid(cc_number):
+ return False
+ return True
+
+ def check_expiration_date(self, cr, uid, ids, context=None):
+ for bank_acc in self.browse(cr, uid, ids, context=context):
+ if bank_acc.state != 'credit_card':
+ continue
+ if not bank_acc.expiration_date:
+ return False
+ m = re.match(r"^([0-9]{2})([0-9]{2})$", bank_acc.expiration_date)
+ if m is None:
+ return False
+ year = int(m.group(1)) + 2000
+ month = int(m.group(2))
+ TODAY = date.today()
+ if year < TODAY.year:
+ return False
+ if not (1 <= month <= 12):
+ return False
+ if year == TODAY.year and month < TODAY.month:
+ return False
+ return True
+
+ def _construct_constraint_msg_card_number(self, cr, uid, ids, context=None):
+ return (_("Credit card number is invalid")), ()
+
+ def _construct_constraint_msg_expiration_date(self, cr, uid, ids, context=None):
+ return (_("Expiration date is invalid")), ()
+
+ _constraints = [
+ (check_credit_card_number, _construct_constraint_msg_card_number, ["acc_number"]),
+ (check_expiration_date, _construct_constraint_msg_expiration_date, ["expiration_date"]),
+ ]
+
+ def create(self, cr, uid, vals, context=None):
+ if vals.get('state') == 'credit_card':
+ partner_obj = self.pool.get('res.partner').browse(cr, uid, vals['partner_id'], context=context)
+ encrypt_cc_vals(partner_obj, vals)
+ return super(res_partner_bank, self).create(cr, uid, vals, context=context)
+
+ def write(self, cr, uid, ids, vals, context=None):
+ self_obj = self.browse(cr, uid, ids[0], context=context)
+ try:
+ state = vals['state']
+ except KeyError:
+ state = self_obj.state
+ if state == 'credit_card':
+ encrypt_cc_vals(self_obj.partner_id, vals)
+ return super(res_partner_bank, self).write(cr, uid, ids, vals, context=context)
+
+class res_company(orm.Model):
+ _inherit = 'res.company'
+
+ def _get_short_pubkey(self, cr, uid, ids, field_name, arg, context):
+ res = {}
+ for company in self.browse(cr, uid, ids, context=context):
+ pubkey = company.cc_number_encrypt_key
+ if pubkey:
+ res[company.id] = pubkey[:30] + "..."
+ else:
+ res[company.id] = ""
+ return res
+
+ def _set_short_pubkey(self, cr, uid, id, name, value, fnct_inv_arg, context):
+ if len(value) > 40:
+ # We only save the key if it's not the truncated value
+ self.write(cr, uid, id, {'cc_number_encrypt_key': value})
+
+ _columns = {
+ 'cc_number_encrypt_key_short': fields.function(_get_short_pubkey, fnct_inv=_set_short_pubkey,
+ type='char', string='Credit Card Encryption Key'),
+ 'cc_number_encrypt_key': fields.char('Credit Card Encryption Key', size=2048,
+ help="Public key with which to encrypt our credit card number before writing them to the DB"),
+ }
=== added file 'encrypted_credit_card/encrypted_credit_card_data.xml'
--- encrypted_credit_card/encrypted_credit_card_data.xml 1970-01-01 00:00:00 +0000
+++ encrypted_credit_card/encrypted_credit_card_data.xml 2013-09-11 12:29:57 +0000
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<openerp>
+ <data>
+ <record id="bank_credit_card" model="res.partner.bank.type">
+ <field name="name">Credit card Account</field>
+ <field name="code">credit_card</field>
+ <field name="format_layout">%(bank_name)s: CC %(acc_number)s - EXP %(expiration_date)s</field>
+ </record>
+ </data>
+
+</openerp>
=== added file 'encrypted_credit_card/encrypted_credit_card_view.xml'
--- encrypted_credit_card/encrypted_credit_card_view.xml 1970-01-01 00:00:00 +0000
+++ encrypted_credit_card/encrypted_credit_card_view.xml 2013-09-11 12:29:57 +0000
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<openerp>
+ <data>
+ <record id="view_partner_bank_form" model="ir.ui.view">
+ <field name="name">res.partner.bank.form</field>
+ <field name="model">res.partner.bank</field>
+ <field name="inherit_id" ref="base.view_partner_bank_form"/>
+ <field name="arch" type="xml">
+ <field name="acc_number" position="after">
+ <field name="expiration_date" attrs="{'invisible':[('state','!=','credit_card')], 'required':[('state','=','credit_card')]}" />
+ </field>
+ </field>
+ </record>
+
+ <record id="view_company_form" model="ir.ui.view">
+ <field name="name">res.company.form</field>
+ <field name="model">res.company</field>
+ <field name="inherit_id" ref="base.view_company_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='currency_id']" position="after">
+ <field name="cc_number_encrypt_key_short"/>
+ </xpath>
+ </field>
+ </record>
+ </data>
+
+</openerp>
=== added directory 'encrypted_credit_card/i18n'
=== added file 'encrypted_credit_card/i18n/encrypted_credit_card.pot'
--- encrypted_credit_card/i18n/encrypted_credit_card.pot 1970-01-01 00:00:00 +0000
+++ encrypted_credit_card/i18n/encrypted_credit_card.pot 2013-09-11 12:29:57 +0000
@@ -0,0 +1,69 @@
+# Translation of OpenERP Server.
+# This file contains the translation of the following modules:
+# * encrypted_credit_card
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: OpenERP Server 7.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2013-08-13 14:05+0000\n"
+"PO-Revision-Date: 2013-08-13 14:05+0000\n"
+"Last-Translator: <>\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: encrypted_credit_card
+#: field:res.partner.bank,encrypted_cc_number:0
+msgid "Encrypted Credit Card Number"
+msgstr ""
+
+#. module: encrypted_credit_card
+#: code:addons/encrypted_credit_card/encrypted_credit_card.py:170
+#, python-format
+msgid "Expiration date is invalid"
+msgstr ""
+
+#. module: encrypted_credit_card
+#: code:addons/encrypted_credit_card/encrypted_credit_card.py:167
+#, python-format
+msgid "Credit card number is invalid"
+msgstr ""
+
+#. module: encrypted_credit_card
+#: model:ir.model,name:encrypted_credit_card.model_res_partner_bank
+msgid "Bank Accounts"
+msgstr ""
+
+#. module: encrypted_credit_card
+#: model:ir.model,name:encrypted_credit_card.model_res_company
+msgid "Companies"
+msgstr ""
+
+#. module: encrypted_credit_card
+#: field:res.partner.bank,expiration_date:0
+msgid "Expiration date (YYMM)"
+msgstr ""
+
+#. module: encrypted_credit_card
+#: help:res.company,cc_number_encrypt_key:0
+msgid "Public key with which to encrypt our credit card number before writing them to the DB"
+msgstr ""
+
+#. module: encrypted_credit_card
+#: model:res.partner.bank.type,format_layout:encrypted_credit_card.bank_credit_card
+msgid "%(bank_name)s: IBAN %(acc_number)s - BIC %(expiration_date)s"
+msgstr ""
+
+#. module: encrypted_credit_card
+#: model:res.partner.bank.type,name:encrypted_credit_card.bank_credit_card
+msgid "Credit card Account"
+msgstr ""
+
+#. module: encrypted_credit_card
+#: field:res.company,cc_number_encrypt_key:0
+msgid "Credit Card Encryption Key"
+msgstr ""
+
=== added directory 'payment_management_dd_desjardins'
=== added file 'payment_management_dd_desjardins/__init__.py'
--- payment_management_dd_desjardins/__init__.py 1970-01-01 00:00:00 +0000
+++ payment_management_dd_desjardins/__init__.py 2013-09-11 12:29:57 +0000
@@ -0,0 +1,22 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 Savoir-faire Linux (<http://www.savoirfairelinux.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import payment_management_dd_desjardins
=== added file 'payment_management_dd_desjardins/__openerp__.py'
--- payment_management_dd_desjardins/__openerp__.py 1970-01-01 00:00:00 +0000
+++ payment_management_dd_desjardins/__openerp__.py 2013-09-11 12:29:57 +0000
@@ -0,0 +1,40 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 Savoir-faire Linux (<http://www.savoirfairelinux.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+{
+ "name" : "Payment management module for desjardins Direct Deposit",
+ "version" : "1.0",
+ "author" : "Savoir-faire Linux",
+ "website" : "http://www.savoirfairelinux.com",
+ "category" : "Accounting & Finance",
+ "description" : """
+ """,
+ "depends" : ['payment_management_desjardins'],
+ "init_xml" : [],
+ "update_xml" : [
+ ],
+ 'data' : [
+ 'payment_management_dd_desjardins_view.xml',
+ ],
+ "demo_xml" : [],
+ "installable" : True,
+ "certificate" : ''
+}
=== added directory 'payment_management_dd_desjardins/i18n'
=== added file 'payment_management_dd_desjardins/i18n/payment_management_dd_desjardins.pot'
--- payment_management_dd_desjardins/i18n/payment_management_dd_desjardins.pot 1970-01-01 00:00:00 +0000
+++ payment_management_dd_desjardins/i18n/payment_management_dd_desjardins.pot 2013-09-11 12:29:57 +0000
@@ -0,0 +1,27 @@
+# Translation of OpenERP Server.
+# This file contains the translation of the following modules:
+# * payment_management_dd_desjardins
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: OpenERP Server 7.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2013-08-21 13:53+0000\n"
+"PO-Revision-Date: 2013-08-21 13:53+0000\n"
+"Last-Translator: <>\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: payment_management_dd_desjardins
+#: field:res.company,desjardins_dd_info:0
+msgid "Desjardins Direct Deposit Info"
+msgstr ""
+
+#. module: payment_management_dd_desjardins
+#: model:ir.model,name:payment_management_dd_desjardins.model_res_company
+msgid "Companies"
+msgstr ""
+
=== added file 'payment_management_dd_desjardins/payment_management_dd_desjardins.py'
--- payment_management_dd_desjardins/payment_management_dd_desjardins.py 1970-01-01 00:00:00 +0000
+++ payment_management_dd_desjardins/payment_management_dd_desjardins.py 2013-09-11 12:29:57 +0000
@@ -0,0 +1,109 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 Savoir-faire Linux (<http://www.savoirfairelinux.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+from __future__ import division
+
+import os
+import logging
+import codecs
+
+from openerp.osv import orm, fields
+
+from .record import get_cd_record, get_a_record, get_z_record
+
+_logger = logging.getLogger(__name__)
+
+class res_company(orm.Model):
+ _inherit = 'res.company'
+ _columns = {
+ 'desjardins_dd_info': fields.many2one('res.company.desjardinsinfo', "Desjardins Direct Deposit Info"),
+ }
+
+ def _default_desjardins_dd_info(self, cr, uid, context=None):
+ res_company_desjardinsinfo = self.pool.get('res.company.desjardinsinfo')
+ res = res_company_desjardinsinfo.create(cr, uid, {}, context=context)
+ return res
+
+ _defaults = {
+ 'desjardins_dd_info': _default_desjardins_dd_info,
+ }
+
+def send_payment_func_dd_desjardins(cpr_obj, cr, uid, customer_payment_requests):
+ company_obj = cpr_obj.pool.get('res.company')
+ # TODO: support multi-company
+ company = company_obj.browse(cr, uid, 1)
+ desjinfo = company.desjardins_dd_info
+ directory = desjinfo.workdir
+
+ if not desjinfo.is_complete():
+ return False
+
+ toprocess = [cpr for cpr in customer_payment_requests if cpr.bank_account_id.state == 'bank']
+ if not toprocess:
+ return False
+ total_number_of_credit = 0
+ total_number_of_debit = 0
+ total_value_of_credit = 0
+ total_value_of_debit = 0
+ lines = []
+
+ # Type A record
+ a_record = get_a_record(len(lines)+1, desjinfo)
+ lines.append(a_record)
+
+ # Type C/D records
+ for cpr in toprocess:
+ bank_account = cpr.bank_account_id
+ partner = bank_account.partner_id
+ # The amount is in cents
+ amount = round(cpr.amount * 100)
+ if amount > 0:
+ total_number_of_debit += 1
+ total_value_of_debit += amount
+ else:
+ total_number_of_credit += 1
+ total_value_of_credit += amount
+ cd_record = get_cd_record(
+ len(lines)+1, company, amount, bank_account, partner
+ )
+ lines.append(cd_record)
+ # With Desjardins RD, there's no response file, so we directly create the payment voucher
+ cpr_obj.register_payment_acceptation(
+ cr, uid, partner.id, cpr.amount, desjinfo.account_id.id,
+ desjinfo.bank_account_id.journal_id.id, leave_draft=desjinfo.leave_vouchers_draft
+ )
+
+ # Type Z record
+ z_record = get_z_record(
+ len(lines)+1, desjinfo,
+ total_number_of_debit, total_value_of_debit,
+ total_number_of_credit, total_value_of_credit
+ )
+ lines.append(z_record)
+ with codecs.open(os.path.join(directory, 'ddcoupon.txt'), 'wt', encoding='latin-1') as fp:
+ fp.write(u'\n'.join(lines))
+ desjinfo.increase_file_number()
+
+try:
+ from customer_payment_request import registry
+ registry.register(send_payment_func_dd_desjardins, None)
+except ImportError:
+ pass
=== added file 'payment_management_dd_desjardins/payment_management_dd_desjardins_view.xml'
--- payment_management_dd_desjardins/payment_management_dd_desjardins_view.xml 1970-01-01 00:00:00 +0000
+++ payment_management_dd_desjardins/payment_management_dd_desjardins_view.xml 2013-09-11 12:29:57 +0000
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<openerp>
+ <data>
+ <record id="view_company_form" model="ir.ui.view">
+ <field name="name">res.company.form</field>
+ <field name="model">res.company</field>
+ <field name="inherit_id" ref="base.view_company_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//page[@string='General Information']/group/group" position="inside">
+ <field name="desjardins_dd_info"/>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="view_partner_bank_form" model="ir.ui.view">
+ <field name="name">res.partner.bank.form</field>
+ <field name="model">res.partner.bank</field>
+ <field name="inherit_id" ref="base.view_partner_bank_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='bank_bic']" position="attributes">
+ <attribute name="attrs">{'required':[('state', 'in', ['iban', 'bank'])]}</attribute>
+ </xpath>
+ </field>
+ </record>
+ </data>
+
+</openerp>
=== added file 'payment_management_dd_desjardins/record.py'
--- payment_management_dd_desjardins/record.py 1970-01-01 00:00:00 +0000
+++ payment_management_dd_desjardins/record.py 2013-09-11 12:29:57 +0000
@@ -0,0 +1,94 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 Savoir-faire Linux (<http://www.savoirfairelinux.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+from __future__ import division, unicode_literals
+
+from datetime import date, datetime
+
+from payment_management_desjardins.record import yyyddd
+
+def ljust(s, num, filler):
+ return s[:num].ljust(num, filler)
+
+def rjust(s, num, filler):
+ return s[:num].rjust(num, filler)
+
+def get_a_record(seq, desjinfo):
+ return ''.join([
+ 'A',
+ '%09d' % seq,
+ desjinfo.originator_id[:10].upper().ljust(10, ' '),
+ '%04d' % desjinfo.file_creation_number,
+ yyyddd(datetime.now()),
+ '81510',
+ ' ' * 20,
+ 'CAD',
+ ' ' * 1406,
+ ])
+
+def get_cd_record(seq, company, amount, bank_account, partner):
+ desjinfo = company.desjardins_dd_info
+ # amount is an integer representing cents
+ if amount > 0:
+ firstchar = 'D'
+ txn_type = desjinfo.txn_code_debit
+ else:
+ firstchar = 'C'
+ txn_type = desjinfo.txn_code_credit
+ amount = abs(amount)
+ return ''.join([
+ firstchar,
+ '%09d' % seq,
+ ljust(desjinfo.originator_id.upper(), 10, ' '),
+ '%04d' % desjinfo.file_creation_number,
+ txn_type,
+ '%010d' % amount,
+ yyyddd(date.today()),
+ rjust(bank_account.bank_bic, 9, '0'),
+ ljust(bank_account.acc_number, 12, ' '),
+ '0' * 22,
+ '0' * 3,
+ ljust(company.name, 15, ' '),
+ ljust(partner.name, 30, ' '),
+ ljust(company.name, 30, ' '),
+ ljust(desjinfo.originator_id.upper(), 10, ' '),
+ '%019d' % partner.id,
+ rjust(desjinfo.bank_account_id.bank_bic, 9, '0'),
+ ljust(desjinfo.bank_account_id.acc_number, 12, ' '),
+ ' ' * 15,
+ ' ' * 22,
+ ' ' * 2,
+ '0' * 11,
+ ])
+
+def get_z_record(seq, desjinfo, num_debit, total_debit, num_credit, total_credit):
+ return ''.join([
+ 'Z',
+ '%09d' % seq,
+ desjinfo.originator_id[:10].upper().ljust(10, ' '),
+ '%04d' % desjinfo.file_creation_number,
+ '%014d' % total_debit,
+ '%08d' % num_debit,
+ '%014d' % total_credit,
+ '%08d' % num_credit,
+ '0' * 44,
+ ' ' * 1352,
+ ])
=== added directory 'payment_management_desjardins'
=== added file 'payment_management_desjardins/__init__.py'
--- payment_management_desjardins/__init__.py 1970-01-01 00:00:00 +0000
+++ payment_management_desjardins/__init__.py 2013-09-11 12:29:57 +0000
@@ -0,0 +1,22 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 Savoir-faire Linux (<http://www.savoirfairelinux.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import payment_management_desjardins
=== added file 'payment_management_desjardins/__openerp__.py'
--- payment_management_desjardins/__openerp__.py 1970-01-01 00:00:00 +0000
+++ payment_management_desjardins/__openerp__.py 2013-09-11 12:29:57 +0000
@@ -0,0 +1,40 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 Savoir-faire Linux (<http://www.savoirfairelinux.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+{
+ "name" : "Payment management module for Desjardins",
+ "version" : "1.0",
+ "author" : "Savoir-faire Linux",
+ "website" : "http://www.savoirfairelinux.com",
+ "category" : "Accounting & Finance",
+ "description" : """
+ """,
+ "depends" : ['account_voucher', 'sale'],
+ "init_xml" : [],
+ "update_xml" : [
+ ],
+ 'data' : [
+ 'payment_management_desjardins_view.xml',
+ ],
+ "demo_xml" : [],
+ "installable" : True,
+ "certificate" : ''
+}
=== added directory 'payment_management_desjardins/i18n'
=== added file 'payment_management_desjardins/i18n/payment_management_desjardins.pot'
--- payment_management_desjardins/i18n/payment_management_desjardins.pot 1970-01-01 00:00:00 +0000
+++ payment_management_desjardins/i18n/payment_management_desjardins.pot 2013-09-11 12:29:57 +0000
@@ -0,0 +1,105 @@
+# Translation of OpenERP Server.
+# This file contains the translation of the following modules:
+# * payment_management_desjardins
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: OpenERP Server 7.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2013-08-21 13:52+0000\n"
+"PO-Revision-Date: 2013-08-21 13:52+0000\n"
+"Last-Translator: <>\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: payment_management_desjardins
+#: model:ir.model,name:payment_management_desjardins.model_res_company_desjardinsinfo
+msgid "res.company.desjardinsinfo"
+msgstr ""
+
+#. module: payment_management_desjardins
+#: help:res.company.desjardinsinfo,merchant_number:0
+msgid "Merchant number assigned by DCS. Must be numeric."
+msgstr ""
+
+#. module: payment_management_desjardins
+#: field:res.company.desjardinsinfo,merchant_number:0
+msgid "Merchant number for Desjardins"
+msgstr ""
+
+#. module: payment_management_desjardins
+#: code:addons/payment_management_desjardins/payment_management_desjardins.py:88
+#, python-format
+msgid "Specified directory %s is not a valid directory. Desjardins payment exportation is aborted."
+msgstr ""
+
+#. module: payment_management_desjardins
+#: field:res.company.desjardinsinfo,workdir:0
+msgid "Storage Directory"
+msgstr ""
+
+#. module: payment_management_desjardins
+#: help:res.company.desjardinsinfo,originator_id:0
+msgid "Name of the directory into which the 'vdcoupon.txt' files will be placed."
+msgstr ""
+
+#. module: payment_management_desjardins
+#: field:res.company.desjardinsinfo,bank_account_id:0
+msgid "Your Desjardins bank account"
+msgstr ""
+
+#. module: payment_management_desjardins
+#: code:addons/payment_management_desjardins/payment_management_desjardins.py:78
+#, python-format
+msgid "Desjardins Merchant Info"
+msgstr ""
+
+#. module: payment_management_desjardins
+#: field:res.company.desjardinsinfo,originator_id:0
+msgid "Originator ID"
+msgstr ""
+
+#. module: payment_management_desjardins
+#: field:res.company.desjardinsinfo,txn_code_credit:0
+msgid "Transaction code for Credit"
+msgstr ""
+
+#. module: payment_management_desjardins
+#: view:res.company.desjardinsinfo:0
+msgid "Desjardins harmonization info"
+msgstr ""
+
+#. module: payment_management_desjardins
+#: field:res.company.desjardinsinfo,txn_code_debit:0
+msgid "Transaction code for Debit"
+msgstr ""
+
+#. module: payment_management_desjardins
+#: field:res.company.desjardinsinfo,file_creation_number:0
+msgid "Next file creation number value"
+msgstr ""
+
+#. module: payment_management_desjardins
+#: help:res.company.desjardinsinfo,file_creation_number:0
+msgid "File sequence number, to be incremented by 1 for each file sent without error."
+msgstr ""
+
+#. module: payment_management_desjardins
+#: field:res.company.desjardinsinfo,account_id:0
+msgid "Account for Desjardins payments"
+msgstr ""
+
+#. module: payment_management_desjardins
+#: help:res.company.desjardinsinfo,workdir:0
+msgid "Specify a directory to export to and import from."
+msgstr ""
+
+#. module: payment_management_desjardins
+#: code:addons/payment_management_desjardins/payment_management_desjardins.py:94
+#, python-format
+msgid "Could not export Desjardins payments because at least one of the following company parameters is missing : (merchant_number, file_creation_number, originator_id)."
+msgstr ""
+
=== added file 'payment_management_desjardins/payment_management_desjardins.py'
--- payment_management_desjardins/payment_management_desjardins.py 1970-01-01 00:00:00 +0000
+++ payment_management_desjardins/payment_management_desjardins.py 2013-09-11 12:29:57 +0000
@@ -0,0 +1,97 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 Savoir-faire Linux (<http://www.savoirfairelinux.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+from __future__ import division
+
+import os
+import logging
+
+from openerp.tools.translate import _
+from openerp.osv import orm, fields
+
+_logger = logging.getLogger(__name__)
+
+class res_company_desjardinsinfo(orm.Model):
+ _name = 'res.company.desjardinsinfo'
+ _columns = {
+ 'file_creation_number': fields.integer(
+ 'Next file creation number value',
+ help="File sequence number, to be incremented by 1 for each file sent without error."
+ ),
+ 'originator_id': fields.char(
+ 'Originator ID',
+ size=10,
+ help="Name of the directory into which the 'vdcoupon.txt' files will be placed."
+ ),
+ 'workdir': fields.char(
+ 'Storage Directory',
+ size=255,
+ help="Specify a directory to export to and import from."
+ ),
+ 'bank_account_id':fields.many2one(
+ 'res.partner.bank',
+ 'Your Desjardins bank account',
+ ),
+ 'account_id':fields.many2one(
+ 'account.account',
+ 'Account for Desjardins payments',
+ ),
+ 'txn_code_debit': fields.char(
+ 'Transaction code for Debit',
+ size=3,
+ ),
+ 'txn_code_credit': fields.char(
+ 'Transaction code for Credit',
+ size=3,
+ ),
+ 'leave_vouchers_draft': fields.boolean(
+ string=u"Leave vouchers as draft",
+ help=u"When creating vouchers, don't validate them, leave then in a draft state",
+ )
+ }
+
+ _defaults = {
+ 'workdir': '/tmp',
+ 'file_creation_number' : 1,
+ }
+
+ def name_get(self, cr, uid, ids, context=None):
+ return dict.fromkeys(ids, _("Desjardins Merchant Info"))
+
+ def is_complete(self, cr, uid, ids, context=None):
+ """Returns whether our info record is complete.
+
+ Logs a warning if not.
+ """
+ id = ids[0] if isinstance(ids, list) else ids
+ desjinfo = self.browse(cr, uid, id)
+ if not os.path.isdir(desjinfo.workdir):
+ msg = _("Specified directory %s is not a valid directory. Desjardins payment exportation is aborted.")
+ _logger.warning(msg, desjinfo.workdir)
+ return False
+
+ return True
+
+ def increase_file_number(self, cr, uid, ids, context=None):
+ id = ids[0] if isinstance(ids, list) else ids
+ desjinfo = self.browse(cr, uid, id)
+ self.write(cr, uid, id, {'file_creation_number': desjinfo.file_creation_number+1})
+
=== added file 'payment_management_desjardins/payment_management_desjardins_view.xml'
--- payment_management_desjardins/payment_management_desjardins_view.xml 1970-01-01 00:00:00 +0000
+++ payment_management_desjardins/payment_management_desjardins_view.xml 2013-09-11 12:29:57 +0000
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<openerp>
+ <data>
+ <record id="view_desjardinsinfo_form" model="ir.ui.view">
+ <field name="name">res.company.desjardinsinfo.form</field>
+ <field name="model">res.company.desjardinsinfo</field>
+ <field name="arch" type="xml">
+ <form string="Desjardins harmonization info">
+ <field name="file_creation_number" required="1"/>
+ <field name="originator_id" required="1"/>
+ <field name="workdir" required="1"/>
+ <field name="bank_account_id" domain="[('company_id', '!=', False)]" widget="selection" />
+ <field name="account_id"/>
+ <field name="txn_code_debit" required="1"/>
+ <field name="txn_code_credit" required="1"/>
+ <field name="leave_vouchers_draft"/>
+ </form>
+ </field>
+ </record>
+ </data>
+
+</openerp>
=== added file 'payment_management_desjardins/record.py'
--- payment_management_desjardins/record.py 1970-01-01 00:00:00 +0000
+++ payment_management_desjardins/record.py 2013-09-11 12:29:57 +0000
@@ -0,0 +1,29 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 Savoir-faire Linux (<http://www.savoirfairelinux.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+from __future__ import division, unicode_literals
+
+from datetime import date, datetime
+
+def yyyddd(d):
+ three_digit_year = d.strftime('%Y')[1:4]
+ day_of_year = d.strftime('%j')
+ return three_digit_year + day_of_year
=== added directory 'payment_management_visa_desjardins'
=== added file 'payment_management_visa_desjardins/__init__.py'
--- payment_management_visa_desjardins/__init__.py 1970-01-01 00:00:00 +0000
+++ payment_management_visa_desjardins/__init__.py 2013-09-11 12:29:57 +0000
@@ -0,0 +1,22 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 Savoir-faire Linux (<http://www.savoirfairelinux.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import payment_management_visa_desjardins
=== added file 'payment_management_visa_desjardins/__openerp__.py'
--- payment_management_visa_desjardins/__openerp__.py 1970-01-01 00:00:00 +0000
+++ payment_management_visa_desjardins/__openerp__.py 2013-09-11 12:29:57 +0000
@@ -0,0 +1,44 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 Savoir-faire Linux (<http://www.savoirfairelinux.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+{
+ "name" : "Payment management module for visa desjardins",
+ "version" : "1.0",
+ "author" : "Savoir-faire Linux",
+ "website" : "http://www.savoirfairelinux.com",
+ "category" : "Accounting & Finance",
+ "description" : """
+ This module generates 'vdcoupon.txt' files from invoices.
+ """,
+ "depends" : [
+ 'payment_management_desjardins',
+ 'encrypted_credit_card',
+ ],
+ "init_xml" : [],
+ "update_xml" : [
+ ],
+ 'data' : [
+ 'payment_management_visa_desjardins_view.xml',
+ ],
+ "demo_xml" : [],
+ "installable" : True,
+ "certificate" : ''
+}
=== added directory 'payment_management_visa_desjardins/i18n'
=== added file 'payment_management_visa_desjardins/i18n/payment_management_visa_desjardins.pot'
--- payment_management_visa_desjardins/i18n/payment_management_visa_desjardins.pot 1970-01-01 00:00:00 +0000
+++ payment_management_visa_desjardins/i18n/payment_management_visa_desjardins.pot 2013-09-11 12:29:57 +0000
@@ -0,0 +1,27 @@
+# Translation of OpenERP Server.
+# This file contains the translation of the following modules:
+# * payment_management_visa_desjardins
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: OpenERP Server 7.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2013-08-21 13:54+0000\n"
+"PO-Revision-Date: 2013-08-21 13:54+0000\n"
+"Last-Translator: <>\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: payment_management_visa_desjardins
+#: model:ir.model,name:payment_management_visa_desjardins.model_res_company
+msgid "Companies"
+msgstr ""
+
+#. module: payment_management_visa_desjardins
+#: field:res.company,desjardins_cc_info:0
+msgid "Visa Desjardins Info"
+msgstr ""
+
=== added file 'payment_management_visa_desjardins/payment_management_visa_desjardins.py'
--- payment_management_visa_desjardins/payment_management_visa_desjardins.py 1970-01-01 00:00:00 +0000
+++ payment_management_visa_desjardins/payment_management_visa_desjardins.py 2013-09-11 12:29:57 +0000
@@ -0,0 +1,175 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 Savoir-faire Linux (<http://www.savoirfairelinux.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+# ABOUT THIS MODULE AND ENCRYPTION
+#
+# This module doesn't handle plain text CC numbers, it only has access to encrypted CC number
+# (managed by encrypted_credit_card). However, the text file it produces, "vdcoupons", wants plain
+# text CC numbers.
+#
+# Because the vdcoupons file we produce isn't going to be sent directly to Desjardins, we don't need
+# it to be exactly correct, but we also want to minimise the work that the external process, which
+# is going to decrypt the CC numbers before sending it to Desjardins. Therefore, we build the rest
+# of the vdcoupons file exactly as it's going to be sent. Thus, all the external process is going to
+# have to do is to decrypt and replace.
+
+from __future__ import division
+
+import os
+import re
+import logging
+import codecs
+
+from openerp.osv import orm, fields
+
+from .record import get_g_record, get_a_record, get_h_record, get_z_record
+
+_logger = logging.getLogger(__name__)
+
+class res_company_desjardinsinfo_visa(orm.Model):
+ _name = 'res.company.desjardinsinfo.visa'
+ _inherit = 'res.company.desjardinsinfo'
+ _columns = {
+ 'merchant_number': fields.char(
+ 'Merchant number for Desjardins',
+ size=8,
+ help="Merchant number assigned by DCS. Must be numeric."
+ ),
+ }
+
+class res_company(orm.Model):
+ _inherit = 'res.company'
+ _columns = {
+ 'desjardins_cc_info': fields.many2one('res.company.desjardinsinfo.visa', "Visa Desjardins Info"),
+ }
+
+ def _default_desjardins_cc_info(self, cr, uid, context):
+ res_company_desjardinsinfo = self.pool.get('res.company.desjardinsinfo.visa')
+ vals = {
+ 'txn_code_debit': '555',
+ 'txn_code_credit': '506',
+ }
+ res = res_company_desjardinsinfo.create(cr, uid, vals, context=context)
+ return res
+
+ _defaults = {
+ 'desjardins_cc_info': _default_desjardins_cc_info,
+ }
+
+def send_payment_func_visa_desjardins(cpr_obj, cr, uid, customer_payment_requests):
+ company_obj = cpr_obj.pool.get('res.company')
+ # TODO: support multi-company
+ company = company_obj.browse(cr, uid, 1)
+ desjinfo = company.desjardins_cc_info
+ directory = desjinfo.workdir
+
+ if not desjinfo.is_complete():
+ return False
+
+ toprocess = [cpr for cpr in customer_payment_requests if cpr.bank_account_id.state == 'credit_card']
+ if not toprocess:
+ return False
+ total_number_of_credit = 0
+ total_number_of_debit = 0
+ total_value_of_credit = 0
+ total_value_of_debit = 0
+ lines = []
+
+ # Type A record
+ a_record = get_a_record(len(lines)+1, company.desjardins_cc_info)
+ lines.append(a_record)
+
+ # Type G records
+ for cpr in toprocess:
+ bank_account = cpr.bank_account_id
+ partner = bank_account.partner_id
+ # The amount is in cents
+ amount = round(cpr.amount * 100)
+ if amount > 0:
+ total_number_of_debit += 1
+ total_value_of_debit += amount
+ else:
+ amount = abs(amount)
+ total_number_of_credit += 1
+ total_value_of_credit += amount
+ cc_number = '[RSA]%s[/RSA]' % bank_account.encrypted_cc_number.strip()
+ g_record = get_g_record(
+ len(lines)+1, company, amount, cc_number, bank_account.expiration_date, partner.id
+ )
+ lines.append(g_record)
+ cpr.write({'state': 'sent', 'batch_ref': 'visa_desjardins'})
+
+ # Type H record
+ h_record = get_h_record(
+ len(lines)+1, company.desjardins_cc_info,
+ total_number_of_debit, total_value_of_debit,
+ total_number_of_credit, total_value_of_credit
+ )
+ lines.append(h_record)
+
+ # Type Z record
+ z_record = get_z_record(
+ len(lines)+1, company.desjardins_cc_info,
+ total_number_of_debit, total_value_of_debit,
+ total_number_of_credit, total_value_of_credit
+ )
+ lines.append(z_record)
+ with codecs.open(os.path.join(directory, 'vdcoupon.txt'), 'wt', encoding='latin-1') as fp:
+ fp.write(u'\n'.join(lines))
+ desjinfo.increase_file_number()
+
+def receive_payment_func_visa_desjardins(cpr_obj, cr, uid):
+ company_obj = cpr_obj.pool.get('res.company')
+ company = company_obj.browse(cr, uid, 1)
+ desjinfo = company.desjardins_cc_info
+ directory = desjinfo.workdir
+ account_id = desjinfo.account_id.id
+ journal_id = desjinfo.bank_account_id.journal_id.id
+
+ filenames = os.listdir(directory)
+ filenames = [fn for fn in filenames if re.match(r"\d{8}-\d{4}-vdcoupon\.acc", fn)]
+ if not filenames:
+ # Nothing to process
+ return
+ for filename in filenames:
+ filepath = os.path.join(directory, filename)
+ lines = open(filepath, 'rt').readlines()
+ for line in lines:
+ if not line.startswith('G'):
+ continue
+ partner_id = int(line[118:118+19])
+ amount = int(line[27:27+10]) / 100
+ cpr_obj.register_payment_acceptation(
+ cr, uid, partner_id, amount, account_id, journal_id,
+ leave_draft=desjinfo.leave_vouchers_draft
+ )
+ os.rename(filepath, filepath[:-3] + 'imported')
+
+ # Any CPR with a "visa_desjardins" batch_ref can be considered as rejected
+ rejected_cpr_ids = cpr_obj.search(cr, uid, [('batch_ref', '=', 'visa_desjardins'), ('state', '=', 'sent')])
+ for cpr in cpr_obj.browse(cr, uid, rejected_cpr_ids):
+ cpr_obj.register_payment_refusal(cr, uid, cpr.partner_id.id)
+
+try:
+ from customer_payment_request import registry
+ registry.register(send_payment_func_visa_desjardins, receive_payment_func_visa_desjardins)
+except ImportError:
+ pass
=== added file 'payment_management_visa_desjardins/payment_management_visa_desjardins_view.xml'
--- payment_management_visa_desjardins/payment_management_visa_desjardins_view.xml 1970-01-01 00:00:00 +0000
+++ payment_management_visa_desjardins/payment_management_visa_desjardins_view.xml 2013-09-11 12:29:57 +0000
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<openerp>
+ <data>
+ <!-- I've tried inheriting the view from res.company.desjardinsinfo, but it didn't work. -->
+ <record id="view_desjardinsinfo_visa_form" model="ir.ui.view">
+ <field name="name">res.company.desjardinsinfo.visa.form</field>
+ <field name="model">res.company.desjardinsinfo.visa</field>
+ <field name="arch" type="xml">
+ <form string="Desjardins harmonization info">
+ <field name="merchant_number" required="1"/>
+ <field name="file_creation_number" required="1"/>
+ <field name="originator_id" required="1"/>
+ <field name="workdir" required="1"/>
+ <field name="bank_account_id" domain="[('company_id', '!=', False)]" widget="selection" />
+ <field name="account_id"/>
+ <field name="txn_code_debit" required="1"/>
+ <field name="txn_code_credit" required="1"/>
+ <field name="leave_vouchers_draft"/>
+ </form>
+ </field>
+ </record>
+
+ <record id="view_company_form" model="ir.ui.view">
+ <field name="name">res.company.form</field>
+ <field name="model">res.company</field>
+ <field name="inherit_id" ref="base.view_company_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//page[@string='General Information']/group/group" position="inside">
+ <field name="desjardins_cc_info"/>
+ </xpath>
+ </field>
+ </record>
+ </data>
+
+</openerp>
=== added file 'payment_management_visa_desjardins/record.py'
--- payment_management_visa_desjardins/record.py 1970-01-01 00:00:00 +0000
+++ payment_management_visa_desjardins/record.py 2013-09-11 12:29:57 +0000
@@ -0,0 +1,108 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 Savoir-faire Linux (<http://www.savoirfairelinux.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+from __future__ import division, unicode_literals
+
+from datetime import date, datetime
+
+from payment_management_desjardins.record import yyyddd
+
+def get_a_record(seq, desjinfo):
+ return ''.join([
+ 'A',
+ '%09d' % seq,
+ desjinfo.originator_id[:10].upper().ljust(10, ' '),
+ '%04d' % desjinfo.file_creation_number,
+ yyyddd(datetime.now()),
+ '81510',
+ ' ' * 1429,
+ ])
+
+def get_h_record(seq, desjinfo, num_debit, total_debit, num_credit, total_credit):
+ return ''.join([
+ 'H',
+ '%09d' % seq,
+ desjinfo.originator_id[:10].upper().ljust(10, ' '),
+ '%04d' % desjinfo.file_creation_number,
+ '%014d' % total_debit,
+ '%08d' % num_debit,
+ '%014d' % total_credit,
+ '%08d' % num_credit,
+ '1',
+ '0' * 10,
+ '0' * 4,
+ desjinfo.merchant_number[:8].rjust(8, '0'),
+ ' ' * 1373,
+ ])
+
+def get_z_record(seq, desjinfo, num_debit, total_debit, num_credit, total_credit):
+ return ''.join([
+ 'Z',
+ '%09d' % seq,
+ desjinfo.originator_id[:10].upper().ljust(10, ' '),
+ '%04d' % desjinfo.file_creation_number,
+ '%014d' % total_debit,
+ '%08d' % num_debit,
+ '%014d' % total_credit,
+ '%08d' % num_credit,
+ '1',
+ '0' * 10,
+ '0' * 4,
+ ' ' * 1381,
+ ])
+
+def get_g_record(seq, company, amount, cc_number, cc_expiration, partner_ref):
+ desjinfo = company.desjardins_cc_info
+ # amount is an integer representing cents
+ if amount > 0:
+ txn_type = desjinfo.txn_code_debit
+ else:
+ txn_type = desjinfo.txn_code_credit
+ amount = abs(amount)
+ return ''.join([
+ 'G', # Alpha 1
+ '%09d' % seq, # Num 9
+ desjinfo.originator_id[:10].upper().ljust(10, ' '), # Alpha 10
+ '%04d' % desjinfo.file_creation_number,# Num 4
+ txn_type, # Num 3
+ '%010d' % amount, # Num 10
+ date.today().strftime("%m%d%y"), # Num 6
+ cc_number.rjust(19, '0'),
+ '0' * 2, # Alpha 2
+ ' ' * 2, # Alpha 2
+ ' ' * 2, # Alpha 2
+ ' ', # Alpha 1
+ ' ', # Alpha 1
+ desjinfo.merchant_number[:8].rjust(8, '0'), # Num 8
+ company.name[:25].ljust(25, ' '), # Alpha 25
+ (company.city or '')[:13].ljust(13, ' '), # Alpha 13
+ company.state_id.code or ' ', # Alpha 2
+ '%019d' % partner_ref, # Num 19
+ '0' * 6, # Alpha 6
+ '0' * 9, # Alpha 9
+ '0' * 4, # Num 4
+ '0' * 4, # Num 4
+ ' ' * 5, # Alpha 5
+ '0', # Num 1
+ '0' * 8, # Alpha 8
+ cc_expiration, # Alpha 4
+ ' ' * 26, # Alpha 26
+ ])
=== added directory 'payment_management_visa_desjardins/scripts'
=== added file 'payment_management_visa_desjardins/scripts/vdcoupon_watch_recv.py'
--- payment_management_visa_desjardins/scripts/vdcoupon_watch_recv.py 1970-01-01 00:00:00 +0000
+++ payment_management_visa_desjardins/scripts/vdcoupon_watch_recv.py 2013-09-11 12:29:57 +0000
@@ -0,0 +1,78 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 Savoir-faire Linux (<http://www.savoirfairelinux.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import argparse
+import time
+import os.path as op
+from datetime import datetime
+import re
+
+import paramiko
+
+def parse_options():
+ parser = argparse.ArgumentParser(description='Wait for vdcoupon.acc from Desjardins, remove CC numbers and timestamp it.')
+ parser.add_argument('dest_folder', help="Folder where vdcoupon is sent.")
+ parser.add_argument('ssh_cred', help="Desjardins SSH creds (user@host:path).")
+ return parser.parse_args()
+
+def process_file(sftp, source_path, dest_folder):
+ source_fp = sftp.open(source_path, 'rt')
+ lines = source_fp.readlines()
+ newlines = []
+ for line in lines:
+ if line.startswith('G'):
+ chars = list(line)
+ # credit card numbers are the 19 characters starting at index 1525
+ chars[43:43+19] = '0' * 19
+ line = ''.join(chars)
+ newlines.append(line)
+ source_fp.close()
+ timestamp = datetime.now().strftime('%Y%m%d-%H%M')
+ result_filename = op.join(dest_folder, '%s-vdcoupon.acc' % timestamp)
+ with open(result_filename, 'wt') as result_fp:
+ result_fp.writelines(newlines)
+ sftp.remove(source_path)
+
+def watch_forever(options):
+ m = re.match(r"(.+)@([^:]+):?(.*)", options.ssh_cred)
+ username = m.group(1)
+ hostname = m.group(2)
+ sshpath = m.group(3)
+ ssh = paramiko.SSHClient()
+ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ ssh.load_system_host_keys()
+ ssh.connect(hostname, 22, username)
+ sftp = ssh.open_sftp()
+ while True:
+ if 'vdcoupon.acc' in sftp.listdir(sshpath):
+ print "File found! Processing"
+ source_path = op.join(sshpath, 'vdcoupon.acc')
+ process_file(sftp, source_path, options.dest_folder)
+ print "Process done, file deleted"
+ time.sleep(10)
+
+def main():
+ options = parse_options()
+ print "Starting to watch for vdcoupon.acc with dest {}...".format(options.dest_folder)
+ watch_forever(options)
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file
=== added file 'payment_management_visa_desjardins/scripts/vdcoupon_watch_send.py'
--- payment_management_visa_desjardins/scripts/vdcoupon_watch_send.py 1970-01-01 00:00:00 +0000
+++ payment_management_visa_desjardins/scripts/vdcoupon_watch_send.py 2013-09-11 12:29:57 +0000
@@ -0,0 +1,84 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 Savoir-faire Linux (<http://www.savoirfairelinux.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import argparse
+import time
+import os
+import os.path as op
+import re
+import binascii
+
+import paramiko
+from Crypto.PublicKey import RSA
+
+def parse_options():
+ parser = argparse.ArgumentParser(description='Wait for vdcoupon.txt from OpenERP, decrypt it and send it to Desjardins.')
+ parser.add_argument('watched_folder', help="Folder where vdcoupon is sent.")
+ parser.add_argument('private_key', help="Private RSA key to decrypt CC numbers with.")
+ parser.add_argument('ssh_cred', help="Desjardins SSH creds (user@host:path).")
+ return parser.parse_args()
+
+def process_file(watched_file, private_key):
+ def repl(match):
+ encrypted_cc_base64 = match.group(1)
+ encrypted_cc = binascii.a2b_base64(encrypted_cc_base64)
+ decrypted_cc = private_key.decrypt(encrypted_cc)
+ return decrypted_cc.zfill(19)
+
+ with open(watched_file, 'rt') as source_fp:
+ contents = source_fp.read()
+ contents = re.sub(r"\[RSA\](.+?)\[/RSA\]", repl, contents)
+ return contents
+
+def send_contents(hostname, username, sshpath, contents):
+ ssh = paramiko.SSHClient()
+ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ ssh.load_system_host_keys()
+ ssh.connect(hostname, 22, username)
+ sftp = ssh.open_sftp()
+ fp = sftp.open(op.join(sshpath, 'vdcoupon.txt'), 'wt')
+ fp.write(contents)
+ fp.close()
+
+def watch_forever(options):
+ watched_file = op.join(options.watched_folder, 'vdcoupon.txt')
+ with open(options.private_key, 'rt') as key_fp:
+ private_key = RSA.importKey(key_fp.read())
+ m = re.match(r"(.+)@([^:]+):?(.*)", options.ssh_cred)
+ username = m.group(1)
+ hostname = m.group(2)
+ sshpath = m.group(3)
+ while True:
+ if op.exists(watched_file):
+ print "File found! Processing"
+ contents = process_file(watched_file, private_key)
+ send_contents(hostname, username, sshpath, contents)
+ os.remove(watched_file)
+ print "Process done, file deleted"
+ time.sleep(10)
+
+def main():
+ options = parse_options()
+ print "Starting to watch for vdcoupon.txt in {}...".format(options.watched_folder)
+ watch_forever(options)
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file
Follow ups
-
Re: [Merge] lp:~savoirfairelinux-openerp/banking-addons/loose-coupling into lp:banking-addons/bank-statement-reconcile-70
From: Pedro Manuel Baeza, 2014-07-02
-
[Merge] lp:~savoirfairelinux-openerp/banking-addons/loose-coupling into lp:banking-addons/bank-statement-reconcile-7.0
From: Maxime Chambreuil (http://www.savoirfairelinux.com), 2013-12-27
-
Re: lp:~savoirfairelinux-openerp/banking-addons/loose-coupling into lp:banking-addons/bank-statement-reconcile-7.0
From: Virgil Dupras, 2013-09-17
-
Re: lp:~savoirfairelinux-openerp/banking-addons/loose-coupling into lp:banking-addons/bank-statement-reconcile-7.0
From: Ronald Portier (Therp), 2013-09-16
-
Re: lp:~savoirfairelinux-openerp/banking-addons/loose-coupling into lp:banking-addons/bank-statement-reconcile-7.0
From: Serpent Consulting Services, 2013-09-16
-
Re: lp:~savoirfairelinux-openerp/banking-addons/loose-coupling into lp:banking-addons/bank-statement-reconcile-7.0
From: Serpent Consulting Services, 2013-09-16
-
Re: lp:~savoirfairelinux-openerp/banking-addons/loose-coupling into lp:banking-addons/bank-statement-reconcile-7.0
From: Virgil Dupras, 2013-09-16
-
Re: lp:~savoirfairelinux-openerp/banking-addons/loose-coupling into lp:banking-addons/bank-statement-reconcile-7.0
From: Serpent Consulting Services, 2013-09-16
-
Re: lp:~savoirfairelinux-openerp/banking-addons/loose-coupling into lp:banking-addons/bank-statement-reconcile-7.0
From: Virgil Dupras, 2013-09-16
-
Re: lp:~savoirfairelinux-openerp/banking-addons/loose-coupling into lp:banking-addons/bank-statement-reconcile-7.0
From: Serpent Consulting Services, 2013-09-11