[MERGE] forward port branch 11.0 up to f5e7b86686
This commit is contained in:
@@ -332,7 +332,7 @@ class account_payment(models.Model):
|
||||
else:
|
||||
if self.payment_type == 'inbound':
|
||||
domain.append(('at_least_one_inbound', '=', True))
|
||||
else:
|
||||
elif self.payment_type == 'outbound':
|
||||
domain.append(('at_least_one_outbound', '=', True))
|
||||
return {'domain': domain, 'journal_types': set(journal_type)}
|
||||
|
||||
@@ -341,9 +341,16 @@ class account_payment(models.Model):
|
||||
jrnl_filters = self._compute_journal_domain_and_types()
|
||||
journal_types = jrnl_filters['journal_types']
|
||||
domain_on_types = [('type', 'in', list(journal_types))]
|
||||
if self.journal_id.type not in journal_types:
|
||||
self.journal_id = self.env['account.journal'].search(domain_on_types, limit=1)
|
||||
return {'domain': {'journal_id': jrnl_filters['domain'] + domain_on_types}}
|
||||
|
||||
journal_domain = jrnl_filters['domain'] + domain_on_types
|
||||
default_journal_id = self.env.context.get('default_journal_id')
|
||||
if not default_journal_id:
|
||||
if self.journal_id.type not in journal_types:
|
||||
self.journal_id = self.env['account.journal'].search(domain_on_types, limit=1)
|
||||
else:
|
||||
journal_domain = journal_domain.append(('id', '=', default_journal_id))
|
||||
|
||||
return {'domain': {'journal_id': journal_domain}}
|
||||
|
||||
@api.one
|
||||
@api.depends('invoice_ids', 'payment_type', 'partner_type', 'partner_id')
|
||||
@@ -374,6 +381,8 @@ class account_payment(models.Model):
|
||||
self.partner_type = 'customer'
|
||||
elif self.payment_type == 'outbound':
|
||||
self.partner_type = 'supplier'
|
||||
else:
|
||||
self.partner_type = False
|
||||
# Set payment method domain
|
||||
res = self._onchange_journal()
|
||||
if not res.get('domain', {}):
|
||||
@@ -505,7 +514,7 @@ class account_payment(models.Model):
|
||||
@api.multi
|
||||
def action_draft(self):
|
||||
return self.write({'state': 'draft'})
|
||||
|
||||
|
||||
def action_validate_invoice_payment(self):
|
||||
""" Posts a payment used to pay an invoice. This function only posts the
|
||||
payment by default but can be overridden to apply specific post or pre-processing.
|
||||
@@ -669,7 +678,7 @@ class account_payment(models.Model):
|
||||
for inv in invoice:
|
||||
if inv.move_id:
|
||||
name += inv.number + ', '
|
||||
name = name[:len(name)-2]
|
||||
name = name[:len(name)-2]
|
||||
return {
|
||||
'name': name,
|
||||
'account_id': self.destination_account_id.id,
|
||||
|
||||
@@ -168,10 +168,6 @@
|
||||
<label for="currency_exchange_journal_id" class="col-md-3 o_light_label"/>
|
||||
<field name="currency_exchange_journal_id"/>
|
||||
</div>
|
||||
<div class="row mt16">
|
||||
<label for="currency_exchange_journal_id" class="col-md-3 o_light_label"/>
|
||||
<field name="currency_exchange_journal_id"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from odoo import api, fields, models
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DF
|
||||
|
||||
from odoo.addons import decimal_precision as dp
|
||||
|
||||
@@ -32,7 +32,7 @@ class AccountRegisterPayments(models.TransientModel):
|
||||
res = super(AccountRegisterPayments, self)._prepare_payment_vals(invoices)
|
||||
if self.payment_method_id == self.env.ref('account_check_printing.account_payment_method_check'):
|
||||
res.update({
|
||||
'check_amount_in_words': self.check_amount_in_words,
|
||||
'check_amount_in_words': self.currency_id.amount_to_text(res['amount']) if self.multi else self.check_amount_in_words,
|
||||
'check_manual_sequencing': self.check_manual_sequencing,
|
||||
})
|
||||
return res
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import barcodes
|
||||
from . import barcode_events_mixin
|
||||
from . import ir_http
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class IrHttp(models.AbstractModel):
|
||||
_inherit = 'ir.http'
|
||||
|
||||
def session_info(self):
|
||||
res = super(IrHttp, self).session_info()
|
||||
res['max_time_between_keys_in_ms'] = int(
|
||||
self.env['ir.config_parameter'].sudo().get_param('barcode.max_time_between_keys_in_ms', default='55'))
|
||||
return res
|
||||
@@ -3,6 +3,7 @@ odoo.define('barcodes.BarcodeEvents', function(require) {
|
||||
|
||||
var core = require('web.core');
|
||||
var mixins = require('web.mixins');
|
||||
var session = require('web.session');
|
||||
|
||||
|
||||
// For IE >= 9, use this, new CustomEvent(), instead of new Event()
|
||||
@@ -26,7 +27,7 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, {
|
||||
suffix: /[\n\r\t]+/,
|
||||
// Keys from a barcode scanner are usually processed as quick as possible,
|
||||
// but some scanners can use an intercharacter delay (we support <= 50 ms)
|
||||
max_time_between_keys_in_ms: 55,
|
||||
max_time_between_keys_in_ms: session.max_time_between_keys_in_ms || 55,
|
||||
|
||||
init: function() {
|
||||
mixins.PropertiesMixin.init.call(this);
|
||||
|
||||
@@ -96,6 +96,7 @@ class EventMailScheduler(models.Model):
|
||||
|
||||
@api.one
|
||||
def execute(self):
|
||||
now = fields.Datetime.now()
|
||||
if self.interval_type == 'after_sub':
|
||||
# update registration lines
|
||||
lines = [
|
||||
@@ -105,9 +106,10 @@ class EventMailScheduler(models.Model):
|
||||
if lines:
|
||||
self.write({'mail_registration_ids': lines})
|
||||
# execute scheduler on registrations
|
||||
self.mail_registration_ids.filtered(lambda reg: reg.scheduled_date and reg.scheduled_date <= datetime.strftime(fields.datetime.now(), tools.DEFAULT_SERVER_DATETIME_FORMAT)).execute()
|
||||
self.mail_registration_ids.filtered(lambda reg: reg.scheduled_date and reg.scheduled_date <= now).execute()
|
||||
else:
|
||||
if not self.mail_sent:
|
||||
# Do not send emails if the mailing was scheduled before the event but the event is over
|
||||
if not self.mail_sent and (self.interval_type != 'before_event' or self.event_id.date_end > now):
|
||||
self.event_id.mail_attendees(self.template_id.id)
|
||||
self.write({'mail_sent': True})
|
||||
return True
|
||||
|
||||
@@ -290,12 +290,14 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- TODO remove me in master -->
|
||||
<record id="hr_employee_action_subordinate_hierachy" model="ir.actions.act_window">
|
||||
<field name="name">Subordinate Hierarchy</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">hr.employee</field>
|
||||
<field name="domain">[('id','in',active_ids)]</field>
|
||||
<field name="view_type">tree</field>
|
||||
<field name="domain">['|', ('id','in',active_ids), ('parent_id', 'in', active_ids)]</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="view_partner_tree2"/>
|
||||
<field name="binding_model_id" ref="hr.model_hr_employee"/>
|
||||
</record>
|
||||
|
||||
@@ -67,7 +67,7 @@ class Task(models.Model):
|
||||
if 'project_id' in values:
|
||||
project_id = values.get('project_id')
|
||||
# a timesheet must have an analytic account (and a project)
|
||||
if not project_id:
|
||||
if self and not project_id:
|
||||
raise UserError(_('This task must have a project since they are linked to timesheets.'))
|
||||
self.sudo().mapped('timesheet_ids').write({
|
||||
'project_id': project_id,
|
||||
|
||||
@@ -418,6 +418,8 @@ class IrHttp(models.AbstractModel):
|
||||
try:
|
||||
_, path = rule.build(arguments)
|
||||
assert path is not None
|
||||
except odoo.exceptions.MissingError:
|
||||
return cls._handle_exception(werkzeug.exceptions.NotFound())
|
||||
except Exception as e:
|
||||
return cls._handle_exception(e)
|
||||
|
||||
|
||||
@@ -17,9 +17,11 @@
|
||||
</div>
|
||||
|
||||
<t t-extend="DashboardApps">
|
||||
<t t-jquery=".o_web_settings_dashboard_pills" t-operation="append">
|
||||
<a class="pull-right" t-att-href="widget.data.url" target="_blank">
|
||||
<i class="fa fa-money fa-2x text-muted"/> In-App Purchases</a>
|
||||
<t t-jquery=".o_web_settings_dashboard_pills" t-operation="after">
|
||||
<div class="text-center" style="display: inline-block">
|
||||
<a t-att-href="widget.data.url" target="_blank">
|
||||
<i class="fa fa-money fa-2x text-muted"/> In-App Purchases</a>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
|
||||
@@ -62,7 +62,8 @@
|
||||
<strong><span t-esc="h['rule_category']"/></strong>
|
||||
</td>
|
||||
<td>
|
||||
<strong><span t-esc="format(h['total'], 'monetary', o.company_id.currency_id)"/></strong>
|
||||
<strong><span t-esc="h['total']"
|
||||
t-esc-options='{"widget": "monetary", "display_currency": o.company_id.currency_id}'/></strong>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
@@ -75,7 +76,8 @@
|
||||
<span t-esc="'..'*h['level']"/><span t-esc="h['rule_category']"/>
|
||||
</td>
|
||||
<td>
|
||||
<span t-esc="format(h['total'], 'monetary', o.company_id.currency_id)"/>
|
||||
<span t-esc="h['total']"
|
||||
t-esc-options='{"widget": "monetary", "display_currency": o.company_id.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
|
||||
@@ -300,7 +300,7 @@ class MailTemplate(models.Model):
|
||||
|
||||
def _process_link(url):
|
||||
new_url = urls.url_parse(url)
|
||||
if new_url.scheme and new_url.netloc:
|
||||
if new_url.scheme and (new_url.netloc or new_url.scheme == 'mailto'):
|
||||
return url
|
||||
return new_url.replace(scheme=base.scheme, netloc=base.netloc).to_url()
|
||||
|
||||
|
||||
@@ -890,7 +890,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
||||
var def = window.Notification && window.Notification.requestPermission();
|
||||
if (def) {
|
||||
def.then(function (value) {
|
||||
if (value === 'denied') {
|
||||
if (value !== 'granted') {
|
||||
utils.send_notification(self, _t('Permission denied'),
|
||||
_t('Odoo will not have the permission to send native notifications on this device.'));
|
||||
} else {
|
||||
|
||||
@@ -368,7 +368,7 @@ class MaintenanceTeam(models.Model):
|
||||
_name = 'maintenance.team'
|
||||
_description = 'Maintenance Teams'
|
||||
|
||||
name = fields.Char(required=True)
|
||||
name = fields.Char(required=True, translate=True)
|
||||
member_ids = fields.Many2many('res.users', 'maintenance_team_users_rel', string="Team Members")
|
||||
color = fields.Integer("Color Index", default=0)
|
||||
request_ids = fields.One2many('maintenance.request', 'maintenance_team_id', copy=False)
|
||||
|
||||
@@ -323,20 +323,20 @@ snippets_editor.Class.include({
|
||||
$body.removeClass(all_classes).addClass(theme_params.className);
|
||||
|
||||
var $old_layout = $editable_area.find(".o_layout");
|
||||
// This wrapper structure is the only way to have a responsive and
|
||||
// centered fixed-width content column on all mail clients
|
||||
var $new_wrapper, $new_wrapper_content;
|
||||
|
||||
if (theme_params.nowrap) {
|
||||
$new_wrapper = $new_wrapper_content = $("<div/>", {"class": "oe_structure"});
|
||||
}
|
||||
else {
|
||||
// This wrapper structure is the only way to have a responsive and
|
||||
// centered fixed-width content column on all mail clients
|
||||
$new_wrapper = $('<table/>', {class: 'o_mail_wrapper'});
|
||||
$new_wrapper_content = $("<td/>", {class: 'o_mail_no_resize o_mail_wrapper_td oe_structure'});
|
||||
$new_wrapper_content = $("<td/>", {class: 'o_mail_no_resize o_mail_no_colorpicker o_mail_wrapper_td oe_structure'});
|
||||
$new_wrapper.append($('<tr/>').append(
|
||||
$("<td/>", {class: 'o_mail_no_resize'}),
|
||||
$("<td/>", {class: 'o_mail_no_resize o_not_editable', contenteditable: 'false'}),
|
||||
$new_wrapper_content,
|
||||
$("<td/>", {class: 'o_mail_no_resize'})
|
||||
$("<td/>", {class: 'o_mail_no_resize o_not_editable', contenteditable: 'false'})
|
||||
));
|
||||
}
|
||||
var $new_layout = $("<div/>", {"class": "o_layout " + theme_params.className}).append($new_wrapper);
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
<t t-raw="0"/>
|
||||
|
||||
<div data-js="width-x"
|
||||
data-selector="img, .mv, .col_mv, td:not(.o_mail_no_resize), th">
|
||||
</div>
|
||||
data-selector="img, .mv, .col_mv, td, th"
|
||||
data-exclude=".o_mail_no_resize, .o_mail_wrapper_td"/>
|
||||
|
||||
<div data-js="table_item" data-selector="td, th"/>
|
||||
<div data-js="table_item"
|
||||
data-selector="td, th"
|
||||
data-exclude=".o_mail_wrapper_td"/>
|
||||
|
||||
<div data-js="table_row"
|
||||
data-selector="tr:has(> .row), tr:has(> .col_mv)"
|
||||
@@ -18,11 +20,13 @@
|
||||
|
||||
<div data-js="table_column"
|
||||
data-selector=".col>td, .col>th"
|
||||
data-exclude=".o_mail_wrapper_td"
|
||||
data-drop-near=".col>td, .col>th">
|
||||
</div>
|
||||
|
||||
<div data-js="table_column_mv"
|
||||
data-selector=".col_mv, td, th"
|
||||
data-exclude=".o_mail_wrapper_td"
|
||||
data-drop-near=".col_mv, td, th">
|
||||
</div>
|
||||
|
||||
@@ -33,19 +37,22 @@
|
||||
</div>
|
||||
|
||||
<div data-js="resize"
|
||||
data-selector="[data-oe-field='body_html'] > div:not(.o_layout), [data-oe-field='body_html'] .oe_structure > div">
|
||||
data-selector="[data-oe-field='body_html'] > div:not(.o_layout), [data-oe-field='body_html'] .oe_structure > div"
|
||||
data-exclude=".o_mail_no_resize, .o_mail_wrapper_td">
|
||||
</div>
|
||||
|
||||
<div data-js="margin-y"
|
||||
data-selector="[data-oe-field='body_html'] > div:not(.o_layout), [data-oe-field='body_html'] .oe_structure > div, td:not(.o_mail_no_resize), th">
|
||||
data-selector="[data-oe-field='body_html'] > div:not(.o_layout), [data-oe-field='body_html'] .oe_structure > div, td, th"
|
||||
data-exclude=".o_mail_no_resize, .o_mail_wrapper_td">
|
||||
</div>
|
||||
|
||||
<div data-js="colorpicker"
|
||||
data-selector="
|
||||
[data-oe-field='body_html'] > div:not(.o_layout):not(.o_mail_no_colorpicker),
|
||||
[data-oe-field='body_html'] .oe_structure > div:not(.o_mail_no_colorpicker),
|
||||
[data-oe-field='body_html'] > div:not(.o_layout),
|
||||
[data-oe-field='body_html'] .oe_structure > div,
|
||||
td, th
|
||||
">
|
||||
"
|
||||
data-exclude=".o_mail_no_colorpicker">
|
||||
<li class="dropdown-submenu">
|
||||
<a tabindex="-1" href="#"><i class="fa fa-eyedropper"/>Background Color</a>
|
||||
<ul class="dropdown-menu">
|
||||
|
||||
@@ -151,7 +151,7 @@ class StockMove(models.Model):
|
||||
# all grouped in the same picking.
|
||||
if not self.picking_type_id:
|
||||
return self
|
||||
bom = self.env['mrp.bom'].sudo()._bom_find(product=self.product_id)
|
||||
bom = self.env['mrp.bom'].sudo()._bom_find(product=self.product_id, company_id=self.company_id.id)
|
||||
if not bom or bom.type != 'phantom':
|
||||
return self
|
||||
phantom_moves = self.env['stock.move']
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<button name="button_unreserve" type="object" string="Unreserve" attrs="{'invisible': [('unreserve_visible', '=', False)]}"/>
|
||||
<span class="label label-danger" attrs="{'invisible': ['|',('availability', 'in', ('assigned', 'none')), ('state', 'not in', ('confirmed','progress'))]}">Raw materials not available!</span>
|
||||
<field name="state" widget="statusbar" statusbar_visible="confirmed,progress,done"/>
|
||||
<button name="action_toggle_is_locked" attrs="{'invisible': ['|', '|', ('state', '=', 'cancel'), ('id', '=', False), ('is_locked', '=', False)]}" string="Unlock" groups="mrp.group_mrp_manager" type="object" help="If the manufacturing order is unlocked you can add to the initial demand."/>
|
||||
<button name="action_toggle_is_locked" attrs="{'invisible': ['|', '|', ('state', '=', 'cancel'), ('id', '=', False), ('is_locked', '=', False)]}" string="Unlock" groups="mrp.group_mrp_manager" type="object" help="Unlock the manufacturing order to correct what has been consumed or produced."/>
|
||||
<button name="action_toggle_is_locked" attrs="{'invisible': [('is_locked', '=', True)]}" string="Lock" class="oe_highlight" groups="mrp.group_mrp_manager" type="object"/>
|
||||
</header>
|
||||
<sheet>
|
||||
|
||||
@@ -132,7 +132,7 @@ class PadCommon(models.AbstractModel):
|
||||
|
||||
# Update the `pad_content_field` if the pad is modified
|
||||
for k, v in list(vals.items()):
|
||||
field = self._fields[k]
|
||||
field = self._fields.get(k)
|
||||
if hasattr(field, 'pad_content_field'):
|
||||
vals[field.pad_content_field] = self.pad_get_content(v)
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ class AcquirerSips(models.Model):
|
||||
if self.environment == 'prod':
|
||||
# For production environment, key version 2 is required
|
||||
merchant_id = getattr(self, 'sips_merchant_id')
|
||||
key_version = '2'
|
||||
key_version = self.env['ir.config_parameter'].sudo().get_param('sips.key_version', '2')
|
||||
else:
|
||||
# Test key provided by Atos Wordline works only with version 1
|
||||
merchant_id = '002001000000001'
|
||||
|
||||
@@ -1018,7 +1018,10 @@ class ReportSaleDetails(models.AbstractModel):
|
||||
for tax in line_taxes['taxes']:
|
||||
taxes.setdefault(tax['id'], {'name': tax['name'], 'tax_amount':0.0, 'base_amount':0.0})
|
||||
taxes[tax['id']]['tax_amount'] += tax['amount']
|
||||
taxes[tax['id']]['base_amount'] += line.price_subtotal
|
||||
taxes[tax['id']]['base_amount'] += tax['base']
|
||||
else:
|
||||
taxes.setdefault(0, {'name': _('No Taxes'), 'tax_amount':0.0, 'base_amount':0.0})
|
||||
taxes[0]['base_amount'] += line.price_subtotal_incl
|
||||
|
||||
st_line_ids = self.env["account.bank.statement.line"].search([('pos_statement_id', 'in', orders.ids)]).ids
|
||||
if st_line_ids:
|
||||
|
||||
@@ -331,7 +331,7 @@ exports.PosModel = Backbone.Model.extend({
|
||||
ids: function(self){ return [self.config.currency_id[0]]; },
|
||||
loaded: function(self, currencies){
|
||||
self.currency = currencies[0];
|
||||
if (self.currency.rounding > 0) {
|
||||
if (self.currency.rounding > 0 && self.currency.rounding < 1) {
|
||||
self.currency.decimals = Math.ceil(Math.log(1.0 / self.currency.rounding) / Math.log(10));
|
||||
} else {
|
||||
self.currency.decimals = 0;
|
||||
@@ -528,7 +528,7 @@ exports.PosModel = Backbone.Model.extend({
|
||||
|
||||
var fields = typeof model.fields === 'function' ? model.fields(self,tmp) : model.fields;
|
||||
var domain = typeof model.domain === 'function' ? model.domain(self,tmp) : model.domain;
|
||||
var context = typeof model.context === 'function' ? model.context(self,tmp) : model.context;
|
||||
var context = typeof model.context === 'function' ? model.context(self,tmp) : model.context || {};
|
||||
var ids = typeof model.ids === 'function' ? model.ids(self,tmp) : model.ids;
|
||||
var order = typeof model.order === 'function' ? model.order(self,tmp): model.order;
|
||||
progress += progress_step;
|
||||
@@ -536,7 +536,7 @@ exports.PosModel = Backbone.Model.extend({
|
||||
if( model.model ){
|
||||
var params = {
|
||||
model: model.model,
|
||||
context: context,
|
||||
context: _.extend(context, session.user_context || {}),
|
||||
};
|
||||
|
||||
if (model.ids) {
|
||||
|
||||
@@ -94,13 +94,11 @@ class AccountInvoice(models.Model):
|
||||
self.purchase_id = False
|
||||
return {}
|
||||
|
||||
@api.onchange('currency_id', 'date_invoice')
|
||||
@api.onchange('currency_id')
|
||||
def _onchange_currency_id(self):
|
||||
if self.currency_id:
|
||||
for line in self.invoice_line_ids.filtered(lambda r: r.purchase_line_id):
|
||||
if line.purchase_id.currency_id != self.currency_id:
|
||||
line.price_unit = line.purchase_id.currency_id.with_context(date=self.date_invoice)\
|
||||
.compute(line.purchase_line_id.price_unit, self.currency_id, round=False)
|
||||
line.price_unit = line.purchase_id.currency_id.with_context(date=self.date_invoice).compute(line.purchase_line_id.price_unit, self.currency_id, round=False)
|
||||
|
||||
@api.onchange('invoice_line_ids')
|
||||
def _onchange_origin(self):
|
||||
|
||||
@@ -128,6 +128,8 @@ class ResourceCalendar(models.Model):
|
||||
)
|
||||
|
||||
def _interval_and(self, interval, interval_dst):
|
||||
if interval.start_datetime > interval_dst.end_datetime or interval.end_datetime < interval_dst.start_datetime:
|
||||
return None
|
||||
return self._interval_obj(
|
||||
interval.start_datetime > interval_dst.start_datetime and interval.start_datetime or interval_dst.start_datetime,
|
||||
interval.end_datetime < interval_dst.end_datetime and interval.end_datetime or interval_dst.end_datetime,
|
||||
@@ -392,10 +394,10 @@ class ResourceCalendar(models.Model):
|
||||
start_datetime=datetime.datetime.combine(day_date, start_time),
|
||||
end_datetime=datetime.datetime.combine(day_date, end_time))
|
||||
|
||||
final_intervals = [
|
||||
self._interval_and(leave_interval, work_interval)
|
||||
for leave_interval in leaves_intervals
|
||||
for work_interval in working_intervals]
|
||||
final_intervals = [i for i in
|
||||
[self._interval_and(leave_interval, work_interval)
|
||||
for leave_interval in leaves_intervals
|
||||
for work_interval in working_intervals] if i]
|
||||
|
||||
# adapt tz
|
||||
return [self._interval_new(
|
||||
|
||||
@@ -55,6 +55,12 @@ class TestIntervals(TestResourceCommon):
|
||||
# Third interval: 04, 17-21, 18-19 being inside 17-21
|
||||
self.assertEqual(cleaned_intervals[2][:2], (Datetime.from_string('2013-02-04 17:00:00'), Datetime.from_string('2013-02-04 21:00:00')))
|
||||
|
||||
def test_interval_and(self):
|
||||
self.assertEqual(self.env['resource.calendar']._interval_and(self.intervals[0], self.intervals[1]),
|
||||
self.calendar._interval_new(Datetime.from_string('2013-02-04 09:00:00'), Datetime.from_string('2013-02-04 11:00:00')))
|
||||
self.assertEqual(self.env['resource.calendar']._interval_and(self.intervals[2], self.intervals[3]),
|
||||
None)
|
||||
|
||||
def test_interval_remove(self):
|
||||
working_interval = self.calendar._interval_new(Datetime.from_string('2013-02-04 08:00:00'), Datetime.from_string('2013-02-04 18:00:00'))
|
||||
result = self.env['resource.calendar']._interval_remove_leaves(working_interval, self.intervals)
|
||||
|
||||
@@ -830,8 +830,9 @@ class SaleOrderLine(models.Model):
|
||||
# Prevent writing on a locked SO.
|
||||
protected_fields = self._get_protected_fields()
|
||||
if 'done' in self.mapped('order_id.state') and any(f in values.keys() for f in protected_fields):
|
||||
protected_fields_modified = list(set(protected_fields) & set(values.keys()))
|
||||
fields = self.env['ir.model.fields'].search([
|
||||
('name', 'in', protected_fields), ('model', '=', self._name)
|
||||
('name', 'in', protected_fields_modified), ('model', '=', self._name)
|
||||
])
|
||||
raise UserError(
|
||||
_('It is forbidden to modify the following fields in a locked order:\n%s')
|
||||
|
||||
@@ -35,7 +35,7 @@ class SendSMS(models.TransientModel):
|
||||
|
||||
def _sms_sanitization(self, partner, field_name):
|
||||
number = partner[field_name]
|
||||
if _sms_phonenumbers_lib_imported:
|
||||
if number and _sms_phonenumbers_lib_imported:
|
||||
country = self._phone_get_country(partner)
|
||||
country_code = country.code if country else None
|
||||
try:
|
||||
|
||||
@@ -246,7 +246,7 @@
|
||||
<field name="backorder_id" readonly="1" attrs="{'invisible': [('backorder_id','=',False)]}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="scheduled_date" attrs="{'readonly': [('id', '=', False)]}"/>
|
||||
<field name="scheduled_date" attrs="{'readonly': [('id', '=', False)], 'required': [('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">
|
||||
|
||||
@@ -43,9 +43,9 @@
|
||||
<field name="inherit_id" ref="stock.view_stock_return_picking_form"/>
|
||||
<field name="model">stock.return.picking</field>
|
||||
<field name="arch" type="xml">
|
||||
<field name="uom_id" position="after">
|
||||
<xpath expr="//field[@name='product_return_moves']/tree" position="inside">
|
||||
<field name="to_refund"/>
|
||||
</field>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -1082,6 +1082,7 @@ var UrlWidget = InputField.extend({
|
||||
_renderReadonly: function () {
|
||||
this.$el.text(this.attrs.text || this.value)
|
||||
.addClass('o_form_uri o_text_overflow')
|
||||
.attr('target', '_blank')
|
||||
.attr('href', this.value);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1104,6 +1104,7 @@ var FieldX2Many = AbstractField.extend({
|
||||
* @param {OdooEvent} event
|
||||
*/
|
||||
_onResequence: function (event) {
|
||||
event.stopPropagation();
|
||||
var self = this;
|
||||
this.trigger_up('freeze_order', {id: this.value.id});
|
||||
var rowIDs = event.data.rowIDs.slice();
|
||||
|
||||
@@ -20,7 +20,8 @@ var FieldTimezoneMismatch = FieldSelection.extend({
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this._datetime = setInterval(this._renderDateTimeTimezone.bind(this), 1000);
|
||||
var interval = navigator.platform.toUpperCase().indexOf('MAC') >= 0 ? 60000 : 1000;
|
||||
this._datetime = setInterval(this._renderDateTimeTimezone.bind(this), interval);
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -103,7 +103,7 @@ var KanbanColumn = Widget.extend({
|
||||
// should find a way to use the touch events to make sortable work.
|
||||
this.$el.sortable({
|
||||
connectWith: '.o_kanban_group',
|
||||
containment: this.draggable ? '.o_kanban_view' : 'parent',
|
||||
containment: this.draggable ? false : 'parent',
|
||||
revert: 0,
|
||||
delay: 0,
|
||||
items: '> .o_kanban_record:not(.o_updating)',
|
||||
|
||||
@@ -86,39 +86,37 @@ div.o_boolean_toggle {
|
||||
width: 32px !important;
|
||||
height: 18px;
|
||||
|
||||
& > label {
|
||||
& > input {
|
||||
> input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& > .slider {
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #a0a0a0;
|
||||
border-radius: 24px;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #a0a0a0;
|
||||
border-radius: 24px;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
content: "";
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
& > input:checked + .slider {
|
||||
background-color: @odoo-brand-optional;
|
||||
> input:checked + .slider {
|
||||
background-color: @odoo-brand-optional;
|
||||
|
||||
&:before {
|
||||
.translate(14px; 0px);
|
||||
}
|
||||
&:before {
|
||||
.translate(14px; 0px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1110,7 +1110,7 @@ QUnit.module('basic_fields', {
|
||||
QUnit.module('UrlWidget');
|
||||
|
||||
QUnit.test('url widget in form view', function (assert) {
|
||||
assert.expect(8);
|
||||
assert.expect(9);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
@@ -1130,6 +1130,8 @@ QUnit.module('basic_fields', {
|
||||
"should have a anchor with correct classes");
|
||||
assert.strictEqual(form.$('a.o_form_uri.o_field_widget.o_text_overflow').attr('href'), 'yop',
|
||||
"should have proper href link");
|
||||
assert.strictEqual(form.$('a.o_form_uri.o_field_widget.o_text_overflow').attr('target'), '_blank',
|
||||
"should have target attribute set to _blank");
|
||||
assert.strictEqual(form.$('a.o_form_uri.o_field_widget.o_text_overflow').text(), 'yop',
|
||||
"the value should be displayed properly");
|
||||
|
||||
|
||||
@@ -1321,6 +1321,93 @@ QUnit.module('relational_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('X2Many sequence list in modal', function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
this.data.partner.fields.sequence = {string: 'Sequence', type: 'integer'};
|
||||
this.data.partner.records[0].sequence = 1;
|
||||
this.data.partner.records[1].sequence = 2;
|
||||
this.data.partner.onchanges = {
|
||||
sequence: function (obj) {
|
||||
if (obj.id === 2) {
|
||||
obj.sequence = 1;
|
||||
assert.step('onchange sequence');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
this.data.product.fields.turtle_ids = {string: 'Turtles', type: 'one2many', relation: 'turtle'};
|
||||
this.data.product.records[0].turtle_ids = [1];
|
||||
|
||||
this.data.turtle.fields.partner_types_ids = {string: "Partner", type: "one2many", relation: 'partner'};
|
||||
this.data.turtle.fields.type_id = {string: "Partner Type", type: "many2one", relation: 'partner_type'};
|
||||
|
||||
this.data.partner_type.fields.partner_ids = {string: "Partner", type: "one2many", relation: 'partner'};
|
||||
this.data.partner_type.records[0].partner_ids = [1,2];
|
||||
|
||||
testUtils.createAsyncView({
|
||||
View: FormView,
|
||||
model: 'product',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<field name="name"/>' +
|
||||
'<field name="turtle_ids" widget="one2many_list">' +
|
||||
'<tree string="Turtles" editable="bottom">' +
|
||||
'<field name="type_id"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
archs: {
|
||||
'partner_type,false,form': '<form><field name="partner_ids"/></form>',
|
||||
'partner,false,list': '<tree string="Vendors">' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="sequence" widget="handle"/>' +
|
||||
'</tree>',
|
||||
},
|
||||
res_id: 37,
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/web/dataset/call_kw/product/read') {
|
||||
return $.when([{id: 37, name: 'xphone', display_name: 'leonardo', turtle_ids: [1]}]);
|
||||
}
|
||||
if (route === '/web/dataset/call_kw/turtle/read') {
|
||||
return $.when([{id: 1, type_id: [12, 'gold']}]);
|
||||
}
|
||||
if (route === '/web/dataset/call_kw/partner_type/get_formview_id') {
|
||||
return $.when(false)
|
||||
}
|
||||
if (route === '/web/dataset/call_kw/partner_type/read') {
|
||||
return $.when([{id: 12, partner_ids: [1,2], display_name: 'gold'}])
|
||||
}
|
||||
if (route === '/web/dataset/call_kw/partner_type/write') {
|
||||
assert.step('partner_type write');
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
}).then(function(form) {
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
form.$('.o_data_cell').click();
|
||||
form.$('.o_external_button').click();
|
||||
|
||||
var $modal = $('.modal-dialog');
|
||||
assert.equal($modal.length, 1,
|
||||
'There should be 1 modal opened');
|
||||
|
||||
var $handles = $modal.find('.ui-sortable-handle');
|
||||
assert.equal($handles.length, 2,
|
||||
'There should be 2 sequence handlers');
|
||||
|
||||
testUtils.dragAndDrop($handles.eq(1), $modal.find('tbody tr').first());
|
||||
|
||||
// Saving the modal and then the original model
|
||||
$modal.find('.modal-footer .btn-primary').click();
|
||||
form.$buttons.find('.o_form_button_save').click();
|
||||
|
||||
assert.verifySteps(['onchange sequence', 'partner_type write']);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('autocompletion in a many2one, in form view with a domain', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
|
||||
@@ -409,7 +409,19 @@ var FieldTextHtml = AbstractField.extend({
|
||||
var layoutInfo = self.editor.rte.editable().data('layoutInfo');
|
||||
$.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo, false);
|
||||
}
|
||||
var $ancestors = self.$iframe.filter(':not(:visible)').parentsUntil(':visible').addBack();
|
||||
var ancestorsStyle = [];
|
||||
// temporarily force displaying iframe (needed for firefox)
|
||||
_.each($ancestors, function (el) {
|
||||
var $el = $(el);
|
||||
ancestorsStyle.unshift($el.attr('style') || null);
|
||||
$el.css({display: 'initial', visibility: 'hidden', height: 1});
|
||||
});
|
||||
self.editor.snippetsMenu && self.editor.snippetsMenu.cleanForSave();
|
||||
_.each($ancestors, function (el) {
|
||||
var $el = $(el);
|
||||
$el.attr('style', ancestorsStyle.pop());
|
||||
});
|
||||
self._setValue(self.$content.html());
|
||||
});
|
||||
},
|
||||
|
||||
@@ -281,8 +281,10 @@ var RTEWidget = Widget.extend({
|
||||
.each(function () {
|
||||
var $node = $(this);
|
||||
|
||||
// fallback for firefox iframe display:none see https://github.com/odoo/odoo/pull/22610
|
||||
var computedStyles = window.getComputedStyle(this) || window.parent.getComputedStyle(this);
|
||||
// add class to display inline-block for empty t-field
|
||||
if (window.getComputedStyle(this).display === 'inline' && $node.data('oe-type') !== 'image') {
|
||||
if (computedStyles.display === 'inline' && $node.data('oe-type') !== 'image') {
|
||||
$node.addClass('o_is_inline_editable');
|
||||
}
|
||||
});
|
||||
@@ -651,26 +653,16 @@ var RTEWidget = Widget.extend({
|
||||
self.historyRecordUndo($target, 'activate', true);
|
||||
});
|
||||
|
||||
// To Fix Google Chrome Tripleclick Issue, which selects the ending
|
||||
// whitespace characters (so Tripleclicking then typing text will remove
|
||||
// the whole paragraph instead of its content).
|
||||
// http://stackoverflow.com/questions/38467334/why-does-google-chrome-always-add-space-after-selected-text
|
||||
if ($.browser.chrome === true && ev.originalEvent.detail === 3) {
|
||||
var currentSelection = range.create();
|
||||
if (currentSelection.sc.parentNode === currentSelection.ec) {
|
||||
_selectSC(currentSelection);
|
||||
} else if (currentSelection.eo === 0) {
|
||||
var $hasNext = $(currentSelection.sc).parent();
|
||||
while (!$hasNext.next().length && !$hasNext.is('body')) {
|
||||
$hasNext = $hasNext.parent();
|
||||
}
|
||||
if ($hasNext.next()[0] === currentSelection.ec) {
|
||||
_selectSC(currentSelection);
|
||||
}
|
||||
}
|
||||
}
|
||||
function _selectSC(selection) {
|
||||
range.create(selection.sc, selection.so, selection.sc, selection.sc.length).select();
|
||||
// Browsers select different content from one to another after a
|
||||
// triple click (especially: if triple-clicking on a paragraph on
|
||||
// Chrome, blank characters of the element following the paragraph are
|
||||
// selected too)
|
||||
//
|
||||
// The triple click behavior is reimplemented for all browsers here
|
||||
if (ev.originalEvent.detail === 3) {
|
||||
// Select the whole content inside the deepest DOM element that was
|
||||
// triple-clicked
|
||||
range.create(ev.target, 0, ev.target, ev.target.childNodes.length).select();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -417,6 +417,9 @@ dom.isImg = function (node) {
|
||||
};
|
||||
var fn_is_forbidden_node = dom.isForbiddenNode || function () {};
|
||||
dom.isForbiddenNode = function (node) {
|
||||
if (node.tagName === "BR") {
|
||||
return false;
|
||||
}
|
||||
return fn_is_forbidden_node(node) || $(node).is(".media_iframe_video");
|
||||
};
|
||||
var fn_is_img_font = dom.isImgFont || function () {};
|
||||
|
||||
@@ -694,10 +694,16 @@ dom.isRemovableEmptyNode = function (node) {
|
||||
dom.isForbiddenNode = function (node) {
|
||||
return node.tagName === "BR" || $(node).is(".fa, img");
|
||||
};
|
||||
dom.listBetween = function (sc, ec) {
|
||||
/**
|
||||
* @todo 'so' and 'eo' were added as a bugfix and are not given everytime. They
|
||||
* however should be as the function may be wrong without them (for example,
|
||||
* when asking the list between an element and its parent, as there is no path
|
||||
* from the beginning of the former to the beginning of the later).
|
||||
*/
|
||||
dom.listBetween = function (sc, ec, so, eo) {
|
||||
var nodes = [];
|
||||
var ancestor = dom.commonAncestor(sc, ec);
|
||||
dom.walkPoint({'node': sc, 'offset': 0}, {'node': ec, 'offset': 0}, function (point) {
|
||||
dom.walkPoint({'node': sc, 'offset': so || 0}, {'node': ec, 'offset': eo || 0}, function (point) {
|
||||
if (ancestor !== point.node || ancestor === sc || ancestor === ec) {
|
||||
nodes.push(point.node);
|
||||
}
|
||||
@@ -1869,7 +1875,7 @@ $.summernote.pluginEvents.formatBlock = function (event, editor, layoutInfo, sTa
|
||||
}
|
||||
|
||||
// fix by odoo because if you select a style in a li with no p tag all the ul is wrapped by the style tag
|
||||
var nodes = dom.listBetween(r.sc, r.ec);
|
||||
var nodes = dom.listBetween(r.sc, r.ec, r.so, r.eo);
|
||||
for (var i=0; i<nodes.length; i++) {
|
||||
if (dom.isBR(nodes[i]) || (dom.isText(nodes[i]) && dom.isVisibleText(nodes[i])) || dom.isB(nodes[i]) || dom.isU(nodes[i]) || dom.isS(nodes[i]) || dom.isI(nodes[i]) || dom.isFont(nodes[i])) {
|
||||
var ancestor = dom.ancestor(nodes[i], isFormatNode);
|
||||
@@ -1984,6 +1990,14 @@ eventHandler.modules.editor.currentStyle = function (target) {
|
||||
if (r)
|
||||
styleInfo.image = r.isOnImg();
|
||||
}
|
||||
// Fix when the target is a link: the text-align buttons state should
|
||||
// indicate the alignment of the link in the parent, not the text inside
|
||||
// the link (which is not possible to customize with summernote). Summernote fixed
|
||||
// this in their newest version... by just not showing the active button
|
||||
// for alignments.
|
||||
if (styleInfo.anchor) {
|
||||
styleInfo['text-align'] = $(styleInfo.anchor).parent().css('text-align');
|
||||
}
|
||||
return styleInfo;
|
||||
};
|
||||
|
||||
|
||||
@@ -742,7 +742,7 @@ var ViewEditor = Widget.extend({
|
||||
|
||||
var text = data.text || '';
|
||||
if (!isSelected) {
|
||||
text = Array($elem.data('level')).join('-') + ' ' + text;
|
||||
text = Array($elem.data('level') + 1).join('-') + ' ' + text;
|
||||
}
|
||||
var $div = $('<div/>', {
|
||||
text: text,
|
||||
|
||||
@@ -136,12 +136,12 @@ QUnit.test('field html_frame widget', function (assert) {
|
||||
model: 'mass.mailing',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="body" widget="html_frame" options="{\'editor_url\': \'/test\'}"/>' +
|
||||
'<field name="body" widget="html_frame" options="{\'editor_url\': \'/logo\'}"/>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
session: {user_context: {lang: "en_us"}},
|
||||
mockRPC: function (route) {
|
||||
if (_.str.startsWith(route, '/test')) {
|
||||
if (_.str.startsWith(route, '/logo')) {
|
||||
// those tests will be executed twice, once in readonly and once in edit
|
||||
assert.ok(route.search('model=mass.mailing') > 0,
|
||||
"the route should specify the correct model");
|
||||
@@ -200,7 +200,7 @@ QUnit.test('html_frame does not crash when saving in readonly', function (assert
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<field name="body" widget="html_frame" options="{\'editor_url\': \'/test\'}"/>' +
|
||||
'<field name="body" widget="html_frame" options="{\'editor_url\': \'/logo\'}"/>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
@@ -208,7 +208,7 @@ QUnit.test('html_frame does not crash when saving in readonly', function (assert
|
||||
if (args.method) {
|
||||
assert.step(args.method);
|
||||
}
|
||||
if (_.str.startsWith(route, '/test')) {
|
||||
if (_.str.startsWith(route, '/logo')) {
|
||||
// manually call the callback to simulate that the iframe has
|
||||
// been loaded (note: just the content, not the editor)
|
||||
window.odoo[$.deparam(route).callback + '_content'].call();
|
||||
@@ -237,7 +237,7 @@ QUnit.test('html_frame does not crash when saving in edit mode (editor not loade
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="body" widget="html_frame" options="{\'editor_url\': \'/test\'}"/>' +
|
||||
'<field name="body" widget="html_frame" options="{\'editor_url\': \'/logo\'}"/>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
@@ -245,7 +245,7 @@ QUnit.test('html_frame does not crash when saving in edit mode (editor not loade
|
||||
if (args.method) {
|
||||
assert.step(args.method);
|
||||
}
|
||||
if (_.str.startsWith(route, '/test')) {
|
||||
if (_.str.startsWith(route, '/logo')) {
|
||||
// manually call the callback to simulate that the iframe has
|
||||
// been partially loaded (just the content, not the editor)
|
||||
window.odoo[$.deparam(route).callback + '_content']();
|
||||
@@ -279,7 +279,7 @@ QUnit.test('html_frame saving in edit mode (editor and content fully loaded)', f
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="body" widget="html_frame" options="{\'editor_url\': \'/test\'}"/>' +
|
||||
'<field name="body" widget="html_frame" options="{\'editor_url\': \'/logo\'}"/>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
@@ -290,7 +290,7 @@ QUnit.test('html_frame saving in edit mode (editor and content fully loaded)', f
|
||||
writeDeferred.resolve();
|
||||
}
|
||||
}
|
||||
if (_.str.startsWith(route, '/test')) {
|
||||
if (_.str.startsWith(route, '/logo')) {
|
||||
// manually call the callback to simulate that the iframe has
|
||||
// been fully loaded (content + editor)
|
||||
var callback = $.deparam(route).callback;
|
||||
|
||||
@@ -123,12 +123,6 @@ class Http(models.AbstractModel):
|
||||
}, mimetype=_guess_mimetype(ext))
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def _serve_404(cls):
|
||||
req_page = request.httprequest.path
|
||||
page404 = 'website.%s' % (request.website.is_publisher() and 'page_404' or '404')
|
||||
return request.render(page404, {'path': req_page[1:]})
|
||||
|
||||
@classmethod
|
||||
def _serve_redirect(cls):
|
||||
req_page = request.httprequest.path
|
||||
@@ -153,7 +147,7 @@ class Http(models.AbstractModel):
|
||||
if redirect:
|
||||
return request.redirect(redirect.url_to, code=redirect.type)
|
||||
|
||||
return cls._serve_404()
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def _handle_exception(cls, exception):
|
||||
@@ -212,11 +206,16 @@ class Http(models.AbstractModel):
|
||||
status_code=code,
|
||||
)
|
||||
|
||||
view_id = code
|
||||
if request.website.is_publisher() and isinstance(exception, werkzeug.exceptions.NotFound):
|
||||
view_id = 'page_404'
|
||||
values['path'] = request.httprequest.path[1:]
|
||||
|
||||
if not request.uid:
|
||||
cls._auth_method_public()
|
||||
|
||||
try:
|
||||
html = request.env['ir.ui.view'].render_template('website.%s' % code, values)
|
||||
html = request.env['ir.ui.view'].render_template('website.%s' % view_id, values)
|
||||
except Exception:
|
||||
html = request.env['ir.ui.view'].render_template('website.http_error', values)
|
||||
return werkzeug.wrappers.Response(html, status=code, content_type='text/html;charset=utf-8')
|
||||
|
||||
@@ -209,7 +209,7 @@ class BlogPost(models.Model):
|
||||
def write(self, vals):
|
||||
result = True
|
||||
for post in self:
|
||||
copy_vals = vals
|
||||
copy_vals = dict(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)
|
||||
|
||||
@@ -417,8 +417,8 @@
|
||||
<div id="o-carousel-product" class="carousel slide" data-ride="carousel" data-interval="0">
|
||||
<div class="carousel-outer">
|
||||
<div class="carousel-inner">
|
||||
<div class="item active" itemprop="image" t-field="product.image" t-options="{'widget': 'image', 'class': 'product_detail_img js_variant_img', 'alt-field': 'name', 'zoom': 'image', 'unique': product['__last_update']}"/>
|
||||
<div t-if="variant_img" class="item" itemprop="image" t-field="product.product_variant_id.image" t-options="{'widget': 'image', 'class': 'product_detail_img', 'alt-field': 'name', 'zoom': 'image', 'unique': product['__last_update'] + (product.product_variant_id['__last_update'] or '')}"/>
|
||||
<div t-if="variant_img" class="item active" itemprop="image" t-field="product.product_variant_id.image" t-options="{'widget': 'image', 'class': 'product_detail_img js_variant_img', 'alt-field': 'name', 'zoom': 'image', 'unique': product['__last_update'] + (product.product_variant_id['__last_update'] or '')}"/>
|
||||
<div t-attf-class="item#{'' if variant_img else ' active'}" itemprop="image" t-field="product.image" t-options="{'widget': 'image', 'class': 'product_detail_img', 'alt-field': 'name', 'zoom': 'image', 'unique': product['__last_update']}"/>
|
||||
<t t-if="len(image_ids)" t-foreach="image_ids" t-as="pimg">
|
||||
<div class="item" t-field="pimg.image" t-options='{"widget": "image", "class": "product_detail_img", "alt-field": "name", "zoom": "image" }'/>
|
||||
</t>
|
||||
@@ -437,11 +437,11 @@
|
||||
</div>
|
||||
|
||||
<ol class="carousel-indicators" t-if="len(image_ids) or variant_img">
|
||||
<li data-target="#o-carousel-product" data-slide-to="0" class="active">
|
||||
<img class="img img-responsive js_variant_img_small" t-attf-src="/website/image/product.template/{{product.id}}/image/90x90" t-att-alt="product.name"/>
|
||||
<li t-if="variant_img" data-target="#o-carousel-product" data-slide-to="0" class="active">
|
||||
<img class="img img-responsive js_variant_img_small" t-attf-src="/website/image/product.product/{{product.product_variant_id.id}}/image/90x90" t-att-alt="product.name"/>
|
||||
</li>
|
||||
<li t-if="variant_img" data-target="#o-carousel-product" data-slide-to="1" class="">
|
||||
<img class="img img-responsive" t-attf-src="/website/image/product.product/{{product.product_variant_id.id}}/image/90x90" t-att-alt="product.name"/>
|
||||
<li data-target="#o-carousel-product" t-att-data-slide-to="1 if variant_img else 0" t-att-class="'' if variant_img else 'active'">
|
||||
<img class="img img-responsive" t-attf-src="/website/image/product.template/{{product.id}}/image/90x90" t-att-alt="product.name"/>
|
||||
</li>
|
||||
<t t-if="len(image_ids)" t-foreach="image_ids" t-as="pimg">
|
||||
<li data-target="#o-carousel-product" t-att-data-slide-to="pimg_index + (variant_img and 2 or 1)">
|
||||
@@ -518,7 +518,7 @@
|
||||
<div t-field="alt_product.image_small" t-options="{'widget': 'image', 'class': 'img-rounded shadow o_alternative_product' }" />
|
||||
<h5>
|
||||
<a t-attf-href="/shop/product/#{ slug(alt_product) }" style="display: block">
|
||||
<span t-att-title="alt_product.name" t-field="alt_product.name" style="display: block;" />
|
||||
<span t-att-title="alt_product.name" t-field="alt_product.name" class="o_text_overflow" style="display: block;" />
|
||||
</a>
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
@@ -42,3 +42,4 @@ 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
|
||||
Tomas Alvarez tomas@vauxoo.com https://github.com/tomeyro
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
Switzerland, 2018-01-30
|
||||
|
||||
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,
|
||||
|
||||
Kumar Aberer office@aberer-solutions.ch https://github.com/kaberer
|
||||
@@ -0,0 +1,9 @@
|
||||
Spain, 2018-01-29
|
||||
|
||||
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,
|
||||
|
||||
Nacho Muñoz nacmuro@gmail.com https://github.com/moutio
|
||||
@@ -0,0 +1,11 @@
|
||||
Vietnam, 2018.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,
|
||||
|
||||
Nguyen Phuc <phucngta@gmail.com> https://github.com/phucngta
|
||||
@@ -1,5 +1,3 @@
|
||||
:banner: banners/module.jpg
|
||||
|
||||
=======
|
||||
Modules
|
||||
=======
|
||||
@@ -84,6 +82,49 @@ Available manifest fields are:
|
||||
to ``auto_install``. When both ``sale`` and ``crm`` are installed, it
|
||||
automatically adds CRM campaigns tracking to sale orders without either
|
||||
``sale`` or ``crm`` being aware of one another
|
||||
``external_dependencies`` (``dict(key=list(str))``)
|
||||
A dictionary containing python and/or binary dependencies.
|
||||
|
||||
For python dependencies, the ``python`` key must be defined for this
|
||||
dictionary and a list of python modules to be imported should be assigned
|
||||
to it.
|
||||
|
||||
For binary dependencies, the ``bin`` key must be defined for this
|
||||
dictionary and a list of binary executable names should be assigned to it.
|
||||
|
||||
The module won't be installed if either the python module is not installed
|
||||
in the host machine or the binary executable is not found within the
|
||||
host machine's PATH environment variable.
|
||||
``application`` (``bool``, default: ``False``)
|
||||
Whether the module should be considered as a fully-fledged application
|
||||
(``True``) or is just a technical module (``False``) that provides some
|
||||
extra functionality to an existing application module.
|
||||
``css`` (``list(str)``)
|
||||
Specify css files with custom rules to be imported, these files should be
|
||||
located in ``static/src/css`` inside the module.
|
||||
``images`` (``list(str)``)
|
||||
Specify image files to be used by the module.
|
||||
``installable`` (``bool`` default: ``False``)
|
||||
Whether a user should be able to install the module from the Web UI or not.
|
||||
``maintainer`` (``str``)
|
||||
Person or entity in charge of the maintenance of this module, by default
|
||||
it is assumed that the author is the maintainer.
|
||||
``{pre_init, post_init, uninstall}_hook`` (``str``)
|
||||
Hooks for module installation/uninstallation, their value should be a
|
||||
string representing the name of a function defined inside the module's
|
||||
``__init__.py``.
|
||||
|
||||
``pre_init_hook`` takes a cursor as its only argument, this function is
|
||||
executed prior to the module's installation.
|
||||
|
||||
``post_init_hook`` takes a cursor and a registry as its arguments, this
|
||||
function is executed right after the module's installation.
|
||||
|
||||
``uninstall_hook`` takes a cursor and a registry as its arguments, this
|
||||
function is executed after the module's uninstallation.
|
||||
|
||||
These hooks should only be used when setup/cleanup required for this module
|
||||
is either extremely difficult or impossible through the api.
|
||||
|
||||
.. _semantic versioning: http://semver.org
|
||||
.. _existing categories:
|
||||
|
||||
+18
-18
@@ -44,7 +44,7 @@ correctly.
|
||||
Configuration samples
|
||||
---------------------
|
||||
|
||||
* show only databases with names beginning with 'mycompany'
|
||||
* Show only databases with names beginning with 'mycompany'
|
||||
|
||||
in ``/etc/odoo.conf`` set:
|
||||
|
||||
@@ -87,18 +87,18 @@ machine, and is the default when no host is provided, but if you want Odoo and
|
||||
PostgreSQL to execute on different machines [#different-machines]_ it will
|
||||
need to `listen to network interfaces`_ [#remote-socket]_, either:
|
||||
|
||||
* only accept loopback connections and `use an SSH tunnel`_ between the
|
||||
* Only accept loopback connections and `use an SSH tunnel`_ between the
|
||||
machine on which Odoo runs and the one on which PostgreSQL runs, then
|
||||
configure Odoo to connect to its end of the tunnel
|
||||
* accept connections to the machine on which Odoo is installed, possibly
|
||||
* Accept connections to the machine on which Odoo is installed, possibly
|
||||
over ssl (see `PostgreSQL connection settings`_ for details), then configure
|
||||
Odoo to connect over the network
|
||||
|
||||
Configuration sample
|
||||
--------------------
|
||||
|
||||
* allow tcp connection on localhost
|
||||
* allow tcp connection from 192.168.1.x network
|
||||
* Allow tcp connection on localhost
|
||||
* Allow tcp connection from 192.168.1.x network
|
||||
|
||||
in ``/etc/postgresql/9.5/main/pg_hba.conf`` set:
|
||||
|
||||
@@ -134,11 +134,11 @@ create a new user (``odoo``) and set it as the database user.
|
||||
simply checked before performing database alterations. It should be set to
|
||||
a randomly generated value to ensure third parties can not use this
|
||||
interface.
|
||||
* all database operations use the :ref:`database options
|
||||
* All database operations use the :ref:`database options
|
||||
<reference/cmdline/server/database>`, including the database management
|
||||
screen. For the database management screen to work requires that the PostgreSQL user
|
||||
have ``createdb`` right.
|
||||
* users can always drop databases they own. For the database management screen
|
||||
* Users can always drop databases they own. For the database management screen
|
||||
to be completely non-functional, the PostgreSQL user needs to be created with
|
||||
``no-createdb`` and the database must be owned by a different PostgreSQL user.
|
||||
|
||||
@@ -248,10 +248,10 @@ authentication information in cleartext. This means a secure deployment of
|
||||
Odoo must use HTTPS\ [#switching]_. SSL termination can be implemented via
|
||||
just about any SSL termination proxy, but requires the following setup:
|
||||
|
||||
* enable Odoo's :option:`proxy mode <odoo-bin --proxy-mode>`. This should only be enabled when Odoo is behind a reverse proxy
|
||||
* set up the SSL termination proxy (`Nginx termination example`_)
|
||||
* set up the proxying itself (`Nginx proxying example`_)
|
||||
* your SSL termination proxy should also automatically redirect non-secure
|
||||
* Enable Odoo's :option:`proxy mode <odoo-bin --proxy-mode>`. This should only be enabled when Odoo is behind a reverse proxy
|
||||
* Set up the SSL termination proxy (`Nginx termination example`_)
|
||||
* Set up the proxying itself (`Nginx proxying example`_)
|
||||
* Your SSL termination proxy should also automatically redirect non-secure
|
||||
connections to the secure port
|
||||
|
||||
.. warning::
|
||||
@@ -263,8 +263,8 @@ just about any SSL termination proxy, but requires the following setup:
|
||||
Configuration sample
|
||||
--------------------
|
||||
|
||||
* redirect http requests to https
|
||||
* proxy requests to odoo
|
||||
* Redirect http requests to https
|
||||
* Proxy requests to odoo
|
||||
|
||||
in ``/etc/odoo.conf`` set:
|
||||
|
||||
@@ -349,10 +349,10 @@ Cron Workers
|
||||
|
||||
To run cron jobs for an Odoo deployment as a WSGI application requires
|
||||
|
||||
* a classical Odoo (run via ``odoo-bin``)
|
||||
* connected to the database in which cron jobs have to be run (via
|
||||
* A classical Odoo (run via ``odoo-bin``)
|
||||
* Connected to the database in which cron jobs have to be run (via
|
||||
:option:`odoo-bin -d`)
|
||||
* which should not be exposed to the network. To ensure cron runners are not
|
||||
* Which should not be exposed to the network. To ensure cron runners are not
|
||||
network-accessible, it is possible to disable the built-in HTTP server
|
||||
entirely with :option:`odoo-bin --no-http` or setting ``http_enable = False``
|
||||
in the configuration file
|
||||
@@ -372,11 +372,11 @@ notifications.
|
||||
|
||||
The solutions to support livechat/motifications in a WSGI application are:
|
||||
|
||||
* deploy a threaded version of Odoo (instread of a process-based preforking
|
||||
* Deploy a threaded version of Odoo (instread of a process-based preforking
|
||||
one) and redirect only requests to URLs starting with ``/longpolling/`` to
|
||||
that Odoo, this is the simplest and the longpolling URL can double up as
|
||||
the cron instance.
|
||||
* deploy an evented Odoo via ``odoo-gevent`` and proxy requests starting
|
||||
* Deploy an evented Odoo via ``odoo-gevent`` and proxy requests starting
|
||||
with ``/longpolling/`` to
|
||||
:option:`the longpolling port <odoo-bin --longpolling-port>`.
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
:banner: banners/localization.jpg
|
||||
|
||||
==========================
|
||||
Making my Own Localization
|
||||
==========================
|
||||
=======================
|
||||
Creating a Localization
|
||||
=======================
|
||||
|
||||
.. warning::
|
||||
|
||||
This tutorial requires knowledges about how to build a module in Odoo.
|
||||
This tutorial requires knowledges about how to build a module in Odoo (see
|
||||
:doc:`../../howtos/backend`).
|
||||
|
||||
Building a localization module
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -8,6 +8,7 @@ from odoo.tools.safe_eval import safe_eval, test_python_expr
|
||||
from odoo.tools import pycompat
|
||||
from odoo.http import request
|
||||
|
||||
import base64
|
||||
from collections import defaultdict
|
||||
import datetime
|
||||
import dateutil
|
||||
@@ -76,6 +77,8 @@ class IrActions(models.Model):
|
||||
'datetime': datetime,
|
||||
'dateutil': dateutil,
|
||||
'timezone': timezone,
|
||||
'b64encode': base64.b64encode,
|
||||
'b64decode': base64.b64decode,
|
||||
}
|
||||
|
||||
@api.model
|
||||
@@ -253,7 +256,7 @@ class IrActionsActWindowView(models.Model):
|
||||
_name = 'ir.actions.act_window.view'
|
||||
_table = 'ir_act_window_view'
|
||||
_rec_name = 'view_id'
|
||||
_order = 'sequence'
|
||||
_order = 'sequence,id'
|
||||
|
||||
sequence = fields.Integer()
|
||||
view_id = fields.Many2one('ir.ui.view', string='View')
|
||||
|
||||
@@ -15,7 +15,6 @@ 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__)
|
||||
|
||||
@@ -373,8 +372,6 @@ 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)
|
||||
@@ -388,10 +385,7 @@ class Users(models.Model):
|
||||
db = self._cr.dbname
|
||||
for id in self.ids:
|
||||
self.__uid_cache[db].pop(id, None)
|
||||
res = super(Users, self).unlink()
|
||||
# force deletion of all sessions for these users
|
||||
root.session_store.delete_sessions_for_uids(self.ids)
|
||||
return res
|
||||
return super(Users, self).unlink()
|
||||
|
||||
@api.model
|
||||
def name_search(self, name='', args=None, operator='ilike', limit=100):
|
||||
|
||||
@@ -92,6 +92,18 @@ class TestFloatPrecision(TransactionCase):
|
||||
try_round(1.8, '2', 0, method='UP')
|
||||
try_round(-1.8, '-2', 0, method='UP')
|
||||
|
||||
# Try some rounding value with rounding method DOWN instead of HALF-UP
|
||||
# We use 2.425 because when normalizing 2.425 with precision_digits=3 it gives
|
||||
# us 2424.9999999999995 as value, and if not handle correctly the rounding DOWN
|
||||
# value will be incorrect (should be 2.425 and not 2.424)
|
||||
try_round(2.425, '2.425', method='DOWN')
|
||||
try_round(2.4249, '2.424', method='DOWN')
|
||||
try_round(-2.425, '-2.425', method='DOWN')
|
||||
try_round(-2.4249, '-2.424', method='DOWN')
|
||||
try_round(-2.500, '-2.500', method='DOWN')
|
||||
try_round(1.8, '1', 0, method='DOWN')
|
||||
try_round(-1.8, '-1', 0, method='DOWN')
|
||||
|
||||
# Extended float range test, inspired by Cloves Almeida's test on bug #882036.
|
||||
fractions = [.0, .015, .01499, .675, .67499, .4555, .4555, .45555]
|
||||
expecteds = ['.00', '.02', '.01', '.68', '.67', '.46', '.456', '.4556']
|
||||
@@ -135,8 +147,8 @@ class TestFloatPrecision(TransactionCase):
|
||||
try_compare(-657.4444, -657.445, 1)
|
||||
|
||||
# Rounding to unusual rounding units (e.g. coin values)
|
||||
def try_round(amount, expected, precision_rounding=None):
|
||||
value = float_round(amount, precision_rounding=precision_rounding)
|
||||
def try_round(amount, expected, precision_rounding=None, method='HALF-UP'):
|
||||
value = float_round(amount, precision_rounding=precision_rounding, rounding_method=method)
|
||||
result = float_repr(value, precision_digits=2)
|
||||
self.assertEqual(result, expected, 'Rounding error: got %s, expected %s' % (result, expected))
|
||||
|
||||
@@ -145,6 +157,8 @@ class TestFloatPrecision(TransactionCase):
|
||||
try_round(457.3, '455.00', precision_rounding=5)
|
||||
try_round(457.5, '460.00', precision_rounding=5)
|
||||
try_round(457.1, '456.00', precision_rounding=3)
|
||||
try_round(2.5, '2.50', precision_rounding=0.05, method='DOWN')
|
||||
try_round(-2.5, '-2.50', precision_rounding=0.05, method='DOWN')
|
||||
|
||||
def test_rounding_04(self):
|
||||
""" check that proper rounding is performed for float persistence """
|
||||
|
||||
+13
-15
@@ -635,8 +635,9 @@ class JsonRequest(WebRequest):
|
||||
body = json.dumps(response, default=ustr)
|
||||
|
||||
return Response(
|
||||
body, headers=[('Content-Type', mime),
|
||||
('Content-Length', len(body))])
|
||||
body, status=error and error.pop('http_status', 200) or 200,
|
||||
headers=[('Content-Type', mime), ('Content-Length', len(body))]
|
||||
)
|
||||
|
||||
def _handle_exception(self, exception):
|
||||
"""Called within an except block to allow converting exceptions
|
||||
@@ -645,13 +646,18 @@ class JsonRequest(WebRequest):
|
||||
try:
|
||||
return super(JsonRequest, self)._handle_exception(exception)
|
||||
except Exception:
|
||||
if not isinstance(exception, (odoo.exceptions.Warning, SessionExpiredException, odoo.exceptions.except_orm)):
|
||||
if not isinstance(exception, (odoo.exceptions.Warning, SessionExpiredException,
|
||||
odoo.exceptions.except_orm, werkzeug.exceptions.NotFound)):
|
||||
_logger.exception("Exception during JSON request handling.")
|
||||
error = {
|
||||
'code': 200,
|
||||
'message': "Odoo Server Error",
|
||||
'data': serialize_exception(exception)
|
||||
}
|
||||
if isinstance(exception, werkzeug.exceptions.NotFound):
|
||||
error['http_status'] = 404
|
||||
error['code'] = 404
|
||||
error['message'] = "404: Not Found"
|
||||
if isinstance(exception, AuthenticationError):
|
||||
error['code'] = 100
|
||||
error['message'] = "Odoo Session Invalid"
|
||||
@@ -1029,6 +1035,7 @@ 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
|
||||
|
||||
@@ -1043,6 +1050,7 @@ 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):
|
||||
@@ -1055,6 +1063,7 @@ 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):
|
||||
@@ -1268,17 +1277,6 @@ 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.
|
||||
"""
|
||||
@@ -1290,7 +1288,7 @@ class Root(object):
|
||||
# Setup http sessions
|
||||
path = odoo.tools.config.session_dir
|
||||
_logger.debug('HTTP sessions stored in: %s', path)
|
||||
return OdooSessionStore(path, session_class=OpenERPSession)
|
||||
return werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
|
||||
|
||||
@lazy_property
|
||||
def nodb_routing_map(self):
|
||||
|
||||
+15
-12
@@ -61,9 +61,6 @@ def float_round(value, precision_digits=None, precision_rounding=None, rounding_
|
||||
# In order to easily support rounding to arbitrary 'steps' (e.g. coin values),
|
||||
# we normalize the value before rounding it as an integer, and de-normalize
|
||||
# after rounding: e.g. float_round(1.3, precision_rounding=.5) == 1.5
|
||||
|
||||
# TIE-BREAKING: HALF-UP (for normal rounding)
|
||||
# We want to apply HALF-UP tie-breaking rules, i.e. 0.5 rounds away from 0.
|
||||
# Due to IEE754 float/double representation limits, the approximation of the
|
||||
# real value may be slightly below the tie limit, resulting in an error of
|
||||
# 1 unit in the last place (ulp) after rounding.
|
||||
@@ -74,25 +71,31 @@ def float_round(value, precision_digits=None, precision_rounding=None, rounding_
|
||||
# Credit: discussion with OpenERP community members on bug 882036
|
||||
|
||||
normalized_value = value / rounding_factor # normalize
|
||||
sign = math.copysign(1.0, normalized_value)
|
||||
epsilon_magnitude = math.log(abs(normalized_value), 2)
|
||||
epsilon = 2**(epsilon_magnitude-53)
|
||||
if rounding_method == 'HALF-UP':
|
||||
normalized_value += math.copysign(epsilon, normalized_value)
|
||||
rounded_value = round(normalized_value) # round to integer
|
||||
|
||||
# TIE-BREAKING: UP (for ceiling[resp. flooring] operations)
|
||||
# When rounding the value up[resp. down], we instead subtract the epsilon value
|
||||
# TIE-BREAKING: UP/DOWN (for ceiling[resp. flooring] operations)
|
||||
# When rounding the value up[resp. down], we instead subtract[resp. add] the epsilon value
|
||||
# as the the approximation of the real value may be slightly *above* the
|
||||
# tie limit, this would result in incorrectly rounding up[resp. down] to the next number
|
||||
# The math.ceil[resp. math.floor] operation is applied on the absolute value in order to
|
||||
# round "away from zero" and not "towards infinity", then the sign is
|
||||
# restored.
|
||||
|
||||
else:
|
||||
func = math.floor if rounding_method == 'DOWN' else math.ceil
|
||||
sign = math.copysign(1.0, normalized_value)
|
||||
if rounding_method == 'UP':
|
||||
normalized_value -= sign*epsilon
|
||||
rounded_value = func(abs(normalized_value)) * sign
|
||||
rounded_value = math.ceil(abs(normalized_value)) * sign
|
||||
|
||||
elif rounding_method == 'DOWN':
|
||||
normalized_value += sign*epsilon
|
||||
rounded_value = math.floor(abs(normalized_value)) * sign
|
||||
|
||||
# TIE-BREAKING: HALF-UP (for normal rounding)
|
||||
# We want to apply HALF-UP tie-breaking rules, i.e. 0.5 rounds away from 0.
|
||||
else:
|
||||
normalized_value += math.copysign(epsilon, normalized_value)
|
||||
rounded_value = round(normalized_value) # round to integer
|
||||
|
||||
result = rounded_value * rounding_factor # de-normalize
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user