[MERGE] forward port branch 11.0 up to eefe879a37

This commit is contained in:
Christophe Simonis
2018-01-25 15:41:45 +01:00
67 changed files with 712 additions and 301 deletions
+3 -1
View File
@@ -1340,6 +1340,7 @@ class AccountInvoice(models.Model):
values['state'] = 'draft'
values['number'] = False
values['origin'] = invoice.number
values['payment_term_id'] = False
values['refund_invoice_id'] = invoice.id
if date:
@@ -1725,6 +1726,7 @@ class AccountPaymentTerm(models.Model):
def compute(self, value, date_ref=False):
date_ref = date_ref or fields.Date.today()
amount = value
sign = value < 0 and -1 or 1
result = []
if self.env.context.get('currency_id'):
currency = self.env['res.currency'].browse(self.env.context['currency_id'])
@@ -1732,7 +1734,7 @@ class AccountPaymentTerm(models.Model):
currency = self.env.user.company_id.currency_id
for line in self.line_ids:
if line.value == 'fixed':
amt = currency.round(line.value_amount)
amt = sign * currency.round(line.value_amount)
elif line.value == 'percent':
amt = currency.round(value * (line.value_amount / 100.0))
elif line.value == 'balance':
+1 -1
View File
@@ -950,7 +950,7 @@ class AccountMoveLine(models.Model):
elif self._context.get('skip_full_reconcile_check') == 'amount_currency_only':
field = 'amount_residual_currency'
#target the pair of move in self that are the oldest
sorted_moves = sorted(self, key=lambda a: a.date)
sorted_moves = sorted(self, key=lambda a: a.date_maturity or a.date)
debit = credit = False
for aml in sorted_moves:
if credit and debit:
@@ -253,8 +253,8 @@
</template>
<template id="portal_invoice_error" name="Invoice error/warning display">
<div class="row">
<div t-att-class="'col-md-6 col-md-offset-3 text-center alert alert-dismissable %s' % ('alert-danger' if error else 'alert-warning')">
<div class="row mr16">
<div t-attf-class="'col-md-12 mr16 ml16 alert alert-dismissable' #{'alert-danger' if error else 'alert-warning'}">
<a href="#" class="close" data-dismiss="alert" aria-label="close" title="close">×</a>
<t t-if="error == 'generic'" name="generic">
There was an error processing this page.
@@ -264,8 +264,8 @@
</template>
<template id="portal_invoice_success" name="Invoice success display">
<div class="row">
<div class="col-md-6 col-md-offset-3 text-center alert alert-dismissable alert-success">
<div class="row mr16">
<div class="col-md-12 mr16 ml16 alert alert-dismissable alert-success">
<a href="#" class="close" data-dismiss="alert" aria-label="close" title="close">×</a>
</div>
</div>
@@ -94,9 +94,9 @@ class PaymentPortal(http.Controller):
# proceed to the payment
res = tx.confirm_invoice_token()
if res is not True:
params['error'] = res
return request.redirect(_build_url_w_params(error_url, params))
params['success'] = 'pay_invoice'
if tx.state != 'authorized' or not tx.acquirer_id.capture_manually:
if res is not True:
params['error'] = res
return request.redirect(_build_url_w_params(error_url, params))
params['success'] = 'pay_invoice'
return request.redirect(_build_url_w_params(success_url, params))
@@ -12,6 +12,36 @@
</xpath>
</template>
<!-- duplicata of the template payment.payment_confirmation_status.
The duplication avoid to break an existing installation in stable version-->
<template id="payment_confirmation_status">
<div t-if="payment_tx_id and payment_tx_id.state == 'done'" class="alert alert-success alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.done_msg' t-raw="payment_tx_id.acquirer_id.done_msg"/>
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'pending'" class="alert alert-warning alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.pending_msg' t-raw="payment_tx_id.acquirer_id.pending_msg"/>
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'cancel'" class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.cancel_msg' t-raw="payment_tx_id.acquirer_id.cancel_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'error'" class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.error_msg' t-raw="payment_tx_id.acquirer_id.error_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'authorized'" class="alert alert-success alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
Your payment has been authorized.
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
</template>
<template id="portal_invoice_page_inherit_payment" name="Payment on My Invoices" inherit_id="account.portal_invoice_page">
<xpath expr="//div[@id='o_portal_account_actions']/a" position="before">
<t t-set="tx_ids" t-value="invoice.payment_tx_ids.filtered(lambda tx: tx.state in ('pending', 'authorized', 'done'))"/>
@@ -28,6 +58,13 @@
<i class="fa fa-check-circle"/> Paid
</a>
</xpath>
<xpath expr="//t[@t-call='account.portal_invoice_success']" position="after">
<div t-if="invoice.payment_tx_ids and invoice.amount_total and not success and not error" class="o_account_payment_tx_status" t-att-data-invoice-id="invoice.id">
<t t-call="account_payment.payment_confirmation_status">
<t t-set="payment_tx_id" t-value="invoice.payment_tx_id"/>
</t>
</div>
</xpath>
<xpath expr="//div[hasclass('panel-body')]" position="after">
<div class="panel-body" t-if="not tx_ids and invoice.state == 'open' and invoice.amount_total" id="portal_pay">
<div t-if="pms or s2s_acquirers or form_acquirers" id="payment_method" class="col-md-offset-3 col-md-6">
@@ -35,8 +72,8 @@
<t t-call="payment.payment_tokens_list">
<t t-set="mode" t-value="'payment'"/>
<t t-set="partner_id" t-value="invoice.partner_id.id"/>
<t t-set="success_url" t-value="'/my/invoices/%s?%s' % (invoice.id, keep_query())"/>
<t t-set="error_url" t-value="'/my/invoices/%s?%s' % (invoice.id, keep_query())"/>
<t t-set="success_url" t-value="'/my/invoices/%s%s' % (invoice.id, ('?access_token=%s' % (access_token)) if access_token else '')"/>
<t t-set="error_url" t-value="'/my/invoices/%s%s' % (invoice.id, ('?access_token=%s' % (access_token)) if access_token else '')"/>
<t t-set="access_token" t-value="access_token or ''"/>
<t t-set="callback_method" t-value="''"/>
<t t-set="form_action" t-value="'/invoice/pay/' + str(invoice.id) + '/s2s_token_tx/'"/>
@@ -119,7 +156,8 @@
inherit_id="account.portal_invoice_success">
<xpath expr="//a[hasclass('close')]" position="after">
<t t-if="success == 'pay_invoice'">
Invoice successfully paid.
<span t-if='invoice.payment_acquirer_id.done_msg' t-raw="invoice.payment_acquirer_id.done_msg"/>
<span t-if='invoice.payment_acquirer_id.post_msg' t-raw="invoice.payment_acquirer_id.post_msg"/>
</t>
</xpath>
</template>
+7 -5
View File
@@ -111,13 +111,15 @@ class Employee(models.Model):
active = fields.Boolean('Active', related='resource_id.active', default=True, store=True)
# private partner
address_home_id = fields.Many2one(
'res.partner', 'Private Address', help='Enter here the private address of the employee, not the one linked to your company.')
'res.partner', 'Private Address', help='Enter here the private address of the employee, not the one linked to your company.',
groups="hr.group_hr_user")
is_address_home_a_company = fields.Boolean(
'The employee adress has a company linked',
compute='_compute_is_address_home_a_company',
groups="hr.group_hr_user",
)
country_id = fields.Many2one(
'res.country', 'Nationality (Country)')
'res.country', 'Nationality (Country)', groups="hr.group_hr_user")
gender = fields.Selection([
('male', 'Male'),
('female', 'Female'),
@@ -139,9 +141,9 @@ class Employee(models.Model):
domain="[('partner_id', '=', address_home_id)]",
groups="hr.group_hr_user",
help='Employee bank salary account')
permit_no = fields.Char('Work Permit No')
visa_no = fields.Char('Visa No')
visa_expire = fields.Date('Visa Expire Date')
permit_no = fields.Char('Work Permit No', groups="hr.group_hr_user")
visa_no = fields.Char('Visa No', groups="hr.group_hr_user")
visa_expire = fields.Date('Visa Expire Date', groups="hr.group_hr_user")
# image: all image fields are base64 encoded and PIL-supported
image = fields.Binary(
@@ -1,7 +1,5 @@
from openerp.modules.registry import RegistryManager
# -*- coding: utf-8 -*-
from odoo.addons.account.models.chart_template import migrate_tags_on_taxes
def migrate(cr, version):
registry = RegistryManager.get(cr.dbname)
from openerp.addons.account.models.chart_template import migrate_tags_on_taxes
migrate_tags_on_taxes(cr, registry)
migrate_tags_on_taxes(cr, None)
+1 -1
View File
@@ -397,7 +397,7 @@ class MassMailing(models.Model):
mailing_model_real = fields.Char(compute='_compute_model', string='Recipients Real Model', default='mail.mass_mailing.contact', required=True)
mailing_model_id = fields.Many2one('ir.model', string='Recipients Model', domain=[('model', 'in', MASS_MAILING_BUSINESS_MODELS)],
default=lambda self: self.env.ref('mass_mailing.model_mail_mass_mailing_list').id)
mailing_model_name = fields.Char(related='mailing_model_id.model', string='Recipients Model Name')
mailing_model_name = fields.Char(related='mailing_model_id.model', string='Recipients Model Name', readonly=True, related_sudo=True)
mailing_domain = fields.Char(string='Domain', oldname='domain', default=[])
contact_list_ids = fields.Many2many('mail.mass_mailing.list', 'mail_mass_mailing_list_rel',
string='Mailing Lists')
+3 -3
View File
@@ -152,9 +152,9 @@ class PaymentAcquirer(models.Model):
"Use this field anywhere a small image is required.")
payment_icon_ids = fields.Many2many('payment.icon', string='Supported Payment Icons')
payment_flow = fields.Selection(selection=[('s2s','The customer encode his payment details on the website.'),
('form', 'The customer is redirected to the website of the acquirer.')],
default='form', required=True, string='Payment flow',
payment_flow = fields.Selection(selection=[('form', 'Redirection to the acquirer website'),
('s2s','Payment from Odoo')],
default='form', required=True, string='Payment Flow',
help="""Note: Subscriptions does not take this field in account, it uses server to server by default.""")
def _search_is_tokenized(self, operator, value):
@@ -14,6 +14,15 @@ div.card_placeholder {
transition: 0.4s cubic-bezier(0.455, 0.03, 0.515, 0.955);
pointer-events: none; }
/* if s2s form not in bootstrap_formatting */
div.o_card_brand_detail {
position: relative;
}
div.o_card_brand_detail div.card_placeholder {
right: 5px;
}
div.amex {
background-image: url("/website_payment/static/src/img/amex.png");
background-repeat: no-repeat; }
+96 -60
View File
@@ -58,29 +58,34 @@ odoo.define('payment.payment_form', function (require) {
console.warn('payment_form: unset partner_id when adding new token; things could go wrong');
}
var form_data = this.getFormData(inputs_form);
var empty_inputs = false;
var wrong_input = false;
inputs_form.toArray().forEach(function (element) {
if (element.dataset.isRequired) {
if (element.value.length === 0) {
//skip the check of non visible inputs
if ($(element).attr('type') == 'hidden') {
return true;
}
$(element).closest('div.form-group').removeClass('has-error');
$(element).siblings( ".o_invalid_field" ).remove();
//force check of forms validity (useful for Firefox that refill forms automatically on f5)
$(element).trigger("focusout");
if (element.dataset.isRequired && element.value.length === 0) {
$(element).closest('div.form-group').addClass('has-error');
empty_inputs = true;
}
else {
$(element).closest('div.form-group').removeClass('has-error');
}
$(element).closest('div.form-group').append('<div style="color: red" class="o_invalid_field">' + _.str.escapeHTML("The value is invalid.") + '</div>');
wrong_input = true;
}
else if ($(element).closest('div.form-group').hasClass('has-error')) {
wrong_input = true;
$(element).closest('div.form-group').append('<div style="color: red" class="o_invalid_field">' + _.str.escapeHTML("The value is invalid.") + '</div>');
}
});
if (empty_inputs) {
this.displayError(
_t('Missing values'),
_t('<p>Please fill all the inputs required.</p>')
);
if (wrong_input) {
return;
}
$(button).attr('disabled', true);
$(button).children('.fa-plus-circle').removeClass('fa-plus-circle')
$(button).prepend('<span class="o_loader"><i class="fa fa-refresh fa-spin"></i>&nbsp;</span>');
var verify_validity = this.$el.find('input[name="verify_validity"]');
@@ -105,24 +110,30 @@ odoo.define('payment.payment_form', function (require) {
}
// if the server has returned false, we display an error
else {
self.displayError(
_t('Server Error'),
_t("<p>We are not able to add your payment method at the moment.</p>"));
if (data.error) {
self.displayError(
'',
data.error);
} else { // if the server doesn't provide an error message
self.displayError(
_t('Server Error'),
_t('e.g. Your credit card details are wrong. Please verify.'));
}
}
// here we remove the 'processing' icon from the 'add a new payment' button
$(button).attr('disabled', false);
$(button).children('.fa').addClass('fa-plus-circle')
$(button).find('span.o_loader').remove();
}).fail(function (message, data) {
// if the rpc fails, pretty obvious
$(button).attr('disabled', false);
$(button).children('.fa').addClass('fa-plus-circle')
$(button).find('span.o_loader').remove();
self.displayError(
_t('Server Error'),
_t("<p>We are not able to add your payment method at the moment.</p>") +
"<p>" +
(config.debug ? (data.data.message.replace(/\n/g, "<br />")): '') +
"</p>"
_t("We are not able to add your payment method at the moment.") +
data.data.message
);
});
}
@@ -132,7 +143,7 @@ odoo.define('payment.payment_form', function (require) {
// if there's a prepare tx url set
if ($tx_url.length === 1) {
// if the user wants to save his credit card info
var form_save_token = $('input[name="o_payment_form_save_token"]').prop('checked');
var form_save_token = acquirer_form.find('input[name="o_payment_form_save_token"]').prop('checked');
// then we call the route to prepare the transaction
ajax.jsonRpc($tx_url[0].value, 'call', {
'acquirer_id': parseInt(acquirer_id),
@@ -160,16 +171,14 @@ odoo.define('payment.payment_form', function (require) {
else {
self.displayError(
_t('Server Error'),
_t("<p>We are not able to redirect you to the payment form.</p>")
_t("We are not able to redirect you to the payment form.")
);
}
}).fail(function (message, data) {
self.displayError(
_t('Server Error'),
_t("<p>We are not able to redirect you to the payment form.</p>") +
"<p>" +
(config.debug ? (data.data.message.replace(/\n/g, "<br />")): '') +
"</p>"
_t("We are not able to redirect you to the payment form.<") +
data.data.message
);
});
}
@@ -177,7 +186,7 @@ odoo.define('payment.payment_form', function (require) {
// we append the form to the body and send it.
this.displayError(
_t("Cannot set-up the payment"),
_t("<p>We're unable to process your payment.</p>")
_t("We're unable to process your payment.")
);
}
}
@@ -188,7 +197,7 @@ odoo.define('payment.payment_form', function (require) {
else {
this.displayError(
_t('No payment method selected'),
_t('<p>Please select a payment method.</p>')
_t('Please select a payment method.')
);
}
},
@@ -210,29 +219,36 @@ odoo.define('payment.payment_form', function (require) {
var inputs_form = $('input', acquirer_form);
var form_data = this.getFormData(inputs_form);
var ds = $('input[name="data_set"]', acquirer_form)[0];
var empty_inputs = false;
var wrong_input = false;
inputs_form.toArray().forEach(function (element) {
if (element.dataset.isRequired) {
if (element.value.length === 0) {
//skip the check of non visible inputs
if ($(element).attr('type') == 'hidden') {
return true;
}
$(element).closest('div.form-group').removeClass('has-error');
$(element).siblings( ".o_invalid_field" ).remove();
//force check of forms validity (useful for Firefox that refill forms automatically on f5)
$(element).trigger("focusout");
if (element.dataset.isRequired && element.value.length === 0) {
$(element).closest('div.form-group').addClass('has-error');
empty_inputs = true;
}
else {
$(element).closest('div.form-group').removeClass('has-error');
}
var message = '<div style="color: red" class="o_invalid_field">' + _.str.escapeHTML("The value is invalid.") + '</div>';
$(element).closest('div.form-group').append(message);
wrong_input = true;
}
else if ($(element).closest('div.form-group').hasClass('has-error')) {
wrong_input = true;
var message = '<div style="color: red" class="o_invalid_field">' + _.str.escapeHTML("The value is invalid.") + '</div>';
$(element).closest('div.form-group').append(message);
}
});
if (empty_inputs) {
this.displayError(
_t('Missing values'),
_t('<p>Please fill all the inputs required.</p>')
);
if (wrong_input) {
return;
}
// We add a 'processing' icon into the 'add a new payment' button
$(button).attr('disabled', true);
$(button).children('.fa-plus-circle').removeClass('fa-plus-circle')
$(button).prepend('<span class="o_loader"><i class="fa fa-refresh fa-spin"></i>&nbsp;</span>');
// we force the check when adding a card trough here
@@ -260,32 +276,37 @@ odoo.define('payment.payment_form', function (require) {
}
// if the server has returned false, we display an error
else {
self.displayError(
_t('Server Error'),
_t("<p>We are not able to add your payment method at the moment.</p>")
);
if (data.error) {
self.displayError(
'',
data.error);
} else { // if the server doesn't provide an error message
self.displayError(
_t('Server Error'),
_t('e.g. Your credit card details are wrong. Please verify.'));
}
}
// here we remove the 'processing' icon from the 'add a new payment' button
$(button).attr('disabled', false);
$(button).children('.fa').addClass('fa-plus-circle')
$(button).find('span.o_loader').remove();
}).fail(function (message, data) {
// if the rpc fails, pretty obvious
$(button).attr('disabled', false);
$(button).children('.fa').addClass('fa-plus-circle')
$(button).find('span.o_loader').remove();
self.displayError(
_t('Server error'),
_t("<p>We are not able to add your payment method at the moment.</p>") +
"<p>" +
(config.debug ? (data.data.message.replace(/\n/g, "<br />")): '') +
"</p>"
_t("We are not able to add your payment method at the moment.</p>") +
data.data.message
);
});
}
else {
this.displayError(
_t('No payment method selected'),
_t('<p>Please select the option to add a new payment method.</p>')
_t('Please select the option to add a new payment method.')
);
}
},
@@ -309,7 +330,7 @@ odoo.define('payment.payment_form', function (require) {
}, function () {
self.displayError(
_t('Server Error'),
_t("<p>We are not able to delete your payment method at the moment.</p>")
_t("We are not able to delete your payment method at the moment.")
);
});
};
@@ -323,7 +344,7 @@ odoo.define('payment.payment_form', function (require) {
// if there's records linked to this payment method
var content = '';
result[pm_id].forEach(function (sub) {
content += '<p><a href="' + sub.url + '" title="' + _.str.escapeHTML(sub.description) + '">' + _.str.escapeHTML(sub.name) + '</a><p/>';
content += '<p><a href="' + sub.url + '" title="' + sub.description + '">' + sub.name + '</a><p/>';
});
content = $('<div>').html(_t('<p>This card is currently linked to the following records:<p/>') + content);
@@ -344,7 +365,7 @@ odoo.define('payment.payment_form', function (require) {
}, function (type, err) {
self.displayError(
_t('Server Error'),
_t("<p>We are not able to delete your payment method at the moment.</p>") + (config.debug ? err.data.message : '')
_t("We are not able to delete your payment method at the moment.") + err.data.message
);
});
},
@@ -392,12 +413,27 @@ odoo.define('payment.payment_form', function (require) {
return $(element).data('acquirer-id');
},
displayError: function (title, message) {
return new Dialog(null, {
title: _t('Error: ') + title,
size: 'medium',
$content: message || "",
buttons: [
{text: _t('Ok'), close: true}]}).open();
var $checkedRadio = this.$('input[type="radio"]:checked'),
acquirerID = this.getAcquirerIdFromRadio($checkedRadio[0]),
$acquirerForm = this.$('#o_payment_add_token_acq_' + acquirerID);
if ($checkedRadio.length === 0) {
return new Dialog(null, {
title: _t('Error: ') + _.str.escapeHTML(title),
size: 'medium',
$content: "<p>" + (_.str.escapeHTML(message) || "") + "</p>" ,
buttons: [
{text: _t('Ok'), close: true}]}).open();
} else {
// removed if exist error message
this.$('#payment_error').remove();
var messageResult = '<div class="alert alert-danger mb4" id="payment_error">';
if (title != '') {
messageResult = messageResult + '<b>' + _.str.escapeHTML(title) + ':</b></br>';
}
messageResult = messageResult + _.str.escapeHTML(message) + '</div>';
$acquirerForm.append(messageResult);
}
},
getFormData: function ($form) {
var unindexed_array = $form.serializeArray();
@@ -17,6 +17,7 @@ $(document).ready(function () {
if (valid_value) {
$(this).parent('.form-group').addClass('has-success');
$(this).parent('.form-group').removeClass('has-error');
$(this).siblings('.o_invalid_field').remove();
}
else {
$(this).parent('.form-group').addClass('has-error');
@@ -31,6 +32,7 @@ $(document).ready(function () {
if (valid_value) {
$(this).parent('.form-group').addClass('has-success');
$(this).parent('.form-group').removeClass('has-error');
$(this).siblings('.o_invalid_field').remove();
}
else {
$(this).parent('.form-group').addClass('has-error');
@@ -46,6 +48,7 @@ $(document).ready(function () {
if (valid_value) {
$(this).parent('.form-group').addClass('has-success');
$(this).parent('.form-group').removeClass('has-error');
$(this).siblings('.o_invalid_field').remove();
}
else {
$(this).parent('.form-group').addClass('has-error');
@@ -1,6 +1,6 @@
.o_payment_form {
label > input[type="radio"], input[type="checkbox"]{
vertical-align: bottom;
vertical-align: top;
margin-right: 5px;
}
@@ -14,6 +14,7 @@
label {
font-weight: normal;
margin-top: 5px;
}
.panel-body:first-child {
@@ -35,16 +36,18 @@
}
.payment_icon_list {
position: relative;
li {
padding-left: 5px !important;
padding-right: 0px !important;
}
.more_option {
.o-position-absolute(@right: 0px, @bottom: -10px);
font-size:10px;
}
margin-top: 0px !important;
margin-bottom: -5px !important;
}
}
}
@@ -34,7 +34,7 @@
<!-- TDE FIXME: was customize_show=True -->
<template id="pay_meth_link" inherit_id="portal.portal_layout">
<xpath expr="//div[hasclass('o_portal_my_details')]" position="inside">
<t t-if="request.env['payment.acquirer'].search([('website_published', '=', True), ('registration_view_template_id', '!=', False)])">
<t t-if="request.env['payment.acquirer'].search([('website_published', '=', True), ('registration_view_template_id', '!=', False), ('payment_flow', '=', 's2s')])">
<div class='manage_payment_method mt16'><a href="/my/payment_method">Manage your payment methods</a></div>
</t>
</xpath>
@@ -122,5 +122,31 @@
</t>
</template>
<template id="payment_confirmation_status">
<div t-if="payment_tx_id and payment_tx_id.state == 'done'" class="alert alert-success alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.done_msg' t-raw="payment_tx_id.acquirer_id.done_msg"/>
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'pending'" class="alert alert-warning alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.pending_msg' t-raw="payment_tx_id.acquirer_id.pending_msg"/>
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'cancel'" class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.cancel_msg' t-raw="payment_tx_id.acquirer_id.cancel_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'error'" class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.error_msg' t-raw="payment_tx_id.acquirer_id.error_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'authorized'" class="alert alert-success alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
Your payment has been authorized.
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
</template>
</data>
</odoo>
+7 -6
View File
@@ -41,6 +41,7 @@
<div class="panel panel-default">
<t t-set="acquirers_count" t-value="(len(s2s_acquirers) if s2s_acquirers else 0) + (len(form_acquirers) if form_acquirers else 0)"/>
<t t-set="pms_count" t-value="len(pms) if pms else 0"/>
<t t-set="MAX_BRAND_LINE" t-value="3"/>
<t t-foreach="s2s_acquirers" t-as="acq">
<div class="panel-body">
<label>
@@ -65,13 +66,13 @@
<ul class="pull-right list-inline payment_icon_list">
<t t-set="i" t-value="0"/>
<t t-foreach="acq.payment_icon_ids" t-as="pm_icon">
<li t-att-class="'' if (i &lt; 3) else 'hidden'">
<li t-att-class="'' if (i &lt; MAX_BRAND_LINE) else 'hidden'">
<span t-field="pm_icon.image_payment_form"
t-options='{"widget": "image", "alt-field": "name"}' />
</li>
<li t-if="i==3" style="display:block;">
<li t-if="i==MAX_BRAND_LINE" style="display:block;">
<span class="pull-right more_option text-info">
<a href="#" class="o_payment_form_pay_icon_more" data-toggle="tooltip" t-att-title="', '.join([opt.name for opt in acq.payment_icon_ids[3:]])">and more</a>
<a href="#" class="o_payment_form_pay_icon_more" data-toggle="tooltip" t-att-title="', '.join([opt.name for opt in acq.payment_icon_ids[MAX_BRAND_LINE:]])">and more</a>
</span>
</li>
<t t-set="i" t-value="i+1"/>
@@ -102,13 +103,13 @@
<ul class="pull-right list-inline payment_icon_list">
<t t-set="i" t-value="0"/>
<t t-foreach="acq.payment_icon_ids" t-as="pm_icon">
<li t-att-class=" '' if (i &lt; 3) else 'hidden'">
<li t-att-class=" '' if (i &lt; MAX_BRAND_LINE) else 'hidden'">
<span t-field="pm_icon.image_payment_form"
t-options='{"widget": "image", "alt-field": "name"}' />
</li>
<li t-if="i==3" style="display:block;">
<li t-if="i==MAX_BRAND_LINE" style="display:block;">
<span class="pull-right more_option text-info">
<a href="#" class="o_payment_form_pay_icon_more" data-toggle="tooltip" t-att-title="', '.join([opt.name for opt in acq.payment_icon_ids[3:]])">and more</a>
<a href="#" class="o_payment_form_pay_icon_more" data-toggle="tooltip" t-att-title="', '.join([opt.name for opt in acq.payment_icon_ids[MAX_BRAND_LINE:]])">and more</a>
</span>
</li>
<t t-set="i" t-value="i+1"/>
+1 -1
View File
@@ -83,7 +83,7 @@
<group name="acquirer_config">
<field name="journal_id" context="{'default_type': 'bank'}"/>
<field name="capture_manually" attrs="{'invisible': [('authorize_implemented', '=', False)]}"/>
<field name="save_token" widget="radio" attrs="{'invisible': [('token_implemented', '=', False)]}"/>
<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_dom_fixed" attrs="{'invisible': [('fees_active', '=', False)]}"/>
<field name="fees_dom_var" attrs="{'invisible': [('fees_active', '=', False)]}"/>
+27 -2
View File
@@ -3,8 +3,9 @@ import pprint
import logging
from werkzeug import urls, utils
from odoo import http
from odoo import http, _
from odoo.http import request
from odoo.exceptions import ValidationError
_logger = logging.getLogger(__name__)
@@ -35,11 +36,35 @@ class AuthorizeController(http.Controller):
def authorize_s2s_create_json(self, **kwargs):
acquirer_id = int(kwargs.get('acquirer_id'))
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
if not kwargs.get('partner_id'):
kwargs = dict(kwargs, partner_id=request.env.user.partner_id.id)
return acquirer.s2s_process(kwargs).id
@http.route(['/payment/authorize/s2s/create_json_3ds'], type='json', auth='public', csrf=False)
def authorize_s2s_create_json_3ds(self, verify_validity=False, **kwargs):
token = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs)
token = False
acquirer = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id')))
try:
if not kwargs.get('partner_id'):
kwargs = dict(kwargs, partner_id=request.env.user.partner_id.id)
token = acquirer.s2s_process(kwargs)
except ValidationError as e:
message = e.args[0]
if isinstance(message, dict) and 'missing_fields' in message:
msg = _("The transaction cannot be processed because some contact details are missing or invalid: ")
message = msg + ', '.join(message['missing_fields']) + '. '
if request.env.user._is_public():
message += _("Please sign in to complete your profile.")
# update message if portal mode = b2b
if request.env['ir.config_parameter'].sudo().get_param('auth_signup.allow_uninvited', 'False').lower() == 'false':
message += _("If you don't have any account, please ask your salesperson to update your profile. ")
else:
message += _("Please complete your profile. ")
return {
'error': message
}
if not token:
res = {
@@ -154,6 +154,11 @@ class AuthorizeAPI():
etree.SubElement(payment_profile, "customerType").text = 'business' if partner.is_company else 'individual'
billTo = etree.SubElement(payment_profile, "billTo")
etree.SubElement(billTo, "address").text = (partner.street or '' + (partner.street2 if partner.street2 else '')) or None
missing_fields = [partner._fields[field].string for field in ['city', 'country_id', 'zip'] if not partner[field]]
if missing_fields:
raise ValidationError({'missing_fields': missing_fields})
etree.SubElement(billTo, "city").text = partner.city
etree.SubElement(billTo, "state").text = partner.state_id.name or None
etree.SubElement(billTo, "zip").text = partner.zip
@@ -51,7 +51,7 @@
<template id="authorize_s2s_form">
<input type="hidden" name="data_set" data-create-route="/payment/authorize/s2s/create_json_3ds"/>
<div class="mt8">
<div t-attf-class="mt8 #{'' if bootstrap_formatting else 'o_card_brand_detail'}">
<div t-att-class="'form-group col-md-12' if bootstrap_formatting else 'form-group'">
<input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="Card number" data-is-required="true"/>
<div class="card_placeholder"></div>
+4
View File
@@ -30,11 +30,15 @@ class OgoneController(http.Controller):
@http.route(['/payment/ogone/s2s/create_json'], type='json', auth='public', csrf=False)
def ogone_s2s_create_json(self, **kwargs):
if not kwargs.get('partner_id'):
kwargs = dict(kwargs, partner_id=request.env.user.partner_id.id)
new_id = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs)
return new_id.id
@http.route(['/payment/ogone/s2s/create_json_3ds'], type='json', auth='public', csrf=False)
def ogone_s2s_create_json_3ds(self, verify_validity=False, **kwargs):
if not kwargs.get('partner_id'):
kwargs = dict(kwargs, partner_id=request.env.user.partner_id.id)
token = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs)
if not token:
@@ -40,7 +40,7 @@
<input type="hidden" name="acquirer_id" t-att-value="id"/>
<input t-if="return_url" type="hidden" name="return_url" t-att-value="return_url"/>
<input t-if="partner_id" type="hidden" name="partner_id" t-att-value="partner_id"/>
<div class="mt8">
<div t-attf-class="mt8 #{'' if bootstrap_formatting else 'o_card_brand_detail'}">
<div t-att-class="'form-group col-md-12' if bootstrap_formatting else 'form-group'">
<input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="Card number" data-is-required="true"/>
<div class="card_placeholder"></div>
@@ -15,6 +15,8 @@ class StripeController(http.Controller):
def stripe_s2s_create_json(self, **kwargs):
acquirer_id = int(kwargs.get('acquirer_id'))
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
if not kwargs.get('partner_id'):
kwargs = dict(kwargs, partner_id=request.env.user.partner_id.id)
return acquirer.s2s_process(kwargs).id
@http.route(['/payment/stripe/s2s/create'], type='http', auth='public')
@@ -36,6 +38,8 @@ class StripeController(http.Controller):
@http.route(['/payment/stripe/s2s/create_json_3ds'], type='json', auth='public', csrf=False)
def stripe_s2s_create_json_3ds(self, verify_validity=False, **kwargs):
if not kwargs.get('partner_id'):
kwargs = dict(kwargs, partner_id=request.env.user.partner_id.id)
token = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs)
if not token:
@@ -120,6 +120,14 @@ odoo.define('payment_stripe.stripe', function(require) {
}).then(function (data) {
try { provider_form[0].innerHTML = data; } catch (e) {};
});
} else if (window.location.href.includes("/my/orders/")) {
var create_tx = ajax.jsonRpc('/pay/sale/' + acquirer_id + '/form_tx/', 'call', {
so_id: so_id,
access_token: access_token,
acquirer_id: acquirer_id
}).then(function (data) {
try { provider_form.innerHTML = data; } catch (e) {};
});
}
else {
var create_tx = ajax.jsonRpc('/shop/payment/transaction/' + acquirer_id, 'call', {
@@ -26,7 +26,7 @@
<template id="stripe_s2s_form">
<input type="hidden" name="data_set" data-create-route="/payment/stripe/s2s/create_json_3ds"/>
<div class="mt8">
<div t-attf-class="mt8 #{'' if bootstrap_formatting else 'o_card_brand_detail'}">
<div t-att-class="'form-group col-md-12' if bootstrap_formatting else 'form-group'">
<input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="Card number" data-is-required="true"/>
<div class="card_placeholder"></div>
+10 -3
View File
@@ -364,15 +364,22 @@ class PosOrder(models.Model):
for order in self:
aml = order.statement_ids.mapped('journal_entry_ids') | order.account_move.line_ids | order.invoice_id.move_id.line_ids
aml = aml.filtered(lambda r: not r.reconciled and r.account_id.internal_type == 'receivable' and r.partner_id == order.partner_id.commercial_partner_id)
# Reconcile returns first
# to avoid mixing up the credit of a payment and the credit of a return
# in the receivable account
aml_returns = aml.filtered(lambda l: (l.journal_id.type == 'sale' and l.credit) or (l.journal_id.type != 'sale' and l.debit))
try:
aml.reconcile()
aml_returns.reconcile()
(aml - aml_returns).reconcile()
except Exception:
# There might be unexpected situations where the automatic reconciliation won't
# work. We don't want the user to be blocked because of this, since the automatic
# reconciliation is introduced for convenience, not for mandatory accounting
# reasons.
_logger.error('Reconciliation did not work for order %s', order.name)
continue
# It may be interesting to have the Traceback logged anyway
# for debugging and support purposes
_logger.exception('Reconciliation did not work for order %s', order.name)
def _default_session(self):
return self.env['pos.session'].search([('state', '=', 'opened'), ('user_id', '=', self.env.uid)], limit=1)
@@ -28,7 +28,7 @@ access_product_pricelist_manager,product.pricelist manager,product.model_product
access_product_pricelist_user,product.pricelist user,product.model_product_pricelist,group_pos_user,1,0,0,0
access_pos_session_user,pos.session user,model_pos_session,group_pos_user,1,1,1,0
access_pos_config_user,pos.config user,model_pos_config,group_pos_user,1,1,0,0
access_pos_config_manager,pos.config user,model_pos_config,group_pos_manager,1,1,1,0
access_pos_config_manager,pos.config manager,model_pos_config,group_pos_manager,1,1,1,1
access_product_category_pos_manager,pos.category manager,model_pos_category,group_pos_manager,1,1,1,1
access_product_category_pos_user,pos.category user,model_pos_category,group_pos_user,1,0,0,0
access_barcode_nomenclature_pos_user,barcode.nomenclature.pos.user,barcodes.model_barcode_nomenclature,group_pos_user,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
28 access_product_pricelist_user product.pricelist user product.model_product_pricelist group_pos_user 1 0 0 0
29 access_pos_session_user pos.session user model_pos_session group_pos_user 1 1 1 0
30 access_pos_config_user pos.config user model_pos_config group_pos_user 1 1 0 0
31 access_pos_config_manager pos.config user pos.config manager model_pos_config group_pos_manager 1 1 1 0 1
32 access_product_category_pos_manager pos.category manager model_pos_category group_pos_manager 1 1 1 1
33 access_product_category_pos_user pos.category user model_pos_category group_pos_user 1 0 0 0
34 access_barcode_nomenclature_pos_user barcode.nomenclature.pos.user barcodes.model_barcode_nomenclature group_pos_user 1 0 0 0
@@ -15,8 +15,8 @@
</div>
<div id="o_portal_signature" class="panel-body" style="padding: 0"/>
</div>
<div class="mt16 pull-right">
<button type="submit" class="btn btn-primary o_portal_sign_submit"><t t-esc="widget.options.signLabel or 'Accept &amp; Sign'"/></button>
<div class="mt16 clearfix">
<button type="submit" class="btn btn-primary o_portal_sign_submit pull-right"><t t-esc="widget.options.signLabel or 'Accept &amp; Sign'"/></button>
</div>
</form>
</t>
+1
View File
@@ -8,6 +8,7 @@ class ResCompany(models.Model):
_inherit = 'res.company'
project_time_mode_id = fields.Many2one('product.uom', string='Project Time Unit',
default=lambda s: s.env.ref('product.product_uom_hour', raise_if_not_found=False),
help="This will set the unit of measure used in projects and tasks.\n"
"If you use the timesheet linked to projects, don't "
"forget to setup the right unit of measure in your employees.")
@@ -11,3 +11,8 @@ class ResConfigSettings(models.TransientModel):
module_project_forecast = fields.Boolean(string="Forecasts")
group_subtask_project = fields.Boolean("Sub-tasks", implied_group="project.group_subtask_project")
group_project_rating = fields.Boolean("Use Rating on Project", implied_group='project.group_project_rating')
project_time_mode_id = fields.Many2one(
'product.uom', related='company_id.project_time_mode_id', string='Project Time Unit',
help="This will set the unit of measure used in projects and tasks.\n"
"If you use the timesheet linked to projects, don't "
"forget to setup the right unit of measure in your employees.")
@@ -60,6 +60,10 @@
<div class="text-muted">
Track time spent on projects and tasks
</div>
<div class="row mt16" attrs="{'invisible':['|', ('module_hr_timesheet', '=', False), ('project_time_mode_id', '!=', False)]}">
<label for="project_time_mode_id" class="col-md-3 o_light_label"/>
<field name="project_time_mode_id" options="{'no_create': True, 'no_open': True}"/>
</div>
</div>
</div>
<div class="col-xs-12 col-md-6 o_setting_box">
+1 -1
View File
@@ -34,7 +34,7 @@ def to_naive_utc(datetime, record):
def to_tz(datetime, tz_name):
tz = pytz.timezone(tz_name)
tz = pytz.timezone(tz_name) if tz_name else pytz.UTC
return pytz.UTC.localize(datetime.replace(tzinfo=None), is_dst=False).astimezone(tz).replace(tzinfo=None)
+4 -4
View File
@@ -335,8 +335,8 @@
</template>
<template id="portal_order_error" name="Order error/warning display">
<div class="row">
<div t-att-class="'col-md-6 col-md-offset-3 text-center alert alert-dismissable %s' % ('alert-danger' if error else 'alert-warning')">
<div class="row mr16">
<div t-attf-class="'col-md-12 mr16 ml16 alert alert-dismissable' #{'alert-danger' if error else 'alert-warning'}">
<a href="#" class="close" data-dismiss="alert" aria-label="close" title="close">×</a>
<t t-if="error == 'generic'" name="generic">
There was an error processing this page.
@@ -346,8 +346,8 @@
</template>
<template id="portal_order_success" name="Order success display">
<div class="row">
<div class="col-md-6 col-md-offset-3 text-center alert alert-dismissable alert-success">
<div class="row mr16">
<div class="col-md-12 mr16 ml16 alert alert-dismissable alert-success">
<a href="#" class="close" data-dismiss="alert" aria-label="close" title="close">×</a>
</div>
</div>
+5 -5
View File
@@ -94,9 +94,9 @@ class PaymentPortal(http.Controller):
# proceed to the payment
res = tx.confirm_sale_token()
if res is not True:
params['error'] = res
return request.redirect(_build_url_w_params(error_url, params))
params['success'] = 'pay_sale'
if tx.state != 'authorized' or not tx.acquirer_id.capture_manually:
if res is not True:
params['error'] = res
return request.redirect(_build_url_w_params(error_url, params))
params['success'] = 'pay_sale'
return request.redirect(_build_url_w_params(success_url, params))
+2 -4
View File
@@ -64,12 +64,10 @@ class PaymentTransaction(models.Model):
_logger.info('<%s> transaction authorized, auto-confirming order %s (ID %s)', self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id)
if self.sale_order_id.state in ('draft', 'sent'):
self.sale_order_id.with_context(send_email=True).action_confirm()
if self.state == 'done':
_logger.info('<%s> transaction completed, auto-confirming order %s (ID %s) and generating invoice', self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id)
elif self.state == 'done':
_logger.info('<%s> transaction completed, auto-confirming order %s (ID %s)', self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id)
if self.sale_order_id.state in ('draft', 'sent'):
self.sale_order_id.with_context(send_email=True).action_confirm()
self._generate_and_pay_invoice()
elif self.state not in ['cancel', 'error'] and self.sale_order_id.state == 'draft':
_logger.info('<%s> transaction pending/to confirm manually, sending quote email for order %s (ID %s)', self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id)
self.sale_order_id.force_quotation_send()
@@ -1,4 +1,34 @@
<odoo>
<!-- duplicata of the template payment.payment_confirmation_status.
The duplication avoid to break an existing installation in stable version-->
<template id="payment_confirmation_status">
<div t-if="payment_tx_id and payment_tx_id.state == 'done'" class="alert alert-success alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.done_msg' t-raw="payment_tx_id.acquirer_id.done_msg"/>
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'pending'" class="alert alert-warning alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.pending_msg' t-raw="payment_tx_id.acquirer_id.pending_msg"/>
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'cancel'" class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.cancel_msg' t-raw="payment_tx_id.acquirer_id.cancel_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'error'" class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.error_msg' t-raw="payment_tx_id.acquirer_id.error_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'authorized'" class="alert alert-success alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
Your payment has been authorized.
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
</template>
<template id="portal_my_orders_payment" name="Payment on my orders" inherit_id="sale.portal_order_page">
<xpath expr="//t[@name='portal_confirmation_sign']" position="after">
<t t-if="not invoices and order.state in ('sent', 'sale') and portal_confirmation == 'pay' and order.payment_tx_id.state != 'done'" name="portal_confirmation_pay">
@@ -12,8 +42,14 @@
</a>
</t>
</xpath>
<xpath expr="//t[@t-call='sale.portal_order_success']" position="after">
<div t-if="order.payment_tx_ids and not invoices and order.state in ('sent', 'sale') and portal_confirmation == 'pay' and not success and not error" class="o_sale_payment_tx_status" t-att-data-order-id="order.id">
<t t-set="payment_tx_id" t-value="order.payment_tx_id"/>
<t t-call="sale_payment.payment_confirmation_status"/>
</div>
</xpath>
<xpath expr="//div[@id='portal_sign']" position="after">
<div class="row" t-if="not invoices and order.state in ('sent', 'sale') and portal_confirmation == 'pay'" id="portal_pay">
<div class="row" t-if="not invoices and order.state in ('sent') and portal_confirmation == 'pay'" id="portal_pay">
<div class="col-md-offset-3 col-md-6">
<div t-if="pms or s2s_acquirers or form_acquirers" id="payment_method" class="text-left col-md-13">
<h4 class="mb24">Pay with</h4>
@@ -21,8 +57,8 @@
<t t-set="mode" t-value="'payment'"/>
<t t-set="partner_id" t-value="order.partner_id.id"/>
<t t-set="access_token" t-value="access_token"/>
<t t-set="success_url" t-value="'/my/orders/%s?%s' % (order.id, keep_query())"/>
<t t-set="error_url" t-value="'/my/orders/%s?%s' % (order.id, keep_query())"/>
<t t-set="success_url" t-value="'/my/orders/%s%s' % (order.id, ('?access_token=%s' % (access_token)) if access_token else '')"/>
<t t-set="error_url" t-value="'/my/orders/%s%s' % (order.id, ('?access_token=%s' % (access_token)) if access_token else '')"/>
<t t-set="callback_method" t-value="''"/>
<t t-set="submit_txt" t-value="'Pay Now'"/>
<t t-set="icon_class" t-value="'fa-lock'"/>
@@ -101,7 +137,8 @@
inherit_id="sale.portal_order_success">
<xpath expr="//a[hasclass('close')]" position="after">
<t t-if="success == 'pay_sale'">
Order successfully paid.
<span t-if='order.payment_acquirer_id.done_msg' t-raw="order.payment_acquirer_id.done_msg"/>
<span t-if='order.payment_acquirer_id.post_msg' t-raw="order.payment_acquirer_id.post_msg"/>
</t>
</xpath>
</template>
+5
View File
@@ -42,6 +42,11 @@ class AccountAnalyticLine(models.Model):
if 'task_id' in values:
task = self.env['project.task'].sudo().browse(values['task_id'])
values['so_line'] = task.sale_line_id.id or values.get('so_line', False)
# Set product_uom_id now so delivered qty is computed in SO line
if not 'product_uom_id' in values and all([v in values for v in ['employee_id', 'project_id']]):
employee = self.env['hr.employee'].sudo().browse(values['employee_id'])
values['product_uom_id'] = employee.company_id.project_time_mode_id.id
return values
@api.multi
+5 -3
View File
@@ -15,8 +15,10 @@ try:
except ImportError:
_sms_phonenumbers_lib_imported = False
_logger.warning(
"The `phonenumbers` Python module is not installed. Try: pip install phonenumbers."
_logger.info(
"The `phonenumbers` Python module is not available. "
"Phone number validation will be skipped. "
"Try `pip3 install phonenumbers` to install it."
)
@@ -70,7 +72,7 @@ class SendSMS(models.TransientModel):
no_phone_partners = []
for partner in partners:
default_field_name = 'mobile' if partner.mobile else 'phone'
number = self._sms_sanitization(partner, self.env.context('field_name') or default_field_name)
number = self._sms_sanitization(partner, self.env.context.get('field_name') or default_field_name)
if number:
phone_numbers.append(number)
else:
+4 -9
View File
@@ -1009,17 +1009,12 @@ class StockMove(models.Model):
move._action_cancel()
# Create extra moves where necessary
for move in moves.filtered(lambda m: m.state != 'cancel'):
# Here, the `quantity_done` was already rounded to the product UOM by the `do_produce` wizard. However,
# it is possible that the user changed the value before posting the inventory by a value that should be
# rounded according to the move's UOM. In this specific case, we chose to round up the value, because it
# is what is expected by the user (if i consumed/produced a little more, the whole UOM unit should be
# consumed/produced and the moves are split correctly).
# FIXME: move rounding to move line
# rounding = move.product_uom.rounding
# move.quantity_done = float_round(move.quantity_done, precision_rounding=rounding, rounding_method ='UP')
for move in moves:
if move.state == 'cancel' or move.quantity_done <= 0:
continue
moves_todo |= move
moves_todo |= move._create_extra_move()
# Split moves where necessary and move quants
for move in moves_todo:
rounding = move.product_uom.rounding
+1 -1
View File
@@ -90,7 +90,7 @@ class StockMoveLine(models.Model):
@api.constrains('product_uom_qty')
def check_reserved_done_quantity(self):
for move_line in self:
if move_line.state == 'done' and not float_is_zero(move_line.product_uom_qty, precision_rounding=self.env['decimal.precision'].precision_get('Product Unit of Measure')):
if move_line.state == 'done' and not float_is_zero(move_line.product_uom_qty, precision_digits=self.env['decimal.precision'].precision_get('Product Unit of Measure')):
raise ValidationError(_('A done move line should never have a reserved quantity.'))
@api.onchange('product_id', 'product_uom_id')
+45
View File
@@ -743,6 +743,51 @@ class TestSinglePicking(TestStockCommon):
backorder = self.env['stock.picking'].search([('backorder_id', '=', delivery_order.id)])
self.assertEqual(backorder.state, 'confirmed')
def test_backorder_3(self):
""" Check the good behavior of creating a backorder for an available move on a picking with
two available moves.
"""
delivery_order = self.env['stock.picking'].create({
'location_id': self.pack_location,
'location_dest_id': self.customer_location,
'partner_id': self.partner_delta_id,
'picking_type_id': self.picking_type_out,
})
self.MoveObj.create({
'name': self.productA.name,
'product_id': self.productA.id,
'product_uom_qty': 2,
'product_uom': self.productA.uom_id.id,
'picking_id': delivery_order.id,
'location_id': self.pack_location,
'location_dest_id': self.customer_location,
})
self.MoveObj.create({
'name': self.productA.name,
'product_id': self.productB.id,
'product_uom_qty': 2,
'product_uom': self.productB.uom_id.id,
'picking_id': delivery_order.id,
'location_id': self.pack_location,
'location_dest_id': self.customer_location,
})
# make some stock
pack_location = self.env['stock.location'].browse(self.pack_location)
self.env['stock.quant']._update_available_quantity(self.productA, pack_location, 2)
self.env['stock.quant']._update_available_quantity(self.productA, pack_location, 2)
# assign to partially available
delivery_order.action_confirm()
delivery_order.action_assign()
self.assertEqual(delivery_order.state, 'assigned')
delivery_order.move_lines[0].move_line_ids[0].qty_done = 2
delivery_order.do_transfer()
backorder = self.env['stock.picking'].search([('backorder_id', '=', delivery_order.id)])
self.assertEqual(backorder.state, 'confirmed')
def test_extra_move_1(self):
""" Check the good behavior of creating an extra move in a delivery order. This usecase
simulates the delivery of 2 item while the initial stock move had to move 1 and there's
+2 -1
View File
@@ -200,6 +200,7 @@
<field name="arch" type="xml">
<form string="Transfer">
<header>
<field name="id" invisible="1"/>
<field name="is_locked" invisible="1"/>
<field name="show_mark_as_todo" invisible="1"/>
<field name="show_check_availability" invisible="1"/>
@@ -245,7 +246,7 @@
<field name="backorder_id" readonly="1" attrs="{'invisible': [('backorder_id','=',False)]}"/>
</group>
<group>
<field name="scheduled_date"/>
<field name="scheduled_date" attrs="{'readonly': [('id', '=', False)]}"/>
<field name="origin" placeholder="e.g. PO0032"/>
<field name="owner_id" groups="stock.group_tracking_owner"/>
<div groups="stock.group_tracking_owner" colspan="2" col="2">
@@ -87,12 +87,6 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
this.active_view = null;
this.registry = view_registry;
this.title = this.action.name;
var actionGroupBy = self.action.context.group_by;
if (!actionGroupBy) {
actionGroupBy = [];
} else if (typeof actionGroupBy === 'string') {
actionGroupBy = [actionGroupBy];
}
_.each(views, function (view) {
var view_type = view[1] || view.view_type;
var View = self.registry.get(view_type); //.prototype.config.Controller;
@@ -114,7 +108,6 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
action: self.action,
limit: self.action.limit,
views: self.action.views,
groupBy: actionGroupBy,
}, self.flags, self.flags[view_type], view.options),
searchable: View.prototype.searchable,
title: self.title,
@@ -271,16 +264,6 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
if (!view.loaded) {
view_options = _.extend({}, view.options, view_options, self.env);
if (view_options.groupBy && !view_options.groupBy.length) {
var actionContext = view_options ? view_options.action.context : {};
var actionGroupBy = actionContext.group_by;
if (!actionGroupBy) {
actionGroupBy = [];
} else if (typeof actionGroupBy === 'string') {
actionGroupBy = [actionGroupBy];
}
view_options.groupBy = actionGroupBy;
}
view.loaded = $.Deferred();
self.create_view(view, view_options).then(function(controller) {
view.controller = controller;
@@ -512,18 +495,15 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
throw new Error(_.str.sprintf(_t("Failed to evaluate search criterions")+": \n%s",
JSON.stringify(results.error)));
}
// this.dataset._model = new Model(this.dataset.model, results.context, results.domain);
// var groupby = results.group_by.length ? results.group_by : action_context.group_by;
// if (_.isString(groupby)) {
// groupby = [groupby];
// }
// if (!controller.grouped && !_.isEmpty(groupby)){
// this.dataset.set_sort([]);
// }
// FORWARDPORT THIS UP TO SAAS-11.1 ONLY, NOT LATER
var groupby = results.group_by.length ? results.group_by : action_context.group_by;
if (_.isString(groupby)) {
groupby = [groupby];
}
return {
context: results.context,
domain: results.domain,
groupBy: results.group_by,
groupBy: groupby || [],
};
},
do_push_state: function(state) {
@@ -3238,92 +3238,7 @@ var BasicModel = AbstractModel.extend({
defs.push(self._fetchNameGet(dp));
record._changes[name] = dp.id;
} else if (field.type === 'one2many' || field.type === 'many2many') {
var fieldInfo = record.fieldsInfo[record.viewType][name];
var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode];
var fieldsInfo = view ? view.fieldsInfo : fieldInfo.fieldsInfo;
var fields = view ? view.fields : fieldInfo.relatedFields;
var viewType = view ? view.type : fieldInfo.viewType;
var x2manyList = self._makeDataPoint({
context: record.context,
fieldsInfo: fieldsInfo,
fields: fields,
limit: fieldInfo.limit,
modelName: field.relation,
parentID: record.id,
rawContext: fieldInfo && fieldInfo.context,
relationField: field.relation_field,
res_ids: [],
static: true,
type: 'list',
viewType: viewType,
});
record._changes[name] = x2manyList.id;
x2manyList._changes = [];
var many2ones = {};
var r;
result[name] = result[name] || []; // handle false value
var isCommandList = result[name].length && _.isArray(result[name][0]);
if (!isCommandList) {
result[name] = [[6, false, result[name]]];
}
_.each(result[name], function (value) {
// value is a command
if (value[0] === 0) {
// CREATE
r = self._makeDataPoint({
modelName: x2manyList.model,
context: x2manyList.context,
fieldsInfo: fieldsInfo,
fields: fields,
parentID: x2manyList.id,
viewType: viewType,
});
x2manyList._changes.push({operation: 'ADD', id: r.id});
x2manyList._cache[r.res_id] = r.id;
// this is necessary so the fields are initialized
_.each(r.getFieldNames(), function (fieldName) {
r.data[fieldName] = null;
});
r._changes = _.defaults(value[2], r.data);
for (var name in r._changes) {
if (!r._changes[name]) {
continue;
}
var isFieldInView = name in r.fields;
if (isFieldInView && r.fields[name].type === 'many2one') {
var rec = self._makeDataPoint({
context: r.context,
modelName: r.fields[name].relation,
data: {id: r._changes[name]},
parentID: r.id,
});
r._changes[name] = rec.id;
many2ones[name] = true;
}
}
}
if (value[0] === 6) {
// REPLACE_WITH
_.each(value[2], function (res_id) {
x2manyList._changes.push({operation: 'ADD', resID: res_id});
});
var def = self._readUngroupedList(x2manyList).then(function () {
return $.when(
self._fetchX2ManysBatched(x2manyList),
self._fetchReferencesBatched(x2manyList)
);
});
defs.push(def);
}
});
// fetch many2ones display_name
_.each(_.keys(many2ones), function (name) {
defs.push(self._fetchNameGets(x2manyList, name));
});
defs.push(self._processX2ManyCommands(record, name, result[name]));
} else {
record._changes[name] = self._parseServerValue(field, result[name]);
}
@@ -3497,6 +3412,122 @@ var BasicModel = AbstractModel.extend({
return record;
});
},
/**
* Process x2many commands in a default record by transforming the list of
* commands in operations (pushed in _changes) and fetch the related
* records fields.
*
* Note that this method can be called recursively.
*
* @todo in master: factorize this code with the postprocessing of x2many in
* _applyOnChange
*
* @private
* @param {Object} record
* @param {string} fieldName
* @param {Array[Array]} commands
* @returns {Deferred}
*/
_processX2ManyCommands: function (record, fieldName, commands) {
var self = this;
var defs = [];
var field = record.fields[fieldName];
var fieldInfo = record.fieldsInfo[record.viewType][fieldName];
var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode];
var fieldsInfo = view ? view.fieldsInfo : fieldInfo.fieldsInfo;
var fields = view ? view.fields : fieldInfo.relatedFields;
var viewType = view ? view.type : fieldInfo.viewType;
var x2manyList = self._makeDataPoint({
context: record.context,
fieldsInfo: fieldsInfo,
fields: fields,
limit: fieldInfo.limit,
modelName: field.relation,
parentID: record.id,
rawContext: fieldInfo && fieldInfo.context,
relationField: field.relation_field,
res_ids: [],
static: true,
type: 'list',
viewType: viewType,
});
record._changes[fieldName] = x2manyList.id;
x2manyList._changes = [];
var many2ones = {};
var r;
commands = commands || []; // handle false value
var isCommandList = commands.length && _.isArray(commands[0]);
if (!isCommandList) {
commands = [[6, false, commands]];
}
_.each(commands, function (value) {
// value is a command
if (value[0] === 0) {
// CREATE
r = self._makeDataPoint({
modelName: x2manyList.model,
context: x2manyList.context,
fieldsInfo: fieldsInfo,
fields: fields,
parentID: x2manyList.id,
viewType: viewType,
});
x2manyList._changes.push({operation: 'ADD', id: r.id});
x2manyList._cache[r.res_id] = r.id;
// this is necessary so the fields are initialized
_.each(r.getFieldNames(), function (fieldName) {
r.data[fieldName] = null;
});
r._changes = _.defaults(value[2], r.data);
for (var fieldName in r._changes) {
if (!r._changes[fieldName]) {
continue;
}
var isFieldInView = fieldName in r.fields;
if (isFieldInView) {
var field = r.fields[fieldName];
var fieldType = field.type;
if (fieldType === 'many2one') {
var rec = self._makeDataPoint({
context: r.context,
modelName: field.relation,
data: {id: r._changes[fieldName]},
parentID: r.id,
});
r._changes[fieldName] = rec.id;
many2ones[fieldName] = true;
} else if (_.contains(['one2many', 'many2many'], fieldType)) {
var x2mCommands = commands[0][2][fieldName];
defs.push(self._processX2ManyCommands(r, fieldName, x2mCommands));
}
}
}
}
if (value[0] === 6) {
// REPLACE_WITH
_.each(value[2], function (res_id) {
x2manyList._changes.push({operation: 'ADD', resID: res_id});
});
var def = self._readUngroupedList(x2manyList).then(function () {
return $.when(
self._fetchX2ManysBatched(x2manyList),
self._fetchReferencesBatched(x2manyList)
);
});
defs.push(def);
}
});
// fetch many2ones display_name
_.each(_.keys(many2ones), function (name) {
defs.push(self._fetchNameGets(x2manyList, name));
});
return $.when.apply($, defs);
},
/**
* Reads data from server for all missing fields.
*
@@ -470,7 +470,7 @@ DebugManager.include({
$defaults.parent().addClass('o_form_invalid');
return;
}
var allUsers = d.$el.find('#formview_default_all').is(':checked');
var selfUser = d.$el.find('#formview_default_self').is(':checked');
var condition = d.$el.find('#formview_default_conditions').val();
var value = _.find(self.fields, function (field) {
return field.name === fieldToSet;
@@ -482,7 +482,7 @@ DebugManager.include({
self._active_view.fields_view.model,
fieldToSet,
value,
allUsers,
selfUser,
true,
condition || false,
],
+3 -1
View File
@@ -208,7 +208,9 @@
.o_kanban_record_headings {
line-height: 1.2;
.o-flex(1, 1, auto);
// Ensure long word doesn't break out of container
word-wrap: break-word;
overflow: hidden;
}
}
@@ -6201,6 +6201,38 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('nested x2many default values', function (assert) {
assert.expect(2);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="turtles">' +
'<tree editable="top">' +
'<field name="partner_ids" widget="many2many_tags"/>' +
'</tree>' +
'</field>' +
'</form>',
mockRPC: function (route, args) {
if (args.model === 'partner' && args.method === 'default_get') {
return $.when({turtles: [[0, 0, {
partner_ids: [[6, 0, [4]]],
}]]});
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(form.$('.o_list_view .o_field_many2manytags[name="partner_ids"] .badge').length, 1,
"m2mtags should contain one tag");
assert.strictEqual(form.$('.o_list_view .o_field_many2manytags[name="partner_ids"] .o_badge_text').text(),
'aaa', "tag name should have been correctly loaded");
form.destroy();
});
QUnit.test('one2many (who contains display_name) with tree view and without form view', function (assert) {
assert.expect(1);
@@ -192,7 +192,6 @@ var ViewEditor = Widget.extend({
*/
start: function () {
this.$viewEditor = this.$('#ace-view-editor');
this.$editor = this.$('.ace_editor');
this.$typeSwitcherChoices = this.$('.o_ace_type_switcher_choice');
this.$typeSwitcherBtn = this.$('.o_ace_type_switcher > .dropdown-toggle');
@@ -210,6 +209,7 @@ var ViewEditor = Widget.extend({
this.aceEditor = window.ace.edit(this.$viewEditor[0]);
this.aceEditor.setTheme('ace/theme/monokai');
this.$editor = this.$('.ace_editor');
var refX = 0;
var resizing = false;
+6 -5
View File
@@ -207,11 +207,12 @@ class BlogPost(models.Model):
@api.multi
def write(self, vals):
self.ensure_one()
if 'website_published' in vals and 'published_date' not in vals:
if (self.published_date or '') <= fields.Datetime.now():
vals['published_date'] = vals['website_published'] and fields.Datetime.now()
result = super(BlogPost, self).write(vals)
result = True
for post in self:
copy_vals = vals
if 'website_published' in vals and 'published_date' not in vals and (post.published_date or '') <= fields.Datetime.now():
copy_vals['published_date'] = vals['website_published'] and fields.Datetime.now() or False
result &= super(BlogPost, self).write(copy_vals)
self._check_for_publication(vals)
return result
@@ -15,7 +15,7 @@ class WebsiteEventTrackController(http.Controller):
@http.route(['''/event/<model("event.event"):event>/track/<model("event.track", "[('event_id','=',event[0])]"):track>'''], type='http', auth="public", website=True)
def event_track_view(self, event, track, **post):
track = track.sudo()
track = track.sudo().with_context(tz=event.date_tz or 'UTC')
values = {'track': track, 'event': track.event_id, 'main_object': track}
return request.render("website_event_track.track_view", values)
@@ -67,6 +67,7 @@ class WebsiteEventTrackController(http.Controller):
@http.route(['''/event/<model("event.event"):event>/agenda'''], type='http', auth="public", website=True, sitemap=False)
def event_agenda(self, event, tag=None, **post):
event = event.with_context(tz=event.date_tz or 'UTC')
local_tz = pytz.timezone(event.date_tz or 'UTC')
days_tracks = collections.defaultdict(lambda: [])
for track in event.track_ids.sorted(lambda track: (track.date or '', bool(track.location_id))):
@@ -93,6 +94,7 @@ class WebsiteEventTrackController(http.Controller):
'''/event/<model("event.event"):event>/track/tag/<model("event.track.tag"):tag>'''
], type='http', auth="public", website=True, sitemap=False)
def event_tracks(self, event, tag=None, **post):
event = event.with_context(tz=event.date_tz or 'UTC')
searches = {}
if tag:
searches.update(tag=tag.id)
@@ -255,7 +255,9 @@
</div>
<div class="panel-body">
<b>Date</b><br/>
<span t-field="track.date" t-options='{"hide_seconds":"True"}'/><br/>
<span t-field="track.date" t-options='{"hide_seconds":"True"}'/>
<t t-if="event.date_tz">(<span t-field="event.date_tz"/>)</t>
<br/>
<b>Duration</b><br/>
<span t-field="track.duration" t-options='{"widget": "duration", "unit": "hour", "round": "minute"}'/><br/>
<b>Location</b><br/>
+1
View File
@@ -79,6 +79,7 @@ class sale_quote(http.Controller):
'tx_id': Transaction.id if Transaction else False,
'tx_state': Transaction.state if Transaction else False,
'tx_post_msg': Transaction.acquirer_id.post_msg if Transaction else False,
'payment_tx': Transaction,
'need_payment': order_sudo.invoice_status == 'to invoice' and Transaction.state in ['draft', 'cancel', 'error'],
'token': token,
'return_url': '/shop/payment/validate',
+4 -1
View File
@@ -51,6 +51,9 @@ class SaleOrder(models.Model):
template = self.env.ref('website_quote.website_quote_template_default', raise_if_not_found=False)
return template and template.active and template or False
def _get_default_online_payment(self):
return 1 if self.env['ir.config_parameter'].sudo().get_param('sale.sale_portal_confirmation_options', default='none') == 'pay' else 0
template_id = fields.Many2one(
'sale.quote.template', 'Quotation Template',
readonly=True,
@@ -66,7 +69,7 @@ class SaleOrder(models.Model):
quote_viewed = fields.Boolean('Quotation Viewed')
require_payment = fields.Selection([
(0, 'Online Signature'),
(1, 'Online Payment')], default=0, string='Confirmation Mode',
(1, 'Online Payment')], default=_get_default_online_payment, string='Confirmation Mode',
help="Choose how you want to confirm an order to launch the delivery process. You can either "
"request a digital signature or an upfront payment. With a digital signature, you can "
"request the payment when issuing the invoice.")
@@ -163,6 +163,34 @@
</xpath>
</template>
<!-- duplicata of the template payment.payment_confirmation_status.
The duplication avoid to break an existing installation in stable version-->
<template id="payment_confirmation_status">
<div t-if="payment_tx_id and payment_tx_id.state == 'done'" class="alert alert-success alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.done_msg' t-raw="payment_tx_id.acquirer_id.done_msg"/>
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'pending'" class="alert alert-warning alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.pending_msg' t-raw="payment_tx_id.acquirer_id.pending_msg"/>
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'cancel'" class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.cancel_msg' t-raw="payment_tx_id.acquirer_id.cancel_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'error'" class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.error_msg' t-raw="payment_tx_id.acquirer_id.error_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'authorized'" class="alert alert-success alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
Your payment has been authorized.
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
</template>
<!-- Complete page of the quotation -->
<template id="so_quotation" name="Product Quotation">
<t t-call="portal.portal_layout">
@@ -257,29 +285,18 @@
and do not hesitate to <a href="#discussion">contact us</a> for
any question.
</div>
<div class="alert alert-warning alert-dismissable" t-if="tx_id and tx_state == 'done'">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
Your payment has been received, thank you for your trust.
<span t-if='tx_post_msg' t-raw="tx_post_msg"/>
</div>
<div class="alert alert-warning alert-dismissable" t-if="tx_id and tx_state == 'pending'">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
Your transaction is waiting confirmation.
<span t-if='tx_post_msg' t-raw="tx_post_msg"/>
</div>
<div class="alert alert-success alert-dismissable" t-if="tx_id and tx_state == 'authorized'">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
Your payment has been authorized.
<span t-if='tx_post_msg' t-raw="tx_post_msg"/>
</div>
<t t-if="payment_tx">
<t t-set="payment_tx_id" t-value="payment_tx"/>
<t t-call="website_quote.payment_confirmation_status"/>
</t>
<div class="alert alert-warning alert-dismissable" t-if="message==4 and quotation.state != 'sent'">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
This order
<t t-if="quotation.state=='draft'">has not yet been sent</t>
<t t-if="quotation.state=='cancel'">has already been cancelled</t>
<t t-if="quotation.state not in ('cancel','draft')">has already been validated</t>
! You can <a href="#discussion">contact us</a> for
any question.
<t t-if="quotation.state=='draft'">has not yet been sent!</t>
<t t-if="quotation.state=='cancel'">has already been cancelled!</t>
<t t-if="quotation.state not in ('cancel','draft')">has already been validated!</t>
You can <a href="#discussion">contact us</a> for
any questions.
</div>
<!-- modal relative to the actions Accept/Reject/Cancel -->
@@ -332,6 +349,7 @@
<t t-set="icon_class" t-value="'fa-lock'"/>
<t t-set="form_action" t-value="'/quote/' + str(quotation.id) + '/transaction/token'"/>
<t t-set="prepare_tx_url" t-value="'/quote/' + str(quotation.id) + '/transaction/'"/>
<t t-set="access_token" t-value="quotation.access_token"/>
</t>
</div>
</div>
+6 -4
View File
@@ -749,7 +749,7 @@ class WebsiteSale(http.Controller):
values['s2s_acquirers'] = [acq for acq in acquirers if acq.payment_flow == 's2s' and acq.registration_view_template_id]
values['tokens'] = request.env['payment.token'].search(
[('partner_id', '=', order.partner_id.id),
('acquirer_id', 'in', [acq.id for acq in values['s2s_acquirers']])])
('acquirer_id', 'in', acquirers.ids)])
return values
@@ -846,9 +846,11 @@ class WebsiteSale(http.Controller):
# we proceed the s2s payment
res = tx.confirm_sale_token()
# we then redirect to the page that validates the payment by giving it error if there's one
if res is not True:
return request.redirect('/shop/payment/validate?success=False&error=%s' % res)
return request.redirect('/shop/payment/validate?success=True')
if tx.state != 'authorized' or not tx.acquirer_id.capture_manually:
if res is not True:
return request.redirect('/shop/payment/validate?success=False&error=%s' % res)
return request.redirect('/shop/payment/validate?success=True')
return request.redirect('/shop/payment/validate')
@http.route('/shop/payment/get_status/<int:sale_order_id>', type='json', auth="public", website=True)
def payment_get_status(self, sale_order_id, **post):
+1
View File
@@ -7,6 +7,7 @@ from . import mail_compose_message
from . import product
from . import res_country
from . import res_partner
from . import payment
from . import sale_order
from . import ir_model_fields
from . import website
+25
View File
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from odoo import models
_logger = logging.getLogger(__name__)
class PaymentTransaction(models.Model):
_inherit = 'payment.transaction'
# --------------------------------------------------
# Sale management
# --------------------------------------------------
def _confirm_so(self):
"""Generate the invoice automatically when option is enabled from ecommerce setting"""
result = super(PaymentTransaction, self)._confirm_so()
IrConfigParameter = self.env['ir.config_parameter'].sudo()
if self.state == 'done' and IrConfigParameter.get_param('website_sale.automatic_invoice', default=False):
_logger.info('<%s> transaction completed, generating invoice for order %s (ID %s)', self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id)
self._generate_and_pay_invoice()
return result
+4
View File
@@ -1537,6 +1537,7 @@
<div t-att-class="'panel %s' % (
(order.payment_tx_id.state == 'pending' and 'panel-info') or
(order.payment_tx_id.state == 'done' and 'panel-success') or
(order.payment_tx_id.state == 'authorized' and 'panel-success') or
'panel-danger')">
<a groups="base.group_system" class="btn pull-right" target="_blank"
t-att-href="'/web#model=%s&amp;id=%s&amp;action=%s&amp;view_type=form' % ('payment.acquirer', order.payment_acquirer_id.id, 'payment.action_payment_acquirer')">
@@ -1555,6 +1556,9 @@
<t t-if="order.payment_tx_id.state == 'error'">
<t t-raw="order.payment_acquirer_id.error_msg"/>
</t>
<t t-if="order.payment_tx_id.state == 'authorized'">
Your payment has been authorized.
</t>
</div>
<div class="panel-body" t-if="order.payment_acquirer_id.post_msg">
<t t-raw="order.payment_acquirer_id.post_msg" />
@@ -65,6 +65,8 @@ class WebsiteSaleDelivery(WebsiteSale):
return {'status': order.delivery_rating_success,
'error_message': order.delivery_message,
'carrier_id': carrier_id,
'new_amount_delivery': order.delivery_price,
'new_amount_delivery': order.currency_id.round(order.delivery_price),
'new_amount_untaxed': order.amount_untaxed,
'new_amount_tax': order.amount_tax}
'new_amount_tax': order.amount_tax,
'new_amount_total': order.amount_total,
}
@@ -18,7 +18,7 @@ odoo.define('website_sale_delivery.checkout', function (require) {
$amount_delivery.text(result.new_amount_delivery);
$amount_untaxed.text(result.new_amount_untaxed);
$amount_tax.text(result.new_amount_tax);
$amount_total.text(result.new_amount_delivery + result.new_amount_untaxed + result.new_amount_tax);
$amount_total.text(result.new_amount_total);
$carrier_badge.children('span').text(result.new_amount_delivery);
$carrier_badge.removeClass('hidden');
$compute_badge.addClass('hidden');
+1 -1
View File
@@ -12,7 +12,7 @@ Yenthe Van Ginneken yenthe.vanginneken@vanroey.be https://github.com/Yenthe666
List of contributors:
Yenthe Van Ginneken yenthe.vanginneken@vanroey.be https://github.com/Yenthe666
Yenthe Van Ginneken yenthe.vanginneken@vanroey.be https://github.com/Yenthe666 (up to 2018-01-26)
Vincent Adriaensen vincent.adriaensen@vanroey.be https://github.com/vincentadriaensen
Pol Van Dingenen pol.vandingenen@vanroey.be https://github.com/Cyhexy
Ward Marissen Ward.Marissen@vanroey.be https://github.com/Wardm95
+1
View File
@@ -41,3 +41,4 @@ Mariano Fernandez mariano@vauxoo.com https://github.com/Batuto
Edgar Rivero edgar@vauxoo.com https://github.com/egrivero
Alexander Olivares alexander@vauxoo.com https://github.com/alxolivares
Jose Manuel Robles josemanuel@vauxoo.com https://github.com/keylor2906
Erick Birbe erick@vauxoo.com https://github.com/ebirbe
+11
View File
@@ -0,0 +1,11 @@
Belarus, 2017-01-17
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Sergei Ruzki sergei.ruzki@icode.by https://github.com/sergeiruzkiicode
+2 -2
View File
@@ -254,8 +254,8 @@
<field name="name">Belarus</field>
<field name="code">by</field>
<field file="base/static/img/country_flags/by.png" name="image" type="base64" />
<field name="currency_id" ref="BYR" />
<field eval="0" name="phone_code" />
<field name="currency_id" ref="BYN" />
<field eval="375" name="phone_code" />
</record>
<record id="bz" model="res.country">
<field name="name">Belize</field>
+10 -1
View File
@@ -516,10 +516,19 @@
<field name="symbol">BR</field>
<field name="rounding">0.01</field>
<field name="active" eval="False"/>
<field name="currency_unit_label">Ruble</field>
<field name="currency_unit_label">Ruble BYR</field>
<field name="currency_subunit_label">Kapeyka</field>
</record>
<record id="BYN" model="res.currency">
<field name="name">BYN</field>
<field name="symbol">Br</field>
<field name="rounding">0.01</field>
<field name="active" eval="False"/>
<field name="currency_unit_label">Rubles</field>
<field name="currency_subunit_label">Kopeks</field>
</record>
<record id="BZD" model="res.currency">
<field name="name">BZD</field>
<field name="symbol">BZ$</field>
@@ -349,6 +349,12 @@
<field name="rate">10228.19</field>
</record>
<record forcecreate="0" id="rateBYN" model="res.currency.rate">
<field name="currency_id" ref="BYN" />
<field name="name">2016-07-01</field>
<field name="rate">2.23151</field>
</record>
<record forcecreate="0" id="rateBZD" model="res.currency.rate">
<field name="currency_id" ref="BZD" />
<field name="name">2010-01-01</field>
+7 -1
View File
@@ -15,6 +15,7 @@ from odoo.exceptions import AccessDenied, AccessError, UserError, ValidationErro
from odoo.osv import expression
from odoo.service.db import check_super
from odoo.tools import partition, pycompat
from odoo.http import root
_logger = logging.getLogger(__name__)
@@ -372,6 +373,8 @@ class Users(models.Model):
if any(key.startswith('context_') or key in ('lang', 'tz') for key in values):
self.context_get.clear_cache(self)
if any(key in values for key in ['active'] + USER_PRIVATE_FIELDS):
# force deletion of all sessions for these users
root.session_store.delete_sessions_for_uids(self.ids)
db = self._cr.dbname
for id in self.ids:
self.__uid_cache[db].pop(id, None)
@@ -385,7 +388,10 @@ class Users(models.Model):
db = self._cr.dbname
for id in self.ids:
self.__uid_cache[db].pop(id, None)
return super(Users, self).unlink()
res = super(Users, self).unlink()
# force deletion of all sessions for these users
root.session_store.delete_sessions_for_uids(self.ids)
return res
@api.model
def name_search(self, name='', args=None, operator='ilike', limit=100):
+12 -4
View File
@@ -1029,7 +1029,6 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
self.db = db
self.uid = uid
self.login = login
self.password = password
request.uid = uid
request.disable_db = False
@@ -1044,7 +1043,6 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
"""
if not self.db or not self.uid:
raise SessionExpiredException("Session expired")
security.check(self.db, self.uid, self.password)
def logout(self, keep_db=False):
for k in list(self):
@@ -1057,7 +1055,6 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
self.setdefault("db", None)
self.setdefault("uid", None)
self.setdefault("login", None)
self.setdefault("password", None)
self.setdefault("context", {})
def get_context(self):
@@ -1271,6 +1268,17 @@ class DisableCacheMiddleware(object):
start_response(status, new_headers)
return self.app(environ, start_wrapped)
class OdooSessionStore(werkzeug.contrib.sessions.FilesystemSessionStore):
def delete_sessions_for_uids(self, uids):
# pretty expensive on large session stores, especially non-local!
uids = set(uids)
_logger.info('Deleting all HTTP sessions for UIDs %s', uids)
for sid in self.list():
s = self.get(sid)
if s.uid and s.uid in uids:
_logger.debug('Deleting session %s', sid)
self.delete(s)
class Root(object):
"""Root WSGI application for the OpenERP Web Client.
"""
@@ -1282,7 +1290,7 @@ class Root(object):
# Setup http sessions
path = odoo.tools.config.session_dir
_logger.debug('HTTP sessions stored in: %s', path)
return werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
return OdooSessionStore(path, session_class=OpenERPSession)
@lazy_property
def nodb_routing_map(self):