[MERGE] forward port branch saas-14 up to e7d174a142

This commit is contained in:
Christophe Simonis
2017-12-04 19:25:51 +01:00
20 changed files with 128 additions and 30 deletions
+1 -1
View File
@@ -726,7 +726,7 @@ class WizardMultiChartsAccounts(models.TransientModel):
if company_id: if company_id:
company = self.env['res.company'].browse(company_id) company = self.env['res.company'].browse(company_id)
currency_id = company.on_change_country(company.country_id.id)['value']['currency_id'] currency_id = company.on_change_country(company.country_id.id)['value']['currency_id']
res.update({'currency_id': currency_id}) res.update({'currency_id': currency_id.id})
chart_templates = account_chart_template.search([('visible', '=', True)]) chart_templates = account_chart_template.search([('visible', '=', True)])
if chart_templates: if chart_templates:
+1 -1
View File
@@ -68,7 +68,7 @@ class ChangeProductionQty(models.TransientModel):
cycle_number * operation.time_cycle * 100.0 / operation.workcenter_id.time_efficiency) cycle_number * operation.time_cycle * 100.0 / operation.workcenter_id.time_efficiency)
quantity = wo.qty_production - wo.qty_produced quantity = wo.qty_production - wo.qty_produced
if production.product_id.tracking == 'serial': if production.product_id.tracking == 'serial':
quantity = 1.0 if float_is_zero(quantity, precision_digits=precision) else 0.0 quantity = 1.0 if not float_is_zero(quantity, precision_digits=precision) else 0.0
else: else:
quantity = quantity if (quantity > 0) else 0 quantity = quantity if (quantity > 0) else 0
if float_is_zero(quantity, precision_digits=precision): if float_is_zero(quantity, precision_digits=precision):
@@ -1125,12 +1125,15 @@ var ClientListScreenWidget = ScreenWidget.extend({
var self = this; var self = this;
var order = this.pos.get_order(); var order = this.pos.get_order();
if( this.has_client_changed() ){ if( this.has_client_changed() ){
if ( this.new_client ) { var default_fiscal_position_id = _.find(this.pos.fiscal_positions, function(fp) {
return fp.id === self.pos.config.default_fiscal_position_id[0];
});
if ( this.new_client && this.new_client.property_account_position_id ) {
order.fiscal_position = _.find(this.pos.fiscal_positions, function (fp) { order.fiscal_position = _.find(this.pos.fiscal_positions, function (fp) {
return fp.id === self.new_client.property_account_position_id[0]; return fp.id === self.new_client.property_account_position_id[0];
}); }) || default_fiscal_position_id;
} else { } else {
order.fiscal_position = undefined; order.fiscal_position = default_fiscal_position_id;
} }
order.set_client(this.new_client); order.set_client(this.new_client);
+5 -2
View File
@@ -32,10 +32,13 @@ class ProductCategory(models.Model):
help="The number of products under this category (Does not consider the children categories)") help="The number of products under this category (Does not consider the children categories)")
def _compute_product_count(self): def _compute_product_count(self):
read_group_res = self.env['product.template'].read_group([('categ_id', 'in', self.ids)], ['categ_id'], ['categ_id']) read_group_res = self.env['product.template'].read_group([('categ_id', 'child_of', self.ids)], ['categ_id'], ['categ_id'])
group_data = dict((data['categ_id'][0], data['categ_id_count']) for data in read_group_res) group_data = dict((data['categ_id'][0], data['categ_id_count']) for data in read_group_res)
for categ in self: for categ in self:
categ.product_count = group_data.get(categ.id, 0) product_count = 0
for sub_categ_id in categ.search([('id', 'child_of', categ.id)]).ids:
product_count += group_data.get(sub_categ_id, 0)
categ.product_count = product_count
@api.constrains('parent_id') @api.constrains('parent_id')
def _check_category_recursion(self): def _check_category_recursion(self):
+1 -1
View File
@@ -112,7 +112,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<search string="Product"> <search string="Product">
<field name="name" string="Product" filter_domain="['|','|',('default_code','ilike',self),('name','ilike',self),('barcode','ilike',self)]"/> <field name="name" string="Product" filter_domain="['|','|',('default_code','ilike',self),('name','ilike',self),('barcode','ilike',self)]"/>
<field name="categ_id" filter_domain="[('categ_id', 'child_of', self)]"/> <field name="categ_id" filter_domain="[('categ_id', 'child_of', raw_value)]"/>
<separator/> <separator/>
<filter string="Services" name="services" domain="[('type','=','service')]"/> <filter string="Services" name="services" domain="[('type','=','service')]"/>
<filter string="Products" name="consumable" domain="[('type', 'in', ['consu', 'product'])]"/> <filter string="Products" name="consumable" domain="[('type', 'in', ['consu', 'product'])]"/>
+4 -1
View File
@@ -243,7 +243,8 @@ class Project(models.Model):
project = super(Project, self).copy(default) project = super(Project, self).copy(default)
for follower in self.message_follower_ids: for follower in self.message_follower_ids:
project.message_subscribe(partner_ids=follower.partner_id.ids, subtype_ids=follower.subtype_ids.ids) project.message_subscribe(partner_ids=follower.partner_id.ids, subtype_ids=follower.subtype_ids.ids)
self.map_tasks(project.id) if 'tasks' not in default:
self.map_tasks(project.id)
return project return project
@api.model @api.model
@@ -264,6 +265,8 @@ class Project(models.Model):
if 'active' in vals: if 'active' in vals:
# archiving/unarchiving a project does it on its tasks, too # archiving/unarchiving a project does it on its tasks, too
self.with_context(active_test=False).mapped('tasks').write({'active': vals['active']}) self.with_context(active_test=False).mapped('tasks').write({'active': vals['active']})
# archiving/unarchiving a project implies that we don't want to use the analytic account anymore
self.with_context(active_test=False).mapped('analytic_account_id').write({'active': vals['active']})
return res return res
@api.multi @api.multi
@@ -1250,7 +1250,7 @@ var FieldBinaryFile = FieldBinary.extend({
this._super(); this._super();
if (this.get("effective_readonly")) { if (this.get("effective_readonly")) {
this.$el.click(function(ev) { this.$el.click(function(ev) {
if (self.get('value')) { if (self.get('value') && self.view.datarecord.id) {
self.on_save_as(ev); self.on_save_as(ev);
} }
return false; return false;
@@ -1268,6 +1268,11 @@ var FieldBinaryFile = FieldBinary.extend({
this.do_toggle(!!this.get('value')); this.do_toggle(!!this.get('value'));
if (this.get('value')) { if (this.get('value')) {
this.$el.empty().append($("<span/>").addClass('fa fa-download')); this.$el.empty().append($("<span/>").addClass('fa fa-download'));
if (this.view.datarecord.id) {
this.$el.css('cursor', 'pointer');
} else {
this.$el.css('cursor', 'not-allowed');
}
if (filename) { if (filename) {
this.$el.append(" " + filename); this.$el.append(" " + filename);
} }
@@ -100,7 +100,7 @@ return Widget.extend({
})); }));
} else { } else {
var chart = this['display_' + this.mode](); var chart = this['display_' + this.mode]();
if (chart) { if (chart && chart.tooltip.chartContainer) {
chart.tooltip.chartContainer(this.$el[0]); chart.tooltip.chartContainer(this.$el[0]);
} }
} }
@@ -152,7 +152,7 @@ var Field = Input.extend( /** @lends instance.web.search.Field# */ {
if (domain) { if (domain) {
value_to_domain = function (facetValue) { value_to_domain = function (facetValue) {
return new data.CompoundDomain(domain) return new data.CompoundDomain(domain)
.set_eval_context({self: self.value_from(facetValue)}); .set_eval_context({self: self.value_from(facetValue), raw_value: facetValue.attributes.value});
}; };
} else { } else {
value_to_domain = function (facetValue) { value_to_domain = function (facetValue) {
+2 -2
View File
@@ -355,8 +355,8 @@ var RTE = Widget.extend({
var $el = $(this); var $el = $(this);
$el.find('[class]').filter(function () { $el.find('[class]').filter(function () {
if (!this.className.match(/\S/)) { if (!this.getAttribute('class').match(/\S/)) {
this.removeAttribute("class"); this.removeAttribute('class');
} }
}); });
@@ -415,16 +415,35 @@ animation.registry.media_video = animation.Class.extend({
start: function () { start: function () {
// TODO: this code should be refactored to make more sense and be better // TODO: this code should be refactored to make more sense and be better
// integrated with Odoo (this refactoring should be done in master). // integrated with Odoo (this refactoring should be done in master).
this.$target.find('iframe').remove();
if (!this.$target.has('.media_iframe_video_size').length) { var def = this._super.apply(this, arguments);
var editor = '<div class="css_editable_mode_display">&nbsp;</div>'; if (this.$target.children('iframe').length) {
var size = '<div class="media_iframe_video_size">&nbsp;</div>'; // There already is an <iframe/>, do nothing
this.$target.html(editor+size); return def;
} }
// rebuilding the iframe, from https://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/
this.$target.html(this.$target.html()+'<iframe sandbox="allow-scripts allow-same-origin" src="'+_.escape(this.$target.data("oe-expression"))+'" frameborder="0" allowfullscreen="allowfullscreen"></iframe>'); // Bug fix / compatibility: empty the <div/> element as all information
return this._super.apply(this, arguments); // to rebuild the iframe should have been saved on the <div/> element
this.$target.empty();
// Add extra content for size / edition
this.$target.append(
'<div class="css_editable_mode_display">&nbsp;</div>' +
'<div class="media_iframe_video_size">&nbsp;</div>'
);
// Rebuild the iframe. Depending on version / compatibility / instance,
// the src is saved in the 'data-src' attribute or the
// 'data-oe-expression' one (the latter is used as a workaround in 10.0
// system but should obviously be reviewed in master).
this.$target.append($('<iframe/>', {
src: _.escape(this.$target.data('oe-expression') || this.$target.data('src')),
frameborder: '0',
allowfullscreen: 'allowfullscreen',
sandbox: 'allow-scripts allow-same-origin', // https://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/
}));
return def;
}, },
}); });
+1 -1
View File
@@ -429,7 +429,7 @@ class WebsiteSale(http.Controller):
Partner = order.partner_id.with_context(show_address=1).sudo() Partner = order.partner_id.with_context(show_address=1).sudo()
shippings = Partner.search([ shippings = Partner.search([
("id", "child_of", order.partner_id.commercial_partner_id.ids), ("id", "child_of", order.partner_id.commercial_partner_id.ids),
'|', ("type", "=", "delivery"), ("id", "=", order.partner_id.commercial_partner_id.id) '|', ("type", "in", ["delivery", "other"]), ("id", "=", order.partner_id.commercial_partner_id.id)
], order='id desc') ], order='id desc')
if shippings: if shippings:
if kw.get('partner_id') or 'use_billing' in kw: if kw.get('partner_id') or 'use_billing' in kw:
+5 -2
View File
@@ -1106,6 +1106,7 @@
<t t-foreach="shippings" t-as="ship"> <t t-foreach="shippings" t-as="ship">
<div class="col-sm-12 col-md-6 one_kanban"> <div class="col-sm-12 col-md-6 one_kanban">
<t t-call="website_sale.address_kanban"> <t t-call="website_sale.address_kanban">
<t t-set="actual_partner" t-value="order.partner_id" />
<t t-set='contact' t-value="ship"/> <t t-set='contact' t-value="ship"/>
<t t-set='selected' t-value="order.partner_shipping_id==ship"/> <t t-set='selected' t-value="order.partner_shipping_id==ship"/>
<t t-set='readonly' t-value="bool(len(shippings)==1)"/> <t t-set='readonly' t-value="bool(len(shippings)==1)"/>
@@ -1139,7 +1140,7 @@
</t> </t>
<input type='submit'/> <input type='submit'/>
</form> </form>
<a class='btn btn-link pull-right fa fa-edit js_edit_address no-decoration' title="Edit this address"></a> <a t-if="not actual_partner or (ship.id in actual_partner.ids + actual_partner.child_ids.ids)" class='btn btn-link pull-right fa fa-edit js_edit_address no-decoration' title="Edit this address"></a>
<div t-att-class="'panel panel-default %s' % (selected and 'border_primary' or 'js_change_shipping')"> <div t-att-class="'panel panel-default %s' % (selected and 'border_primary' or 'js_change_shipping')">
<div class='panel-body' style='min-height: 130px;'> <div class='panel-body' style='min-height: 130px;'>
<t t-esc="contact" t-options="dict(widget='contact', fields=['name', 'address'], no_marker=True)"/> <t t-esc="contact" t-options="dict(widget='contact', fields=['name', 'address'], no_marker=True)"/>
@@ -1175,7 +1176,9 @@
<t t-if="mode == ('new', 'billing')"> <t t-if="mode == ('new', 'billing')">
<h3 class="page-header mt32 ml16">Your Address <h3 class="page-header mt32 ml16">Your Address
<small> or </small> <small> or </small>
<t t-set='connect' t-value="request.env['ir.config_parameter'].sudo().get_param('auth_signup.allow_uninvited') == 'True' and ('signup', 'Sign Up') or ('login', 'Log In')"/> <t t-set="signup_text">Sign Up</t>
<t t-set="login_text">Log In</t>
<t t-set='connect' t-value="request.env['ir.config_parameter'].sudo().get_param('auth_signup.allow_uninvited') == 'True' and ('signup', signup_text) or ('login', login_text)"/>
<a t-attf-href='/web/{{connect[0]}}?redirect=/shop/checkout' class='btn btn-primary' style="margin-top: -11px"><t t-esc='connect[1]'/></a> <a t-attf-href='/web/{{connect[0]}}?redirect=/shop/checkout' class='btn btn-primary' style="margin-top: -11px"><t t-esc='connect[1]'/></a>
</h3> </h3>
</t> </t>
+11
View File
@@ -0,0 +1,11 @@
France, 2017-11-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,
Maximilien TANTIN <maximilien.tantin@gmail.com> https://github.com/MTantin
+11
View File
@@ -0,0 +1,11 @@
Germany, 2017-12-01
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,
Alex Vai satriani-vai@users.noreply.github.com https://github.com/satriani-vai
+16 -2
View File
@@ -5,7 +5,7 @@ import threading
import time import time
import psycopg2 import psycopg2
import pytz import pytz
from datetime import datetime from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
import odoo import odoo
@@ -15,6 +15,7 @@ from odoo.exceptions import UserError
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
BASE_VERSION = odoo.modules.load_information_from_description_file('base')['version'] BASE_VERSION = odoo.modules.load_information_from_description_file('base')['version']
MAX_FAIL_TIME = timedelta(hours=5) # chosen with a fair roll of the dice
class BadVersion(Exception): class BadVersion(Exception):
@@ -168,7 +169,7 @@ class ir_cron(models.Model):
(version,) = cr.fetchone() (version,) = cr.fetchone()
cr.execute("SELECT COUNT(*) FROM ir_module_module WHERE state LIKE %s", ['to %']) cr.execute("SELECT COUNT(*) FROM ir_module_module WHERE state LIKE %s", ['to %'])
(changes,) = cr.fetchone() (changes,) = cr.fetchone()
if not version or changes: if version is None:
raise BadModuleState() raise BadModuleState()
elif version != BASE_VERSION: elif version != BASE_VERSION:
raise BadVersion() raise BadVersion()
@@ -179,6 +180,19 @@ class ir_cron(models.Model):
ORDER BY priority""") ORDER BY priority""")
jobs = cr.dictfetchall() jobs = cr.dictfetchall()
if changes:
if not jobs:
raise BadModuleState()
# nextcall is never updated if the cron is not executed,
# it is used as a sentinel value to check whether cron jobs
# have been locked for a long time (stuck)
parse = fields.Datetime.from_string
oldest = min([parse(job['nextcall']) for job in jobs])
if datetime.now() - oldest > MAX_FAIL_TIME:
odoo.modules.reset_modules_state(db_name)
else:
raise BadModuleState()
for job in jobs: for job in jobs:
lock_cr = db.cursor() lock_cr = db.cursor()
try: try:
+2 -1
View File
@@ -1647,7 +1647,8 @@ class BaseModel(object):
order = '"%s" %s' % (order_field, '' if len(order_split) == 1 else order_split[1]) order = '"%s" %s' % (order_field, '' if len(order_split) == 1 else order_split[1])
orderby_terms.append(order) orderby_terms.append(order)
elif order_field in aggregated_fields: elif order_field in aggregated_fields:
orderby_terms.append(order_part) order_split[0] = '"' + order_field + '"'
orderby_terms.append(' '.join(order_split))
else: else:
# Cannot order by a field that will not appear in the results (needs to be grouped or aggregated) # Cannot order by a field that will not appear in the results (needs to be grouped or aggregated)
_logger.warn('%s: read_group order by `%s` ignored, cannot sort on empty columns (not grouped/aggregated)', _logger.warn('%s: read_group order by `%s` ignored, cannot sort on empty columns (not grouped/aggregated)',
+1 -1
View File
@@ -7,7 +7,7 @@
from . import db, graph, loading, migration, module, registry from . import db, graph, loading, migration, module, registry
from odoo.modules.loading import load_modules from odoo.modules.loading import load_modules, reset_modules_state
from odoo.modules.module import ( from odoo.modules.module import (
adapt_version, adapt_version,
+21
View File
@@ -421,3 +421,24 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
finally: finally:
cr.close() cr.close()
def reset_modules_state(db_name):
"""
Resets modules flagged as "to x" to their original state
"""
# Warning, this function was introduced in response to commit 763d714
# which locks cron jobs for dbs which have modules marked as 'to %'.
# The goal of this function is to be called ONLY when module
# installation/upgrade/uninstallation fails, which is the only known case
# for which modules can stay marked as 'to %' for an indefinite amount
# of time
db = odoo.sql_db.db_connect(db_name)
with db.cursor() as cr:
cr.execute(
"UPDATE ir_module_module SET state='installed' WHERE state IN ('to remove', 'to upgrade')"
)
cr.execute(
"UPDATE ir_module_module SET state='uninstalled' WHERE state='to install'"
)
_logger.warning("Transient module states were reset")
+5 -1
View File
@@ -80,7 +80,11 @@ class Registry(Mapping):
try: try:
registry.setup_signaling() registry.setup_signaling()
# This should be a method on Registry # This should be a method on Registry
odoo.modules.load_modules(registry._db, force_demo, status, update_module) try:
odoo.modules.load_modules(registry._db, force_demo, status, update_module)
except Exception:
odoo.modules.reset_modules_state(db_name)
raise
except Exception: except Exception:
_logger.exception('Failed to load registry') _logger.exception('Failed to load registry')
del cls.registries[db_name] del cls.registries[db_name]