[MERGE] forward port branch saas-11.1 up to 8613724bfc
This commit is contained in:
@@ -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':
|
||||
|
||||
@@ -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">&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">&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">&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">&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">&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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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> </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> </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">&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">&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">&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">&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">&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>
|
||||
|
||||
@@ -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 < 3) else 'hidden'">
|
||||
<li t-att-class="'' if (i < 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 < 3) else 'hidden'">
|
||||
<li t-att-class=" '' if (i < 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"/>
|
||||
|
||||
@@ -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)]}"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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 & 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 & Sign'"/></button>
|
||||
</div>
|
||||
</form>
|
||||
</t>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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">&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">&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">&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">&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">&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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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">&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">&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">&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">&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">&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">&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">&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">&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">&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>
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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&id=%s&action=%s&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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user