diff --git a/addons/barcodes/static/src/js/form_view_barcode_handler.js b/addons/barcodes/static/src/js/form_view_barcode_handler.js
index 7f2614f0ef1..524ec1513c3 100644
--- a/addons/barcodes/static/src/js/form_view_barcode_handler.js
+++ b/addons/barcodes/static/src/js/form_view_barcode_handler.js
@@ -28,6 +28,8 @@ var FormViewBarcodeHandler = common.AbstractField.extend(BarcodeHandlerMixin, {
this.__quantity_listener = _.bind(this._set_quantity_listener, this);
BarcodeHandlerMixin.init.apply(this, arguments);
+ this.process_barcode_mutex = new utils.Mutex();
+
return this._super.apply(this, arguments);
},
@@ -153,25 +155,35 @@ var FormViewBarcodeHandler = common.AbstractField.extend(BarcodeHandlerMixin, {
else if (this.form_view.get('actual_mode') === 'view')
this._display_no_edit_mode_warning();
else {
- // Call hook method possibly implemented by subclass
- this.pre_onchange_hook(barcode).then(function(proceed) {
- if (proceed === true) {
- // Wait for hypothetical ongoing onchange to finish
- self.form_view.onchanges_mutex.exec(function() {
- // A real onchange is triggered when a value actually changes (which can correspond
- // to a widget's blur event per example). Commit the value of fields before
- // programmatically triggering an onchange to be consistent with this.
- var mutex_commit_value = new utils.Mutex();
- _.each(self.form_view.fields, function(field) {
- mutex_commit_value.exec(_.bind(field.commit_value, field));
- });
- return mutex_commit_value.def.then(function(){
- // Trigger the barcode onchange
- self.set_value(barcode);
- });
- });
+ var process_barcode = function () {
+ // this function can be passed to `Mutex.exec` in order to make sure
+ // that every ongoing onchanges in the form view are done
+ var form_onchanges_mutex = function () {
+ return self.form_view.onchanges_mutex.def;
}
- });
+
+ // before setting the barcode field with the received barcode, we commit
+ // every fields of the form view and we wait for their hypothetical ongoing
+ // onchanges to finish
+ var commit_mutex = new utils.Mutex();
+ _.each(self.form_view.fields, function (field) {
+ commit_mutex.exec(function () {
+ return field.commit_value();
+ });
+ commit_mutex.exec(form_onchanges_mutex);
+ });
+
+ return commit_mutex.def.then(function () {
+ return self.pre_onchange_hook(barcode).then(function (proceed) {
+ if (proceed) {
+ self.set_value(barcode); // set the barcode field with the received one
+ return form_onchanges_mutex(); // wait for its onchange to finish
+ }
+ });
+ });
+ };
+
+ this.process_barcode_mutex.exec(process_barcode);
}
},
diff --git a/addons/base_gengo/views/res_config_views.xml b/addons/base_gengo/views/res_config_views.xml
index a78bf51c07f..66546b2d1e5 100644
--- a/addons/base_gengo/views/res_config_views.xml
+++ b/addons/base_gengo/views/res_config_views.xml
@@ -7,32 +7,32 @@
-
-
+
+
-
-
+
+
-
+
-
+
-
-
+
+
diff --git a/addons/google_calendar/models/google_calendar.py b/addons/google_calendar/models/google_calendar.py
index 7a5fda8575e..007bbffeaf9 100644
--- a/addons/google_calendar/models/google_calendar.py
+++ b/addons/google_calendar/models/google_calendar.py
@@ -402,6 +402,20 @@ class GoogleCalendar(models.AbstractModel):
data_json = json.dumps(data)
return self.env['google.service']._do_request(url, data_json, headers, type='PUT')
+ def create_from_google(self, event, partner_id):
+ context_tmp = dict(self._context, NewMeeting=True)
+ res = self.with_context(context_tmp).update_from_google(False, event.GG.event, "create")
+ event.OE.event_id = res
+ meeting = self.env['calendar.event'].browse(res)
+ attendee_record = self.env['calendar.attendee'].search([('partner_id', '=', partner_id), ('event_id', '=', res)])
+ attendee_record.with_context(context_tmp).write({'oe_synchro_date': meeting.oe_update_date, 'google_internal_event_id': event.GG.event['id']})
+ if meeting.recurrency:
+ attendees = self.env['calendar.attendee'].sudo().search([('google_internal_event_id', '=ilike', '%s\_%%' % event.GG.event['id'])])
+ excluded_recurrent_event_ids = set(attendee.event_id for attendee in attendees)
+ for event in excluded_recurrent_event_ids:
+ event.write({'recurrent_id': meeting.id, 'recurrent_id_date': event.start, 'user_id': meeting.user_id.id})
+ return event
+
def update_from_google(self, event, single_event_dict, type):
""" Update an event in Odoo with information from google calendar
:param event : record od calendar.event to update
@@ -786,13 +800,8 @@ class GoogleCalendar(models.AbstractModel):
if isinstance(actToDo, NothingToDo):
continue
elif isinstance(actToDo, Create):
- context_tmp = {'newMeeting': True}
if actSrc == 'GG':
- res = recs.with_context(context_tmp).update_from_google(False, event.GG.event, "create")
- event.OE.event_id = res
- meeting = CalendarEvent.browse(res)
- attendee_records = CalendarAttendee.search([('partner_id', '=', my_partner_id), ('event_id', '=', res)])
- attendee_records.with_context(context_tmp).write({'oe_synchro_date': meeting.oe_update_date, 'google_internal_event_id': event.GG.event['id']})
+ self.create_from_google(event, my_partner_id)
elif actSrc == 'OE':
raise "Should be never here, creation for OE is done before update !"
#TODO Add to batch
@@ -817,6 +826,12 @@ class GoogleCalendar(models.AbstractModel):
main_ev = CalendarAttendee.with_context(context_novirtual).search([('google_internal_event_id', '=', event.GG.event['id'].rsplit('_', 1)[0])], limit=1)
event_to_synchronize[base_event][0][1].OE.event_id = main_ev.event_id.id
+ if event_to_synchronize[base_event][0][1].OE.event_id:
+ parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].OE.event_id, new_google_event_id)
+ res = self.update_from_google(parent_event, event.GG.event, "copy")
+ else:
+ self.create_from_google(event, my_partner_id)
+
parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].OE.event_id, new_google_event_id)
res = recs.update_from_google(parent_event, event.GG.event, "copy")
else:
diff --git a/addons/l10n_es/data/account_account_template_data.xml b/addons/l10n_es/data/account_account_template_data.xml
index 4c86cb55442..455d8132f04 100644
--- a/addons/l10n_es/data/account_account_template_data.xml
+++ b/addons/l10n_es/data/account_account_template_data.xml
@@ -1568,28 +1568,28 @@
4700Hacienda Pública, deudora por IVA
-
+ 4708Hacienda Pública, deudora por subvenciones concedidas
-
+ 4709Hacienda Pública, deudora por devolución de impuestos
-
+ 471Organismos de la Seguridad Social, deudores
-
+
@@ -1603,77 +1603,77 @@
473Hacienda Pública, retenciones y pagos a cuenta
-
+ 4740Activos por diferencias temporarias deducibles
-
+ 4742Derechos por deducciones y bonificaciones pendientes de aplicar
-
+ 4745Crédito por pérdidas a compensar del ejercicio
-
+ 4750Hacienda Pública, acreedora por IVA
-
+ 4751Hacienda Pública, acreedora por retenciones practicadas
-
+ 4752Hacienda Pública, acreedora por impuesto sobre sociedades
-
+ 4758Hacienda Pública, acreedora por subvenciones a reintegrar
-
+ 476Organismos de la Seguridad Social, acreedores
-
+ 477Hacienda Pública. IVA repercutido
-
+ 479Pasivos por diferencias temporarias imponibles
-
+
diff --git a/addons/l10n_mx/__manifest__.py b/addons/l10n_mx/__manifest__.py
index 4d804ad96a6..8ec93d7a7e3 100644
--- a/addons/l10n_mx/__manifest__.py
+++ b/addons/l10n_mx/__manifest__.py
@@ -40,5 +40,7 @@ With this module you will have:
"data/account_data.xml",
"data/account_tax_data.xml",
"data/account_chart_template_data.yml",
+ "data/fiscal_position_data.xml",
+ "views/res_config_view.xml",
],
}
diff --git a/addons/l10n_mx/data/fiscal_position_data.xml b/addons/l10n_mx/data/fiscal_position_data.xml
new file mode 100644
index 00000000000..cc22638112f
--- /dev/null
+++ b/addons/l10n_mx/data/fiscal_position_data.xml
@@ -0,0 +1,115 @@
+
+
+
+
+ Personas morales del régimen general
+ Es para las sociedades mercantiles, asociaciones civiles de profesionales que:
+
+- Realicen actividades lucrativas,
+- Sociedades cooperativas de producción,
+- Instituciones de crédito tales como bancos y casas de bolsa,
+- Organismos descentralizados que comercialicen bienes o servicios, entre otras.
+
+
+http://www.sat.gob.mx/6_406.html
+
+última modificación :
+16/febrero/2010, 12:56, información vigente.
+
+
+
+ Personas morales con fines no lucrativos
+ Es para las personas morales cuya finalidad no es obtener una ganancia económica o lucro, por ejemplo:
+
+- Instituciones de asistencia o de beneficencia
+- Asociaciones o sociedades civiles con fines políticos o deportivos
+- Asociaciones religiosas
+- Asociaciones patronales; sindicatos obreros; cámaras de comercio e industria
+- Asociaciones o sociedades civiles de enseñanza; de investigación científica o tecnológica
+- Sociedades cooperativas de consumo o sociedades mutualistas.
+- Colegios de profesionales
+- Partidos políticos
+- Asociaciones de padres de familia
+- Asociaciones civiles de colonos o las que administren inmuebles en condominio
+
+
+http://www.sat.gob.mx/6_404.html
+
+última modificación :
+16/febrero/2010, 12:57, información vigente.
+
+
+
+ Asociaciones Religiosas
+ Son congregaciones que tienen como fin el ejercer culto a una divinidad; para efectos fiscales, se considerarán a las iglesias y demás agrupaciones que obtengan su correspondiente registro constitutivo ante la Secretaría de Gobernación, en los términos de la Ley de Asociaciones Religiosas y Culto Público, ya que es a través de este registro como se obtiene la personalidad jurídica.
+
+
+http://www.sat.gob.mx/sitio_internet/asistencia_contribuyente/principiantes/permonl/68_1813.html
+
+última modificación :
+10/abril/2012, 18:15, información vigente.
+
+
+
+ Personas morales del régimen simplificado
+ Deben tributar en el régimen simplificado las siguientes personas morales:
+
+- Las dedicadas exclusivamente al autotransporte terrestre de carga o de pasajeros.
+
+- Las que se dediquen exclusivamente a actividades:
+ 1) Agrícolas
+ 2) Ganaderas
+ 3) Silvícolas
+ 4) Pesqueras
+
+- Las constituidas como empresas integradoras.
+
+
+http://www.sat.gob.mx/6_405.html
+última modificación :
+24/agosto/2009, 12:03, información vigente
+
+
+
+
+
+ Persona física con actividad empresarial
+ Es el régimen conforme al cual pueden pagar sus impuestos todas las personas físicas que se dediquen a las siguientes actividades
+
+- Comerciales.
+
+- Industriales, que consisten en la extracción, conservación o transformación de materias primas, acabado de productos y la elaboración de satisfactores;
+
+- Agrícolas, que comprenden las actividades de siembra, cultivo, cosecha y la primera enajenación de los productos obtenidos, que no hayan sido objeto de transformación industrial;
+
+- Ganaderas, que son las consistentes en la cría y engorda de ganado, aves de corral y animales, así como la primera enajenación de sus productos, que no hayan sido objeto de transformación industrial;
+
+- De pesca, que incluyen la cría, cultivo, fomento y cuidado de la reproducción de toda clase de especies marinas y de agua dulce, incluida la acuacultura, así como la captura y extracción de las mismas y la primera enajenación de sus productos, que no hayan sido objeto de transformación industrial;
+
+- Silvícolas, que son las de cultivo de los bosques o montes, así como la cría, conservación, restauración, fomento y aprovechamiento de la vegetación de los mismos y la primera enajenación de sus productos, que no hayan sido objeto de transformación industrial.
+
+
+http://www.sat.gob.mx/sitio_internet/asistencia_contribuyente/principiantes/empresarial/11_428.html
+última modificación :
+12/septiembre/2005, 15:58, información vigente
+
+
+
+
+ Foreign Customer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/l10n_mx/models/__init__.py b/addons/l10n_mx/models/__init__.py
index 5376a315b04..f2b298f3cb9 100644
--- a/addons/l10n_mx/models/__init__.py
+++ b/addons/l10n_mx/models/__init__.py
@@ -4,3 +4,4 @@
from . import chart_template
from . import account
+from . import res_config
diff --git a/addons/l10n_mx/models/res_config.py b/addons/l10n_mx/models/res_config.py
new file mode 100644
index 00000000000..913a422cf4b
--- /dev/null
+++ b/addons/l10n_mx/models/res_config.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+
+from odoo import fields, models
+
+
+class AccountConfigSettings(models.TransientModel):
+ _inherit = 'res.config.settings'
+
+ module_l10n_mx_edi = fields.Boolean('Mexican Electronic Invoicing')
diff --git a/addons/l10n_mx/views/res_config_view.xml b/addons/l10n_mx/views/res_config_view.xml
new file mode 100644
index 00000000000..d5f59f6937f
--- /dev/null
+++ b/addons/l10n_mx/views/res_config_view.xml
@@ -0,0 +1,23 @@
+
+
+
+ account.config.settings.form.inherit
+ account.config.settings
+
+
+
+
+
+
+
+
+
+
+ Create your electronic invoices automatically (CFDI format)
+
+
+
+
+
+
+
diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py
index a6e20c46bd2..bf145c9d59e 100644
--- a/addons/mrp/models/mrp_production.py
+++ b/addons/mrp/models/mrp_production.py
@@ -271,8 +271,8 @@ class MrpProduction(models.Model):
boms, lines = production.bom_id.explode(production.product_id, factor, picking_type=production.bom_id.picking_type_id)
production._generate_raw_moves(lines)
# Check for all draft moves whether they are mto or not
- self._adjust_procure_method()
- self.move_raw_ids.action_confirm()
+ production._adjust_procure_method()
+ production.move_raw_ids.action_confirm()
return True
def _generate_finished_moves(self):
diff --git a/addons/payment_paypal/models/payment.py b/addons/payment_paypal/models/payment.py
index 6aaff94a631..60c5f57ea51 100644
--- a/addons/payment_paypal/models/payment.py
+++ b/addons/payment_paypal/models/payment.py
@@ -4,6 +4,9 @@ import json
import logging
import urlparse
+import dateutil.parser
+import pytz
+
from odoo import api, fields, models, _
from odoo.addons.payment.models.payment_acquirer import ValidationError
from odoo.addons.payment_paypal.controllers.main import PaypalController
@@ -193,7 +196,16 @@ class TxPaypal(models.Model):
}
if status in ['Completed', 'Processed']:
_logger.info('Validated Paypal payment for tx %s: set as done' % (self.reference))
- res.update(state='done', date_validate=data.get('payment_date', fields.datetime.now()))
+ try:
+ # dateutil and pytz don't recognize abbreviations PDT/PST
+ tzinfos = {
+ 'PST': -8 * 3600,
+ 'PDT': -7 * 3600,
+ }
+ date_validate = dateutil.parser.parse(data.get('payment_date'), tzinfos=tzinfos).astimezone(pytz.utc)
+ except:
+ date_validate = fields.Datetime.now()
+ res.update(state='done', date_validate=date_validate)
return self.write(res)
elif status in ['Pending', 'Expired']:
_logger.info('Received notification for Paypal payment %s: set as pending' % (self.reference))
diff --git a/addons/payment_paypal/tests/test_paypal.py b/addons/payment_paypal/tests/test_paypal.py
index 7cbe98f1a6b..c6a44dec41f 100644
--- a/addons/payment_paypal/tests/test_paypal.py
+++ b/addons/payment_paypal/tests/test_paypal.py
@@ -187,4 +187,4 @@ class PaypalForm(PaypalCommon):
# check
self.assertEqual(tx.state, 'done', 'paypal: wrong state after receiving a valid pending notification')
self.assertEqual(tx.acquirer_reference, '08D73520KX778924N', 'paypal: wrong txn_id after receiving a valid pending notification')
- self.assertEqual(tx.date_validate, '2013-11-18 03:21:19', 'paypal: wrong validation date')
+ self.assertEqual(tx.date_validate, '2013-11-18 11:21:19', 'paypal: wrong validation date')
diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js
index 5f61deb5e0c..f9cf49864d2 100644
--- a/addons/point_of_sale/static/src/js/screens.js
+++ b/addons/point_of_sale/static/src/js/screens.js
@@ -1432,6 +1432,7 @@ var ReceiptScreenWidget = ScreenWidget.extend({
print_xml: function() {
var env = {
widget: this,
+ order: this.pos.get_order(),
receipt: this.pos.get_order().export_for_printing(),
paymentlines: this.pos.get_order().get_paymentlines()
};
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index b88542f04e6..25fe10b5315 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -502,10 +502,14 @@ class SaleOrder(models.Model):
res = {}
currency = self.currency_id or self.company_id.currency_id
for line in self.order_line:
+ base_tax = 0
for tax in line.tax_id:
group = tax.tax_group_id
res.setdefault(group, 0.0)
- res[group] += tax.compute_all(line.price_reduce, quantity=line.product_uom_qty)['taxes'][0]['amount']
+ amount = tax.compute_all(line.price_reduce + base_tax, quantity=line.product_uom_qty)['taxes'][0]['amount']
+ res[group] += amount
+ if tax.include_base_amount:
+ base_tax += amount
res = sorted(res.items(), key=lambda l: l[0].sequence)
res = map(lambda l: (l[0].name, l[1]), res)
return res
diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py
index f52f6dc331d..9c95a6682ac 100644
--- a/addons/stock/models/stock_quant.py
+++ b/addons/stock/models/stock_quant.py
@@ -44,7 +44,7 @@ class Quant(models.Model):
lot_id = fields.Many2one(
'stock.production.lot', 'Lot/Serial Number',
index=True, ondelete="restrict", readonly=True)
- cost = fields.Float('Unit Cost')
+ cost = fields.Float('Unit Cost', group_operator='avg')
owner_id = fields.Many2one(
'res.partner', 'Owner',
index=True, readonly=True,
diff --git a/addons/web/static/src/js/framework/domain_utils.js b/addons/web/static/src/js/framework/domain_utils.js
new file mode 100644
index 00000000000..8cefb7cabbe
--- /dev/null
+++ b/addons/web/static/src/js/framework/domain_utils.js
@@ -0,0 +1,19 @@
+odoo.define("web.domainUtils", function (require) {
+"use strict";
+
+var pyeval = require("web.pyeval");
+
+function domainToString(domain) {
+ if (_.isString(domain)) return domain;
+ return JSON.stringify(domain || []).replace(/false/g, "False").replace(/true/g, "True");
+}
+function stringToDomain(domain) {
+ if (!_.isString(domain)) return domain;
+ return pyeval.eval("domain", domain || "[]");
+}
+
+return {
+ domainToString: domainToString,
+ stringToDomain: stringToDomain,
+};
+});
diff --git a/addons/web/static/src/js/views/form_widgets.js b/addons/web/static/src/js/views/form_widgets.js
index a3c7bcaaff3..78d8ea7c5d1 100644
--- a/addons/web/static/src/js/views/form_widgets.js
+++ b/addons/web/static/src/js/views/form_widgets.js
@@ -11,6 +11,7 @@ var Priority = require('web.Priority');
var ProgressBar = require('web.ProgressBar');
var Dialog = require('web.Dialog');
var DomainSelector = require("web.DomainSelector");
+var DomainSelectorDialog = require("web.DomainSelectorDialog");
var common = require('web.form_common');
var formats = require('web.formats');
var framework = require('web.framework');
@@ -400,11 +401,19 @@ var FieldDomain = common.AbstractField.extend(common.ReinitializeFieldMixin).ext
e.preventDefault();
this._showSelection();
},
+ "click .o_form_field_domain_dialog_button": function (e) {
+ e.preventDefault();
+ this.openDomainDialog();
+ },
},
custom_events: {
"domain_changed": function (e) {
+ if (this.options.in_dialog) return;
this.set_value(this.domainSelector.getDomain(), true);
},
+ "domain_selected": function (e) {
+ this.set_value(e.data.domain);
+ },
},
init: function () {
this._super.apply(this, arguments);
@@ -412,6 +421,7 @@ var FieldDomain = common.AbstractField.extend(common.ReinitializeFieldMixin).ext
this.valid = true;
this.debug = session.debug;
this.options = _.defaults(this.options || {}, {
+ in_dialog: false,
model: undefined, // this option is mandatory !
fs_filters: {}, // Field selector filters (to only show a subset of available fields @see FieldSelector)
});
@@ -457,7 +467,7 @@ var FieldDomain = common.AbstractField.extend(common.ReinitializeFieldMixin).ext
this.domainSelector.destroy();
}
this.domainSelector = new DomainSelector(this, this.model, domain, {
- readonly: this.get("effective_readonly"),
+ readonly: this.get("effective_readonly") || this.options.in_dialog,
fs_filters: this.options.fs_filters,
debugMode: session.debug,
});
@@ -492,6 +502,13 @@ var FieldDomain = common.AbstractField.extend(common.ReinitializeFieldMixin).ext
disable_multiple_selection: true,
}).open();
},
+ openDomainDialog: function () {
+ new DomainSelectorDialog(this, this.model, this.get("value"), {
+ readonly: this.get("effective_readonly"),
+ fs_filters: this.options.fs_filters,
+ debugMode: session.debug,
+ }).open();
+ },
});
var FieldDate = common.AbstractField.extend(common.ReinitializeFieldMixin, {
diff --git a/addons/web/static/src/js/views/list_view_editable.js b/addons/web/static/src/js/views/list_view_editable.js
index 3f5d39e60e1..ef9bf1a6062 100644
--- a/addons/web/static/src/js/views/list_view_editable.js
+++ b/addons/web/static/src/js/views/list_view_editable.js
@@ -641,7 +641,8 @@ ListView.include(/** @lends instance.web.ListView# */{
if (saveInfo.created) {
return self.start_edition();
}
- var record = self.records[next_record](saveInfo.record);
+ var options = { wraparound: !self.is_action_enabled('create') };
+ var record = self.records[next_record](saveInfo.record, options);
if (record === undefined) {
return self.start_edition();
}
diff --git a/addons/web/static/src/js/views/search_view.js b/addons/web/static/src/js/views/search_view.js
index 9f985fc3f49..65dbb4557e7 100644
--- a/addons/web/static/src/js/views/search_view.js
+++ b/addons/web/static/src/js/views/search_view.js
@@ -466,7 +466,7 @@ var SearchView = View.extend({
*/
select_completion: function (e, ui) {
e.preventDefault();
- if(ui.item.facet.values && ui.item.facet.values.length && String(ui.item.facet.values[0].value).trim() !== "") {
+ if(ui.item.facet && ui.item.facet.values && ui.item.facet.values.length && String(ui.item.facet.values[0].value).trim() !== "") {
this.query.add(ui.item.facet);
} else {
this.query.trigger('add');
diff --git a/addons/web/static/src/js/widgets/domain_selector.js b/addons/web/static/src/js/widgets/domain_selector.js
index 5f4287a8d8d..15e967ff4d4 100644
--- a/addons/web/static/src/js/widgets/domain_selector.js
+++ b/addons/web/static/src/js/widgets/domain_selector.js
@@ -3,9 +3,9 @@ odoo.define("web.DomainSelector", function (require) {
var core = require("web.core");
var datepicker = require("web.datepicker");
+var domainUtils = require("web.domainUtils");
var formats = require ("web.formats");
var ModelFieldSelector = require("web.ModelFieldSelector");
-var pyeval = require("web.pyeval");
var Widget = require("web.Widget");
var _t = core._t;
@@ -55,7 +55,7 @@ var DomainNode = Widget.extend({
},
/// A DomainNode needs a model and domain to work. It can also receives a set of options
/// @param model - a string with the model name
- /// @param domain - an array of the prefix representation of the domain
+ /// @param domain - an array of the prefix representation of the domain (or a string which represents it)
/// @param options - an object with possible values:
/// - readonly, a boolean to indicate if the widget is readonly or not (default to true)
/// - fs_filters, an object with a series of filters to use for the field selector (see @FieldSelector)
@@ -109,7 +109,7 @@ var DomainTree = DomainNode.extend({
/// @see DomainTree._addFlattenedChildren
init: function (parent, model, domain, options) {
this._super.apply(this, arguments);
- this._initialize(domain);
+ this._initialize(domainUtils.stringToDomain(domain));
},
/// @see DomainTree.init
_initialize: function (domain) {
@@ -265,8 +265,9 @@ var DomainSelector = DomainTree.extend({
/// the widget is re-rendered and notifies the parents. If not, a warning is shown to the
/// user and the input is ignored.
"change .o_domain_debug_input": function (e) {
+ var domain;
try {
- var domain = pyeval.eval("domain", $(e.currentTarget).val() || "[]");
+ domain = domainUtils.stringToDomain($(e.currentTarget).val());
} catch (err) {
this.do_warn(_t("Syntax error"), _t("The domain you entered is not properly formed"));
return;
@@ -292,12 +293,7 @@ var DomainSelector = DomainTree.extend({
// Display technical domain if in debug mode
this.$debugInput = this.$(".o_domain_debug_input");
if (this.$debugInput.length) {
- var strValue = this.getDomain() || "[]";
- if (!_.isString(strValue)) { // TODO there is no "pyuneval" function
- strValue = JSON.stringify(strValue);
- strValue = strValue.replace(/false/g, "False").replace(/true/g, "True");
- }
- this.$debugInput.val(strValue);
+ this.$debugInput.val(domainUtils.domainToString(this.getDomain()));
}
},
});
@@ -328,6 +324,7 @@ var DomainLeaf = DomainNode.extend({
init: function (parent, model, domain, options) {
this._super.apply(this, arguments);
+ domain = domainUtils.stringToDomain(domain);
this.chain = domain[0][0];
this.operator = domain[0][1];
this.value = domain[0][2];
diff --git a/addons/web/static/src/js/widgets/domain_selector_dialog.js b/addons/web/static/src/js/widgets/domain_selector_dialog.js
new file mode 100644
index 00000000000..47bfa4e1359
--- /dev/null
+++ b/addons/web/static/src/js/widgets/domain_selector_dialog.js
@@ -0,0 +1,48 @@
+odoo.define("web.DomainSelectorDialog", function (require) {
+"use strict";
+
+var core = require("web.core");
+var Dialog = require("web.Dialog");
+var DomainSelector = require("web.DomainSelector");
+
+var _t = core._t;
+
+return Dialog.extend({
+ init: function (parent, model, domain, options) {
+ this.model = model;
+ this.options = _.extend({
+ readonly: true,
+ fs_filters: {},
+ debugMode: false,
+ }, options || {});
+
+ var buttons;
+ if (this.options.readonly) {
+ buttons = [
+ {text: _t("Close"), close: true},
+ ];
+ } else {
+ buttons = [
+ {text: _t("Save"), classes: "btn-primary", close: true, click: function () {
+ this.trigger_up("domain_selected", {domain: this.domainSelector.getDomain()});
+ }},
+ {text: _t("Discard"), close: true},
+ ];
+ }
+
+ this._super(parent, _.extend({}, {
+ title: _t("Domain"),
+ buttons: buttons,
+ }, options || {}));
+
+ this.domainSelector = new DomainSelector(this, model, domain, options);
+ },
+ start: function () {
+ this.$el.css("overflow", "visible").closest(".modal-dialog").css("height", "auto"); // This restores default modal height (bootstrap) and allows field selector to overflow
+ return $.when(
+ this._super.apply(this, arguments),
+ this.domainSelector.appendTo(this.$el)
+ );
+ },
+});
+});
diff --git a/addons/web/static/src/less/domain_selector.less b/addons/web/static/src/less/domain_selector.less
index 7dced900b21..f3917526ec5 100644
--- a/addons/web/static/src/less/domain_selector.less
+++ b/addons/web/static/src/less/domain_selector.less
@@ -24,7 +24,10 @@
&.o_domain_selector {
&.o_edit_mode {
position: relative;
- padding-right: @o-domain-selector-panel-space;
+
+ > .o_domain_node_children_container {
+ padding-right: @o-domain-selector-panel-space;
+ }
}
> .o_domain_node_children_container {
diff --git a/addons/web/static/src/less/form_view_layout.less b/addons/web/static/src/less/form_view_layout.less
index 281cf0d217e..fc5522035e5 100644
--- a/addons/web/static/src/less/form_view_layout.less
+++ b/addons/web/static/src/less/form_view_layout.less
@@ -91,7 +91,7 @@
// Title
.oe_title {
> h1, > h2, > h3 {
- width: 100%; // Needed because inline-block when is a hx.o_row
+ width: 100%; // Needed because inline-block when is a hx.o_row
margin-top: 0;
margin-bottom: 0;
line-height: inherit;
@@ -276,6 +276,20 @@
padding: 0 1em;
}
}
+
+ .o_form_field_domain {
+ > .o_form_field_domain_panel {
+ margin-top: 8px;
+ }
+ &.o_inline_mode.o_edit_mode {
+ position: relative;
+
+ > .o_form_field_domain_panel {
+ .o-position-absolute(0, 0);
+ margin-top: 0;
+ }
+ }
+ }
}
// Overriden style when form view in modal
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index f6ac60c4d7e..2ee261a3bdd 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -670,13 +670,16 @@
t-att-maxlength="widget.field.size"/>
-
+
-
+
- Records Selected
-
+
Invalid domain
+
@@ -684,7 +687,7 @@
-
+
@@ -712,9 +715,9 @@
- Match the records with the following rule:
+ Match records with the following rule:
- Match the records with
+ Match records withof the following rules:
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml
index 533e8e8b23a..c0a9b2c372f 100644
--- a/addons/web/views/webclient_templates.xml
+++ b/addons/web/views/webclient_templates.xml
@@ -134,6 +134,7 @@
+
@@ -155,6 +156,7 @@
+
diff --git a/addons/web_editor/static/src/js/backend.js b/addons/web_editor/static/src/js/backend.js
index 9c6f1df5e8c..ce8643f04de 100644
--- a/addons/web_editor/static/src/js/backend.js
+++ b/addons/web_editor/static/src/js/backend.js
@@ -161,6 +161,11 @@ var FieldTextHtmlSimple = widget.extend({
return !this.get('value') || this.get('value') === "
" || !this.get('value').match(/\S/);
},
commit_value: function () {
+ /* Switch to WYSIWYG mode if currently in code view */
+ if (session.debug) {
+ var layoutInfo = this.$textarea.data('layoutInfo');
+ $.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo, false);
+ }
if (this.options['style-inline']) {
transcoder.class_to_style(this.$content);
transcoder.font_to_img(this.$content);
@@ -377,6 +382,11 @@ var FieldTextHtml = widget.extend({
this._dirty_flag = false;
return this.editor.save();
} else if (this._dirty_flag && this.editor && this.editor.buildingBlock) {
+ /* Switch to WYSIWYG mode if currently in code view */
+ if (session.debug) {
+ var layoutInfo = this.editor.rte.editable().data('layoutInfo');
+ $.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo, false);
+ }
this.editor.buildingBlock.clean_for_save();
this.internal_set_value( this.$content.html() );
}
diff --git a/addons/web_editor/static/src/js/rte.summernote.js b/addons/web_editor/static/src/js/rte.summernote.js
index 1789ed69e7e..9f908a245bc 100644
--- a/addons/web_editor/static/src/js/rte.summernote.js
+++ b/addons/web_editor/static/src/js/rte.summernote.js
@@ -511,12 +511,32 @@ function prettify_html(html) {
}
return result;
}
-$.summernote.pluginEvents.codeview = function (event, editor, layoutInfo) {
+
+/*
+ * This override when clicking on the 'Code View' button has two aims:
+ *
+ * - have our own code view implementation for FieldTextHtml
+ * - add an 'enable' paramater to call the function directly and allow us to
+ * disable (false) or enable (true) the code view mode.
+ */
+$.summernote.pluginEvents.codeview = function (event, editor, layoutInfo, enable) {
+ if (layoutInfo === undefined) {
+ return;
+ }
if (layoutInfo.toolbar) {
+ // if editor inline (FieldTextHtmlSimple)
+ var is_activated = $.summernote.eventHandler.modules.codeview.isActivated(layoutInfo);
+ if (is_activated === enable) {
+ return;
+ }
return eventHandler.modules.codeview.toggle(layoutInfo);
} else {
+ // if editor iframe (FieldTextHtml)
var $editor = layoutInfo.editor();
var $textarea = $editor.prev('textarea');
+ if ($textarea.is('textarea') === enable) {
+ return;
+ }
if (!$textarea.length) {
// init and create texarea
diff --git a/addons/web_kanban/static/src/js/kanban_view.js b/addons/web_kanban/static/src/js/kanban_view.js
index 4917c24b241..7cce7617da9 100644
--- a/addons/web_kanban/static/src/js/kanban_view.js
+++ b/addons/web_kanban/static/src/js/kanban_view.js
@@ -710,24 +710,19 @@ var KanbanView = View.extend({
var context = {};
context['default_' + this.group_by_field] = column.id;
var name = event.data.value;
- this.dataset.name_create(name, context).then(function on_success (data) {
+ this.dataset.name_create(name).done(function(data) {
add_record(data[0]);
- }, function on_fail (event) {
+ }).fail(function(error, event) {
event.preventDefault();
- var popup = new form_common.SelectCreatePopup(this);
- popup.select_element(
- self.model,
- {
- title: _t("Create: "),
- initial_view: "form",
- disable_multiple_selection: true
- },
- [],
- {"default_name": name}
- );
- popup.on("elements_selected", self, function(element_ids) {
- add_record(element_ids[0]);
- });
+ var dialog = new form_common.FormViewDialog(self, {
+ res_model: self.model,
+ context: _.extend({"default_name": name}, context),
+ title: _t("Create"),
+ disable_multiple_selection: true,
+ on_selected: function(element_ids) {
+ add_record(element_ids[0]);
+ }
+ }).open();
});
function add_record(id) {
@@ -817,8 +812,20 @@ function transform_qweb_template (node, fvg, many2manys) {
} else if (fields_registry.contains(ftype)) {
// do nothing, the kanban record will handle it
} else {
- node.tag = qweb.prefix;
- node.attrs[qweb.prefix + '-esc'] = 'record.' + node.attrs.name + '.value';
+ if (node.attrs.bold) {
+ node.tag = 'strong';
+ var children_node = {
+ attrs: {},
+ children: [],
+ tag: qweb.prefix,
+ };
+ children_node.attrs[qweb.prefix + '-esc'] = 'record.' + node.attrs.name + '.value';
+ node.children.push(children_node);
+ } else {
+ node.tag = qweb.prefix;
+ node.attrs[qweb.prefix + '-esc'] = 'record.' + node.attrs.name + '.value';
+ }
+
}
break;
case 'button':
diff --git a/addons/web_kanban/static/src/less/kanban_view.less b/addons/web_kanban/static/src/less/kanban_view.less
index 877559ddf1f..0640fe9f3e3 100644
--- a/addons/web_kanban/static/src/less/kanban_view.less
+++ b/addons/web_kanban/static/src/less/kanban_view.less
@@ -65,7 +65,9 @@
}
.o_dropdown_kanban {
- float: right;
+ position: absolute;
+ top: 5px;
+ right: 10px;
@media (max-width: @screen-xs-max) {
visibility: visible;
}
diff --git a/addons/website_crm/views/website_crm_config_settings_views.xml b/addons/website_crm/views/website_crm_config_settings_views.xml
index 8f8c4e9dbe6..a141b4e4509 100644
--- a/addons/website_crm/views/website_crm_config_settings_views.xml
+++ b/addons/website_crm/views/website_crm_config_settings_views.xml
@@ -6,7 +6,7 @@
diff --git a/doc/howtos/backend.rst b/doc/howtos/backend.rst
index 8c67eedecb1..e43f31ea2e3 100644
--- a/doc/howtos/backend.rst
+++ b/doc/howtos/backend.rst
@@ -54,7 +54,7 @@ Business objects
by Odoo based on their configuration
Data files
- XML or CSV files declaring metadata (views or workflows), configuration
+ XML or CSV files declaring metadata (views or reports), configuration
data (modules parameterization), demonstration data and more
Web controllers
diff --git a/doc/howtos/backend/exercise-dashboard b/doc/howtos/backend/exercise-dashboard
index 4a6eceb8fd9..91bc239dd7a 100644
--- a/doc/howtos/backend/exercise-dashboard
+++ b/doc/howtos/backend/exercise-dashboard
@@ -17,7 +17,6 @@ Index: addons/openacademy/__manifest__.py
@@ -31,6 +31,7 @@
'views/openacademy.xml',
'views/partner.xml',
- 'views/session_workflow.xml',
+ 'views/session_board.xml',
'reports.xml',
],
diff --git a/doc/howtos/backend/exercise-report b/doc/howtos/backend/exercise-report
index 6e8a2d78705..351d34a92b2 100644
--- a/doc/howtos/backend/exercise-report
+++ b/doc/howtos/backend/exercise-report
@@ -6,9 +6,9 @@ Index: addons/openacademy/__manifest__.py
--- addons.orig/openacademy/__manifest__.py 2014-08-29 08:39:43.975536806 +0200
+++ addons/openacademy/__manifest__.py 2014-08-29 08:39:52.000000000 +0200
@@ -31,6 +31,7 @@
+ 'templates.xml',
'views/openacademy.xml',
'views/partner.xml',
- 'views/session_workflow.xml',
+ 'reports.xml',
],
# only loaded in demonstration mode
diff --git a/doc/reference/mixins.rst b/doc/reference/mixins.rst
index 25bf8d4a41b..c172c79dd2a 100644
--- a/doc/reference/mixins.rst
+++ b/doc/reference/mixins.rst
@@ -738,6 +738,65 @@ you to make your alias easily configurable from the record's form view.
res = super(BusinessExpense, self).message_new(msg, custom_values=defaults)
return res
+.. _reference/mixins/mail/activities:
+
+Activities tracking
+-------------------
+
+Activities are actions users have to take on a document like making a phone call
+or organizing a meeting. Activities come with the mail module as they are
+integrated in the Chatter but are *not bundled with mail.thread*. Activities
+are records of the ``mail.activity`` class, which have a type (``mail.activity.type``),
+name, description, scheduled time (among others). Pending activities are visible
+above the message history in the chatter widget.
+
+You can integrate activities using the ``mail.activity.mixin`` class on your object
+and the specific widgets to display them (via the field ``activity_ids``) in the form
+view and kanban view of your records (``mail_activity`` and ``kanban_activity``
+widgets, respectively).
+
+.. admonition:: Example
+
+ Organizing a business trip is a tedious process and tracking needed activities
+ like ordering plane tickets or a cab for the airport could be useful. To do so,
+ we will add the activities mixin on our model and display the next planned activities
+ in the message history of our trip.
+
+ .. code-block:: python
+
+ class BusinessTrip(models.Model):
+ _name = 'business.trip'
+ _inherit = ['mail.thread', 'mail.activity.mixin']
+ _description = 'Business Trip'
+
+ name = fields.Char()
+ # [...]
+
+ We modify the form view of our trips to display their next activites:
+
+ .. code-block:: xml
+
+
+ business.trip.form
+ business.trip
+
+
+
+
+
+You can find concrete examples of integration in the following models:
+
+* ``crm.lead`` in the CRM (*crm*) Application
+* ``sale.order`` in the Sales (*sale*) Application
+* ``project.task`` in the Project (*poject*) Application
+
.. _reference/mixins/website:
diff --git a/odoo-bin b/odoo-bin
index b2a0e17ff83..2670299b150 100755
--- a/odoo-bin
+++ b/odoo-bin
@@ -1,4 +1,7 @@
#!/usr/bin/env python
+
+# set server timezone in UTC before time module imported
+__import__('os').environ['TZ'] = 'UTC'
__import__('pkg_resources').declare_namespace('odoo.addons')
import odoo
diff --git a/odoo/__init__.py b/odoo/__init__.py
index 69e10a4c011..dbf17c02200 100644
--- a/odoo/__init__.py
+++ b/odoo/__init__.py
@@ -27,15 +27,12 @@ multi_process = False
#----------------------------------------------------------
# libc UTC hack
#----------------------------------------------------------
-# Make sure the OpenERP server runs in UTC. This is especially necessary
-# under Windows as under Linux it seems the real import of time is
-# sufficiently deferred so that setting the TZ environment variable
-# in odoo.cli.server was working.
+# Make sure the OpenERP server runs in UTC.
import os
-os.environ['TZ'] = 'UTC' # Set the timezone...
-import time # ... *then* import time.
-del os
-del time
+os.environ['TZ'] = 'UTC' # Set the timezone
+import time
+if hasattr(time, 'tzset'):
+ time.tzset()
#----------------------------------------------------------
# Shortcuts
diff --git a/odoo/addons/base/ir/ir_ui_view.py b/odoo/addons/base/ir/ir_ui_view.py
index 5c74c83b9a9..cd3eb73f96e 100644
--- a/odoo/addons/base/ir/ir_ui_view.py
+++ b/odoo/addons/base/ir/ir_ui_view.py
@@ -123,6 +123,31 @@ def get_view_arch_from_file(filename, xmlid):
_logger.warning("Could not find view arch definition in file '%s' for xmlid '%s'", filename, xmlid)
return None
+def add_text_before(node, text):
+ """ Add text before ``node`` in its XML tree. """
+ if text is None:
+ return
+ prev = node.getprevious()
+ if prev is not None:
+ prev.tail = (prev.tail or "") + text
+ else:
+ parent = node.getparent()
+ parent.text = (parent.text or "") + text
+
+def add_text_inside(node, text):
+ """ Add text inside ``node``. """
+ if text is None:
+ return
+ if len(node):
+ node[-1].tail = (node[-1].tail or "") + text
+ else:
+ node.text = (node.text or "") + text
+
+def remove_element(node):
+ """ Remove ``node`` but not its tail, from its XML tree. """
+ add_text_before(node, node.tail)
+ node.getparent().remove(node)
+
xpath_utils = etree.FunctionNamespace(None)
xpath_utils['hasclass'] = _hasclass
@@ -555,21 +580,26 @@ actual arch.
node.set(attribute, value)
elif attribute in node.attrib:
del node.attrib[attribute]
- else:
- sib = node.getnext()
+ elif pos == 'inside':
+ add_text_inside(node, spec.text)
for child in spec:
- if pos == 'inside':
- node.append(child)
- elif pos == 'after':
- if sib is None:
- node.addnext(child)
- node = child
- else:
- sib.addprevious(child)
- elif pos == 'before':
- node.addprevious(child)
- else:
- self.raise_view_error(_("Invalid position attribute: '%s'") % pos, inherit_id)
+ node.append(child)
+ elif pos == 'after':
+ # add a sentinel element right after node, insert content of
+ # spec before the sentinel, then remove the sentinel element
+ sentinel = E.sentinel()
+ node.addnext(sentinel)
+ add_text_before(sentinel, spec.text)
+ for child in spec:
+ sentinel.addprevious(child)
+ remove_element(sentinel)
+ elif pos == 'before':
+ add_text_before(node, spec.text)
+ for child in spec:
+ node.addprevious(child)
+ else:
+ self.raise_view_error(_("Invalid position attribute: '%s'") % pos, inherit_id)
+
else:
attrs = ''.join([
' %s="%s"' % (attr, spec.get(attr))
diff --git a/odoo/addons/base/tests/test_views.py b/odoo/addons/base/tests/test_views.py
index 7790f2d11db..09f84661f3d 100644
--- a/odoo/addons/base/tests/test_views.py
+++ b/odoo/addons/base/tests/test_views.py
@@ -736,6 +736,66 @@ class TestViews(ViewCase):
),
string="Replacement title", version="7.0"))
+ def test_view_inheritance_text_inside(self):
+ """ Test view inheritance when adding elements and text. """
+ view1 = self.View.create({
+ 'name': "alpha",
+ 'model': 'ir.ui.view',
+ 'arch': '',
+ })
+ view2 = self.View.create({
+ 'name': "beta",
+ 'model': 'ir.ui.view',
+ 'inherit_id': view1.id,
+ 'arch': '