[MERGE] forward port branch saas-11.2 up to 45329c0cf2
This commit is contained in:
@@ -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 < 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 < 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 > 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
|
||||
------------------------------
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user