[MERGE] forward port branch 11.0 up to f5e7b86686

This commit is contained in:
Christophe Simonis
2018-02-01 18:15:21 +01:00
59 changed files with 444 additions and 199 deletions
+15 -6
View File
@@ -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
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from . import barcodes
from . import barcode_events_mixin
from . import ir_http
+14
View File
@@ -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);
+4 -2
View File
@@ -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
+4 -2
View File
@@ -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>
+1 -1
View File
@@ -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,
+2
View File
@@ -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)
+5 -3
View File
@@ -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>
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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">
+1 -1
View File
@@ -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']
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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'
+4 -1
View File
@@ -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:
+3 -3
View File
@@ -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) {
+2 -4
View File
@@ -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):
+6 -4
View File
@@ -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(
+6
View File
@@ -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)
+2 -1
View File
@@ -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')
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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)',
+24 -26
View File
@@ -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());
});
},
+13 -21
View File
@@ -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;
+7 -8
View File
@@ -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')
+1 -1
View File
@@ -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)
+7 -7
View File
@@ -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>
+1
View File
@@ -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
+11
View File
@@ -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
+9
View File
@@ -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
+11
View File
@@ -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
+43 -2
View File
@@ -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
View File
@@ -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>`.
+5 -4
View File
@@ -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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+4 -1
View File
@@ -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')
+1 -7
View File
@@ -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):
+16 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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