[MERGE] forward port branch saas-16 up to cc38b93ab2

This commit is contained in:
Christophe Simonis
2017-06-09 17:11:47 +02:00
60 changed files with 1051 additions and 197 deletions
+4 -1
View File
@@ -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:
+17 -12
View File
@@ -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))
+8 -5
View File
@@ -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):
+1
View File
@@ -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. """
+20 -20
View File
@@ -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
+5 -2
View File
@@ -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)])
+1 -1
View File
@@ -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"/>
+5 -2
View File
@@ -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')
+16 -7
View File
@@ -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();
});
}
+1 -1
View File
@@ -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">
+3
View File
@@ -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)
+6
View File
@@ -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
+3
View File
@@ -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):
+6
View File
@@ -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)
+5 -1
View File
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
78 access_stock_forecast_user report.stock.forecast.user model_report_stock_forecast stock.group_stock_user 1 0 0 0
79 access_stock_forecast_manager report.stock.forecast.manager model_report_stock_forecast stock.group_stock_manager 1 1 1 1
80 access_stock_scrap_user stock.scrap.user model_stock_scrap stock.group_stock_user 1 1 1 0
81 access_procurement_order procurement.order stock.model_procurement_order base.group_portal 1 0 0 0
82 access_product_attribute_manager product.attribute manager product.model_product_attribute stock.group_stock_manager 1 1 1 1
83 access_product_attribute_value_manager product.attribute manager value product.model_product_attribute_value stock.group_stock_manager 1 1 1 1
84 access_product_attribute_price_manager product.attribute manager price product.model_product_attribute_price stock.group_stock_manager 1 1 1 1
85 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', '&lt;=', 0.99)]}" invisible="not context.get('show_reserved') or context.get('state_done')"/>
+3
View File
@@ -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({
@@ -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];
+10 -3
View File
@@ -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 {
+1 -6
View File
@@ -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) {
+41 -32
View File
@@ -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',
}
+95 -2
View File
@@ -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();
});
});
});
+22 -1
View File
@@ -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);
+3 -2
View File
@@ -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')
+6 -1
View File
@@ -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();
+21 -21
View File
@@ -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>
+22 -16
View File
@@ -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&lt;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&lt;required_karma and required_karma or 0"/>
<t t-set="classes" t-value="'fa-magic pull-right'"/>
</t>
+5 -5
View File
@@ -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