Merge branch 'master-sheetal-06072018' into 'master-patch-july-2018'

Master sheetal 06072018

See merge request flectra-hq/flectra!96
This commit is contained in:
Parthiv Patel 2018-07-13 09:34:32 +00:00
commit 3881ab378e
44 changed files with 364 additions and 67 deletions

View File

@ -78,6 +78,8 @@ class AuthSignupHome(Home):
login, request.env.user.login, request.httprequest.remote_addr) login, request.env.user.login, request.httprequest.remote_addr)
request.env['res.users'].sudo().reset_password(login) request.env['res.users'].sudo().reset_password(login)
qcontext['message'] = _("An email has been sent with credentials to reset your password") qcontext['message'] = _("An email has been sent with credentials to reset your password")
except UserError as e:
qcontext['error'] = e.name or e.value
except SignupError: except SignupError:
qcontext['error'] = _("Could not reset your password") qcontext['error'] = _("Could not reset your password")
_logger.exception('error when resetting password') _logger.exception('error when resetting password')

View File

@ -45,6 +45,9 @@
</div> </div>
<div style="padding:0px;width:600px;margin:auto; margin-top: 10px; background: #fff repeat top /100%;color:#777777"> <div style="padding:0px;width:600px;margin:auto; margin-top: 10px; background: #fff repeat top /100%;color:#777777">
${user.signature | safe} ${user.signature | safe}
<p style="font-size: 11px; margin-top: 10px;">
<strong>Sent by ${user.company_id.name} using <a href="www.flectrahq.com" style="text-decoration:none; color: #875A7B;">Flectra</a></strong>
</p>
</div>]]></field> </div>]]></field>
<field name="user_signature" eval="False"/> <field name="user_signature" eval="False"/>
</record> </record>
@ -81,6 +84,9 @@
</div> </div>
<div style="padding:0px;width:600px;margin:auto; margin-top: 10px; background: #fff repeat top /100%;color:#777777"> <div style="padding:0px;width:600px;margin:auto; margin-top: 10px; background: #fff repeat top /100%;color:#777777">
${user.signature | safe} ${user.signature | safe}
<p style="font-size: 11px; margin-top: 10px;">
<strong>Sent by ${user.company_id.name} using <a href="www.flectrahq.com" style="text-decoration:none; color: #875A7B;">Flectra</a></strong>
</p>
</div>]]></field> </div>]]></field>
<field name="user_signature" eval="False"/> <field name="user_signature" eval="False"/>
</record> </record>
@ -122,6 +128,9 @@
</div> </div>
<div style="padding:0px;width:600px;margin:auto; margin-top: 10px; background: #fff repeat top /100%;color:#777777"> <div style="padding:0px;width:600px;margin:auto; margin-top: 10px; background: #fff repeat top /100%;color:#777777">
${user.signature | safe} ${user.signature | safe}
<p style="font-size: 11px; margin-top: 10px;">
<strong>Sent by ${user.company_id.name} using <a href="www.flectrahq.com" style="text-decoration:none; color: #875A7B;">Flectra</a></strong>
</p>
</div></field> </div></field>
<field name="user_signature" eval="False"/> <field name="user_signature" eval="False"/>
</record> </record>

View File

@ -32,6 +32,7 @@ class ResPartner(models.Model):
signup_url = fields.Char(compute='_compute_signup_url', string='Signup URL') signup_url = fields.Char(compute='_compute_signup_url', string='Signup URL')
@api.multi @api.multi
@api.depends('signup_token', 'signup_expiration')
def _compute_signup_valid(self): def _compute_signup_valid(self):
dt = now() dt = now()
for partner in self: for partner in self:

View File

@ -155,5 +155,5 @@ class ResUsers(models.Model):
sup = super(ResUsers, self) sup = super(ResUsers, self)
if not default or not default.get('email'): if not default or not default.get('email'):
# avoid sending email to the user we are duplicating # avoid sending email to the user we are duplicating
sup = super(ResUsers, self.with_context(reset_password=False)) sup = super(ResUsers, self.with_context(no_reset_password=True))
return sup.copy(default=default) return sup.copy(default=default)

View File

@ -53,7 +53,7 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, {
navigator.userAgent.match(/iPod/i) || navigator.userAgent.match(/iPod/i) ||
navigator.userAgent.match(/BlackBerry/i) || navigator.userAgent.match(/BlackBerry/i) ||
navigator.userAgent.match(/Windows Phone/i); navigator.userAgent.match(/Windows Phone/i);
this.isChromeMobile = isMobile && window.chrome; this.isChromeMobile = isMobile && navigator.userAgent.match(/Chrome/i);
// Creates an input who will receive the barcode scanner value. // Creates an input who will receive the barcode scanner value.
if (this.isChromeMobile) { if (this.isChromeMobile) {
@ -61,13 +61,17 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, {
name: 'barcode', name: 'barcode',
type: 'text', type: 'text',
css: { css: {
'position': 'absolute', 'position': 'fixed',
'opacity': 0, 'top': '50%',
'transform': 'translateY(-50%)',
'z-index': '-1',
}, },
}); });
// Avoid to show autocomplete for a non appearing input
this.$barcodeInput.attr('autocomplete', 'off');
} }
this.__removeBarcodeField = _.debounce(this._removeBarcodeField, this.inputTimeOut); this.__blurBarcodeInput = _.debounce(this._blurBarcodeInput, this.inputTimeOut);
}, },
handle_buffered_keys: function() { handle_buffered_keys: function() {
@ -230,7 +234,7 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, {
this.max_time_between_keys_in_ms); this.max_time_between_keys_in_ms);
} }
// if the barcode input doesn't receive keydown for a while, remove it. // if the barcode input doesn't receive keydown for a while, remove it.
this.__removeBarcodeField(); this.__blurBarcodeInput();
} }
}, },
@ -245,21 +249,22 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, {
var barcodeValue = this.$barcodeInput.val(); var barcodeValue = this.$barcodeInput.val();
if (barcodeValue.match(this.regexp)) { if (barcodeValue.match(this.regexp)) {
core.bus.trigger('barcode_scanned', barcodeValue, $(e.target).parent()[0]); core.bus.trigger('barcode_scanned', barcodeValue, $(e.target).parent()[0]);
this.$barcodeInput.val(''); this._blurBarcodeInput();
} }
}, },
/** /**
* Remove the temporary input created to store the barcode value. * Removes the value and focus from the barcode input.
* If nothing happens, this input will be removed, so the focus will be lost * If nothing happens, the focus will be lost and
* and the virtual keyboard on mobile devices will be closed. * the virtual keyboard on mobile devices will be closed.
* *
* @private * @private
*/ */
_removeBarcodeField: function () { _blurBarcodeInput: function () {
if (this.$barcodeInput) { if (this.$barcodeInput) {
// Reset the value and remove from the DOM. // Close the virtual keyboard on mobile browsers
this.$barcodeInput.val('').remove(); // FIXME: actually we can't prevent keyboard from opening
this.$barcodeInput.val('').blur();
} }
}, },

View File

@ -146,10 +146,10 @@ FormController.include({
* @param {Object} activeBarcode: options sent by the field who use barcode features * @param {Object} activeBarcode: options sent by the field who use barcode features
* @returns {Deferred} * @returns {Deferred}
*/ */
_barcodeSelectedCandidate: function (candidate, record, barcode, activeBarcode) { _barcodeSelectedCandidate: function (candidate, record, barcode, activeBarcode, quantity) {
var changes = {}; var changes = {};
var candidateChanges = {}; var candidateChanges = {};
candidateChanges[activeBarcode.quantity] = candidate.data[activeBarcode.quantity] + 1; candidateChanges[activeBarcode.quantity] = quantity ? quantity : candidate.data[activeBarcode.quantity] + 1;
changes[activeBarcode.fieldName] = { changes[activeBarcode.fieldName] = {
operation: 'UPDATE', operation: 'UPDATE',
id: candidate.id, id: candidate.id,
@ -283,6 +283,9 @@ FormController.include({
function (reserved) {return barcode.indexOf(reserved) === 0;}); function (reserved) {return barcode.indexOf(reserved) === 0;});
var hasCommand = false; var hasCommand = false;
var defs = []; var defs = [];
if (! $.contains(target, self.el)) {
return;
}
for (var k in self.activeBarcode) { for (var k in self.activeBarcode) {
var activeBarcode = self.activeBarcode[k]; var activeBarcode = self.activeBarcode[k];
// Handle the case where there are several barcode widgets on the same page. Since the // Handle the case where there are several barcode widgets on the same page. Since the
@ -306,6 +309,8 @@ FormController.include({
} }
return self.alive($.when.apply($, defs)).then(function () { return self.alive($.when.apply($, defs)).then(function () {
if (!prefixed) { if (!prefixed) {
// remember the barcode scanned for the quantity listener
self.current_barcode = barcode;
// redraw the view if we scanned a real barcode (required if // redraw the view if we scanned a real barcode (required if
// we manually apply the change in JS, e.g. incrementing the // we manually apply the change in JS, e.g. incrementing the
// quantity) // quantity)
@ -321,6 +326,9 @@ FormController.include({
_quantityListener: function (event) { _quantityListener: function (event) {
var character = String.fromCharCode(event.which); var character = String.fromCharCode(event.which);
if (! $.contains(event.target, this.el)) {
return;
}
// only catch the event if we're not focused in // only catch the event if we're not focused in
// another field and it's a number // another field and it's a number
if (!$(event.target).is('body, .modal') || !/[0-9]/.test(character)) { if (!$(event.target).is('body, .modal') || !/[0-9]/.test(character)) {
@ -355,9 +363,10 @@ FormController.include({
title: _t('Set quantity'), title: _t('Set quantity'),
buttons: [{text: _t('Select'), classes: 'btn-primary', close: true, click: function () { buttons: [{text: _t('Select'), classes: 'btn-primary', close: true, click: function () {
var new_qty = this.$content.find('.o_set_qty_input').val(); var new_qty = this.$content.find('.o_set_qty_input').val();
var values = {}; var record = self.model.get(self.handle);
values[activeBarcode.quantity] = parseFloat(new_qty); return self._barcodeSelectedCandidate(activeBarcode.candidate, record,
return self.model.notifyChanges(activeBarcode.candidate.id, values).then(function () { self.current_barcode, activeBarcode, parseFloat(new_qty))
.then(function () {
self.update({}, {reload: false}); self.update({}, {reload: false});
}); });
}}, {text: _t('Discard'), close: true}], }}, {text: _t('Discard'), close: true}],

View File

@ -18,6 +18,9 @@ var BarcodeParser = Class.extend({
// only when those data have been loaded // only when those data have been loaded
load: function(){ load: function(){
var self = this; var self = this;
if (!this.nomenclature_id) {
return;
}
var id = this.nomenclature_id[0]; var id = this.nomenclature_id[0];
rpc.query({ rpc.query({
model: 'barcode.nomenclature', model: 'barcode.nomenclature',

View File

@ -404,4 +404,136 @@ QUnit.test('specification of widget barcode_handler', function (assert) {
barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = delay; barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = delay;
delete fieldRegistry.map.test_barcode_handler; delete fieldRegistry.map.test_barcode_handler;
}); });
QUnit.test('specification of widget barcode_handler with keypress and notifyChange', function (assert) {
assert.expect(6);
var done = assert.async();
var delay = barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms;
barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = 0;
this.data.order.onchanges = {
_barcode_scanned: function () {},
};
// Define a specific barcode_handler widget for this test case
var TestBarcodeHandler = AbstractField.extend({
init: function () {
this._super.apply(this, arguments);
this.trigger_up('activeBarcode', {
name: 'test',
fieldName: 'line_ids',
notifyChange: false,
setQuantityWithKeypress: true,
quantity: 'quantity',
commands: {
barcode: '_barcodeAddX2MQuantity',
}
});
},
});
fieldRegistry.add('test_barcode_handler', TestBarcodeHandler);
var form = createView({
View: FormView,
model: 'order',
data: this.data,
arch: '<form>' +
'<field name="_barcode_scanned" widget="test_barcode_handler"/>' +
'<field name="line_ids">' +
'<tree>' +
'<field name="product_id"/>' +
'<field name="product_barcode" invisible="1"/>' +
'<field name="quantity"/>' +
'</tree>' +
'</field>' +
'</form>',
mockRPC: function (route, args) {
assert.step(args.method);
return this._super.apply(this, arguments);
},
res_id: 1,
viewOptions: {
mode: 'edit',
},
});
_.each(['1','2','3','4','5','6','7','8','9','0','Enter'], triggerKeypressEvent);
// Quantity listener should open a dialog.
triggerKeypressEvent('5');
setTimeout(function () {
var keycode = $.ui.keyCode.ENTER;
assert.strictEqual($('.modal .modal-body').length, 1, 'should open a modal with a quantity as input');
assert.strictEqual($('.modal .modal-body .o_set_qty_input').val(), '5', 'the quantity by default in the modal shoud be 5');
$('.modal .modal-body .o_set_qty_input').val('7');
$('.modal .modal-body .o_set_qty_input').trigger($.Event('keypress', {which: keycode, keyCode: keycode}));
assert.strictEqual(form.$('.o_data_row .o_data_cell:nth(1)').text(), '7',
"quantity checked should be 7");
assert.verifySteps(['read', 'read']);
form.destroy();
barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = delay;
delete fieldRegistry.map.test_barcode_handler;
done();
});
});
QUnit.test('barcode_scanned only trigger error for active view', function (assert) {
assert.expect(2);
this.data.order_line.fields._barcode_scanned = {string: 'Barcode scanned', type: 'char'};
var form = createView({
View: FormView,
model: 'order',
data: this.data,
arch: '<form>' +
'<field name="_barcode_scanned" widget="barcode_handler"/>' +
'<field name="line_ids">' +
'<tree>' +
'<field name="product_id"/>' +
'<field name="product_barcode" invisible="1"/>' +
'<field name="quantity"/>' +
'</tree>' +
'</field>' +
'</form>',
archs: {
"order_line,false,form":
'<form string="order line">' +
'<field name="_barcode_scanned" widget="barcode_handler"/>' +
'<field name="product_id"/>' +
'</form>',
},
res_id: 1,
intercepts: {
warning: function (event) {
assert.step(event.name);
}
},
viewOptions: {
mode: 'edit',
},
});
form.$('.o_data_row:first').click();
// We do not trigger on the body since modal and
// form view are both inside it.
function modalTriggerKeypressEvent(char) {
var keycode;
if (char === "Enter") {
keycode = $.ui.keyCode.ENTER;
} else {
keycode = char.charCodeAt(0);
}
return $('.modal').trigger($.Event('keypress', {which: keycode, keyCode: keycode}));
}
_.each(['O','-','B','T','N','.','c','a','n','c','e','l','Enter'], modalTriggerKeypressEvent);
assert.verifySteps(['warning'], "only one event should be triggered");
form.destroy();
});
}); });

View File

@ -10,7 +10,7 @@ from flectra.tools.translate import _
class Partner(models.Model): class Partner(models.Model):
_inherit = 'res.partner' _inherit = 'res.partner'
country_enforce_cities = fields.Boolean(related='country_id.enforce_cities') country_enforce_cities = fields.Boolean(related='country_id.enforce_cities', readonly=True)
city_id = fields.Many2one('res.city', string='City') city_id = fields.Many2one('res.city', string='City')
@api.onchange('city_id') @api.onchange('city_id')

View File

@ -17,7 +17,8 @@ For example, in legal reports, some countries need to split the street into seve
with the street name, the house number, and room number. with the street name, the house number, and room number.
""", """,
'data': [ 'data': [
'views/base_address_extended.xml' 'views/base_address_extended.xml',
'data/base_address_extended_data.xml',
], ],
'depends': ['base'], 'depends': ['base'],
} }

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<flectra>
<data noupdate="1">
<record id="base.nl" model="res.country">
<field eval="'%(street_name)s %(street_number)s/%(street_number2)s'" name="street_format" />
</record>
</data>
</flectra>

View File

@ -89,6 +89,7 @@ class WebsitePayment(http.Controller):
'amount': float(amount), 'amount': float(amount),
'currency_id': currency_id, 'currency_id': currency_id,
'partner_id': partner_id, 'partner_id': partner_id,
'type': 'form_save' if acquirer.save_token != 'none' and partner_id else 'form',
} }
tx = request.env['payment.transaction'].sudo().create(values) tx = request.env['payment.transaction'].sudo().create(values)
@ -117,7 +118,8 @@ class WebsitePayment(http.Controller):
'amount': float(amount), 'amount': float(amount),
'currency_id': int(currency_id), 'currency_id': int(currency_id),
'partner_id': partner_id, 'partner_id': partner_id,
'payment_token_id': pm_id 'payment_token_id': pm_id,
'type': 'form_save' if token.acquirer_id.save_token != 'none' and partner_id else 'form',
} }
tx = request.env['payment.transaction'].sudo().create(values) tx = request.env['payment.transaction'].sudo().create(values)

View File

@ -3,8 +3,7 @@
<template id="default_acquirer_button"> <template id="default_acquirer_button">
<input type="hidden" name="data_set" t-att-data-action-url="tx_url"/> <input type="hidden" name="data_set" t-att-data-action-url="tx_url"/>
<input type="hidden" name="csrf_token" <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
t-att-value="request.csrf_token()"/>
<t t-if="return_url"> <t t-if="return_url">
<input type="hidden" name="return_url" t-att-value="return_url"/> <input type="hidden" name="return_url" t-att-value="return_url"/>
</t> </t>
@ -148,8 +147,7 @@
</field> </field>
<field name="description" type="html"> <field name="description" type="html">
<p> <p>
Provide instructions to customers so that they can pay their Provide instructions to customers so that they can pay their orders manually.
orders manually.
</p> </p>
</field> </field>
</record> </record>

View File

@ -3,3 +3,4 @@
from . import payment_acquirer from . import payment_acquirer
from . import res_partner from . import res_partner
from . import account_payment from . import account_payment
from . import chart_template

View File

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
from flectra import api, fields, models, _
class WizardMultiChartsAccounts(models.TransientModel):
_inherit = 'wizard.multi.charts.accounts'
@api.multi
def _create_bank_journals_from_o2m(self, company, acc_template_ref):
res = super(WizardMultiChartsAccounts, self)._create_bank_journals_from_o2m(company, acc_template_ref)
# Try to generate the missing journals
return res + self.env['payment.acquirer']._create_missing_journal_for_acquirers(company=company)

View File

@ -8,6 +8,7 @@ from flectra import api, exceptions, fields, models, _
from flectra.tools import consteq, float_round, image_resize_images, image_resize_image, ustr from flectra.tools import consteq, float_round, image_resize_images, image_resize_image, ustr
from flectra.addons.base.module import module from flectra.addons.base.module import module
from flectra.exceptions import ValidationError from flectra.exceptions import ValidationError
from flectra import api, SUPERUSER_ID
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -20,6 +21,11 @@ def _partner_split_name(partner_name):
return [' '.join(partner_name.split()[:-1]), ' '.join(partner_name.split()[-1:])] return [' '.join(partner_name.split()[:-1]), ' '.join(partner_name.split()[-1:])]
def create_missing_journal_for_acquirers(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})
env['payment.acquirer']._create_missing_journal_for_acquirers()
class PaymentAcquirer(models.Model): class PaymentAcquirer(models.Model):
""" Acquirer Model. Each specific acquirer can extend the model by adding """ Acquirer Model. Each specific acquirer can extend the model by adding
its own fields, using the acquirer_name as a prefix for the new fields. its own fields, using the acquirer_name as a prefix for the new fields.
@ -51,6 +57,9 @@ class PaymentAcquirer(models.Model):
_description = 'Payment Acquirer' _description = 'Payment Acquirer'
_order = 'website_published desc, sequence, name' _order = 'website_published desc, sequence, name'
def _get_default_view_template_id(self):
return self.env.ref('payment.default_acquirer_button', raise_if_not_found=False)
name = fields.Char('Name', required=True, translate=True) name = fields.Char('Name', required=True, translate=True)
description = fields.Html('Description') description = fields.Html('Description')
sequence = fields.Integer('Sequence', default=10, help="Determine the display order") sequence = fields.Integer('Sequence', default=10, help="Determine the display order")
@ -196,6 +205,58 @@ class PaymentAcquirer(models.Model):
""" """
return dict(authorize=[], tokenize=[], fees=[]) return dict(authorize=[], tokenize=[], fees=[])
@api.multi
def _prepare_account_journal_vals(self):
'''Prepare the values to create the acquirer's journal.
:return: a dictionary to create a account.journal record.
'''
self.ensure_one()
account_vals = self.env['account.journal']._prepare_liquidity_account(
self.name, self.company_id, None, 'bank')
account_vals['user_type_id'] = self.env.ref('account.data_account_type_current_assets').id
account_vals['reconcile'] = True
account = self.env['account.account'].create(account_vals)
return {
'name': self.name,
'code': self.name.upper(),
'sequence': 999,
'type': 'bank',
'company_id': self.company_id.id,
'default_debit_account_id': account.id,
'default_credit_account_id': account.id,
# Show the journal on dashboard if the acquirer is published on the website.
'show_on_dashboard': self.website_published,
# Don't show payment methods in the backend.
'inbound_payment_method_ids': [],
'outbound_payment_method_ids': [],
}
@api.model
def _create_missing_journal_for_acquirers(self, company=None):
'''Create the journal for active acquirers.
We want one journal per acquirer. However, we can't create them during the 'create' of the payment.acquirer
because every acquirers are defined on the 'payment' module but is active only when installing their own module
(e.g. payment_paypal for Paypal). We can't do that in such modules because we have no guarantee the chart template
is already installed.
'''
# Search for installed acquirers modules.
# If this method is triggered by a post_init_hook, the module is 'to install'.
# If the trigger comes from the chart template wizard, the modules are already installed.
acquirer_modules = self.env['ir.module.module'].search(
[('name', 'like', 'payment_%'), ('state', 'in', ('to install', 'installed'))])
acquirer_names = [a.name.split('_')[1] for a in acquirer_modules]
# Search for acquirers having no journal
company = company or self.env.user.company_id
acquirers = self.env['payment.acquirer'].search(
[('provider', 'in', acquirer_names), ('journal_id', '=', False), ('company_id', '=', company.id)])
journals = self.env['account.journal']
for acquirer in acquirers.filtered(lambda l: not l.journal_id and l.company_id.chart_template_id):
acquirer.journal_id = self.env['account.journal'].create(acquirer._prepare_account_journal_vals())
journals += acquirer.journal_id
return journals
@api.model @api.model
def create(self, vals): def create(self, vals):
image_resize_images(vals) image_resize_images(vals)
@ -208,7 +269,13 @@ class PaymentAcquirer(models.Model):
@api.multi @api.multi
def toggle_website_published(self): def toggle_website_published(self):
self.write({'website_published': not self.website_published}) ''' When clicking on the website publish toggle button, the website_published is reversed and
the acquirer journal is set or not in favorite on the dashboard.
'''
self.ensure_one()
self.website_published = not self.website_published
if self.journal_id:
self.journal_id.show_on_dashboard = self.website_published
return True return True
@api.multi @api.multi
@ -617,6 +684,10 @@ class PaymentTransaction(models.Model):
@api.model @api.model
def get_next_reference(self, reference): def get_next_reference(self, reference):
return self._get_next_reference(reference)
@api.model
def _get_next_reference(self, reference, acquirer=None):
ref_suffix = 1 ref_suffix = 1
init_ref = reference init_ref = reference
while self.env['payment.transaction'].sudo().search_count([('reference', '=', reference)]): while self.env['payment.transaction'].sudo().search_count([('reference', '=', reference)]):

View File

@ -414,8 +414,14 @@ flectra.define('payment.payment_form', function (require) {
}, },
displayError: function (title, message) { displayError: function (title, message) {
var $checkedRadio = this.$('input[type="radio"]:checked'), var $checkedRadio = this.$('input[type="radio"]:checked'),
acquirerID = this.getAcquirerIdFromRadio($checkedRadio[0]), acquirerID = this.getAcquirerIdFromRadio($checkedRadio[0]);
var $acquirerForm;
if (this.isNewPaymentRadio($checkedRadio[0])) {
$acquirerForm = this.$('#o_payment_add_token_acq_' + acquirerID); $acquirerForm = this.$('#o_payment_add_token_acq_' + acquirerID);
}
else if (this.isFormPaymentRadio($checkedRadio[0])) {
$acquirerForm = this.$('#o_payment_form_acq_' + acquirerID);
}
if ($checkedRadio.length === 0) { if ($checkedRadio.length === 0) {
return new Dialog(null, { return new Dialog(null, {

View File

@ -71,7 +71,7 @@
</page> </page>
<page string="Messages"> <page string="Messages">
<group> <group>
<field name="pre_msg"/> <field name="pre_msg" invisible="1"/>
<field name="post_msg"/> <field name="post_msg"/>
<field name="pending_msg"/> <field name="pending_msg"/>
<field name="done_msg"/> <field name="done_msg"/>
@ -81,7 +81,7 @@
</page> </page>
<page string="Configuration"> <page string="Configuration">
<group name="acquirer_config"> <group name="acquirer_config">
<field name="journal_id" context="{'default_type': 'bank'}" required="1"/> <field name="journal_id" context="{'default_type': 'bank'}"/>
<field name="capture_manually" attrs="{'invisible': [('authorize_implemented', '=', False)]}"/> <field name="capture_manually" attrs="{'invisible': [('authorize_implemented', '=', False)]}"/>
<field name="save_token" widget="radio" attrs="{'invisible': ['|', ('token_implemented', '=', False), ('payment_flow', '=', 's2s')]}"/> <field name="save_token" widget="radio" attrs="{'invisible': ['|', ('token_implemented', '=', False), ('payment_flow', '=', 's2s')]}"/>
<field name="fees_active" attrs="{'invisible': [('fees_implemented', '=', False)]}"/> <field name="fees_active" attrs="{'invisible': [('fees_implemented', '=', False)]}"/>
@ -113,7 +113,7 @@
</div> </div>
<field name="registration_view_template_id" groups="base.group_no_one"/> <field name="registration_view_template_id" groups="base.group_no_one"/>
<field name="payment_icon_ids" widget="many2many_tags"/> <field name="payment_icon_ids" widget="many2many_tags"/>
<field name="payment_flow" widget="radio"/> <field name="payment_flow" widget="radio" attrs="{'invisible': [('token_implemented', '=', False)]}"/>
</group> </group>
</page> </page>
</notebook> </notebook>

View File

@ -3,3 +3,4 @@
from . import models from . import models
from . import controllers from . import controllers
from flectra.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers

View File

@ -14,4 +14,5 @@
'data/payment_acquirer_data.xml', 'data/payment_acquirer_data.xml',
], ],
'installable': True, 'installable': True,
'post_init_hook': 'create_missing_journal_for_acquirers',
} }

View File

@ -14,6 +14,7 @@ from werkzeug import urls
from flectra import api, fields, models, tools, _ from flectra import api, fields, models, tools, _
from flectra.addons.payment.models.payment_acquirer import ValidationError from flectra.addons.payment.models.payment_acquirer import ValidationError
from flectra.addons.payment_adyen.controllers.main import AdyenController from flectra.addons.payment_adyen.controllers.main import AdyenController
from flectra.tools.pycompat import to_native
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -192,7 +193,7 @@ class TxAdyen(models.Model):
shasign_check = tx.acquirer_id._adyen_generate_merchant_sig_sha256('out', data) shasign_check = tx.acquirer_id._adyen_generate_merchant_sig_sha256('out', data)
else: else:
shasign_check = tx.acquirer_id._adyen_generate_merchant_sig('out', data) shasign_check = tx.acquirer_id._adyen_generate_merchant_sig('out', data)
if shasign_check != data.get('merchantSig'): if to_native(shasign_check) != to_native(data.get('merchantSig')):
error_msg = _('Adyen: invalid merchantSig, received %s, computed %s') % (data.get('merchantSig'), shasign_check) error_msg = _('Adyen: invalid merchantSig, received %s, computed %s') % (data.get('merchantSig'), shasign_check)
_logger.warning(error_msg) _logger.warning(error_msg)
raise ValidationError(error_msg) raise ValidationError(error_msg)

View File

@ -3,3 +3,4 @@
from . import models from . import models
from . import controllers from . import controllers
from flectra.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers

View File

@ -14,4 +14,5 @@
'data/payment_acquirer_data.xml', 'data/payment_acquirer_data.xml',
], ],
'installable': True, 'installable': True,
'post_init_hook': 'create_missing_journal_for_acquirers',
} }

View File

@ -11,7 +11,7 @@ import time
from flectra import _, api, fields, models from flectra import _, api, fields, models
from flectra.addons.payment.models.payment_acquirer import ValidationError from flectra.addons.payment.models.payment_acquirer import ValidationError
from flectra.addons.payment_authorize.controllers.main import AuthorizeController from flectra.addons.payment_authorize.controllers.main import AuthorizeController
from flectra.tools.float_utils import float_compare from flectra.tools.float_utils import float_compare, float_repr
from flectra.tools.safe_eval import safe_eval from flectra.tools.safe_eval import safe_eval
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -59,12 +59,21 @@ class PaymentAcquirerAuthorize(models.Model):
@api.multi @api.multi
def authorize_form_generate_values(self, values): def authorize_form_generate_values(self, values):
self.ensure_one() self.ensure_one()
# State code is only supported in US, use state name by default
# See https://developer.authorize.net/api/reference/
state = values['partner_state'].name if values.get('partner_state') else ''
if values.get('partner_country') and values.get('partner_country') == self.env.ref('base.us', False):
state = values['partner_state'].code if values.get('partner_state') else ''
billing_state = values['billing_partner_state'].name if values.get('billing_partner_state') else ''
if values.get('billing_partner_country') and values.get('billing_partner_country') == self.env.ref('base.us', False):
billing_state = values['billing_partner_state'].code if values.get('billing_partner_state') else ''
base_url = self.env['ir.config_parameter'].get_param('web.base.url') base_url = self.env['ir.config_parameter'].get_param('web.base.url')
authorize_tx_values = dict(values) authorize_tx_values = dict(values)
temp_authorize_tx_values = { temp_authorize_tx_values = {
'x_login': self.authorize_login, 'x_login': self.authorize_login,
'x_trans_key': self.authorize_transaction_key, 'x_trans_key': self.authorize_transaction_key,
'x_amount': str(values['amount']), 'x_amount': float_repr(values['amount'], values['currency'].decimal_places if values['currency'] else 2),
'x_show_form': 'PAYMENT_FORM', 'x_show_form': 'PAYMENT_FORM',
'x_type': 'AUTH_CAPTURE' if not self.capture_manually else 'AUTH_ONLY', 'x_type': 'AUTH_CAPTURE' if not self.capture_manually else 'AUTH_ONLY',
'x_method': 'CC', 'x_method': 'CC',
@ -83,7 +92,7 @@ class PaymentAcquirerAuthorize(models.Model):
'first_name': values.get('partner_first_name'), 'first_name': values.get('partner_first_name'),
'last_name': values.get('partner_last_name'), 'last_name': values.get('partner_last_name'),
'phone': values.get('partner_phone'), 'phone': values.get('partner_phone'),
'state': values.get('partner_state') and values['partner_state'].code or '', 'state': state,
'billing_address': values.get('billing_partner_address'), 'billing_address': values.get('billing_partner_address'),
'billing_city': values.get('billing_partner_city'), 'billing_city': values.get('billing_partner_city'),
'billing_country': values.get('billing_partner_country') and values.get('billing_partner_country').name or '', 'billing_country': values.get('billing_partner_country') and values.get('billing_partner_country').name or '',
@ -92,7 +101,7 @@ class PaymentAcquirerAuthorize(models.Model):
'billing_first_name': values.get('billing_partner_first_name'), 'billing_first_name': values.get('billing_partner_first_name'),
'billing_last_name': values.get('billing_partner_last_name'), 'billing_last_name': values.get('billing_partner_last_name'),
'billing_phone': values.get('billing_partner_phone'), 'billing_phone': values.get('billing_partner_phone'),
'billing_state': values.get('billing_partner_state') and values['billing_partner_state'].code or '', 'billing_state': billing_state,
} }
temp_authorize_tx_values['returndata'] = authorize_tx_values.pop('return_url', '') temp_authorize_tx_values['returndata'] = authorize_tx_values.pop('return_url', '')
temp_authorize_tx_values['x_fp_hash'] = self._authorize_generate_hashing(temp_authorize_tx_values) temp_authorize_tx_values['x_fp_hash'] = self._authorize_generate_hashing(temp_authorize_tx_values)

View File

@ -51,7 +51,7 @@ class AuthorizeForm(AuthorizeCommon):
form_values = { form_values = {
'x_login': self.authorize.authorize_login, 'x_login': self.authorize.authorize_login,
'x_trans_key': self.authorize.authorize_transaction_key, 'x_trans_key': self.authorize.authorize_transaction_key,
'x_amount': '320.0', 'x_amount': '56.16',
'x_show_form': 'PAYMENT_FORM', 'x_show_form': 'PAYMENT_FORM',
'x_type': 'AUTH_CAPTURE', 'x_type': 'AUTH_CAPTURE',
'x_method': 'CC', 'x_method': 'CC',
@ -87,7 +87,7 @@ class AuthorizeForm(AuthorizeCommon):
form_values['x_fp_hash'] = self._authorize_generate_hashing(form_values) form_values['x_fp_hash'] = self._authorize_generate_hashing(form_values)
# render the button # render the button
res = self.authorize.render('SO004', 320.0, self.currency_usd.id, values=self.buyer_values) res = self.authorize.render('SO004', 56.16, self.currency_usd.id, values=self.buyer_values)
# check form result # check form result
tree = objectify.fromstring(res) tree = objectify.fromstring(res)

View File

@ -3,3 +3,4 @@
from . import models from . import models
from . import controllers from . import controllers
from flectra.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers

View File

@ -14,4 +14,5 @@
'data/payment_acquirer_data.xml', 'data/payment_acquirer_data.xml',
], ],
'installable': True, 'installable': True,
'post_init_hook': 'create_missing_journal_for_acquirers',
} }

View File

@ -3,3 +3,4 @@
from . import models from . import models
from . import controllers from . import controllers
from flectra.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers

View File

@ -14,4 +14,5 @@
'data/payment_acquirer_data.xml', 'data/payment_acquirer_data.xml',
], ],
'installable': True, 'installable': True,
'post_init_hook': 'create_missing_journal_for_acquirers',
} }

View File

@ -110,7 +110,7 @@ class OgoneController(http.Controller):
def feedback(self, **kwargs): def feedback(self, **kwargs):
try: try:
tx = request.env['payment.transaction'].sudo()._ogone_form_get_tx_from_data(kwargs) tx = request.env['payment.transaction'].sudo()._ogone_form_get_tx_from_data(kwargs)
tx._ogone_s2s_validate() tx._ogone_s2s_validate_tree(kwargs)
except ValidationError: except ValidationError:
return 'ko' return 'ko'
return 'ok' return 'ok'

View File

@ -3,3 +3,4 @@
from . import models from . import models
from . import controllers from . import controllers
from flectra.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers

View File

@ -14,4 +14,5 @@
'data/payment_acquirer_data.xml', 'data/payment_acquirer_data.xml',
], ],
'installable': True, 'installable': True,
'post_init_hook': 'create_missing_journal_for_acquirers',
} }

View File

@ -3,3 +3,4 @@
from . import models from . import models
from . import controllers from . import controllers
from flectra.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers

View File

@ -17,4 +17,5 @@
'views/payment_payumoney_templates.xml', 'views/payment_payumoney_templates.xml',
'data/payment_acquirer_data.xml', 'data/payment_acquirer_data.xml',
], ],
'post_init_hook': 'create_missing_journal_for_acquirers',
} }

View File

@ -1,2 +1,3 @@
from . import models from . import models
from . import controllers from . import controllers
from flectra.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers

View File

@ -20,4 +20,5 @@ payments acquirer using Worldline SIPS.""",
'data/payment_acquirer_data.xml', 'data/payment_acquirer_data.xml',
], ],
'installable': True, 'installable': True,
'post_init_hook': 'create_missing_journal_for_acquirers',
} }

View File

@ -111,6 +111,16 @@ class AcquirerSips(models.Model):
return self.environment == 'prod' and self.sips_prod_url or self.sips_test_url return self.environment == 'prod' and self.sips_prod_url or self.sips_test_url
class PaymentTransactionSips(models.Model):
_inherit = 'payment.transaction'
@api.model
def _get_next_reference(self, reference, acquirer=None):
if acquirer and acquirer.provider == 'sips':
reference = re.sub(r'[^0-9a-zA-Z]+', 'x' , reference)
return super(PaymentTransactionSips, self)._get_next_reference(reference, acquirer=acquirer)
class TxSips(models.Model): class TxSips(models.Model):
_inherit = 'payment.transaction' _inherit = 'payment.transaction'

View File

@ -3,3 +3,4 @@
from . import models from . import models
from . import controllers from . import controllers
from flectra.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers

View File

@ -15,4 +15,5 @@
], ],
'images': ['static/description/icon.png'], 'images': ['static/description/icon.png'],
'installable': True, 'installable': True,
'post_init_hook': 'create_missing_journal_for_acquirers',
} }

View File

@ -8,6 +8,7 @@ from flectra import api, fields, models, _
from flectra.addons.payment.models.payment_acquirer import ValidationError from flectra.addons.payment.models.payment_acquirer import ValidationError
from flectra.exceptions import UserError from flectra.exceptions import UserError
from flectra.tools.safe_eval import safe_eval from flectra.tools.safe_eval import safe_eval
from flectra.tools.float_utils import float_round
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -41,12 +42,12 @@ class PaymentAcquirerStripe(models.Model):
stripe_tx_values = dict(tx_values) stripe_tx_values = dict(tx_values)
temp_stripe_tx_values = { temp_stripe_tx_values = {
'company': self.company_id.name, 'company': self.company_id.name,
'amount': tx_values.get('amount'), 'amount': tx_values['amount'], # Mandatory
'currency': tx_values.get('currency') and tx_values.get('currency').name or '', 'currency': tx_values['currency'].name, # Mandatory anyway
'currency_id': tx_values.get('currency') and tx_values.get('currency').id or '', 'currency_id': tx_values['currency'].id, # same here
'address_line1': tx_values.get('partner_address'), 'address_line1': tx_values.get('partner_address'), # Any info of the partner is not mandatory
'address_city': tx_values.get('partner_city'), 'address_city': tx_values.get('partner_city'),
'address_country': tx_values.get('partner_country') and tx_values['partner_country'].name or '', 'address_country': tx_values.get('partner_country') and tx_values.get('partner_country').name or '',
'email': tx_values.get('partner_email'), 'email': tx_values.get('partner_email'),
'address_zip': tx_values.get('partner_zip'), 'address_zip': tx_values.get('partner_zip'),
'name': tx_values.get('partner_name'), 'name': tx_values.get('partner_name'),
@ -106,7 +107,7 @@ class PaymentTransactionStripe(models.Model):
def _create_stripe_charge(self, acquirer_ref=None, tokenid=None, email=None): def _create_stripe_charge(self, acquirer_ref=None, tokenid=None, email=None):
api_url_charge = 'https://%s/charges' % (self.acquirer_id._get_stripe_api_url()) api_url_charge = 'https://%s/charges' % (self.acquirer_id._get_stripe_api_url())
charge_params = { charge_params = {
'amount': int(self.amount if self.currency_id.name in INT_CURRENCIES else self.amount*100), 'amount': int(self.amount if self.currency_id.name in INT_CURRENCIES else float_round(self.amount * 100, 2)),
'currency': self.currency_id.name, 'currency': self.currency_id.name,
'metadata[reference]': self.reference, 'metadata[reference]': self.reference,
'description': self.reference, 'description': self.reference,
@ -126,7 +127,7 @@ class PaymentTransactionStripe(models.Model):
@api.multi @api.multi
def stripe_s2s_do_transaction(self, **kwargs): def stripe_s2s_do_transaction(self, **kwargs):
self.ensure_one() self.ensure_one()
result = self._create_stripe_charge(acquirer_ref=self.payment_token_id.acquirer_ref) result = self._create_stripe_charge(acquirer_ref=self.payment_token_id.acquirer_ref, email=self.partner_email)
return self._stripe_s2s_validate_tree(result) return self._stripe_s2s_validate_tree(result)
@ -135,7 +136,7 @@ class PaymentTransactionStripe(models.Model):
refund_params = { refund_params = {
'charge': self.acquirer_reference, 'charge': self.acquirer_reference,
'amount': int(self.amount*100), # by default, stripe refund the full amount (we don't really need to specify the value) 'amount': int(float_round(self.amount * 100, 2)), # by default, stripe refund the full amount (we don't really need to specify the value)
'metadata[reference]': self.reference, 'metadata[reference]': self.reference,
} }

View File

@ -62,31 +62,14 @@ class StripeTest(StripeCommon):
# ---------------------------------------- # ----------------------------------------
# Test: button direct rendering # Test: button direct rendering
# ---------------------------------------- # ----------------------------------------
form_values = {
'amount': 320.0,
'currency': 'EUR',
'address_line1': 'Huge Street 2/543',
'address_city': 'Sin City',
'address_country': 'Belgium',
'email': 'norbert.buyer@example.com',
'address_zip': '1000',
'name': 'Norbert Buyer',
'phone': '0032 12 34 56 78'
}
# render the button # render the button
res = self.stripe.render('SO404', 320.0, self.currency_euro.id, values=self.buyer_values) res = self.stripe.render('SO404', 320.0, self.currency_euro.id, values=self.buyer_values).decode('utf-8')
post_url = "https://checkout.stripe.com/checkout.js" popup_script_src = 'script src="https://checkout.stripe.com/checkout.js"'
email = "norbert.buyer@example.com"
# check form result # check form result
if "https://checkout.stripe.com/checkout.js" in res[0]: self.assertIn(popup_script_src, res, "Stripe: popup script not found in template render")
self.assertEqual(post_url, 'https://checkout.stripe.com/checkout.js', 'Stripe: wrong form POST url')
# Generated and received # Generated and received
if email in res[0]: self.assertIn(self.buyer_values.get('partner_email'), res, 'Stripe: email input not found in rendered template')
self.assertEqual(
email, form_values.get('email'),
'Stripe: wrong value for input %s: received %s instead of %s' % (email, email, form_values.get('email'))
)
@unittest.skip("Stripe test disabled: We do not want to overload Stripe with runbot's requests") @unittest.skip("Stripe test disabled: We do not want to overload Stripe with runbot's requests")
def test_30_stripe_form_management(self): def test_30_stripe_form_management(self):

View File

@ -3,3 +3,4 @@
from . import models from . import models
from . import controllers from . import controllers
from flectra.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers

View File

@ -15,4 +15,5 @@
], ],
'installable': True, 'installable': True,
'auto_install': True, 'auto_install': True,
'post_init_hook': 'create_missing_journal_for_acquirers',
} }

View File

@ -15,6 +15,19 @@ class TransferPaymentAcquirer(models.Model):
provider = fields.Selection(selection_add=[('transfer', 'Wire Transfer')], default='transfer') provider = fields.Selection(selection_add=[('transfer', 'Wire Transfer')], default='transfer')
@api.model
def _create_missing_journal_for_acquirers(self, company=None):
# By default, the wire transfer method uses the default Bank journal.
company = company or self.env.user.company_id
acquirers = self.env['payment.acquirer'].search(
[('provider', '=', 'transfer'), ('journal_id', '=', False), ('company_id', '=', company.id)])
bank_journal = self.env['account.journal'].search(
[('type', '=', 'bank'), ('company_id', '=', company.id)], limit=1)
if bank_journal:
acquirers.write({'journal_id': bank_journal.id})
return super(TransferPaymentAcquirer, self)._create_missing_journal_for_acquirers(company=company)
def transfer_get_form_action_url(self): def transfer_get_form_action_url(self):
return '/payment/transfer/feedback' return '/payment/transfer/feedback'