[MERGE] forward port branch saas-16 up to cc38b93ab2
This commit is contained in:
@@ -523,7 +523,10 @@ class AccountJournal(models.Model):
|
||||
@api.model
|
||||
def name_search(self, name='', args=None, operator='ilike', limit=100):
|
||||
args = args or []
|
||||
recs = self.search(['|', ('code', operator, name), ('name', operator, name)] + args, limit=limit)
|
||||
connector = '|'
|
||||
if operator in expression.NEGATIVE_TERM_OPERATORS:
|
||||
connector = '&'
|
||||
recs = self.search([connector, ('code', operator, name), ('name', operator, name)] + args, limit=limit)
|
||||
return recs.name_get()
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -248,7 +248,7 @@ class AccountBankStatement(models.Model):
|
||||
for aml in st_line.journal_entry_ids:
|
||||
moves |= aml.move_id
|
||||
if moves:
|
||||
moves.post()
|
||||
moves.filtered(lambda m: m.state != 'posted').post()
|
||||
statement.message_post(body=_('Statement %s confirmed, journal items were created.') % (statement.name,))
|
||||
statements.link_bank_to_partner()
|
||||
statements.write({'state': 'confirm', 'date_done': time.strftime("%Y-%m-%d %H:%M:%S")})
|
||||
|
||||
@@ -28,3 +28,30 @@ class AccountingTestCase(HttpCase):
|
||||
if theorical_lines:
|
||||
raise ValidationError('Remaining theorical line (not found). %s)' % ([(aml[0], aml[1], aml[2]) for aml in theorical_lines]))
|
||||
return True
|
||||
|
||||
def ensure_account_property(self, property_name):
|
||||
'''Ensure the ir.property targetting an account.account passed as parameter exists.
|
||||
In case it's not: create it with a random account. This is useful when testing with
|
||||
partially defined localization (missing stock properties for example)
|
||||
|
||||
:param property_name: The name of the property.
|
||||
'''
|
||||
company_id = self.env.user.company_id
|
||||
field_id = self.env['ir.model.fields'].search(
|
||||
[('model', '=', 'product.template'), ('name', '=', property_name)], limit=1)
|
||||
property_id = self.env['ir.property'].search([
|
||||
('company_id', '=', company_id.id),
|
||||
('name', '=', property_name),
|
||||
('res_id', '=', None),
|
||||
('fields_id', '=', field_id.id)], limit=1)
|
||||
account_id = self.env['account.account'].search([('company_id', '=', company_id.id)], limit=1)
|
||||
value_reference = 'account.account,%d' % account_id.id
|
||||
if property_id and not property_id.value_reference:
|
||||
property_id.value_reference = value_reference
|
||||
else:
|
||||
self.env['ir.property'].create({
|
||||
'name': property_name,
|
||||
'company_id': company_id.id,
|
||||
'fields_id': field_id.id,
|
||||
'value_reference': value_reference,
|
||||
})
|
||||
|
||||
@@ -9,8 +9,9 @@ from odoo.exceptions import UserError
|
||||
|
||||
|
||||
def geo_find(addr):
|
||||
if not addr:
|
||||
return None
|
||||
url = 'https://maps.googleapis.com/maps/api/geocode/json'
|
||||
|
||||
try:
|
||||
result = requests.get(url, params={'sensor': 'false', 'address': addr}).json()
|
||||
except Exception as e:
|
||||
|
||||
@@ -100,18 +100,23 @@ class IrModule(models.Model):
|
||||
raise UserError(_("File '%s' exceed maximum allowed file size") % zf.filename)
|
||||
|
||||
with tempdir() as module_dir:
|
||||
z.extractall(module_dir)
|
||||
dirs = [d for d in os.listdir(module_dir) if os.path.isdir(opj(module_dir, d))]
|
||||
for mod_name in dirs:
|
||||
module_names.append(mod_name)
|
||||
try:
|
||||
# assert mod_name.startswith('theme_')
|
||||
path = opj(module_dir, mod_name)
|
||||
self._import_module(mod_name, path, force=force)
|
||||
success.append(mod_name)
|
||||
except Exception as e:
|
||||
_logger.exception('Error while importing module')
|
||||
errors[mod_name] = exception_to_unicode(e)
|
||||
import odoo.modules as addons
|
||||
try:
|
||||
addons.module.ad_paths.append(module_dir)
|
||||
z.extractall(module_dir)
|
||||
dirs = [d for d in os.listdir(module_dir) if os.path.isdir(opj(module_dir, d))]
|
||||
for mod_name in dirs:
|
||||
module_names.append(mod_name)
|
||||
try:
|
||||
# assert mod_name.startswith('theme_')
|
||||
path = opj(module_dir, mod_name)
|
||||
self._import_module(mod_name, path, force=force)
|
||||
success.append(mod_name)
|
||||
except Exception as e:
|
||||
_logger.exception('Error while importing module')
|
||||
errors[mod_name] = exception_to_unicode(e)
|
||||
finally:
|
||||
addons.module.ad_paths.remove(module_dir)
|
||||
r = ["Successfully imported module '%s'" % mod for mod in success]
|
||||
for mod, error in pycompat.items(errors):
|
||||
r.append("Error while importing module '%s': %r" % (mod, error))
|
||||
|
||||
@@ -567,15 +567,18 @@ class Meeting(models.Model):
|
||||
if not event_date:
|
||||
event_date = datetime.now()
|
||||
|
||||
# Convert the event date to saved timezone (or context tz) as it'll
|
||||
# define the correct hour/day asked by the user to repeat for recurrence.
|
||||
event_date = event_date.astimezone(timezone) # transform "+hh:mm" timezone
|
||||
rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date, forceset=True)
|
||||
if self.allday and self.rrule and 'UNTIL' in self.rrule and 'Z' not in self.rrule:
|
||||
rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date.replace(tzinfo=None), forceset=True, ignoretz=True)
|
||||
else:
|
||||
# Convert the event date to saved timezone (or context tz) as it'll
|
||||
# define the correct hour/day asked by the user to repeat for recurrence.
|
||||
event_date = event_date.astimezone(timezone) # transform "+hh:mm" timezone
|
||||
rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date, forceset=True, tzinfos={})
|
||||
recurring_meetings = self.search([('recurrent_id', '=', self.id), '|', ('active', '=', False), ('active', '=', True)])
|
||||
|
||||
for meeting in recurring_meetings:
|
||||
rset1._exdate.append(todate(meeting.recurrent_id_date))
|
||||
return [d.astimezone(pytz.UTC) for d in rset1]
|
||||
return [d.astimezone(pytz.UTC) if d.tzinfo else d for d in rset1]
|
||||
|
||||
@api.multi
|
||||
def _get_recurrency_end_date(self):
|
||||
|
||||
@@ -247,6 +247,7 @@ class Lead(models.Model):
|
||||
team_id = self.env['crm.team']._get_default_team_id(user_id=user_id)
|
||||
return {'team_id': team_id}
|
||||
|
||||
@api.constrains('user_id')
|
||||
@api.onchange('user_id')
|
||||
def _onchange_user_id(self):
|
||||
""" When changing the user, also set a team_id or restrict team id to the ones user_id is member of. """
|
||||
|
||||
@@ -226,26 +226,26 @@ class DeliveryCarrier(models.Model):
|
||||
if record.delivery_type == 'fixed':
|
||||
PriceRule.search([('carrier_id', '=', record.id)]).unlink()
|
||||
|
||||
line_data = {
|
||||
'carrier_id': record.id,
|
||||
'variable': 'price',
|
||||
'operator': '>=',
|
||||
}
|
||||
# Create the delivery price rules
|
||||
if record.free_if_more_than:
|
||||
line_data.update({
|
||||
'max_value': record.amount,
|
||||
'standard_price': 0.0,
|
||||
'list_base_price': 0.0,
|
||||
})
|
||||
PriceRule.create(line_data)
|
||||
if record.fixed_price is not False:
|
||||
line_data.update({
|
||||
'max_value': 0.0,
|
||||
'standard_price': record.fixed_price,
|
||||
'list_base_price': record.fixed_price,
|
||||
})
|
||||
PriceRule.create(line_data)
|
||||
line_data = {
|
||||
'carrier_id': record.id,
|
||||
'variable': 'price',
|
||||
'operator': '>=',
|
||||
}
|
||||
# Create the delivery price rules
|
||||
if record.free_if_more_than:
|
||||
line_data.update({
|
||||
'max_value': record.amount,
|
||||
'standard_price': 0.0,
|
||||
'list_base_price': 0.0,
|
||||
})
|
||||
PriceRule.create(line_data)
|
||||
if record.fixed_price is not False:
|
||||
line_data.update({
|
||||
'max_value': 0.0,
|
||||
'standard_price': record.fixed_price,
|
||||
'list_base_price': record.fixed_price,
|
||||
})
|
||||
PriceRule.create(line_data)
|
||||
return True
|
||||
|
||||
@api.model
|
||||
|
||||
@@ -329,9 +329,12 @@ class Holidays(models.Model):
|
||||
res = []
|
||||
for leave in self:
|
||||
if leave.type == 'remove':
|
||||
res.append((leave.id, _("%s on %s : %.2f day(s)") % (leave.employee_id.name or leave.category_id.name, leave.holiday_status_id.name, leave.number_of_days_temp)))
|
||||
if self.env.context.get('short_name'):
|
||||
res.append((leave.id, _("%s : %.2f day(s)") % (leave.name or leave.holiday_status_id.name, leave.number_of_days_temp)))
|
||||
else:
|
||||
res.append((leave.id, _("%s on %s : %.2f day(s)") % (leave.employee_id.name or leave.category_id.name, leave.holiday_status_id.name, leave.number_of_days_temp)))
|
||||
else:
|
||||
res.append((leave.id, _("Allocation of %s : %.2f day(s) To %s") % (leave.holiday_status_id.name, leave.number_of_days_temp,leave.employee_id.name)))
|
||||
res.append((leave.id, _("Allocation of %s : %.2f day(s) To %s") % (leave.holiday_status_id.name, leave.number_of_days_temp, leave.employee_id.name)))
|
||||
return res
|
||||
|
||||
def _check_state_access_right(self, vals):
|
||||
|
||||
@@ -7,6 +7,13 @@ from odoo import api, fields, models
|
||||
class AccountAnalyticLine(models.Model):
|
||||
_inherit = 'account.analytic.line'
|
||||
|
||||
@api.model
|
||||
def default_get(self, field_list):
|
||||
result = super(AccountAnalyticLine, self).default_get(field_list)
|
||||
if 'employee_id' in field_list and result.get('user_id') and result.get('project_id'):
|
||||
result['employee_id'] = self.env['hr.employee'].search([('user_id', '=', result['user_id'])], limit=1).id
|
||||
return result
|
||||
|
||||
task_id = fields.Many2one('project.task', 'Task')
|
||||
project_id = fields.Many2one('project.project', 'Project', domain=[('allow_timesheets', '=', True)])
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
<field name="planned_hours" widget="float_time"/>
|
||||
</field>
|
||||
<xpath expr="//notebook/page[@name='description_page']" position="after">
|
||||
<page string="Timesheets" attrs="{'invisible': [('project_id', '=', False)]}">
|
||||
<page string="Timesheets" attrs="{'invisible': [('project_id', '=', False)]}" groups="hr_timesheet.group_hr_timesheet_user">
|
||||
<field name="timesheet_ids" context="{'default_project_id': project_id}">
|
||||
<tree editable="bottom" string="Timesheet Activities" default_order="date">
|
||||
<field name="date"/>
|
||||
|
||||
@@ -208,8 +208,11 @@ class MrpProduction(models.Model):
|
||||
@api.depends('move_raw_ids.quantity_done', 'move_finished_ids.quantity_done')
|
||||
def _compute_post_visible(self):
|
||||
for order in self:
|
||||
order.post_visible = any(order.move_raw_ids.filtered(lambda x: (x.quantity_done) > 0 and (x.state not in ['done', 'cancel']))) or \
|
||||
any(order.move_finished_ids.filtered(lambda x: (x.quantity_done) > 0 and (x.state not in ['done', 'cancel'])))
|
||||
if order.product_tmpl_id._is_cost_method_standard():
|
||||
order.post_visible = any((x.quantity_done > 0 and x.state not in ['done', 'cancel']) for x in order.move_raw_ids) or \
|
||||
any((x.quantity_done > 0 and x.state not in ['done' 'cancel']) for x in order.move_finished_ids)
|
||||
else:
|
||||
order.post_visible = any((x.quantity_done > 0 and x.state not in ['done' 'cancel']) for x in order.move_finished_ids)
|
||||
|
||||
@api.multi
|
||||
@api.depends('workorder_ids.state', 'move_finished_ids')
|
||||
|
||||
@@ -51,19 +51,23 @@ KanbanRecord.include({
|
||||
}
|
||||
|
||||
function open_cover_images_dialog(attachment_ids) {
|
||||
var cover_id = self.record.displayed_image_id.raw_value[0];
|
||||
var coverID = self.record.displayed_image_id && self.record.displayed_image_id.raw_value[0];
|
||||
var $content = $(QWeb.render("project.SetCoverModal", {
|
||||
cover_id: cover_id,
|
||||
cover_id: coverID,
|
||||
attachment_ids: attachment_ids,
|
||||
}));
|
||||
var $imgs = $content.find('img');
|
||||
|
||||
var dialog = new Dialog(self, {
|
||||
title: _t("Set a Cover Image"),
|
||||
buttons: [{text: _t("Select"), classes: 'btn-primary', close: true, disabled: !cover_id, click: function () {
|
||||
self._updateRecord({displayed_image_id: $imgs.filter('.o_selected').data('id')});
|
||||
buttons: [{text: _t("Select"), classes: 'btn-primary', close: true, disabled: !coverID, click: function () {
|
||||
var $img = $imgs.filter('.o_selected');
|
||||
var data = {
|
||||
id: $img.data('id'),
|
||||
display_name: $img.data('name')
|
||||
};
|
||||
self._updateRecord({displayed_image_id: data});
|
||||
}}, {text: _t("Remove Cover Image"), close: true, click: function () {
|
||||
self._updateRecord({displayed_image_id: 0});
|
||||
self._updateRecord({displayed_image_id: false});
|
||||
}}, {text: _t("Discard"), close: true}],
|
||||
$content: $content,
|
||||
}).open();
|
||||
@@ -75,7 +79,12 @@ KanbanRecord.include({
|
||||
});
|
||||
|
||||
$content.on('dblclick', 'img', function (ev) {
|
||||
self._updateRecord({displayed_image_id: $(ev.currentTarget).data('id')});
|
||||
var $img = $(ev.currentTarget);
|
||||
var data = {
|
||||
id: $img.data('id'),
|
||||
display_name: $img.data('name')
|
||||
};
|
||||
self._updateRecord({displayed_image_id: data});
|
||||
dialog.close();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="project.SetCoverModal">
|
||||
<div t-if="attachment_ids.length > 0" class="o_kanban_task_cover_image">
|
||||
<img t-foreach="attachment_ids" t-as="attachment" t-attf-class="img-responsive#{cover_id == attachment['id'] ? ' o_selected' : ''}" t-attf-src="/web/image/#{attachment['id']}?unique=1" t-att-data-id="attachment['id']"/>
|
||||
<img t-foreach="attachment_ids" t-as="attachment" t-attf-class="img-responsive#{cover_id == attachment['id'] ? ' o_selected' : ''}" t-attf-src="/web/image/#{attachment['id']}?unique=1" t-att-data-id="attachment['id']" t-att-data-name="attachment['name']"/>
|
||||
</div>
|
||||
<div t-if="attachment_ids.length == 0">There is no available image to be set as cover. Send a message on the task with an attached image.</div>
|
||||
</t>
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
odoo.define('project.project_kanban_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var KanbanView = require('web.KanbanView');
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
var createView = testUtils.createView;
|
||||
|
||||
QUnit.module('project', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
'ir.attachment': {
|
||||
fields: {
|
||||
name: {
|
||||
string: "Name",
|
||||
type: "char"
|
||||
},
|
||||
},
|
||||
records: [{
|
||||
id: 1,
|
||||
name: "1.png"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "2.png"
|
||||
},
|
||||
]
|
||||
},
|
||||
'project.task': {
|
||||
fields: {
|
||||
name: {
|
||||
string: "Task Title",
|
||||
type: "char"
|
||||
},
|
||||
sequence: {
|
||||
string: "sequence",
|
||||
type: "integer"
|
||||
},
|
||||
displayed_image_id: {
|
||||
string: "cover",
|
||||
type: "many2one",
|
||||
relation: "ir.attachment"
|
||||
},
|
||||
kanban_state: {
|
||||
string: "State",
|
||||
type: "selection",
|
||||
selection: [
|
||||
["abc", "ABC"],
|
||||
["def", "DEF"],
|
||||
["ghi", "GHI"]
|
||||
]
|
||||
},
|
||||
},
|
||||
records: [{
|
||||
id: 1,
|
||||
name: "task1",
|
||||
sequence: 1,
|
||||
kanban_state: "abc"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "task2",
|
||||
sequence: 2,
|
||||
kanban_state: "abc"
|
||||
},
|
||||
]
|
||||
},
|
||||
};
|
||||
}
|
||||
}, function () {
|
||||
QUnit.module('image test');
|
||||
|
||||
QUnit.test('cover_image_test', function (assert) {
|
||||
assert.expect(6);
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'project.task',
|
||||
data: this.data,
|
||||
arch: '<kanban class="o_kanban_test">' +
|
||||
'<templates>' +
|
||||
'<t t-name="kanban-box">' +
|
||||
'<div>' +
|
||||
'<field name="name"/>' +
|
||||
'<div class="o_dropdown_kanban dropdown">' +
|
||||
'<a class="dropdown-toggle btn" data-toggle="dropdown" href="#">' +
|
||||
'<span class="fa fa-bars fa-lg"/>' +
|
||||
'</a>' +
|
||||
'<ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">' +
|
||||
'<li>' +
|
||||
'<a type="set_cover">Set Cover Image</a>'+
|
||||
'</li>' +
|
||||
'</ul>' +
|
||||
'</div>' +
|
||||
'<div>'+
|
||||
'<field name="displayed_image_id" widget="attachment_image"/>'+
|
||||
'</div>'+
|
||||
'</div>' +
|
||||
'</t>' +
|
||||
'</templates>' +
|
||||
'</kanban>',
|
||||
mockRPC: function(route, args) {
|
||||
if (args.model === 'ir.attachment' && args.method === 'search_read') {
|
||||
return $.when([{
|
||||
id: 1,
|
||||
name: "1.png"
|
||||
},{
|
||||
id: 2,
|
||||
name: "2.png"
|
||||
}]);
|
||||
}
|
||||
if (args.model === 'project.task' && args.method === 'write') {
|
||||
assert.step(args.args[0][0]);
|
||||
return this._super(route, args);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
assert.strictEqual(kanban.$('img').length, 0, "Initially there is no image.");
|
||||
kanban.$('.o_dropdown_kanban [data-type=set_cover]').eq(0).click();
|
||||
// single click on image
|
||||
$('.modal').find("img[data-id='1']").click();
|
||||
$('.modal-footer .btn-primary').click();
|
||||
assert.strictEqual(kanban.$('img[src*="/web/image/1"]').length, 1, "Image inserted in record");
|
||||
$('.o_dropdown_kanban [data-type=set_cover]').eq(1).click();
|
||||
// double click on image
|
||||
$('.modal').find("img[data-id='2']").dblclick();
|
||||
assert.strictEqual(kanban.$('img[src*="/web/image/2"]').length, 1, "Image inserted after double click");
|
||||
// varify write on both kanban record
|
||||
assert.verifySteps([1,2]);
|
||||
kanban.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,4 +10,9 @@
|
||||
<script type="text/javascript" src="/project/static/src/js/web_planner_project.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
<template id="qunit_suite" name="project_test" inherit_id="web.qunit_suite">
|
||||
<xpath expr="//t[@t-set='head']" position="inside">
|
||||
<script type="text/javascript" src="/project/static/tests/project_tests.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<field name="progress" widget="progressbar" attrs="{'invisible':[('task_id','=',False)]}" class="oe_inline"/>
|
||||
</field>
|
||||
<xpath expr="//notebook/page[@name='extra_info']" position="before">
|
||||
<page string="Timesheets" attrs="{'invisible': [('project_id', '=', False)]}">
|
||||
<page string="Timesheets" attrs="{'invisible': [('project_id', '=', False)]}" groups="hr_timesheet.group_hr_timesheet_user">
|
||||
<field name="timesheet_ids" colspan="4" nolabel="1" context="{'default_user_id' : uid, 'default_project_id': project_id}"
|
||||
groups="base.group_user">
|
||||
<tree editable="top" string="Timesheet Activities">
|
||||
|
||||
@@ -105,6 +105,9 @@ class TestSaleOrder(TestSale):
|
||||
|
||||
def test_cost_invoicing(self):
|
||||
""" Test confirming a vendor invoice to reinvoice cost on the so """
|
||||
# force the pricelist to have the same currency as the company
|
||||
self.env.ref('product.list0').currency_id = self.env.ref('base.main_company').currency_id
|
||||
|
||||
serv_cost = self.env.ref('product.service_cost_01')
|
||||
prod_gap = self.env.ref('product.product_product_1')
|
||||
so = self.env['sale.order'].create({
|
||||
|
||||
@@ -7,6 +7,9 @@ from odoo.addons.sale.tests.test_sale_common import TestSale
|
||||
class TestSaleExpense(TestSale):
|
||||
def test_sale_expense(self):
|
||||
""" Test the behaviour of sales orders when managing expenses """
|
||||
# force the pricelist to have the same currency as the company
|
||||
self.env.ref('product.list0').currency_id = self.env.ref('base.main_company').currency_id
|
||||
|
||||
# create a so with a product invoiced on delivery
|
||||
prod = self.env.ref('product.product_product_1')
|
||||
so = self.env['sale.order'].create({
|
||||
|
||||
@@ -53,7 +53,7 @@ class AccountInvoice(models.Model):
|
||||
|
||||
def _compute_timesheet_revenue(self):
|
||||
for invoice in self:
|
||||
for invoice_line in invoice.invoice_line_ids.filtered(lambda line: line.product_id.type == 'service'):
|
||||
for invoice_line in invoice.invoice_line_ids.filtered(lambda line: line.product_id.type == 'service').sorted(key=lambda inv_line: (inv_line.invoice_id, inv_line.id)):
|
||||
uninvoiced_timesheet_lines = self.env['account.analytic.line'].sudo().search([
|
||||
('so_line', 'in', invoice_line.sale_line_ids.ids),
|
||||
('project_id', '!=', False),
|
||||
@@ -63,13 +63,12 @@ class AccountInvoice(models.Model):
|
||||
|
||||
# NOTE JEM : changing quantity (or unit price) of invoice line does not impact the revenue calculation. (FP specs)
|
||||
if uninvoiced_timesheet_lines:
|
||||
precision = invoice_line.currency_id.decimal_places
|
||||
# delivered : update revenue with the prorata of number of hours on the timesheet line
|
||||
if invoice_line.product_id.invoice_policy == 'delivery':
|
||||
invoiced_price_per_hour = float_round(invoice_line.price_subtotal / float(sum(uninvoiced_timesheet_lines.mapped('unit_amount'))), precision)
|
||||
invoiced_price_per_hour = invoice_line.currency_id.round(invoice_line.price_subtotal / float(sum(uninvoiced_timesheet_lines.mapped('unit_amount'))))
|
||||
# invoicing analytic lines of different currency
|
||||
total_revenue_per_currency = dict.fromkeys(uninvoiced_timesheet_lines.mapped('company_currency_id').ids, 0.0)
|
||||
for index, timesheet_line in enumerate(uninvoiced_timesheet_lines):
|
||||
for index, timesheet_line in enumerate(uninvoiced_timesheet_lines.sorted(key=lambda ts: ts.date)):
|
||||
if index+1 != len(uninvoiced_timesheet_lines):
|
||||
line_revenue = invoice_line.currency_id.compute(invoiced_price_per_hour, timesheet_line.company_currency_id) * timesheet_line.unit_amount
|
||||
total_revenue_per_currency[timesheet_line.company_currency_id.id] += line_revenue
|
||||
@@ -92,7 +91,7 @@ class AccountInvoice(models.Model):
|
||||
# invoicing analytic lines of different currency
|
||||
total_revenue_per_currency = dict.fromkeys(no_zero_timesheet_revenue.mapped('company_currency_id').ids, 0.0)
|
||||
|
||||
for index, timesheet_line in enumerate(no_zero_timesheet_revenue):
|
||||
for index, timesheet_line in enumerate(no_zero_timesheet_revenue.sorted(key=lambda ts: ts.date)):
|
||||
if index+1 != len(no_zero_timesheet_revenue):
|
||||
price_subtotal_inv = invoice_line.currency_id.compute(invoice_line.price_subtotal, timesheet_line.company_currency_id)
|
||||
price_subtotal_sol = timesheet_line.so_line.currency_id.compute(timesheet_line.so_line.price_subtotal, timesheet_line.company_currency_id)
|
||||
|
||||
@@ -6383,6 +6383,12 @@ msgid "You cannot set a negative product quantity in an inventory line:\n"
|
||||
" %s - qty: %s"
|
||||
msgstr ""
|
||||
|
||||
#. module: stock
|
||||
#: code:addons/stock/models/stock_production_lot.py:37
|
||||
#, python-format
|
||||
msgid "You are not allowed to create a lot for this picking type"
|
||||
msgstr ""
|
||||
|
||||
#. module: stock
|
||||
#: code:addons/stock/models/stock_move.py:927
|
||||
#, python-format
|
||||
|
||||
@@ -419,6 +419,9 @@ class ProductTemplate(models.Model):
|
||||
relation="stock.location.route", string="Category Routes",
|
||||
related='categ_id.total_route_ids')
|
||||
|
||||
def _is_cost_method_standard(self):
|
||||
return True
|
||||
|
||||
def _compute_quantities(self):
|
||||
res = self._compute_quantities_dict()
|
||||
for template in self:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ProductionLot(models.Model):
|
||||
@@ -27,6 +28,15 @@ class ProductionLot(models.Model):
|
||||
('name_ref_uniq', 'unique (name, product_id)', 'The combination of serial number and product must be unique !'),
|
||||
]
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
pack_id = self.env.context.get('active_pack_operation', False)
|
||||
if pack_id:
|
||||
pack = self.env['stock.pack.operation'].browse(pack_id)
|
||||
if pack.picking_id and not pack.picking_id.picking_type_id.use_create_lots:
|
||||
raise UserError(_("You are not allowed to create a lot for this picking type"))
|
||||
return super(ProductionLot, self).create(vals)
|
||||
|
||||
@api.one
|
||||
@api.depends('quant_ids.qty')
|
||||
def _product_qty(self):
|
||||
|
||||
@@ -66,6 +66,12 @@ class StockScrap(models.Model):
|
||||
scrap.do_scrap()
|
||||
return scrap
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
if 'done' in self.mapped('state'):
|
||||
raise UserError(_('You cannot delete a scrap which is done.'))
|
||||
return super(StockScrap, self).unlink()
|
||||
|
||||
def _get_origin_moves(self):
|
||||
return self.picking_id and self.picking_id.move_lines.filtered(lambda x: x.product_id == self.product_id)
|
||||
|
||||
|
||||
@@ -78,4 +78,8 @@ access_barcode_rule_stock_manager,barcode.rule.stock.manager,barcodes.model_barc
|
||||
access_stock_forecast_user,report.stock.forecast.user,model_report_stock_forecast,stock.group_stock_user,1,0,0,0
|
||||
access_stock_forecast_manager,report.stock.forecast.manager,model_report_stock_forecast,stock.group_stock_manager,1,1,1,1
|
||||
access_stock_scrap_user,stock.scrap.user,model_stock_scrap,stock.group_stock_user,1,1,1,0
|
||||
access_procurement_order,procurement.order,stock.model_procurement_order,base.group_portal,1,0,0,0
|
||||
access_procurement_order,procurement.order,stock.model_procurement_order,base.group_portal,1,0,0,0
|
||||
access_product_attribute_manager,product.attribute manager,product.model_product_attribute,stock.group_stock_manager,1,1,1,1
|
||||
access_product_attribute_value_manager,product.attribute manager value,product.model_product_attribute_value,stock.group_stock_manager,1,1,1,1
|
||||
access_product_attribute_price_manager,product.attribute manager price,product.model_product_attribute_price,stock.group_stock_manager,1,1,1,1
|
||||
access_product_attribute_line_manager,product.attribute manager line,product.model_product_attribute_line,stock.group_stock_manager,1,1,1,1
|
||||
|
@@ -85,7 +85,7 @@
|
||||
<field name="lot_name" invisible="not context.get('only_create', False)"/>
|
||||
<field name="lot_id" invisible="context.get('only_create', False)"
|
||||
domain="[('product_id','=', parent.product_id)]"
|
||||
context="{'default_product_id': parent.product_id}"/>
|
||||
context="{'default_product_id': parent.product_id, 'active_pack_operation': parent.id}"/>
|
||||
<field name="qty_todo" invisible="not context.get('show_reserved') or context.get('serial') or context.get('state_done')" readonly="1"/>
|
||||
<field name="qty" invisible="context.get('serial')"/>
|
||||
<button name="do_minus" type="object" icon="fa-minus-square" attrs="{'invisible': [('qty', '<=', 0.99)]}" invisible="not context.get('show_reserved') or context.get('state_done')"/>
|
||||
|
||||
@@ -51,6 +51,9 @@ class ProductTemplate(models.Model):
|
||||
def _compute_cost_method(self):
|
||||
self.cost_method = self.property_cost_method or self.categ_id.property_cost_method
|
||||
|
||||
def _is_cost_method_standard(self):
|
||||
return self.property_cost_method == 'standard'
|
||||
|
||||
@api.one
|
||||
def _set_cost_method(self):
|
||||
return self.write({'property_cost_method': self.cost_method})
|
||||
|
||||
@@ -59,6 +59,9 @@ class TestStockLandedCostsCommon(AccountingTestCase):
|
||||
self.brokerage_quantity = self._create_services('Brokerage Cost')
|
||||
self.transportation_weight = self._create_services('Transportation Cost')
|
||||
self.packaging_volume = self._create_services('Packaging Cost')
|
||||
# Ensure the account properties exists.
|
||||
self.ensure_account_property('property_stock_account_input')
|
||||
self.ensure_account_property('property_stock_account_output')
|
||||
|
||||
def _create_services(self, name):
|
||||
return self.Product.create({
|
||||
|
||||
+6
@@ -421,6 +421,12 @@
|
||||
element.children().first().after(widget);
|
||||
}
|
||||
|
||||
// !! ODOO FIX START !!
|
||||
var parentOffset = parent.offset();
|
||||
position.top = offset.top - parentOffset.top;
|
||||
position.left = offset.left - parentOffset.left;
|
||||
// !! ODOO FIX END !!
|
||||
|
||||
// Top and bottom logic
|
||||
if (vertical === 'auto') {
|
||||
if (offset.top + widget.height() * 1.5 >= $(window).height() + $(window).scrollTop() &&
|
||||
|
||||
@@ -82,6 +82,7 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
||||
}
|
||||
});
|
||||
},
|
||||
show_wow: '_onShowWow',
|
||||
},
|
||||
init: function (parent) {
|
||||
this.client_options = {};
|
||||
@@ -319,6 +320,22 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
||||
this.notification_manager.warn(e.data.title, e.data.message, e.data.sticky);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Displays a thumb up, heart or peace image (randomly) for a moment (e.g.
|
||||
* used when an opportunity is won)
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onShowWow: function () {
|
||||
var className = 'o_wow_thumbs';
|
||||
if (Math.random() > 0.9) {
|
||||
var otherClasses = ['o_wow_peace', 'o_wow_heart'];
|
||||
className = otherClasses[Math.floor(Math.random()*otherClasses.length)];
|
||||
}
|
||||
var $body = $('body');
|
||||
$body.addClass(className);
|
||||
setTimeout($body.removeClass.bind($body, className), 1000);
|
||||
},
|
||||
});
|
||||
|
||||
return AbstractWebClient;
|
||||
|
||||
@@ -176,8 +176,8 @@ var AbstractField = Widget.extend({
|
||||
* focusable element was not found or invisible
|
||||
*/
|
||||
activate: function (options) {
|
||||
var $focusable = this.getFocusableElement();
|
||||
if ($focusable.length && $focusable.is(':visible')) {
|
||||
if (this.isFocusable()) {
|
||||
var $focusable = this.getFocusableElement();
|
||||
$focusable.focus();
|
||||
if ($focusable.is('input[type="text"], textarea')) {
|
||||
$focusable[0].selectionStart = $focusable[0].selectionEnd = $focusable[0].value.length;
|
||||
@@ -211,6 +211,15 @@ var AbstractField = Widget.extend({
|
||||
getFocusableElement: function () {
|
||||
return $();
|
||||
},
|
||||
/**
|
||||
* Returns true iff the widget has a visible element that can take the focus
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isFocusable: function () {
|
||||
var $focusable = this.getFocusableElement();
|
||||
return $focusable.length && $focusable.is(':visible');
|
||||
},
|
||||
/**
|
||||
* this method is used to determine if the field value is set to a meaningful
|
||||
* value. This is useful to determine if a field should be displayed as empty
|
||||
@@ -351,12 +360,13 @@ var AbstractField = Widget.extend({
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.forceChange=false] if true, the change event will be
|
||||
* triggered even if the new value is the same as the old one
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_setValue: function (value, options) {
|
||||
// we try to avoid doing useless work, if the value given has not
|
||||
// changed. Note that we compare the unparsed values.
|
||||
if (this.lastSetValue === value || (this.value === false && value === '')) {
|
||||
return;
|
||||
return $.when();
|
||||
}
|
||||
this.lastSetValue = value;
|
||||
try {
|
||||
@@ -364,18 +374,22 @@ var AbstractField = Widget.extend({
|
||||
this._isValid = true;
|
||||
} catch (e) {
|
||||
this._isValid = false;
|
||||
return;
|
||||
return $.Deferred().reject();
|
||||
}
|
||||
if (!(options && options.forceChange) && this._isSameValue(value)) {
|
||||
return;
|
||||
return $.when();
|
||||
}
|
||||
var def = $.Deferred();
|
||||
var changes = {};
|
||||
changes[this.name] = value;
|
||||
this.trigger_up('field_changed', {
|
||||
dataPointID: this.dataPointID,
|
||||
changes: changes,
|
||||
viewType: this.viewType,
|
||||
onSuccess: def.resolve.bind(def),
|
||||
onFailure: def.reject.bind(def),
|
||||
});
|
||||
return def;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
@@ -1281,8 +1281,26 @@ var PriorityWidget = AbstractField.extend({
|
||||
},
|
||||
});
|
||||
|
||||
var AttachmentImage = AbstractField.extend({
|
||||
template: 'AttachmentImage',
|
||||
var AttachmentImage = AbstractField.extend({
|
||||
className: 'o_attachment_image',
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Reset cover image when widget value change
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_render: function () {
|
||||
if (this.value) {
|
||||
this.$el.empty().append($('<img>/', {
|
||||
src: "/web/image/" + this.value.data.id + "?unique=1",
|
||||
title: this.value.data.display_name
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var StateSelectionWidget = AbstractField.extend({
|
||||
|
||||
@@ -213,17 +213,20 @@ var FieldMany2One = AbstractField.extend({
|
||||
var self = this;
|
||||
var slowCreate = this._searchCreatePopup.bind(this, "form", false, this._createContext(name));
|
||||
if (this.nodeOptions.quick_create) {
|
||||
return this._rpc({
|
||||
model: this.field.relation,
|
||||
method: 'name_create',
|
||||
args: [name],
|
||||
context: this.record.getContext(this.recordParams),
|
||||
})
|
||||
.then(function (result) {
|
||||
if (self.mode === "edit") {
|
||||
self.reinitialize({id: result[0], display_name: result[1]});
|
||||
}
|
||||
}, slowCreate);
|
||||
this.trigger_up('mutexify', {
|
||||
action: function () {
|
||||
return self._rpc({
|
||||
model: self.field.relation,
|
||||
method: 'name_create',
|
||||
args: [name],
|
||||
context: self.record.getContext(self.recordParams),
|
||||
}).then(function (result) {
|
||||
if (self.mode === "edit") {
|
||||
self.reinitialize({id: result[0], display_name: result[1]});
|
||||
}
|
||||
}, slowCreate);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
slowCreate();
|
||||
}
|
||||
@@ -964,7 +967,6 @@ var FieldOne2Many = FieldX2Many.extend({
|
||||
var index = self.editable === 'top' ? 0 : self.value.data.length - 1;
|
||||
var newID = self.value.data[index].id;
|
||||
self.renderer.editRecord(newID);
|
||||
self.creatingRecord = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1014,6 +1016,7 @@ var FieldOne2Many = FieldX2Many.extend({
|
||||
* in the kanban view
|
||||
*/
|
||||
_onAddRecord: function (ev) {
|
||||
var self = this;
|
||||
// we don't want interference with the components upstream.
|
||||
ev.stopPropagation();
|
||||
|
||||
@@ -1023,10 +1026,11 @@ var FieldOne2Many = FieldX2Many.extend({
|
||||
this._setValue({
|
||||
operation: 'CREATE',
|
||||
position: this.editable,
|
||||
}).always(function () {
|
||||
self.creatingRecord = false;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
var self = this;
|
||||
this._openFormDialog({
|
||||
on_saved: function (record) {
|
||||
self._setValue({ operation: 'ADD', id: record.id });
|
||||
|
||||
@@ -11,6 +11,7 @@ var utils = require('web.utils');
|
||||
return core.Class.extend({
|
||||
init: function () {
|
||||
this._init_cache();
|
||||
core.bus.on('clear_cache', this, this.invalidate.bind(this));
|
||||
},
|
||||
|
||||
_init_cache: function () {
|
||||
|
||||
@@ -46,6 +46,9 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
start: function () {
|
||||
// add classname to reflect the (absence of) access rights (used to
|
||||
// correctly display the nocontent helper)
|
||||
this.$el.toggleClass('o_cannot_create', !this.activeActions.create);
|
||||
return this._super.apply(this, arguments)
|
||||
.then(this._updateEnv.bind(this));
|
||||
},
|
||||
@@ -186,8 +189,12 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
|
||||
// user if some discarding has to be made). This operation must also be
|
||||
// mutex-protected as commitChanges function of x2m has to be aware of
|
||||
// all final changes made to a row.
|
||||
this.mutex.exec(this.renderer.commitChanges.bind(this.renderer, recordID || this.handle)); // TODO write a test for this
|
||||
return this.mutex.exec(this._saveRecord.bind(this, recordID, options));
|
||||
var self = this;
|
||||
return this.mutex
|
||||
.exec(this.renderer.commitChanges.bind(this.renderer, recordID || this.handle))
|
||||
.then(function () {
|
||||
return self.mutex.exec(self._saveRecord.bind(self, recordID, options));
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
|
||||
@@ -1841,7 +1841,6 @@ var BasicModel = AbstractModel.extend({
|
||||
return d.res_id === res_id;
|
||||
});
|
||||
m2mList.data.push(dataPoint.id);
|
||||
m2mList.count++;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2476,7 +2475,10 @@ var BasicModel = AbstractModel.extend({
|
||||
// REPLACE_WITH
|
||||
x2manyList.res_ids = value[2];
|
||||
x2manyList.count = x2manyList.res_ids.length;
|
||||
defs.push(self._readUngroupedList(x2manyList));
|
||||
var def = self._readUngroupedList(x2manyList).then(function () {
|
||||
return self._fetchX2ManysBatched(x2manyList);
|
||||
});
|
||||
defs.push(def);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -99,7 +99,9 @@ var FieldManagerMixin = {
|
||||
// subrecord's form view), otherwise it bubbles up to the main form view
|
||||
// but its model doesn't have any data related to the given dataPointID
|
||||
event.stopPropagation();
|
||||
this._applyChanges(event.data.dataPointID, event.data.changes, event);
|
||||
this._applyChanges(event.data.dataPointID, event.data.changes, event)
|
||||
.done(event.data.onSuccess || function () {})
|
||||
.fail(event.data.onFailure || function () {});
|
||||
},
|
||||
/**
|
||||
* Some widgets need to trigger a reload of their data. For example, a
|
||||
|
||||
@@ -39,12 +39,10 @@ var FormController = BasicController.extend({
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This method is supposed to focus the first active control, I think. It
|
||||
* is currently only called by the FormViewDialog.
|
||||
*
|
||||
* @todo To be implemented
|
||||
* Calls autofocus on the renderer
|
||||
*/
|
||||
autofocus: function () {
|
||||
this.renderer.autofocus();
|
||||
},
|
||||
/**
|
||||
* This method switches the form view in edit mode, with a new record.
|
||||
@@ -92,6 +90,14 @@ var FormController = BasicController.extend({
|
||||
var dataPoint = this.model.get(this.handle, {raw: true});
|
||||
return dataPoint.data.display_name || _t('New');
|
||||
},
|
||||
/**
|
||||
* Called each time the form view is attached into the DOM
|
||||
*
|
||||
* @todo convert to new style
|
||||
*/
|
||||
on_attach_callback: function () {
|
||||
this.autofocus();
|
||||
},
|
||||
/**
|
||||
* Render buttons for the control panel. The form view can be rendered in
|
||||
* a dialog, and in that case, if we have buttons defined in the footer, we
|
||||
@@ -374,10 +380,8 @@ var FormController = BasicController.extend({
|
||||
});
|
||||
}
|
||||
|
||||
if (event.data.show_wow) {
|
||||
def.then(function () {
|
||||
self.show_wow();
|
||||
});
|
||||
if (event.data.showWow) {
|
||||
def.then(this.trigger_up.bind(this, 'show_wow'));
|
||||
}
|
||||
|
||||
def.always(this._enableButtons.bind(this));
|
||||
|
||||
@@ -25,6 +25,29 @@ var FormRenderer = BasicRenderer.extend({
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Focuses the field having attribute 'default_focus' set, if any, or the
|
||||
* first focusable field otherwise.
|
||||
*/
|
||||
autofocus: function () {
|
||||
if (this.mode === 'readonly') {
|
||||
return;
|
||||
}
|
||||
var focusWidget = this.defaultFocusField;
|
||||
if (!focusWidget || !focusWidget.isFocusable()) {
|
||||
var widgets = this.allFieldWidgets[this.state.id];
|
||||
for (var i = 0; i < (widgets ? widgets.length : 0); i++) {
|
||||
var widget = widgets[i];
|
||||
if (widget.isFocusable()) {
|
||||
focusWidget = widget;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (focusWidget) {
|
||||
focusWidget.activate({noselect: true});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Extend the method so that labels also receive the 'o_field_invalid' class
|
||||
* if necessary.
|
||||
@@ -183,7 +206,7 @@ var FormRenderer = BasicRenderer.extend({
|
||||
self.trigger_up('button_clicked', {
|
||||
attrs: node.attrs,
|
||||
record: self.state,
|
||||
show_wow: self.$el.hasClass('o_wow'), // TODO: implement this (in view)
|
||||
showWow: $el.hasClass('o_wow'),
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -709,7 +732,10 @@ var FormRenderer = BasicRenderer.extend({
|
||||
* @returns {jQueryElement}
|
||||
*/
|
||||
_renderTagSeparator: function (node) {
|
||||
return $('<div/>').addClass('o_horizontal_separator').text(node.attrs.string);
|
||||
var $separator = $('<div/>').addClass('o_horizontal_separator').text(node.attrs.string);
|
||||
this._handleAttributes($separator, node);
|
||||
this._registerModifiers(node, this.state, $separator);
|
||||
return $separator;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
@@ -769,20 +795,16 @@ var FormRenderer = BasicRenderer.extend({
|
||||
core.bus.trigger('DOM_updated');
|
||||
|
||||
// Attach the tooltips on the fields' label
|
||||
var focusWidget = this.defaultFocusField;
|
||||
_.each(this.allFieldWidgets[this.state.id], function (widget) {
|
||||
if (!focusWidget) {
|
||||
focusWidget = widget;
|
||||
}
|
||||
if (core.debug || widget.attrs.help || widget.field.help) {
|
||||
var idForLabel = self.idsForLabels[widget.name];
|
||||
var $label = idForLabel ? self.$('label[for=' + idForLabel + ']') : $();
|
||||
self._addFieldTooltip(widget, $label);
|
||||
}
|
||||
});
|
||||
if (focusWidget) {
|
||||
focusWidget.activate({noselect: true});
|
||||
}
|
||||
|
||||
// Focus the default field (in edit mode)
|
||||
self.autofocus();
|
||||
},
|
||||
/**
|
||||
* Sets id attribute of given widget to idForLabel
|
||||
|
||||
@@ -155,7 +155,7 @@ ListRenderer.include({
|
||||
var self = this;
|
||||
var rowIndex = _.findIndex(this.state.data, {id: recordID});
|
||||
if (rowIndex < 0) {
|
||||
return;
|
||||
return $.when();
|
||||
}
|
||||
var editMode = (mode === 'edit');
|
||||
var record = this.state.data[rowIndex];
|
||||
|
||||
@@ -4,6 +4,7 @@ odoo.define('web.view_dialogs', function (require) {
|
||||
var core = require('web.core');
|
||||
var data = require('web.data');
|
||||
var Dialog = require('web.Dialog');
|
||||
var dom = require('web.dom');
|
||||
var ListController = require('web.ListController');
|
||||
var ListView = require('web.ListView');
|
||||
var pyeval = require('web.pyeval');
|
||||
@@ -194,8 +195,10 @@ var FormViewDialog = ViewDialog.extend({
|
||||
if ($buttons.children().length) {
|
||||
self.$footer.empty().append($buttons.contents());
|
||||
}
|
||||
_super().$el.append(fragment);
|
||||
self.form_view.autofocus();
|
||||
dom.append(_super().$el, fragment, {
|
||||
callbacks: [{widget: self.form_view}],
|
||||
in_DOM: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -281,6 +284,7 @@ var SelectCreateDialog = ViewDialog.extend({
|
||||
if(this.options.initial_view !== "search") {
|
||||
return this.create_edit_record();
|
||||
}
|
||||
var self = this;
|
||||
var user_context = this.getSession().user_context;
|
||||
|
||||
var _super = this._super.bind(this);
|
||||
@@ -298,7 +302,10 @@ var SelectCreateDialog = ViewDialog.extend({
|
||||
this.loadViews(this.dataset.model, this.dataset.get_context(), [[false, 'list'], [false, 'search']], {})
|
||||
.then(this.setup.bind(this, search_defaults))
|
||||
.then(function (fragment) {
|
||||
_super().$el.append(fragment);
|
||||
dom.append(_super().$el, fragment, {
|
||||
callbacks: [{widget: self.list_controller}],
|
||||
in_DOM: true,
|
||||
});
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
@@ -42,6 +42,7 @@ var DateWidget = Widget.extend({
|
||||
locale : moment.locale(),
|
||||
allowInputToggle: true,
|
||||
keyBinds: null,
|
||||
widgetParent: 'body',
|
||||
});
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,11 @@ var modelFieldsCache = {
|
||||
cacheDefs: {},
|
||||
};
|
||||
|
||||
core.bus.on('clear_cache', null, function () {
|
||||
modelFieldsCache.cache = {};
|
||||
modelFieldsCache.cacheDefs = {};
|
||||
});
|
||||
|
||||
/**
|
||||
* The ModelFieldSelector widget can be used to display/select a particular
|
||||
* field chain from a given model.
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
.o-flex-flow(row, wrap);
|
||||
.o-align-items(center);
|
||||
height: 100%;
|
||||
min-height: 20px; // needed when there is no value in it and used standalone
|
||||
&:active, &:focus, &:active:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
> .o_field_selector_chain_part {
|
||||
padding: 0px 1px;
|
||||
|
||||
@@ -12,6 +12,7 @@ a {
|
||||
// General
|
||||
// ------------------------------------------------------------------
|
||||
.o_web_client {
|
||||
position: relative; // normally useless but required by bootstrap-datepicker
|
||||
background-color: @odoo-webclient-background-color;
|
||||
}
|
||||
.o_text_overflow {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
<div t-name="CrashManager.warning" class="o_dialog_warning">
|
||||
<t t-js="d">
|
||||
var message = d.message ? d.message : d.error.data.message;
|
||||
var message = (d.message !== undefined) ? d.message : d.error.data.message;
|
||||
d.html_error = context.engine.tools.html_escape(message).replace(/\n/g, '<br/>');
|
||||
</t>
|
||||
<t t-raw="html_error"/>
|
||||
@@ -836,11 +836,6 @@
|
||||
t-att-width="widget.node.attrs.img_width || widget.node.attrs.width"
|
||||
t-att-height="widget.node.attrs.img_height || widget.node.attrs.height"/>
|
||||
</t>
|
||||
<t t-name="AttachmentImage">
|
||||
<div class="o_attachment_image">
|
||||
<img t-att-src="'/web/image/' + widget.value + '?unique=1'"/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-name="FieldBinaryFile">
|
||||
<a t-if="widget.mode === 'readonly'" href="javascript:void(0)" class="o_form_uri"/>
|
||||
|
||||
|
||||
@@ -482,6 +482,183 @@ QUnit.module('relational_fields', {
|
||||
// assert.strictEqual(form.$('input').eq(1).val(), 'xphone', "onchange should have been applied");
|
||||
// });
|
||||
|
||||
QUnit.test('form: quick create then save directly', function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(5);
|
||||
|
||||
var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
|
||||
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
|
||||
|
||||
var def = $.Deferred();
|
||||
var newRecordID;
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<field name="trululu"/>' +
|
||||
'</form>',
|
||||
mockRPC: function (route, args) {
|
||||
var result = this._super.apply(this, arguments);
|
||||
if (args.method === 'name_create') {
|
||||
assert.step('name_create');
|
||||
return def.then(_.constant(result)).then(function (nameGet) {
|
||||
newRecordID = nameGet[0];
|
||||
return nameGet;
|
||||
});
|
||||
}
|
||||
if (args.method === 'create') {
|
||||
assert.step('create');
|
||||
assert.strictEqual(args.args[0].trululu, newRecordID,
|
||||
"should create with the correct m2o id");
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
|
||||
form.$('.o_field_many2one input').val('b').trigger('keydown');
|
||||
concurrency.delay(0).then(function () {
|
||||
$dropdown.find('li:first()').click(); // quick create 'b'
|
||||
form.$buttons.find('.o_form_button_save').click();
|
||||
|
||||
assert.verifySteps(['name_create'],
|
||||
"should wait for the name_create before creating the record");
|
||||
|
||||
def.resolve();
|
||||
|
||||
assert.verifySteps(['name_create', 'create']);
|
||||
|
||||
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
|
||||
form.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('list: quick create then save directly', function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(8);
|
||||
|
||||
var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
|
||||
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
|
||||
|
||||
var def = $.Deferred();
|
||||
var newRecordID;
|
||||
var list = createView({
|
||||
View: ListView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<tree editable="top">' +
|
||||
'<field name="trululu"/>' +
|
||||
'</tree>',
|
||||
mockRPC: function (route, args) {
|
||||
var result = this._super.apply(this, arguments);
|
||||
if (args.method === 'name_create') {
|
||||
assert.step('name_create');
|
||||
return def.then(_.constant(result)).then(function (nameGet) {
|
||||
newRecordID = nameGet[0];
|
||||
return nameGet;
|
||||
});
|
||||
}
|
||||
if (args.method === 'create') {
|
||||
assert.step('create');
|
||||
assert.strictEqual(args.args[0].trululu, newRecordID,
|
||||
"should create with the correct m2o id");
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
list.$buttons.find('.o_list_button_add').click();
|
||||
|
||||
var $dropdown = list.$('.o_field_many2one input').autocomplete('widget');
|
||||
list.$('.o_field_many2one input').val('b').trigger('keydown');
|
||||
concurrency.delay(0).then(function () {
|
||||
$dropdown.find('li:first()').click(); // quick create 'b'
|
||||
list.$buttons.find('.o_list_button_add').click();
|
||||
|
||||
assert.verifySteps(['name_create'],
|
||||
"should wait for the name_create before creating the record");
|
||||
assert.strictEqual(list.$('.o_data_row').length, 4,
|
||||
"should wait for the name_create before adding the new row");
|
||||
|
||||
def.resolve();
|
||||
|
||||
assert.verifySteps(['name_create', 'create']);
|
||||
assert.strictEqual(list.$('.o_data_row:nth(1) .o_data_cell').text(), 'b',
|
||||
"created row should have the correct m2o value");
|
||||
assert.strictEqual(list.$('.o_data_row').length, 5,
|
||||
"should have added the fifth row");
|
||||
|
||||
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
|
||||
list.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('list in form: quick create then save directly', function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(6);
|
||||
|
||||
var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
|
||||
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
|
||||
|
||||
var def = $.Deferred();
|
||||
var newRecordID;
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<sheet>' +
|
||||
'<field name="p">' +
|
||||
'<tree editable="bottom">' +
|
||||
'<field name="trululu"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
mockRPC: function (route, args) {
|
||||
var result = this._super.apply(this, arguments);
|
||||
if (args.method === 'name_create') {
|
||||
assert.step('name_create');
|
||||
return def.then(_.constant(result)).then(function (nameGet) {
|
||||
newRecordID = nameGet[0];
|
||||
return nameGet;
|
||||
});
|
||||
}
|
||||
if (args.method === 'create') {
|
||||
assert.step('create');
|
||||
assert.strictEqual(args.args[0].p[0][2].trululu, newRecordID,
|
||||
"should create with the correct m2o id");
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
|
||||
var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
|
||||
form.$('.o_field_many2one input').val('b').trigger('keydown');
|
||||
concurrency.delay(0).then(function () {
|
||||
$dropdown.find('li:first()').click(); // quick create 'b'
|
||||
form.$buttons.find('.o_form_button_save').click();
|
||||
|
||||
assert.verifySteps(['name_create'],
|
||||
"should wait for the name_create before creating the record");
|
||||
|
||||
def.resolve();
|
||||
|
||||
assert.verifySteps(['name_create', 'create']);
|
||||
assert.strictEqual(form.$('.o_data_row:first .o_data_cell').text(), 'b',
|
||||
"first row should have the correct m2o value");
|
||||
|
||||
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
|
||||
form.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('autocompletion in a many2one, in form view with a domain', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
@@ -3852,6 +4029,115 @@ QUnit.module('relational_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('editable list: value reset by an onchange', function (assert) {
|
||||
// this test reproduces a subtle behavior that may occur in a form view:
|
||||
// the user adds a record in a one2many field, and directly clicks on a
|
||||
// datetime field of the form view which has an onchange, which totally
|
||||
// overrides the value of the one2many (commands 5 and 0). The handler
|
||||
// that switches the edited row to readonly is then called after the
|
||||
// new value of the one2many field is applied (the one returned by the
|
||||
// onchange), so the row that must go to readonly doesn't exist anymore.
|
||||
assert.expect(2);
|
||||
|
||||
this.data.partner.onchanges = {
|
||||
datetime: function (obj) {
|
||||
obj.turtles = [[5], [0, 0, {display_name: 'new'}]];
|
||||
},
|
||||
};
|
||||
|
||||
var def;
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="datetime"/>' +
|
||||
'<field name="turtles">' +
|
||||
'<tree editable="bottom">' +
|
||||
'<field name="display_name"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
mockRPC: function (route, args) {
|
||||
var result = this._super.apply(this, arguments);
|
||||
if (args.method === 'onchange') {
|
||||
return $.when(def).then(_.constant(result));
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
// trigger the two onchanges
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
form.$('.o_data_row .o_field_widget').val('a name').trigger('input');
|
||||
def = $.Deferred();
|
||||
form.$('.o_datepicker_input').click(); // focusout o2m and set value to today
|
||||
|
||||
// resolve the onchange def
|
||||
def.resolve();
|
||||
|
||||
assert.strictEqual(form.$('.o_data_row').length, 1,
|
||||
"should have one record in the o2m");
|
||||
assert.strictEqual(form.$('.o_data_row .o_data_cell').text(), 'new',
|
||||
"should be the record created by the onchange");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('editable list: onchange that returns a warning', function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
this.data.turtle.onchanges = {
|
||||
display_name: function () {},
|
||||
};
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="turtles">' +
|
||||
'<tree editable="bottom">' +
|
||||
'<field name="display_name"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'onchange') {
|
||||
assert.step(args.method);
|
||||
return $.when({
|
||||
value: {},
|
||||
warning: {
|
||||
title: "Warning",
|
||||
message: "You must first select a partner"
|
||||
},
|
||||
});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
intercepts: {
|
||||
warning: function () {
|
||||
assert.step('warning');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// add a line (this should trigger an onchange and a warning)
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
|
||||
// check if 'Add an item' still works (this should trigger an onchange
|
||||
// and a warning again)
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
|
||||
assert.verifySteps(['onchange', 'warning', 'onchange', 'warning']);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.module('FieldMany2Many');
|
||||
|
||||
QUnit.test('many2many kanban: edition', function (assert) {
|
||||
|
||||
@@ -13,6 +13,7 @@ odoo.define('web.test_utils', function (require) {
|
||||
var basic_fields = require('web.basic_fields');
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var dom = require('web.dom');
|
||||
var session = require('web.session');
|
||||
var MockServer = require('web.MockServer');
|
||||
var Widget = require('web.Widget');
|
||||
@@ -172,7 +173,14 @@ function createAsyncView(params) {
|
||||
widget.destroy();
|
||||
$('#qunit-fixture').off('DOMNodeInserted.removeSRC');
|
||||
};
|
||||
return view.appendTo($view_manager).then(function () {
|
||||
// render the view in a fragment as they must be able to render correctly
|
||||
// without being in the DOM
|
||||
var fragment = document.createDocumentFragment();
|
||||
return view.appendTo(fragment).then(function () {
|
||||
dom.append($view_manager, fragment, {
|
||||
callbacks: [{widget: view}],
|
||||
in_DOM: true,
|
||||
});
|
||||
view.$el.on('click', 'a', function (ev) {
|
||||
ev.preventDefault();
|
||||
});
|
||||
@@ -244,52 +252,53 @@ function addMockEnvironment(widget, params) {
|
||||
logLevel: params.logLevel,
|
||||
currentDate: params.currentDate,
|
||||
});
|
||||
var widgetDestroy;
|
||||
|
||||
// make sure the debounce value for input fields is set to 0
|
||||
var initialDebounce = DebouncedField.prototype.DEBOUNCE;
|
||||
DebouncedField.prototype.DEBOUNCE = params.fieldDebounce || 0;
|
||||
|
||||
var initialSession, initialConfig, initialParameters;
|
||||
if ('session' in params) {
|
||||
var initialSession = _.extend({}, session);
|
||||
initialSession = _.extend({}, session);
|
||||
_.extend(session, params.session);
|
||||
widgetDestroy = widget.destroy;
|
||||
widget.destroy = function () {
|
||||
for (var key in session) {
|
||||
}
|
||||
if ('config' in params) {
|
||||
initialConfig = _.extend({}, config);
|
||||
_.extend(config, params.config);
|
||||
}
|
||||
if ('translateParameters' in params) {
|
||||
initialParameters = _.extend({}, core._t.database.parameters);
|
||||
_.extend(core._t.database.parameters, params.translateParameters);
|
||||
}
|
||||
|
||||
var widgetDestroy = widget.destroy;
|
||||
widget.destroy = function () {
|
||||
// clear the caches (e.g. data_manager, ModelFieldSelector) when the
|
||||
// widget is destroyed, at the end of each test to avoid collisions
|
||||
core.bus.trigger('clear_cache');
|
||||
|
||||
DebouncedField.prototype.DEBOUNCE = initialDebounce;
|
||||
|
||||
var key;
|
||||
if ('session' in params) {
|
||||
for (key in session) {
|
||||
delete session[key];
|
||||
}
|
||||
_.extend(session, initialSession);
|
||||
widgetDestroy.call(this);
|
||||
};
|
||||
}
|
||||
|
||||
if ('config' in params) {
|
||||
var initialConfig = _.extend({}, config);
|
||||
_.extend(config, params.config);
|
||||
widgetDestroy = widget.destroy;
|
||||
widget.destroy = function () {
|
||||
for (var key in config) {
|
||||
}
|
||||
if ('config' in params) {
|
||||
for (key in config) {
|
||||
delete config[key];
|
||||
}
|
||||
_.extend(config, initialConfig);
|
||||
DebouncedField.prototype.DEBOUNCE = initialDebounce;
|
||||
widgetDestroy.call(this);
|
||||
};
|
||||
}
|
||||
|
||||
if ('translateParameters' in params) {
|
||||
var initialParameters = _.extend({}, core._t.database.parameters);
|
||||
_.extend(core._t.database.parameters, params.translateParameters);
|
||||
var oldDestroy = widget.destroy;
|
||||
widget.destroy = function () {
|
||||
for (var key in core._t.database.parameters) {
|
||||
}
|
||||
if ('translateParameters' in params) {
|
||||
for (key in core._t.database.parameters) {
|
||||
delete core._t.database.parameters[key];
|
||||
}
|
||||
_.extend(core._t.database.parameters, initialParameters);
|
||||
oldDestroy.call(this);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
widgetDestroy.call(this);
|
||||
};
|
||||
|
||||
intercept(widget, 'call_service', function (event) {
|
||||
if (event.data.service === 'ajax') {
|
||||
|
||||
@@ -1165,6 +1165,54 @@ QUnit.module('Views', {
|
||||
model.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('default_get: fetch x2manys inside x2manys', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
this.data.partner.fields.o2m = {
|
||||
string: "O2M", type: 'one2many', relation: 'partner', default: [[6, 0, [1]]],
|
||||
};
|
||||
|
||||
var model = createModel({
|
||||
Model: BasicModel,
|
||||
data: this.data,
|
||||
});
|
||||
|
||||
var params = {
|
||||
fieldNames: ['o2m'],
|
||||
fields: this.data.partner.fields,
|
||||
fieldsInfo: {
|
||||
form: {
|
||||
o2m: {
|
||||
relatedFields: this.data.partner.fields,
|
||||
fieldsInfo: {
|
||||
list: {
|
||||
category: {
|
||||
relatedFields: { display_name: {} },
|
||||
},
|
||||
},
|
||||
},
|
||||
viewType: 'list',
|
||||
},
|
||||
},
|
||||
},
|
||||
modelName: 'partner',
|
||||
type: 'record',
|
||||
viewType: 'form',
|
||||
};
|
||||
|
||||
model.load(params).then(function (resultID) {
|
||||
var record = model.get(resultID);
|
||||
assert.strictEqual(record.data.o2m.count, 1, "o2m field should contain 1 record");
|
||||
var categoryList = record.data.o2m.data[0].data.category;
|
||||
assert.strictEqual(categoryList.count, 1,
|
||||
"category field should contain 1 record");
|
||||
assert.strictEqual(categoryList.data[0].data.display_name,
|
||||
'gold', "category records should have been fetched");
|
||||
});
|
||||
|
||||
model.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('contexts and domains can be properly fetched', function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
@@ -1448,6 +1496,7 @@ QUnit.module('Views', {
|
||||
fieldsInfo: {
|
||||
form: {
|
||||
product_ids: {
|
||||
relatedFields: this.data.product.fields,
|
||||
fieldsInfo: { list: { name: {}, date: {} } },
|
||||
viewType: 'list',
|
||||
}
|
||||
|
||||
@@ -837,6 +837,28 @@ QUnit.module('Views', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('invisible attrs on separators', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<group>' +
|
||||
'<separator string="Geolocation" attrs=\'{"invisible": [["bar", "=", True]]}\'/>'+
|
||||
'<field name="bar"/>' +
|
||||
'</group>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
});
|
||||
assert.strictEqual(form.$('div.o_horizontal_separator').hasClass('o_invisible_modifier'), true,
|
||||
"separator div should be hidden");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('buttons in form view', function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
@@ -3770,7 +3792,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
|
||||
assert.strictEqual(form.$('input[name="foo"]')[0], document.activeElement,
|
||||
assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0],
|
||||
"foo field should have focus");
|
||||
assert.strictEqual(form.$('input[name="foo"]')[0].selectionStart, 3,
|
||||
"cursor should be at the end");
|
||||
@@ -3792,7 +3814,25 @@ QUnit.module('Views', {
|
||||
res_id: 1,
|
||||
});
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
assert.strictEqual(form.$('input[name="foo"]')[0], document.activeElement,
|
||||
assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0],
|
||||
"foo field should have focus");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('in create mode, autofocus fields are focused', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="int_field"/>' +
|
||||
'<field name="foo" default_focus="1"/>' +
|
||||
'</form>',
|
||||
});
|
||||
assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0],
|
||||
"foo field should have focus");
|
||||
|
||||
form.destroy();
|
||||
@@ -3821,6 +3861,24 @@ QUnit.module('Views', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('autofocus first visible field', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="int_field" invisible="1"/>' +
|
||||
'<field name="foo"/>' +
|
||||
'</form>',
|
||||
});
|
||||
assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0],
|
||||
"foo field should have focus");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('open one2many form containing many2many_tags', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
@@ -4893,5 +4951,40 @@ QUnit.module('Views', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('wow effect', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
this.data.partner.fields.model_name = { string: "Model name", type: "char" };
|
||||
|
||||
var clickCount = 0;
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<button name="test" string="Mark Won" type="object" class="o_wow"/>' +
|
||||
'</form>',
|
||||
intercepts: {
|
||||
execute_action: function (event) {
|
||||
if (clickCount === 0) {
|
||||
event.data.on_success();
|
||||
} else {
|
||||
event.data.on_fail();
|
||||
}
|
||||
clickCount++;
|
||||
},
|
||||
show_wow: function () {
|
||||
assert.step('wow');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
form.$('button.o_wow').click();
|
||||
assert.verifySteps(['wow']);
|
||||
form.$('button.o_wow').click();
|
||||
assert.verifySteps(['wow']);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -84,7 +84,7 @@ QUnit.module('Views', {
|
||||
QUnit.module('ListView');
|
||||
|
||||
QUnit.test('simple readonly list', function (assert) {
|
||||
assert.expect(9);
|
||||
assert.expect(10);
|
||||
|
||||
var list = createView({
|
||||
View: ListView,
|
||||
@@ -93,6 +93,9 @@ QUnit.module('Views', {
|
||||
arch: '<tree><field name="foo"/><field name="int_field"/></tree>',
|
||||
});
|
||||
|
||||
assert.notOk(list.$el.hasClass('o_cannot_create'),
|
||||
"should not have className 'o_cannot_create'");
|
||||
|
||||
// 3 th (1 for checkbox, 2 for columns)
|
||||
assert.strictEqual(list.$('th').length, 3, "should have 3 columns");
|
||||
|
||||
@@ -114,6 +117,24 @@ QUnit.module('Views', {
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('list with create="0"', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var list = createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree create="0"><field name="foo"/></tree>',
|
||||
});
|
||||
|
||||
assert.ok(list.$el.hasClass('o_cannot_create'),
|
||||
"should have className 'o_cannot_create'");
|
||||
assert.strictEqual(list.$buttons.find('.o_list_button_add').length, 0,
|
||||
"should not have the 'Create' button");
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('simple editable rendering', function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
|
||||
@@ -397,9 +397,10 @@ class Website(models.Model):
|
||||
|
||||
@api.model
|
||||
def get_current_website(self):
|
||||
domain_name = request.httprequest.environ.get('HTTP_HOST', '').split(':')[0]
|
||||
domain_name = request and request.httprequest.environ.get('HTTP_HOST', '').split(':')[0] or None
|
||||
website_id = self._get_current_website_id(domain_name)
|
||||
request.context = dict(request.context, website_id=website_id)
|
||||
if request:
|
||||
request.context = dict(request.context, website_id=website_id)
|
||||
return self.browse(website_id)
|
||||
|
||||
@tools.cache('domain_name')
|
||||
|
||||
@@ -3,18 +3,23 @@ odoo.define('website.ace', function (require) {
|
||||
|
||||
var Widget = require('web.Widget');
|
||||
var website = require('website.website');
|
||||
var mixins = require('web.mixins');
|
||||
|
||||
var ViewEditor = require('web_editor.ace');
|
||||
|
||||
var hash = "#advanced-view-editor";
|
||||
|
||||
var Ace = Widget.extend({
|
||||
var Ace = Widget.extend(mixins.ServiceProvider, {
|
||||
events: {
|
||||
"click a[data-action=ace]": function (e) {
|
||||
e.preventDefault();
|
||||
this.launchAce();
|
||||
},
|
||||
},
|
||||
init: function () {
|
||||
mixins.ServiceProvider.init.apply(this, arguments);
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
start: function () {
|
||||
if (window.location.hash.substr(0, hash.length) === hash) {
|
||||
this.launchAce();
|
||||
|
||||
@@ -6,6 +6,7 @@ var ajax = require('web.ajax');
|
||||
var Class = require('web.Class');
|
||||
var Dialog = require('web.Dialog');
|
||||
var mixins = require('web.mixins');
|
||||
var rpc = require("web.rpc");
|
||||
var Widget = require('web.Widget');
|
||||
var base = require('web_editor.base');
|
||||
var website = require('website.website');
|
||||
@@ -490,22 +491,21 @@ var Configurator = Dialog.extend({
|
||||
def.resolve(null);
|
||||
} else {
|
||||
var fields = ['website_meta_title', 'website_meta_description', 'website_meta_keywords'];
|
||||
this._rpc({
|
||||
model: obj.model,
|
||||
method: 'read',
|
||||
args: [[obj.id], fields, base.get_context()],
|
||||
})
|
||||
.then(function (data) {
|
||||
if (data.length) {
|
||||
var meta = data[0];
|
||||
meta.model = obj.model;
|
||||
def.resolve(meta);
|
||||
} else {
|
||||
def.resolve(null);
|
||||
}
|
||||
}).fail(function () {
|
||||
def.reject();
|
||||
});
|
||||
rpc.query({
|
||||
model: obj.model,
|
||||
method: 'read',
|
||||
args: [[obj.id], fields, base.get_context()],
|
||||
}).then(function (data) {
|
||||
if (data.length) {
|
||||
var meta = data[0];
|
||||
meta.model = obj.model;
|
||||
def.resolve(meta);
|
||||
} else {
|
||||
def.resolve(null);
|
||||
}
|
||||
}).fail(function () {
|
||||
def.reject();
|
||||
});
|
||||
}
|
||||
return def;
|
||||
},
|
||||
@@ -514,11 +514,11 @@ var Configurator = Dialog.extend({
|
||||
if (!obj) {
|
||||
return $.Deferred().reject();
|
||||
} else {
|
||||
return this._rpc({
|
||||
model: obj.model,
|
||||
method: 'write',
|
||||
args: [[obj.id], data, base.get_context()],
|
||||
});
|
||||
return rpc.query({
|
||||
model: obj.model,
|
||||
method: 'write',
|
||||
args: [[obj.id], data, base.get_context()],
|
||||
});
|
||||
}
|
||||
},
|
||||
titleChanged: function () {
|
||||
|
||||
@@ -48,8 +48,6 @@ class CrmLead(models.Model):
|
||||
for salesman_id, leads_ids in pycompat.items(salesmans_leads):
|
||||
leads = self.browse(leads_ids)
|
||||
leads.write({'user_id': salesman_id})
|
||||
for lead in leads:
|
||||
lead._onchange_user_id()
|
||||
|
||||
@api.multi
|
||||
def action_assign_partner(self):
|
||||
|
||||
@@ -75,7 +75,7 @@ tour.register('event_buy_tickets', {
|
||||
trigger: 'a[href="/shop/confirm_order"]:contains("Confirm")',
|
||||
},
|
||||
{
|
||||
content: "Check that the subtotal is 5,500.00",
|
||||
content: "Check that the subtotal is 5,500.00 USD", // this test will fail if the currency of the main company is not USD
|
||||
trigger: '#order_total_untaxed .oe_currency_value:contains("5,500.00")',
|
||||
run: function () {}, // it's a check
|
||||
},
|
||||
|
||||
@@ -604,6 +604,7 @@ msgid "Bad Request"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.model.fields,field_description:website_forum.field_res_users_badge_ids
|
||||
#: model:ir.ui.view,arch_db:website_forum.badge
|
||||
#: model:ir.ui.view,arch_db:website_forum.header
|
||||
#: model:ir.ui.view,arch_db:website_forum.user_detail_full
|
||||
@@ -782,6 +783,7 @@ msgstr ""
|
||||
#. openerp-web
|
||||
#: code:addons/website_forum/static/src/xml/website_forum_share_templates.xml:46
|
||||
#: selection:forum.post,state:0
|
||||
#: model:ir.ui.view,arch_db:website_forum.post_description_full
|
||||
#, python-format
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
@@ -883,6 +885,16 @@ msgstr ""
|
||||
msgid "Convert all answers to comments and vice versa"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.ui.view,arch_db:website_forum.post_answers
|
||||
msgid "Convert as a comment"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.ui.view,arch_db:website_forum.post_comment
|
||||
msgid "Convert as an answer"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.model.fields,field_description:website_forum.field_forum_post_karma_accept
|
||||
msgid "Convert comment to answer"
|
||||
@@ -968,6 +980,12 @@ msgstr ""
|
||||
msgid "Default Post"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.ui.view,arch_db:website_forum.post_answers
|
||||
#: model:ir.ui.view,arch_db:website_forum.post_description_full
|
||||
msgid "Delete"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.model.fields,field_description:website_forum.field_forum_forum_karma_unlink_all
|
||||
msgid "Delete all posts"
|
||||
@@ -1038,6 +1056,8 @@ msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.ui.view,arch_db:website_forum.edit_post
|
||||
#: model:ir.ui.view,arch_db:website_forum.post_answers
|
||||
#: model:ir.ui.view,arch_db:website_forum.post_description_full
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
@@ -1184,6 +1204,11 @@ msgstr ""
|
||||
msgid "First upvote"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.ui.view,arch_db:website_forum.post_description_full
|
||||
msgid "Flag"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.model.fields,field_description:website_forum.field_forum_forum_karma_flag
|
||||
#: model:ir.model.fields,field_description:website_forum.field_forum_post_karma_flag
|
||||
@@ -1193,6 +1218,7 @@ msgstr ""
|
||||
#. module: website_forum
|
||||
#: selection:forum.post,state:0
|
||||
#: model:ir.ui.view,arch_db:website_forum.moderation_queue
|
||||
#: model:ir.ui.view,arch_db:website_forum.post_description_full
|
||||
msgid "Flagged"
|
||||
msgstr ""
|
||||
|
||||
@@ -2015,6 +2041,11 @@ msgstr ""
|
||||
msgid "Re: %s"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.ui.view,arch_db:website_forum.post_description_full
|
||||
msgid "Reactivate"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.ui.view,arch_db:website_forum.header
|
||||
msgid "Read Guidelines"
|
||||
@@ -2067,6 +2098,11 @@ msgstr ""
|
||||
msgid "Relevance Computation"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.ui.view,arch_db:website_forum.post_description_full
|
||||
msgid "Reopen"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.ui.view,arch_db:website_forum.post_answers
|
||||
#: model:ir.ui.view,arch_db:website_forum.post_description_full
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<t t-name="website_forum.social_message_answer">
|
||||
<p>By sharing you answer, you will get additional <b>karma points</b> if your
|
||||
answer is selected as the right one. See what you can do with karma
|
||||
<a href="https://www.odoo.com/forum/how-to/miscellaneous-14/how-does-karma-work-odoo-v8-forum-software-57845" target="_blank">here</a>.</p>
|
||||
<a href="/forum/help-1/faq" target="_blank">here</a>.</p>
|
||||
</t>
|
||||
<t t-name="website_forum.social_message_default">
|
||||
<p>Share this content to increase your chances to be featured on the front page and attract more visitors.</p>
|
||||
|
||||
@@ -793,9 +793,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted mb16">
|
||||
<i t-if="question.post_type == 'question'" t-attf-class="fa fa-question-circle" t-att-title="question.post_type.capitalize()" />
|
||||
<i t-if="question.post_type == 'discussion'" t-attf-class="fa fa-comments-o" t-att-title="question.post_type.capitalize()" />
|
||||
<i t-if="question.post_type == 'link'" t-attf-class="fa fa-link" t-att-title="question.post_type.capitalize()" />
|
||||
<t t-set="type_labels" t-value="dict(question._fields['post_type']._description_selection(question.env))"/>
|
||||
<i t-if="question.post_type == 'question'" t-attf-class="fa fa-question-circle" t-att-title="type_labels[question.post_type]" />
|
||||
<i t-if="question.post_type == 'discussion'" t-attf-class="fa fa-comments-o" t-att-title="type_labels[question.post_type]" />
|
||||
<i t-if="question.post_type == 'link'" t-attf-class="fa fa-link" t-att-title="type_labels[question.post_type]" />
|
||||
<span>By </span>
|
||||
<a t-att-href="(question.create_uid.id == request.session.uid or question.create_uid.sudo().website_published) and '/forum/%s/user/%s' % (slug(forum), question.create_uid.id) or None"
|
||||
t-field="question.create_uid"
|
||||
@@ -819,7 +820,7 @@
|
||||
<div class="mt16 mb24 text-center">
|
||||
<t t-call="website_forum.link_button">
|
||||
<t t-set="url" t-value="'/forum/' + slug(forum) + '/question/' + slug(question) + '/reopen'"/>
|
||||
<t t-set="label" t-value="'Reopen'"/>
|
||||
<t t-set="label">Reopen</t>
|
||||
<t t-set="classes" t-value="'fa-arrow-right'"/>
|
||||
<t t-set="karma" t-value="not question.can_close and question.karma_close or 0"/>
|
||||
</t>
|
||||
@@ -832,7 +833,7 @@
|
||||
<li t-if="question.state == 'close'">
|
||||
<t t-call="website_forum.link_button">
|
||||
<t t-set="url" t-value="'/forum/' + slug(forum) +'/question/' + slug(question) + '/reopen'"/>
|
||||
<t t-set="label" t-value="'Reopen'"/>
|
||||
<t t-set="label">Reopen</t>
|
||||
<t t-set="classes" t-value="'fa-undo'"/>
|
||||
<t t-set="karma" t-value="not question.can_close and question.karma_close or 0"/>
|
||||
</t>
|
||||
@@ -840,7 +841,7 @@
|
||||
<li>
|
||||
<t t-call="website_forum.link_button">
|
||||
<t t-set="url" t-value="'/forum/' + slug(forum) +'/post/' + slug(question) + '/edit'"/>
|
||||
<t t-set="label" t-value="'Edit'"/>
|
||||
<t t-set="label">Edit</t>
|
||||
<t t-set="classes" t-value="'fa-pencil-square-o'"/>
|
||||
<t t-set="karma" t-value="not question.can_edit and question.karma_edit or 0"/>
|
||||
</t>
|
||||
@@ -848,7 +849,7 @@
|
||||
<li t-if="question.state != 'close'">
|
||||
<t t-call="website_forum.link_button">
|
||||
<t t-set="url" t-value="'/forum/' + slug(forum) +'/question/' + slug(question) + '/ask_for_close'"/>
|
||||
<t t-set="label" t-value="'Close'"/>
|
||||
<t t-set="label">Close</t>
|
||||
<t t-set="classes" t-value="'fa-times'"/>
|
||||
<t t-set="karma" t-value="not question.can_close and question.karma_close or 0"/>
|
||||
</t>
|
||||
@@ -856,7 +857,7 @@
|
||||
<li t-if="question.active">
|
||||
<t t-call="website_forum.link_button">
|
||||
<t t-set="url" t-value="'/forum/' + slug(forum) +'/question/' + slug(question) + '/delete'"/>
|
||||
<t t-set="label" t-value="'Delete'"/>
|
||||
<t t-set="label">Delete</t>
|
||||
<t t-set="classes" t-value="'fa-trash-o'"/>
|
||||
<t t-set="karma" t-value="not question.can_unlink and question.karma_unlink or 0"/>
|
||||
</t>
|
||||
@@ -864,7 +865,7 @@
|
||||
<li t-if="not question.active and question.state != 'offensive'">
|
||||
<t t-call="website_forum.link_button">
|
||||
<t t-set="url" t-value="'/forum/' + slug(forum) +'/question/' + slug(question) + '/undelete'"/>
|
||||
<t t-set="label" t-value="'Reactivate'"/>
|
||||
<t t-set="label">Reactivate</t>
|
||||
<t t-set="classes" t-value="'fa-trash-o'"/>
|
||||
<t t-set="karma" t-value="not question.can_unlink and question.karma_unlink or 0"/>
|
||||
</t>
|
||||
@@ -872,7 +873,7 @@
|
||||
<li t-if="not question.active and question.state == 'offensive'">
|
||||
<t t-call="website_forum.link_button">
|
||||
<t t-set="url" t-value="'/forum/' + slug(forum) +'/post/' + slug(question) + '/validate'"/>
|
||||
<t t-set="label" t-value="'Reactivate'"/>
|
||||
<t t-set="label">Reactivate</t>
|
||||
<t t-set="classes" t-value="'fa-trash-o'"/>
|
||||
<t t-set="karma" t-value="not question.can_moderate and question.forum_id.karma_moderate or 0"/>
|
||||
</t>
|
||||
@@ -883,7 +884,12 @@
|
||||
t-attf-data-karma="#{not question.can_flag and question.forum_id.karma_flag or 0}"
|
||||
t-attf-data-href="/forum/#{slug(forum)}/post/#{slug(question)}/flag"
|
||||
>
|
||||
<t class="flag_flagged_label" t-esc="question.state == 'flagged' and 'Flagged' or 'Flag'"/>
|
||||
<t t-if="question.state == 'flagged'">
|
||||
Flagged
|
||||
</t>
|
||||
<t t-if="question.state != 'flagged'">
|
||||
Flag
|
||||
</t>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -982,7 +988,7 @@
|
||||
<li>
|
||||
<t t-call="website_forum.link_button">
|
||||
<t t-set="url" t-value="'/forum/' + slug(forum) + '/post/' + slug(answer) + '/edit'"/>
|
||||
<t t-set="label" t-value="'Edit'"/>
|
||||
<t t-set="label">Edit</t>
|
||||
<t t-set="classes" t-value="'fa fa-pencil-square-o'"/>
|
||||
<t t-set="karma" t-value="not answer.can_edit and answer.karma_edit or 0"/>
|
||||
</t>
|
||||
@@ -990,7 +996,7 @@
|
||||
<li>
|
||||
<t t-call="website_forum.link_button">
|
||||
<t t-set="url" t-value="'/forum/' + slug(forum) + '/post/' + slug(answer) + '/delete'"/>
|
||||
<t t-set="label" t-value="'Delete'"/>
|
||||
<t t-set="label">Delete</t>
|
||||
<t t-set="classes" t-value="'fa-trash-o'"/>
|
||||
<t t-set="karma" t-value="not answer.can_unlink and answer.karma_unlink or 0"/>
|
||||
</t>
|
||||
@@ -1007,7 +1013,7 @@
|
||||
<li t-if="question.post_type == 'question'">
|
||||
<t t-call="website_forum.link_button">
|
||||
<t t-set="url" t-value="'/forum/' + slug(forum) + '/post/' + slug(answer) + '/convert_to_comment'"/>
|
||||
<t t-set="label" t-value="'Convert as a comment'"/>
|
||||
<t t-set="label">Convert as a comment</t>
|
||||
<t t-set="classes" t-value="'fa-magic'"/>
|
||||
<t t-set="karma" t-value="not answer.can_comment_convert and answer.karma_comment_convert or 0"/>
|
||||
</t>
|
||||
@@ -1081,7 +1087,7 @@
|
||||
<t t-set="required_karma" t-value="message.author_id.id == user.partner_id.id and object.forum_id.karma_comment_unlink_own or object.forum_id.karma_comment_unlink_all"/>
|
||||
<t t-call="website_forum.link_button">
|
||||
<t t-set="url" t-value="'/forum/' + slug(forum) + '/post/' + slug(object) + '/comment/' + slug(message) + '/delete'"/>
|
||||
<t t-set="label" t-value="' '"/>
|
||||
<t t-set="label"> </t>
|
||||
<t t-set="karma" t-value="user.karma<required_karma and required_karma or 0"/>
|
||||
<t t-set="classes" t-value="'close comment_delete fa-times'"/>
|
||||
</t>
|
||||
@@ -1094,7 +1100,7 @@
|
||||
<t t-if="allow_post_comment">
|
||||
<t t-call="website_forum.link_button" >
|
||||
<t t-set="url" t-value="'/forum/' + slug(forum) + '/post/' + slug(object) + '/comment/' + slug(message) + '/convert_to_answer'"/>
|
||||
<t t-set="label" t-value="'Convert as an answer'"/>
|
||||
<t t-set="label">Convert as an answer</t>
|
||||
<t t-set="karma" t-value="user.karma<required_karma and required_karma or 0"/>
|
||||
<t t-set="classes" t-value="'fa-magic pull-right'"/>
|
||||
</t>
|
||||
|
||||
@@ -77,19 +77,19 @@ class Website(models.Model):
|
||||
:param bool show_visible: if True, we don't display pricelist where selectable is False (Eg: Code promo)
|
||||
:returns: pricelist recordset
|
||||
"""
|
||||
website = request.website
|
||||
if not request.website:
|
||||
website = request and request.website or None
|
||||
if not website:
|
||||
if self.env.context.get('website_id'):
|
||||
website = self.browse(self.env.context['website_id'])
|
||||
else:
|
||||
website = self.search([], limit=1)
|
||||
isocountry = request.session.geoip and request.session.geoip.get('country_code') or False
|
||||
isocountry = request and request.session.geoip and request.session.geoip.get('country_code') or False
|
||||
partner = self.env.user.partner_id
|
||||
order_pl = partner.last_website_so_id and partner.last_website_so_id.state == 'draft' and partner.last_website_so_id.pricelist_id
|
||||
partner_pl = partner.property_product_pricelist
|
||||
pricelists = website._get_pl_partner_order(isocountry, show_visible,
|
||||
website.user_id.sudo().partner_id.property_product_pricelist.id,
|
||||
request.session.get('website_sale_current_pl'),
|
||||
request and request.session.get('website_sale_current_pl') or None,
|
||||
website.pricelist_ids,
|
||||
partner_pl=partner_pl and partner_pl.id or None,
|
||||
order_pl=order_pl and order_pl.id or None)
|
||||
@@ -113,7 +113,7 @@ class Website(models.Model):
|
||||
available_pricelists = self.get_pricelist_available()
|
||||
pl = None
|
||||
partner = self.env.user.partner_id
|
||||
if request.session.get('website_sale_current_pl'):
|
||||
if request and request.session.get('website_sale_current_pl'):
|
||||
# `website_sale_current_pl` is set only if the user specifically chose it:
|
||||
# - Either, he chose it from the pricelist selection
|
||||
# - Either, he entered a coupon code
|
||||
|
||||
Reference in New Issue
Block a user