[MERGE] forward port branch saas-16 up to 0f2b2c4e2b

This commit is contained in:
Christophe Simonis
2017-07-07 14:30:30 +02:00
136 changed files with 3898 additions and 936 deletions
@@ -3,7 +3,7 @@
from odoo import api, fields, models, _
from odoo.osv import expression
from odoo.tools import float_is_zero, pycompat
from odoo.tools import float_compare, float_round
from odoo.tools import float_compare, float_round, float_repr
from odoo.tools.misc import formatLang
from odoo.exceptions import UserError, ValidationError
@@ -606,7 +606,8 @@ class AccountBankStatementLine(models.Model):
from_clause = "FROM account_move_line aml JOIN account_account acc ON acc.id = aml.account_id "
where_clause = """WHERE aml.company_id = %(company_id)s
AND (
(aml.statement_id IS NULL AND aml.account_id IN %(account_payable_receivable)s)
(aml.statement_id IS NULL AND aml.account_id IN %(account_payable_receivable)s
AND aml.payment_id IS NOT NULL)
OR
("""+acc_type+""" AND aml.reconciled = false)
)"""
@@ -630,7 +631,7 @@ class AccountBankStatementLine(models.Model):
precision = st_line_currency and st_line_currency.decimal_places or company_currency.decimal_places
params = {'company_id': self.env.user.company_id.id,
'account_payable_receivable': (self.journal_id.default_credit_account_id.id, self.journal_id.default_debit_account_id.id),
'amount': float_round(amount, precision_digits=precision),
'amount': float_repr(float_round(amount, precision_digits=precision), precision_digits=precision),
'partner_id': self.partner_id.id,
'excluded_ids': tuple(excluded_ids),
'ref': self.name,
@@ -651,9 +652,9 @@ class AccountBankStatementLine(models.Model):
# Look for a single move line with the same amount
field = currency and 'amount_residual_currency' or 'amount_residual'
liquidity_field = currency and 'amount_currency' or amount > 0 and 'debit' or 'credit'
liquidity_amt_clause = currency and '%(amount)s' or 'abs(%(amount)s)'
liquidity_amt_clause = currency and '%(amount)s::numeric' or 'abs(%(amount)s::numeric)'
sql_query = self._get_common_sql_query(excluded_ids=excluded_ids) + \
" AND ("+field+" = %(amount)s OR (acc.internal_type = 'liquidity' AND "+liquidity_field+" = " + liquidity_amt_clause + ")) \
" AND ("+field+" = %(amount)s::numeric OR (acc.internal_type = 'liquidity' AND "+liquidity_field+" = " + liquidity_amt_clause + ")) \
ORDER BY date_maturity desc, aml.id desc LIMIT 1"
self.env.cr.execute(sql_query, params)
results = self.env.cr.fetchone()
@@ -127,7 +127,12 @@ class AccountConfigSettings(models.TransientModel):
# related values, including the currency_id field on res_company. This in turn will trigger the recomputation
# of account_move_line related field company_currency_id which can be slow depending on the number of entries
# in the database. Thus, if we do not explicitely change the currency_id, we should not write it on the company
# Same for the field `code_digits` which will trigger a write on all the account.account to complete the
# code the missing characters to complete the desired number of digit, leading to a sql_constraint.
if ('company_id' in values and 'currency_id' in values):
if self.env['res.company'].browse(values.get('company_id')).currency_id.id == values.get('currency_id'):
company = self.env['res.company'].browse(values.get('company_id'))
if company.currency_id.id == values.get('currency_id'):
values.pop('currency_id')
if company.accounts_code_digits == values.get('code_digits'):
values.pop('code_digits')
return super(AccountConfigSettings, self).create(values)
+1 -1
View File
@@ -489,7 +489,7 @@ class AccountInvoice(models.Model):
@api.onchange('invoice_line_ids')
def _onchange_invoice_line_ids(self):
taxes_grouped = self.get_taxes_values()
tax_lines = self.tax_line_ids.browse([])
tax_lines = self.tax_line_ids.filtered('manual')
for tax in pycompat.values(taxes_grouped):
tax_lines += tax_lines.new(tax)
self.tax_line_ids = tax_lines
+1 -1
View File
@@ -695,7 +695,7 @@ class AccountMoveLine(models.Model):
company_currency = line.account_id.company_id.currency_id
ret_line = {
'id': line.id,
'name': line.name != '/' and line.move_id.name + ': ' + line.name or line.move_id.name,
'name': line.name and line.name != '/' and line.move_id.name + ': ' + line.name or line.move_id.name,
'ref': line.move_id.ref or '',
# For reconciliation between statement transactions and already registered payments (eg. checks)
# NB : we don't use the 'reconciled' field because the line we're selecting is not the one that gets reconciled
+9
View File
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from ast import literal_eval
from operator import itemgetter
import time
@@ -427,3 +428,11 @@ class ResPartner(models.Model):
return super(ResPartner, self)._commercial_fields() + \
['debit_limit', 'property_account_payable_id', 'property_account_receivable_id', 'property_account_position_id',
'property_payment_term_id', 'property_supplier_payment_term_id', 'last_time_entries_checked']
@api.multi
def action_view_partner_invoices(self):
self.ensure_one()
action = self.env.ref('account.action_invoice_refund_out_tree').read()[0]
action['domain'] = literal_eval(action['domain'])
action['domain'].append(('partner_id', 'child_of', self.id))
return action
@@ -268,13 +268,13 @@
<field name="account_analytic_id" groups="analytic.group_analytic_accounting"
domain="[('company_id', '=', parent.company_id)]"
context="{'default_partner_id': parent.partner_id}"/>
<field name="analytic_tag_ids" groups="analytic.group_analytic_accounting" widget="many2many_tags"/>
<field name="analytic_tag_ids" groups="analytic.group_analytic_accounting" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="quantity"/>
<field name="uom_id" groups="product.group_uom"/>
<field name="price_unit"/>
<field name="discount" groups="base.group_no_one"/>
<field name="invoice_line_tax_ids" widget="many2many_tags" context="{'type':parent.type}"
domain="[('type_tax_use','=','purchase'),('company_id', '=', parent.company_id)]" options="{'no_create': True}"/>
<field name="invoice_line_tax_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create': True}" context="{'type':parent.type}"
domain="[('type_tax_use','=','purchase'),('company_id', '=', parent.company_id)]"/>
<field name="price_subtotal"/>
<field name="currency_id" invisible="1"/>
</tree>
@@ -404,13 +404,13 @@
<field name="account_analytic_id" groups="analytic.group_analytic_accounting"
domain="[('company_id', '=', parent.company_id)]"
context="{'default_partner_id': parent.partner_id}"/>
<field name="analytic_tag_ids" groups="analytic.group_analytic_accounting" widget="many2many_tags"/>
<field name="analytic_tag_ids" groups="analytic.group_analytic_accounting" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="quantity"/>
<field name="uom_id" groups="product.group_uom"/>
<field name="price_unit"/>
<field name="discount" groups="base.group_no_one"/>
<field name="invoice_line_tax_ids" widget="many2many_tags" context="{'type':parent.type}"
domain="[('type_tax_use','=','sale'),('company_id', '=', parent.company_id)]" options="{'no_create': True}"/>
<field name="invoice_line_tax_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create': True}" context="{'type':parent.type}"
domain="[('type_tax_use','=','sale'),('company_id', '=', parent.company_id)]"/>
<field name="price_subtotal"/>
<field name="currency_id" invisible="1"/>
</tree>
+4 -4
View File
@@ -304,8 +304,8 @@
<group>
<group string="Control-Access" groups="base.group_no_one">
<div class="text-muted" colspan="2">Keep empty for no control</div>
<field name="type_control_ids" widget="many2many_tags"/>
<field name="account_control_ids" widget="many2many_tags"/>
<field name="type_control_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="account_control_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</group>
<group string="Miscellaneous">
<field name="inbound_payment_method_ids" widget="many2many_checkboxes" attrs="{'invisible': [('type', 'not in', ['bank', 'cash'])]}"/>
@@ -2037,8 +2037,8 @@
<field name="name"/>
<field name="attribute_value_ids" widget="many2many_tags" groups="product.group_product_variant"/>
<field name="lst_price"/>
<field name="taxes_id" widget="many2many_tags"/>
<field name="supplier_taxes_id" widget="many2many_tags"/>
<field name="taxes_id" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="supplier_taxes_id" widget="many2many_tags" options="{'color_field': 'color'}"/>
</tree>
</field>
</record>
+11
View File
@@ -95,6 +95,17 @@
<field name="priority" eval="20"/>
<field name="groups_id" eval="[(4, ref('account.group_account_invoice'))]"/>
<field name="arch" type="xml">
<div name="button_box" position="inside">
<button type="object" class="oe_stat_button" icon="fa-pencil-square-o" name="action_view_partner_invoices"
attrs="{'invisible': [('customer', '=', False)]}">
<div class="o_form_field o_stat_info">
<span class="o_stat_value">
<field name="total_invoiced" widget='monetary' options="{'currency_field': 'currency_id'}"/>
</span>
<span class="o_stat_text">Invoiced</span>
</div>
</button>
</div>
<group name="container_row_2" position="inside">
<group string="Payments" name="banks">
<field name="currency_id" invisible="1"/>
+2 -2
View File
@@ -11,13 +11,13 @@
<page string="Invoicing" name="invoicing">
<group name="properties">
<group string="Receivables">
<field name="taxes_id" widget="many2many_tags"/>
<field name="taxes_id" widget="many2many_tags" options="{'color_field': 'color'}/>
<field name="property_account_income_id"
domain="[('internal_type','=','other'),('deprecated','=',False)]"
groups="account.group_account_user"/>
</group>
<group string="Payables" name="payables">
<field name="supplier_taxes_id" widget="many2many_tags"/>
<field name="supplier_taxes_id" widget="many2many_tags" options="{'color_field': 'color'}/>
<field name="property_account_expense_id"
domain="[('internal_type','=','other'),('deprecated','=',False)]"
groups="account.group_account_user"/>
+1 -1
View File
@@ -44,7 +44,7 @@
<strong>Customer Code:</strong>
<p t-field="o.partner_id.ref"/>
</div>
<div name="reference" class="col-xs-2" t-if="o.reference and o.type == 'in_invoice'">
<div name="reference" class="col-xs-2" t-if="o.reference">
<strong>Reference:</strong>
<p t-field="o.reference"/>
</div>
@@ -253,7 +253,7 @@
<field name="account_analytic_id" domain="[('company_id', '=', parent.company_id)]" groups="analytic.group_analytic_accounting"/>
<field name="quantity"/>
<field name="price_unit"/>
<field name="tax_ids" widget="many2many_tags" domain="[('type_tax_use','=','sale')]"/>
<field name="tax_ids" widget="many2many_tags" options="{'color_field': 'color'}" domain="[('type_tax_use','=','sale')]"/>
<field name="price_subtotal"/>
<field name="currency_id" invisible="1"/>
<field name="company_id" invisible="1"/>
@@ -369,7 +369,7 @@
<field name="account_analytic_id" domain="[('company_id', '=', parent.company_id)]" groups="analytic.group_analytic_accounting"/>
<field name="quantity"/>
<field name="price_unit"/>
<field name="tax_ids" widget="many2many_tags" domain="[('type_tax_use','=','purchase')]"/>
<field name="tax_ids" widget="many2many_tags" options="{'color_field': 'color'}" domain="[('type_tax_use','=','purchase')]"/>
<field name="price_subtotal"/>
<field name="currency_id" invisible="1"/>
<field name="company_id" invisible="1"/>
@@ -20,6 +20,8 @@ class Partner(models.Model):
@api.model
def _fields_view_get_address(self, arch):
arch = super(Partner, self)._fields_view_get_address(arch)
if not self._context.get('no_address_format'):
return arch
# render the partner address accordingly to address_view_id
doc = etree.fromstring(arch)
for city_node in doc.xpath("//field[@name='city']"):
@@ -8,7 +8,7 @@
<field name="arch" type="xml">
<xpath expr="//div[contains(@class, 'oe_kanban_partner_categories')]" position="inside">
<span class="oe_kanban_list_many2many">
<field name="category_id"/>
<field name="category_id" widget="many2many_tags" options="{'color_field': 'color'}"/>
</span>
</xpath>
</field>
+6 -4
View File
@@ -254,11 +254,13 @@ class Lead(models.Model):
self.update(values)
@api.constrains('user_id')
@api.multi
def _valid_team(self):
if self.user_id:
values = self.with_context(team_id=self.team_id.id)._onchange_user_values(self.user_id.id)
if values:
self.update(values)
for lead in self:
if lead.user_id:
values = lead.with_context(team_id=lead.team_id.id)._onchange_user_values(lead.user_id.id)
if values:
lead.update(values)
@api.onchange('state_id')
def _onchange_state(self):
+4 -5
View File
@@ -94,7 +94,7 @@
</group>
<group>
<field name="priority" widget="priority"/>
<field name="tag_ids" widget="many2many_tags" options="{'no_create_edit': True}"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}"/>
</group>
</group>
<notebook>
@@ -243,7 +243,7 @@
<group>
<group>
<field name="name" string="Opportunity Title" placeholder="e.g. Customer Deal"/>
<field name="partner_id"/>
<field name="partner_id" domain="[('customer', '=', True)]" context="{'search_default_customer': 1}"/>
<field name="email_from" invisible="1"/>
<field name="phone" invisible="1"/>
<label for="planned_revenue"/>
@@ -287,7 +287,6 @@
<field name="user_id"/>
<field name="partner_address_email"/>
<field name="message_needaction_counter"/>
<field name="tag_ids"/>
<field name="partner_id"/>
<field name="activity_summary"/>
<field name="active"/>
@@ -313,7 +312,7 @@
</div>
<div class="oe_kanban_content">
<div>
<field name="tag_ids"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</div>
<div>
<strong><field name="name"/></strong>
@@ -486,7 +485,7 @@
</group>
<group>
<field name="priority" widget="priority"/>
<field name="tag_ids" widget="many2many_tags" options="{'no_create_edit': True}"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}"/>
<field name="lost_reason" attrs="{'invisible': [('active', '=', True)]}"/>
<field name="date_conversion" invisible="1"/>
</group>
+2 -2
View File
@@ -108,8 +108,8 @@
</group>
<group>
<group>
<field name="country_ids" widget="many2many_tags"/>
<field name="state_ids" widget="many2many_tags"/>
<field name="country_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="state_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</group>
<group></group>
<group>
+2 -2
View File
@@ -66,7 +66,7 @@
<field name="license_plate" class="oe_inline" placeholder="e.g. PAE 326"/>
</h2>
<label for="tag_ids" class="oe_edit_only"/>
<field name="tag_ids" widget="many2many_tags" options="{'no_create_edit': True}"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}"/>
</div>
<group col="2" string="General Properties">
<group >
@@ -180,7 +180,7 @@
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<div class="o_kanban_tags_section">
<field name="tag_ids"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</div>
<div class="o_kanban_image">
<img t-att-src="kanban_image('fleet.vehicle', 'image_small', record.id.value)"/>
@@ -5,97 +5,143 @@ var core = require('web.core');
var Dialog = require('web.Dialog');
var framework = require('web.framework');
var CalendarRenderer = require('web.CalendarRenderer');
var CalendarController = require('web.CalendarController');
var _t = core._t;
CalendarRenderer.include({
events: _.extend({}, CalendarRenderer.prototype.events, {
'click .o_google_sync_button': function () {
this.syncCalendar(this.fields_view);
}
CalendarController.include({
custom_events: _.extend({}, CalendarController.prototype.custom_events, {
syncCalendar: '_onSyncCalendar',
}),
syncCalendar: function() {
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Try to sync the calendar with Google Calendar. According to the result
* from Google API, this function may require an action of the user by the
* mean of a dialog.
*
* @private
* @returns {OdooEvent} event
*/
_onSyncCalendar: function (event) {
var self = this;
var context = this.getSession().user_context;
this.$google_button.prop('disabled', true);
this.trigger_up('perform_rpc', {
this._rpc({
route: '/google_calendar/sync_data',
args: {
model: this.state.model,
params: {
model: this.modelName,
fromurl: window.location.href,
local_context: context,
},
on_success: function (o) {
if (o.status === "need_auth") {
Dialog.alert(self, _t("You will be redirected to Google to authorize access to your calendar!"), {
}
}).then(function (o) {
if (o.status === "need_auth") {
Dialog.alert(self, _t("You will be redirected to Google to authorize access to your calendar!"), {
confirm_callback: function() {
framework.redirect(o.url);
},
title: _t('Redirection'),
});
} else if (o.status === "need_config_from_admin") {
if (!_.isUndefined(o.action) && parseInt(o.action)) {
Dialog.confirm(self, _t("The Google Synchronization needs to be configured before you can use it, do you want to do it now?"), {
confirm_callback: function() {
framework.redirect(o.url);
self.do_action(o.action);
},
title: _t('Redirection'),
title: _t('Configuration'),
});
} else if (o.status === "need_config_from_admin") {
if (!_.isUndefined(o.action) && parseInt(o.action)) {
Dialog.confirm(self, _t("The Google Synchronization needs to be configured before you can use it, do you want to do it now?"), {
confirm_callback: function() {
self.do_action(o.action);
},
title: _t('Configuration'),
});
} else {
Dialog.alert(self, _t("An administrator needs to configure Google Synchronization before you can use it!"), {
title: _t('Configuration'),
});
}
} else if (o.status === "need_refresh") {
self.$calendar.fullCalendar('refetchEvents');
} else if (o.status === "need_reset") {
var confirm_text1 = _t("The account you are trying to synchronize (%s) is not the same as the last one used (%s)!");
var confirm_text2 = _t("In order to do this, you first need to disconnect all existing events from the old account.");
var confirm_text3 = _t("Do you want to do this now?");
var text = _.str.sprintf(confirm_text1 + "\n" + confirm_text2 + "\n\n" + confirm_text3, o.info.new_name, o.info.old_name);
Dialog.confirm(self, text, {
confirm_callback: function() {
self._rpc({
route: '/google_calendar/remove_references',
params: {
model: self.state.model,
local_context: context,
},
})
.done(function(o) {
if (o.status === "OK") {
Dialog.alert(self, _t("All events have been disconnected from your previous account. You can now restart the synchronization"), {
title: _t('Event disconnection success'),
});
} else if (o.status === "KO") {
Dialog.alert(self, _t("An error occured while disconnecting events from your previous account. Please retry or contact your administrator."), {
title: _t('Event disconnection error'),
});
} // else NOP
});
},
title: _t('Accounts'),
} else {
Dialog.alert(self, _t("An administrator needs to configure Google Synchronization before you can use it!"), {
title: _t('Configuration'),
});
}
self.$google_button.prop('disabled', false);
},
} else if (o.status === "need_refresh") {
self.reload();
} else if (o.status === "need_reset") {
var confirmText1 = _t("The account you are trying to synchronize (%s) is not the same as the last one used (%s)!");
var confirmText2 = _t("In order to do this, you first need to disconnect all existing events from the old account.");
var confirmText3 = _t("Do you want to do this now?");
var text = _.str.sprintf(confirmText1 + "\n" + confirmText2 + "\n\n" + confirmText3, o.info.new_name, o.info.old_name);
Dialog.confirm(self, text, {
confirm_callback: function() {
self._rpc({
route: '/google_calendar/remove_references',
params: {
model: self.state.model,
local_context: context,
},
})
.done(function(o) {
if (o.status === "OK") {
Dialog.alert(self, _t("All events have been disconnected from your previous account. You can now restart the synchronization"), {
title: _t('Event disconnection success'),
});
} else if (o.status === "KO") {
Dialog.alert(self, _t("An error occured while disconnecting events from your previous account. Please retry or contact your administrator."), {
title: _t('Event disconnection error'),
});
} // else NOP
});
},
title: _t('Accounts'),
});
}
}).always(function () {
event.data.on_always();
});
},
initSidebar: function() {
}
});
CalendarRenderer.include({
events: _.extend({}, CalendarRenderer.prototype.events, {
'click .o_google_sync_button': '_onSyncCalendar',
}),
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Adds the Sync with Google button in the sidebar
*
* @private
*/
_initSidebar: function () {
var self = this;
this._super();
this.$google_button = $();
if (this.state.model === "calendar.event") {
this.$google_button = $('<button/>', {type: 'button', html: _t("Sync with <b>Google</b>")})
this._super.apply(this, arguments);
this.$googleButton = $();
if (this.model === "calendar.event") {
this.$googleButton = $('<button/>', {type: 'button', html: _t("Sync with <b>Google</b>")})
.addClass('o_google_sync_button oe_button btn btn-sm btn-default')
.prepend($('<img/>', {
src: "/google_calendar/static/src/img/calendar_32.png",
}))
.appendTo(self.$('.o_calendar_sidebar'));
.appendTo(self.$sidebar);
}
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Requests to sync the calendar with Google Calendar
*
* @private
*/
_onSyncCalendar: function () {
var self = this;
var context = this.getSession().user_context;
this.$googleButton.prop('disabled', true);
this.trigger_up('syncCalendar', {
on_always: function () {
self.$googleButton.prop('disabled', false);
},
});
},
});
});
@@ -0,0 +1,122 @@
odoo.define('google_calendar.calendar_tests', function (require) {
"use strict";
var CalendarView = require('web.CalendarView');
var testUtils = require('web.test_utils');
var createView = testUtils.createView;
var initialDate = new Date("2016-12-12T08:00:00Z");
QUnit.module('Google Calendar', {
beforeEach: function () {
this.data = {
'calendar.event': {
fields: {
id: {string: "ID", type: "integer"},
user_id: {string: "user", type: "many2one", relation: 'user'},
partner_id: {string: "user", type: "many2one", relation: 'partner', related: 'user_id.partner_id'},
name: {string: "name", type: "char"},
start_date: {string: "start date", type: "date"},
stop_date: {string: "stop date", type: "date"},
start: {string: "start datetime", type: "datetime"},
stop: {string: "stop datetime", type: "datetime"},
allday: {string: "allday", type: "boolean"},
partner_ids: {string: "attendees", type: "one2many", relation: 'partner'},
type: {string: "type", type: "integer"},
},
records: [
{id: 5, user_id: 4, partner_id: 4, name: "event 1", start: "2016-12-13 15:55:05", stop: "2016-12-15 18:55:05", allday: false, partner_ids: [], type: 2},
{id: 6, user_id: 4, partner_id: 4, name: "event 2", start: "2016-12-18 08:00:00", stop: "2016-12-18 09:00:00", allday: false, partner_ids: [], type: 3}
],
check_access_rights: function () {
return $.when(true);
}
},
user: {
fields: {
id: {string: "ID", type: "integer"},
display_name: {string: "Displayed name", type: "char"},
partner_id: {string: "partner", type: "many2one", relation: 'partner'},
image: {string: "image", type: "integer"},
},
records: [
{id: 4, display_name: "user 4", partner_id: 4},
]
},
partner: {
fields: {
id: {string: "ID", type: "integer"},
display_name: {string: "Displayed name", type: "char"},
image: {string: "image", type: "integer"},
},
records: [
{id: 4, display_name: "partner 4", image: 'DDD'}
]
},
filter_partner: {
fields: {
id: {string: "ID", type: "integer"},
user_id: {string: "user", type: "many2one", relation: 'user'},
partner_id: {string: "partner", type: "many2one", relation: 'partner'},
},
records: [
{id: 3, user_id: 4, partner_id: 4}
]
},
};
}
}, function () {
QUnit.test('sync google calendar', function (assert) {
assert.expect(6);
var calendar = createView({
View: CalendarView,
model: 'calendar.event',
data: this.data,
arch:
'<calendar class="o_calendar_test" '+
'date_start="start" '+
'date_stop="stop" '+
'mode="month">'+
'<field name="name"/>'+
'</calendar>',
viewOptions: {
initialDate: initialDate,
},
mockRPC: function (route, args) {
if (route === '/google_calendar/sync_data') {
assert.step(route);
this.data['calendar.event'].records.push(
{id: 7, user_id: 4, partner_id: 4, name: "event from google calendar", start: "2016-12-28 15:55:05", stop: "2016-12-29 18:55:05", allday: false, partner_ids: [], type: 2}
);
return $.when({status: 'need_refresh'});
} else if (route === '/google_calendar/static/src/img/calendar_32.png') {
return $.when();
} else if (route === '/web/dataset/call_kw/calendar.event/search_read') {
assert.step(route);
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(calendar.$('.fc-event').length, 2, "should display 2 events on the month");
var $sidebar = calendar.$('.o_calendar_sidebar');
calendar.$('.o_google_sync_button').click();
assert.verifySteps([
'/web/dataset/call_kw/calendar.event/search_read',
'/google_calendar/sync_data',
'/web/dataset/call_kw/calendar.event/search_read',
], 'should do a search_read before and after the call to sync_data');
assert.strictEqual(calendar.$('.fc-event').length, 3, "should now display 3 events on the month");
calendar.destroy();
});
});
});
@@ -6,4 +6,10 @@
<script type="text/javascript" src="/google_calendar/static/src/js/google_calendar.js"></script>
</xpath>
</template>
<template id="qunit_suite" name="google_calendar_tests" inherit_id="web.qunit_suite">
<xpath expr="//t[@t-set='head']" position="inside">
<script type="text/javascript" src="/google_calendar/static/tests/google_calendar_tests.js"></script>
</xpath>
</template>
</odoo>
+1 -1
View File
@@ -158,7 +158,7 @@ class GoogleDrive(models.Model):
# check if a model is configured with a template
configs = self.search([('model_id', '=', res_model)])
config_values = []
for config in configs:
for config in configs.sudo():
if config.filter_id:
if config.filter_id.user_id and config.filter_id.user_id.id != self.env.user.id:
#Private
+2 -2
View File
@@ -52,7 +52,7 @@
<field name="name" placeholder="Employee's Name"/>
</h1>
</div>
<field name="category_ids" widget="many2many_tags" placeholder="e.g. Part Time" options="{'no_create_edit': True}" groups="hr.group_hr_user"/>
<field name="category_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}" placeholder="e.g. Part Time" groups="hr.group_hr_user"/>
<notebook>
<page name="public" string="Work Information">
<group>
@@ -175,7 +175,7 @@
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<field name="category_ids"/>
<field name="category_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<div class="o_kanban_image">
<img t-att-src="kanban_image('hr.employee', 'image_medium', record.id.value)"/>
<div class="o_employee_summary_icons">
+2 -2
View File
@@ -299,7 +299,7 @@
</group>
<group string="Accounting">
<field name="property_account_expense_id"/>
<field name="supplier_taxes_id" widget="many2many_tags"/>
<field name="supplier_taxes_id" widget="many2many_tags" options="{'color_field': 'color'}"/>
</group>
</group>
</sheet>
@@ -464,7 +464,7 @@
<field name="message_unread" invisible="1"/>
<field name="attachment_number" string=" "/>
<button name="action_get_attachment_view" string="View Attachments" type="object" icon="fa-paperclip"/>
<field name="tax_ids" widget="many2many_tags"/>
<field name="tax_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="total_amount" sum="Total Amount" widget="monetary"/>
</tree>
</field>
-1
View File
@@ -6,7 +6,6 @@
'description': """
Print amount in words on checks issued for expenses
""",
'category': 'Accounting &amp; Finance',
'version': '1.0',
'depends': ['account_check_printing', 'hr_expense'],
'auto_install': True,
@@ -112,7 +112,7 @@
</div>
<div>
<label for="categ_ids" class="oe_edit_only"/>
<field name="categ_ids" widget="many2many_tags" options="{'no_create_edit': True}"/>
<field name="categ_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}"/>
</div>
</h2>
</div>
@@ -255,7 +255,6 @@
<field name="type_id"/>
<field name="job_id"/>
<field name="department_id"/>
<field name="categ_ids"/>
<field name="message_needaction_counter"/>
<field name="attachment_number"/>
<field name="active"/>
@@ -283,7 +282,7 @@
</ul>
</div>
<div class="oe_kanban_content" tooltip="kanban-tooltip">
<field name="categ_ids"/>
<field name="categ_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<b t-if="record.partner_name.raw_value">
<field name="partner_name"/>
</b><t t-else="1">
+10
View File
@@ -48,3 +48,13 @@ class Task(models.Model):
timesheet_ids = fields.One2many('account.analytic.line', 'task_id', 'Timesheets')
_constraints = [(models.BaseModel._check_recursion, 'Circular references are not permitted between tasks and sub-tasks', ['parent_id'])]
@api.model
def create(self, vals):
context = dict(self.env.context)
# Remove default_parent_id to avoid a confusion in get_record_data
if context.get('default_parent_id', False):
vals['parent_id'] = context.pop('default_parent_id', None)
task = super(Task, self.with_context(context)).create(vals)
return task
+1 -1
View File
@@ -9,7 +9,7 @@
""",
'depends': ['l10n_fr'],
'installable': True,
'auto_install': True,
'auto_install': False,
'application': True,
'data': [
'views/no_cancel.xml',
@@ -102,7 +102,7 @@ class AccountMove(models.Model):
"""
moves = self.search([('state', '=', 'posted'),
('company_id', '=', company_id),
('l10n_fr_secure_sequence_number', '!=', False)],
('l10n_fr_secure_sequence_number', '!=', 0)],
order="l10n_fr_secure_sequence_number ASC")
if not moves:
+3 -1
View File
@@ -221,7 +221,9 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
this.$buttons.on('click', '.o_mail_chat_button_mark_read', function () {
chat_manager.mark_all_as_read(self.channel, self.domain);
});
this.$buttons.on('click', '.o_mail_chat_button_unstar_all', chat_manager.unstar_all);
this.$buttons.on('click', '.o_mail_chat_button_unstar_all', function () {
chat_manager.unstar_all();
});
this.thread.on('redirect', this, function (res_model, res_id) {
chat_manager.redirect(res_model, res_id, this.set_channel.bind(this));
@@ -710,7 +710,7 @@
</div>
<group>
<group>
<field name="member_ids" widget="many2many_tags"/>
<field name="member_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</group>
</group>
</sheet>
+6 -7
View File
@@ -434,15 +434,14 @@ class MassMailing(models.Model):
def _compute_next_departure(self):
cron_next_call = self.env.ref('mass_mailing.ir_cron_mass_mailing_queue').sudo().nextcall
str2dt = fields.Datetime.from_string
cron_time = str2dt(cron_next_call)
for mass_mailing in self:
schedule_date = mass_mailing.schedule_date
if schedule_date:
if datetime.now() > fields.Datetime.from_string(schedule_date):
mass_mailing.next_departure = cron_next_call
else:
mass_mailing.next_departure = schedule_date
if mass_mailing.schedule_date:
schedule_date = str2dt(mass_mailing.schedule_date)
mass_mailing.next_departure = max(schedule_date, cron_time)
else:
mass_mailing.next_departure = cron_next_call
mass_mailing.next_departure = cron_time
@api.onchange('mass_mailing_campaign_id')
def _onchange_mass_mailing_campaign_id(self):
@@ -423,7 +423,7 @@
context="{'search_default_not_opt_out':1}"/>
<div attrs="{'invisible': [('mailing_model', '!=', 'mail.mass_mailing.list')]}">
<label for="contact_list_ids" string="Select mailing lists:" class="oe_edit_only"/>
<field name="contact_list_ids" widget="many2many_tags"
<field name="contact_list_ids" widget="many2many_tags" options="{'color_field': 'color'}"
placeholder="Select mailing lists..." class="oe_inline"/>
</div>
</div>
@@ -758,7 +758,7 @@
<group>
<field name="name" string="Campaign Name"/>
<field name="user_id"/>
<field name="tag_ids" widget="many2many_tags" options="{'no_create_edit': True}"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}"/>
<field name="unique_ab_testing" groups="base.group_no_one"/>
</group>
<notebook>
@@ -817,11 +817,11 @@
</ul>
</div>
<div class="oe_kanban_content">
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<div>
<img t-att-src="kanban_image('res.users', 'image_small', record.user_id.raw_value)"
t-att-title="record.user_id.value" width="24" height="24" class="oe_kanban_avatar pull-right"/>
<h3 class="oe_margin_bottom_8"><field name="name"/></h3>
<field name="tag_ids"/>
<a name="%(action_view_mass_mailings_from_campaign)d" type="action"
class="oe_mailings">
<h4 class="oe_margin_top_8"><t t-raw="record.mass_mailing_ids.raw_value.length"/> Mailings</h4>
+1 -1
View File
@@ -98,7 +98,7 @@
<field name="list_price" string="Membership Fee"/>
<field
name="property_account_income_id"/>
<field name="taxes_id" widget="many2many_tags" string="Taxes"/>
<field name="taxes_id" widget="many2many_tags" options="{'color_field': 'color'}" string="Taxes"/>
</group>
</group>
<label for="description"/>
+2 -2
View File
@@ -282,7 +282,7 @@ class MrpWorkorder(models.Model):
for move_lot in self.active_move_lot_ids:
# Check if move_lot already exists
if move_lot.quantity_done <= 0: # rounding...
move_lot.unlink()
move_lot.sudo().unlink()
continue
if not move_lot.lot_id:
raise UserError(_('You should provide a lot for a component'))
@@ -291,7 +291,7 @@ class MrpWorkorder(models.Model):
if lots:
lots[0].quantity_done += move_lot.quantity_done
lots[0].lot_produced_id = self.final_lot_id.id
move_lot.unlink()
move_lot.sudo().unlink()
else:
move_lot.lot_produced_id = self.final_lot_id.id
move_lot.done_wo = True
+6
View File
@@ -11,3 +11,9 @@ class StockQuant(models.Model):
'stock.quant', 'stock_quant_consume_rel', 'produce_quant_id', 'consume_quant_id')
produced_quant_ids = fields.Many2many(
'stock.quant', 'stock_quant_consume_rel', 'consume_quant_id', 'produce_quant_id')
def _prepare_history(self):
vals = super(StockQuant, self)._prepare_history()
vals['consumed_quant_ids'] = [(4, quant.id) for quant in self.consumed_quant_ids]
vals['produced_quant_ids'] = [(4, quant.id) for quant in self.produced_quant_ids]
return vals
+1 -1
View File
@@ -47,7 +47,7 @@
<button name="action_see_attachments" type="object" icon="fa-file" attrs="{'invisible': [('has_attachments', '=', True)]}"/>
<field name="product_qty"/>
<field name="product_uom_id" options="{'no_open':True,'no_create':True}" groups="product.group_uom"/>
<field name="attribute_value_ids" widget="many2many_tags" domain="[('product_ids.product_tmpl_id', '=', parent.product_tmpl_id)]" options="{'no_create':True}" groups="product.group_product_variant"/>
<field name="attribute_value_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create': True}" domain="[('product_ids.product_tmpl_id', '=', parent.product_tmpl_id)]" groups="product.group_product_variant"/>
<field name="operation_id" groups="mrp.group_mrp_routings" domain="[('routing_id', '=', parent.routing_id)]" options="{'no_quick_create':True,'no_create_edit':True}"/>
</tree>
</field>
+1 -1
View File
@@ -22,7 +22,7 @@ class ChangeProductionQty(models.TransientModel):
if 'mo_id' in fields and not res.get('mo_id') and self._context.get('active_model') == 'mrp.production' and self._context.get('active_id'):
res['mo_id'] = self._context['active_id']
if 'product_qty' in fields and not res.get('product_qty') and res.get('mo_id'):
res['product_qty'] = self.env['mrp.production'].browse(res.get['mo_id']).product_qty
res['product_qty'] = self.env['mrp.production'].browse(res['mo_id']).product_qty
return res
@api.model
@@ -10,6 +10,7 @@
<form string="Change Product Qty">
<group>
<field name="product_qty"/>
<field name="mo_id" invisible="1"/>
</group>
<footer>
<button name="change_prod_qty" string="Approve"
+4 -4
View File
@@ -89,7 +89,7 @@
</div>
<field name="type" invisible="1"/>
<field name="price_unit"/>
<field name="tax_id" widget="many2many_tags" domain="[('type_tax_use','=','sale')]"/>
<field name="tax_id" widget="many2many_tags" options="{'color_field': 'color'}" domain="[('type_tax_use','=','sale')]"/>
<field name="invoiced" invisible="1"/>
<field name="price_subtotal" widget="monetary" invisible="1"/>
</group>
@@ -117,7 +117,7 @@
<field name="product_uom_qty" string="Quantity"/>
<field name="product_uom" string="Unit of Measure" groups="product.group_uom"/>
<field name="price_unit"/>
<field name="tax_id" widget="many2many_tags"/>
<field name="tax_id" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="price_subtotal" widget="monetary"/>
</tree>
</field>
@@ -146,7 +146,7 @@
<field name="product_uom" groups="product.group_uom"/>
</div>
<field name="price_unit"/>
<field widget="many2many_tags" name="tax_id" domain="[('type_tax_use','=','sale')]"/>
<field widget="many2many_tags" options="{'color_field': 'color'}" name="tax_id" domain="[('type_tax_use','=','sale')]"/>
<field name="price_subtotal" widget="monetary" invisible="1"/>
</group>
</form>
@@ -156,7 +156,7 @@
<field name="product_uom_qty" string="Quantity"/>
<field name="product_uom" string="Unit of Measure" groups="product.group_uom"/>
<field name="price_unit"/>
<field name="tax_id" domain="[('type_tax_use', '=', 'sale')]" widget="many2many_tags"/>
<field name="tax_id" domain="[('type_tax_use', '=', 'sale')]" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="price_subtotal" widget="monetary"/>
</tree>
</field>
+3 -4
View File
@@ -87,7 +87,6 @@
<field name="memo"/>
<field name="date_done"/>
<field name="message_partner_ids"/>
<field name="tag_ids"/>
<field name="activity_ids" />
<field name="activity_state" />
<templates>
@@ -95,7 +94,7 @@
<div t-attf-class="#{kanban_color(record.color.raw_value)} oe_kanban_global_click_edit oe_semantic_html_override oe_kanban_card">
<field name="tag_ids"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<div class="o_dropdown_kanban dropdown">
<a class="dropdown-toggle btn" data-toggle="dropdown" href="#" >
@@ -143,7 +142,7 @@
<field name="name"/>
<field name="open"/>
<field name="stage_id"/>
<field name="tag_ids" widget="many2many_tags"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</tree>
</field>
</record>
@@ -158,7 +157,7 @@
<field name="stage_id" domain="[('user_id','=',uid)]" widget="statusbar" clickable="1"/>
</header>
<sheet>
<field name="tag_ids" widget="many2many_tags" placeholder="Tags"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" placeholder="Tags"/>
<field name="memo" widget="html" class="oe_memo" editor_height="450px" />
</sheet>
<div class="oe_chatter">
@@ -19,8 +19,9 @@
<field name="arch" type="xml">
<field name="payment_method_id" position="after">
<field name="payment_method_code" invisible="1"/>
<field name="payment_token_id" attrs="{'invisible': [('payment_method_code', '!=', 'electronic')],
'required': [('payment_method_code', '=', 'electronic')]}"/>
<field name="payment_token_id" options="{'no_create': True}"
attrs="{'invisible': [('payment_method_code', '!=', 'electronic')],
'required': [('payment_method_code', '=', 'electronic')]}"/>
</field>
</field>
</record>
+1 -1
View File
@@ -245,7 +245,7 @@
<group>
<field name="acquirer_id"/>
<field name="provider" invisible="1"/>
<field name="payment_token_id"/>
<field name="payment_token_id" options="{'no_create': True}"/>
<field name="acquirer_reference" readonly="1"/>
<field name="create_date"/>
<field name="date_validate"/>
@@ -9,7 +9,7 @@
<group attrs="{'invisible': [('provider', '!=', 'authorize')]}">
<field name="authorize_login"/>
<field name="authorize_transaction_key" password="True"/>
<a colspan="2" href="http://welcome.authorize.net/about_authorizenet/" target="_blank">How to configure your Authorize.Net account (look for Getting Started Guide) ?</a>
<a colspan="2" href="https://www.odoo.com/documentation/user/online/ecommerce/shopper_experience/authorize.html" target="_blank">How to get paid with Authorize.Net</a>
</group>
</xpath>
</field>
+13 -17
View File
@@ -164,6 +164,7 @@ class PosOrder(models.Model):
'origin': self.name,
'account_id': self.partner_id.property_account_receivable_id.id,
'journal_id': self.session_id.config_id.invoice_journal_id.id,
'company_id': self.company_id.id,
'type': 'out_invoice',
'reference': self.name,
'partner_id': self.partner_id.id,
@@ -881,21 +882,14 @@ class PosOrderLine(models.Model):
@api.depends('price_unit', 'tax_ids', 'qty', 'discount', 'product_id')
def _compute_amount_line_all(self):
for line in self:
currency = line.order_id.pricelist_id.currency_id
taxes = line.tax_ids.filtered(lambda tax: tax.company_id.id == line.order_id.company_id.id)
fiscal_position_id = line.order_id.fiscal_position_id
if fiscal_position_id:
taxes = fiscal_position_id.map_tax(taxes, line.product_id, line.order_id.partner_id)
price = self.env['account.tax']._fix_tax_included_price(
line.price_unit * (1 - (line.discount or 0.0) / 100.0), line.product_id.taxes_id, taxes)
line.price_subtotal = line.price_subtotal_incl = price * line.qty
if taxes:
taxes = taxes.compute_all(price, currency, line.qty, product=line.product_id, partner=line.order_id.partner_id or False)
line.price_subtotal = taxes['total_excluded']
line.price_subtotal_incl = taxes['total_included']
line.price_subtotal = currency.round(line.price_subtotal)
line.price_subtotal_incl = currency.round(line.price_subtotal_incl)
fpos = line.order_id.fiscal_position_id
tax_ids_after_fiscal_position = fpos.map_tax(line.tax_ids, line.product_id, line.order_id.partner_id) if fpos else line.tax_ids
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
taxes = tax_ids_after_fiscal_position.compute_all(price, line.order_id.pricelist_id.currency_id, line.qty, product=line.product_id, partner=line.order_id.partner_id)
line.update({
'price_subtotal_incl': taxes['total_included'],
'price_subtotal': taxes['total_excluded'],
})
@api.onchange('product_id')
def _onchange_product_id(self):
@@ -907,8 +901,10 @@ class PosOrderLine(models.Model):
price = self.order_id.pricelist_id.get_product_price(
self.product_id, self.qty or 1.0, self.order_id.partner_id)
self._onchange_qty()
self.price_unit = price
self.tax_ids = self.product_id.taxes_id
self.tax_ids = self.product_id.taxes_id.filtered(lambda r: not self.company_id or r.company_id == self.company_id)
fpos = self.order_id.fiscal_position_id
tax_ids_after_fiscal_position = fpos.map_tax(self.tax_ids, line.product_id, line.order_id.partner_id) if fpos else self.tax_ids
self.price_unit = self.env['account.tax']._fix_tax_included_price(price, self.product_id.taxes_id, tax_ids_after_fiscal_position)
@api.onchange('qty', 'discount', 'price_unit', 'tax_ids')
def _onchange_qty(self):
@@ -37,8 +37,8 @@
<field name="qty"/>
<field name="price_unit" widget="monetary"/>
<field name="discount" widget="monetary"/>
<field name="tax_ids_after_fiscal_position" widget="many2many_tags"/>
<field name="tax_ids" widget="many2many_tags" invisible="1"/>
<field name="tax_ids_after_fiscal_position" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="tax_ids" widget="many2many_tags" options="{'color_field': 'color'}" invisible="1"/>
<field name="price_subtotal" widget="monetary"/>
<field name="price_subtotal_incl" widget="monetary"/>
</tree>
@@ -50,8 +50,8 @@
<field name="price_unit" widget="monetary"/>
<field name="price_subtotal" invisible="1" widget="monetary"/>
<field name="price_subtotal_incl" invisible="1" widget="monetary"/>
<field name="tax_ids_after_fiscal_position" widget="many2many_tags"/>
<field name="tax_ids" widget="many2many_tags" invisible="1"/>
<field name="tax_ids_after_fiscal_position" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="tax_ids" widget="many2many_tags" options="{'color_field': 'color'}" invisible="1"/>
<field name="notice"/>
</group>
</form>
+1 -1
View File
@@ -21,7 +21,7 @@ class ProductTemplate(models.Model):
if self._context.get('categ_id') or self._context.get('default_categ_id'):
return self._context.get('categ_id') or self._context.get('default_categ_id')
category = self.env.ref('product.product_category_all', raise_if_not_found=False)
return category and category.id or False
return category.id if category.type == 'normal' else False
def _get_default_uom_id(self):
return self.env["product.uom"].search([], limit=1, order='id').id
+2 -3
View File
@@ -406,7 +406,7 @@ action = model.activate_sample_project()
</group>
<group>
<field name="date_deadline"/>
<field name="tag_ids" widget="many2many_tags" options="{'no_create_edit': True}"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}"/>
</group>
</group>
<notebook>
@@ -458,7 +458,6 @@ action = model.activate_sample_project()
<field name="remaining_hours" groups="project.group_time_work_estimation_tasks"/>
<field name="date_deadline"/>
<field name="message_needaction_counter"/>
<field name="tag_ids"/>
<field name="attachment_ids"/>
<field name="displayed_image_id"/>
<field name="active"/>
@@ -484,7 +483,7 @@ action = model.activate_sample_project()
</div>
<div class="oe_kanban_content">
<div>
<field name="tag_ids"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</div>
<div>
<strong><field name="name"/></strong>
@@ -25,7 +25,7 @@
<group>
<field name="user_id"/>
<field name="priority" groups="base.group_user" widget="priority"/>
<field name="tag_ids" widget="many2many_tags"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</group>
<group>
<field name="partner_id"/>
@@ -200,7 +200,7 @@
</ul>
</div>
<div class="oe_kanban_content" tooltip="kanban-tooltip">
<field name="tag_ids"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<div>
<b><field name="name"/></b> <br/>
<field name="partner_id"/> <br/>
+4 -4
View File
@@ -218,13 +218,13 @@
<field name="date_planned"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
<field name="account_analytic_id" context="{'default_partner_id':parent.partner_id}" groups="purchase.group_analytic_accounting"/>
<field name="analytic_tag_ids" groups="purchase.group_analytic_accounting" widget="many2many_tags"/>
<field name="analytic_tag_ids" groups="purchase.group_analytic_accounting" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="product_qty"/>
<field name="qty_received" invisible="not context.get('show_purchase', True)"/>
<field name="qty_invoiced" invisible="not context.get('show_purchase', True)"/>
<field name="product_uom" groups="product.group_uom" attrs="{'readonly': [('state', 'in', ('purchase', 'done', 'cancel'))]}"/>
<field name="price_unit"/>
<field name="taxes_id" widget="many2many_tags" domain="[('type_tax_use','=','purchase')]" context="{'default_type_tax_use': 'purchase'}" options="{'no_create': True'}"/>
<field name="taxes_id" widget="many2many_tags" domain="[('type_tax_use','=','purchase')]" context="{'default_type_tax_use': 'purchase'}" options="{'no_create': True', 'color_field': 'color'}"/>
<field name="price_subtotal" widget="monetary"/>
</tree>
<form string="Purchase Order Line">
@@ -242,7 +242,7 @@
<field name="price_unit"/>
</group>
<group>
<field name="taxes_id" widget="many2many_tags" domain="[('type_tax_use', '=', 'purchase')]" options="{'no_create': True'}"/>
<field name="taxes_id" widget="many2many_tags" domain="[('type_tax_use', '=', 'purchase')]" options="{'no_create': True', 'color_field': 'color'}"/>
<field name="date_planned" widget="date"/>
<field name="account_analytic_id" colspan="2" groups="purchase.group_analytic_accounting"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
@@ -614,7 +614,7 @@
<field name="property_account_expense_id" domain="[('deprecated','=',False)]" attrs="{'readonly': [('purchase_ok', '=', 0)]}"/>
</field>
<field name='supplier_taxes_id' position="replace" >
<field name="supplier_taxes_id" colspan="2" widget="many2many_tags" attrs="{'readonly':[('purchase_ok','=',0)]}"/>
<field name="supplier_taxes_id" colspan="2" widget="many2many_tags" options="{'color_field': 'color'}" attrs="{'readonly':[('purchase_ok','=',0)]}"/>
</field>
</field>
</record>
+1 -1
View File
@@ -28,7 +28,7 @@ class Rating(http.Controller):
'rate_name': rate_names[rate], 'rate': rate
})
@http.route(['/rating/<string:token>/<int:rate>/submit_feedback'], type="http", auth="public", method=['post'])
@http.route(['/rating/<string:token>/<int:rate>/submit_feedback'], type="http", auth="public", methods=['post'])
def submit_rating(self, token, rate, **kwargs):
rating = request.env['rating.rating'].sudo().search([('access_token', '=', token)])
if not rating:
+18
View File
@@ -4,3 +4,21 @@
from . import models
from . import report
from . import wizard
from functools import partial
import odoo
from odoo import api, SUPERUSER_ID
def uninstall_hook(cr, registry):
def update_dashboard_graph_model(dbname):
db_registry = odoo.modules.registry.Registry.new(dbname)
with api.Environment.manage(), db_registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
if 'crm.team' in env:
recs = env['crm.team'].search([])
for rec in recs:
rec._onchange_team_type()
cr.after("commit", partial(update_dashboard_graph_model, cr.dbname))
+1
View File
@@ -32,6 +32,7 @@ This module contains all the common features of Sales Management and eCommerce.
'data/sale_demo.xml',
'data/product_product_demo.xml',
],
'uninstall_hook': "uninstall_hook",
'css': ['static/src/css/sale.css'],
'installable': True,
'auto_install': False,
+4 -4
View File
@@ -238,7 +238,7 @@
</div>
</group>
<group>
<field name="tax_id" widget="many2many_tags" domain="[('type_tax_use','=','sale'),('company_id','=',parent.company_id)]"
<field name="tax_id" widget="many2many_tags" options="{'color_field': 'color'}" domain="[('type_tax_use','=','sale'),('company_id','=',parent.company_id)]"
attrs="{'readonly': [('qty_invoiced', '&gt;', 0)]}" options="{'no_create': True}"/>
<label for="customer_lead"/>
<div>
@@ -246,7 +246,7 @@
</div>
<label for="analytic_tag_ids"/>
<div>
<field name="analytic_tag_ids" widget="many2many_tags"/>
<field name="analytic_tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</div>
</group>
</group>
@@ -279,10 +279,10 @@
attrs="{'readonly': [('state', 'in', ('sale','done', 'cancel'))]}"
context="{'company_id': parent.company_id}"
groups="product.group_uom" options='{"no_open": True}'/>
<field name="analytic_tag_ids" groups="analytic.group_analytic_accounting" widget="many2many_tags"/>
<field name="analytic_tag_ids" groups="analytic.group_analytic_accounting" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="price_unit"
attrs="{'readonly': [('qty_invoiced', '&gt;', 0)]}"/>
<field name="tax_id" widget="many2many_tags" domain="[('type_tax_use','=','sale'),('company_id','=',parent.company_id)]"
<field name="tax_id" widget="many2many_tags" options="{'color_field': 'color'}" domain="[('type_tax_use','=','sale'),('company_id','=',parent.company_id)]"
attrs="{'readonly': [('qty_invoiced', '&gt;', 0)]}" options="{'no_create': True}"/>
<field name="discount" groups="sale.group_discount_per_so_line"/>
<field name="price_subtotal" widget="monetary" groups="sale.group_show_price_subtotal"/>
+6 -3
View File
@@ -317,12 +317,15 @@ class Quant(models.Model):
# price update + accounting entries adjustments
solved_quants._price_update(solving_quant.cost)
# merge history (and cost?)
solved_quants.write({
'history_ids': [(4, history_move.id) for history_move in solving_quant.history_ids]
})
solved_quants.write(solving_quant._prepare_history())
solving_quant.with_context(force_unlink=True).unlink()
solving_quant = remaining_solving_quant
def _prepare_history(self):
return {
'history_ids': [(4, history_move.id) for history_move in self.history_ids],
}
@api.multi
def _price_update(self, newprice):
# TDE note: use ACLs instead of sudoing everything
+1 -1
View File
@@ -128,7 +128,7 @@
<group name="inventory">
<group string="Operations" name="operations">
<field name="route_ids" widget="many2many_checkboxes" attrs="{'invisible':[('type', 'in', ['service', 'digital'])]}"/>
<field name="route_from_categ_ids" widget="many2many_tags" readonly="1" attrs="{'invisible':[('route_from_categ_ids', '=', [])]}"/>
<field name="route_from_categ_ids" widget="many2many_tags" options="{'color_field': 'color'}" readonly="1" attrs="{'invisible':[('route_from_categ_ids', '=', [])]}"/>
<label for="sale_delay"/>
<div>
<field name="sale_delay" attrs="{'readonly':[('sale_ok','=',False)]}" class="oe_inline" style="vertical-align:baseline"/> days
+2
View File
@@ -1016,6 +1016,8 @@ class Binary(http.Controller):
elif status != 200 and download:
return request.not_found()
height = int(height)
width = int(width)
if content and (width or height):
# resize maximum 500*500
if width > 500:
+12 -8
View File
@@ -59,7 +59,7 @@ var Apps = Widget.extend({
},
destroy: function() {
$(window).off("message.apps");
$(window).off("message." + this.uniq);
if (this.$ifr) {
this.$ifr.remove();
this.$ifr = null;
@@ -83,13 +83,13 @@ var Apps = Widget.extend({
});
},
'rpc': function(m) {
self.session.rpc.apply(self.session, m.args).then(function(r) {
return self._rpc({route: m.args[0], params: m.args[1]}).then(function(r) {
var w = self.$ifr[0].contentWindow;
w.postMessage({id: m.id, result: r}, client.origin);
});
},
'Model': function(m) {
return this._rpc({model: m.model, method: m.method, args: m.args})
return self._rpc({model: m.model, method: m.args[0], args: m.args[1]})
.then(function(r) {
var w = self.$ifr[0].contentWindow;
w.postMessage({id: m.id, result: r}, client.origin);
@@ -114,10 +114,11 @@ var Apps = Widget.extend({
qs.debug = session.debug;
}
var u = $.param.querystring(client.origin + "/apps/embed/client", qs);
var css = {width: '100%', height: '400px'};
var css = {width: '100%', height: '750px'};
self.$ifr = $('<iframe>').attr('src', u);
$(window).on("message.apps", self.proxy('_on_message'));
self.uniq = _.uniqueId('apps');
$(window).on("message." + self.uniq, self.proxy('_on_message'));
self.on('message:ready', self, function(m) {
var w = this.$ifr[0].contentWindow;
@@ -125,8 +126,8 @@ var Apps = Widget.extend({
type: 'ir.actions.client',
tag: this.remote_action_tag,
params: _.extend({}, this.params, {
db: this.session.db,
origin: this.session.origin,
db: session.db,
origin: session.origin,
})
};
w.postMessage({type:'action', action: act}, client.origin);
@@ -145,7 +146,10 @@ var Apps = Widget.extend({
def.resolve();
}, function() {
self.do_warn(_t('Odoo Apps will be available soon'), _t('Showing locally available modules'), true);
return session.rpc('/web/action/load', {action_id: self.failback_action_id}).then(function(action) {
return self._rpc({
route: '/web/action/load',
params: {action_id: self.failback_action_id},
}).then(function(action) {
return self.do_action(action);
}).always(function () {
def.reject();
+1 -1
View File
@@ -144,7 +144,7 @@ var Pager = Widget.extend({
_edit: function () {
if (this.options.can_edit) {
var self = this;
var $input = $('<input>', {type: 'text', value: this.$value.html()});
var $input = $('<input>', {class: 'o_input', type: 'text', value: this.$value.html()});
this.$value.html($input);
$input.focus();
@@ -567,11 +567,10 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
// Wrong default_* and search_default_* values will no give the expected result
// Wrong group_by values will simply fail and forbid rendering of the destination view
var ncontext = new Context(
_.object(_.reject(_.pairs(self.env.context), function(pair) {
_.object(_.reject(_.pairs(_.extend({}, action_data.context, self.env.context)), function(pair) {
return pair[0].match('^(?:(?:default_|search_default_|show_).+|.+_view_ref|group_by|group_by_no_leaf|active_id|active_ids)$') !== null;
}))
);
ncontext.add(action_data.context || {});
ncontext.add({active_model: self.env.modelName});
if (res_ids) {
ncontext.add({
@@ -273,7 +273,7 @@ var AbstractField = Widget.extend({
*/
reset: function (record, event) {
this._reset(record, event);
return this._render();
return this._render() || $.when();
},
//--------------------------------------------------------------------------
+31 -16
View File
@@ -17,7 +17,6 @@ var dom = require('web.dom');
var Domain = require('web.Domain');
var DomainSelector = require('web.DomainSelector');
var DomainSelectorDialog = require('web.DomainSelectorDialog');
var field_utils = require('web.field_utils');
var framework = require('web.framework');
var session = require('web.session');
var utils = require('web.utils');
@@ -476,15 +475,14 @@ var FieldMonetary = InputField.extend({
this._setCurrency();
if (this.mode === 'edit' && this.currency) {
if (this.mode === 'edit') {
this.tagName = 'div';
this.className += ' o_input';
}
// use the formatFloat function in edit
if (this.mode === 'edit') {
// use the formatFloat function in edit
this.formatType = 'float';
}
this.formatOptions.currency = this.currency;
this.formatOptions.digits = [16, 2];
},
@@ -514,20 +512,19 @@ var FieldMonetary = InputField.extend({
* @private
*/
_renderEdit: function () {
if (!this.currency) {
this._super.apply(this, arguments);
return;
}
this.$el.empty();
// Prepare and add the input
this._prepareInput().appendTo(this.$el);
// prepare and add the currency symbol
var $currencySymbol = $('<span>', {text: this.currency.symbol});
if (this.currency.position === "after") {
this.$el.append($currencySymbol);
} else {
this.$el.prepend($currencySymbol);
if (this.currency) {
// Prepare and add the currency symbol
var $currencySymbol = $('<span>', {text: this.currency.symbol});
if (this.currency.position === "after") {
this.$el.append($currencySymbol);
} else {
this.$el.prepend($currencySymbol);
}
}
},
/**
@@ -791,6 +788,23 @@ var FieldText = InputField.extend(TranslatableFieldMixin, {
}
return this._super();
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Stops the enter navigation in a text area.
*
* @private
* @param {OdooEvent} ev
*/
_onKeydown: function (ev) {
if (ev.which === $.ui.keyCode.ENTER) {
return;
}
this._super.apply(this, arguments);
},
});
/**
@@ -2237,6 +2251,7 @@ var AceEditor = DebouncedField.extend({
});
return {
TranslatableFieldMixin: TranslatableFieldMixin,
DebouncedField: DebouncedField,
FieldEmail: FieldEmail,
FieldBinaryFile: FieldBinaryFile,
@@ -64,6 +64,7 @@ registry
.add('many2many_checkboxes', relational_fields.FieldMany2ManyCheckBoxes)
.add('one2many', relational_fields.FieldOne2Many)
.add('statusbar', relational_fields.FieldStatus)
.add('reference', relational_fields.FieldReference)
.add('one2many_list', relational_fields.FieldOne2Many);
// Special fields
+49 -3
View File
@@ -427,6 +427,52 @@ function parseFloat(value) {
return parsed;
}
/**
* Parse a String containing currency symbol and returns amount
*
* @param {string} value
* The string to be parsed
* We assume that a monetary is always a pair (symbol, amount) separated
* by a non breaking space. A simple float can also be accepted as value
* @param {Object} [field]
* a description of the field (returned by fields_get for example).
* @param {Object} [options] additional options.
* @param {Object} [options.currency] - the description of the currency to use
* @param {integer} [options.currency_id]
* the id of the 'res.currency' to use (ignored if options.currency)
* @param {string} [options.currency_field]
* the name of the field whose value is the currency id
* (ignore if options.currency or options.currency_id)
* Note: if not given it will default to the field currency_field value
* or to 'currency_id'.
* @param {Object} [options.data]
* a mapping of field name to field value, required with
* options.currency_field
*
* @returns {float} the float value contained in the string representation
* @throws {Error} if no float is found or if parameter does not respect monetary condition
*/
function parseMonetary(value, field, options) {
var values = value.split('&nbsp;');
if (values.length === 1) {
return parseFloat(value);
}
else if (values.length !== 2) {
throw new Error(_.str.sprintf(core._t("'%s' is not a correct monetary field"), value));
}
options = options || {};
var currency = options.currency;
if (!currency) {
var currency_id = options.currency_id;
if (!currency_id && options.data) {
var currency_field = options.currency_field || field.currency_field || 'currency_id';
currency_id = options.data[currency_field] && options.data[currency_field].res_id;
}
currency = session.get_currency(currency_id);
}
return parseFloat(values[0] === currency.symbol ? values[1] : values[0]);
}
function parseFloatTime(value) {
var factor = 1;
if (value[0] === '-') {
@@ -494,7 +540,7 @@ return {
many2one: formatMany2one,
monetary: formatMonetary,
one2many: formatX2Many,
reference: _.identity, // todo
reference: formatMany2one,
selection: formatSelection,
text: formatChar,
},
@@ -510,9 +556,9 @@ return {
integer: parseInteger,
many2many: _.identity, // todo
many2one: parseMany2one,
monetary: parseFloat,
monetary: parseMonetary,
one2many: _.identity,
reference: _.identity, // todo
reference: parseMany2one,
selection: _.identity, // todo
text: _.identity, // todo
},
@@ -657,7 +657,14 @@ var FieldX2Many = AbstractField.extend({
reset: function (record, ev) {
if (ev && ev.target === this && ev.data.changes && this.view.arch.tag === 'tree') {
var command = ev.data.changes[this.name];
if (command.operation === 'UPDATE') {
// Here, we only consider 'UPDATE' commands with data, which occur
// with editable list view. In order to keep the current line in
// edition, we call confirmChange which will reset the widgets
// instead of re-rendering the list. 'UPDATE' commands with no data
// can be ignored: they occur in one2manys when the record is
// updated from a dialog and in this case, we can re-render the
// whole subview.
if (command.operation === 'UPDATE' && command.data) {
var state = record.data[this.name];
var fieldNames = state.getFieldNames();
this.renderer.confirmChange(state, command.id, fieldNames, ev.initialEvent);
@@ -677,7 +684,7 @@ var FieldX2Many = AbstractField.extend({
*
* @override
* @private
* @returns {Deferred}
* @returns {Deferred|undefined}
*/
_render: function () {
if (!this.view) {
@@ -1068,9 +1075,15 @@ var FieldOne2Many = FieldX2Many.extend({
// we don't want interference with the components upstream.
ev.stopPropagation();
var id = ev.data.id;
// trigger an empty 'UPDATE' operation when the user clicks on 'Save' in
// the dialog, to notify the main record that a subrecord of this
// relational field has changed (those changes will be already stored on
// that subrecord, thanks to the 'Save').
var onSaved = this._setValue.bind(this, { operation: 'UPDATE', id: id }, {});
this._openFormDialog({
id: ev.data.id,
on_saved: this._setValue.bind(this, { operation: 'NOOP' }, {}),
id: id,
on_saved: onSaved,
readonly: this.mode === 'readonly',
});
},
@@ -1354,7 +1367,6 @@ var FieldMany2ManyTags = AbstractField.extend({
'click .o_delete': '_onDeleteTag',
}),
fieldsToFetch: {
color: {type: 'integer'},
display_name: {type: 'char'},
},
@@ -1367,6 +1379,8 @@ var FieldMany2ManyTags = AbstractField.extend({
if (this.mode === 'edit') {
this.className += ' o_input';
}
this.colorField = this.nodeOptions.color_field;
},
//--------------------------------------------------------------------------
@@ -1431,6 +1445,7 @@ var FieldMany2ManyTags = AbstractField.extend({
_getRenderTagsContext: function () {
var elements = this.value ? _.pluck(this.value.data, 'data') : [];
return {
colorField: this.colorField,
elements: elements,
readonly: this.mode === "readonly",
};
@@ -1558,7 +1573,7 @@ var FormFieldMany2ManyTags = FieldMany2ManyTags.extend({
_onOpenColorPicker: function (event) {
var tag_id = $(event.currentTarget).data('id');
var tag = _.findWhere(this.value.data, { res_id: tag_id });
if (tag && 'color' in tag.data) { // if there is a color field on the related model
if (tag && this.colorField in tag.data) { // if there is a color field on the related model
this.$color_picker = $(qweb.render('FieldMany2ManyTag.colorpicker', {
'widget': this,
'tag_id': tag_id,
@@ -1583,11 +1598,11 @@ var FormFieldMany2ManyTags = FieldMany2ManyTags.extend({
if (color === current_color) { return; }
var changes = {};
changes[this.colorField] = color;
this.trigger_up('field_changed', {
dataPointID: _.findWhere(this.value.data, {res_id: id}).id,
changes: {
color: color,
},
changes: changes,
force_save: true,
});
},
@@ -1598,12 +1613,19 @@ var KanbanFieldMany2ManyTags = FieldMany2ManyTags.extend({
var self = this;
this.$el.empty().addClass('o_field_many2manytags o_kanban_tags');
_.each(this.value.data, function (m2m) {
// 10th color is invisible
if ('color' in m2m.data && m2m.data.color !== 10) {
$('<span>')
.addClass('o_tag o_tag_color_' + m2m.data.color)
var $tag = $('<span>')
.attr('title', _.str.escapeHTML(m2m.data.display_name))
.appendTo(self.$el);
if (self.colorField in m2m.data) {
if (m2m.data[self.colorField] === 10) {
// 10th color is invisible
$tag.hide();
} else {
$tag.addClass('o_tag o_tag_color_' + m2m.data[self.colorField]);
}
} else {
// display tags in grey by default
$tag.addClass('o_tag o_tag_color_0');
}
});
},
@@ -1772,15 +1794,7 @@ var FieldSelection = AbstractField.extend({
*/
init: function () {
this._super.apply(this, arguments);
if (this.field.type === 'many2one') {
this.values = this.record.specialData[this.name];
this.formatType = 'many2one';
} else {
this.values = _.reject(this.field.selection, function (v) {
return v[0] === false && v[1] === '';
});
}
this.values = [[false, this.attrs.placeholder || '']].concat(this.values);
this._setValues();
},
//--------------------------------------------------------------------------
@@ -1810,13 +1824,12 @@ var FieldSelection = AbstractField.extend({
* @private
*/
_renderEdit: function () {
if (!this.$el.children().length) {
for (var i = 0 ; i < this.values.length ; i++) {
this.$el.append($('<option/>', {
value: JSON.stringify(this.values[i][0]),
html: this.values[i][1]
}));
}
this.$el.empty();
for (var i = 0 ; i < this.values.length ; i++) {
this.$el.append($('<option/>', {
value: JSON.stringify(this.values[i][0]),
html: this.values[i][1]
}));
}
var value = this.value;
if (this.field.type === 'many2one' && value) {
@@ -1831,6 +1844,31 @@ var FieldSelection = AbstractField.extend({
_renderReadonly: function () {
this.$el.empty().text(this._formatValue(this.value));
},
/**
* @override
*/
_reset: function () {
this._super.apply(this, arguments);
this._setValues();
},
/**
* Sets the possible field values. If the field is a many2one, those values
* may change during the lifecycle of the widget if the domain change (an
* onchange may change the domain).
*
* @private
*/
_setValues: function () {
if (this.field.type === 'many2one') {
this.values = this.record.specialData[this.name];
this.formatType = 'many2one';
} else {
this.values = _.reject(this.field.selection, function (v) {
return v[0] === false && v[1] === '';
});
}
this.values = [[false, this.attrs.placeholder || '']].concat(this.values);
},
//--------------------------------------------------------------------------
// Handlers
@@ -1856,7 +1894,9 @@ var FieldSelection = AbstractField.extend({
});
var FieldRadio = FieldSelection.extend({
template: 'FieldRadio',
template: null,
className: 'o_field_radio',
tagName: 'span',
specialData: "_fetchSpecialMany2ones",
supportedFieldTypes: ['selection', 'many2one'],
events: _.extend({}, AbstractField.prototype.events, {
@@ -1867,14 +1907,12 @@ var FieldRadio = FieldSelection.extend({
*/
init: function () {
this._super.apply(this, arguments);
if (this.field.type === 'selection') {
this.values = this.field.selection || [];
} else if (this.field.type === 'many2one') {
this.values = _.map(this.record.specialData[this.name], function (val) {
return [val.id, val.display_name];
});
if (this.mode === 'edit') {
this.tagName = 'div';
this.className += this.nodeOptions.horizontal ? 'o_horizontal' : 'o_vertical';
}
this.unique_id = _.uniqueId("radio");
this._setValues();
},
//--------------------------------------------------------------------------
@@ -1898,17 +1936,45 @@ var FieldRadio = FieldSelection.extend({
* @override
*/
_renderEdit: function () {
var value;
var self = this;
var currentValue;
if (this.field.type === 'many2one') {
value = this.value && this.value.data.id;
currentValue = this.value && this.value.data.id;
} else {
value = this.value;
currentValue = this.value;
}
var index = _.findIndex(this.values, function (option) {
return option[0] === value;
this.$el.empty();
_.each(this.values, function (value, index) {
self.$el.append(qweb.render('FieldRadio.button', {
checked: value[0] === currentValue,
id: self.unique_id + '_' + value[0],
index: index,
value: value,
}));
});
this.$("input").prop("checked", false);
this.$('input[data-index="' + index + '"]').prop('checked', true);
},
/**
* @override
*/
_reset: function () {
this._super.apply(this, arguments);
this._setValues();
},
/**
* Sets the possible field values. If the field is a many2one, those values
* may change during the lifecycle of the widget if the domain change (an
* onchange may change the domain).
*
* @private
*/
_setValues: function () {
if (this.field.type === 'selection') {
this.values = this.field.selection || [];
} else if (this.field.type === 'many2one') {
this.values = _.map(this.record.specialData[this.name], function (val) {
return [val.id, val.display_name];
});
}
},
//--------------------------------------------------------------------------
@@ -1930,6 +1996,106 @@ var FieldRadio = FieldSelection.extend({
},
});
/**
* The FieldReference is a combination of a select (for the model) and
* a FieldMany2one for its value.
* Its intern representation is similar to the many2one (a datapoint with a
* `name_get` as data).
*/
var FieldReference = FieldMany2One.extend({
supportedFieldTypes: ['reference'],
template: 'FieldReference',
events: _.extend({}, FieldMany2One.prototype.events, {
'change select': '_onSelectionChange',
}),
/**
* @override
*/
init: function () {
this._super.apply(this, arguments);
// needs to be copied as it is an unmutable object
this.field = _.extend({}, this.field);
if (this.value) {
this._setRelation(this.value.model);
}
},
/**
* @override
*/
start: function () {
this.$('select').val(this.field.relation);
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Add a select in edit mode (for the model).
*
* @override
*/
_renderEdit: function () {
this._super.apply(this, arguments);
if (this.$('select').val()) {
this.$('.o_input_dropdown').show();
this.$el.addClass('o_row'); // this class is used to display the two
// components (select & input) on the same line
} else {
// hide the many2one if the selection is empty
this.$('.o_input_dropdown').hide();
}
},
/**
* @override
* @private
*/
_reset: function () {
this._super.apply(this, arguments);
this._setRelation(this.value && this.value.model);
},
/**
* Set `relation` key in field properties.
*
* @private
* @param {string} model
*/
_setRelation: function (model) {
// used to generate the search in many2one
this.field.relation = model;
},
/**
* @override
* @private
*/
_setValue: function (value, options) {
value = value || {};
// we need to specify the model for the change in basic_model
// the value is then now a dict with id, display_name and model
value.model = this.$('select').val();
return this._super(value, options);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* When the selection (model) changes, the many2one is reset.
*
* @private
*/
_onSelectionChange: function () {
var value = this.$('select').val();
this.reinitialize(false);
this._setRelation(value);
},
});
return {
FieldMany2One: FieldMany2One,
KanbanFieldMany2One: KanbanFieldMany2One,
@@ -1947,6 +2113,8 @@ return {
FieldRadio: FieldRadio,
FieldSelection: FieldSelection,
FieldStatus: FieldStatus,
FieldReference: FieldReference,
};
});
@@ -355,6 +355,11 @@ return core.Class.extend({
return {};
}),
};
if (attrs.options.color_field) {
// used by m2m tags
attrs.relatedFields[attrs.options.color_field] = { type: 'integer' };
attrs.fieldsInfo.default[attrs.options.color_field] = {};
}
}
}
return attrs;
@@ -104,39 +104,15 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
return true;
},
/**
* Discards the changes made to the record whose ID is given, if necessary.
* Automatically leaves to default mode for the given record.
* Waits for the mutex to be unlocked and then calls _.discardChanges.
* This ensures that the confirm dialog isn't displayed directly if there is
* a pending 'write' rpc.
*
* @param {string} [recordID] - default to main recordID
* @param {Object} [options]
* @param {boolean} [options.readonlyIfRealDiscard=false]
* After discarding record changes, the usual option is to make the
* record readonly. However, the view manager calls this function
* at inappropriate times in the current code and in that case, we
* don't want to go back to readonly if there is nothing to discard
* (e.g. when switching record in edit mode in form view, we expect
* the new record to be in edit mode too, but the view manager calls
* this function as the URL changes...) @todo get rid of this when
* the view manager is improved.
* @returns {Deferred}
* @see _.discardChanges
*/
discardChanges: function (recordID, options) {
var self = this;
recordID = recordID || this.handle;
return this.canBeDiscarded(recordID).then(function (needDiscard) {
if (options && options.readonlyIfRealDiscard && !needDiscard) {
return;
}
if (needDiscard) { // Just some optimization
self.model.discardChanges(recordID);
}
if (self.model.isNew(recordID)) {
self._abandonRecord(recordID);
return;
}
return self._confirmSave(recordID);
});
return this.mutex.exec(function () {})
.then(this._discardChanges.bind(this, recordID || this.handle, options));
},
/**
* Method that will be overriden by the views with the ability to have selected ids
@@ -326,6 +302,42 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
this.$buttons.find('button').attr('disabled', true);
}
},
/**
* Discards the changes made to the record whose ID is given, if necessary.
* Automatically leaves to default mode for the given record.
*
* @private
* @param {string} [recordID] - default to main recordID
* @param {Object} [options]
* @param {boolean} [options.readonlyIfRealDiscard=false]
* After discarding record changes, the usual option is to make the
* record readonly. However, the view manager calls this function
* at inappropriate times in the current code and in that case, we
* don't want to go back to readonly if there is nothing to discard
* (e.g. when switching record in edit mode in form view, we expect
* the new record to be in edit mode too, but the view manager calls
* this function as the URL changes...) @todo get rid of this when
* the view manager is improved.
* @returns {Deferred}
*/
_discardChanges: function (recordID, options) {
var self = this;
recordID = recordID || this.handle;
return this.canBeDiscarded(recordID)
.then(function (needDiscard) {
if (options && options.readonlyIfRealDiscard && !needDiscard) {
return;
}
if (needDiscard) { // Just some optimization
self.model.discardChanges(recordID);
}
if (self.model.isNew(recordID)) {
self._abandonRecord(recordID);
return;
}
return self._confirmSave(recordID);
});
},
/**
* Enables buttons so they can be clicked again.
*
@@ -501,11 +513,8 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
var self = this;
ev.stopPropagation();
var recordID = ev.data.recordID;
this.discardChanges(recordID)
this._discardChanges(recordID)
.done(function () {
if (self.model.isNew(recordID)) {
self._abandonRecord(recordID);
}
// TODO this will tell the renderer to rerender the widget that
// asked for the discard but will unfortunately lose the click
// made on another row if any
File diff suppressed because it is too large Load Diff
@@ -112,13 +112,18 @@ var CalendarController = AbstractController.extend({
*/
_onChangeDate: function (event) {
var modelData = this.model.get();
if (modelData.target_date.toString() === event.data.date) {
if (modelData.target_date.isSame(event.data.date)) {
// When clicking on same date, toggle between the two views
switch (modelData.scale) {
case 'month': this.model.setScale('week'); break;
case 'week': this.model.setScale('day'); break;
case 'day': this.model.setScale('month'); break;
}
} else if (modelData.target_date.week() === event.data.date.week()) {
// When clicking on a date in the same week, switch to day view
this.model.setScale('day');
} else {
// When clicking on a random day of a random other week, switch to week view
this.model.setScale('week');
}
this.model.setDate(event.data.date, true);
@@ -151,8 +156,10 @@ var CalendarController = AbstractController.extend({
self.quick.destroy();
self.quick = null;
self.reload(id);
}, function () {
}, function (error, errorEvent) {
// This will occurs if there are some more fields required
// Preventdefaulting the error event will prevent the traceback window
errorEvent.preventDefault();
event.data.options.disableQuickCreate = true;
event.data.data.on_save = self.quick.destroy.bind(self.quick);
self._onOpenCreate(event.data);
@@ -393,16 +393,19 @@ return AbstractModel.extend({
this.data.fc_options = this._getFullCalendarOptions();
var defs = _.map(this.data.filters, this._loadFilter.bind(this));
return $.when.apply($, defs).then(function () {
return self._rpc({
var eventsDef = $.Deferred().resolve([]);
var filterDomain = self._getFilterDomain();
if (filterDomain.length || _.isEmpty(self.data.filters)) {
eventsDef = self._rpc({
model: self.modelName,
method: 'search_read',
context: self.data.context,
fields: self.fieldNames,
domain: self.data.domain.concat(self._getRangeDomain()).concat(self._getFilterDomain())
})
.then(function (events) {
domain: self.data.domain.concat(self._getRangeDomain()).concat(filterDomain)
});
}
return eventsDef.then(function (events) {
self.data.data = _.map(events, self._recordToCalendarEvent.bind(self));
return $.when(
self._loadColors(self.data, self.data.data),
@@ -164,6 +164,7 @@ return AbstractRenderer.extend({
init: function (parent, state, params) {
this._super.apply(this, arguments);
this.displayFields = params.displayFields;
this.model = params.model;
this.filters = [];
this.color_map = {};
@@ -119,6 +119,7 @@ var CalendarView = AbstractView.extend({
this.rendererParams.displayFields = displayFields;
this.rendererParams.eventTemplate = _.findWhere(arch.children, {'tag': 'templates'});
this.rendererParams.model = viewInfo.model;
this.loadParams.fieldNames = _.uniq(fieldNames);
this.loadParams.mapping = mapping;
@@ -409,7 +409,7 @@ var FormController = BasicController.extend({
* @private
*/
_onDiscard: function () {
this.discardChanges();
this._discardChanges();
},
/**
* Called when the user clicks on 'Duplicate Record' in the sidebar
@@ -49,27 +49,6 @@ var ListController = BasicController.extend({
// Public
//--------------------------------------------------------------------------
/**
* To improve performance, list view must not be rerendered if it is asked
* to discard all its changes. Indeed, only the in-edition row needs to be
* discarded in that case.
*
* @override
* @param {string} [recordID] - default to main recordID
* @returns {Deferred}
*/
discardChanges: function (recordID) {
if ((recordID || this.handle) === this.handle) {
recordID = this.renderer.getEditableRecordID();
if (recordID === null) {
return $.when();
}
}
var self = this;
return this._super(recordID).then(function () {
self._updateButtons('readonly');
});
},
/**
* Calculate the active domain of the list view. This should be done only
* if the header checkbox has been checked. This is done by evaluating the
@@ -245,6 +224,28 @@ var ListController = BasicController.extend({
return this.renderer.updateState(state, {noRender: true})
.then(this._setMode.bind(this, 'readonly', id));
},
/**
* To improve performance, list view must not be rerendered if it is asked
* to discard all its changes. Indeed, only the in-edition row needs to be
* discarded in that case.
*
* @override
* @private
* @param {string} [recordID] - default to main recordID
* @returns {Deferred}
*/
_discardChanges: function (recordID) {
if ((recordID || this.handle) === this.handle) {
recordID = this.renderer.getEditableRecordID();
if (recordID === null) {
return $.when();
}
}
var self = this;
return this._super(recordID).then(function () {
self._updateButtons('readonly');
});
},
/**
* @override
* @private
@@ -362,7 +363,7 @@ var ListController = BasicController.extend({
*/
_onDiscard: function (ev) {
ev.stopPropagation(); // So that it is not considered as a row leaving
this.discardChanges();
this._discardChanges();
},
/**
* Called when the user asks to edit a row -> Updates the controller buttons
@@ -99,13 +99,16 @@
.o-flex-display();
.o-flex-flow(row, wrap);
.o_cp_buttons, .o_cp_pager {
.o_cp_buttons {
.o-flex-display();
margin-right: auto;
> div {
margin-top: 5px;
}
}
.o_cp_buttons > div {
margin-top: 5px;
.o_cp_pager {
.o-flex-display();
margin-left: auto;
}
}
@@ -64,9 +64,6 @@
.o_field_type_char:first-child {
font-weight: bold;
}
.o_field_type_many2many:last-child img {
float: right;
}
.fc-view {
overflow-x: auto;
> table {
+14 -10
View File
@@ -725,15 +725,11 @@
t-att-autofocus="widget.attrs.autofocus"
t-att-id="widget.idForLabel"/>
</t>
<t t-name="FieldRadio">
<span t-if="widget.mode === 'readonly'"/>
<div t-if="widget.mode !== 'readonly'" t-attf-class="o_field_radio #{widget.nodeOptions.horizontal ? 'o_horizontal' : 'o_vertical'}">
<div t-foreach="widget.values" t-as="value" class="o_radio_item">
<input type="radio" class="o_radio_input" t-att-id="widget.unique_id + '_' + value[0]"
t-att-name="widget.unique_id" t-att-data-value="value[0]" t-att-data-index="value_index"/>
<label t-if="!widget.nodeOptions.no_radiolabel" class="o_form_label" t-att-for="widget.unique_id + '_' + value[0]"><t t-esc="value[1]"/></label>
</div>
<t t-name="FieldRadio.button">
<div class="o_radio_item">
<input type="radio" class="o_radio_input" t-att-checked="checked ? true : undefined"
t-att-data-value="value[0]" t-att-data-index="index" t-att-id="id"/>
<label class="o_form_label" t-att-for="id"><t t-esc="value[1]"/></label>
</div>
</t>
<t t-name="FieldMany2One">
@@ -754,9 +750,17 @@
<button type="button" t-if="!widget.nodeOptions.no_open" class="fa fa-external-link btn btn-default o_external_button" tabindex="-1" draggable="false"/>
</div>
</t>
<t t-name="FieldReference" t-extend="FieldMany2One">
<t t-jquery=".o_input_dropdown" t-operation="before">
<select class="o_input o_field_widget">
<option/>
<option t-foreach="widget.field.selection" t-as="model" t-att-value="model[0]"><t t-esc="model[1]"/></option>
</select>
</t>
</t>
<t t-name="FieldMany2ManyTag">
<t t-foreach="elements" t-as="el">
<span t-attf-class="badge dropdown o_tag_color_#{el.color || 10}" t-att-data-color="el.color || 10" t-att-data-index="el_index" t-att-data-id="el.id">
<span t-attf-class="badge dropdown o_tag_color_#{el[colorField] || 10}" t-att-data-color="el[colorField] || 10" t-att-data-index="el_index" t-att-data-id="el.id">
<span class="o_badge_text"><t t-raw="el.display_name"/></span>
<span t-if="!readonly" class="fa fa-times o_delete"/>
</span>
+5 -1
View File
@@ -20,7 +20,11 @@
<t t-foreach="widget.displayFields" t-as="name">
<div t-attf-class="o_field_#{name} o_field_type_#{fields[name].type}">
<t t-if="widget.displayFields[name].avatar_field">
<t t-esc="fields[name].string"/>: <t t-foreach="widget.getAvatars(record, name, widget.displayFields[name].avatar_field).slice(0,5)" t-as="image"><t t-raw="image"/></t>
<t t-esc="fields[name].string"/>:
<div class="o_calendar_avatars pull-right">
<t t-foreach="widget.getAvatars(record, name, widget.displayFields[name].avatar_field).slice(0,3)" t-as="image"><t t-raw="image"/></t>
<span t-if="record[name].length - 3 > 0">+<t t-esc="record[name].length - 3"/></span>
</div>
</t>
<t t-else="">
<t t-esc="format(record, name)"/>
@@ -1043,6 +1043,40 @@ QUnit.module('basic_fields', {
_t.database.multi_lang = multiLang;
});
QUnit.test('go to next line (and not the next row) when pressing enter', function (assert) {
assert.expect(4);
this.data.partner.fields.foo.type = 'text';
var list = createView({
View: ListView,
model: 'partner',
data: this.data,
arch: '<list editable="top">' +
'<field name="int_field"/>' +
'<field name="foo"/>' +
'<field name="qux"/>' +
'</list>',
});
list.$('tbody tr:first .o_list_text').click();
var $textarea = list.$('textarea.o_field_text');
assert.strictEqual($textarea.length, 1, "should have a text area");
assert.strictEqual($textarea.val(), 'yop', 'should still be "yop" in edit');
assert.strictEqual(list.$('textarea').get(0), document.activeElement,
"text area should have the focus");
// click on enter
list.$('textarea')
.trigger({type: "keydown", which: $.ui.keyCode.ENTER})
.trigger({type: "keyup", which: $.ui.keyCode.ENTER});
assert.strictEqual(list.$('textarea').first().get(0), document.activeElement,
"text area should still have the focus");
list.destroy();
});
QUnit.module('FieldBinary');
QUnit.test('binary fields are correctly rendered', function (assert) {
@@ -1973,6 +2007,63 @@ QUnit.module('basic_fields', {
form.destroy();
});
QUnit.test('monetary field with currency set by an onchange', function (assert) {
// this test ensures that the monetary field can be re-rendered with and
// without currency (which can happen as the currency can be set by an
// onchange)
assert.expect(8);
this.data.partner.onchanges = {
int_field: function (obj) {
obj.currency_id = obj.int_field ? 2 : null;
},
};
var list = createView({
View: ListView,
model: 'partner',
data: this.data,
arch: '<tree editable="top">' +
'<field name="int_field"/>' +
'<field name="qux" widget="monetary"/>' +
'<field name="currency_id" invisible="1"/>' +
'</tree>',
session: {
currencies: _.indexBy(this.data.currency.records, 'id'),
},
});
list.$buttons.find('.o_list_button_add').click();
assert.strictEqual(list.$('div.o_field_widget[name=qux] input').length, 1,
"monetary field should have been rendered correctly (without currency)");
assert.strictEqual(list.$('.o_field_widget[name=qux] span').length, 0,
"monetary field should have been rendered correctly (without currency)");
// set a value for int_field -> should set the currency and re-render qux
list.$('.o_field_widget[name=int_field]').click().val('7').trigger('input');
assert.strictEqual(list.$('div.o_field_widget[name=qux] input').length, 1,
"monetary field should have been re-rendered correctly (with currency)");
assert.strictEqual(list.$('.o_field_widget[name=qux] span:contains(€)').length, 1,
"monetary field should have been re-rendered correctly (with currency)");
var $quxInput = list.$('.o_field_widget[name=qux] input');
$quxInput.click(); // check that the field is focusable
assert.strictEqual(document.activeElement, $quxInput[0],
"focus should be on the qux field's input");
// unset the value of int_field -> should unset the currency and re-render qux
list.$('.o_field_widget[name=int_field]').click().val('0').trigger('input');
$quxInput = list.$('div.o_field_widget[name=qux] input');
assert.strictEqual($quxInput.length, 1,
"monetary field should have been re-rendered correctly (without currency)");
assert.strictEqual(list.$('.o_field_widget[name=qux] span').length, 0,
"monetary field should have been re-rendered correctly (without currency)");
$quxInput.click(); // check that the field is still focusable
assert.strictEqual(document.activeElement, $quxInput[0],
"focus should be on the qux field's input");
list.destroy();
});
QUnit.module('FieldInteger');
QUnit.test('integer field when unset', function (assert) {
@@ -2024,7 +2115,7 @@ QUnit.module('basic_fields', {
});
QUnit.test('integer field in form view with virtual id', function (assert) {
assert.expect(2);
assert.expect(1);
var params = {
View: FormView,
model: 'partner',
@@ -2036,15 +2127,7 @@ QUnit.module('basic_fields', {
var form = createView(params);
assert.strictEqual(form.$('.o_field_widget').text(), "2-20170808020000",
"Should display virtual id");
form.destroy();
params.res_id = this.data.partner.records[1].id = "2.65-20170808020000";
try {
form = createView(params);
} catch (error) {
assert.strictEqual(error.message, '"2.65-20170808020000" is not an integer or a virtual id',
"Should throw an exception because it's a wrong value");
}
form.destroy();
});
@@ -2,6 +2,7 @@ odoo.define('web.field_utils_tests', function (require) {
"use strict";
var core = require('web.core');
var session = require('web.session');
var fieldUtils = require('web.field_utils');
QUnit.module('fields', {}, function () {
@@ -122,7 +123,20 @@ QUnit.test('parse float', function(assert) {
});
QUnit.test('parse monetary', function(assert) {
assert.expect(6);
assert.expect(11);
var originalCurrencies = session.currencies;
session.currencies = {
1: {
digits: [69, 2],
position: "after",
symbol: "€"
},
3: {
digits: [69, 2],
position: "before",
symbol: "$"
}
};
assert.strictEqual(fieldUtils.parse.monetary(""), 0);
assert.strictEqual(fieldUtils.parse.monetary("0"), 0);
@@ -130,6 +144,13 @@ QUnit.test('parse monetary', function(assert) {
assert.strictEqual(fieldUtils.parse.monetary("-100.00"), -100);
assert.strictEqual(fieldUtils.parse.monetary("1,000.00"), 1000);
assert.strictEqual(fieldUtils.parse.monetary("1,000,000.00"), 1000000);
assert.strictEqual(fieldUtils.parse.monetary("$&nbsp;125.00", {}, {currency_id: 3}), 125);
assert.strictEqual(fieldUtils.parse.monetary("1,000.00&nbsp;€", {}, {currency_id: 1}), 1000);
assert.throws(function() {fieldUtils.parse.monetary("$ 12.00", {}, {currency_id: 3})}, /is not a correct/);
assert.throws(function() {fieldUtils.parse.monetary("$&nbsp;12.00", {}, {currency_id: 1})}, /is not a correct/);
assert.throws(function() {fieldUtils.parse.monetary("$&nbsp;12.00&nbsp;34", {}, {currency_id: 3})}, /is not a correct/);
session.currencies = originalCurrencies;
});
});
});
@@ -34,6 +34,8 @@ QUnit.module('relational_fields', {
date: {string: "Some Date", type: "date"},
datetime: {string: "Datetime Field", type: 'datetime'},
user_id: {string: "User", type: 'many2one', relation: 'user'},
reference: {string: "Reference Field", type: 'reference', selection: [
["product", "Product"], ["partner_type", "Partner Type"], ["partner", "Partner"]]},
},
records: [{
id: 1,
@@ -47,6 +49,7 @@ QUnit.module('relational_fields', {
timmy: [],
trululu: 4,
user_id: 17,
reference: 'product,37',
}, {
id: 2,
display_name: "second record",
@@ -963,7 +966,9 @@ QUnit.module('relational_fields', {
});
QUnit.test('domain and context are correctly used when doing a name_search in a m2o', function (assert) {
assert.expect(2);
assert.expect(4);
this.data.partner.records[0].timmy = [12];
var form = createView({
View: FormView,
@@ -975,11 +980,13 @@ QUnit.module('relational_fields', {
'domain="[[\'foo\', \'=\', \'bar\'], [\'foo\', \'=\', foo]]" ' +
'context="{\'hello\': \'world\', \'test\': foo}"/>' +
'<field name="foo"/>' +
'<field name="trululu" context="{\'timmy\': timmy}" domain="[[\'id\', \'in\', timmy]]"/>' +
'<field name="timmy" widget="many2many_tags" invisible="1"/>' +
'</form>',
res_id: 1,
session: {user_context: {hey: "ho"}},
mockRPC: function (route, args) {
if (route === '/web/dataset/call_kw/product/name_search') {
if (args.method === 'name_search' && args.model === 'product') {
assert.deepEqual(
args.kwargs.args,
[['foo', '=', 'bar'], ['foo', '=', 'yop']],
@@ -990,12 +997,20 @@ QUnit.module('relational_fields', {
'the field attr context should have been used for the '
+ 'RPC (evaluated and merged with the session one)');
}
if (args.method === 'name_search' && args.model === 'partner') {
assert.deepEqual(args.kwargs.args, [['id', 'in', [12]]],
'the field attr domain should have been used for the RPC (and evaluated)');
assert.deepEqual(args.kwargs.context, {hey: 'ho', timmy: [[6, false, [12]]]},
'the field attr context should have been used for the RPC (and evaluated)');
}
return this._super.apply(this, arguments);
},
});
form.$buttons.find('.o_form_button_edit').click();
form.$('input').first().click();
form.$('.o_field_widget[name=product_id] input').click();
form.$('.o_field_widget[name=trululu] input').click();
form.destroy();
});
@@ -1235,6 +1250,56 @@ QUnit.module('relational_fields', {
});
});
QUnit.test('many2one: domain updated by an onchange', function (assert) {
assert.expect(2);
this.data.partner.onchanges = {
int_field: function () {},
};
var domain = [];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<field name="int_field"/>' +
'<field name="trululu"/>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (args.method === 'onchange') {
domain = [['id', 'in', [10]]];
return $.when({
domain: {
trululu: domain,
unexisting_field: domain,
}
});
}
if (args.method === 'name_search') {
assert.deepEqual(args.kwargs.args, domain,
"sent domain should be correct");
}
return this._super(route, args);
},
viewOptions: {
mode: 'edit',
},
});
// trigger a name_search (domain should be [])
form.$('.o_field_widget[name=trululu] input').click();
// close the dropdown
form.$('.o_field_widget[name=trululu] input').click();
// trigger an onchange that will update the domain
form.$('.o_field_widget[name=int_field]').val(2).trigger('input');
// trigger a name_search (domain should be [['id', 'in', [10]]])
form.$('.o_field_widget[name=trululu] input').click();
form.destroy();
});
QUnit.module('FieldOne2Many');
QUnit.test('one2many basic properties', function (assert) {
@@ -1522,6 +1587,178 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('edition of one2many field with pager', function (assert) {
assert.expect(31);
var ids = [];
for (var i = 0; i < 45; i++) {
var id = 10 + i;
ids.push(id);
this.data.partner.records.push({
id: id,
display_name: "relational record " + id,
});
}
this.data.partner.records[0].p = ids;
var saveCount = 0;
var checkRead = false;
var readIDs;
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="p">' +
'<kanban>' +
'<field name="display_name"/>' +
'<templates>' +
'<t t-name="kanban-box">' +
'<div class="oe_kanban_global_click">' +
'<a t-if="!read_only_mode" type="delete" class="fa fa-times pull-right delete_icon"/>' +
'<span><t t-esc="record.display_name.value"/></span>' +
'</div>' +
'</t>' +
'</templates>' +
'</kanban>' +
'</field>' +
'</form>',
archs: {
'partner,false,form': '<form><field name="display_name"/></form>',
},
mockRPC: function (route, args) {
if (args.method === 'read' && checkRead) {
readIDs = args.args[0];
checkRead = false;
}
if (args.method === 'write') {
saveCount++;
var nbCommands = args.args[1].p.length;
var nbLinkCommands = _.filter(args.args[1].p, function (command) {
return command[0] === 4;
}).length;
switch(saveCount) {
case 1:
assert.strictEqual(nbCommands, 46,
"should send 46 commands (one for each record)");
assert.strictEqual(nbLinkCommands, 45,
"should send a LINK_TO command for each existing record");
assert.deepEqual(args.args[1].p[45], [0, false, {
display_name: 'new record',
}], "should sent a CREATE command for the new record");
break;
case 2:
assert.strictEqual(nbCommands, 46,
"should send 46 commands");
assert.strictEqual(nbLinkCommands, 45,
"should send a LINK_TO command for each existing record");
assert.deepEqual(args.args[1].p[45], [2, 10, false],
"should sent a DELETE command for the deleted record");
break;
case 3:
assert.strictEqual(nbCommands, 47,
"should send 47 commands");
assert.strictEqual(nbLinkCommands, 43,
"should send a LINK_TO command for each existing record");
assert.deepEqual(args.args[1].p[43],
[0, false, {display_name: 'new record page 1'}],
"should sent correct CREATE command");
assert.deepEqual(args.args[1].p[44],
[0, false, {display_name: 'new record page 2'}],
"should sent correct CREATE command");
assert.deepEqual(args.args[1].p[45],
[2, 11, false],
"should sent correct DELETE command");
assert.deepEqual(args.args[1].p[46],
[2, 52, false],
"should sent correct DELETE command");
break;
}
}
return this._super.apply(this, arguments);
},
res_id: 1,
});
assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
'there should be 40 records on page 1');
assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
'1-40 / 45', "pager range should be correct");
// add a record on page one
checkRead = true;
form.$buttons.find('.o_form_button_edit').click();
form.$('.o-kanban-button-new').click();
$('.modal input').val('new record').trigger('input');
$('.modal .modal-footer .btn-primary:first').click(); // save and close
// checks
assert.strictEqual(readIDs, undefined, "should not have read any record");
assert.strictEqual(form.$('span:contains(new record)').length, 0,
"new record should be on page 2");
assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
'there should be 40 records on page 1');
assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
'1-40 / 46', "pager range should be correct");
assert.strictEqual(form.$('.o_kanban_record:first span:contains(new record)').length,
0, 'new record should not be on page 1');
// save
form.$buttons.find('.o_form_button_save').click();
// delete a record on page one
checkRead = true;
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('.o_kanban_record:first span:contains(relational record 10)').length,
1, 'first record should be the one with id 10 (next checks rely on that)');
form.$('.delete_icon:first').click();
// checks
assert.deepEqual(readIDs, [50],
"should have read a record (to display 40 records on page 1)");
assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
'there should be 40 records on page 1');
assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
'1-40 / 45', "pager range should be correct");
// save
form.$buttons.find('.o_form_button_save').click();
// add and delete records in both pages
form.$buttons.find('.o_form_button_edit').click();
checkRead = true;
readIDs = undefined;
// add and delete a record in page 1
form.$('.o-kanban-button-new').click();
$('.modal input').val('new record page 1').trigger('input');
$('.modal .modal-footer .btn-primary:first').click(); // save and close
assert.strictEqual(form.$('.o_kanban_record:first span:contains(relational record 11)').length,
1, 'first record should be the one with id 11 (next checks rely on that)');
form.$('.delete_icon:first').click();
assert.deepEqual(readIDs, [51],
"should have read a record (to display 40 records on page 1)");
// add and delete a record in page 2
form.$('.o_x2m_control_panel .o_pager_next').click();
assert.strictEqual(form.$('.o_kanban_record:first span:contains(relational record 52)').length,
1, 'first record should be the one with id 52 (next checks rely on that)');
checkRead = true;
readIDs = undefined;
form.$('.delete_icon:first').click();
form.$('.o-kanban-button-new').click();
$('.modal input').val('new record page 2').trigger('input');
$('.modal .modal-footer .btn-primary:first').click(); // save and close
assert.strictEqual(readIDs, undefined, "should not have read any record");
// checks
assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 5,
'there should be 5 records on page 2');
assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
'41-45 / 45', "pager range should be correct");
assert.strictEqual(form.$('.o_kanban_record span:contains(new record page 1)').length,
1, 'new records should be on page 2');
assert.strictEqual(form.$('.o_kanban_record span:contains(new record page 2)').length,
1, 'new records should be on page 2');
// save
form.$buttons.find('.o_form_button_save').click();
form.destroy();
});
QUnit.test('sorting one2many fields', function (assert) {
assert.expect(4);
@@ -2357,6 +2594,45 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('editable o2m with onchange and required field: delete an invalid line', function (assert) {
assert.expect(5);
this.data.partner.onchanges = {
turtles: function () {},
};
this.data.partner.records[0].turtles = [1];
this.data.turtle.records[0].product_id = 37;
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="turtles">' +
'<tree editable="top">' +
'<field name="product_id"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
assert.step(args.method);
return this._super.apply(this, arguments);
},
viewOptions: {
mode: 'edit',
},
});
form.$('.o_data_cell:first').click();
form.$('.o_field_widget[name="product_id"] input').val('').trigger('keyup');
assert.verifySteps(['read', 'read'], 'no onchange should be done as line is invalid');
form.$('.o_list_record_delete').click();
assert.verifySteps(['read', 'read', 'onchange'], 'onchange should have been done');
form.destroy();
});
QUnit.test('onchange in a one2many', function (assert) {
assert.expect(1);
@@ -2515,6 +2791,75 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('one2many and onchange (with command DELETE_ALL)', function (assert) {
assert.expect(5);
this.data.partner.onchanges = {
foo: function (obj) {
obj.p = [[5]];
},
p: function () {}, // dummy onchange on the o2m to execute _isX2ManyValid()
};
this.data.partner.records[0].p = [2];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<field name="foo"/>' +
'<field name="p">' +
'<tree editable="bottom">' +
'<field name="display_name"/>' +
'</tree>' +
'</field>' +
'</form>',
mockRPC: function (method, args) {
if (args.method === 'write') {
assert.deepEqual(args.args[1].p, [
[0, false, {display_name: 'z'}],
[2, 2, false],
], "correct commands should be sent");
}
return this._super.apply(this, arguments);
},
res_id: 1,
viewOptions: {
mode: 'edit',
},
});
assert.strictEqual(form.$('.o_data_row').length, 1,
"o2m should contain one row");
// empty o2m by triggering the onchange
form.$('.o_field_widget[name=foo]').val('trigger onchange').trigger('input');
assert.strictEqual(form.$('.o_data_row').length, 0,
"rows of the o2m should have been deleted");
// add two new subrecords
form.$('.o_field_x2many_list_row_add a').click();
form.$('.o_field_widget[name=display_name]').val('x').trigger('input');
form.$('.o_field_x2many_list_row_add a').click();
form.$('.o_field_widget[name=display_name]').val('y').trigger('input');
assert.strictEqual(form.$('.o_data_row').length, 2,
"o2m should contain two rows");
// empty o2m by triggering the onchange
form.$('.o_field_widget[name=foo]').val('trigger onchange again').trigger('input');
assert.strictEqual(form.$('.o_data_row').length, 0,
"rows of the o2m should have been deleted");
form.$('.o_field_x2many_list_row_add a').click();
form.$('.o_field_widget[name=display_name]').val('z').trigger('input');
form.$buttons.find('.o_form_button_save').click();
form.destroy();
});
QUnit.test('one2many list (editable): readonly domain is evaluated', function (assert) {
assert.expect(2);
@@ -2782,6 +3127,41 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('value of invisible x2many fields is correctly evaluated in context', function (assert) {
assert.expect(1);
this.data.partner.records[0].timmy = [12];
this.data.partner.records[0].p = [2, 3];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:
'<form string="Partners">' +
'<field name="product_id" context="{\'p\': p, \'timmy\': timmy}"/>' +
'<field name="p" invisible="1"/>' +
'<field name="timmy" invisible="1"/>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (args.method === 'name_search') {
assert.deepEqual(
args.kwargs.context, {
p: [[4, 2, false], [4, 3, false]],
timmy: [[6, false, [12]]],
}, 'values of x2manys should have been correctly evaluated in context');
}
return this._super.apply(this, arguments);
},
});
form.$buttons.find('.o_form_button_edit').click();
form.$('.o_field_widget[name=product_id] input').click();
form.destroy();
});
QUnit.test('one2many list, editable, with many2one and with context with parent key', function (assert) {
assert.expect(1);
@@ -4328,6 +4708,93 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('editable list: contexts are correctly sent', function (assert) {
assert.expect(5);
this.data.partner.records[0].timmy = [12];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<field name="foo"/>' +
'<field name="timmy" context="{\'key\': parent.foo}">' +
'<tree editable="top">' +
'<field name="display_name"/>' +
'</tree>' +
'</field>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'read' && args.model === 'partner') {
assert.deepEqual(args.kwargs.context, {
active_field: 2,
bin_size: true,
someKey: 'some value',
}, "sent context should be correct");
}
if (args.method === 'read' && args.model === 'partner_type') {
assert.deepEqual(args.kwargs.context, {
key: 'yop',
someKey: 'some value',
}, "sent context should be correct");
}
if (args.method === 'write') {
assert.deepEqual(args.kwargs.context, {
active_field: 2,
someKey: 'some value',
}, "sent context should be correct");
}
return this._super.apply(this, arguments);
},
session: {
user_context: {someKey: 'some value'},
},
viewOptions: {
mode: 'edit',
context: {active_field: 2},
},
res_id: 1,
});
form.$('.o_data_cell:first').click();
form.$('.o_field_widget[name=display_name]').val('abc').trigger('input');
form.$buttons.find('.o_form_button_save').click();
form.destroy();
});
QUnit.test('resetting invisible one2manys', function (assert) {
assert.expect(3);
this.data.partner.records[0].turtles = [];
this.data.partner.onchanges.foo = function (obj) {
obj.turtles = [[5], [4, 1]];
};
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<field name="foo"/>' +
'<field name="turtles" invisible="1"/>' +
'</form>',
viewOptions: {
mode: 'edit',
},
res_id: 1,
mockRPC: function (route, args) {
assert.step(args.method);
return this._super.apply(this, arguments);
},
});
form.$('input[name="foo"]').val('abcd').trigger('input');
assert.verifySteps(['read', 'onchange']);
form.destroy();
});
QUnit.module('FieldMany2Many');
QUnit.test('many2many kanban: edition', function (assert) {
@@ -4987,6 +5454,96 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('onchange with 40+ commands for a many2many', function (assert) {
// this test ensures that the basic_model correctly handles more LINK_TO
// commands than the limit of the dataPoint (40 for x2many kanban)
assert.expect(23);
// create a lot of partner_types that will be linked by the onchange
var commands = [[5]];
for (var i = 0; i < 45; i++) {
var id = 100 + i;
this.data.partner_type.records.push({id: id, display_name: "type " + id});
commands.push([4, id]);
}
this.data.partner.onchanges = {
foo: function (obj) {
obj.timmy = commands;
},
};
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<field name="foo"/>' +
'<field name="timmy">' +
'<kanban>' +
'<field name="display_name"/>' +
'<templates>' +
'<t t-name="kanban-box">' +
'<div><t t-esc="record.display_name.value"/></div>' +
'</t>' +
'</templates>' +
'</kanban>' +
'</field>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
assert.step(args.method);
if (args.method === 'write') {
assert.strictEqual(args.args[1].timmy[0][0], 6,
"should send a command 6");
assert.strictEqual(args.args[1].timmy[0][2].length, 45,
"should replace with 45 ids");
}
return this._super.apply(this, arguments);
},
viewOptions: {
mode: 'edit',
},
});
assert.verifySteps(['read']);
form.$('.o_field_widget[name=foo]').val('trigger onchange').trigger('input');
assert.verifySteps(['read', 'onchange', 'read']);
assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
'1-40 / 45', "pager should be correct");
assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
'there should be 40 records displayed on page 1');
form.$('.o_pager_next').click();
assert.verifySteps(['read', 'onchange', 'read', 'read']);
assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
'41-45 / 45', "pager should be correct");
assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 5,
'there should be 5 records displayed on page 2');
form.$buttons.find('.o_form_button_save').click();
assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
'1-40 / 45', "pager should be correct");
assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
'there should be 40 records displayed on page 1');
form.$('.o_pager_next').click();
assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
'41-45 / 45', "pager should be correct");
assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 5,
'there should be 5 records displayed on page 2');
form.$('.o_pager_next').click(); // back to page 1
assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
'1-40 / 45', "pager should be correct");
assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
'there should be 40 records displayed on page 1');
form.destroy();
});
QUnit.module('FieldStatus');
QUnit.test('static statusbar widget on many2one field', function (assert) {
@@ -5380,10 +5937,117 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('unset selection on a many2one field', function (assert) {
assert.expect(1);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="trululu" widget="selection"/>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'write') {
assert.strictEqual(args.args[1].trululu, false,
"should send 'false' as trululu value");
}
return this._super.apply(this, arguments);
},
res_id: 1,
viewOptions: {
mode: 'edit',
},
});
form.$('select').val("false").trigger('change');
form.$buttons.find('.o_form_button_save').click();
form.destroy();
});
QUnit.test('widget selection on a many2one: domain updated by an onchange', function (assert) {
assert.expect(4);
this.data.partner.onchanges = {
int_field: function () {},
};
var domain = [];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<field name="int_field"/>' +
'<field name="trululu" widget="selection"/>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (args.method === 'onchange') {
domain = [['id', 'in', [10]]];
return $.when({
domain: {
trululu: domain,
}
});
}
if (args.method === 'name_search') {
assert.deepEqual(args.args[1], domain,
"sent domain should be correct");
}
return this._super(route, args);
},
viewOptions: {
mode: 'edit',
},
});
assert.strictEqual(form.$('.o_field_widget[name=trululu] option').length, 4,
"should be 4 options in the selection");
// trigger an onchange that will update the domain
form.$('.o_field_widget[name=int_field]').val(2).trigger('input');
assert.strictEqual(form.$('.o_field_widget[name=trululu] option').length, 1,
"should be 1 option in the selection");
form.destroy();
});
QUnit.module('FieldMany2ManyTags');
QUnit.test('fieldmany2many tags: rendering and edition', function (assert) {
assert.expect(17);
QUnit.test('fieldmany2many tags without color', function (assert) {
assert.expect(4);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<field name="timmy" widget="many2many_tags"/>' +
'</form>',
mockRPC: function (route, args) {
if (args.method ==='read' && args.model === 'partner_type') {
assert.deepEqual(args.args , [[12], ['display_name']], "should not read any color field");
}
return this._super.apply(this, arguments);
}
});
var $input = form.$('.o_field_many2manytags input');
$input.click(); // opens the dropdown
assert.strictEqual($input.autocomplete('widget').find('li').length, 3,
"autocomplete dropdown should have 3 entries (2 values + 'Search and Edit...')");
$input.autocomplete('widget').find('li:first()').click(); // adds a tag
assert.strictEqual(form.$('.o_field_many2manytags > span').length, 1,
"should contain 1 tag");
assert.ok(form.$('.o_field_many2manytags > span:contains("gold")').length,
"should contain newly added tag 'gold'");
form.destroy();
});
QUnit.test('fieldmany2many tags with color: rendering and edition', function (assert) {
assert.expect(20);
this.data.partner.records[0].timmy = [12, 14];
this.data.partner_type.records.push({id: 13, display_name: "red", color: 8});
@@ -5392,7 +6056,7 @@ QUnit.module('relational_fields', {
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<field name="timmy" widget="many2many_tags" options="{\'no_create_edit\': True}"/>' +
'<field name="timmy" widget="many2many_tags" options="{\'color_field\': \'color\', \'no_create_edit\': True}"/>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
@@ -5403,6 +6067,9 @@ QUnit.module('relational_fields', {
assert.ok(_.isEqual(_.sortBy(commands[0][2], _.identity.bind(_)), [12, 13]),
"new value should be [12, 13]");
}
if (args.method ==='read' && args.model === 'partner_type') {
assert.deepEqual(args.args[1], ['display_name', 'color'], "should read the color field");
}
return this._super.apply(this, arguments);
},
});
@@ -5548,7 +6215,7 @@ QUnit.module('relational_fields', {
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<field name="timmy" widget="many2many_tags"/>' +
'<field name="timmy" widget="many2many_tags" options="{\'color_field\': \'color\'}"/>' +
'</form>',
res_id: 1,
});
@@ -5794,6 +6461,58 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('widget radio on a many2one: domain updated by an onchange', function (assert) {
assert.expect(4);
this.data.partner.onchanges = {
int_field: function () {},
};
var domain = [];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<field name="int_field"/>' +
'<field name="trululu" widget="radio"/>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (args.method === 'onchange') {
domain = [['id', 'in', [10]]];
return $.when({
value: {
trululu: false,
},
domain: {
trululu: domain,
},
});
}
if (args.method === 'search_read') {
assert.deepEqual(args.kwargs.domain, domain,
"sent domain should be correct");
}
return this._super(route, args);
},
viewOptions: {
mode: 'edit',
},
});
assert.strictEqual(form.$('.o_field_widget[name=trululu] .o_radio_item').length, 3,
"should be 3 radio buttons");
// trigger an onchange that will update the domain
form.$('.o_field_widget[name=int_field]').val(2).trigger('input');
assert.strictEqual(form.$('.o_field_widget[name=trululu] .o_radio_item').length, 0,
"should be no more radio button");
form.destroy();
});
QUnit.module('FieldMany2ManyCheckBoxes');
QUnit.test('widget many2many_checkboxes', function (assert) {
@@ -5973,6 +6692,103 @@ QUnit.module('relational_fields', {
});
});
QUnit.module('FieldReference');
QUnit.test('reference in form view', function (assert) {
assert.expect(15);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<group>' +
'<field name="reference" string="custom label"/>' +
'</group>' +
'</sheet>' +
'</form>',
archs: {
'product,false,form': '<form string="Product"><field name="display_name"/></form>',
},
res_id: 1,
mockRPC: function (route, args) {
if (args.method === 'get_formview_action') {
assert.deepEqual(args.args[0], [37], "should call get_formview_action with correct id");
return $.when({
res_id: 17,
type: 'ir.actions.act_window',
target: 'current',
res_model: 'res.partner'
});
}
if (args.method === 'get_formview_id') {
assert.deepEqual(args.args[0], [37], "should call get_formview_id with correct id");
return $.when(false);
}
if (args.method === 'name_search') {
assert.strictEqual(args.model, 'partner_type',
"the name_search should be done on the newly set model");
}
if (args.method === 'write') {
assert.strictEqual(args.model, 'partner',
"should write on the current model");
assert.deepEqual(args.args, [[1], {reference: 'partner_type,12'}],
"should write the correct value");
}
return this._super(route, args);
},
});
testUtils.intercept(form, 'do_action', function (event) {
assert.strictEqual(event.data.action.res_id, 17,
"should do a do_action with correct parameters");
});
assert.strictEqual(form.$('a.o_form_uri:contains(xphone)').length, 1,
"should contain a link");
form.$('a.o_form_uri').click(); // click on the link in readonly mode (should trigger do_action)
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('.o_field_widget').length, 2,
"should contain two field widgets (selection and many2one)");
assert.strictEqual(form.$('.o_field_many2one').length, 1,
"should contain one many2one");
assert.strictEqual(form.$('.o_field_widget select').val(), "product",
"widget should contain one select with the model");
assert.strictEqual(form.$('.o_field_widget input').val(), "xphone",
"widget should contain one input with the record");
var options = _.map(form.$('.o_field_widget select > option'), function (el) {
return $(el).val();
});
assert.deepEqual(options, ['', 'product', 'partner_type', 'partner'],
"the options should be correctly set");
form.$('.o_external_button').click(); // click on the external button (should do an RPC)
assert.strictEqual($('.modal .modal-title').text().trim(), 'Open: custom label',
"dialog title should display the custom string label");
$('.modal .o_form_button_cancel').click();
form.$('.o_field_widget select').val('partner_type').trigger('change');
assert.strictEqual(form.$('.o_field_widget input').val(), "",
"many2one value should be reset after model change");
var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
// change the value of the m2o with a suggestion of the dropdown
form.$('.o_field_many2one input').click(); // will trigger a name_search on partner_type model
$dropdown.find('li:first()').click();
form.$buttons.find('.o_form_button_save').click();
assert.strictEqual(form.$('a.o_form_uri:contains(gold)').length, 1,
"should contain a link with the new value");
form.destroy();
});
});
});
});
@@ -94,6 +94,7 @@ var MockServer = Class.extend({
args = JSON.parse(JSON.stringify(args));
if (logLevel === 2) {
console.log('%c[rpc] request ' + route, 'color: blue; font-weight: bold;', args);
args = JSON.parse(JSON.stringify(args));
}
return this._performRpc(route, args).then(function (result) {
var resultString = JSON.stringify(result || false);
@@ -567,8 +568,10 @@ var MockServer = Class.extend({
} else {
result[fields[i]] = false;
}
} else if (field.type === 'one2many' || field.type === 'many2many') {
result[fields[i]] = record[fields[i]] || [];
} else {
result[fields[i]] = record[fields[i]];
result[fields[i]] = record[fields[i]] || false;
}
}
return result;
@@ -356,6 +356,15 @@ function createModel(params) {
addMockEnvironment(widget, params);
// override the model's 'destroy' so that it calls 'destroy' on the widget
// instead, as the widget is the parent of the model and the mockServer.
model.destroy = function () {
// remove the override to properly destroy the model when it will be
// called the second time (by its parent)
delete model.destroy;
widget.destroy();
};
return model;
}
@@ -20,6 +20,7 @@ QUnit.module('Views', {
product_ids: {string: "Favorite products", type: 'one2many', relation: 'product'},
category: {string: "Category M2M", type: 'many2many', relation: 'partner_type'},
date: {string: "Date Field", type: 'date'},
reference: {string: "Reference Field", type: 'reference', selection: [["product", "Product"], ["partner_type", "Partner Type"], ["partner", "Partner"]]},
},
records: [
{id: 1, foo: 'blip', bar: 1, product_id: 37, category: [12], display_name: "first partner", date: "2017-01-25"},
@@ -71,9 +72,15 @@ QUnit.module('Views', {
Model: BasicModel,
data: this.data,
mockRPC: function (route, args) {
assert.strictEqual(args.kwargs.context.active_field, 2,
"should have sent the correct context");
assert.deepEqual(args.kwargs.context, {
active_field: 2,
bin_size: true,
someKey: 'some value',
}, "should have sent the correct context");
return this._super.apply(this, arguments);
},
session: {
user_context: {someKey: 'some value'},
}
});
@@ -302,6 +309,89 @@ QUnit.module('Views', {
model.destroy();
});
QUnit.test('notifyChange on a one2many', function (assert) {
assert.expect(9);
this.data.partner.records[1].product_ids = [37];
this.params.fieldNames = ['product_ids'];
var model = createModel({
Model: BasicModel,
data: this.data,
mockRPC: function (route, args) {
if (args.method === 'name_get') {
assert.strictEqual(args.model, 'product');
}
return this._super(route, args);
},
});
var o2mParams = {
modelName: 'product',
fields: this.data.product.fields,
fieldNames: ['display_name'],
};
model.load(this.params).then(function (resultID) {
model.load(o2mParams).then(function (newRecordID) {
var record = model.get(resultID);
var x2mListID = record.data.product_ids.id;
assert.strictEqual(record.data.product_ids.count, 1,
"there should be one record in the relation");
// trigger a 'ADD' command
model.notifyChanges(resultID, {product_ids: {operation: 'ADD', id: newRecordID}});
assert.deepEqual(model.localData[x2mListID]._changes, [{
operation: 'ADD', id: newRecordID,
}], "_changes should be correct");
record = model.get(resultID);
assert.strictEqual(record.data.product_ids.count, 2,
"there should be two records in the relation");
// trigger a 'UPDATE' command
model.notifyChanges(resultID, {product_ids: {operation: 'UPDATE', id: newRecordID}});
assert.deepEqual(model.localData[x2mListID]._changes, [{
operation: 'ADD', id: newRecordID,
}, {
operation: 'UPDATE', id: newRecordID,
}], "_changes should be correct");
record = model.get(resultID);
assert.strictEqual(record.data.product_ids.count, 2,
"there should be two records in the relation");
// trigger a 'REMOVE' command on the existing record
var existingRecordID = record.data.product_ids.data[0].id;
model.notifyChanges(resultID, {product_ids: {operation: 'REMOVE', ids: [existingRecordID]}});
assert.deepEqual(model.localData[x2mListID]._changes, [{
operation: 'ADD', id: newRecordID,
}, {
operation: 'UPDATE', id: newRecordID,
}, {
operation: 'REMOVE', id: existingRecordID,
}],
"_changes should be correct");
record = model.get(resultID);
assert.strictEqual(record.data.product_ids.count, 1,
"there should be one record in the relation");
// trigger a 'REMOVE' command on the new record
model.notifyChanges(resultID, {product_ids: {operation: 'REMOVE', ids: [newRecordID]}});
assert.deepEqual(model.localData[x2mListID]._changes, [{
operation: 'REMOVE', id: existingRecordID,
}], "_changes should be correct");
record = model.get(resultID);
assert.strictEqual(record.data.product_ids.count, 0,
"there should be no record in the relation");
});
});
model.destroy();
});
QUnit.test('notifyChange on a many2one, without display_name', function (assert) {
assert.expect(3);
@@ -1328,7 +1418,14 @@ QUnit.module('Views', {
this.data.partner.onchanges.foo = function (obj) {
obj.bar = obj.foo.length;
};
this.data.partner.fields.bar.readonly = true;
this.params.fieldsInfo = {
default: {
foo: {},
bar: {
modifiers:"{\"readonly\": true}",
},
}
};
var model = createModel({
Model: BasicModel,
@@ -1694,10 +1791,97 @@ QUnit.module('Views', {
product_id: 37,
product_ids: [],
qux: false,
reference: false,
total: 0
}, "should use the proper eval context");
});
model.destroy();
});
QUnit.test('x2manys in contexts and domains are correctly evaluated', function (assert) {
assert.expect(4);
this.data.partner.records[0].product_ids = [37, 41];
this.params.fieldNames = Object.keys(this.data.partner.fields);
this.params.fieldsInfo = {
form: {
qux: {
context: "{'category': category, 'product_ids': product_ids}",
domain: "[['id', 'in', category], ['id', 'in', product_ids]]",
relatedFields: this.data.partner.fields,
},
category: {
relatedFields: this.data.partner_type.fields,
},
product_ids: {
relatedFields: this.data.product.fields,
},
},
};
this.params.viewType = 'form';
this.params.res_id = 1;
var model = createModel({
Model: BasicModel,
data: this.data,
});
model.load(this.params).then(function (resultID) {
var record = model.get(resultID);
var context = record.getContext({fieldName: 'qux'});
var domain = record.getDomain({fieldName: 'qux'});
assert.deepEqual(context, {
category: [12],
product_ids: [37, 41],
}, "x2many values in context manipulated client-side should be lists of ids");
assert.strictEqual(JSON.stringify(context),
"{\"category\":[[6,false,[12]]],\"product_ids\":[[4,37,false],[4,41,false]]}",
"x2many values in context sent to the server should be commands");
assert.deepEqual(domain, [
['id', 'in', [12]],
['id', 'in', [37, 41]],
], "x2many values in domains should be lists of ids");
assert.strictEqual(JSON.stringify(domain),
"[[\"id\",\"in\",[12]],[\"id\",\"in\",[37,41]]]",
"x2many values in domains should be lists of ids");
});
model.destroy();
});
QUnit.test('fetch references in list, with not too many rpcs', function (assert) {
assert.expect(5);
this.data.partner.records[0].reference = 'product,37';
this.data.partner.records[1].reference = 'product,41';
this.params.fieldNames = ['reference'];
this.params.domain = [];
this.params.groupedBy = [];
this.params.res_id = undefined;
var model = createModel({
Model: BasicModel,
data: this.data,
mockRPC: function (route, args) {
assert.step(route);
if (route === "/web/dataset/call_kw/product/name_get") {
assert.deepEqual(args.args, [[37, 41]],
"the name_get should contain the product ids");
}
return this._super(route, args);
},
});
model.load(this.params).then(function (resultID) {
var record = model.get(resultID);
assert.strictEqual(record.data[0].data.reference.data.display_name, "xphone",
"name_get should have been correctly fetched");
assert.verifySteps(["/web/dataset/search_read", "/web/dataset/call_kw/product/name_get"],
"should have done 2 rpc (searchread and name_get for product)");
});
model.destroy();
});
});});
@@ -143,7 +143,7 @@ QUnit.module('Views', {
};
QUnit.test('simple calendar rendering', function (assert) {
assert.expect(19);
assert.expect(22);
var calendar = createView({
View: CalendarView,
@@ -200,6 +200,13 @@ QUnit.module('Views', {
assert.strictEqual($attendeesFilter.find('.o_calendar_filter_item').length, 3, "should display 3 filter items for 'attendees' who use write_model (2 saved + Everything)");
assert.ok($attendeesFilter.find('.o_field_many2one').length, "should display one2many search bar for 'attendees' filter");
assert.strictEqual(calendar.$('.fc-event').length, 6,
"should display 6 events ('event 5' counts for 2 because it spans two weeks and thus generate two fc-event elements)");
calendar.$('.o_calendar_filter .o_checkbox input').first().click(); // Disable first filter
assert.strictEqual(calendar.$('.fc-event').length, 4, "should now only display 4 event");
calendar.$('.o_calendar_filter .o_checkbox input').eq(1).click(); // Disable second filter
assert.strictEqual(calendar.$('.fc-event').length, 0, "should not display any event anymore");
// test search bar in filter
$sidebar.find('input[type="text"]').trigger('click');
@@ -208,7 +215,7 @@ QUnit.module('Views', {
assert.strictEqual($sidebar.find('.o_calendar_filter:has(h3:contains(attendees)) .o_calendar_filter_item').length, 4, "should display 4 filter items for 'attendees'");
$sidebar.find('input[type="text"]').trigger('click');
assert.strictEqual($('ul.ui-autocomplete li:not(.o_m2o_dropdown_option)').text(), "partner 4", "should display the last choice in one2many autocomplete"); // TODO: remove :not(.o_m2o_dropdown_option) because can't have "create & edit" choice
$sidebar.find('.o_calendar_filter_item[data-id="1"] .o_remove').trigger('click');
$sidebar.find('.o_calendar_filter_item .o_remove').first().trigger('click');
assert.ok($('.modal button.btn:contains(Ok)').length, "should display the confirm message");
$('.modal button.btn:contains(Ok)').trigger('click');
assert.strictEqual($sidebar.find('.o_calendar_filter:has(h3:contains(attendees)) .o_calendar_filter_item').length, 3, "click on remove then should display 3 filter items for 'attendees'");
@@ -349,6 +356,63 @@ QUnit.module('Views', {
calendar.destroy();
});
QUnit.test('quickcreate switching to actual create for required fields', function (assert) {
assert.expect(4);
var event = $.Event();
var calendar = createView({
View: CalendarView,
model: 'event',
data: this.data,
arch:
'<calendar class="o_calendar_test" '+
'string="Events" ' +
'event_open_popup="true" '+
'date_start="start" '+
'date_stop="stop" '+
'all_day="allday" '+
'mode="month" '+
'readonly_form_view_id="1">'+
'<field name="name"/>'+
'</calendar>',
archs: archs,
viewOptions: {
initialDate: initialDate,
},
mockRPC: function (route, args) {
if (args.method === "create") {
return $.Deferred().reject({
code: 200,
data: {},
message: "Odoo server error",
}, event);
}
return this._super(route, args);
},
});
// create a new event
var $cell = calendar.$('.fc-day-grid .fc-row:eq(2) .fc-day:eq(2)');
testUtils.triggerMouseEvent($cell, "mousedown");
testUtils.triggerMouseEvent($cell, "mouseup");
assert.strictEqual($('.modal-dialog.modal-sm .modal-title').text(), 'Create',
"should open the quick create dialog");
$('.modal-body input:first').val('new event in quick create').trigger('input');
$('.modal button.btn:contains(Create)').trigger('click').trigger('click');
// If the event is not default-prevented, a traceback will be raised, which we do not want
assert.ok(event.isDefaultPrevented(), "fail deferred event should have been default-prevented");
assert.strictEqual($('.modal-dialog.modal-lg .modal-title').text(), 'Create: Events',
"should have switched to a bigger modal for an actual create rather than quickcreate");
assert.strictEqual($('.modal-dialog.modal-lg .modal-body .o_form_view.o_form_editable').length, 1,
"should open the full event form view in a dialog");
calendar.destroy();
});
QUnit.test('create event with timezone in week mode', function (assert) {
assert.expect(5);
@@ -640,7 +704,7 @@ QUnit.module('Views', {
});
QUnit.test('use mini calendar', function (assert) {
assert.expect(2);
assert.expect(12);
var calendar = createView({
View: CalendarView,
@@ -665,17 +729,36 @@ QUnit.module('Views', {
},
});
assert.strictEqual(calendar.$('.fc-agendaWeek-view').length, 1, "should be in week mode");
assert.strictEqual(calendar.$('.fc-event').length, 9, "should display 9 events on the week (4 event + 5 days event)");
$('.o_calendar_mini a:contains(19)').click();
calendar.$('.o_calendar_mini a:contains(19)').click();
// Clicking on a day in another week should switch to the other week view
assert.strictEqual(calendar.$('.fc-agendaWeek-view').length, 1, "should be in week mode");
assert.strictEqual(calendar.$('.fc-event').length, 4, "should display 4 events on the week (1 event + 3 days event)");
// Clicking on a day in the same week should switch to that particular day view
calendar.$('.o_calendar_mini a:contains(18)').click();
assert.strictEqual(calendar.$('.fc-agendaDay-view').length, 1, "should be in day mode");
assert.strictEqual(calendar.$('.fc-event').length, 2, "should display 2 events on the day");
// Clicking on the same day should toggle between day, month and week views
calendar.$('.o_calendar_mini a:contains(18)').click();
assert.strictEqual(calendar.$('.fc-month-view').length, 1, "should be in month mode");
assert.strictEqual(calendar.$('.fc-event').length, 7, "should display 7 events on the month (event 5 is on multiple weeks and generates to .fc-event)");
calendar.$('.o_calendar_mini a:contains(18)').click();
assert.strictEqual(calendar.$('.fc-agendaWeek-view').length, 1, "should be in week mode");
assert.strictEqual(calendar.$('.fc-event').length, 4, "should display 4 events on the week (1 event + 3 days event)");
calendar.$('.o_calendar_mini a:contains(18)').click();
assert.strictEqual(calendar.$('.fc-agendaDay-view').length, 1, "should be in day mode");
assert.strictEqual(calendar.$('.fc-event').length, 2, "should display 2 events on the day");
calendar.destroy();
});
QUnit.test('rendering, with many2many', function (assert) {
assert.expect(1);
assert.expect(5);
this.data.event.fields.partner_ids.type = 'many2many';
this.data.event.records[0].partner_ids = [1,2,3,4,5];
this.data.partner.records.push({id: 5, display_name: "partner 5", image: 'EEE'});
var calendar = createView({
View: CalendarView,
@@ -698,6 +781,15 @@ QUnit.module('Views', {
assert.strictEqual(calendar.$('.o_calendar_filter_items .o_cal_avatar').length, 3,
"should have 3 avatars in the side bar");
var $event1Avatars = calendar.$('.fc-event .o_calendar_avatars').first();
assert.strictEqual($event1Avatars.find('img').length, 1, "should have 1 avatar");
assert.strictEqual($event1Avatars.find('span').length, 0,
"should not have a span for more attendees since there is only one");
var $event2Avatars = calendar.$('.fc-event:contains(All the day) .o_calendar_avatars');
assert.strictEqual($event2Avatars.find('img').length, 3, "should have 3 avatars");
assert.strictEqual($event2Avatars.find('span').text(), '+2',
"should indicate there are 2 more attendees that we don't show");
calendar.destroy();
});
+60 -1
View File
@@ -2968,6 +2968,53 @@ QUnit.module('Views', {
form.destroy();
});
QUnit.test('discarding before save returns', function (assert) {
assert.expect(4);
var def = $.Deferred();
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<group><field name="foo"/></group>' +
'</form>',
res_id: 2,
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
if (args.method === 'write') {
return def.then(_.constant(result));
}
return result;
},
viewOptions: {
mode: 'edit',
},
});
form.$('input').val("1234").trigger('input');
// save the value and discard directly
form.$buttons.find('.o_form_button_save').click();
form.discardChanges(); // Simulate click on breadcrumb
assert.strictEqual(form.$('.o_field_widget[name="foo"]').val(), "1234",
"field foo should still contain new value");
assert.strictEqual($('.modal').length, 0,
"Confirm dialog should not be displayed");
// complete the write
def.resolve();
assert.strictEqual($('.modal').length, 0,
"Confirm dialog should not be displayed");
assert.strictEqual(form.$('.o_field_widget[name="foo"]').text(), "1234",
"value should have been saved and rerendered in readonly");
form.destroy();
});
QUnit.test('unchanged relational data is sent for onchanges', function (assert) {
assert.expect(1);
@@ -4361,7 +4408,7 @@ QUnit.module('Views', {
});
QUnit.test('default_order on x2many embedded view', function (assert) {
assert.expect(8);
assert.expect(11);
this.data.partner.fields.display_name.sortable = true;
this.data.partner.records[0].p = [1, 4];
@@ -4421,6 +4468,18 @@ QUnit.module('Views', {
assert.ok(form.$('.o_field_one2many tbody tr:eq(2) td:contains(xop)').length,
"record xop should be third");
// client-side sort on edit
form.$buttons.find('.o_form_button_edit').click();
form.$('.o_field_one2many tbody tr:eq(1) td:contains(yop)').click();
$('.modal input[name="foo"]').val('zzz').trigger("input");
$('.modal .modal-footer button:first').click(); // Save
assert.ok(form.$('.o_field_one2many tbody tr:eq(0) td:contains(zzz)').length,
"record zzz should be first");
assert.ok(form.$('.o_field_one2many tbody tr:eq(1) td:contains(zop)').length,
"record zop should be second");
assert.ok(form.$('.o_field_one2many tbody tr:eq(2) td:contains(xop)').length,
"record xop should be third");
form.destroy();
});
@@ -457,7 +457,7 @@ QUnit.module('Views', {
arch: '<kanban class="o_kanban_test">' +
'<templates><t t-name="kanban-box">' +
'<div>' +
'<field name="category_ids"/>' +
'<field name="category_ids" widget="many2many_tags" options="{\'color_field\': \'color\'}"/>' +
'<field name="foo"/>' +
'<field name="state" widget="priority"/>' +
'</div>' +
+68 -2
View File
@@ -2,6 +2,7 @@ odoo.define('web.list_tests', function (require) {
"use strict";
var config = require('web.config');
var FormView = require('web.FormView');
var ListView = require('web.ListView');
var testUtils = require('web.test_utils');
@@ -23,6 +24,8 @@ QUnit.module('Views', {
currency_id: {string: "Currency", type: "many2one",
relation: "res_currency", default: 1},
datetime: {string: "Datetime Field", type: 'datetime'},
reference: {string: "Reference Field", type: 'reference', selection: [
["bar", "Bar"], ["res_currency", "Currency"], ["event", "Event"]]},
},
records: [
{
@@ -37,11 +40,12 @@ QUnit.module('Views', {
currency_id: 2,
date: "2017-01-25",
datetime: "2016-12-12 10:55:05",
reference: 'bar,1',
},
{id: 2, bar: true, foo: "blip", int_field: 9, qux: 13,
m2o: 2, m2m: [1, 2, 3], amount: 500},
m2o: 2, m2m: [1, 2, 3], amount: 500, reference: 'res_currency,1'},
{id: 3, bar: true, foo: "gnap", int_field: 17, qux: -3,
m2o: 1, m2m: [], amount: 300},
m2o: 1, m2m: [], amount: 300, reference: 'res_currency,2'},
{id: 4, bar: false, foo: "blip", int_field: -4, qux: 9,
m2o: 1, m2m: [1], amount: 0},
]
@@ -2400,6 +2404,68 @@ QUnit.module('Views', {
list.destroy();
});
QUnit.test('reference field rendering', function (assert) {
assert.expect(4);
this.data.foo.records.push({
id: 5,
reference: 'res_currency,2',
});
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="reference"/></tree>',
mockRPC: function (route, args) {
if (args.method === 'name_get') {
assert.step(args.model);
}
return this._super.apply(this, arguments);
},
});
assert.verifySteps(['bar', 'res_currency'], "should have done 1 name_get by model in reference values");
assert.strictEqual(list.$('tbody td').text(), "Value 1USDEUREUR",
"should have the display_name of the reference");
list.destroy();
});
QUnit.test('editable list view: contexts are correctly sent', function (assert) {
assert.expect(6);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="top">' +
'<field name="foo"/>' +
'</tree>',
mockRPC: function (route, args) {
var context;
if (route === '/web/dataset/search_read') {
context = args.context;
} else {
context = args.kwargs.context;
}
assert.strictEqual(context.active_field, 2, "context should be correct");
assert.strictEqual(context.someKey, 'some value', "context should be correct");
return this._super.apply(this, arguments);
},
session: {
user_context: {someKey: 'some value'},
},
viewOptions: {
context: {active_field: 2},
},
});
list.$('.o_data_cell:first').click();
list.$('.o_field_widget[name=foo]').val('abc').trigger('input');
list.$buttons.find('.o_list_button_save').click();
list.destroy();
});
});
});
+1 -1
View File
@@ -15,7 +15,7 @@ class IrHttp(models.AbstractModel):
context['editable'] = True
if 'edit_translations' in request.httprequest.args and 'edit_translations' not in context:
context['edit_translations'] = True
if context.get('lang') != "en_US" and 'translatable' not in context:
if context.get('edit_translations') and 'translatable' not in context:
context['translatable'] = True
request.context = context
return super(IrHttp, cls)._dispatch()
+1 -1
View File
@@ -49,7 +49,7 @@ class IrUiView(models.Model):
if value is not None:
# TODO: batch writes?
if self.get_default_lang_code() and field != 'arch' and not isinstance(Model._fields[field], fields.Html):
if not self.env.context.get('lang') and self.get_default_lang_code():
Model.browse(int(el.get('data-oe-id'))).with_context(lang=self.get_default_lang_code()).write({field: value})
else:
Model.browse(int(el.get('data-oe-id'))).write({field: value})
+46 -61
View File
@@ -10,7 +10,10 @@ var field_registry = require('web.field_registry');
var transcoder = require('web_editor.transcoder');
var TranslatableFieldMixin = basic_fields.TranslatableFieldMixin;
var QWeb = core.qweb;
var _t = core._t;
/**
@@ -18,11 +21,8 @@ var QWeb = core.qweb;
* Intended to display HTML content. This widget uses the summernote editor
* improved by odoo.
*
* FIXME: this field has a custom handling of the translation feature, which
* hasn't been re-introduced yet (because this feature hasn't been introduced
* yet in the fields in general)
*/
var FieldTextHtmlSimple = basic_fields.DebouncedField.extend({
var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMixin, {
className: 'oe_form_field oe_form_field_html_text',
supportedFieldTypes: ['html'],
@@ -133,6 +133,7 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend({
if (history) {
history.reset();
}
this.$('.note-toolbar').append(this._renderTranslateButton());
},
/**
* @override
@@ -187,6 +188,19 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend({
}
return value;
},
/**
* @override
* @private
* @returns {jQueryElement}
*/
_renderTranslateButton: function () {
if (_t.database.multi_lang && this.field.translate && this.res_id) {
return $(QWeb.render('web_editor.FieldTextHtml.button.translate', {widget: this}))
.on('click', this._onTranslate.bind(this));
}
return $();
},
});
var FieldTextHtml = AbstractField.extend({
@@ -196,6 +210,7 @@ var FieldTextHtml = AbstractField.extend({
start: function () {
var self = this;
this.loaded = false;
this.callback = _.uniqueId('FieldTextHtml_');
window.odoo[this.callback+"_editor"] = function (EditorBar) {
setTimeout(function () {
@@ -206,8 +221,7 @@ var FieldTextHtml = AbstractField.extend({
self.on_content_loaded();
};
window.odoo[this.callback+"_updown"] = null;
window.odoo[this.callback+"_downup"] = function (value) {
self._setValue(value);
window.odoo[this.callback+"_downup"] = function () {
self.resize();
};
@@ -233,8 +247,6 @@ var FieldTextHtml = AbstractField.extend({
this.old_initialize_content();
var def = this._super.apply(this, arguments);
// this.$translate.remove();
// this.$translate = $();
return def;
},
getDatarecord: function () {
@@ -261,18 +273,13 @@ var FieldTextHtml = AbstractField.extend({
if (this.nodeOptions.template) {
attr.template = this.nodeOptions.template;
}
if (this. mode === "edit") {
if (this.mode === "edit") {
attr.enable_editor = 1;
}
if (this.field.translate) {
attr.translatable = 1;
}
if (session.debug) {
attr.debug = session.debug;
}
attr.lang = attr.enable_editor ? 'en_US' : this.getSession().user_context.lang;
for (k in _attr) {
attr[k] = _attr[k];
}
@@ -306,26 +313,10 @@ var FieldTextHtml = AbstractField.extend({
this.document = this.$iframe.contents()[0];
this.$body = $("body", this.document);
this.$content = this.$body.find("#editable_area");
// this._toggle_label();
this.lang = this.$iframe.attr('src').match(/[?&]lang=([^&]+)/);
this.lang = this.lang ? this.lang[1] : session.user_context.lang;
this._dirty_flag = false;
this.render();
setTimeout(function () {
self.trigger_up('perform_model_rpc', {
model: 'res.lang',
method: 'search_read',
args: [
[['code', '!=', 'en_US']],
["name", "code"]
],
on_success: function (res) {
self.languages = res;
self.add_button();
setTimeout(self.resize,0);
}
});
}, 0);
this.add_button();
this.loaded = true;
setTimeout(self.resize, 0);
},
on_editor_loaded: function (EditorBar) {
var self = this;
@@ -341,19 +332,6 @@ var FieldTextHtml = AbstractField.extend({
var self = this;
var $to = this.$body.find("#web_editor-top-edit, #wrapwrap").first();
$(QWeb.render('FieldTextHtml.translate', {'widget': this}))
.appendTo($to)
.on('change', 'select', function () {
var lang = $(this).val();
var edit = self. mode === "edit";
var trans = lang !== 'en_US';
self.$iframe.attr("src", self.get_url({
'edit_translations': edit && trans,
'enable_editor': edit && !trans,
'lang': lang
}));
});
$(QWeb.render('web_editor.FieldTextHtml.fullscreen'))
.appendTo($to)
.on('click', '.o_fullscreen', function () {
@@ -369,9 +347,6 @@ var FieldTextHtml = AbstractField.extend({
});
},
render: function () {
if (this.lang !== session.user_context.lang || this.$iframe.attr('src').match(/[?&]edit_translations=1/)) {
return;
}
var value = (this.value || "").replace(/^<p[^>]*>(\s*|<br\/?>)<\/p>$/, '');
if (!this.$content) {
return;
@@ -392,24 +367,34 @@ var FieldTextHtml = AbstractField.extend({
has_no_value: function () {
return this.value === false || !this.$content.html() || !this.$content.html().match(/\S/);
},
// FIXME: not sure what this is supposed to do that is still relevant now
// commit_value: function () {
// if (this.lang !== 'en_US' && this.$body.find('.o_dirty').length) {
// this.internal_set_value( this.view.datarecord[this.name] );
// this._dirty_flag = false;
// return this.editor.save();
// } else if (this._dirty_flag && this.editor && this.editor.buildingBlock) {
// this.editor.buildingBlock.clean_for_save();
// this.internal_set_value( this.$content.html() );
// }
// },
destroy: function () {
$(window).off('resize', this.resize);
delete window.odoo[this.callback+"_editor"];
delete window.odoo[this.callback+"_content"];
delete window.odoo[this.callback+"_updown"];
delete window.odoo[this.callback+"_downup"];
}
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
commitChanges: function () {
if (!this.loaded) {
return;
}
// switch to WYSIWYG mode if currently in code mode to get all changes
if (config.debug && this.mode === 'edit' && this.editor.rte) {
var layoutInfo = this.editor.rte.editable().data('layoutInfo');
$.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo, false);
}
this.editor.buildingBlock.clean_for_save();
this._setValue(this.$content.html());
return this._super.apply(this, arguments);
},
});
field_registry
+1 -1
View File
@@ -36,7 +36,7 @@ data.ready = function () {
// TODO find a better way to find if we are in the backend or not
// We don't load translation if we are in the backend since it's already done by the webclient
if (!$(document).find('.o_web_client').length) {
load_translations();
return load_translations();
}
});
} else if(all_ready.state() === "resolved") { // can add async template
@@ -1090,8 +1090,15 @@ body.editor_enable {
background-color: @odoo-brand-secondary;
#wrapwrap {
position: relative;
display: table;
table-layout: fixed;
width: 100%;
height: 100%;
> * {
display: table-row;
}
> main {
height: 100%;
+1 -28
View File
@@ -7,41 +7,14 @@
</div>
</t>
<t t-name="web_editor.FieldTextHtml.button.reset">
<div class="btn-group pull-right">
<button class="o_reset_translation btn btn-default btn-sm btn-small" title="Reset from source"><span class="fa fa-upload"></span></button>
</div>
</t>
<t t-name="web_editor.FieldTextHtml.button.translate">
<div class="btn-group pull-right">
<button t-if="widget.field.translate" class="oe_field_translate btn btn-default btn-sm btn-small" style="height: 24px; padding: 1px 3px 0px 5px">
<button t-if="widget.field.translate" class="o_field_translate btn btn-default btn-sm btn-small" style="height: 24px; padding: 1px 17px 0px 5px">
<span class="fa fa-language fa-lg oe_input_icon"/>
</button>
</div>
</t>
<t t-name="web_editor.FieldTextHtml.translate">
<div t-if="widget.recordData.id &amp;&amp; widget.languages.length" style="margin: 5px; position: fixed; top: 0; left: 0; z-index: 2000;">
<select style="
width: 148px;
height: 22px;
padding: 2px 4px;
border: 1px solid #ccc;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
background: white;">
<option value="en_US" t-att-selected="'en_US' == widget.lang ? 'selected' : null">
<t t-if="!widget.get('effective_readonly')">English (edit mode)</t>
<t t-if="widget.get('effective_readonly')">English</t>
</option>
<t t-foreach="widget.languages" t-as="lang">
<option t-att-value="lang.code" t-att-selected="lang.code == widget.lang ? 'selected' : null"><t t-esc="lang.name"/></option>
</t>
</select>
</div>
</t>
<t t-name="web_editor.FieldTextHtml.fullscreen">
<span style="margin: 5px; position: fixed; top: 0; right: 0; z-index: 2000;">
<button class="o_fullscreen btn btn-primary" style="width: 24px; height: 24px; background-color: #337ab7; border: 1px solid #2e6da4; border-radius: 4px; padding: 0; position: relative;">
@@ -3,6 +3,9 @@ odoo.define('web_editor.web_editor_tests', function (require) {
var FormView = require('web.FormView');
var testUtils = require('web.test_utils');
var core = require('web.core');
var _t = core._t;
QUnit.module('web_editor', {
beforeEach: function() {
@@ -87,6 +90,43 @@ QUnit.test('field html widget (with options inline-style)', function (assert) {
}, 0);
});
QUnit.test('field html translatable', function (assert) {
assert.expect(3);
var multiLang = _t.database.multi_lang;
_t.database.multi_lang = true;
this.data['mass.mailing'].fields.body.translate = true;
var form = testUtils.createView({
View: FormView,
model: 'mass.mailing',
data: this.data,
arch: '<form string="Partners">' +
'<field name="body" widget="html"/>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (route === '/web/dataset/call_button' && args.method === 'translate_fields') {
assert.deepEqual(args.args, ['mass.mailing',1,'body',{}], "should call 'call_button' route");
return $.when();
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(form.$('.oe_form_field_html_text .o_field_translate').length, 0,
"should not have a translate button in readonly mode");
form.$buttons.find('.o_form_button_edit').click();
var $button = form.$('.oe_form_field_html_text .o_field_translate');
assert.strictEqual($button.length, 1, "should have a translate button");
$button.click();
form.destroy();
_t.database.multi_lang = multiLang;
});
QUnit.test('field html_frame widget', function (assert) {
assert.expect(6);
+1 -1
View File
@@ -208,7 +208,7 @@ class Http(models.AbstractModel):
path.pop(1)
request.context = context
return cls.reroute('/'.join(path) or '/')
if path[1] == request.website.default_lang_code:
if request.lang == request.website.default_lang_code:
context['edit_translations'] = False
if not context.get('tz'):
context['tz'] = request.session.get('geoip', {}).get('time_zone')
+5
View File
@@ -7,6 +7,7 @@ odoo.define('website.website', function (require) {
var Widget = require('web.Widget');
var session = require('web.session');
var base = require('web_editor.base');
var utils = require('web.utils');
var qweb = core.qweb;
var _t = core._t;
@@ -244,6 +245,10 @@ odoo.define('website.website', function (require) {
}
});
/* Load localizations */
var lang = utils.get_cookie('website_lang') || $('html').attr('lang') || 'en_US';
ajax.loadJS('/web/webclient/locale/' + lang.replace('-', '_'));
/**
* Object who contains all method and bind for the top bar, the template is create server side.
*/
+1
View File
@@ -174,6 +174,7 @@ class WebsiteBlog(http.Controller):
v['blog'] = blog
v['base_url'] = request.env['ir.config_parameter'].sudo().get_param('web.base.url')
v['posts'] = request.env['blog.post'].search([('blog_id','=', blog.id)], limit=min(int(limit), 50))
v['html2plaintext'] = html2plaintext
r = request.render("website_blog.blog_feed", v, headers=[('Content-Type', 'application/atom+xml')])
return r
@@ -555,7 +555,7 @@
<link t-att-href="'%s%s' % (base_url, post.website_url)"/>
<id t-esc="'%s%s' % (base_url, post.website_url)"/>
<author><name t-esc="post.sudo().author_id.name"/></author>
<summary t-esc="post.subtitle"/>
<summary t-esc="html2plaintext(post.teaser)"/>
<updated t-esc="post.write_date.replace(' ', 'T') + 'Z'"/>
</entry>
</feed>
@@ -7,7 +7,7 @@
<field name="arch" type="xml">
<data>
<xpath expr="//field[@name='company_id']" position="after">
<field name="website_tag_ids" widget="many2many_tags" string="Website Tags"/>
<field name="website_tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" string="Website Tags"/>
</xpath>
</data>
</field>
+27 -23
View File
@@ -57,7 +57,7 @@
<h4 class="media-heading">
<a itemprop="url" t-att-class="event.state == 'done' and 'text-success'" t-attf-href="/event/#{ slug(event) }/#{(not event.menu_id) and 'register' or ''}"><span itemprop="name" t-field="event.name"> </span></a>
<small t-if="event.is_participating" class="label label-info">Participating</small>
<small t-if="not event.address_id" class="label label-info">Online</small>
<small t-if="not event.is_online" class="label label-info">Online</small>
<small t-if="not event.website_published" class="label label-danger">unpublished</small>
</h4>
<div>
@@ -68,10 +68,12 @@
<div>
<i class="fa fa-clock-o"></i> <span itemprop="startDate" t-field="event.with_context(tz=event.date_tz).date_begin" t-options='{"hide_seconds":"True"}'> </span> <i>to</i> <span itemprop="endDate" t-field="event.with_context(tz=event.date_tz).date_end" t-options='{"hide_seconds":"True"}'> </span>
</div>
<div itemprop="location" t-field="event.address_id" t-options='{
"widget": "contact",
"fields": ["city"]
}'/>
<t t-if="not event.is_online">
<div itemprop="location" t-field="event.address_id" t-options='{
"widget": "contact",
"fields": ["city"]
}'/>
</t>
<div class="text-muted" t-if="event.event_type_id">
<i class="fa fa-tag"></i> <span t-field="event.event_type_id"/>
</div>
@@ -333,25 +335,27 @@
<div class="col-md-4 css_noprint">
<div class="clearfix"/>
<div class="panel panel-default" t-if="event.address_id">
<div class="panel-heading">
<h4>Where</h4>
<t t-if="not event.is_online">
<div class="panel panel-default" t-if="event.address_id">
<div class="panel-heading">
<h4>Where</h4>
</div>
<div class="panel-body">
<t t-if="event.google_map_img()">
<a t-att-href="event.google_map_link()" target="_BLANK">
<img t-att-src="event.google_map_img()" width="100%%"/>
</a>
</t>
<t t-else="1">
<a t-att-href="event.google_map_link()" target="_BLANK">Visualize on Google Maps</a>
</t>
<div itemprop="location" class="mt16 mb8" t-field="event.address_id" t-options='{
"widget": "contact",
"fields": ["name", "address", "phone", "mobile", "fax", "email"]
}'/>
</div>
</div>
<div class="panel-body">
<t t-if="event.google_map_img()">
<a t-att-href="event.google_map_link()" target="_BLANK">
<img t-att-src="event.google_map_img()" width="100%%"/>
</a>
</t>
<t t-else="1">
<a t-att-href="event.google_map_link()" target="_BLANK">Visualize on Google Maps</a>
</t>
<div itemprop="location" class="mt16 mb8" t-field="event.address_id" t-options='{
"widget": "contact",
"fields": ["name", "address", "phone", "mobile", "fax", "email"]
}'/>
</div>
</div>
</t>
<div class="panel panel-default">
<div class="panel-heading">

Some files were not shown because too many files have changed in this diff Show More