[MERGE] forward port branch saas-12.4 up to bef26222b8

This commit is contained in:
Christophe Simonis
2019-09-25 16:53:26 +02:00
19 changed files with 178 additions and 43 deletions
+1 -1
View File
@@ -270,7 +270,7 @@ class ResPartner(models.Model):
RIGHT JOIN account_account acc ON aml.account_id = acc.id
WHERE acc.internal_type = %s
AND NOT acc.deprecated AND acc.company_id = %s
AND move.state == 'posted'
AND move.state = 'posted'
GROUP BY partner.id
HAVING %s * COALESCE(SUM(aml.amount_residual), 0) ''' + operator + ''' %s''', (account_type, self.env.user.company_id.id, sign, operand))
res = self._cr.fetchall()
@@ -68,6 +68,28 @@ class AccountAnalyticDefault(models.Model):
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
@api.model
def default_get(self, fields_list):
defaults = super().default_get(fields_list)
if {'account_analytic_id', 'analytic_tag_ids'} & set(fields_list):
rec = self.env['account.analytic.default'].account_get(
product_id=self.product_id.id,
partner_id=self.move_id.commercial_partner_id.id,
user_id=self.move_id.user_id.id or self.env.uid,
date=fields.Date.today(),
company_id=self.company_id.id,
)
if rec:
if 'account_analytic_id' in fields_list:
defaults.update({
'account_analytic_id': rec.analytic_id.id,
})
if 'analytic_tag_ids' in fields_list:
defaults.update({
'analytic_tag_ids': rec.analytic_tag_ids.ids,
})
return defaults
@api.onchange('product_id', 'account_id')
def _onchange_product_id_account_id(self):
rec = self.env['account.analytic.default'].account_get(
+1
View File
@@ -922,6 +922,7 @@ class Import(models.TransientModel):
if dryrun:
self._cr.execute('ROLLBACK TO SAVEPOINT import')
# cancel all changes done to the registry/ormcache
self.pool.clear_caches()
self.pool.reset_changes()
else:
self._cr.execute('RELEASE SAVEPOINT import')
@@ -398,7 +398,17 @@ class GoogleCalendar(models.AbstractModel):
url = "/calendar/v3/calendars/%s/events/%s?access_token=%s" % ('primary', instance_id, self.get_token())
headers = {'Content-type': 'application/json'}
data.update(recurringEventId=event_ori_google_id, originalStartTime=event_new.recurrent_id_date, sequence=self.get_sequence(instance_id))
_originalStartTime = dict()
if event_new.allday:
_originalStartTime['date'] = event_new.recurrent_id_date.strftime("%Y-%m-%d")
else:
_originalStartTime['datetime'] = event_new.recurrent_id_date.strftime("%Y-%m-%dT%H:%M:%S.%fz")
data.update(
recurringEventId=event_ori_google_id,
originalStartTime=_originalStartTime,
sequence=self.get_sequence(instance_id)
)
data_json = json.dumps(data)
return self.env['google.service']._do_request(url, data_json, headers, type='PUT')
@@ -665,8 +675,9 @@ class GoogleCalendar(models.AbstractModel):
else:
_logger.warning("Impossible to create event %s. [%s]", att.event_id.id, status)
_logger.debug("Response : %s", response)
except:
pass
except Exception as e:
_logger.warning("Exception when updating recurrent event exclusions on google: %s", e)
return new_ids
def update_events(self, lastSync=False):
+4 -2
View File
@@ -149,6 +149,7 @@
<field name="date"/>
<field name="state"/>
<field name="activity_state"/>
<field name="currency_id"/>
<progressbar field="activity_state" colors='{"planned": "success", "today": "warning", "overdue": "danger"}'/>
<templates>
<t t-name="kanban-box">
@@ -156,7 +157,7 @@
<div class="row">
<div class="col-12">
<strong class="o_kanban_record_title"><span><t t-esc="record.name.value"/></span></strong>
<strong class="o_kanban_record_subtitle float-right"><span class="text-right"><t t-esc="record.total_amount.value"/></span></strong>
<strong class="o_kanban_record_subtitle float-right"><span class="text-right"><field name="total_amount" widget="monetary"/></span></strong>
</div>
</div>
<div class="row mt8">
@@ -560,13 +561,14 @@
<field name="total_amount"/>
<field name="accounting_date"/>
<field name="state"/>
<field name="currency_id"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_card oe_kanban_global_click">
<div class="row">
<div class="col-12">
<strong class="o_kanban_record_title"><span><t t-esc="record.name.value"/></span></strong>
<strong class="o_kanban_record_subtitle float-right"><span class="text-right"><t t-esc="record.total_amount.value"/></span></strong>
<strong class="o_kanban_record_subtitle float-right"><span class="text-right"><field name="total_amount" widget="monetary"/></span></strong>
</div>
</div>
<div class="row mt8">
@@ -8,7 +8,7 @@
<field name="arch" type="xml">
<xpath expr="//field[@name='invoice_sent']" position="after">
<field name="l10n_ch_isr_sent" invisible="1"/>
<field name="l10n_ch_currency_name" invisible="1"/>
<field name="l10n_ch_currency_name" invisible="1" readonly="1"/>
</xpath>
<xpath expr="//button[@id='account_invoice_payment_btn']" position="before">
@@ -112,6 +112,7 @@
<IdCodice t-esc="get_vat_number(record.commercial_partner_id.vat)"/>
</IdFiscaleIVA>
<CodiceFiscale t-if="not record.commercial_partner_id.vat" t-esc="record.commercial_partner_id.l10n_it_codice_fiscale"/>
<CodiceFiscale t-if="not record.commercial_partner_id.vat and not record.commercial_partner_id.l10n_it_codice_fiscale" t-esc="99999999999"/>
<Anagrafica>
<Denominazione t-if="record.commercial_partner_id.is_company" t-esc="record.commercial_partner_id.display_name"/>
<Nome t-if="not record.commercial_partner_id.is_company" t-esc="record.commercial_partner_id.name"/>
+1 -1
View File
@@ -505,6 +505,7 @@ var Discuss = AbstractAction.extend({
*/
_initRender: function () {
var self = this;
this.$('.o_mail_discuss_loading').remove();
this._basicComposer = new BasicComposer(this, {
mentionPartnersRestricted: true,
showTyping: true,
@@ -1419,7 +1420,6 @@ var Discuss = AbstractAction.extend({
if (!this._isStarted) {
return;
}
this.$('.o_mail_discuss_loading').remove();
this._initRender();
},
/**
+54
View File
@@ -1,6 +1,7 @@
odoo.define('mail.discuss_test', function (require) {
"use strict";
const Discuss = require('mail.Discuss');
var mailTestUtils = require('mail.testUtils');
var testUtils = require('web.test_utils');
@@ -191,6 +192,59 @@ QUnit.test('messaging not ready', async function (assert) {
discuss.destroy();
});
QUnit.test('messaging initially ready', async function (assert) {
assert.expect(7);
const startDiscussProm = testUtils.makeTestPromise();
testUtils.mock.patch(Discuss, {
/**
* @override
*/
async start() {
const _super = this._super.bind(this, ...arguments); // due to limitation of class.js
assert.step('discuss:starting');
await startDiscussProm;
assert.step('discuss:started');
return _super();
},
});
const discussProm = createDiscuss({
id: 1,
context: {},
params: {},
data: this.data,
services: this.services,
mockRPC(route) {
if (route === '/mail/init_messaging') {
assert.step(route);
}
return this._super(...arguments);
}
});
await testUtils.nextTick();
assert.verifySteps([
'/mail/init_messaging',
'discuss:starting',
]);
startDiscussProm.resolve();
await testUtils.nextTick();
assert.verifySteps(['discuss:started']);
const discuss = await discussProm;
assert.ok(
discuss.el,
"discuss should be rendered");
assert.containsNone(
discuss,
'.o_mail_discuss_loading',
"should not display sidebar or content is loading (messaging is ready)");
testUtils.mock.unpatch(Discuss);
discuss.destroy();
});
QUnit.test('searchview options visibility', async function (assert) {
assert.expect(5);
+1 -2
View File
@@ -572,7 +572,7 @@ class ProductProduct(models.Model):
'target': 'new'}
def _prepare_sellers(self, params):
return self.seller_ids.sorted(lambda s: (s.sequence, -s.min_qty, s.price))
return self.seller_ids.filtered(lambda s: s.name.active).sorted(lambda s: (s.sequence, -s.min_qty, s.price))
def _select_seller(self, partner_id=False, quantity=0.0, date=None, uom_id=False, params=False):
self.ensure_one()
@@ -710,7 +710,6 @@ class SupplierInfo(models.Model):
'res.partner', 'Vendor',
ondelete='cascade', required=True,
help="Vendor of this product", check_company=True)
active = fields.Boolean(related='name.active', readonly=True)
product_name = fields.Char(
'Vendor Product Name',
help="This vendor's product name will be used when printing a request for quotation. Keep empty to use the internal one.")
@@ -115,12 +115,11 @@ class PurchaseOrderLine(models.Model):
def _onchange_quantity(self):
res = super(PurchaseOrderLine, self)._onchange_quantity()
if self.order_id.requisition_id:
for line in self.order_id.requisition_id.line_ids:
if line.product_id == self.product_id:
if line.product_uom_id != self.product_uom:
self.price_unit = line.product_uom_id._compute_price(
line.price_unit, self.product_uom)
else:
self.price_unit = line.price_unit
break
for line in self.order_id.requisition_id.line_ids.filtered(lambda l: l.product_id == self.product_id):
if line.product_uom_id != self.product_uom:
self.price_unit = line.product_uom_id._compute_price(
line.price_unit, self.product_uom)
else:
self.price_unit = line.price_unit
break
return res
+4 -5
View File
@@ -588,11 +588,10 @@ class ProductTemplate(models.Model):
outgoing_qty = fields.Float(
'Outgoing', compute='_compute_quantities', search='_search_outgoing_qty',
compute_sudo=False, digits='Product Unit of Measure')
# The goal of these fields is not to be able to search a location_id/warehouse_id but
# to properly make these fields "dummy": only used to put some keys in context from
# the search view in order to influence computed field
location_id = fields.Many2one('stock.location', 'Location', store=False, search=lambda operator, operand, vals: [])
warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse', store=False, search=lambda operator, operand, vals: [])
# The goal of these fields is to be able to put some keys in context from search view in order
# to influence computed field.
location_id = fields.Many2one('stock.location', 'Location', store=False)
warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse', store=False)
route_ids = fields.Many2many(
'stock.location.route', 'stock_route_product', 'product_id', 'route_id', 'Routes',
domain=[('product_selectable', '=', True)],
+2 -2
View File
@@ -73,8 +73,8 @@
<field name="arch" type="xml">
<xpath expr="//field[@name='pricelist_id']" position="after">
<separator/>
<field name="location_id" context="{'location': self}"/>
<field name="warehouse_id" context="{'warehouse': self}"/>
<field name="location_id" context="{'location': self}" filter_domain="[]"/>
<field name="warehouse_id" context="{'warehouse': self}" filter_domain="[]"/>
<separator/>
<filter name="real_stock_available" string="Available Products" domain="[('qty_available','&gt;',0)]"/>
<filter name="real_stock_negative" string="Negative Forecasted Quantity" domain="[('virtual_available','&lt;',0)]"/>
@@ -404,10 +404,13 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
options = options || {};
return this.canBeDiscarded(recordID)
.then(function (needDiscard) {
if (options.noAbandon || (options.readonlyIfRealDiscard && !needDiscard)) {
if (options.readonlyIfRealDiscard && !needDiscard) {
return;
}
self.model.discardChanges(recordID);
if (options.noAbandon) {
return;
}
if (self.model.canBeAbandoned(recordID)) {
self._abandonRecord(recordID);
return;
@@ -4227,6 +4227,45 @@ QUnit.module('ActionManager', {
actionManager.destroy();
});
QUnit.test('Call twice clearUncommittedChanges in a row does not display twice the discard warning', async function (assert) {
assert.expect(4);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
intercepts: {
clear_uncommitted_changes: function () {
actionManager.clearUncommittedChanges();
},
},
});
// execute an action and edit existing record
await actionManager.doAction(3);
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
assert.containsOnce(actionManager, '.o_form_view.o_form_readonly');
await testUtils.dom.click($('.o_control_panel .o_form_button_edit'));
assert.containsOnce(actionManager, '.o_form_view.o_form_editable');
await testUtils.fields.editInput(actionManager.$('input[name=foo]'), 'val');
actionManager.trigger_up('clear_uncommitted_changes');
await testUtils.nextTick();
assert.containsOnce($('body'), '.modal'); // confirm discard dialog
// confirm discard changes
await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
actionManager.trigger_up('clear_uncommitted_changes');
await testUtils.nextTick();
assert.containsNone($('body'), '.modal');
actionManager.destroy();
});
});
});
+4 -2
View File
@@ -26,8 +26,10 @@ class WebsiteBackend(http.Controller):
current_website = website_id and Website.browse(website_id) or Website.get_current_website()
multi_website = request.env.user.has_group('website.group_multi_website')
dashboard_data['websites'] = (multi_website and request.env['website'].search([]) or current_website).read(['id', 'name'])
for website in dashboard_data['websites']:
websites = multi_website and request.env['website'].search([]) or current_website
dashboard_data['websites'] = websites.read(['id', 'name'])
for rec, website in zip(websites, dashboard_data['websites']):
website['domain'] = rec._get_http_domain()
if website['id'] == current_website.id:
website['selected'] = True
+2 -2
View File
@@ -338,9 +338,9 @@ class View(models.Model):
cur = Website.get_current_website()
if self.env.user.has_group('website.group_website_publisher') and self.env.user.has_group('website.group_multi_website'):
qcontext['multi_website_websites_current'] = {'website_id': cur.id, 'name': cur.name, 'domain': cur.domain}
qcontext['multi_website_websites_current'] = {'website_id': cur.id, 'name': cur.name, 'domain': cur._get_http_domain()}
qcontext['multi_website_websites'] = [
{'website_id': website.id, 'name': website.name, 'domain': website.domain}
{'website_id': website.id, 'name': website.name, 'domain': website._get_http_domain()}
for website in Website.search([]) if website != cur
]
@@ -133,6 +133,12 @@ var Dashboard = AbstractAction.extend({
}).open();
},
on_go_to_website: function (ev) {
ev.preventDefault();
var website = _.findWhere(this.websites, {selected: true});
window.location.href = $.param.querystring(website.domain + '/', {'fw': website.id});
},
on_save_ga_client_id: function(ga_client_id, ga_analytics_key) {
var self = this;
return this._rpc({
@@ -393,10 +399,14 @@ var Dashboard = AbstractAction.extend({
self.on_website_button($(ev.target).data('website-id'));
});
}
var $buttons = $(QWeb.render("website.GoToButtons"));
$buttons.on('click', this.on_go_to_website.bind(this));
this.updateControlPanel({
cp_content: {
$searchview: this.$searchview,
$buttons: QWeb.render("website.GoToButtons"),
$buttons: $buttons,
},
});
},
@@ -181,21 +181,14 @@ var WebsiteRoot = publicRootData.PublicRoot.extend({
* @param {Event} ev
*/
_onWebsiteSwitch: function (ev) {
var websiteID = ev.currentTarget.getAttribute('website-id');
// need to force in each case, even if domain is set
// Website 1: localhost; Website 2: 0.0.0.0; website 3: -
// when you switch 3 <--> 1, you need to force the website
var websiteId = ev.currentTarget.getAttribute('website-id');
var websiteDomain = ev.currentTarget.getAttribute('domain');
var url = $.param.querystring(window.location.href, {fw: websiteID});
var url = window.location.href;
if (websiteDomain && window.location.hostname !== websiteDomain) {
// if domain unchanged, this line will do a nop while we need to refresh
// the page to load the new forced website.
url = new URL(url);
url.hostname = websiteDomain;
var path = window.location.pathname + window.location.search + window.location.hash;
url = websiteDomain + path;
}
window.location.href = url;
window.location.href = $.param.querystring(url, {'fw': websiteId});
},
/**
* @private