[MERGE] forward port branch 11.0 up to c3ea0a7811

This commit is contained in:
Christophe Simonis
2018-09-10 19:21:57 +02:00
18 changed files with 157 additions and 25 deletions
@@ -1292,7 +1292,8 @@ var ManualModel = StatementModel.extend({
});
} else {
var mv_line_ids = _.pluck(_.filter(props, function (prop) {return !isNaN(prop.id);}), 'id');
var new_mv_line_dicts = _.map(_.filter(props, function (prop) {return isNaN(prop.id) && prop.display;}), self._formatToProcessReconciliation.bind(self, line));
// Dear KangOl, please FORWARD-PORT UP TO SAAS-11.3. Thank you for your hard work.
var new_mv_line_dicts = _.map(_.filter(props, function (prop) {return isNaN(prop.id) && prop.display && !prop.is_tax;}), self._formatToProcessReconciliation.bind(self, line));
process_reconciliations.push({
id: null,
type: null,
+27 -8
View File
@@ -187,6 +187,28 @@ var Activity = AbstractActivityField.extend({
this.trigger_up('reload_mail_fields', fieldsToReload);
},
/** Binds a focusout handler on a bootstrap popover
* Useful to do some operations on the popover's HTML,
* like keeping the user's input for the feedback
* @param {JQuery} $popover_el: the element on which
* the popover() method has been called
*/
_bindPopoverFocusout: function ($popover_el) {
var self = this;
// Retrieve the actual popover's HTML
var $popover = $popover_el.data("bs.popover").tip();
var activity_id = $popover_el.data('activity-id');
$popover.off('focusout');
$popover.focusout(function (e) {
// outside click of popover hides the popover
// e.relatedTarget is the element receiving the focus
self.feedbackValue[activity_id] = $popover.find('#activity_feedback').val().trim();
if(!$popover.is(e.relatedTarget) && !$popover.find(e.relatedTarget).length) {
$popover_el.popover('hide');
}
});
},
// handlers
_onClickRedirect: function (ev) {
var id = $(ev.target).data('oe-id');
@@ -236,19 +258,23 @@ var Activity = AbstractActivityField.extend({
})
.then(this._reload.bind(this, {activity: true}));
},
_onMarkActivityDone: function (event) {
event.preventDefault();
var self = this;
this.feedbackValue = this.feedbackValue || {};
var $popover_el = $(event.currentTarget);
var activity_id = $popover_el.data('activity-id');
var previous_activity_type_id = $popover_el.data('previous-activity-type-id');
if (!$popover_el.data('bs.popover')) {
this.feedbackValue[activity_id] = "";
$popover_el.popover({
title : _t('Feedback'),
html: 'true',
trigger:'click',
content : function() {
var $popover = $(QWeb.render("mail.activity_feedback_form", {'previous_activity_type_id': previous_activity_type_id}));
$popover.find('#activity_feedback').val(self.feedbackValue[activity_id]);
$popover.on('click', '.o_activity_popover_done_next', function () {
var feedback = _.escape($popover.find('#activity_feedback').val());
var previous_activity_type_id = $popover_el.data('previous-activity-type-id');
@@ -272,14 +298,7 @@ var Activity = AbstractActivityField.extend({
}).on("shown.bs.popover", function () {
var $popover = $(this).data("bs.popover").tip();
$popover.find('#activity_feedback').focus();
$popover.off('focusout');
$popover.focusout(function (e) {
// outside click of popover hide the popover
// e.relatedTarget is the element receiving the focus
if(!$popover.is(e.relatedTarget) && !$popover.find(e.relatedTarget).length) {
$popover.popover('hide');
}
});
self._bindPopoverFocusout($(this));
}).popover('show');
}
},
+70
View File
@@ -17,6 +17,8 @@ var createBusService = mailTestUtils.createBusService;
var createAsyncView = testUtils.createAsyncView;
var createView = testUtils.createView;
var Activity = require('mail.Activity');
QUnit.module('mail', {}, function () {
QUnit.module('Chatter', {
@@ -72,6 +74,7 @@ QUnit.module('Chatter', {
type: 'selection',
selection: [['overdue', 'Overdue'], ['today', 'Today'], ['planned', 'Planned']],
},
note : { string: "Note", type: "char" },
},
},
'mail.activity.type': {
@@ -143,6 +146,73 @@ QUnit.test('basic rendering', function (assert) {
form.destroy();
});
QUnit.test('Activity Done keep feedback on blur', function (assert) {
assert.expect(3);
var done = assert.async();
this.data['mail.activity'].records = [
{activity_type_id: 1, id: 1, user_id: 2, state: 'today', note: 'But I\'m talkin\' about Shaft'},
];
this.data.partner.records[0].activity_ids = [1];
var shownDef = $.Deferred();
var hiddenDef = $.Deferred();
testUtils.patch(Activity, {
_bindPopoverFocusout: function () {
this._super.apply(this, arguments);
shownDef.resolve();
},
});
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
res_id: 2,
arch:'<form string="Partners">' +
'<div class="oe_chatter">' +
'<field name="activity_ids" widget="mail_activity"/>' +
'</div>' +
'</form>',
});
// sanity checks
var $activityEl = form.$('.o_mail_activity[name=activity_ids]');
assert.strictEqual($activityEl.find('.o_thread_message').length, 1,
'There should be one activity');
assert.strictEqual($activityEl.find('.o_thread_message .o_thread_message_note').text().trim(),
'But I\'m talkin\' about Shaft', 'The activity should have the right note');
var $popoverEl = $activityEl.find('.o_thread_message_tools .o_activity_done');
$popoverEl.on('hidden.bs.popover', hiddenDef.resolve.bind(hiddenDef));
// open popover
$popoverEl.click();
shownDef.then(function () {
// write a feedback and focusout
var $feedbackPopover = $popoverEl.data('bs.popover').tip();
$feedbackPopover.find('#activity_feedback').val('John Shaft').focusout();
hiddenDef.then(function () {
shownDef = $.Deferred();
// re-open popover
$popoverEl.click();
shownDef.then(function () {
var $feedbackPopover = $popoverEl.data('bs.popover').tip();
assert.strictEqual($feedbackPopover.find('#activity_feedback').val(), 'John Shaft',
"feedback should have been kept");
form.destroy();
testUtils.unpatch(Activity);
done();
});
});
});
});
QUnit.test('chatter in create mode', function (assert) {
assert.expect(9);
+4 -2
View File
@@ -164,8 +164,10 @@ class Partner(models.Model):
@api.model
def _cron_update_membership(self):
# used to recompute 'membership_state'; should no longer be necessary
pass
partners = self.search([('membership_state', 'in', ['invoiced', 'paid'])])
# mark the field to be recomputed, and recompute it
partners._recompute_todo(self._fields['membership_state'])
self.recompute()
@api.multi
def create_membership_invoice(self, product_id=None, datas=None):
+6 -2
View File
@@ -185,9 +185,13 @@ class PaymentAcquirer(models.Model):
def _check_required_if_provider(self):
""" If the field has 'required_if_provider="<provider>"' attribute, then it
required if record.provider is <provider>. """
empty_field = []
for acquirer in self:
if any(getattr(f, 'required_if_provider', None) == acquirer.provider and not acquirer[k] for k, f in self._fields.items()):
return False
for k, f in acquirer._fields.items():
if getattr(f, 'required_if_provider', None) == acquirer.provider and not acquirer[k]:
empty_field.append(self.env['ir.model.fields'].search([('name', '=', k), ('model', '=', acquirer._name)]).field_description)
if empty_field:
raise ValidationError((', ').join(empty_field))
return True
_constraints = [
@@ -451,7 +451,7 @@
Operation type used to record product pickings
</div>
<div class="content-group mt16">
<field name="picking_type_id"/>
<field name="picking_type_id" required="1"/>
</div>
</div>
</div>
+1 -1
View File
@@ -384,7 +384,7 @@
<field name="team_id" options="{'no_create': True}"/>
<field name="client_order_ref"/>
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
<field name="analytic_account_id" context="{'default_partner_id':partner_invoice_id, 'default_name':name}" attrs="{'readonly': [('invoice_count','!=',0),('state','=','sale')]}" groups="analytic.group_analytic_accounting"/>
<field name="analytic_account_id" context="{'default_partner_id':partner_invoice_id, 'default_name':name}" attrs="{'readonly': [('invoice_count','!=',0),('state','=','sale')]}" groups="analytic.group_analytic_accounting" force_save="1"/>
</group>
<group name="sale_pay" string="Invoicing">
<field name="date_order" attrs="{'invisible': [('state', 'in', ['sale', 'done', 'cancel'])]}"/>
@@ -190,7 +190,9 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
// If it is not handled, we should display something clearer than the common crash_manager error dialog
// since it won't show anything except "Script error."
// This link will probably explain it better: https://blog.sentry.io/2016/05/17/what-is-script-error.html
if (message === "Script error." && !file && !line && !col && !error) {
if (!file && !line && !col) {
// Chrome and Opera set "Script error." on the `message` and hide the `error`
// Firefox handles the "Script error." directly. It sets the error thrown by the CORS file into `error`
if (window.onOriginError) {
window.onOriginError();
delete window.onOriginError;
@@ -198,7 +200,7 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
crash_manager.show_error({
type: _t("Odoo Client Error"),
message: _t("Unknown CORS error"),
data: {debug: _t("An unknown CORS error occured. The error probably originates from a JavaScript file served from a different origin.")},
data: {debug: _t("An unknown CORS error occured. The error probably originates from a JavaScript file served from a different origin. (Opening your browser console might give you a hint on the error.)")},
});
}
} else {
@@ -4,6 +4,7 @@ odoo.define('web_settings_dashboard', function (require) {
var AbstractAction = require('web.AbstractAction');
var core = require('web.core');
var framework = require('web.framework');
var session = require('web.session');
var Widget = require('web.Widget');
var QWeb = core.qweb;
@@ -11,12 +11,12 @@ var Dashboard = webSettingsDashboard.Dashboard;
function createDashboard(params) {
var widget = new Widget();
var dashboard = new Dashboard(widget);
dashboard.all_dashboards = ['invitations']; // test only user invitations
dashboard.all_dashboards = params.dashboards || ['invitations']; // test only user invitations
testUtils.addMockEnvironment(widget, params);
var originalDestroy = Dashboard.prototype.destroy;
Dashboard.destroy = function () {
dashboard.destroy = function () {
dashboard.destroy = originalDestroy;
widget.destroy();
};
@@ -3,6 +3,7 @@
<template id="report_saleorder_validity_date" inherit_id="sale.report_saleorder_document">
<xpath expr="//div[hasclass('page')]/p[@id='fiscal_position_remark']" position="after">
<div t-if="doc.options and doc.state in ['draft', 'sent']" style="page-break-before:always;">
<t t-set="has_option_discount" t-value="any(doc.options.filtered(lambda o: o.discount != 0.0))" />
<h2>
<span>Suggested Products</span>
</h2>
@@ -11,7 +12,7 @@
<tr>
<th>Product</th>
<th>Description</th>
<th groups="sale.group_discount_per_so_line"></th>
<th t-if="has_option_discount" groups="sale.group_discount_per_so_line"></th>
<th class="text-right">Unit Price</th>
</tr>
</thead>
@@ -23,8 +24,8 @@
<td>
<span t-field="option.name"/>
</td>
<td groups="sale.group_discount_per_so_line">
<strong class="text-info">
<td t-if="has_option_discount" groups="sale.group_discount_per_so_line">
<strong t-if="option.discount != 0.0" class="text-info">
<t t-esc="((option.discount % 1) and '%s' or '%d') % option.discount"/>% discount
</strong>
</td>
-1
View File
@@ -105,7 +105,6 @@ class SaleOrder(models.Model):
'order_id': order_id,
'product_uom': product.uom_id.id,
'price_unit': pu,
'customer_lead': product.sale_delay,
}
@api.multi
@@ -31,6 +31,13 @@ class SaleOrder(models.Model):
values['warning'] = self.warning_stock
return values
@api.multi
def _website_product_id_change(self, order_id, product_id, qty=0):
res = super(SaleOrder, self)._website_product_id_change(order_id, product_id, qty=qty)
product = self.env['product.product'].browse(product_id)
res['customer_lead'] = product.sale_delay
return res
@api.multi
def _get_stock_warning(self, clear=True):
self.ensure_one()
+1
View File
@@ -33,3 +33,4 @@ Akim Juillerat akim.juillerat@camptocamp.com https://github.com/grindtildeath
Damien Crier damien.crier@camptocamp.com https://github.com/damdam-s
Simone Orsi simahawk@gmail.com https://github.com/simahawk
Patrick Tombez patrick.tombez@camptocamp.com https://github.com/p-tombez
Julien Coux julien.coux@camptocamp.com https://github.com/jcoux
+11
View File
@@ -0,0 +1,11 @@
Guatemala, 2018-09-04
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,
Rodrigo Fernandez rodrigo@solucionesprisma.com https://github.com/rocketgithub
+1 -1
View File
@@ -651,7 +651,7 @@
<field name="name">Guatemala</field>
<field name="code">gt</field>
<field file="base/static/img/country_flags/gt.png" name="image" type="base64" />
<field name="currency_id" ref="QTQ" />
<field name="currency_id" ref="GTQ" />
<field eval="502" name="phone_code" />
</record>
<record id="gu" model="res.country">
+14 -1
View File
@@ -1,4 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
#from . import models
from odoo import api, SUPERUSER_ID
#
# Conditional installation of enterprise modules.
#
# This module is defined in community but some steps (defined with 'edition: "enterprise"')
# are only used to test enterprise. As it's not possible to direcly add enterprise
# modules dependencies, this post install hook will install account_accountant if exists.
#
def _auto_install_enterprise_dependencies(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})
module_list = ['account_accountant']
module_ids = env['ir.module.module'].search([('name', 'in', module_list), ('state', '=', 'uninstalled')])
module_ids.sudo().button_install()
@@ -13,4 +13,5 @@ It will install some main apps and will try to execute the most important action
'views/templates.xml',
],
'installable': True,
'post_init_hook': '_auto_install_enterprise_dependencies',
}