[MERGE] forward port branch saas-16 up to 0f2b2c4e2b
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
'description': """
|
||||
Print amount in words on checks issued for expenses
|
||||
""",
|
||||
'category': 'Accounting & 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">
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
@@ -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,
|
||||
|
||||
@@ -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', '>', 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', '>', 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', '>', 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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(' ');
|
||||
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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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("$ 125.00", {}, {currency_id: 3}), 125);
|
||||
assert.strictEqual(fieldUtils.parse.monetary("1,000.00 €", {}, {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("$ 12.00", {}, {currency_id: 1})}, /is not a correct/);
|
||||
assert.throws(function() {fieldUtils.parse.monetary("$ 12.00 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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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>' +
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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%;
|
||||
|
||||
|
||||
@@ -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 && 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);
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user