[MERGE] forward port branch saas-11.2 up to 45329c0cf2

This commit is contained in:
Christophe Simonis
2018-07-05 18:53:17 +02:00
26 changed files with 357 additions and 176 deletions
+2 -2
View File
@@ -70,7 +70,7 @@ class AccountInvoice(models.Model):
@api.onchange('amount_total')
def _onchange_amount_total(self):
for inv in self:
if inv.amount_total < 0:
if float_compare(inv.amount_total, 0.0, precision_rounding=inv.currency_id.rounding) == -1:
raise Warning(_('You cannot validate an invoice with a negative total amount. You should create a credit note instead.'))
@api.model
@@ -784,7 +784,7 @@ class AccountInvoice(models.Model):
to_open_invoices = self.filtered(lambda inv: inv.state != 'open')
if to_open_invoices.filtered(lambda inv: inv.state != 'draft'):
raise UserError(_("Invoice must be in draft state in order to validate it."))
if to_open_invoices.filtered(lambda inv: inv.amount_total < 0):
if to_open_invoices.filtered(lambda inv: float_compare(inv.amount_total, 0.0, precision_rounding=inv.currency_id.rounding) == -1):
raise UserError(_("You cannot validate an invoice with a negative total amount. You should create a credit note instead."))
to_open_invoices.action_date_assign()
to_open_invoices.action_move_create()
@@ -868,7 +868,7 @@ var StatementModel = BasicModel.extend({
}) : false,
account_code: self.accounts[line.st_line.open_balance_account_id],
};
line.balance.type = line.balance.amount_currency ? (line.balance.amount_currency > 0 && line.st_line.partner_id ? 0 : -1) : 1;
line.balance.type = line.balance.amount_currency ? (line.st_line.partner_id ? 0 : -1) : 1;
});
},
/**
@@ -133,7 +133,7 @@
<td class="cell_action"><span class="toggle_create fa fa-play"></span></td>
<td class="cell_account_code"><t t-esc="state.balance.account_code"/></td>
<td class="cell_due_date"></td>
<td class="cell_label"><t t-if="state.balance.amount_currency &lt; 0">Create Write-off</t><t t-elif="state.st_line.partner_id">Open balance</t><t t-else="">Choose counterpart</t></td>
<td class="cell_label"><t t-if="state.st_line.partner_id">Open balance</t><t t-else="">Choose counterpart or Create Write-off</t></td>
<td class="cell_left"><t t-if="state.balance.amount_currency &lt; 0"><span t-if="state.balance.amount_currency_str" t-attf-class="o_multi_currency o_multi_currency_color_#{state.balance.currency_id%8} line_info_button fa fa-money" t-att-data-content="state.balance.amount_currency_str"/><t t-raw="state.balance.amount_str"/></t></td>
<td class="cell_right"><t t-if="state.balance.amount_currency &gt; 0"><span t-if="state.balance.amount_currency_str" t-attf-class="o_multi_currency o_multi_currency_color_#{state.balance.currency_id%8} line_info_button fa fa-money" t-att-data-content="state.balance.amount_currency_str"/><t t-raw="state.balance.amount_str"/></t></td>
<td class="cell_info_popover"></td>
@@ -1047,8 +1047,8 @@ QUnit.module('account', {
widget.$('.create .create_label input').val('test1').trigger('input');
assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text(), "$ 200.00", "should display the value 200.00 in left column");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Create Write-off", "should display 'Create Write-off'");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$ 25.00", "should display 'Create Write-off' with 25.00 in left column");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open balance'");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$ 25.00", "should display 'Open balance' with 25.00 in left column");
assert.strictEqual(widget.$('.accounting_view tbody tr').length, 3, "should have 3 created reconcile lines");
clientAction.destroy();
@@ -1154,8 +1154,8 @@ QUnit.module('account', {
$('.ui-autocomplete .ui-menu-item a:contains(20.00%)').trigger('mouseenter').trigger('click');
assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "$\u00a01100.00$\u00a0220.00", "should have 2 created reconcile lines with right column values");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Create Write-off", "should display 'Create Write-off'");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$\u00a0145.00", "should display 'Create Write-off' with 145.00 in right column");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open balance'");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$\u00a0145.00", "should display 'Open balance' with 145.00 in right column");
assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines");
clientAction.destroy();
@@ -1179,7 +1179,7 @@ QUnit.module('account', {
assert.strictEqual(widget.$('.accounting_view tbody .cell_label, .accounting_view tbody .cell_right').text().replace(/[\n\r\s$,]+/g, ' '),
" ATOS Banque 1145.62 Tax 20.00% 229.12 ATOS Frais 26.71 Tax 10.00% include 2.67", "should display 4 lines");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label, .accounting_view tfoot .cell_left').text().replace(/[\n\r\s$,]+/g, ' '),
"Create Write-off229.12", "should display the 'Create Write-off' line with value in left column");
"Open balance229.12", "should display the 'Open balance' line with value in left column");
widget.$('.create .create_amount input').val('100').trigger('input');
@@ -1187,7 +1187,7 @@ QUnit.module('account', {
" 101120 ATOS Banque 1075.00 101120 Tax 20.00% 215.00 101130 ATOS Frais 90.91 101300 Tax 10.00% include 9.09 ",
"should update the value of the 4 lines (because the line must have 100% of the value)");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label, .accounting_view tfoot .cell_left').text().replace(/[\n\r\s$,]+/g, ' '),
"Create Write-off215.00", "should change the 'Create Write-off' line because the 20.00% tax is not an include tax");
"Open balance215.00", "should change the 'Open balance' line because the 20.00% tax is not an include tax");
widget.$('.accounting_view tbody .cell_account_code:first').trigger('click');
widget.$('.accounting_view tbody .cell_label:first').trigger('click');
@@ -11,10 +11,10 @@ Barcodes encoding
The Barcodes module defines barcode nomenclatures whose rules identify specific type
of items e.g. products, locations. It contains the following features:
- Barcode patterns to identify barcodes containing a numerical value (e.g. weight, price)
- Patterns to identify barcodes containing a numerical value (e.g. weight, price)
- Definitin of barcode aliases that allow to identify the same product with different barcodes
- Unlimited barcode patterns and definitions,
- Barcode EAN13 encoding supported.
- Support for encodings EAN-13, EAN-8 and UPC-A
Barcode encodings
-----------------
@@ -65,7 +65,7 @@ We now want to write a barcode for 2.75kg of oranges. This barcode should be '21
numerical content of this barcode is '02750', and the correct checksum is '6'). When scanned, this
barcode matches the Weighted Product rule (since is starts with '21'). The numerical content is extracted,
and replaced by a sequence of '0's. The correct checksum is then computed for the obtained barcode
('2100001000004') and the corresponding product (oranges) qgit is retrieved from product table.
('2100001000004') and the corresponding product (oranges) is retrieved from product table.
Note: the special characters '{' and '}' in patterns are used to identify numerical content. To
explicitely specify '{' or '}' in a pattern, they must be escaped.
@@ -86,6 +86,17 @@ matching the original, 12-digit long, barcode.
Barcodes scanning
==============================
Scanning
--------
Use a USB scanner (that mimics keyboard inputs) in order to work with barcodes in Odoo.
The scanner must be configured to use no prefix and a carriage return or tab as suffix.
The delay between each character input must be less than or equal to 50 milliseconds.
Most barcode scanners will work out of the box.
However, make sure the scanner uses the same keyboard layout as the device it's plugged in.
Either by setting the device's keyboard layout to US QWERTY (default value for most readers)
or by changing the scanner's keyboard layout (check the manual).
Barcode events
------------------------------
-21
View File
@@ -3,27 +3,6 @@
'version': '2.0',
'category': 'Extra Tools',
'summary': 'Barcodes Scanning and Parsing',
'description': """
This module adds support for barcode scanning and parsing.
Scanning
--------
Use a USB scanner (that mimics keyboard inputs) in order to work with barcodes in Odoo.
The scanner must be configured to use no prefix and a carriage return or tab as suffix.
The delay between each character input must be less than or equal to 50 milliseconds.
Most barcode scanners will work out of the box.
However, make sure the scanner uses the same keyboard layout as the device it's plugged in.
Either by setting the device's keyboard layout to US QWERTY (default value for most readers)
or by changing the scanner's keyboard layout (check the manual).
Parsing
-------
The barcodes are interpreted using the rules defined by a nomenclature.
It provides the following features:
- Patterns to identify barcodes containing a numerical value (e.g. weight, price)
- Definition of barcode aliases that allow to identify the same product with different barcodes
- Support for encodings EAN-13, EAN-8 and UPC-A
""",
'depends': ['web'],
'data': [
'data/barcodes_data.xml',
+3 -2
View File
@@ -127,9 +127,10 @@ class MailActivity(models.Model):
for record in self.filtered(lambda activity: activity.date_deadline):
today = today_default
if record.user_id.tz:
tz = record.user_id.sudo().tz
if tz:
today_utc = pytz.UTC.localize(datetime.utcnow())
today_tz = today_utc.astimezone(pytz.timezone(record.user_id.tz))
today_tz = today_utc.astimezone(pytz.timezone(tz))
today = date(year=today_tz.year, month=today_tz.month, day=today_tz.day)
date_deadline = fields.Date.from_string(record.date_deadline)
+15 -55
View File
@@ -8,7 +8,7 @@ import random
import threading
from ast import literal_eval
from odoo import api, fields, models, tools, _
from odoo import api, fields, models, tools, _, SUPERUSER_ID
from odoo.exceptions import UserError
from odoo.tools.safe_eval import safe_eval
@@ -218,7 +218,8 @@ class MassMailingCampaign(models.Model):
_inherits = {'utm.campaign': 'campaign_id'}
stage_id = fields.Many2one('mail.mass_mailing.stage', string='Stage', ondelete='restrict', required=True,
default=lambda self: self.env['mail.mass_mailing.stage'].search([], limit=1))
default=lambda self: self.env['mail.mass_mailing.stage'].search([], limit=1),
group_expand='_group_expand_stage_ids')
user_id = fields.Many2one(
'res.users', string='Responsible',
required=True, default=lambda self: self.env.uid)
@@ -322,31 +323,12 @@ class MassMailingCampaign(models.Model):
return res
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
""" Override read_group to always display all states. """
if groupby and groupby[0] == "stage_id":
# Default result structure
states_read = self.env['mail.mass_mailing.stage'].search_read([], ['name'])
states = [(state['id'], state['name']) for state in states_read]
read_group_all_states = [{
'__context': {'group_by': groupby[1:]},
'__domain': domain + [('stage_id', '=', state_value)],
'stage_id': state_value,
'state_count': 0,
} for state_value, state_name in states]
# Get standard results
read_group_res = super(MassMailingCampaign, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby)
# Update standard results with default results
result = []
for state_value, state_name in states:
res = [x for x in read_group_res if x['stage_id'] == (state_value, state_name)]
if not res:
res = [x for x in read_group_all_states if x['stage_id'] == state_value]
res[0]['stage_id'] = [state_value, state_name]
result.append(res[0])
return result
else:
return super(MassMailingCampaign, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby)
def _group_expand_stage_ids(self, stages, domain, order):
""" Read group customization in order to display all the stages in the
kanban view, even if they are empty
"""
stage_ids = stages._search([], order=order, access_rights_uid=SUPERUSER_ID)
return stages.browse(stage_ids)
class MassMailing(models.Model):
@@ -399,7 +381,7 @@ class MassMailing(models.Model):
help="This is the delivery method, e.g. Postcard, Email, or Banner Ad", default=lambda self: self.env.ref('utm.utm_medium_email'))
clicks_ratio = fields.Integer(compute="_compute_clicks_ratio", string="Number of Clicks")
state = fields.Selection([('draft', 'Draft'), ('in_queue', 'In Queue'), ('sending', 'Sending'), ('done', 'Sent')],
string='Status', required=True, copy=False, default='draft')
string='Status', required=True, copy=False, default='draft', group_expand='_group_expand_states')
color = fields.Integer(string='Color Index')
user_id = fields.Many2one('res.users', string='Mailing Manager', default=lambda self: self.env.user)
# mailing options
@@ -424,6 +406,7 @@ class MassMailing(models.Model):
statistics_ids = fields.One2many('mail.mail.statistics', 'mass_mailing_id', string='Emails Statistics')
total = fields.Integer(compute="_compute_total")
scheduled = fields.Integer(compute="_compute_statistics")
expected = fields.Integer(compute="_compute_statistics")
sent = fields.Integer(compute="_compute_statistics")
delivered = fields.Integer(compute="_compute_statistics")
opened = fields.Integer(compute="_compute_statistics")
@@ -465,7 +448,7 @@ class MassMailing(models.Model):
self.env.cr.execute("""
SELECT
m.id as mailing_id,
COUNT(s.id) AS total,
COUNT(s.id) AS expected,
COUNT(CASE WHEN s.sent is not null THEN 1 ELSE null END) AS sent,
COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null THEN 1 ELSE null END) AS scheduled,
COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is not null THEN 1 ELSE null END) AS failed,
@@ -486,7 +469,7 @@ class MassMailing(models.Model):
m.id
""", (tuple(self.ids), ))
for row in self.env.cr.dictfetchall():
total = row.pop('total') or 1
total = row['expected'] or 1
row['received_ratio'] = 100.0 * row['delivered'] / total
row['opened_ratio'] = 100.0 * row['opened'] / total
row['clicks_ratio'] = 100.0 * row['clicked'] / total
@@ -568,31 +551,8 @@ class MassMailing(models.Model):
name=_('%s (copy)') % self.name)
return super(MassMailing, self).copy(default=default)
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
""" Override read_group to always display all states. """
if groupby and groupby[0] == "state":
# Default result structure
states = [('draft', _('Draft')), ('in_queue', _('In Queue')), ('sending', _('Sending')), ('done', _('Sent'))]
read_group_all_states = [{
'__context': {'group_by': groupby[1:]},
'__domain': domain + [('state', '=', state_value)],
'state': state_value,
'state_count': 0,
} for state_value, state_name in states]
# Get standard results
read_group_res = super(MassMailing, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby)
# Update standard results with default results
result = []
for state_value, state_name in states:
res = [x for x in read_group_res if x['state'] == state_value]
if not res:
res = [x for x in read_group_all_states if x['state'] == state_value]
res[0]['state'] = state_value
result.append(res[0])
return result
else:
return super(MassMailing, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby)
def _group_expand_states(self, states, domain, order):
return [key for key, val in type(self).state.selection]
def update_opt_out(self, email, res_ids, value):
model = self.env[self.mailing_model_real].with_context(active_test=False)
@@ -27,11 +27,11 @@ class MailMailStats(models.Model):
model = fields.Char(string='Document model')
res_id = fields.Integer(string='Document ID')
# campaign / wave data
mass_mailing_id = fields.Many2one('mail.mass_mailing', string='Mass Mailing')
mass_mailing_id = fields.Many2one('mail.mass_mailing', string='Mass Mailing', index=True)
mass_mailing_campaign_id = fields.Many2one(
related='mass_mailing_id.mass_mailing_campaign_id',
string='Mass Mailing Campaign',
store=True, readonly=True)
store=True, readonly=True, index=True)
# Bounce and tracking
scheduled = fields.Datetime(help='Date when the email has been created', default=fields.Datetime.now)
sent = fields.Datetime(help='Date when the email has been sent')
@@ -558,7 +558,7 @@
<field name="arch" type="xml">
<kanban>
<field name='color'/>
<field name='total'/>
<field name='expected'/>
<field name='failed'/>
<field name='sent_date'/>
<templates>
@@ -617,7 +617,7 @@
</div>
</div>
<div class='o_kanban_primary_bottom'>
<field name="delivered" widget="progressbar" title="Delivered" options="{'current_value': 'delivered', 'max_value': 'total', 'editable': false}"/>
<field name="delivered" widget="progressbar" title="Delivered" options="{'current_value': 'delivered', 'max_value': 'expected', 'editable': false}"/>
</div>
</t>
</div>
+1 -1
View File
@@ -22,7 +22,7 @@
<label for="product_uom_qty"/>
<div class="o_row">
<span><field name="product_uom_qty" readonly="1" nolabel="1"/></span>
<span><field name="product_uom" readonly="1" nolabel="1"/></span>
<span><field name="product_uom" readonly="1" force_save="1" nolabel="1"/></span>
</div>
<label for="quantity_done"/>
<div class="o_row">
+10 -6
View File
@@ -3,6 +3,7 @@
from odoo import api, fields, models, _
from datetime import datetime
import pytz
class CrmTeam(models.Model):
@@ -41,13 +42,16 @@ class CrmTeam(models.Model):
""" If the type of the sales team is point of sale ('pos'), the graph will display the sales data.
The override here is to get data from pos.order instead of sale.order.
"""
offset = datetime.now(pytz.timezone(self.env.user.tz or 'UTC')).utcoffset()
min_date = fields.Datetime.to_string(datetime.combine(start_date, datetime.min.time()) - offset)
max_date = fields.Datetime.to_string(datetime.combine(end_date, datetime.max.time()) - offset)
if self.team_type == 'pos':
result = []
if self.dashboard_graph_group_pos == 'pos':
order_data = self.env['report.pos.order'].read_group(
domain=[
('date', '>=', fields.Date.to_string(start_date)),
('date', '<=', fields.Datetime.to_string(datetime.combine(end_date, datetime.max.time()))),
('date', '>=', min_date),
('date', '<=', max_date),
('config_id', 'in', self.pos_config_ids.ids),
('state', 'in', ['paid', 'done', 'invoiced'])],
fields=['config_id', 'price_total'],
@@ -63,8 +67,8 @@ class CrmTeam(models.Model):
elif self.dashboard_graph_group_pos == 'user':
order_data = self.env['report.pos.order'].read_group(
domain=[
('date', '>=', fields.Date.to_string(start_date)),
('date', '<=', fields.Datetime.to_string(datetime.combine(end_date, datetime.max.time()))),
('date', '>=', min_date),
('date', '<=', max_date),
('config_id', 'in', self.pos_config_ids.ids),
('state', 'in', ['paid', 'done', 'invoiced'])],
fields=['user_id', 'price_total'],
@@ -78,8 +82,8 @@ class CrmTeam(models.Model):
# /!\ do not use en_US as it's not ISO-standard and does not match datetime's library
order_data = self.env['report.pos.order'].with_context(lang='en_GB').read_group(
domain=[
('date', '>=', fields.Date.to_string(start_date)),
('date', '<=', fields.Datetime.to_string(datetime.combine(end_date, datetime.max.time()))),
('date', '>=', min_date),
('date', '<=', max_date),
('config_id', 'in', self.pos_config_ids.ids),
('state', 'in', ['paid', 'done', 'invoiced'])],
fields=['date', 'price_total'],
+1 -1
View File
@@ -22,7 +22,6 @@
'data': [
'security/project_security.xml',
'security/ir.model.access.csv',
'data/project_data.xml',
'report/project_report_views.xml',
'views/rating_views.xml',
'views/project_views.xml',
@@ -33,6 +32,7 @@
'views/project_portal_templates.xml',
'views/project_rating_templates.xml',
'data/project_mail_template_data.xml',
'data/project_data.xml',
],
'qweb': ['static/src/xml/project.xml'],
'demo': ['data/project_demo.xml'],
+1 -1
View File
@@ -16,7 +16,7 @@ class StockMove(models.Model):
_inherit = 'stock.move'
purchase_line_id = fields.Many2one('purchase.order.line',
'Purchase Order Line', ondelete='set null', index=True, readonly=True, copy=False)
'Purchase Order Line', ondelete='set null', index=True, readonly=True)
created_purchase_line_id = fields.Many2one('purchase.order.line',
'Created Purchase Order Line', ondelete='set null', readonly=True, copy=False)
+1 -1
View File
@@ -209,7 +209,7 @@ class CrmTeam(models.Model):
self.ensure_one()
values = []
today = date.today()
today = fields.Date.from_string(fields.Date.context_today(self))
start_date, end_date = self._graph_get_dates(today)
graph_data = self._graph_data(start_date, end_date)
-1
View File
@@ -727,7 +727,6 @@ class SurveyUserInput(models.Model):
_sql_constraints = [
('unique_token', 'UNIQUE (token)', 'A token must be unique!'),
('deadline_in_the_past', 'CHECK (deadline >= date_create)', 'The deadline cannot be in the past')
]
@api.model
@@ -1591,6 +1591,7 @@ var FieldMany2ManyBinaryMultiFiles = AbstractField.extend({
this.$('form.o_form_binary_form').submit();
this.$('.oe_fileupload').hide();
ev.target.value = "";
},
/**
* @private
@@ -825,7 +825,8 @@ var BasicModel = AbstractModel.extend({
return this.mutex.exec(this._applyChange.bind(this, record_id, changes, options));
},
/**
* Reload all data for a given resource
* Reload all data for a given resource. At any time there is at most one
* reload operation active.
*
* @param {string} id local id for a resource
* @param {Object} [options]
@@ -834,64 +835,7 @@ var BasicModel = AbstractModel.extend({
* @returns {Deferred<string>} resolves to the id of the resource
*/
reload: function (id, options) {
options = options || {};
var element = this.localData[id];
if (element.type === 'record') {
if (!options.currentId && (('currentId' in options) || this.isNew(id))) {
var params = {
context: element.context,
fieldsInfo: element.fieldsInfo,
fields: element.fields,
viewType: element.viewType,
};
return this._makeDefaultRecord(element.model, params);
}
if (!options.keepChanges) {
this.discardChanges(id, {rollback: false});
}
} else if (element._changes) {
delete element.tempLimitIncrement;
_.each(element._changes, function (change) {
delete change.isNew;
});
}
if (options.context !== undefined) {
element.context = options.context;
}
if (options.domain !== undefined) {
element.domain = options.domain;
}
if (options.groupBy !== undefined) {
element.groupedBy = options.groupBy;
}
if (options.limit !== undefined) {
element.limit = options.limit;
}
if (options.offset !== undefined) {
this._setOffset(element.id, options.offset);
}
if (options.loadMoreOffset !== undefined) {
element.loadMoreOffset = options.loadMoreOffset;
} else {
// reset if not specified
element.loadMoreOffset = 0;
}
if (options.currentId !== undefined) {
element.res_id = options.currentId;
}
if (options.ids !== undefined) {
element.res_ids = options.ids;
element.count = element.res_ids.length;
}
if (element.type === 'record') {
element.offset = _.indexOf(element.res_ids, element.res_id);
}
var loadOptions = _.pick(options, 'fieldNames', 'viewType');
return this._load(element, loadOptions).then(function (result) {
return result.id;
});
return this.mutex.exec(this._reload.bind(this, id, options));
},
/**
* In some case, we may need to remove an element from a list, without going
@@ -2934,13 +2878,10 @@ var BasicModel = AbstractModel.extend({
continue;
}
changes = this._generateChanges(relRecord, options);
if (changes.id) {
if (!this.isNew(relRecord.id)) {
// the subrecord already exists in db
commands[fieldName].push(x2ManyCommands.link_to(relRecord.res_id));
delete changes.id;
if (this.isNew(record.id)) {
// if the main record is new, link the subrecord to it
commands[fieldName].push(x2ManyCommands.link_to(relRecord.res_id));
}
if (!_.isEmpty(changes)) {
commands[fieldName].push(x2ManyCommands.update(relRecord.res_id, changes));
}
@@ -4045,6 +3986,76 @@ var BasicModel = AbstractModel.extend({
});
});
},
/**
* Reload all data for a given resource
*
* @private
* @param {string} id local id for a resource
* @param {Object} [options]
* @param {boolean} [options.keepChanges=false] if true, doesn't discard the
* changes on the record before reloading it
* @returns {Deferred<string>} resolves to the id of the resource
*/
_reload: function (id, options) {
options = options || {};
var element = this.localData[id];
if (element.type === 'record') {
if (!options.currentId && (('currentId' in options) || this.isNew(id))) {
var params = {
context: element.context,
fieldsInfo: element.fieldsInfo,
fields: element.fields,
viewType: element.viewType,
};
return this._makeDefaultRecord(element.model, params);
}
if (!options.keepChanges) {
this.discardChanges(id, {rollback: false});
}
} else if (element._changes) {
delete element.tempLimitIncrement;
_.each(element._changes, function (change) {
delete change.isNew;
});
}
if (options.context !== undefined) {
element.context = options.context;
}
if (options.domain !== undefined) {
element.domain = options.domain;
}
if (options.groupBy !== undefined) {
element.groupedBy = options.groupBy;
}
if (options.limit !== undefined) {
element.limit = options.limit;
}
if (options.offset !== undefined) {
this._setOffset(element.id, options.offset);
}
if (options.loadMoreOffset !== undefined) {
element.loadMoreOffset = options.loadMoreOffset;
} else {
// reset if not specified
element.loadMoreOffset = 0;
}
if (options.currentId !== undefined) {
element.res_id = options.currentId;
}
if (options.ids !== undefined) {
element.res_ids = options.ids;
element.count = element.res_ids.length;
}
if (element.type === 'record') {
element.offset = _.indexOf(element.res_ids, element.res_id);
}
var loadOptions = _.pick(options, 'fieldNames', 'viewType');
return this._load(element, loadOptions).then(function (result) {
return result.id;
});
},
/**
* Allows to save a value in the specialData cache associated to a given
* record and fieldName. If the value in the cache was already the given
@@ -3207,6 +3207,136 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('onchange for embedded one2many in a one2many with a second page', function (assert) {
assert.expect(1);
this.data.turtle.fields.partner_ids.type = 'one2many';
this.data.turtle.records[0].partner_ids = [1];
// we need a second page, so we set two records and only display one per page
this.data.partner.records[0].turtles = [1, 2];
this.data.partner.onchanges = {
turtles: function (obj) {
obj.turtles = [
[5],
[1, 1, {
turtle_foo: "hop",
partner_ids: [[5], [4, 1]],
}],
[1, 2, {
turtle_foo: "blip",
partner_ids: [[5], [4, 2], [4, 4]],
}],
];
},
};
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<field name="turtles">' +
'<tree editable="bottom" limit="1">' +
'<field name="turtle_foo"/>' +
'<field name="partner_ids" widget="many2many_tags"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (args.method === 'write') {
var expectedResultTurtles = [
[1, 1, {
turtle_foo: "hop",
}],
[1, 2, {
partner_ids: [[4, 2, false], [4, 4, false]],
turtle_foo: "blip",
}],
];
assert.deepEqual(args.args[1].turtles, expectedResultTurtles,
"the right values should be written");
}
return this._super.apply(this, arguments);
}
});
form.$buttons.find('.o_form_button_edit').click();
form.$('.o_data_cell').eq(1).click();
var $cell = form.$('.o_selected_row .o_input[name=turtle_foo]');
$cell.val("hop").trigger('change');
form.$buttons.find('.o_form_button_save').click();
form.destroy();
});
QUnit.test('onchange for embedded one2many in a one2many updated by server', function (assert) {
// here we test that after an onchange, the embedded one2many field has
// been updated by a new list of ids by the server response, to this new
// list should be correctly sent back at save time
assert.expect(3);
this.data.turtle.fields.partner_ids.type = 'one2many';
this.data.partner.records[0].turtles = [2];
this.data.turtle.records[1].partner_ids = [2];
this.data.partner.onchanges = {
turtles: function (obj) {
obj.turtles = [
[5],
[1, 2, {
turtle_foo: "hop",
partner_ids: [[5], [4, 2], [4, 4]],
}],
];
},
};
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<field name="turtles">' +
'<tree editable="bottom">' +
'<field name="turtle_foo"/>' +
'<field name="partner_ids" widget="many2many_tags"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (route === '/web/dataset/call_kw/partner/write') {
var expectedResultTurtles = [
[1, 2, {
partner_ids: [[4, 2, false], [4, 4, false]],
turtle_foo: "hop",
}],
];
assert.deepEqual(args.args[1].turtles, expectedResultTurtles,
'The right values should be written');
}
return this._super.apply(this, arguments);
}
});
assert.deepEqual(form.$('.o_many2many_tags_cell').text().trim(), "second record",
"the partner_ids should be as specified at initialization");
form.$buttons.find('.o_form_button_edit').click();
form.$('.o_data_cell').eq(1).click();
var $cell = form.$('.o_selected_row .o_input[name=turtle_foo]');
$cell.val("hop").trigger("change");
form.$buttons.find('.o_form_button_save').click();
assert.deepEqual(form.$('.o_many2many_tags_cell').text().trim().split(/\s+/),
[ "second", "record", "aaa" ],
'The partner_ids should have been updated');
form.destroy();
});
QUnit.test('onchange for embedded one2many with handle widget', function (assert) {
assert.expect(2);
@@ -237,7 +237,6 @@ var MockServer = Class.extend({
if (attrs) {
attrs = pyeval.py_eval(attrs);
_.extend(modifiers, attrs);
node.removeAttribute('attrs');
}
var states = node.getAttribute('states');
@@ -3582,6 +3582,56 @@ QUnit.module('Views', {
testUtils.unpatch(mixins.ParentedMixin);
});
QUnit.test('grouped kanban becomes ungrouped when clearing domain then clearing groupby', function (assert) {
// in this test, we simulate that clearing the domain is slow, so that
// clearing the groupby does not corrupt the data handled while
// reloading the kanban view.
assert.expect(4);
var def = $.Deferred();
var kanban = createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban class="o_kanban_test">' +
'<field name="bar"/>' +
'<templates><t t-name="kanban-box">' +
'<div><field name="foo"/></div>' +
'</t></templates></kanban>',
domain: [['foo', '=', 'norecord']],
groupBy: ['bar'],
mockRPC: function (route, args) {
var result = this._super(route, args);
if (args.method === 'read_group') {
var isFirstUpdate = _.isEmpty(args.kwargs.domain) &&
args.kwargs.groupby &&
args.kwargs.groupby[0] === 'bar';
if (isFirstUpdate) {
return def.then(_.constant(result));
}
}
return result;
},
});
assert.ok(kanban.$('.o_kanban_view').hasClass('o_kanban_grouped'),
"the kanban view should be grouped");
assert.notOk(kanban.$('.o_kanban_view').hasClass('o_kanban_ungrouped'),
"the kanban view should not be ungrouped");
kanban.update({domain: []}); // 1st update on kanban view
kanban.update({groupBy: false}); // 2n update on kanban view
def.resolve(); // simulate slow 1st update of kanban view
assert.notOk(kanban.$('.o_kanban_view').hasClass('o_kanban_grouped'),
"the kanban view should not longer be grouped");
assert.ok(kanban.$('.o_kanban_view').hasClass('o_kanban_ungrouped'),
"the kanban view should have become ungrouped");
kanban.destroy();
});
QUnit.test('keyboard navigation on kanban basic rendering', function (assert) {
assert.expect(3);
@@ -15,6 +15,7 @@ var NewContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
}),
events: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.events || {}, {
'click > a': '_onMenuToggleClick',
'click > #o_new_content_menu_choices': '_onBackgroundClick',
}),
/**
@@ -73,6 +74,15 @@ var NewContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
ev.preventDefault();
this.$newContentMenuChoices.toggleClass('o_hidden');
},
/**
* Called when a click outside the menu's options occurs -> Closes the menu
*
* @private
* @param {Event} ev
*/
_onBackgroundClick: function (ev) {
this.$newContentMenuChoices.addClass('o_hidden');
},
});
websiteNavbarData.websiteNavbarRegistry.add(NewContentMenu, '.o_new_content_menu');
+11
View File
@@ -0,0 +1,11 @@
India, 2018-06-27
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Maulik Raval maulik.raval502@gmail.com https://github.com/maulik-raval
+6
View File
@@ -413,6 +413,12 @@ actual arch.
self.clear_caches()
return super(View, self).write(self._compute_defaults(vals))
def unlink(self):
# if in uninstall mode and has children views, emulate an ondelete cascade
if self.env.context.get('_force_unlink', False) and self.mapped('inherit_children_ids'):
self.mapped('inherit_children_ids').unlink()
super(View, self).unlink()
@api.multi
def toggle(self):
""" Switches between enabled and disabled statuses
+1 -1
View File
@@ -9,7 +9,7 @@
<form string="Create Menu">
<group>
<field name="name"/>
<field name="menu_id" domain="[('parent_id','!=',False)]"/>
<field name="menu_id"/>
</group>
<footer>
<button name="menu_create" string="Create _Menu" type="object" class="btn-primary"/>
+13 -4
View File
@@ -120,7 +120,17 @@ def load_module_graph(cr, graph, status=None, perform_checks=True,
continue
_logger.debug('loading module %s (%d/%d)', module_name, index, module_count)
migrations.migrate_module(package, 'pre')
needs_update = (
hasattr(package, "init")
or hasattr(package, "update")
or package.state in ("to install", "to upgrade")
)
if needs_update:
if package.name != 'base':
registry.setup_models(cr)
migrations.migrate_module(package, 'pre')
load_openerp_module(package.name)
new_install = package.state == 'to install'
@@ -133,8 +143,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True,
model_names = registry.load(cr, package)
loaded_modules.append(package.name)
if (hasattr(package, 'init') or hasattr(package, 'update')
or package.state in ('to install', 'to upgrade')):
if needs_update:
models_updated |= set(model_names)
models_to_check -= set(model_names)
registry.setup_models(cr)
@@ -154,7 +163,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True,
if hasattr(package, 'init') or package.state == 'to install':
mode = 'init'
if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
if needs_update:
env = api.Environment(cr, SUPERUSER_ID, {})
# Can't put this line out of the loop: ir.module.module will be
# registered by init_models() above.