summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2017-12-06 11:59:38 +0100
committerChristophe Simonis <chs@odoo.com>2017-12-06 11:59:38 +0100
commitf96a797fe6238306ec70d3db013b8a4d348ef181 (patch)
tree40990f6ca69840a52421c4a52f68e439ba1a5b42
parentdc5755698cc41e0c5f51ab5dccee1356a0f23a3f (diff)
parentb8540eefe3b82fc4a54c8a808ce3bfa0bea8a29a (diff)
[MERGE] forward port branch saas-16 up to b8540eefe3b
-rw-r--r--addons/account/models/chart_template.py2
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_model.js5
-rw-r--r--addons/auth_oauth/data/auth_oauth_data.xml2
-rw-r--r--addons/hr_payroll/data/hr_payroll_data.xml2
-rw-r--r--addons/l10n_be_hr_payroll/data/l10n_be_hr_payroll_data.xml32
-rw-r--r--addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py8
-rw-r--r--addons/l10n_be_hr_payroll/views/l10n_be_hr_payroll_view.xml4
-rw-r--r--addons/mrp/wizard/change_production_qty.py2
-rw-r--r--addons/point_of_sale/static/src/js/db.js2
-rw-r--r--addons/point_of_sale/static/src/js/screens.js3
-rw-r--r--addons/product/models/product.py7
-rw-r--r--addons/product/views/product_views.xml2
-rw-r--r--addons/project/models/project.py5
-rw-r--r--addons/sales_team/models/crm_team.py2
-rw-r--r--addons/web/static/lib/bootstrap-datetimepicker/src/js/bootstrap-datetimepicker.js8
-rw-r--r--addons/web/static/src/js/chrome/search_filters.js4
-rw-r--r--addons/web/static/src/js/chrome/search_inputs.js2
-rw-r--r--addons/web/static/src/js/chrome/search_view.js2
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js14
-rw-r--r--addons/web/static/src/js/views/graph/graph_renderer.js2
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js45
-rw-r--r--addons/web_editor/static/src/js/backend/fields.js3
-rw-r--r--addons/web_editor/static/src/js/editor/rte.js2
-rw-r--r--addons/website/static/src/js/content/snippets.animation.js35
-rw-r--r--addons/website_sale/controllers/main.py2
-rw-r--r--addons/website_sale/views/templates.xml3
-rw-r--r--doc/cla/individual/mtantin.md11
-rw-r--r--doc/cla/individual/ruter.md11
-rw-r--r--doc/cla/individual/satriani-vai.md11
-rw-r--r--odoo/addons/base/ir/ir_cron.py18
-rw-r--r--odoo/models.py3
-rw-r--r--odoo/modules/__init__.py2
-rw-r--r--odoo/modules/loading.py21
-rw-r--r--odoo/modules/registry.py6
34 files changed, 228 insertions, 55 deletions
diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py
index eaacb3721cc..9121e9f115e 100644
--- a/addons/account/models/chart_template.py
+++ b/addons/account/models/chart_template.py
@@ -769,7 +769,7 @@ class WizardMultiChartsAccounts(models.TransientModel):
if company_id:
company = self.env['res.company'].browse(company_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)])
if chart_templates:
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js
index 8f81b700168..b77882323a3 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_model.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js
@@ -605,9 +605,14 @@ var StatementModel = BasicModel.extend({
var line = self.getLine(handle);
var props = _.filter(line.reconciliation_proposition, function (prop) {return !prop.is_tax && !prop.invalid;});
if (props.length === 0) {
+ // Usability: if user has not choosen any lines and click validate, it has the same behavior
+ // as creating a write-off of the same amount.
props.push(self._formatQuickCreate(line, {
account_id: [line.st_line.open_balance_account_id, self.accounts[line.st_line.open_balance_account_id]],
}));
+ // update balance of line otherwise it won't be to zero and another line will be added
+ line.reconciliation_proposition.push(props[0]);
+ self._computeLine(line);
}
ids.push(line.id);
var values_dict = {
diff --git a/addons/auth_oauth/data/auth_oauth_data.xml b/addons/auth_oauth/data/auth_oauth_data.xml
index 1b1ff532ae4..8417c9c4b2d 100644
--- a/addons/auth_oauth/data/auth_oauth_data.xml
+++ b/addons/auth_oauth/data/auth_oauth_data.xml
@@ -16,7 +16,7 @@
<field name="auth_endpoint">https://www.facebook.com/dialog/oauth</field>
<field name="scope">public_profile,email</field>
<field name="validation_endpoint">https://graph.facebook.com/me</field>
- <field name="data_endpoint"></field>
+ <field name="data_endpoint">https://graph.facebook.com/me?fields=id,name,email</field>
<field name="css_class">fa fa-facebook-square</field>
<field name="body">Log in with facebook</field>
</record>
diff --git a/addons/hr_payroll/data/hr_payroll_data.xml b/addons/hr_payroll/data/hr_payroll_data.xml
index c4b16bd05a4..2eede6d92bb 100644
--- a/addons/hr_payroll/data/hr_payroll_data.xml
+++ b/addons/hr_payroll/data/hr_payroll_data.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
- <data noupdate="1">
+ <data noupdate="0">
<record id="contrib_register_employees" model="hr.contribution.register">
<field name="name">Employees</field>
<field name="partner_id" eval="False"/>
diff --git a/addons/l10n_be_hr_payroll/data/l10n_be_hr_payroll_data.xml b/addons/l10n_be_hr_payroll/data/l10n_be_hr_payroll_data.xml
index 537021de787..28ed0b1dc14 100644
--- a/addons/l10n_be_hr_payroll/data/l10n_be_hr_payroll_data.xml
+++ b/addons/l10n_be_hr_payroll/data/l10n_be_hr_payroll_data.xml
@@ -91,7 +91,7 @@
<field name="code">SALARY</field>
<field name="condition_select">none</field>
<field name="amount_select">code</field>
- <field name="amount_python_compute">result = contract.wage * 1.08</field>
+ <field name="amount_python_compute">result = categories.BASIC * 1.08</field>
</record>
<record id="hr_payroll_rules_employee" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_salary"/>
@@ -100,7 +100,7 @@
<field name="condition_select">none</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">
-result = contract.wage
+result = categories.BASIC
if contract.internet:
result += 5.0
if contract.mobile:
@@ -163,14 +163,15 @@ elif salary &lt;= 2461.27:
<field name="condition_python">result = (((employee.marital=='single') or (employee.marital=='married' and employee.spouse_fiscal_status=='without income')) and (employee.resident_bool!=True))</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">
-if contract.wage &gt;= 0.01 and contract.wage &lt;= 1095.09:
+wage = categories.BASIC
+if wage &gt;= 0.01 and wage &lt;= 1095.09:
result = 0.0
-elif contract.wage &gt;= 1095.10 and contract.wage &lt;= 1945.38:
+elif wage &gt;= 1095.10 and wage &lt;= 1945.38:
result = 0.0
-elif contract.wage &gt;= 1945.39 and contract.wage &lt;= 2190.18:
- result = -min((contract.wage - 1945.38) * 0.076, 18.60)
-elif contract.wage &gt;= 2190.19 and contract.wage &lt;= 6038.82:
- result = -min(18.60 + (contract.wage - 2190.18) * 0.011, 60.94)
+elif wage &gt;= 1945.39 and wage &lt;= 2190.18:
+ result = -min((wage - 1945.38) * 0.076, 18.60)
+elif wage &gt;= 2190.19 and wage &lt;= 6038.82:
+ result = -min(18.60 + (wage - 2190.18) * 0.011, 60.94)
else:
result = -60.94
</field>
@@ -186,14 +187,15 @@ else:
<field name="condition_python">result = (((employee.marital=='married' and employee.spouse_fiscal_status=='with income')) and (employee.resident_bool!=True))</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">
-if contract.wage &gt;= 0.01 and contract.wage &lt;= 1095.09:
+wage = categories.BASIC
+if wage &gt;= 0.01 and wage &lt;= 1095.09:
result = 0.0
-elif contract.wage &gt;= 1095.10 and contract.wage &lt;= 1945.38:
+elif wage &gt;= 1095.10 and wage &lt;= 1945.38:
result = -9.30
-elif contract.wage &gt;= 1945.39 and contract.wage &lt;= 2190.18:
- result = -min(max((contract.wage - 1945.38) * 0.076, 9.30), 18.60)
-elif contract.wage &gt;= 2190.19 and contract.wage &lt;= 6038.82:
- result = -min(18.60 + (contract.wage - 2190.18) * 0.011, 51.64)
+elif wage &gt;= 1945.39 and wage &lt;= 2190.18:
+ result = -min(max((wage - 1945.38) * 0.076, 9.30), 18.60)
+elif wage &gt;= 2190.19 and wage &lt;= 6038.82:
+ result = -min(18.60 + (wage - 2190.18) * 0.011, 51.64)
else:
result = -51.64
</field>
@@ -635,7 +637,7 @@ result = -contract.car_atn
<field name="code">holidays</field>
<field name="default_value">20.0</field>
<field name="lower_bound">0.0</field>
- <field name="upper_bound">30.0</field>
+ <field name="upper_bound">35.0</field>
</record>
<record id="hr_contract_advantage_template_representation_fees" model="hr.contract.advantage.template">
<field name="name">Representation fees</field>
diff --git a/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py b/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py
index 9174c408f64..e6d11322c6e 100644
--- a/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py
+++ b/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py
@@ -19,8 +19,8 @@ class HrContract(models.Model):
public_transport_employee_amount = fields.Monetary('Paid by the employee (Monthly)')
thirteen_month = fields.Monetary(compute='_compute_holidays_advantages', string='13th Month',
help="Yearly gross amount the employee receives as 13th month bonus.")
- double_holidays = fields.Monetary(compute='_compute_holidays_advantages', string='Double Holidays',
- help="Yearly gross amount the employee receives as double holidays bonus.")
+ double_holidays = fields.Monetary(compute='_compute_holidays_advantages', string='Holiday Bonus',
+ help="Yearly gross amount the employee receives as holidays bonus.")
warrant_value_employee = fields.Monetary(compute='_compute_warrants_cost', string="Warrant value for the employee")
# Employer costs fields
@@ -67,7 +67,7 @@ class HrContract(models.Model):
help="Number of days of paid leaves the employee gets per year.")
holidays_editable = fields.Boolean(string="Editable Leaves", default=True)
holidays_compensation = fields.Monetary(compute='_compute_holidays_compensation', string="Holidays Compensation")
- wage_with_holidays = fields.Monetary(compute='_compute_wage_with_holidays', sting="Wage update with holidays retenues")
+ wage_with_holidays = fields.Monetary(compute='_compute_wage_with_holidays', string="Wage update with holidays retenues")
additional_net_amount = fields.Monetary(string="Net Supplements",
help="Monthly net amount the employee receives.")
retained_net_amount = fields.Monetary(sting="Net Retained",
@@ -116,7 +116,7 @@ class HrContract(models.Model):
contract.transport_employer_cost
)
- @api.depends('yearly_cost_before_charges', 'social_security_contributions',
+ @api.depends('yearly_cost_before_charges', 'social_security_contributions', 'wage',
'social_security_contributions', 'double_holidays', 'warrants_cost', 'meal_voucher_paid_by_employer')
def _compute_final_yearly_costs(self):
for contract in self:
diff --git a/addons/l10n_be_hr_payroll/views/l10n_be_hr_payroll_view.xml b/addons/l10n_be_hr_payroll/views/l10n_be_hr_payroll_view.xml
index 0d6f6b7fde3..4e7d429f102 100644
--- a/addons/l10n_be_hr_payroll/views/l10n_be_hr_payroll_view.xml
+++ b/addons/l10n_be_hr_payroll/views/l10n_be_hr_payroll_view.xml
@@ -57,7 +57,7 @@
</div>
<field name="monthly_yearly_costs"/>
</group>
- <group string="Monthly Benefits">
+ <group string="Monthly Benefit in Kind">
<field name="transport_mode" widget="radio"/>
<field name="car_atn" attrs="{'invisible': [('transport_mode', '!=', 'company_car')]}" invisible="1"/>
<field name="transport_employer_cost" invisible="1"/>
@@ -109,7 +109,7 @@
<field name="double_holidays" nolabel="1"/>
<span>/ year</span>
</div>
- <field name="holidays_editable"/>
+ <field name="holidays_editable" invisible="1"/>
<label for="holidays"/>
<div class="o_row">
<field name="holidays" nolabel="1" attrs="{'readonly': [('holidays_editable', '=', False)]}"/>
diff --git a/addons/mrp/wizard/change_production_qty.py b/addons/mrp/wizard/change_production_qty.py
index 39f60d319ea..90db7f09481 100644
--- a/addons/mrp/wizard/change_production_qty.py
+++ b/addons/mrp/wizard/change_production_qty.py
@@ -67,7 +67,7 @@ class ChangeProductionQty(models.TransientModel):
cycle_number * operation.time_cycle * 100.0 / operation.workcenter_id.time_efficiency)
quantity = wo.qty_production - wo.qty_produced
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:
quantity = quantity if (quantity > 0) else 0
if float_is_zero(quantity, precision_digits=precision):
diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js
index e8ec348288b..dc93133be09 100644
--- a/addons/point_of_sale/static/src/js/db.js
+++ b/addons/point_of_sale/static/src/js/db.js
@@ -301,7 +301,7 @@ var PosDB = core.Class.extend({
search_partner: function(query){
try {
query = query.replace(/[\[\]\(\)\+\*\?\.\-\!\&\^\$\|\~\_\{\}\:\,\\\/]/g,'.');
- query = query.replace(' ','.+');
+ query = query.replace(/ /g,'.+');
var re = RegExp("([0-9]+):.*?"+query,"gi");
}catch(e){
return [];
diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js
index 8f5e1b9808b..f3a077cf881 100644
--- a/addons/point_of_sale/static/src/js/screens.js
+++ b/addons/point_of_sale/static/src/js/screens.js
@@ -1166,11 +1166,12 @@ var ClientListScreenWidget = ScreenWidget.extend({
save_changes: function(){
var order = this.pos.get_order();
if( this.has_client_changed() ){
+ var default_fiscal_position_id = _.findWhere(this.pos.fiscal_positions, {'id': self.pos.config.default_fiscal_position_id[0]});
if ( this.new_client ) {
order.fiscal_position = _.findWhere(this.pos.fiscal_positions, {'id': this.new_client.property_account_position_id[0]});
order.set_pricelist(_.findWhere(this.pos.pricelists, {'id': this.new_client.property_product_pricelist[0]}) || this.pos.default_pricelist);
} else {
- order.fiscal_position = undefined;
+ order.fiscal_position = default_fiscal_position_id;
order.set_pricelist(this.pos.default_pricelist);
}
diff --git a/addons/product/models/product.py b/addons/product/models/product.py
index d0aed74bc66..62347b4cd06 100644
--- a/addons/product/models/product.py
+++ b/addons/product/models/product.py
@@ -42,10 +42,13 @@ class ProductCategory(models.Model):
category.complete_name = category.name
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)
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')
def _check_category_recursion(self):
diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml
index 0e192af09c2..b540deacd01 100644
--- a/addons/product/views/product_views.xml
+++ b/addons/product/views/product_views.xml
@@ -116,7 +116,7 @@
<field name="arch" type="xml">
<search string="Product">
<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/>
<filter string="Services" name="services" domain="[('type','=','service')]"/>
<filter string="Products" name="consumable" domain="[('type', 'in', ['consu', 'product'])]"/>
diff --git a/addons/project/models/project.py b/addons/project/models/project.py
index 178ab8b5c66..a873dea6d59 100644
--- a/addons/project/models/project.py
+++ b/addons/project/models/project.py
@@ -245,7 +245,8 @@ class Project(models.Model):
project = super(Project, self).copy(default)
for follower in self.message_follower_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
@api.model
@@ -265,6 +266,8 @@ class Project(models.Model):
if 'active' in vals:
# archiving/unarchiving a project does it on its tasks, too
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']})
if vals.get('partner_id') or vals.get('privacy_visibility'):
for project in self.filtered(lambda project: project.privacy_visibility == 'portal'):
project.message_subscribe(project.partner_id.ids)
diff --git a/addons/sales_team/models/crm_team.py b/addons/sales_team/models/crm_team.py
index 5589d2e0aa0..cf95b66a7e2 100644
--- a/addons/sales_team/models/crm_team.py
+++ b/addons/sales_team/models/crm_team.py
@@ -24,7 +24,7 @@ class CrmTeam(models.Model):
def _get_default_team_id(self, user_id=None):
if not user_id:
user_id = self.env.uid
- company_id = self.sudo(user_id).company_id.id
+ company_id = self.sudo(user_id).env.user.company_id.id
team_id = self.env['crm.team'].sudo().search([
'|', ('user_id', '=', user_id), ('member_ids', '=', user_id),
'|', ('company_id', '=', False), ('company_id', 'child_of', [company_id])
diff --git a/addons/web/static/lib/bootstrap-datetimepicker/src/js/bootstrap-datetimepicker.js b/addons/web/static/lib/bootstrap-datetimepicker/src/js/bootstrap-datetimepicker.js
index 8f0724bbb00..19f261ff4fc 100644
--- a/addons/web/static/lib/bootstrap-datetimepicker/src/js/bootstrap-datetimepicker.js
+++ b/addons/web/static/lib/bootstrap-datetimepicker/src/js/bootstrap-datetimepicker.js
@@ -479,8 +479,12 @@
},
notifyEvent = function (e) {
- if (e.type === 'dp.change' && ((e.date && e.date.isSame(e.oldDate)) || (!e.date && !e.oldDate))) {
- return;
+ if (e.type === 'dp.change') {
+ // check _isUTC flag to ensure that we are not comparing apples and oranges
+ var bothUTC = e.date && e.oldDate && e.date._isUTC === e.oldDate._isUTC;
+ if ((bothUTC && e.date.isSame(e.oldDate)) || (!e.date && !e.oldDate)) {
+ return;
+ }
}
element.trigger(e);
},
diff --git a/addons/web/static/src/js/chrome/search_filters.js b/addons/web/static/src/js/chrome/search_filters.js
index e2eff03546d..582af7e70ef 100644
--- a/addons/web/static/src/js/chrome/search_filters.js
+++ b/addons/web/static/src/js/chrome/search_filters.js
@@ -248,6 +248,10 @@ var Date = DateTime.extend({
attributes: {
type: 'date'
},
+ get_value: function (index) {
+ // retrieve the datepicker value
+ return this["datewidget_" + (index || 0)].getValue();
+ },
_get_widget_class: function () {
return datepicker.DateWidget;
},
diff --git a/addons/web/static/src/js/chrome/search_inputs.js b/addons/web/static/src/js/chrome/search_inputs.js
index d567551873f..a2923d1a607 100644
--- a/addons/web/static/src/js/chrome/search_inputs.js
+++ b/addons/web/static/src/js/chrome/search_inputs.js
@@ -153,7 +153,7 @@ var Field = Input.extend( /** @lends instance.web.search.Field# */ {
value_to_domain = function (facetValue) {
return Domain.prototype.stringToArray(
domain,
- {self: self.value_from(facetValue)}
+ {self: self.value_from(facetValue), raw_value: facetValue.attributes.value}
);
};
} else {
diff --git a/addons/web/static/src/js/chrome/search_view.js b/addons/web/static/src/js/chrome/search_view.js
index ecd7db41a7d..7b7b3afcd70 100644
--- a/addons/web/static/src/js/chrome/search_view.js
+++ b/addons/web/static/src/js/chrome/search_view.js
@@ -339,7 +339,7 @@ var SearchView = Widget.extend({
},
set_default_filters: function () {
var self = this,
- default_custom_filter = this.$buttons && this.favorite_menu.get_default_filter();
+ default_custom_filter = this.$buttons && this.favorite_menu && this.favorite_menu.get_default_filter();
if (!self.options.disable_custom_filters && default_custom_filter) {
return this.favorite_menu.toggle_filter(default_custom_filter, true);
}
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 7ebe94f7b30..6f4bf1630f8 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -1219,7 +1219,7 @@ var FieldBinaryFile = AbstractFieldBinary.extend({
template: 'FieldBinaryFile',
events: _.extend({}, AbstractFieldBinary.prototype.events, {
'click': function (event) {
- if (this.mode === 'readonly' && this.value) {
+ if (this.mode === 'readonly' && this.value && this.recordData.id) {
this.on_save_as(event);
}
},
@@ -1236,10 +1236,20 @@ var FieldBinaryFile = AbstractFieldBinary.extend({
this.do_toggle(!!this.value);
if (this.value) {
this.$el.empty().append($("<span/>").addClass('fa fa-download'));
+ if (this.recordData.id) {
+ this.$el.css('cursor', 'pointer');
+ } else {
+ this.$el.css('cursor', 'not-allowed');
+ }
if (this.filename_value) {
this.$el.append(" " + this.filename_value);
}
}
+ if (!this.res_id) {
+ this.$el.css('cursor', 'not-allowed');
+ } else {
+ this.$el.css('cursor', 'pointer');
+ }
},
_renderEdit: function () {
if (this.value) {
@@ -1264,7 +1274,7 @@ var FieldBinaryFile = AbstractFieldBinary.extend({
if (!this.value) {
this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
ev.stopPropagation();
- } else {
+ } else if (this.res_id) {
framework.blockUI();
var c = crash_manager;
var filename_fieldname = this.attrs.filename;
diff --git a/addons/web/static/src/js/views/graph/graph_renderer.js b/addons/web/static/src/js/views/graph/graph_renderer.js
index 2c3a7a7db19..74443843405 100644
--- a/addons/web/static/src/js/views/graph/graph_renderer.js
+++ b/addons/web/static/src/js/views/graph/graph_renderer.js
@@ -81,7 +81,7 @@ return AbstractRenderer.extend({
setTimeout(function () {
self.$el.empty();
var chart = self['_render' + _.str.capitalize(self.state.mode) + 'Chart']();
- if (chart) {
+ if (chart && chart.tooltip.chartContainer) {
self.to_remove = chart.update;
nv.utils.onWindowResize(chart.update);
chart.tooltip.chartContainer(self.el);
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index 5064dd81af1..4a418c05b72 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -1389,6 +1389,51 @@ QUnit.module('basic_fields', {
session.get_file = oldGetFile;
});
+ QUnit.test('binary fields that are readonly in create mode do not download', function (assert) {
+ assert.expect(2);
+
+ // save the session function
+ var oldGetFile = session.get_file;
+ session.get_file = function (option) {
+ assert.step('We shouldn\'t be getting the file.');
+ return oldGetFile.bind(session)(option);
+ };
+
+ this.data.partner.onchanges = {
+ product_id: function (obj) {
+ obj.document = "onchange==\n";
+ },
+ };
+
+ this.data.partner.fields.document.readonly = true;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="product_id"/>' +
+ '<field name="document" filename="\'yooo\'"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ form.$buttons.find('.o_form_button_create').click();
+ var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
+
+ form.$('.o_field_many2one input').click();
+ $dropdown.find('li:not(.o_m2o_dropdown_option):contains(xphone)').click();
+
+ assert.strictEqual(form.$('a.o_field_widget[name="document"] > .fa-download').length, 1,
+ 'The link to download the binary should be present');
+
+ form.$('a.o_field_widget[name="document"]').click();
+
+ assert.verifySteps([]); // We shoudln't have passed through steps
+
+ form.destroy();
+ session.get_file = oldGetFile;
+ });
QUnit.test('text field rendering in list view', function (assert) {
assert.expect(1);
diff --git a/addons/web_editor/static/src/js/backend/fields.js b/addons/web_editor/static/src/js/backend/fields.js
index 00c02e20513..95afaa90073 100644
--- a/addons/web_editor/static/src/js/backend/fields.js
+++ b/addons/web_editor/static/src/js/backend/fields.js
@@ -51,6 +51,9 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
var layoutInfo = this.$textarea.data('layoutInfo');
$.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo, false);
}
+ if (this._getValue() !== this.value) {
+ this._isDirty = true;
+ }
this._super.apply(this, arguments);
},
/**
diff --git a/addons/web_editor/static/src/js/editor/rte.js b/addons/web_editor/static/src/js/editor/rte.js
index c0e2218d974..6da2ea72ea1 100644
--- a/addons/web_editor/static/src/js/editor/rte.js
+++ b/addons/web_editor/static/src/js/editor/rte.js
@@ -405,7 +405,7 @@ var RTEWidget = Widget.extend({
var $el = $(el);
$el.find('[class]').filter(function () {
- if (!this.className.match(/\S/)) {
+ if (!this.getAttribute('class').match(/\S/)) {
this.removeAttribute('class');
}
});
diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js
index e5115ec7b18..f4f49271b87 100644
--- a/addons/website/static/src/js/content/snippets.animation.js
+++ b/addons/website/static/src/js/content/snippets.animation.js
@@ -592,16 +592,35 @@ registry.mediaVideo = Animation.extend({
start: function () {
// TODO: this code should be refactored to make more sense and be better
// 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 editor = '<div class="css_editableMode_display">&nbsp;</div>';
- var size = '<div class="media_iframe_video_size">&nbsp;</div>';
- this.$target.html(editor+size);
+ var def = this._super.apply(this, arguments);
+ if (this.$target.children('iframe').length) {
+ // There already is an <iframe/>, do nothing
+ 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>');
- return this._super.apply(this, arguments);
+
+ // Bug fix / compatibility: empty the <div/> element as all information
+ // 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;
},
});
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index eae57d4a6cf..7f05a55d26d 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -445,7 +445,7 @@ class WebsiteSale(http.Controller):
Partner = order.partner_id.with_context(show_address=1).sudo()
shippings = Partner.search([
("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')
if shippings:
if kw.get('partner_id') or 'use_billing' in kw:
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 42bac83468a..c70eea3c314 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -1055,6 +1055,7 @@
<t t-foreach="shippings" t-as="ship">
<div class="col-sm-12 col-md-6 one_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='selected' t-value="order.partner_shipping_id==ship"/>
<t t-set='readonly' t-value="bool(len(shippings)==1)"/>
@@ -1088,7 +1089,7 @@
</t>
<input type='submit'/>
</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 class='panel-body' style='min-height: 130px;'>
<t t-esc="contact" t-options="dict(widget='contact', fields=['name', 'address'], no_marker=True)"/>
diff --git a/doc/cla/individual/mtantin.md b/doc/cla/individual/mtantin.md
new file mode 100644
index 00000000000..b3aee232ccf
--- /dev/null
+++ b/doc/cla/individual/mtantin.md
@@ -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 \ No newline at end of file
diff --git a/doc/cla/individual/ruter.md b/doc/cla/individual/ruter.md
new file mode 100644
index 00000000000..1560ba2b7f3
--- /dev/null
+++ b/doc/cla/individual/ruter.md
@@ -0,0 +1,11 @@
+China, 2017-12-05
+
+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,
+
+Ruter ruter.lv@gmail.com https://github.com/ruter
diff --git a/doc/cla/individual/satriani-vai.md b/doc/cla/individual/satriani-vai.md
new file mode 100644
index 00000000000..df2c38eb7be
--- /dev/null
+++ b/doc/cla/individual/satriani-vai.md
@@ -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
diff --git a/odoo/addons/base/ir/ir_cron.py b/odoo/addons/base/ir/ir_cron.py
index 2aa7a1e2773..375d9a966da 100644
--- a/odoo/addons/base/ir/ir_cron.py
+++ b/odoo/addons/base/ir/ir_cron.py
@@ -5,7 +5,7 @@ import threading
import time
import psycopg2
import pytz
-from datetime import datetime
+from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
import odoo
@@ -15,6 +15,7 @@ from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
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):
@@ -170,7 +171,7 @@ class ir_cron(models.Model):
(version,) = cr.fetchone()
cr.execute("SELECT COUNT(*) FROM ir_module_module WHERE state LIKE %s", ['to %'])
(changes,) = cr.fetchone()
- if not version or changes:
+ if version is None:
raise BadModuleState()
elif version != BASE_VERSION:
raise BadVersion()
@@ -181,6 +182,19 @@ class ir_cron(models.Model):
ORDER BY priority""")
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:
lock_cr = db.cursor()
try:
diff --git a/odoo/models.py b/odoo/models.py
index ec219398d78..e7be6c5b381 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -1651,7 +1651,8 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
order = '"%s" %s' % (order_field, '' if len(order_split) == 1 else order_split[1])
orderby_terms.append(order)
elif order_field in aggregated_fields:
- orderby_terms.append(order_part)
+ order_split[0] = '"' + order_field + '"'
+ orderby_terms.append(' '.join(order_split))
else:
# 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)',
diff --git a/odoo/modules/__init__.py b/odoo/modules/__init__.py
index 530f81ff5e3..b223df99280 100644
--- a/odoo/modules/__init__.py
+++ b/odoo/modules/__init__.py
@@ -7,7 +7,7 @@
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 (
adapt_version,
diff --git a/odoo/modules/loading.py b/odoo/modules/loading.py
index d8ee12c1690..81c90e2b728 100644
--- a/odoo/modules/loading.py
+++ b/odoo/modules/loading.py
@@ -431,3 +431,24 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
finally:
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")
diff --git a/odoo/modules/registry.py b/odoo/modules/registry.py
index 1efc6aa195f..5c1c5111cf0 100644
--- a/odoo/modules/registry.py
+++ b/odoo/modules/registry.py
@@ -81,7 +81,11 @@ class Registry(Mapping):
try:
registry.setup_signaling()
# 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:
_logger.exception('Failed to load registry')
del cls.registries[db_name]