[MERGE] forward port branch saas-11.1 up to 8613724bfc

This commit is contained in:
Christophe Simonis
2018-01-25 17:38:41 +01:00
68 changed files with 709 additions and 281 deletions
+3 -1
View File
@@ -1338,6 +1338,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:
@@ -1723,6 +1724,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'])
@@ -1730,7 +1732,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:
@@ -68,6 +68,7 @@ class ResConfigSettings(models.TransientModel):
'company_id': self.company_id.id,
'chart_template_id': self.chart_template_id.id,
'transfer_account_id': self.chart_template_id.transfer_account_id.id,
'code_digits': self.chart_template_id.code_digits,
'sale_tax_rate': 15.0,
'purchase_tax_rate': 15.0,
'code_digits': self.chart_template_id.code_digits,
@@ -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
@@ -198,6 +198,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"/>
@@ -243,7 +244,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">
@@ -1,11 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import time
from datetime import date
from odoo import tools
from odoo import fields, tools
from odoo.modules.module import get_module_resource
from odoo.tests import common, Form
@@ -89,10 +85,10 @@ class TestLifoPrice(common.TransactionCase):
# Let us send some goods
out_form = Form(self.env['stock.picking'])
out_form.picking_type_id = self.env.ref('stock.picking_type_out')
out_form.scheduled_date = date.today().strftime('%Y-%m-%d')
with out_form.move_lines.new() as move:
move.product_id = product_lifo_icecream
move.quantity_done = 20.0
move.date_expected = fields.Date.context_today(self.env['stock.move.line'])
outgoing_lifo_shipment = out_form.save()
# I assign this outgoing shipment
@@ -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.
*
@@ -463,7 +463,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;
@@ -475,7 +475,7 @@ DebugManager.include({
self._controller.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;
}
}
@@ -6207,6 +6207,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
@@ -1538,6 +1538,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')">
@@ -1556,6 +1557,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):