[MERGE] forward port branch saas-11.4 up to 3c108977c1
This commit is contained in:
@@ -381,7 +381,7 @@ class AccountBankStatementLine(models.Model):
|
||||
####################################################
|
||||
|
||||
def _get_common_sql_query(self, overlook_partner = False, excluded_ids = None, split = False):
|
||||
acc_type = "acc.internal_type IN ('payable', 'receivable')" if (self.partner_id or overlook_partner) else "acc.reconcile = true"
|
||||
acc_type = "acc.reconcile = true"
|
||||
select_clause = "SELECT aml.id "
|
||||
from_clause = "FROM account_move_line aml JOIN account_account acc ON acc.id = aml.account_id "
|
||||
account_clause = ''
|
||||
|
||||
@@ -226,7 +226,7 @@ class AccountInvoice(models.Model):
|
||||
for line in self.move_id.line_ids.filtered(lambda l: l.account_id.id == self.account_id.id):
|
||||
payment_lines.update(line.mapped('matched_credit_ids.credit_move_id.id'))
|
||||
payment_lines.update(line.mapped('matched_debit_ids.debit_move_id.id'))
|
||||
self.payment_move_line_ids = self.env['account.move.line'].browse(list(payment_lines))
|
||||
self.payment_move_line_ids = self.env['account.move.line'].browse(list(payment_lines)).sorted()
|
||||
|
||||
name = fields.Char(string='Reference/Description', index=True,
|
||||
readonly=True, states={'draft': [('readonly', False)]}, copy=False, help='The name that will be used on account move lines')
|
||||
|
||||
@@ -18,18 +18,29 @@ class ReportAgedPartnerBalance(models.AbstractModel):
|
||||
# Do an invoice and a payment and unreconcile. The amount will be nullified
|
||||
# By default, the partner wouldn't appear in this report.
|
||||
# The context key allow it to appear
|
||||
# In case of a period_length of 30 days as of 2019-02-08, we want the following periods:
|
||||
# Name Stop Start
|
||||
# 1 - 30 : 2019-02-07 - 2019-01-09
|
||||
# 31 - 60 : 2019-01-08 - 2018-12-10
|
||||
# 61 - 90 : 2018-12-09 - 2018-11-10
|
||||
# 91 - 120 : 2018-11-09 - 2018-10-11
|
||||
# +120 : 2018-10-10
|
||||
ctx = self._context
|
||||
periods = {}
|
||||
date_from = fields.Date.from_string(date_from)
|
||||
start = date_from - relativedelta(days=1)
|
||||
for i in range(5)[::-1]:
|
||||
stop = start - relativedelta(days=period_length)
|
||||
period_name = str((5-(i+1)) * period_length + 1) + '-' + str((5-i) * period_length)
|
||||
period_stop = (start - relativedelta(days=1)).strftime('%Y-%m-%d')
|
||||
if i == 0:
|
||||
period_name = '+' + str(4 * period_length)
|
||||
periods[str(i)] = {
|
||||
'name': (i!=0 and (str((5-(i+1)) * period_length) + '-' + str((5-i) * period_length)) or ('+'+str(4 * period_length))),
|
||||
'stop': start.strftime('%Y-%m-%d'),
|
||||
'name': period_name,
|
||||
'stop': period_stop,
|
||||
'start': (i!=0 and stop.strftime('%Y-%m-%d') or False),
|
||||
}
|
||||
start = stop - relativedelta(days=1)
|
||||
start = stop
|
||||
|
||||
res = []
|
||||
total = []
|
||||
|
||||
@@ -43,6 +43,7 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, {
|
||||
var defs = [this._super.apply(this, arguments)];
|
||||
this.time = Date.now();
|
||||
this.$progress = this.$('.progress');
|
||||
this.clickStatementName = this._initialState.bank_statement_id ? true : false;
|
||||
|
||||
if (this._initialState.bank_statement_id) {
|
||||
var def = this.model.makeRecord("account.bank.statement", [{
|
||||
|
||||
@@ -782,7 +782,7 @@ class TestReconciliation(AccountingTestCase):
|
||||
def test_aged_report(self):
|
||||
AgedReport = self.env['report.account.report_agedpartnerbalance'].with_context(include_nullified_amount=True)
|
||||
account_type = ['receivable']
|
||||
report_date_to = time.strftime('%Y') + '-07-16'
|
||||
report_date_to = time.strftime('%Y') + '-07-17'
|
||||
partner = self.env['res.partner'].create({'name': 'AgedPartner'})
|
||||
currency = self.env.user.company_id.currency_id
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="analytic_tag_ids" groups="analytic.group_analytic_accounting" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||
<field domain="[('company_id', '=', parent.company_id)]" name="account_id" groups="account.group_account_user"/>
|
||||
<field name="invoice_line_tax_ids" context="{'type':parent.type}" domain="[('type_tax_use','!=','none'),('company_id', '=', parent.company_id)]" widget="many2many_tags" options="{'no_create': True}"/>
|
||||
<field domain="[('company_id', '=', parent.company_id)]" name="account_analytic_id" groups="analytic.group_analytic_accounting"/>
|
||||
|
||||
@@ -430,8 +430,10 @@
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="bank_acc_number"/>
|
||||
<field name="bank_id"/>
|
||||
<field name="company_partner_id" invisible="1"/>
|
||||
<field name="bank_account_id"/>
|
||||
<field name="bank_acc_number" attrs="{'readonly': [('bank_account_id', '!=', False)]}"/>
|
||||
<field name="bank_id" attrs="{'readonly': [('bank_account_id', '!=', False)]}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
|
||||
|
||||
@@ -27,7 +27,7 @@ class ResPartner(models.Model):
|
||||
signup_token = fields.Char(copy=False, groups="base.group_erp_manager")
|
||||
signup_type = fields.Char(string='Signup Token Type', copy=False, groups="base.group_erp_manager")
|
||||
signup_expiration = fields.Datetime(copy=False, groups="base.group_erp_manager")
|
||||
signup_valid = fields.Boolean(compute='_compute_signup_valid', string='Signup Token is Valid', compute_sudo=True)
|
||||
signup_valid = fields.Boolean(compute='_compute_signup_valid', compute_sudo=True, string='Signup Token is Valid')
|
||||
signup_url = fields.Char(compute='_compute_signup_url', string='Signup URL')
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -10,6 +10,7 @@ var FormController = require('web.FormController');
|
||||
var FormRenderer = require('web.FormRenderer');
|
||||
var FormView = require('web.FormView');
|
||||
var pyUtils = require('web.py_utils');
|
||||
var session = require('web.session');
|
||||
var viewRegistry = require('web.view_registry');
|
||||
|
||||
var _t = core._t;
|
||||
@@ -246,7 +247,10 @@ var BoardRenderer = FormRenderer.extend({
|
||||
// the action does not exist anymore
|
||||
return $.when();
|
||||
}
|
||||
var context = pyUtils.eval('context', new Context(params.context, action.context));
|
||||
// tz and lang are saved in the custom view
|
||||
// override the language to take the current one
|
||||
var rawContext = new Context(params.context, action.context, {lang: session.user_context.lang});
|
||||
var context = pyUtils.eval('context', rawContext);
|
||||
var domain = params.domain || pyUtils.eval('domain', action.domain || '[]', action.context);
|
||||
var viewType = params.viewType || action.views[0][1];
|
||||
var view = _.find(action.views, function (descr) {
|
||||
|
||||
@@ -6,6 +6,7 @@ var BoardView = require('board.BoardView');
|
||||
var ListController = require('web.ListController');
|
||||
var testUtils = require('web.test_utils');
|
||||
var ListRenderer = require('web.ListRenderer');
|
||||
var pyUtils = require('web.py_utils');
|
||||
|
||||
var createActionManager = testUtils.createActionManager;
|
||||
var createView = testUtils.createView;
|
||||
@@ -746,4 +747,44 @@ QUnit.test('save to dashboard actions with flag keepSearchView', function (asser
|
||||
actionManager.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("Views should be loaded in the user's language", function (assert) {
|
||||
assert.expect(2);
|
||||
var form = createView({
|
||||
View: BoardView,
|
||||
model: 'board',
|
||||
data: this.data,
|
||||
session: {user_context: {lang: 'fr_FR'}},
|
||||
arch: '<form string="My Dashboard">' +
|
||||
'<board style="2-1">' +
|
||||
'<column>' +
|
||||
'<action context="{\'lang\': \'en_US\'}" view_mode="list" string="ABC" name="51" domain="[]"></action>' +
|
||||
'</column>' +
|
||||
'</board>' +
|
||||
'</form>',
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'load_views') {
|
||||
assert.deepEqual(pyUtils.eval('context', args.kwargs.context), {lang: 'fr_FR'},
|
||||
'The views should be loaded with the correct context');
|
||||
}
|
||||
if (route === "/web/dataset/search_read") {
|
||||
assert.deepEqual(args.context, {lang: 'fr_FR'},
|
||||
'The data should be loaded with the correct context');
|
||||
}
|
||||
if (route === '/web/action/load') {
|
||||
return $.when({
|
||||
res_model: 'partner',
|
||||
views: [[4, 'list']],
|
||||
});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
archs: {
|
||||
'partner,4,list':
|
||||
'<list string="Partner"><field name="foo"/></list>',
|
||||
},
|
||||
});
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -57,10 +57,8 @@ WebClient.include({
|
||||
// Clear previously set timeouts and destroy currently displayed calendar notifications
|
||||
clearTimeout(this.get_next_calendar_notif_timeout);
|
||||
_.each(this.calendar_notif_timeouts, clearTimeout);
|
||||
_.each(this.calendar_notif, function(notif) {
|
||||
if (!notif.isDestroyed()) {
|
||||
notif.destroy();
|
||||
}
|
||||
_.each(this.calendar_notif, function (notificationID) {
|
||||
self.call('notification', 'close', notificationID, true);
|
||||
});
|
||||
this.calendar_notif_timeouts = {};
|
||||
this.calendar_notif = {};
|
||||
@@ -68,13 +66,13 @@ WebClient.include({
|
||||
// For each notification, set a timeout to display it
|
||||
_.each(notifications, function(notif) {
|
||||
self.calendar_notif_timeouts[notif.event_id] = setTimeout(function() {
|
||||
var notification = new CalendarNotification(self.notification_manager, {
|
||||
var notificationID = self.call('notification', 'notify', {
|
||||
Notification: CalendarNotification,
|
||||
title: notif.title,
|
||||
text: notif.message,
|
||||
eventID: notif.event_id,
|
||||
});
|
||||
self.notification_manager.display(notification);
|
||||
self.calendar_notif[notif.event_id] = notification;
|
||||
self.calendar_notif[notif.event_id] = notificationID;
|
||||
}, notif.timer * 1000);
|
||||
last_notif_timer = Math.max(last_notif_timer, notif.timer);
|
||||
});
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="product_id" context="{'default_can_be_expensed': 1}"/>
|
||||
<field name="product_id" context="{'default_can_be_expensed': 1, 'tree_view_ref': 'hr_expense.product_product_expense_tree_view'}"/>
|
||||
<field name="unit_amount" required="1" widget="monetary" options="{'currency_field': 'currency_id'}"/>
|
||||
<label for="quantity"/>
|
||||
<div>
|
||||
@@ -315,6 +315,22 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_product_expense_tree_view" model="ir.ui.view">
|
||||
<field name="name">product.product.expense.tree</field>
|
||||
<field name="model">product.product</field>
|
||||
<field eval="50" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Product Variants">
|
||||
<field name="default_code"/>
|
||||
<field name="name"/>
|
||||
<field name="attribute_value_ids" widget="many2many_tags" groups="product.group_product_variant"/>
|
||||
<field name="standard_price"/>
|
||||
<field name="uom_id" options="{'no_open': True, 'no_create': True}" groups="uom.group_uom"/>
|
||||
<field name="barcode"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_expense_product" model="ir.actions.act_window">
|
||||
<field name="name">Expense Products</field>
|
||||
<field name="res_model">product.product</field>
|
||||
|
||||
@@ -297,7 +297,7 @@
|
||||
<!-- Discuss utility templates for notifications -->
|
||||
<template id="message_user_assigned">
|
||||
<p style="margin: 0px;">
|
||||
<span>Dear <t t-esc="object.user_id.name"/>,</span><br />
|
||||
<span>Dear <t t-esc="object.user_id.sudo().name"/>,</span><br />
|
||||
<span style="margin-top: 8px;">You have been assigned to the <t t-esc="object._description.lower()"/> <t t-esc="object.name_get()[0][1]"/>.</span>
|
||||
</p>
|
||||
<p style="margin-top: 24px; margin-bottom: 16px;">
|
||||
|
||||
@@ -119,6 +119,8 @@ class MailMail(models.Model):
|
||||
ids = filtered_ids
|
||||
else:
|
||||
ids = list(set(filtered_ids) & set(ids))
|
||||
ids.sort()
|
||||
|
||||
res = None
|
||||
try:
|
||||
# auto-commit except in testing mode
|
||||
@@ -292,7 +294,6 @@ class MailMail(models.Model):
|
||||
values['partner_id'] = partner
|
||||
email_list.append(values)
|
||||
|
||||
|
||||
# headers
|
||||
headers = {}
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
@@ -316,6 +317,22 @@ class MailMail(models.Model):
|
||||
'state': 'exception',
|
||||
'failure_reason': _('Error without exception. Probably due do sending an email without computed recipients.'),
|
||||
})
|
||||
# Update notification in a transient exception state to avoid concurrent
|
||||
# update in case an email bounces while sending all emails related to current
|
||||
# mail record.
|
||||
notifs = self.env['mail.notification'].search([
|
||||
('is_email', '=', True),
|
||||
('mail_id', 'in', mail.ids),
|
||||
('email_status', 'not in', ('sent', 'canceled'))
|
||||
])
|
||||
if notifs:
|
||||
notif_msg = _('Error without exception. Probably due do concurrent access update of notification records. Please see with an administrator.')
|
||||
notifs.write({
|
||||
'email_status': 'exception',
|
||||
'failure_type': 'UNKNOWN',
|
||||
'failure_reason': notif_msg,
|
||||
})
|
||||
|
||||
# build an RFC2822 email.message.Message object and send it without queuing
|
||||
res = None
|
||||
for email in email_list:
|
||||
|
||||
@@ -63,8 +63,8 @@ class MailTracking(models.Model):
|
||||
values.update({
|
||||
'old_value_integer': initial_value and initial_value.id or 0,
|
||||
'new_value_integer': new_value and new_value.id or 0,
|
||||
'old_value_char': initial_value and initial_value.name_get()[0][1] or '',
|
||||
'new_value_char': new_value and new_value.name_get()[0][1] or ''
|
||||
'old_value_char': initial_value and initial_value.sudo().name_get()[0][1] or '',
|
||||
'new_value_char': new_value and new_value.sudo().name_get()[0][1] or ''
|
||||
})
|
||||
else:
|
||||
tracked = False
|
||||
|
||||
@@ -349,8 +349,8 @@ class MaintenanceRequest(models.Model):
|
||||
res = super(MaintenanceRequest, self).write(vals)
|
||||
if vals.get('owner_user_id') or vals.get('user_id'):
|
||||
self._add_followers()
|
||||
if self.stage_id.done and 'stage_id' in vals:
|
||||
self.write({'close_date': fields.Date.today()})
|
||||
if 'stage_id' in vals:
|
||||
self.filtered(lambda m: m.stage_id.done).write({'close_date': fields.Date.today()})
|
||||
self.activity_feedback(['maintenance.mail_act_maintenance_request'])
|
||||
if vals.get('user_id') or vals.get('schedule_date'):
|
||||
self.activity_update()
|
||||
|
||||
@@ -87,7 +87,6 @@ td {
|
||||
}
|
||||
|
||||
.btn.btn-primary {
|
||||
-webkit-user-select: none;
|
||||
color: $o-mm-def-btn-text;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
|
||||
@@ -310,8 +310,10 @@ class MrpWorkorder(models.Model):
|
||||
if self.product_id.tracking != 'none':
|
||||
qty_to_add = float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding)
|
||||
move._generate_consumed_move_line(qty_to_add, self.final_lot_id)
|
||||
else:
|
||||
elif len(move._get_move_lines()) < 2:
|
||||
move.quantity_done += float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding)
|
||||
else:
|
||||
move._set_quantity_done(move.quantity_done + float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding))
|
||||
|
||||
# Transfer quantities from temporary to final move lots or make them final
|
||||
for move_line in self.active_move_line_ids:
|
||||
|
||||
@@ -55,7 +55,7 @@ class ReportBomStructure(models.AbstractModel):
|
||||
@api.model
|
||||
def get_operations(self, bom_id=False, qty=0, level=0):
|
||||
bom = self.env['mrp.bom'].browse(bom_id)
|
||||
lines = self._get_operation_line(bom.routing_id, qty, level)
|
||||
lines = self._get_operation_line(bom.routing_id, float_round(qty / bom.product_qty, precision_rounding=1, rounding_method='UP'), level)
|
||||
values = {
|
||||
'bom_id': bom_id,
|
||||
'currency': self.env.user.company_id.currency_id,
|
||||
@@ -109,7 +109,7 @@ class ReportBomStructure(models.AbstractModel):
|
||||
else:
|
||||
product = bom.product_tmpl_id
|
||||
attachments = self.env['mrp.document'].search([('res_model', '=', 'product.template'), ('res_id', '=', product.id)])
|
||||
operations = self._get_operation_line(bom.routing_id, (bom_quantity / bom.product_qty), 0)
|
||||
operations = self._get_operation_line(bom.routing_id, float_round(bom_quantity / bom.product_qty, precision_rounding=1, rounding_method='UP'), 0)
|
||||
lines = {
|
||||
'bom': bom,
|
||||
'bom_qty': bom_quantity,
|
||||
@@ -139,7 +139,7 @@ class ReportBomStructure(models.AbstractModel):
|
||||
continue
|
||||
price = line.product_id.uom_id._compute_price(line.product_id.standard_price, line.product_uom_id) * line_quantity
|
||||
if line.child_bom_id:
|
||||
factor = line.product_uom_id._compute_quantity(line_quantity, line.child_bom_id.product_uom_id) * line.child_bom_id.product_qty
|
||||
factor = float_round(line.product_uom_id._compute_quantity(line_quantity, line.child_bom_id.product_uom_id) / line.child_bom_id.product_qty, precision_rounding=1, rounding_method='UP')
|
||||
sub_total = self._get_price(line.child_bom_id, factor)
|
||||
else:
|
||||
sub_total = price
|
||||
@@ -167,7 +167,8 @@ class ReportBomStructure(models.AbstractModel):
|
||||
operations = []
|
||||
total = 0.0
|
||||
for operation in routing.operation_ids:
|
||||
duration_expected = qty * operation.time_cycle + operation.workcenter_id.time_stop + operation.workcenter_id.time_start
|
||||
operation_cycle = float_round(qty / operation.workcenter_id.capacity, precision_rounding=1, rounding_method='UP')
|
||||
duration_expected = operation_cycle * operation.time_cycle + operation.workcenter_id.time_stop + operation.workcenter_id.time_start
|
||||
total = ((duration_expected / 60.0) * operation.workcenter_id.costs_hour)
|
||||
operations.append({
|
||||
'level': level or 0,
|
||||
|
||||
@@ -1509,6 +1509,7 @@ exports.Orderline = Backbone.Model.extend({
|
||||
// when we add an new orderline we want to merge it with the last line to see reduce the number of items
|
||||
// in the orderline. This returns true if it makes sense to merge the two
|
||||
can_be_merged_with: function(orderline){
|
||||
var price = parseFloat(round_di(this.price || 0, this.pos.dp['Product Price']).toFixed(this.pos.dp['Product Price']));
|
||||
if( this.get_product().id !== orderline.get_product().id){ //only orderline of the same product can be merged
|
||||
return false;
|
||||
}else if(!this.get_unit() || !this.get_unit().is_pos_groupable){
|
||||
@@ -1517,7 +1518,7 @@ exports.Orderline = Backbone.Model.extend({
|
||||
return false;
|
||||
}else if(this.get_discount() > 0){ // we don't merge discounted orderlines
|
||||
return false;
|
||||
}else if(this.price !== orderline.get_product().get_price(orderline.order.pricelist, this.get_quantity())){
|
||||
}else if(price !== orderline.get_product().get_price(orderline.order.pricelist, this.get_quantity())){
|
||||
return false;
|
||||
}else if(this.product.tracking == 'lot') {
|
||||
return false;
|
||||
|
||||
@@ -34,6 +34,7 @@ var core = require('web.core');
|
||||
var rpc = require('web.rpc');
|
||||
var utils = require('web.utils');
|
||||
var field_utils = require('web.field_utils');
|
||||
var BarcodeEvents = require('barcodes.BarcodeEvents').BarcodeEvents;
|
||||
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
@@ -1689,6 +1690,13 @@ var PaymentScreenWidget = ScreenWidget.extend({
|
||||
// also called explicitly to handle some keydown events that
|
||||
// do not generate keypress events.
|
||||
this.keyboard_handler = function(event){
|
||||
// On mobile Chrome BarcodeEvents relies on an invisible
|
||||
// input being filled by a barcode device. Let events go
|
||||
// through when this input is focused.
|
||||
if (BarcodeEvents.$barcodeInput && BarcodeEvents.$barcodeInput.is(":focus")) {
|
||||
return;
|
||||
}
|
||||
|
||||
var key = '';
|
||||
|
||||
if (event.type === "keypress") {
|
||||
|
||||
@@ -107,7 +107,7 @@ class ProductProduct(models.Model):
|
||||
select
|
||||
sum(l.price_unit * l.quantity)/nullif(sum(l.quantity),0) as avg_unit_price,
|
||||
sum(l.quantity) as num_qty,
|
||||
sum(l.quantity * (l.price_subtotal/(nullif(l.quantity,0)))) as total,
|
||||
sum(l.quantity * (l.price_subtotal_signed/(nullif(l.quantity,0)))) as total,
|
||||
sum(l.quantity * pt.list_price) as sale_expected
|
||||
from account_invoice_line l
|
||||
left join account_invoice i on (l.invoice_id = i.id)
|
||||
|
||||
@@ -27,7 +27,8 @@ class PurchaseOrderLine(models.Model):
|
||||
# In the case of a kit, we need to check if all components are shipped. Since the BOM might
|
||||
# have changed, we don't compute the quantities but verify the move state.
|
||||
if bom:
|
||||
bom_delivered = all([move.state == 'done' for move in self.move_ids])
|
||||
moves = self.move_ids.filtered(lambda m: m.picking_id and m.picking_id.state != 'cancel')
|
||||
bom_delivered = all([move.state == 'done' for move in moves])
|
||||
if bom_delivered:
|
||||
return self.product_qty
|
||||
else:
|
||||
|
||||
@@ -21,8 +21,8 @@ KanbanRecord.include({
|
||||
ev.preventDefault();
|
||||
|
||||
this.$target_input = $('<input>');
|
||||
this.$('.o_kanban_primary_bottom').html(this.$target_input);
|
||||
this.$('.o_kanban_primary_bottom').prepend(_t("Set an invoicing target: "));
|
||||
this.$('.o_kanban_primary_bottom:last').html(this.$target_input);
|
||||
this.$('.o_kanban_primary_bottom:last').prepend(_t("Set an invoicing target: "));
|
||||
this.$target_input.focus();
|
||||
|
||||
var self = this;
|
||||
|
||||
@@ -131,14 +131,12 @@
|
||||
<input type="hidden" class="product_display_name" t-attf-name="optional-productname-#{product.id}" t-att-value="product.product_variant_id.display_name if len(product.product_variant_ids) == 1 else ''"/>
|
||||
<span t-field="product.image_small" t-options='{"widget": "image"}' class="variant_image"/>
|
||||
</td>
|
||||
<td class='td-product_name'>
|
||||
<td class='td-product_name' colspan="2">
|
||||
<div class="float-left">
|
||||
<strong class="product-name" t-field="product.name"/>
|
||||
<div class="text-muted small" t-field="product.description_sale"/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="td-qty">
|
||||
<div class="float-left">
|
||||
<div class="float-right">
|
||||
<t t-call="sale.variants"/>
|
||||
</div>
|
||||
</td>
|
||||
@@ -268,4 +266,4 @@
|
||||
</t>
|
||||
</ul>
|
||||
</template>
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
||||
@@ -187,7 +187,7 @@ class SaleOrderLine(models.Model):
|
||||
qty = 0.0
|
||||
for move in line.move_ids.filtered(lambda r: r.state == 'done' and not r.scrapped):
|
||||
if move.location_dest_id.usage == "customer":
|
||||
if not move.origin_returned_move_id:
|
||||
if not move.origin_returned_move_id or (move.origin_returned_move_id and move.to_refund):
|
||||
qty += move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom)
|
||||
elif move.location_dest_id.usage != "customer" and move.to_refund:
|
||||
qty -= move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom)
|
||||
|
||||
@@ -188,7 +188,7 @@
|
||||
<td>
|
||||
<div t-if="repartition_employee_max" class="progress" t-att-style="'width: ' + str(repartition_employee[employee_id]['total'] / repartition_employee_max * 100) +'%'">
|
||||
|
||||
<t t-set="total" t-value="repartition_employee[employee_id]['total']" />
|
||||
<t t-set="total" t-value="repartition_employee[employee_id]['total'] or 1.0" />
|
||||
<t t-set="billable_fixed" t-value="repartition_employee[employee_id]['billable_fixed']" />
|
||||
<t t-set="billable_time" t-value="repartition_employee[employee_id]['billable_time']" />
|
||||
<t t-set="non_billable" t-value="repartition_employee[employee_id]['non_billable']" />
|
||||
|
||||
@@ -233,7 +233,7 @@ class Inventory(models.Model):
|
||||
def _get_inventory_lines_values(self):
|
||||
# TDE CLEANME: is sql really necessary ? I don't think so
|
||||
locations = self.env['stock.location'].search([('id', 'child_of', [self.location_id.id])])
|
||||
domain = ' location_id in %s AND quantity != 0'
|
||||
domain = ' location_id in %s AND quantity != 0 AND active = TRUE'
|
||||
args = (tuple(locations.ids),)
|
||||
|
||||
vals = []
|
||||
@@ -274,6 +274,8 @@ class Inventory(models.Model):
|
||||
|
||||
self.env.cr.execute("""SELECT product_id, sum(quantity) as product_qty, location_id, lot_id as prod_lot_id, package_id, owner_id as partner_id
|
||||
FROM stock_quant
|
||||
LEFT JOIN product_product
|
||||
ON product_product.id = stock_quant.product_id
|
||||
WHERE %s
|
||||
GROUP BY product_id, location_id, lot_id, package_id, partner_id """ % domain, args)
|
||||
|
||||
|
||||
@@ -182,6 +182,8 @@ class StockMoveLine(models.Model):
|
||||
|
||||
for ml in mls:
|
||||
if ml.state == 'done':
|
||||
if 'qty_done' in vals:
|
||||
ml.move_id.product_uom_qty = ml.move_id.quantity_done
|
||||
if ml.product_id.type == 'product':
|
||||
Quant = self.env['stock.quant']
|
||||
quantity = ml.product_uom_id._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id,rounding_method='HALF-UP')
|
||||
@@ -507,6 +509,8 @@ class StockMoveLine(models.Model):
|
||||
candidate.product_uom_qty = 0.0
|
||||
else:
|
||||
candidate.unlink()
|
||||
if float_is_zero(quantity, precision_rounding=rounding):
|
||||
break
|
||||
else:
|
||||
# split this move line and assign the new part to our extra move
|
||||
quantity_split = float_round(
|
||||
@@ -514,8 +518,6 @@ class StockMoveLine(models.Model):
|
||||
precision_rounding=self.product_uom_id.rounding,
|
||||
rounding_method='UP')
|
||||
candidate.product_uom_qty = self.product_id.uom_id._compute_quantity(quantity_split, candidate.product_uom_id, rounding_method='HALF-UP')
|
||||
quantity -= quantity_split
|
||||
move_to_recompute_state |= candidate.move_id
|
||||
if quantity == 0.0:
|
||||
break
|
||||
move_to_recompute_state._recompute_state()
|
||||
|
||||
@@ -2008,6 +2008,48 @@ class StockMove(TransactionCase):
|
||||
self.assertAlmostEqual(move_pack_cust.reserved_availability, 1.0)
|
||||
self.assertEqual(move_pack_cust.state, 'partially_available')
|
||||
|
||||
def test_use_reserved_move_line_1(self):
|
||||
""" Test that _free_reservation work when quantity is only available on
|
||||
reserved move lines.
|
||||
"""
|
||||
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 10.0)
|
||||
move1 = self.env['stock.move'].create({
|
||||
'name': 'test_use_unreserved_move_line_1_1',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 5.0,
|
||||
})
|
||||
move2 = self.env['stock.move'].create({
|
||||
'name': 'test_use_unreserved_move_line_1_1',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 5.0,
|
||||
})
|
||||
move1._action_confirm()
|
||||
move1._action_assign()
|
||||
move2._action_confirm()
|
||||
move2._action_assign()
|
||||
move3 = self.env['stock.move'].create({
|
||||
'name': 'test_use_unreserved_move_line_1_1',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 0.0,
|
||||
'quantity_done': 1.0,
|
||||
})
|
||||
move3._action_confirm()
|
||||
move3._action_assign()
|
||||
move3._action_done()
|
||||
self.assertEqual(move3.state, 'done')
|
||||
quant = self.env['stock.quant']._gather(self.product1, self.stock_location)
|
||||
self.assertEqual(quant.quantity, 9.0)
|
||||
self.assertEqual(quant.reserved_quantity, 9.0)
|
||||
|
||||
def test_use_unreserved_move_line_1(self):
|
||||
""" Test that validating a stock move linked to an untracked product reserved by another one
|
||||
correctly unreserves the other one.
|
||||
|
||||
@@ -332,6 +332,7 @@
|
||||
<field name="group_id" groups="base.group_no_one"/>
|
||||
<field name="priority" attrs="{'invisible': [('picking_type_code', '=', 'incoming')]}"/>
|
||||
</group>
|
||||
<field name="note" string="Note" placeholder="Add an internal note that will be printed on the Picking Operations sheet"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import datetime
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.misc import clean_context
|
||||
|
||||
|
||||
class ProductReplenish(models.TransientModel):
|
||||
@@ -51,7 +52,7 @@ class ProductReplenish(models.TransientModel):
|
||||
uom_reference = self.product_id.uom_id
|
||||
self.quantity = self.product_uom_id._compute_quantity(self.quantity, uom_reference)
|
||||
try:
|
||||
self.env['procurement.group'].run(
|
||||
self.env['procurement.group'].with_context(clean_context(self.env.context)).run(
|
||||
self.product_id,
|
||||
self.quantity,
|
||||
uom_reference,
|
||||
|
||||
@@ -11,9 +11,11 @@ class SaleOrderLine(models.Model):
|
||||
|
||||
@api.multi
|
||||
def _get_qty_procurement(self):
|
||||
if not self.move_ids.filtered(lambda r: r.state != 'cancel') and self.purchase_line_ids.filtered(lambda r: r.state != 'cancel'):
|
||||
# People without purchase rights should be able to do this operation
|
||||
purchase_lines_sudo = self.sudo().purchase_line_ids
|
||||
if not self.move_ids.filtered(lambda r: r.state != 'cancel') and purchase_lines_sudo.filtered(lambda r: r.state != 'cancel'):
|
||||
qty = 0.0
|
||||
for po_line in self.purchase_line_ids.filtered(lambda r: r.state != 'cancel'):
|
||||
for po_line in purchase_lines_sudo.filtered(lambda r: r.state != 'cancel'):
|
||||
qty += po_line.product_uom._compute_quantity(po_line.product_qty, self.product_uom, rounding_method='HALF-UP')
|
||||
return qty
|
||||
else:
|
||||
|
||||
@@ -4,6 +4,7 @@ from . import test_mail_activity
|
||||
from . import test_mail_followers
|
||||
from . import test_mail_message
|
||||
from . import test_mail_mail
|
||||
from . import test_mail_race
|
||||
from . import test_mail_resend
|
||||
from . import test_mail_channel
|
||||
from . import test_mail_gateway
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from contextlib import closing
|
||||
import psycopg2
|
||||
from odoo import api
|
||||
from odoo.addons.test_mail.tests import common as mail_common
|
||||
from odoo.tests import common
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
class TestMailRace(common.TransactionCase, mail_common.MockEmails):
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mail_bounce_during_send(self):
|
||||
self.partner = self.env['res.partner'].create({
|
||||
'name': 'Ernest Partner',
|
||||
})
|
||||
# we need to simulate a mail sent by the cron task, first create mail, message and notification by hand
|
||||
mail = self.env['mail.mail'].create({
|
||||
'body_html': '<p>Test</p>',
|
||||
'notification': True,
|
||||
'state': 'outgoing',
|
||||
'recipient_ids': [(4, self.partner.id)]
|
||||
})
|
||||
message = self.env['mail.message'].create({
|
||||
'subject': 'S',
|
||||
'body': 'B',
|
||||
'subtype_id': self.ref('mail.mt_comment'),
|
||||
'needaction_partner_ids': [(6, 0, [self.partner.id])],
|
||||
})
|
||||
notif = self.env['mail.notification'].search([('res_partner_id', '=', self.partner.id)])
|
||||
notif.write({
|
||||
'mail_id': mail.id,
|
||||
'is_email': True,
|
||||
'is_read': True,
|
||||
'email_status': 'ready',
|
||||
})
|
||||
# we need to commit transaction or cr will keep the lock on notif
|
||||
self.cr.commit()
|
||||
|
||||
# patch send_email in order to create a concurent update and check the notif is already locked by _send()
|
||||
this = self # coding in javascript ruinned my life
|
||||
bounce_deferred = []
|
||||
@api.model
|
||||
def send_email(self, message, *args, **kwargs):
|
||||
with this.registry.cursor() as cr, mute_logger('odoo.sql_db'):
|
||||
try:
|
||||
# try ro aquire lock (no wait) on notification (should fail)
|
||||
cr.execute("SELECT email_status FROM mail_message_res_partner_needaction_rel WHERE id = %s FOR UPDATE NOWAIT", [notif.id])
|
||||
except psycopg2.OperationalError:
|
||||
# record already locked by send, all good
|
||||
bounce_deferred.append(True)
|
||||
else:
|
||||
# this should trigger psycopg2.extensions.TransactionRollbackError in send().
|
||||
# Only here to simulate the initial use case
|
||||
# If the record is lock, this line would create a deadlock since we are in the same thread
|
||||
# In practice, the update will wait the end of the send() transaction and set the notif as bounce, as expeced
|
||||
cr.execute("UPDATE mail_message_res_partner_needaction_rel SET email_status='bounce' WHERE id = %s", [notif.id])
|
||||
return message['Message-Id']
|
||||
self.env['ir.mail_server']._patch_method('send_email', send_email)
|
||||
|
||||
mail.send()
|
||||
|
||||
self.assertTrue(bounce_deferred, "The bounce should have been deferred")
|
||||
self.assertEqual(notif.email_status, 'sent')
|
||||
|
||||
# some cleaning since we commited the cr
|
||||
notif.unlink()
|
||||
message.unlink()
|
||||
mail.unlink()
|
||||
self.partner.unlink()
|
||||
self.env.cr.commit()
|
||||
@@ -205,7 +205,7 @@ class TestAdvMailPerformance(TransactionCase):
|
||||
self.user_test.write({'notification_type': 'email'})
|
||||
record = self.env['mail.test.track'].create({'name': 'Test'})
|
||||
|
||||
with self.assertQueryCount(__system__=56, emp=75): # com runbot: 56 - 75 // test_mail only: 56 - 75
|
||||
with self.assertQueryCount(__system__=58, emp=77): # com runbot: 56 - 75 // test_mail only: 56 - 75
|
||||
record.write({
|
||||
'user_id': self.user_test.id,
|
||||
})
|
||||
@@ -259,7 +259,7 @@ class TestAdvMailPerformance(TransactionCase):
|
||||
def test_message_post_one_email_notification(self):
|
||||
record = self.env['mail.test.simple'].create({'name': 'Test'})
|
||||
|
||||
with self.assertQueryCount(__system__=48, emp=68): # com runbot: 45 - 65 // test_mail only: 48 - 68
|
||||
with self.assertQueryCount(__system__=50, emp=70): # com runbot: 45 - 65 // test_mail only: 48 - 68
|
||||
record.message_post(
|
||||
body='<p>Test Post Performances with an email ping</p>',
|
||||
partner_ids=self.customer.ids,
|
||||
@@ -382,7 +382,7 @@ class TestHeavyMailPerformance(TransactionCase):
|
||||
})
|
||||
mail_ids = mail.ids
|
||||
|
||||
with self.assertQueryCount(__system__=13, emp=20): # test_mail only: 13 - 20
|
||||
with self.assertQueryCount(__system__=14, emp=21): # test_mail only: 13 - 20
|
||||
self.env['mail.mail'].browse(mail_ids).send()
|
||||
|
||||
self.assertEqual(mail.body_html, '<p>Test</p>')
|
||||
@@ -395,7 +395,7 @@ class TestHeavyMailPerformance(TransactionCase):
|
||||
self.umbrella.message_subscribe(self.user_portal.partner_id.ids)
|
||||
record = self.umbrella.sudo(self.env.user)
|
||||
|
||||
with self.assertQueryCount(__system__=80, emp=103): # com runbot 90 - 113 // test_mail only: 97 - 120
|
||||
with self.assertQueryCount(__system__=84, emp=107): # com runbot 90 - 113 // test_mail only: 97 - 120
|
||||
record.message_post(
|
||||
body='<p>Test Post Performances</p>',
|
||||
message_type='comment',
|
||||
@@ -412,7 +412,7 @@ class TestHeavyMailPerformance(TransactionCase):
|
||||
record = self.umbrella.sudo(self.env.user)
|
||||
template_id = self.env.ref('test_mail.mail_test_tpl').id
|
||||
|
||||
with self.assertQueryCount(__system__=99, emp=134): # com runbot 109 - 144 // test_mail only: 116 - 151
|
||||
with self.assertQueryCount(__system__=103, emp=138): # com runbot 109 - 144 // test_mail only: 116 - 151
|
||||
record.message_post_with_template(template_id, message_type='comment', composition_mode='comment')
|
||||
|
||||
self.assertEqual(record.message_ids[0].body, '<p>Adding stuff on %s</p>' % record.name)
|
||||
@@ -482,7 +482,7 @@ class TestHeavyMailPerformance(TransactionCase):
|
||||
})
|
||||
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id)
|
||||
|
||||
with self.assertQueryCount(__system__=58, emp=77): # com runbot: 65 - 83 // test_mail only: 66 - 84
|
||||
with self.assertQueryCount(__system__=60, emp=79): # com runbot: 65 - 83 // test_mail only: 66 - 84
|
||||
rec.write({'user_id': self.user_portal.id})
|
||||
|
||||
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id)
|
||||
@@ -505,7 +505,7 @@ class TestHeavyMailPerformance(TransactionCase):
|
||||
customer_id = self.customer.id
|
||||
user_id = self.user_portal.id
|
||||
|
||||
with self.assertQueryCount(__system__=156, emp=190): # com runbot: 156 - 190 // test_mail only: 156 - 190
|
||||
with self.assertQueryCount(__system__=162, emp=198): # com runbot: 156 - 190 // test_mail only: 156 - 190
|
||||
rec = self.env['mail.test.full'].create({
|
||||
'name': 'Test',
|
||||
'umbrella_id': umbrella_id,
|
||||
@@ -534,7 +534,7 @@ class TestHeavyMailPerformance(TransactionCase):
|
||||
})
|
||||
self.assertEqual(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id)
|
||||
|
||||
with self.assertQueryCount(__system__=94, emp=111): # com runbot: 104 - 123 // test_mail only: 105 - 124
|
||||
with self.assertQueryCount(__system__=99, emp=119): # com runbot: 104 - 123 // test_mail only: 105 - 124
|
||||
rec.write({
|
||||
'name': 'Test2',
|
||||
'umbrella_id': self.umbrella.id,
|
||||
@@ -572,7 +572,7 @@ class TestHeavyMailPerformance(TransactionCase):
|
||||
})
|
||||
self.assertEqual(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id)
|
||||
|
||||
with self.assertQueryCount(__system__=100, emp=122): # test_mail only: 111 - 134
|
||||
with self.assertQueryCount(__system__=104, emp=126): # test_mail only: 111 - 134
|
||||
rec.write({
|
||||
'name': 'Test2',
|
||||
'umbrella_id': umbrella_id,
|
||||
@@ -606,7 +606,7 @@ class TestHeavyMailPerformance(TransactionCase):
|
||||
})
|
||||
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id)
|
||||
|
||||
with self.assertQueryCount(__system__=54, emp=76): # test_mail only: 54 - 75
|
||||
with self.assertQueryCount(__system__=55, emp=77): # test_mail only: 54 - 75
|
||||
rec.write({
|
||||
'name': 'Test2',
|
||||
'customer_id': customer_id,
|
||||
|
||||
@@ -30,7 +30,7 @@ class TestMassMailPerformance(common.MassMailingCase):
|
||||
'mailing_domain': [('id', 'in', self.mm_recs.ids)],
|
||||
})
|
||||
|
||||
with self.assertQueryCount(__system__=2382, marketing=3038):
|
||||
with self.assertQueryCount(__system__=2432, marketing=3088):
|
||||
mailing.send_mail()
|
||||
|
||||
self.assertEqual(mailing.sent, 50)
|
||||
@@ -67,7 +67,7 @@ class TestMassMailBlPerformance(common.MassMailingCase):
|
||||
'mailing_domain': [('id', 'in', self.mm_recs.ids)],
|
||||
})
|
||||
|
||||
with self.assertQueryCount(__system__=2757, marketing=3509):
|
||||
with self.assertQueryCount(__system__=2807, marketing=3559):
|
||||
mailing.send_mail()
|
||||
|
||||
self.assertEqual(mailing.sent, 50)
|
||||
|
||||
@@ -500,6 +500,10 @@ var FieldDate = InputField.extend({
|
||||
this._super.apply(this, arguments);
|
||||
// use the session timezone when formatting dates
|
||||
this.formatOptions.timezone = true;
|
||||
this.datepickerOptions = _.defaults(
|
||||
this.nodeOptions.datepicker || {},
|
||||
{defaultDate: this.value}
|
||||
);
|
||||
},
|
||||
/**
|
||||
* In edit mode, instantiates a DateWidget datepicker and listen to changes.
|
||||
@@ -583,13 +587,7 @@ var FieldDate = InputField.extend({
|
||||
* @private
|
||||
*/
|
||||
_makeDatePicker: function () {
|
||||
return new datepicker.DateWidget(
|
||||
this,
|
||||
_.defaults(
|
||||
this.nodeOptions.datepicker || {},
|
||||
{defaultDate: this.value}
|
||||
)
|
||||
);
|
||||
return new datepicker.DateWidget(this, this.datepickerOptions);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -607,6 +605,17 @@ var FieldDate = InputField.extend({
|
||||
var FieldDateTime = FieldDate.extend({
|
||||
supportedFieldTypes: ['datetime'],
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
if (this.value) {
|
||||
var offset = this.getSession().getTZOffset(this.value);
|
||||
var displayedValue = this.value.clone().add(offset, 'minutes');
|
||||
this.datepickerOptions.defaultDate = displayedValue;
|
||||
}
|
||||
},
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -637,10 +646,8 @@ var FieldDateTime = FieldDate.extend({
|
||||
* @private
|
||||
*/
|
||||
_makeDatePicker: function () {
|
||||
var value = this.value && this.value.clone().add(this.getSession().getTZOffset(this.value), 'minutes');
|
||||
return new datepicker.DateTimeWidget(this, {defaultDate: value});
|
||||
return new datepicker.DateTimeWidget(this, this.datepickerOptions);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the datepicker to the right value rather than the default one.
|
||||
*
|
||||
@@ -1171,6 +1178,9 @@ var HandleWidget = AbstractField.extend({
|
||||
|
||||
var FieldEmail = InputField.extend({
|
||||
className: 'o_field_email',
|
||||
events: _.extend({}, InputField.prototype.events, {
|
||||
'click': '_onClick',
|
||||
}),
|
||||
prefix: 'mailto',
|
||||
supportedFieldTypes: ['char'],
|
||||
|
||||
@@ -1211,7 +1221,21 @@ var FieldEmail = InputField.extend({
|
||||
this.$el.text(this.value)
|
||||
.addClass('o_form_uri o_text_overflow')
|
||||
.attr('href', this.prefix + ':' + this.value);
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Prevent the URL click from opening the record (when used on a list).
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClick: function (ev) {
|
||||
ev.stopPropagation();
|
||||
},
|
||||
});
|
||||
|
||||
var FieldPhone = FieldEmail.extend({
|
||||
@@ -1238,6 +1262,9 @@ var FieldPhone = FieldEmail.extend({
|
||||
|
||||
var UrlWidget = InputField.extend({
|
||||
className: 'o_field_url',
|
||||
events: _.extend({}, InputField.prototype.events, {
|
||||
'click': '_onClick',
|
||||
}),
|
||||
supportedFieldTypes: ['char'],
|
||||
|
||||
/**
|
||||
@@ -1279,7 +1306,21 @@ var UrlWidget = InputField.extend({
|
||||
.addClass('o_form_uri o_text_overflow')
|
||||
.attr('target', '_blank')
|
||||
.attr('href', this.value);
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Prevent the URL click from opening the record (when used on a list).
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClick: function (ev) {
|
||||
ev.stopPropagation();
|
||||
},
|
||||
});
|
||||
|
||||
var CopyClipboard = {
|
||||
@@ -1645,19 +1686,23 @@ var FieldPdfViewer = FieldBinaryFile.extend({
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {string} [fileURI] file URI if specified
|
||||
* @returns {string} the pdf viewer URI
|
||||
*/
|
||||
_getURI: function () {
|
||||
var queryObj = {
|
||||
model: this.model,
|
||||
field: this.name,
|
||||
id: this.res_id,
|
||||
};
|
||||
_getURI: function (fileURI) {
|
||||
var page = this.recordData[this.name + '_page'] || 1;
|
||||
var queryString = $.param(queryObj);
|
||||
var url = encodeURIComponent('/web/image?' + queryString);
|
||||
if (!fileURI) {
|
||||
var queryObj = {
|
||||
model: this.model,
|
||||
field: this.name,
|
||||
id: this.res_id,
|
||||
};
|
||||
var queryString = $.param(queryObj);
|
||||
fileURI = '/web/image?' + queryString
|
||||
}
|
||||
fileURI = encodeURIComponent(fileURI);
|
||||
var viewerURL = '/web/static/lib/pdfjs/web/viewer.html?file=';
|
||||
return viewerURL + url + '#page=' + page;
|
||||
return viewerURL + fileURI + '#page=' + page;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
@@ -1701,14 +1746,16 @@ var FieldPdfViewer = FieldBinaryFile.extend({
|
||||
*/
|
||||
on_file_change: function (ev) {
|
||||
this._super.apply(this, arguments);
|
||||
var files = ev.target.files;
|
||||
if (!files || files.length === 0) {
|
||||
return;
|
||||
}
|
||||
// TOCheck: is there requirement to fallback on FileReader if browser don't support URL
|
||||
var fileURI = URL.createObjectURL(files[0]);
|
||||
if (this.PDFViewerApplication) {
|
||||
var files = ev.target.files;
|
||||
if (!files || files.length === 0) {
|
||||
return;
|
||||
}
|
||||
var file = files[0];
|
||||
// TOCheck: is there requirement to fallback on FileReader if browser don't support URL
|
||||
this.PDFViewerApplication.open(URL.createObjectURL(file), 0);
|
||||
this.PDFViewerApplication.open(fileURI, 0);
|
||||
} else {
|
||||
this.$('.o_pdfview_iframe').attr('src', this._getURI(fileURI));
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -2584,10 +2584,12 @@ var FieldSelectionBadge = FieldSelection.extend({
|
||||
* a FieldMany2one for its value.
|
||||
* Its intern representation is similar to the many2one (a datapoint with a
|
||||
* `name_get` as data).
|
||||
* Note that there is some logic to support char field because of one use in our
|
||||
* codebase, but this use should be removed along with this note.
|
||||
*/
|
||||
var FieldReference = FieldMany2One.extend({
|
||||
specialData: "_fetchSpecialReference",
|
||||
supportedFieldTypes: ['char', 'reference'],
|
||||
supportedFieldTypes: ['reference'],
|
||||
template: 'FieldReference',
|
||||
events: _.extend({}, FieldMany2One.prototype.events, {
|
||||
'change select': '_onSelectionChange',
|
||||
|
||||
@@ -58,6 +58,8 @@ var NotificationService = AbstractService.extend({
|
||||
* Note that this method does not wait for the appendTo method to complete.
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {function} [params.Notification] javascript class of a notification
|
||||
* to instantiate by default use 'web.Notification'
|
||||
* @param {string} params.title notification title
|
||||
* @param {string} params.message notification main message
|
||||
* @param {string} params.type 'notification' or 'warning'
|
||||
@@ -78,7 +80,8 @@ var NotificationService = AbstractService.extend({
|
||||
this.$el = $('<div class="o_notification_manager"/>');
|
||||
this.$el.prependTo('body');
|
||||
}
|
||||
var notification = this.notifications[++id] = new Notification(this, params);
|
||||
var NotificationWidget = params.Notification || Notification;
|
||||
var notification = this.notifications[++id] = new NotificationWidget(this, params);
|
||||
notification.appendTo(this.$el);
|
||||
return id;
|
||||
},
|
||||
|
||||
@@ -308,3 +308,55 @@ var AbstractView = Class.extend({
|
||||
return AbstractView;
|
||||
|
||||
});
|
||||
|
||||
odoo.define('web.viewUtils', function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* FIXME: move this module to its own file in master
|
||||
*/
|
||||
|
||||
var utils = {
|
||||
/**
|
||||
* Returns the value of a group dataPoint, i.e. the value of the groupBy
|
||||
* field for the records in that group.
|
||||
*
|
||||
* @param {Object} group dataPoint of type list, corresponding to a group
|
||||
* @param {string} groupByField the name of the groupBy field
|
||||
* @returns {string | integer | false}
|
||||
*/
|
||||
getGroupValue: function (group, groupByField) {
|
||||
var groupedByField = group.fields[groupByField];
|
||||
switch (groupedByField.type) {
|
||||
case 'many2one':
|
||||
return group.res_id || false;
|
||||
case 'selection':
|
||||
var descriptor = _.findWhere(groupedByField.selection, group.value);
|
||||
return descriptor && descriptor[0];
|
||||
default:
|
||||
return group.value;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* States whether or not the quick create feature is available for the given
|
||||
* datapoint, depending on its groupBy field.
|
||||
*
|
||||
* @param {Object} list dataPoint of type list
|
||||
* @returns {Boolean} true iff the kanban quick create feature is available
|
||||
*/
|
||||
isQuickCreateEnabled: function (list) {
|
||||
var groupByField = list.groupedBy[0] && list.groupedBy[0].split(':')[0];
|
||||
if (!groupByField) {
|
||||
return false;
|
||||
}
|
||||
var availableTypes = ['char', 'boolean', 'many2one', 'selection'];
|
||||
if (!_.contains(availableTypes, list.fields[groupByField].type)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
return utils;
|
||||
|
||||
});
|
||||
|
||||
@@ -1016,27 +1016,51 @@ var BasicModel = AbstractModel.extend({
|
||||
route: '/web/dataset/resequence',
|
||||
params: params,
|
||||
})
|
||||
.then(function () {
|
||||
var offset = options.offset ? options.offset : 0;
|
||||
var old_data = data.data.slice();
|
||||
data.data = _.sortBy(data.data, function (d) {
|
||||
if (_.contains(resIDs, self.localData[d].res_id)) {
|
||||
return _.indexOf(resIDs, self.localData[d].res_id) + offset;
|
||||
} else {
|
||||
return _.indexOf(old_data, d);
|
||||
.then(function (wasResequenced) {
|
||||
if (!wasResequenced) {
|
||||
// the field on which the resequence was triggered does not
|
||||
// exist, so no resequence happened server-side
|
||||
return $.when();
|
||||
}
|
||||
var field = params.field ? params.field : 'sequence';
|
||||
|
||||
return self._rpc({
|
||||
model: modelName,
|
||||
method: 'read',
|
||||
args: [resIDs, [field]],
|
||||
}).then(function (records) {
|
||||
if (data.data.length) {
|
||||
var dataType = self.localData[data.data[0]].type;
|
||||
if (dataType === 'record') {
|
||||
_.each(data.data, function (dataPoint) {
|
||||
var recordData = self.localData[dataPoint].data;
|
||||
var inRecords = _.findWhere(records, {id: recordData.id});
|
||||
if (inRecords) {
|
||||
recordData[field] = inRecords[field];
|
||||
}
|
||||
});
|
||||
data.data = _.sortBy(data.data, function (d) {
|
||||
return self.localData[d].data[field];
|
||||
});
|
||||
}
|
||||
if (dataType === 'list') {
|
||||
data.data = _.sortBy(data.data, function (d) {
|
||||
return _.indexOf(resIDs, self.localData[d].res_id)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
data.res_ids = [];
|
||||
_.each(data.data, function (d) {
|
||||
var dataPoint = self.localData[d];
|
||||
if (dataPoint.type === 'record') {
|
||||
data.res_ids.push(dataPoint.res_id);
|
||||
} else {
|
||||
data.res_ids = data.res_ids.concat(dataPoint.res_ids);
|
||||
}
|
||||
});
|
||||
self._updateParentResIDs(data);
|
||||
return parentID;
|
||||
data.res_ids = [];
|
||||
_.each(data.data, function (d) {
|
||||
var dataPoint = self.localData[d];
|
||||
if (dataPoint.type === 'record') {
|
||||
data.res_ids.push(dataPoint.res_id);
|
||||
} else {
|
||||
data.res_ids = data.res_ids.concat(dataPoint.res_ids);
|
||||
}
|
||||
});
|
||||
self._updateParentResIDs(data);
|
||||
return parentID;
|
||||
})
|
||||
});
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -225,7 +225,7 @@ var BasicView = AbstractView.extend({
|
||||
attrs.options = attrs.options ? pyUtils.py_eval(attrs.options) : {};
|
||||
}
|
||||
|
||||
if (attrs.on_change && !field.onChange) {
|
||||
if (attrs.on_change && attrs.on_change !== "0" && !field.onChange) {
|
||||
field.onChange = "1";
|
||||
}
|
||||
|
||||
@@ -378,6 +378,10 @@ var BasicView = AbstractView.extend({
|
||||
if (!(dependency_name in fields)) {
|
||||
fields[dependency_name] = dependency_dict;
|
||||
}
|
||||
|
||||
if (fv.fields && !(dependency_name in fv.fields)) {
|
||||
fv.fields[dependency_name] = dependency_dict;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -77,9 +77,9 @@ var SidebarFilter = Widget.extend(FieldManagerMixin, {
|
||||
self.model.get(recordID),
|
||||
{
|
||||
mode: 'edit',
|
||||
can_create: false,
|
||||
attrs: {
|
||||
'placeholder': _.str.sprintf(_t("Add %s"), self.title),
|
||||
placeholder: _.str.sprintf(_t("Add %s"), self.title),
|
||||
can_create: false
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -995,10 +995,7 @@ var FormRenderer = BasicRenderer.extend({
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onNavigationMove: function (ev) {
|
||||
if (ev.data.direction !== "cancel") {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
ev.stopPropagation();
|
||||
var index;
|
||||
if (ev.data.direction === "next") {
|
||||
index = this.allFieldWidgets[this.state.id].indexOf(ev.data.target || ev.target);
|
||||
|
||||
@@ -7,6 +7,7 @@ var Dialog = require('web.Dialog');
|
||||
var KanbanRecord = require('web.KanbanRecord');
|
||||
var RecordQuickCreate = require('web.kanban_record_quick_create');
|
||||
var view_dialogs = require('web.view_dialogs');
|
||||
var viewUtils = require('web.viewUtils');
|
||||
var Widget = require('web.Widget');
|
||||
var KanbanColumnProgressBar = require('web.KanbanColumnProgressBar');
|
||||
|
||||
@@ -201,7 +202,7 @@ var KanbanColumn = Widget.extend({
|
||||
this.trigger_up('close_quick_create'); // close other quick create widgets
|
||||
this.trigger_up('start_quick_create');
|
||||
var context = this.data.getContext();
|
||||
context['default_' + this.groupedBy] = this.id;
|
||||
context['default_' + this.groupedBy] = viewUtils.getGroupValue(this.data, this.groupedBy);
|
||||
this.quickCreateWidget = new RecordQuickCreate(this, {
|
||||
context: context,
|
||||
formViewRef: this.quickCreateView,
|
||||
|
||||
@@ -12,6 +12,7 @@ var Context = require('web.Context');
|
||||
var core = require('web.core');
|
||||
var Domain = require('web.Domain');
|
||||
var view_dialogs = require('web.view_dialogs');
|
||||
var viewUtils = require('web.viewUtils');
|
||||
|
||||
var _t = core._t;
|
||||
var qweb = core.qweb;
|
||||
@@ -35,12 +36,15 @@ var KanbanController = BasicController.extend({
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} params
|
||||
* @param {boolean} params.quickCreateEnabled set to false to disable the
|
||||
* quick create feature
|
||||
*/
|
||||
init: function (parent, model, renderer, params) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this.on_create = params.on_create;
|
||||
this.hasButtons = params.hasButtons;
|
||||
this.quickCreateEnabled = params.quickCreateEnabled;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -268,8 +272,8 @@ var KanbanController = BasicController.extend({
|
||||
*/
|
||||
_onButtonNew: function () {
|
||||
var state = this.model.get(this.handle, {raw: true});
|
||||
var hasColumns = state.groupedBy.length > 0 && state.data.length > 0;
|
||||
if (hasColumns && this.on_create === 'quick_create') {
|
||||
var quickCreateEnabled = this.quickCreateEnabled && viewUtils.isQuickCreateEnabled(state);
|
||||
if (this.on_create === 'quick_create' && quickCreateEnabled) {
|
||||
// Activate the quick create in the first column
|
||||
this.renderer.addQuickCreate();
|
||||
} else if (this.on_create && this.on_create !== 'quick_create') {
|
||||
@@ -396,10 +400,11 @@ var KanbanController = BasicController.extend({
|
||||
var columnState = self.model.get(column.db_id, {raw: true});
|
||||
var context = columnState.getContext();
|
||||
var state = self.model.get(self.handle, {raw: true});
|
||||
context['default_' + state.groupedBy[0]] = columnState.res_id;
|
||||
var groupedBy = state.groupedBy[0];
|
||||
context['default_' + groupedBy] = viewUtils.getGroupValue(columnState, groupedBy);
|
||||
new view_dialogs.FormViewDialog(self, {
|
||||
res_model: state.model,
|
||||
context: _.extend({default_name: name}, context),
|
||||
context: _.extend({default_name: values.name || values.display_name}, context),
|
||||
title: _t("Create"),
|
||||
disable_multiple_selection: true,
|
||||
on_saved: function (record) {
|
||||
|
||||
@@ -7,6 +7,7 @@ odoo.define('web.KanbanModel', function (require) {
|
||||
*/
|
||||
|
||||
var BasicModel = require('web.BasicModel');
|
||||
var viewUtils = require('web.viewUtils');
|
||||
|
||||
var KanbanModel = BasicModel.extend({
|
||||
|
||||
@@ -105,7 +106,8 @@ var KanbanModel = BasicModel.extend({
|
||||
var group = this.localData[groupID];
|
||||
var context = this._getContext(group);
|
||||
var parent = this.localData[group.parentID];
|
||||
context['default_' + parent.groupedBy[0]] = group.res_id;
|
||||
var groupedBy = parent.groupedBy;
|
||||
context['default_' + groupedBy] = viewUtils.getGroupValue(group, groupedBy);
|
||||
var def;
|
||||
if (Object.keys(values).length === 1 && 'display_name' in values) {
|
||||
// only 'display_name is given, perform a 'name_create'
|
||||
@@ -132,7 +134,7 @@ var KanbanModel = BasicModel.extend({
|
||||
},
|
||||
/**
|
||||
* Add the following (kanban specific) keys when performing a `get`:
|
||||
*
|
||||
*
|
||||
* - tooltipData
|
||||
* - progressBarValues
|
||||
* - isGroupedByM2ONoColumn
|
||||
|
||||
@@ -509,10 +509,12 @@ var KanbanRecord = Widget.extend({
|
||||
ischild = false;
|
||||
}
|
||||
var test_event = events && events.click && (events.click.length > 1 || events.click[0].namespace !== 'bs.tooltip');
|
||||
var testLinkWithHref = elem.nodeName.toLowerCase() === 'a' && elem.href;
|
||||
if (ischild) {
|
||||
children.push(elem);
|
||||
if (test_event) {
|
||||
// do not trigger global click if one child has a click event registered
|
||||
if (test_event || testLinkWithHref) {
|
||||
// Do not trigger global click if one child has a click
|
||||
// event registered (or it is a link with href)
|
||||
trigger = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ var ColumnQuickCreate = require('web.kanban_column_quick_create');
|
||||
var QWeb = require('web.QWeb');
|
||||
var session = require('web.session');
|
||||
var utils = require('web.utils');
|
||||
var viewUtils = require('web.viewUtils');
|
||||
|
||||
var qweb = core.qweb;
|
||||
|
||||
@@ -98,6 +99,9 @@ var KanbanRenderer = BasicRenderer.extend({
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} params
|
||||
* @param {boolean} params.quickCreateEnabled set to false to disable the
|
||||
* quick create feature
|
||||
*/
|
||||
init: function (parent, state, params) {
|
||||
this._super.apply(this, arguments);
|
||||
@@ -116,6 +120,7 @@ var KanbanRenderer = BasicRenderer.extend({
|
||||
if (this.columnOptions.hasProgressBar) {
|
||||
this.columnOptions.progressBarStates = {};
|
||||
}
|
||||
this.quickCreateEnabled = params.quickCreateEnabled;
|
||||
this._setState(state);
|
||||
},
|
||||
/**
|
||||
@@ -453,6 +458,7 @@ var KanbanRenderer = BasicRenderer.extend({
|
||||
groupedBy: groupByField,
|
||||
grouped_by_m2o: this.groupedByM2O,
|
||||
relation: relation,
|
||||
quick_create: this.quickCreateEnabled && viewUtils.isQuickCreateEnabled(state),
|
||||
});
|
||||
this.createColumnEnabled = this.groupedByM2O && this.columnOptions.group_creatable;
|
||||
},
|
||||
|
||||
@@ -63,7 +63,6 @@ var KanbanView = BasicView.extend({
|
||||
deletable: activeActions.group_delete,
|
||||
archivable: this.arch.attrs.archivable ? JSON.parse(this.arch.attrs.archivable) : true,
|
||||
group_creatable: activeActions.group_create && !config.device.isMobile,
|
||||
quick_create: params.isQuickCreateEnabled || this._isQuickCreateEnabled(),
|
||||
quickCreateView: this.arch.attrs.quick_create_view || null,
|
||||
hasProgressBar: !!progressBar,
|
||||
};
|
||||
@@ -72,6 +71,7 @@ var KanbanView = BasicView.extend({
|
||||
deletable: activeActions.delete,
|
||||
read_only_mode: params.readOnlyMode,
|
||||
};
|
||||
this.rendererParams.quickCreateEnabled = this._isQuickCreateEnabled();
|
||||
var examples = this.arch.attrs.examples;
|
||||
if (examples) {
|
||||
this.rendererParams.examples = kanbanExamplesRegistry.get(examples);
|
||||
@@ -80,6 +80,7 @@ var KanbanView = BasicView.extend({
|
||||
this.controllerParams.on_create = this.arch.attrs.on_create;
|
||||
this.controllerParams.readOnlyMode = false;
|
||||
this.controllerParams.hasButtons = true;
|
||||
this.controllerParams.quickCreateEnabled = this.rendererParams.quickCreateEnabled;
|
||||
|
||||
if (config.device.isMobile) {
|
||||
this.jsLibs.push('/web/static/lib/jquery.touchSwipe/jquery.touchSwipe.js');
|
||||
@@ -93,13 +94,10 @@ var KanbanView = BasicView.extend({
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} viewInfo
|
||||
* @returns {boolean} true iff the quick create feature is not explicitely
|
||||
* disabled (with create="False" or quick_create="False" in the arch)
|
||||
*/
|
||||
_isQuickCreateEnabled: function () {
|
||||
var groupBy = this.loadParams.groupBy[0];
|
||||
groupBy = groupBy !== undefined ? groupBy.split(':')[0] : undefined;
|
||||
if (groupBy !== undefined && !_.contains(['char', 'boolean', 'many2one'], this.fields[groupBy].type)) {
|
||||
return false;
|
||||
}
|
||||
if (!this.controllerParams.activeActions.create) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -810,7 +810,7 @@
|
||||
|
||||
<t t-name="FieldMany2One">
|
||||
<t t-if="widget.mode === 'readonly'">
|
||||
<a t-if="!widget.nodeOptions.no_open" class="o_form_uri" href="#"/>
|
||||
<a t-if="!widget.nodeOptions.no_open" t-att-tabindex="widget.attrs.tabindex" class="o_form_uri" href="#"/>
|
||||
<span t-if="widget.nodeOptions.no_open"/>
|
||||
</t>
|
||||
<div t-if="widget.mode === 'edit'" class="o_field_widget o_field_many2one" aria-atomic="true">
|
||||
|
||||
@@ -10,6 +10,7 @@ var KanbanView = require('web.KanbanView');
|
||||
var ListView = require('web.ListView');
|
||||
var session = require('web.session');
|
||||
var testUtils = require('web.test_utils');
|
||||
var field_registry = require('web.field_registry');
|
||||
|
||||
var createView = testUtils.createView;
|
||||
var createAsyncView = testUtils.createAsyncView;
|
||||
@@ -1907,6 +1908,49 @@ QUnit.module('basic_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("pdf_viewer: upload rendering", function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
testUtils.patch(field_registry.map.pdf_viewer, {
|
||||
on_file_change: function (ev) {
|
||||
ev.target = {files: [new Blob()]};
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
_getURI: function (fileURI) {
|
||||
var res = this._super.apply(this, arguments);
|
||||
assert.step('_getURI');
|
||||
assert.ok(_.str.startsWith(fileURI, 'blob:'));
|
||||
this.PDFViewerApplication = {
|
||||
open: function (URI) {
|
||||
assert.step('open');
|
||||
assert.ok(_.str.startsWith(URI, 'blob:'));
|
||||
},
|
||||
};
|
||||
return 'about:blank';
|
||||
},
|
||||
});
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:
|
||||
'<form>' +
|
||||
'<field name="document" widget="pdf_viewer"/>' +
|
||||
'</form>',
|
||||
});
|
||||
|
||||
// first upload initialize iframe
|
||||
form.$('input[type="file"]').trigger('change');
|
||||
assert.verifySteps(['_getURI']);
|
||||
// second upload call pdfjs method inside iframe
|
||||
form.$('input[type="file"]').trigger('change');
|
||||
assert.verifySteps(['_getURI', 'open']);
|
||||
|
||||
testUtils.unpatch(field_registry.map.pdf_viewer);
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('text field rendering in list view', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
@@ -2106,6 +2150,40 @@ QUnit.module('basic_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('image fields in x2many list are loaded correctly', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.data.partner_type.fields.image = {name: 'image', type: 'binary'};
|
||||
this.data.partner_type.records[0].image = 'product_image';
|
||||
this.data.partner.records[0].timmy = [12];
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="timmy" widget="many2many">' +
|
||||
'<tree>' +
|
||||
'<field name="image" widget="image"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
mockRPC: function (route, args) {
|
||||
if (route === 'data:image/png;base64,product_image') {
|
||||
assert.ok(true, "The list's image should have been fetched");
|
||||
return $.when();
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(form.$('tr.o_data_row').length, 1,
|
||||
'There should be one record in the many2many');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('image fields with required attribute', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
@@ -2779,8 +2857,7 @@ QUnit.module('basic_fields', {
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
assert.strictEqual(form.$('.o_datepicker_input').val(), expectedDateString,
|
||||
'the datetime should be correct in edit mode');
|
||||
// click on the input and select 22 February at 8:23:33
|
||||
form.$('.o_datepicker_input').click();
|
||||
// select 22 February at 8:23:33
|
||||
assert.ok($('.bootstrap-datetimepicker-widget').length, 'datepicker should be open');
|
||||
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Month selection
|
||||
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Year selection
|
||||
@@ -2900,8 +2977,7 @@ QUnit.module('basic_fields', {
|
||||
assert.strictEqual(list.$('input.o_datepicker_input').val(), expectedDateString,
|
||||
'the date should be correct in edit mode');
|
||||
|
||||
// click on the input and select 22 February at 8:23:33
|
||||
list.$('input.o_datepicker_input').click();
|
||||
// select 22 February at 8:23:33
|
||||
assert.ok($('.bootstrap-datetimepicker-widget').length, 'datepicker should be open');
|
||||
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Month selection
|
||||
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Year selection
|
||||
@@ -3061,6 +3137,32 @@ QUnit.module('basic_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('datepicker option: daysOfWeekDisabled', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.data.partner.fields.datetime.default = "2017-08-02 12:00:05";
|
||||
this.data.partner.fields.datetime.required = true;
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form string="Partners">' +
|
||||
'<field name="datetime" ' +
|
||||
'options=\'{"datepicker": {"daysOfWeekDisabled": [0, 6]}}\'/>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
form.$buttons.find('.o_form_button_create').click();
|
||||
assert.ok($('.day:last-child(),.day:nth-child(2)').hasClass('disabled'),
|
||||
'first and last days must be disabled');
|
||||
assert.notOk($('.day:not(:last-child()):not(:nth-child(2))').hasClass('disabled'),
|
||||
'other days must stay clickable');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.module('FieldMonetary');
|
||||
|
||||
QUnit.test('monetary field in form view', function (assert) {
|
||||
|
||||
@@ -5580,7 +5580,7 @@ QUnit.module('relational_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('pressing enter in a o2m with a required empty m2o', function (assert) {
|
||||
QUnit.test('pressing enter in a o2m with a required empty field', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
this.data.turtle.fields.turtle_foo.required = true;
|
||||
@@ -10259,6 +10259,81 @@ QUnit.module('relational_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('click on URL should not open the record', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.data.partner.records[0].turtles = [1];
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form string="Partners">' +
|
||||
'<field name="turtles">' +
|
||||
'<tree>' +
|
||||
'<field name="display_name" widget="email"/>' +
|
||||
'<field name="turtle_foo" widget="url"/>' +
|
||||
'</tree>' +
|
||||
'<form></form>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
form.$('.o_email_cell a').click();
|
||||
assert.strictEqual($('.modal .o_form_view').length, 0,
|
||||
'click should not open the modal');
|
||||
|
||||
form.$('.o_url_cell a').click();
|
||||
assert.strictEqual($('.modal .o_form_view').length, 0,
|
||||
'click should not open the modal');
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('create and edit on m2o in o2m, and press ESCAPE', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<field name="turtles">' +
|
||||
'<tree editable="top">' +
|
||||
'<field name="turtle_trululu"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
archs: {
|
||||
'partner,false,form': '<form><field name="display_name"/></form>',
|
||||
},
|
||||
});
|
||||
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
|
||||
assert.strictEqual(form.$('.o_selected_row').length, 1,
|
||||
"should have create a new row in edition");
|
||||
|
||||
var $input = form.$('.o_field_widget[name="turtle_trululu"] input');
|
||||
$input.click();
|
||||
$input.autocomplete('widget').find('.o_m2o_dropdown_option').focus().click();
|
||||
|
||||
assert.strictEqual($('.modal .o_form_view').length, 1,
|
||||
"should have opened a form view in a dialog");
|
||||
|
||||
$('.modal .o_form_view .o_field_widget[name=display_name]').trigger($.Event('keydown', {
|
||||
which: $.ui.keyCode.ESCAPE,
|
||||
keyCode: $.ui.keyCode.ESCAPE,
|
||||
}));
|
||||
|
||||
assert.strictEqual($('.modal .o_form_view').length, 0,
|
||||
"should have closed the dialog");
|
||||
assert.strictEqual(form.$('.o_selected_row').length, 1,
|
||||
"new row should still be present");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('one2many add a line should not crash if orderedResIDs is not set', function (assert) {
|
||||
// There is no assertion, the code will just crash before the bugfix.
|
||||
assert.expect(0);
|
||||
@@ -13679,6 +13754,59 @@ QUnit.module('relational_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('when creating a new many2one on a x2many then discarding it immediately with ESCAPE, it should not crash', function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(1);
|
||||
|
||||
this.data.partner.records[0].turtles = [];
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
data: this.data,
|
||||
arch:'<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<field name="turtles">' +
|
||||
'<tree editable="top">' +
|
||||
'<field name="turtle_foo"/>' +
|
||||
'<field name="turtle_trululu"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
archs: {
|
||||
'partner,false,form': '<form><group><field name="foo"/><field name="bar"/></group></form>'
|
||||
},
|
||||
});
|
||||
|
||||
// add a new line
|
||||
form.$el.find('.o_field_x2many_list_row_add>a').click();
|
||||
|
||||
// open the field turtle_trululu (one2many)
|
||||
var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
|
||||
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
|
||||
form.$el.find('.o_input_dropdown>input').click();
|
||||
|
||||
concurrency.delay(0).then(function () {
|
||||
// click create and edit
|
||||
$('.ui-autocomplete .ui-menu-item a:contains(Create and)').trigger('mouseenter').click();
|
||||
|
||||
// hit escape immediately
|
||||
var escapeKey = $.ui.keyCode.ESCAPE;
|
||||
$(document.activeElement).trigger(
|
||||
$.Event('keydown', {which: escapeKey, keyCode: escapeKey}));
|
||||
|
||||
assert.ok('did not crash');
|
||||
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
|
||||
form.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('navigating through an editable list with custom controls [REQUIRE FOCUS]', function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
|
||||
@@ -904,10 +904,14 @@ var MockServer = Class.extend({
|
||||
var offset = args.offset ? Number(args.offset) : 0;
|
||||
var field = args.field ? args.field : 'sequence';
|
||||
var records = this.data[args.model].records;
|
||||
if (!(field in this.data[args.model].fields)) {
|
||||
return false;
|
||||
}
|
||||
for (var i in args.ids) {
|
||||
var record = _.findWhere(records, {id: args.ids[i]});
|
||||
record[field] = Number(i) + offset;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* Simulate a 'search_count' operation
|
||||
|
||||
@@ -257,6 +257,12 @@ function createAsyncView(params) {
|
||||
controlPanel.appendTo($web_client);
|
||||
var $content = $('<div>').addClass('o_content').appendTo($web_client);
|
||||
|
||||
if (params.interceptsPropagate) {
|
||||
_.each(params.interceptsPropagate, function (cb, name) {
|
||||
intercept(widget, name, cb, true);
|
||||
});
|
||||
}
|
||||
|
||||
return view.getController(widget).then(function (view) {
|
||||
// override the view's 'destroy' so that it calls 'destroy' on the widget
|
||||
// instead, as the widget is the parent of the view and the mockServer.
|
||||
|
||||
@@ -246,4 +246,38 @@ QUnit.module('Services', {
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('Display a custom notification', function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(3);
|
||||
|
||||
var Custom = Notification.extend({
|
||||
init: function (parent, params) {
|
||||
this._super.apply(this, arguments);
|
||||
assert.ok(params.customParams, 'instantiate custom notification');
|
||||
},
|
||||
start: function () {
|
||||
var self = this;
|
||||
return this._super().then(function () {
|
||||
self.$el.html('Custom');
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
var view = createView(this.viewParams);
|
||||
view.call('notification', 'notify', {
|
||||
Notification: Custom,
|
||||
customParams: true,
|
||||
});
|
||||
assert.strictEqual(
|
||||
$('body .o_notification_manager .o_notification:contains(Custom)').length, 1,
|
||||
"should display the notification");
|
||||
view.destroy();
|
||||
setTimeout(function () {
|
||||
assert.strictEqual(
|
||||
$('body .o_notification_manager .o_notification').length, 0,
|
||||
"should destroy the notification");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});});
|
||||
|
||||
@@ -192,6 +192,8 @@ QUnit.module('Views', {
|
||||
var done = assert.async();
|
||||
assert.expect(8);
|
||||
|
||||
this.data.product.fields.sequence = {string: "Sequence", type: "integer"};
|
||||
this.data.partner.fields.sequence = {string: "Sequence", type: "integer"};
|
||||
this.data.partner.records.push({id: 3, foo: 'aaa', product_id: 37});
|
||||
|
||||
var nbReseq = 0;
|
||||
@@ -204,7 +206,7 @@ QUnit.module('Views', {
|
||||
if (nbReseq === 1) { // resequencing columns
|
||||
assert.deepEqual(args.ids, [41, 37],
|
||||
"ids should be correct");
|
||||
assert.strictEqual(args.model, 'product_id',
|
||||
assert.strictEqual(args.model, 'product',
|
||||
"model should be correct");
|
||||
} else if (nbReseq === 2) { // resequencing records
|
||||
assert.deepEqual(args.ids, [3, 1],
|
||||
@@ -212,7 +214,6 @@ QUnit.module('Views', {
|
||||
assert.strictEqual(args.model, 'partner',
|
||||
"model should be correct");
|
||||
}
|
||||
return $.when();
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
@@ -229,7 +230,7 @@ QUnit.module('Views', {
|
||||
"first group should be res_id 37");
|
||||
|
||||
// resequence columns
|
||||
return model.resequence('product_id', [41, 37], stateID);
|
||||
return model.resequence('product', [41, 37], stateID);
|
||||
})
|
||||
.then(function (stateID) {
|
||||
var state = model.get(stateID);
|
||||
|
||||
@@ -1559,7 +1559,7 @@ QUnit.module('Views', {
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('quick create record fail in grouped', function (assert) {
|
||||
QUnit.test('quick create record fail in grouped by many2one', function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
var kanban = createView({
|
||||
@@ -1684,6 +1684,120 @@ QUnit.module('Views', {
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('quick create record fails in grouped by char', function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div><field name="foo"/></div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
archs: {
|
||||
'partner,false,form': '<form>' +
|
||||
'<field name="foo"/>' +
|
||||
'</form>',
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'name_create') {
|
||||
return $.Deferred().reject({
|
||||
code: 200,
|
||||
data: {},
|
||||
message: "Odoo server error",
|
||||
}, $.Event());
|
||||
}
|
||||
if (args.method === 'create') {
|
||||
assert.deepEqual(args.args[0], {foo: 'yop'},
|
||||
"should write the correct value for foo");
|
||||
assert.deepEqual(args.kwargs.context, {default_foo: 'yop', default_name: 'test'},
|
||||
"should send the correct default value for foo");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
groupBy: ['foo'],
|
||||
});
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
|
||||
"there should be 1 record in first column");
|
||||
|
||||
kanban.$('.o_kanban_header:first .o_kanban_quick_add i').click();
|
||||
kanban.$('.o_kanban_quick_create input').val('test').trigger('input');
|
||||
kanban.$('.o_kanban_add').click();
|
||||
|
||||
assert.strictEqual($('.modal .o_form_view.o_form_editable').length, 1,
|
||||
"a form view dialog should have been opened (in edit)");
|
||||
assert.strictEqual($('.modal .o_field_widget[name=foo]').val(), 'yop',
|
||||
"the correct default value for foo should already be set");
|
||||
|
||||
$('.modal-footer .btn-primary').click();
|
||||
|
||||
assert.strictEqual($('.modal').length, 0, "the modal should be closed");
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2,
|
||||
"there should be 2 records in first column");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('quick create record fails in grouped by selection', function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div><field name="state"/></div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
archs: {
|
||||
'partner,false,form': '<form>' +
|
||||
'<field name="state"/>' +
|
||||
'</form>',
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'name_create') {
|
||||
return $.Deferred().reject({
|
||||
code: 200,
|
||||
data: {},
|
||||
message: "Odoo server error",
|
||||
}, $.Event());
|
||||
}
|
||||
if (args.method === 'create') {
|
||||
assert.deepEqual(args.args[0], {state: 'abc'},
|
||||
"should write the correct value for state");
|
||||
assert.deepEqual(args.kwargs.context, {default_state: 'abc', default_name: 'test'},
|
||||
"should send the correct default value for state");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
groupBy: ['state'],
|
||||
});
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
|
||||
"there should be 1 record in first column");
|
||||
|
||||
kanban.$('.o_kanban_header:first .o_kanban_quick_add i').click();
|
||||
kanban.$('.o_kanban_quick_create input').val('test').trigger('input');
|
||||
kanban.$('.o_kanban_add').click();
|
||||
|
||||
assert.strictEqual($('.modal .o_form_view.o_form_editable').length, 1,
|
||||
"a form view dialog should have been opened (in edit)");
|
||||
assert.strictEqual($('.modal .o_field_widget[name=state]').val(), '"abc"',
|
||||
"the correct default value for state should already be set");
|
||||
|
||||
$('.modal-footer .btn-primary').click();
|
||||
|
||||
assert.strictEqual($('.modal').length, 0, "the modal should be closed");
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2,
|
||||
"there should be 2 records in first column");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('quick create record in empty grouped kanban', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
@@ -1726,6 +1840,327 @@ QUnit.module('Views', {
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('quick create record in grouped on date(time) field', function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div><field name="display_name"/></div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
groupBy: ['date'],
|
||||
intercepts: {
|
||||
switch_view: function (ev) {
|
||||
assert.deepEqual(_.pick(ev.data, 'res_id', 'view_type'), {
|
||||
res_id: undefined,
|
||||
view_type: 'form',
|
||||
}, "should trigger an event to open the form view (twice)");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 0,
|
||||
"quick create should be disabled when grouped on a date field");
|
||||
|
||||
// clicking on CREATE in control panel should not open a quick create
|
||||
kanban.$buttons.find('.o-kanban-button-new').click();
|
||||
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0,
|
||||
"should not have opened the quick create widget");
|
||||
|
||||
kanban.reload({groupBy: ['datetime']});
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 0,
|
||||
"quick create should be disabled when grouped on a datetime field");
|
||||
|
||||
// clicking on CREATE in control panel should not open a quick create
|
||||
kanban.$buttons.find('.o-kanban-button-new').click();
|
||||
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0,
|
||||
"should not have opened the quick create widget");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('quick create record feature is properly enabled/disabled at reload', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div><field name="display_name"/></div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
groupBy: ['foo'],
|
||||
});
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 3,
|
||||
"quick create should be enabled when grouped on a char field");
|
||||
|
||||
kanban.reload({groupBy: ['date']});
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 0,
|
||||
"quick create should now be disabled (grouped on date field)");
|
||||
|
||||
kanban.reload({groupBy: ['bar']});
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 2,
|
||||
"quick create should be enabled again (grouped on boolean field)");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('quick create record in grouped by char field', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div><field name="display_name"/></div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
groupBy: ['foo'],
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'name_create') {
|
||||
assert.deepEqual(args.kwargs.context, {default_foo: 'yop'},
|
||||
"should send the correct default value for foo");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 3,
|
||||
"quick create should be enabled when grouped on a char field");
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
|
||||
"first column should contain 1 record");
|
||||
|
||||
kanban.$('.o_kanban_header:first .o_kanban_quick_add i').click();
|
||||
kanban.$('.o_kanban_quick_create input').val('new record').trigger('input');
|
||||
kanban.$('.o_kanban_add').click();
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2,
|
||||
"first column should now contain 2 records");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('quick create record in grouped by boolean field', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div><field name="display_name"/></div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
groupBy: ['bar'],
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'name_create') {
|
||||
assert.deepEqual(args.kwargs.context, {default_bar: true},
|
||||
"should send the correct default value for bar");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 2,
|
||||
"quick create should be enabled when grouped on a boolean field");
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:nth(1) .o_kanban_record').length, 3,
|
||||
"second column (true) should contain 3 records");
|
||||
|
||||
kanban.$('.o_kanban_header:nth(1) .o_kanban_quick_add i').click();
|
||||
kanban.$('.o_kanban_quick_create input').val('new record').trigger('input');
|
||||
kanban.$('.o_kanban_add').click();
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:nth(1) .o_kanban_record').length, 4,
|
||||
"second column (true) should now contain 4 records");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('quick create record in grouped on selection field', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div><field name="display_name"/></div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'name_create') {
|
||||
assert.deepEqual(args.kwargs.context, {default_state: 'abc'},
|
||||
"should send the correct default value for bar");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
groupBy: ['state'],
|
||||
});
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 3,
|
||||
"quick create should be enabled when grouped on a selection field");
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
|
||||
"first column (abc) should contain 1 record");
|
||||
|
||||
kanban.$('.o_kanban_header:first .o_kanban_quick_add i').click();
|
||||
kanban.$('.o_kanban_quick_create input').val('new record').trigger('input');
|
||||
kanban.$('.o_kanban_add').click();
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2,
|
||||
"first column (abc) should contain 2 records");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('quick create record in grouped by char field (within quick_create_view)', function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban on_create="quick_create" quick_create_view="some_view_ref">' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div><field name="foo"/></div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
archs: {
|
||||
'partner,some_view_ref,form': '<form>' +
|
||||
'<field name="foo"/>' +
|
||||
'</form>',
|
||||
},
|
||||
groupBy: ['foo'],
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'create') {
|
||||
assert.deepEqual(args.args[0], {foo: 'yop'},
|
||||
"should write the correct value for foo");
|
||||
assert.deepEqual(args.kwargs.context, {default_foo: 'yop'},
|
||||
"should send the correct default value for foo");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 3,
|
||||
"quick create should be enabled when grouped on a char field");
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
|
||||
"first column should contain 1 record");
|
||||
|
||||
kanban.$('.o_kanban_header:first .o_kanban_quick_add i').click();
|
||||
assert.strictEqual(kanban.$('.o_kanban_quick_create input').val(), 'yop',
|
||||
"should have set the correct foo value by default");
|
||||
kanban.$('.o_kanban_add').click();
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2,
|
||||
"first column should now contain 2 records");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('quick create record in grouped by boolean field (within quick_create_view)', function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban on_create="quick_create" quick_create_view="some_view_ref">' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div><field name="bar"/></div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
archs: {
|
||||
'partner,some_view_ref,form': '<form>' +
|
||||
'<field name="bar"/>' +
|
||||
'</form>',
|
||||
},
|
||||
groupBy: ['bar'],
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'create') {
|
||||
assert.deepEqual(args.args[0], {bar: true},
|
||||
"should write the correct value for bar");
|
||||
assert.deepEqual(args.kwargs.context, {default_bar: true},
|
||||
"should send the correct default value for bar");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 2,
|
||||
"quick create should be enabled when grouped on a boolean field");
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:nth(1) .o_kanban_record').length, 3,
|
||||
"second column (true) should contain 3 records");
|
||||
|
||||
kanban.$('.o_kanban_header:nth(1) .o_kanban_quick_add i').click();
|
||||
assert.ok(kanban.$('.o_kanban_quick_create .o_field_boolean input').is(':checked'),
|
||||
"should have set the correct bar value by default");
|
||||
kanban.$('.o_kanban_add').click();
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:nth(1) .o_kanban_record').length, 4,
|
||||
"second column (true) should now contain 4 records");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('quick create record in grouped by selection field (within quick_create_view)', function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban on_create="quick_create" quick_create_view="some_view_ref">' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div><field name="state"/></div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
archs: {
|
||||
'partner,some_view_ref,form': '<form>' +
|
||||
'<field name="state"/>' +
|
||||
'</form>',
|
||||
},
|
||||
groupBy: ['state'],
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'create') {
|
||||
assert.deepEqual(args.args[0], {state: 'abc'},
|
||||
"should write the correct value for state");
|
||||
assert.deepEqual(args.kwargs.context, {default_state: 'abc'},
|
||||
"should send the correct default value for state");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 3,
|
||||
"quick create should be enabled when grouped on a selection field");
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
|
||||
"first column (abc) should contain 1 record");
|
||||
|
||||
kanban.$('.o_kanban_header:first .o_kanban_quick_add i').click();
|
||||
assert.strictEqual(kanban.$('.o_kanban_quick_create select').val(), '"abc"',
|
||||
"should have set the correct state value by default");
|
||||
kanban.$('.o_kanban_add').click();
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2,
|
||||
"first column (abc) should now contain 2 records");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('many2many_tags in kanban views', function (assert) {
|
||||
assert.expect(12);
|
||||
|
||||
@@ -1796,6 +2231,72 @@ QUnit.module('Views', {
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Do not open record when clicking on `a` with `href`', function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
this.data.partner.records = [
|
||||
{ id: 1, foo: 'yop' },
|
||||
];
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban class="o_kanban_test">' +
|
||||
'<templates>' +
|
||||
'<t t-name="kanban-box">' +
|
||||
'<div class="oe_kanban_global_click">' +
|
||||
'<field name="foo"/>' +
|
||||
'<div>' +
|
||||
'<a class="o_test_link" href="#">test link</a>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</t>' +
|
||||
'</templates>' +
|
||||
'</kanban>',
|
||||
intercepts: {
|
||||
// when clicking on a record in kanban view,
|
||||
// it switches to form view.
|
||||
switch_view: function () {
|
||||
throw new Error("should not switch view");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var $record = kanban.$('.o_kanban_record:not(.o_kanban_ghost)');
|
||||
assert.strictEqual($record.length, 1,
|
||||
"should display a kanban record");
|
||||
|
||||
var $testLink = $record.find('a');
|
||||
assert.strictEqual($testLink.length, 1,
|
||||
"should contain a link in the kanban record");
|
||||
assert.ok(!!$testLink[0].href,
|
||||
"link inside kanban record should have non-empty href");
|
||||
|
||||
// Mocked views prevent accessing a link with href. This is intented
|
||||
// most of the time, but not in this test which specifically needs to
|
||||
// let the browser access a link with href.
|
||||
kanban.$el.off('click', 'a');
|
||||
// Prevent the browser default behaviour when clicking on anything.
|
||||
// This includes clicking on a `<a>` with `href`, so that it does not
|
||||
// change the URL in the address bar.
|
||||
// Note that we should not specify a click listener on 'a', otherwise
|
||||
// it may influence the kanban record global click handler to not open
|
||||
// the record.
|
||||
$(document.body).on('click.o_test', function (ev) {
|
||||
assert.notOk(ev.isDefaultPrevented(),
|
||||
"should not prevented browser default behaviour beforehand");
|
||||
assert.strictEqual(ev.target, $testLink[0],
|
||||
"should have clicked on the test link in the kanban record");
|
||||
ev.preventDefault();
|
||||
});
|
||||
|
||||
$testLink.click();
|
||||
|
||||
$(document.body).off('click.o_test');
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('can drag and drop a record from one column to the next', function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
@@ -2258,7 +2759,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('delete a column in grouped on m2o', function (assert) {
|
||||
assert.expect(33);
|
||||
assert.expect(36);
|
||||
|
||||
testUtils.patch(KanbanRenderer, {
|
||||
_renderGrouped: function () {
|
||||
@@ -3224,6 +3725,7 @@ QUnit.module('Views', {
|
||||
|
||||
QUnit.test('resequence columns in grouped by m2o', function (assert) {
|
||||
assert.expect(7);
|
||||
this.data.product.fields.sequence = {string: "Sequence", type: "integer"};
|
||||
|
||||
var envIDs = [1, 3, 2, 4]; // the ids that should be in the environment during this test
|
||||
var kanban = createView({
|
||||
@@ -3237,12 +3739,6 @@ QUnit.module('Views', {
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
groupBy: ['product_id'],
|
||||
mockRPC: function (route) {
|
||||
if (route === '/web/dataset/resequence') {
|
||||
return $.when();
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
intercepts: {
|
||||
env_updated: function (event) {
|
||||
assert.deepEqual(event.data.env.ids, envIDs,
|
||||
@@ -3266,7 +3762,7 @@ QUnit.module('Views', {
|
||||
kanban.update({}, {reload: false}); // re-render without reloading
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:first').data('id'), 5,
|
||||
"first column should be id 5 before resequencing");
|
||||
"first column should be id 5 after resequencing");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
@@ -4085,7 +4581,7 @@ QUnit.module('Views', {
|
||||
'<div><field name="foo"/></div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
groupBy: ['int_field'],
|
||||
groupBy: ['foo'],
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/web/dataset/resequence') {
|
||||
return $.when(true);
|
||||
|
||||
@@ -3223,10 +3223,10 @@ QUnit.module('Views', {
|
||||
foo: {
|
||||
fields: {int_field: {string: "int_field", type: "integer", sortable: true}},
|
||||
records: [
|
||||
{id: 1, int_field: 0},
|
||||
{id: 2, int_field: 1},
|
||||
{id: 3, int_field: 2},
|
||||
{id: 4, int_field: 3},
|
||||
{id: 1, int_field: 11},
|
||||
{id: 2, int_field: 12},
|
||||
{id: 3, int_field: 13},
|
||||
{id: 4, int_field: 14},
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -3245,14 +3245,15 @@ QUnit.module('Views', {
|
||||
assert.deepEqual(args, {
|
||||
model: "foo",
|
||||
ids: [4, 3],
|
||||
offset: 2,
|
||||
offset: 13,
|
||||
field: "int_field",
|
||||
});
|
||||
}
|
||||
if (moves === 1) {
|
||||
assert.deepEqual(args, {
|
||||
model: "foo",
|
||||
ids: [1, 4, 2, 3],
|
||||
ids: [4, 2],
|
||||
offset: 12,
|
||||
field: "int_field",
|
||||
});
|
||||
}
|
||||
@@ -3260,14 +3261,15 @@ QUnit.module('Views', {
|
||||
assert.deepEqual(args, {
|
||||
model: "foo",
|
||||
ids: [2, 4],
|
||||
offset: 1,
|
||||
offset: 12,
|
||||
field: "int_field",
|
||||
});
|
||||
}
|
||||
if (moves === 3) {
|
||||
assert.deepEqual(args, {
|
||||
model: "foo",
|
||||
ids: [1, 4, 2, 3],
|
||||
ids: [4, 2],
|
||||
offset: 12,
|
||||
field: "int_field",
|
||||
});
|
||||
}
|
||||
@@ -3334,7 +3336,6 @@ QUnit.module('Views', {
|
||||
"should write the right field as sequence");
|
||||
assert.deepEqual(args.ids, [4, 2, 3],
|
||||
"should write the sequence in correct order");
|
||||
return $.when();
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
@@ -3464,13 +3465,16 @@ QUnit.module('Views', {
|
||||
'</tree>',
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/web/dataset/resequence') {
|
||||
var _super = this._super.bind(this);
|
||||
assert.strictEqual(args.offset, 1,
|
||||
"should write the sequence starting from the lowest current one");
|
||||
assert.strictEqual(args.field, 'int_field',
|
||||
"should write the right field as sequence");
|
||||
assert.deepEqual(args.ids, [4, 2, 3],
|
||||
"should write the sequence in correct order");
|
||||
return $.when(def);
|
||||
return $.when(def).then(function () {
|
||||
return _super(route, args);
|
||||
});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
@@ -749,6 +749,7 @@ registry.colorpicker = SnippetOption.extend({
|
||||
*/
|
||||
_onColorResetButtonClick: function () {
|
||||
this.$target.removeClass(this.classes).css('background-color', '');
|
||||
self.$target.trigger('content_changed');
|
||||
this.$el.find('.colorpicker button.selected').removeClass('selected');
|
||||
},
|
||||
});
|
||||
@@ -896,7 +897,7 @@ registry.background = SnippetOption.extend({
|
||||
* @param {string} value
|
||||
*/
|
||||
_setCustomBackground: function (value) {
|
||||
this.__customImageSrc = this._getSrcFromCssValue(value);
|
||||
this.__customImageSrc = value;
|
||||
this.background(false, this.__customImageSrc);
|
||||
this.$target.addClass('oe_custom_bg');
|
||||
this._setActive();
|
||||
|
||||
@@ -368,7 +368,7 @@ var ImageWidget = MediaWidget.extend({
|
||||
if (needle && needle.length) {
|
||||
domain.push('|', ['datas_fname', 'ilike', needle], ['name', 'ilike', needle]);
|
||||
}
|
||||
domain.push('!', ['datas_fname', '=like', '%.crop'], '!', ['name', '=like', '%.crop']);
|
||||
domain.push('|', ['datas_fname', '=', false], '!', ['datas_fname', '=like', '%.crop'], '!', ['name', '=like', '%.crop']);
|
||||
return this._rpc({
|
||||
model: 'ir.attachment',
|
||||
method: 'search_read',
|
||||
|
||||
@@ -988,6 +988,9 @@ body.editor_enable {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
#web_editor_inside_iframe {
|
||||
|
||||
@@ -62,6 +62,7 @@ if (!$('.o_website_links_chart').length) {
|
||||
var chart = nv.models.lineChart()
|
||||
.x(function (d) { return getDate(d); })
|
||||
.y(function (d) { return getNbClicks(d); })
|
||||
.margin({top: 10, right: 60, bottom: 60, left: 60})
|
||||
.showYAxis(true)
|
||||
.showXAxis(true);
|
||||
|
||||
@@ -75,7 +76,7 @@ if (!$('.o_website_links_chart').length) {
|
||||
|
||||
chart.yAxis
|
||||
.tickFormat(d3.format("d"))
|
||||
.ticks(chart_data[0]['values'].length - 1);
|
||||
.ticks(Math.min(chart_data[0]['values'].length - 1, 10));
|
||||
|
||||
d3.select(self.$element + ' svg')
|
||||
.datum(chart_data)
|
||||
|
||||
@@ -1069,6 +1069,23 @@ class WebsiteSale(ProductConfiguratorController):
|
||||
phone_code=country.phone_code
|
||||
)
|
||||
|
||||
@http.route(['/shop/update_carrier'], type='json', auth='public', methods=['POST'], website=True, csrf=False)
|
||||
def update_eshop_carrier(self, **post):
|
||||
results = {}
|
||||
if hasattr(self, '_update_website_sale_delivery'):
|
||||
results.update(self._update_website_sale_delivery(**post))
|
||||
|
||||
if hasattr(self, '_update_website_sale_coupon'):
|
||||
results.update(self._update_website_sale_coupon(**post))
|
||||
|
||||
return results
|
||||
|
||||
def _format_amount(self, amount, currency):
|
||||
fmt = "%.{0}f".format(currency.decimal_places)
|
||||
lang = request.env['res.lang']._lang_get(request.env.context.get('lang') or 'en_US')
|
||||
return lang.format(fmt, currency.round(amount), grouping=True, monetary=True)\
|
||||
.replace(r' ', u'\N{NO-BREAK SPACE}').replace(r'-', u'-\N{ZERO WIDTH NO-BREAK SPACE}')
|
||||
|
||||
@http.route(['/shop/cart/update_option'], type='http', auth="public", methods=['POST'], website=True, multilang=False)
|
||||
def cart_options_update_json(self, product_id, add_qty=1, set_qty=0, goto_shop=None, lang=None, **kw):
|
||||
if lang:
|
||||
|
||||
@@ -55,8 +55,7 @@ class WebsiteSaleDelivery(WebsiteSale):
|
||||
values['delivery_action_id'] = request.env.ref('delivery.action_delivery_carrier_form').id
|
||||
return values
|
||||
|
||||
@http.route(['/shop/update_carrier'], type='json', auth='public', methods=['POST'], website=True, csrf=False)
|
||||
def update_eshop_carrier(self, **post):
|
||||
def _update_website_sale_delivery(self, **post):
|
||||
order = request.website.sale_get_order()
|
||||
carrier_id = int(post['carrier_id'])
|
||||
currency = order.currency_id
|
||||
@@ -70,10 +69,4 @@ class WebsiteSaleDelivery(WebsiteSale):
|
||||
'new_amount_tax': self._format_amount(order.amount_tax, currency),
|
||||
'new_amount_total': self._format_amount(order.amount_total, currency),
|
||||
}
|
||||
|
||||
def _format_amount(self, amount, currency):
|
||||
fmt = "%.{0}f".format(currency.decimal_places)
|
||||
lang = request.env['res.lang']._lang_get(request.env.context.get('lang') or 'en_US')
|
||||
|
||||
return lang.format(fmt, currency.round(amount), grouping=True, monetary=True)\
|
||||
.replace(r' ', u'\N{NO-BREAK SPACE}').replace(r'-', u'-\N{ZERO WIDTH NO-BREAK SPACE}')
|
||||
return {}
|
||||
|
||||
@@ -3,6 +3,8 @@ odoo.define('website_sale_delivery.checkout', function (require) {
|
||||
|
||||
require('web.dom_ready');
|
||||
var ajax = require('web.ajax');
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
|
||||
/* Handle interactive carrier choice + cart update */
|
||||
var $pay_button = $('#o_payment_form_pay');
|
||||
@@ -14,6 +16,17 @@ odoo.define('website_sale_delivery.checkout', function (require) {
|
||||
var $amount_total = $('#order_total span.oe_currency_value');
|
||||
var $carrier_badge = $('#delivery_carrier input[name="delivery_type"][value=' + result.carrier_id + '] ~ .badge.d-none');
|
||||
var $compute_badge = $('#delivery_carrier input[name="delivery_type"][value=' + result.carrier_id + '] ~ .o_delivery_compute');
|
||||
var $discount = $('#order_discounted');
|
||||
|
||||
if ($discount && result.new_amount_order_discounted) {
|
||||
// Cross module without bridge
|
||||
// Update discount of the order
|
||||
$discount.find('.oe_currency_value').text(result.new_amount_order_discounted);
|
||||
|
||||
// We are in freeshipping, so every carrier is Free
|
||||
$('#delivery_carrier .badge').text(_t('Free'));
|
||||
}
|
||||
|
||||
if (result.status === true) {
|
||||
$amount_delivery.text(result.new_amount_delivery);
|
||||
$amount_untaxed.text(result.new_amount_untaxed);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
India 2018-08-27
|
||||
|
||||
Cybrosys Techno SOlutions agrees to the terms of the Odoo Corporate
|
||||
Contributor License Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Niyas Raphy niyas@cybrosys.com https://github.com/CybroOdoo
|
||||
|
||||
List of contributors:
|
||||
|
||||
Sainu Abideen sainu@cybrosys.com https://github.com/CybroOdoo
|
||||
Cybrosys odoo@cybrosys.com https://github.com/CybroOdoo
|
||||
@@ -0,0 +1,11 @@
|
||||
France, 2018-10-12
|
||||
|
||||
I hereby agree to the terms of the Odoo Individual Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Benoît Fontaine <benoitfontaine.ba@gmail.com> https://github.com/Linekio
|
||||
@@ -0,0 +1,11 @@
|
||||
Germany, 2017-11-24
|
||||
|
||||
I hereby agree to the terms of the Odoo Individual Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Andreas Raster lazor@affenbande.org https://github.com/rakete
|
||||
@@ -0,0 +1,11 @@
|
||||
Belgium, 2018-09-05
|
||||
|
||||
I hereby agree to the terms of the Odoo Individual Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Stijn Houben stijnhouben92@gmail.com https://github.com/stijnh92
|
||||
@@ -0,0 +1,11 @@
|
||||
FRANCE, 2018-09-26
|
||||
|
||||
I hereby agree to the terms of the Odoo Individual Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
zoggy 43614933+zoggy444@users.noreply.github.com https://github.com/zoggy444
|
||||
@@ -1611,7 +1611,7 @@ order.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<field name="datefield" options='{"datepicker": {"daysOfWeekDisabled": [0, 6]}}'/>
|
||||
<field name="datetimefield" options='{"datepicker": {"daysOfWeekDisabled": [0, 6]}}'/>
|
||||
|
||||
- monetary (FieldMonetary)
|
||||
This is the default field type for fields of type 'monetary'. It is used to
|
||||
|
||||
@@ -1011,7 +1011,7 @@ attributes:
|
||||
``quick_create``
|
||||
whether it should be possible to create records without switching to the
|
||||
form view. By default, ``quick_create`` is enabled when the Kanban view is
|
||||
grouped, and disabled when not.
|
||||
grouped by many2one, selection, char or boolean fields, and disabled when not.
|
||||
|
||||
Set to ``true`` to always enable it, and to ``false`` to always disable it.
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ CREATE TABLE res_company (
|
||||
name varchar NOT NULL,
|
||||
partner_id integer,
|
||||
currency_id integer,
|
||||
sequence integer,
|
||||
create_date timestamp without time zone,
|
||||
primary key(id)
|
||||
);
|
||||
|
||||
@@ -457,7 +457,7 @@
|
||||
<field name="name">Ecuador</field>
|
||||
<field name="code">ec</field>
|
||||
<field file="base/static/img/country_flags/ec.png" name="image" type="base64" />
|
||||
<field name="currency_id" ref="ECS" />
|
||||
<field name="currency_id" ref="USD" />
|
||||
<field eval="593" name="phone_code" />
|
||||
</record>
|
||||
<record id="ee" model="res.country">
|
||||
@@ -1734,7 +1734,7 @@
|
||||
<field file="base/static/img/country_flags/vn.png" name="image" type="base64" />
|
||||
<field name="currency_id" ref="VND" />
|
||||
<field eval="84" name="phone_code" />
|
||||
<field eval="'%(street)s\n%(street2)s\n%(city)s\n%(name)s %(zip)s %(country_name)s'" name="address_format" />
|
||||
<field eval="'%(street)s\n%(street2)s\n%(city)s\n%(state_name)s %(country_name)s'" name="address_format" />
|
||||
</record>
|
||||
<record id="vu" model="res.country">
|
||||
<field name="name">Vanuatu</field>
|
||||
|
||||
@@ -345,7 +345,7 @@ class IrActionsServer(models.Model):
|
||||
DEFAULT_PYTHON_CODE = """# Available variables:
|
||||
# - env: Odoo Environment on which the action is triggered
|
||||
# - model: Odoo Model of the record on which the action is triggered; is a void recordset
|
||||
# - record: record on which the action is triggered; may be be void
|
||||
# - record: record on which the action is triggered; may be void
|
||||
# - records: recordset of all records on which the action is triggered in multi-mode; may be void
|
||||
# - time, datetime, dateutil, timezone: useful Python libraries
|
||||
# - log: log(message, level='info'): logging function to record debug information in ir.logging table
|
||||
|
||||
@@ -307,7 +307,7 @@ class IrHttp(models.AbstractModel):
|
||||
if not cls._check_access_mode(env, id, access_mode, model, access_token=access_token,
|
||||
related_id=related_id):
|
||||
return (403, [], None)
|
||||
elif not consteq(obj.access_token or '', access_token):
|
||||
elif not consteq(obj.access_token or u'', access_token):
|
||||
return (403, [], None)
|
||||
|
||||
# check read access
|
||||
|
||||
@@ -712,11 +712,15 @@ class IrModelFields(models.Model):
|
||||
field = getattr(obj, '_fields', {}).get(item.name)
|
||||
|
||||
if vals.get('name', item.name) != item.name:
|
||||
# We need to rename the column
|
||||
# We need to rename the field
|
||||
item._prepare_update()
|
||||
if column_rename:
|
||||
raise UserError(_('Can only rename one field at a time!'))
|
||||
column_rename = (obj._table, item.name, vals['name'], item.index)
|
||||
if item.ttype in ('one2many', 'many2many'):
|
||||
# those field names are not explicit in the database!
|
||||
pass
|
||||
else:
|
||||
if column_rename:
|
||||
raise UserError(_('Can only rename one field at a time!'))
|
||||
column_rename = (obj._table, item.name, vals['name'], item.index)
|
||||
|
||||
# We don't check the 'state', because it might come from the context
|
||||
# (thus be set for multiple fields) and will be ignored anyway.
|
||||
@@ -779,6 +783,7 @@ class IrModelFields(models.Model):
|
||||
'index': bool(field.index),
|
||||
'store': bool(field.store),
|
||||
'copied': bool(field.copy),
|
||||
'on_delete': getattr(field, 'ondelete', None),
|
||||
'related': ".".join(field.related) if field.related else None,
|
||||
'readonly': bool(field.readonly),
|
||||
'required': bool(field.required),
|
||||
|
||||
@@ -130,6 +130,13 @@ def get_view_arch_from_file(filename, xmlid):
|
||||
if node.tag in ('template', 'record'):
|
||||
if node.tag == 'record':
|
||||
field = node.find('field[@name="arch"]')
|
||||
if field is None:
|
||||
if node.find('field[@name="view_id"]') is not None:
|
||||
view_id = node.find('field[@name="view_id"]').attrib.get('ref')
|
||||
ref_id = '%s%s' % ('.' not in view_id and xmlid.split('.')[0] + '.' or '', view_id)
|
||||
return get_view_arch_from_file(filename, ref_id)
|
||||
else:
|
||||
return None
|
||||
_fix_multiple_roots(field)
|
||||
inner = u''.join([etree.tostring(child, encoding='unicode') for child in field.iterchildren()])
|
||||
return field.text + inner
|
||||
|
||||
@@ -95,4 +95,4 @@
|
||||
"access_ir_actions_client","ir_actions_client all","model_ir_actions_client",,1,0,0,0
|
||||
"access_ir_logging","ir_logging admin","model_ir_logging","group_erp_manager",1,1,1,1
|
||||
"paperformat_access_portal","ir_actions_report_paperformat group_portal","model_report_paperformat",,1,0,0,0
|
||||
"paperformat_access_employee","ir_actions_report_paperformat group_hr_user","model_report_paperformat",,1,0,1,0
|
||||
"paperformat_access_employee","ir_actions_report_paperformat group_system","model_report_paperformat",group_system,1,1,1,1
|
||||
|
||||
|
@@ -97,29 +97,29 @@ class test_ir_http_mimetype(common.TransactionCase):
|
||||
|
||||
def test_access(**kwargs):
|
||||
status, _, _ = self.env['ir.http'].binary_content(
|
||||
**defaults, **kwargs
|
||||
**dict(defaults, **kwargs)
|
||||
)
|
||||
return status
|
||||
|
||||
status = test_access()
|
||||
self.assertEqual(status, 403, "no access")
|
||||
|
||||
status = test_access(access_token='Secret')
|
||||
status = test_access(access_token=u'Secret')
|
||||
self.assertEqual(status, 403,
|
||||
"no access if access token for attachment without access token")
|
||||
|
||||
attachment.access_token = 'Secret'
|
||||
status = test_access(access_token='Secret')
|
||||
attachment.access_token = u'Secret'
|
||||
status = test_access(access_token=u'Secret')
|
||||
self.assertEqual(status, 200, "access for correct access token")
|
||||
|
||||
status = test_access(access_token='Wrong')
|
||||
status = test_access(access_token=u'Wrong')
|
||||
self.assertEqual(status, 403, "no access for wrong access token")
|
||||
|
||||
attachment.public = True
|
||||
status = test_access()
|
||||
self.assertEqual(status, 200, "access for attachment with access")
|
||||
|
||||
status = test_access(access_token='Wrong')
|
||||
status = test_access(access_token=u'Wrong')
|
||||
self.assertEqual(status, 403,
|
||||
"no access for wrong access token for attachment with access")
|
||||
|
||||
@@ -127,6 +127,6 @@ class test_ir_http_mimetype(common.TransactionCase):
|
||||
status = test_access()
|
||||
self.assertEqual(status, 404, "no access for deleted attachment")
|
||||
|
||||
status = test_access(access_token='Secret')
|
||||
status = test_access(access_token=u'Secret')
|
||||
self.assertEqual(status, 404,
|
||||
"no access with access token for deleted attachment")
|
||||
|
||||
@@ -180,6 +180,8 @@ class FSWatcher(object):
|
||||
try:
|
||||
source = open(path, 'rb').read() + b'\n'
|
||||
compile(source, path, 'exec')
|
||||
except FileNotFoundError:
|
||||
_logger.error('autoreload: python code change detected, FileNotFound for %s', path)
|
||||
except SyntaxError:
|
||||
_logger.error('autoreload: python code change detected, SyntaxError in %s', path)
|
||||
else:
|
||||
|
||||
@@ -6,8 +6,8 @@ helpers and classes to write tests.
|
||||
"""
|
||||
import base64
|
||||
import collections
|
||||
import inspect
|
||||
import importlib
|
||||
import inspect
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
|
||||
@@ -471,6 +471,8 @@ class configmanager(object):
|
||||
os.path.abspath(os.path.expanduser(os.path.expandvars(x.strip())))
|
||||
for x in self.options['addons_path'].split(','))
|
||||
|
||||
self.options['data_dir'] = os.path.abspath(os.path.expanduser(os.path.expandvars(self.options['data_dir'].strip())))
|
||||
|
||||
self.options['init'] = opt.init and dict.fromkeys(opt.init.split(','), 1) or {}
|
||||
self.options['demo'] = (dict(self.options['init'])
|
||||
if not self.options['without_demo'] else {})
|
||||
|
||||
Reference in New Issue
Block a user