[MERGE] forward port branch saas-16 up to 104dfe3c07

This commit is contained in:
Christophe Simonis
2017-07-20 11:48:49 +02:00
76 changed files with 792 additions and 288 deletions
+15
View File
@@ -142,6 +142,11 @@ file_filter = addons/base_import_module/i18n/<lang>.po
source_file = addons/base_import_module/i18n/base_import_module.pot
source_lang = en
[odoo-master.base_sparse_field]
file_filter = addons/base_sparse_field/i18n/<lang>.po
source_file = addons/base_sparse_field/i18n/base_sparse_field.pot
source_lang = en
[odoo-master.base_setup]
file_filter = addons/base_setup/i18n/<lang>.po
source_file = addons/base_setup/i18n/base_setup.pot
@@ -892,6 +897,11 @@ file_filter = addons/website_sale/i18n/<lang>.po
source_file = addons/website_sale/i18n/website_sale.pot
source_lang = en
[odoo-master.website_sale_comparison]
file_filter = addons/website_sale_comparison/i18n/<lang>.po
source_file = addons/website_sale_comparison/i18n/website_sale_comparison.pot
source_lang = en
[odoo-master.website_sale_delivery]
file_filter = addons/website_sale_delivery/i18n/<lang>.po
source_file = addons/website_sale_delivery/i18n/website_sale_delivery.pot
@@ -912,6 +922,11 @@ file_filter = addons/website_sale_stock/i18n/<lang>.po
source_file = addons/website_sale_stock/i18n/website_sale_stock.pot
source_lang = en
[odoo-master.website_sale_wishlist]
file_filter = addons/website_sale_wishlist/i18n/<lang>.po
source_file = addons/website_sale_wishlist/i18n/website_sale_wishlist.pot
source_lang = en
[odoo-master.website_slides]
file_filter = addons/website_slides/i18n/<lang>.po
source_file = addons/website_slides/i18n/website_slides.pot
@@ -910,7 +910,7 @@ class AccountBankStatementLine(models.Model):
counterpart_moves = (counterpart_moves | move)
# Create The payment
payment = False
payment = self.env['account.payment']
if abs(total)>0.00001:
partner_id = self.partner_id and self.partner_id.id or False
partner_type = False
+10 -5
View File
@@ -773,7 +773,7 @@ class AccountInvoice(models.Model):
total_currency = 0
for line in invoice_move_lines:
if self.currency_id != company_currency:
currency = self.currency_id.with_context(date=self.date_invoice or fields.Date.context_today(self))
currency = self.currency_id.with_context(date=self.date or self.date_invoice or fields.Date.context_today(self))
if not (line.get('currency_id') and line.get('amount_currency')):
line['currency_id'] = currency.id
line['amount_currency'] = currency.round(line['price'])
@@ -907,7 +907,6 @@ class AccountInvoice(models.Model):
if not inv.date_invoice:
inv.with_context(ctx).write({'date_invoice': fields.Date.context_today(self)})
date_invoice = inv.date_invoice
company_currency = inv.company_id.currency_id
# create move lines (one per invoice line + eventual taxes and analytic lines)
@@ -920,9 +919,9 @@ class AccountInvoice(models.Model):
name = inv.name or '/'
if inv.payment_term_id:
totlines = inv.with_context(ctx).payment_term_id.with_context(currency_id=company_currency.id).compute(total, date_invoice)[0]
totlines = inv.with_context(ctx).payment_term_id.with_context(currency_id=company_currency.id).compute(total, inv.date_invoice)[0]
res_amount_currency = total_currency
ctx['date'] = date_invoice
ctx['date'] = inv.date or inv.date_invoice
for i, t in enumerate(totlines):
if inv.currency_id != company_currency:
amount_currency = company_currency.with_context(ctx).compute(t[1], inv.currency_id)
@@ -962,7 +961,7 @@ class AccountInvoice(models.Model):
journal = inv.journal_id.with_context(ctx)
line = inv.finalize_invoice_move_lines(line)
date = inv.date or date_invoice
date = inv.date or inv.date_invoice
move_vals = {
'ref': inv.reference,
'line_ids': line,
@@ -1579,6 +1578,12 @@ class AccountPaymentTerm(models.Model):
result.append((last_date, dist))
return result
@api.multi
def unlink(self):
property_recs = self.env['ir.property'].search([('value_reference', 'in', ['account.payment.term,%s'%payment_term.id for payment_term in self])])
property_recs.unlink()
return super(AccountPaymentTerm, self).unlink()
class AccountPaymentTermLine(models.Model):
_name = "account.payment.term.line"
+8 -2
View File
@@ -57,10 +57,16 @@ Best Regards,''')
if (date.month < last_month or (date.month == last_month and date.day <= last_day)):
date = date.replace(month=last_month, day=last_day)
else:
date = date.replace(month=last_month, day=last_day, year=date.year + 1)
if last_month == 2 and last_day == 29 and (date.year + 1) % 4 != 0:
date = date.replace(month=last_month, day=28, year=date.year + 1)
else:
date = date.replace(month=last_month, day=last_day, year=date.year + 1)
date_to = date
date_from = date + timedelta(days=1)
date_from = date_from.replace(year=date_from.year - 1)
if date_from.month == 2 and date_from.day == 29:
date_from = date_from.replace(day=28, year=date_from.year - 1)
else:
date_from = date_from.replace(year=date_from.year - 1)
return {'date_from': date_from, 'date_to': date_to}
def get_new_account_code(self, current_code, old_prefix, new_prefix, digits):
@@ -198,7 +198,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
@api.model
def get_report_values(self, docids, data=None):
if not data.get('form') or not self.env.context.get('active_model') or not self.env.context.get('active_id'):
raise UserError(_("Some data are missing, this report cannot be printed."))
raise UserError(_("Form content is missing, this report cannot be printed."))
total = []
model = self.env.context.get('active_model')
+1 -1
View File
@@ -61,7 +61,7 @@ class ReportTrialBalance(models.AbstractModel):
@api.model
def get_report_values(self, docids, data=None):
if not data.get('form') or not self.env.context.get('active_model'):
raise UserError(_("Some data are missing, this report cannot be printed."))
raise UserError(_("Form content is missing, this report cannot be printed."))
self.model = self.env.context.get('active_model')
docs = self.env[self.model].browse(self.env.context.get('active_ids', []))
@@ -111,7 +111,7 @@ class ReportGeneralLedger(models.AbstractModel):
@api.model
def get_report_values(self, docids, data=None):
if not data.get('form') or not self.env.context.get('active_model'):
raise UserError(_("Some data are missing, this report cannot be printed."))
raise UserError(_("Form content is missing, this report cannot be printed."))
self.model = self.env.context.get('active_model')
docs = self.env[self.model].browse(self.env.context.get('active_ids', []))
+1 -1
View File
@@ -97,7 +97,7 @@ class ReportJournal(models.AbstractModel):
@api.model
def get_report_values(self, docids, data=None):
if not data.get('form'):
raise UserError(_("Some data are missing, this report cannot be printed."))
raise UserError(_("Form content is missing, this report cannot be printed."))
target_move = data['form'].get('target_move', 'all')
sort_selection = data['form'].get('sort_selection', 'date')
@@ -71,7 +71,7 @@ class ReportPartnerLedger(models.AbstractModel):
@api.model
def get_report_values(self, docids, data=None):
if not data.get('form'):
raise UserError(_("Some data are missing, this report cannot be printed."))
raise UserError(_("Form content is missing, this report cannot be printed."))
data['computed'] = {}
@@ -147,7 +147,7 @@ class ReportFinancial(models.AbstractModel):
@api.model
def get_report_values(self, docids, data=None):
if not data.get('form') or not self.env.context.get('active_model') or not self.env.context.get('active_id'):
raise UserError(_("Some data are missing, this report cannot be printed."))
raise UserError(_("Form content is missing, this report cannot be printed."))
self.model = self.env.context.get('active_model')
docs = self.env[self.model].browse(self.env.context.get('active_id'))
+1 -1
View File
@@ -200,7 +200,7 @@
</p>
<div class="alert alert-info info_icon" role="alert">
<span class="fa fa-lightbulb-o fa-lg"/>
<p>Exchange rates can be automatically updated once a day from <strong>Yahoo Finance</strong> or the <strong>European Central Bank</strong>. You can activate this feature in the bottom of the <a t-att-href="prepare_backend_url('account.action_account_config')">Accounting Settings</a><a t-att-href="prepare_backend_url('account.action_account_config')">accounting settings</a>.</p>
<p>Exchange rates can be automatically updated once a day from <strong>Yahoo Finance</strong> or the <strong>European Central Bank</strong>. You can activate this feature in the bottom of the <a t-att-href="prepare_backend_url('account.action_account_config')">Accounting Settings</a>.</p>
</div>
</t>
<t t-call="web_planner.page">
@@ -223,7 +223,7 @@ class AccountBankStatementImport(models.TransientModel):
else:
ignored_statement_lines_import_ids.append(line_vals['unique_import_id'])
if 'balance_start' in st_vals:
st_vals['balance_start'] += line_vals['amount']
st_vals['balance_start'] += float(line_vals['amount'])
if len(filtered_st_lines) > 0:
# Remove values that won't be used to create records
@@ -126,7 +126,7 @@
<t t-name="kanban-box">
<div class="oe_kanban_vignette oe_semantic_html_override">
<a type="open" href="#" class="oe_kanban_action">
<img t-att-src="kanban_image('fleet.vehicle.model.brand', 'image_small', record.id.value)" class="img-responsive" style="width:100px;"/>
<img t-att-src="kanban_image('fleet.vehicle.model.brand', 'image_small', record.id.raw_value)" class="img-responsive" style="width:100px;"/>
</a>
<div class="text-center">
<h4 class="oe_partner_heading">
+2 -2
View File
@@ -183,7 +183,7 @@
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</div>
<div class="o_kanban_image">
<img t-att-src="kanban_image('fleet.vehicle', 'image_small', record.id.value)"/>
<img t-att-src="kanban_image('fleet.vehicle', 'image_small', record.id.raw_value)"/>
</div>
<div class="oe_kanban_details">
<strong>
@@ -397,4 +397,4 @@
<menuitem id="fleet_vehicle_tag_menu" parent="fleet_configuration" action="fleet_vehicle_tag_action" sequence="112"/>
</odoo>
</odoo>
+234 -68
View File
@@ -20,76 +20,242 @@
<record id="simple_report_template" model="mail.template">
<field name="name">Simple Challenge Report Progress</field>
<field name="model_id" ref="gamification.model_gamification_challenge"/>
<field name="body_html"><![CDATA[
<p>
<strong>${object.name}</strong>
</p>
<p class="oe_grey">The following message contains the current progress for the challenge ${object.name}</p>
% if object.visibility_mode == 'personal':
<table width="100%" border="1">
<tr>
<th>Goal</th>
<th>Target</th>
<th>Current</th>
<th>Completeness</th>
</tr>
% for line in ctx["challenge_lines"]:
<tr
% if line['completeness'] >= 100:
style="font-weight:bold;"
% endif
>
<td>${line['name']}</td>
<td>${line['target']}
% if line['suffix']:
${line['suffix']}
% endif
</td>
<td>${line['current']}
% if line['suffix']:
${line['suffix']}
% endif
</td>
<td>${line['completeness']} %</td>
</tr>
% endfor
</table>
% else:
% for line in ctx["challenge_lines"]:
<table width="100%" border="1">
<tr>
<th colspan="4">${line['name']}</th>
</tr>
<tr>
<th>#</th>
<th>Person</th>
<th>Completeness</th>
<th>Current</th>
</tr>
% for goal in line['goals']:
<tr
% if goal.completeness >= 100:
style="font-weight:bold;"
% endif
>
<td>${goal['rank']}</td>
<td>${goal['name']}</td>
<td>${goal['completeness']}%</td>
<td>${goal['current']}/${line['target']}
% if line['suffix']:
${line['suffix']}
% endif
</td>
</tr>
% endfor
<field name="body_html">
<![CDATA[
% set record = ctx.get('record')
% set company = record and record.company_id or user.company_id
% set challenge_lines = ctx.get('challenge_lines', [])
<table cellspacing="0" cellpadding="0" width="100%" style="font-family: 'Helvetica'; background-color: #EEE; border-collapse: collapse;">
<tr>
<td valign="top" align="center">
<table cellspacing="0" cellpadding="0" width="600" style="margin: 0 auto; width: 600px;">
<tr><td>
<table cellspacing="0" cellpadding="0" width="100%">
<tr>
<td valign="middle" align="left" style="padding:30px 15px;">
<img width="90" src="/logo.png?company=${user.company_id.id}" style="width:80px;"/>
</td>
% if object.visibility_mode == 'ranking':
<td style="padding:15px;">
<p style="font-size:20px;color:#666666;" align="right">Leaderboard</p>
</td>
% endif
</tr>
</table>
<table cellspacing="0" cellpadding="0" width="100%" bgcolor="#fff" style="background-color:#fff;">
<tr><td style="padding: 15px;">
% if object.visibility_mode == 'personal':
<span style="color:#666666;font-size:13px;">Here is your current progress in the challenge <strong>${object.name}</strong>.</span>
<table cellspacing="0" cellpadding="0" width="100%" style="margin-top:20px;">
<tr>
<td align="center">
<p style="font-size:27px;color:#666666;">Personal Performance</p>
</td>
</tr>
</table>
<table cellspacing="0" cellpadding="0" width="100%" style="margin-top:30px;color:#666666;">
<thead>
<tr style="color:#9A6C8E; font-size:12px;">
<th align="left" style="padding-bottom: 0px;width:40%;text-align:left;">Goals</th>
<th style="width:20%;text-align:right;" align="left">Target</th>
<th style="width:20%;text-align:right;" align="right">Current</th>
<th style="width:20%;text-align:right;" align="right">Completeness</th>
</tr>
<tr>
<td colspan="5" style="height:1px;background-color:#9A6C8E;"></td>
</tr>
</thead>
<tbody>
% for line in challenge_lines:
<tr
% if line['completeness'] >= 100:
style="font-weight:bold;"
% endif
>
<td style="padding: 20px 0;" align="left">
${line['name']}
% if line['suffix'] or line['monetary']:
(${line['full_suffix']})
% endif
</td>
<td style="padding: 20px 0;" align="right">${"%.2f" % line['target']}
% if line['suffix']:
${line['suffix']}
% endif
</td>
<td style="padding: 20px 0;" align="right">${"%.2f" % line['current']}
% if line['suffix']:
${line['suffix']}
% endif
</td>
<td style="padding: 20px 0;font-size:25px;color:#9A6C8E;" align="right"><strong>${line['completeness']| int}%</strong></td>
</tr>
<tr>
<td colspan="5" style="height:1px;background-color:#e3e3e3;"></td>
</tr>
% endfor
</tbody>
</table>
% else:
<span style="color:#A8A8A8;font-size:13px;">
The following message contains the current progress for the challenge <strong>${object.name}</strong>.
</span>
% for line in challenge_lines:
<!-- Header + Button table -->
<table cellspacing="0" cellpadding="0" width="100%"style="margin-top:35px;">
<tr>
<td width="50%">
<p style="font-size:22px;color:#666666;">Top Achievers for goal <strong>${line['name']}</strong></p>
</td>
</tr>
</table>
<!-- Podium -->
% if len(line['goals'])>2:
<table cellspacing="0" cellpadding="0" width="100%" style="margin-top:10px;">
<tr><td style="padding:0 30px;">
<table cellspacing="0" cellpadding="0" width="100%">
<tr>
% set top_goals = [line['goals'][1], line['goals'][0], line['goals'][2]]
% for goal in top_goals:
<td align="center" style="width:32%;">
% if loop.index == 1:
% set extra_div = '<div style="height:40px;"></div>'
% set heightA = 95
% set heightB = 75
% set bgColor = '#b898b0'
% set fontSize = 50
% set podiumPosition = '2'
% elif loop.index == 2:
% set extra_div = ''
% set heightA = 55
% set heightB = 115
% set bgColor = '#9A6C8E'
% set fontSize = 85
% set podiumPosition = '1'
% elif loop.index == 3:
% set extra_div = '<div style="height:60px;"></div>'
% set heightA = 115
% set heightB = 55
% set bgColor = '#c8afc1'
% set fontSize = 35
% set podiumPosition = '3'
% endif
<div style="margin:0 3px 0 3px;height:220px;">
<div style="height:${heightA}px;">
${extra_div | safe}
<div style="height:55px;">
<img style="margin-bottom:5px;width:50px;height:50px;border-radius:50%;" src="/web/image?model=res.users&amp;field=image_small&amp;id=${goal['user_id']}" alt=""/>
</div>
<div align="center" style ="color:${bgColor};">
${goal['name']}
</div
</div>
<div style="background-color:${bgColor | safe};height:${heightB}px;">
<strong><span style="color:#fff;font-size:${fontSize}px;">${podiumPosition | safe}</span></strong>
</div>
<div style="height:30px;">
<p style="color:#9A6C8E;font-size:20px;margin-top:10px;">
${"%.2f" % goal['current']}
% if line['suffix'] or line['monetary']:
${line['full_suffix']}
% endif
</p>
</div>
</div>
</td>
% endfor
</tr>
</table>
</td>
</tr>
</table>
% endif
<!-- data table -->
<table cellspacing="0" cellpadding="0" width="100%" style="margin-bottom:5px">
<tr>
<td>
<table cellspacing="0" cellpadding="0" width="100%" style="margin-top:30px;margin-bottom:5px;color:#666666;">
<thead>
<tr style="color:#9A6C8E; font-size:12px;">
<th style="width:10%;text-align:center;">Rank</th>
<th style="width:15%;text-align:left;">Name</th>
<th style="width:30%;text-align:right;">Performance
% if line['suffix']:
(${line['suffix']})
% elif line['monetary']:
(${company.currency_id.symbol})
% endif
</th>
<th style="width:30%;text-align:right;"">Completeness</th>
</tr>
<tr>
<td colspan="5" style="height:1px;background-color:#9A6C8E;"></td>
</tr>
</thead>
<tbody>
% for goal in line['goals']:
<tr>
% set tdBgColor = '#fff'
% set tdColor = 'gray'
% set mutedColor = '#AAAAAA'
% set tdPercentageColor = '#9A6C8E'
<td width="10%" align="center" valign="middle" style="background-color:${tdBgColor};padding :5px 0;font-size:20px;">${goal['rank']+1}
</td>
<td width="15%" align="left" valign="middle" style="background-color:${tdBgColor};padding :5px 0;font-size:13px;">${goal['name']}</td>
<td width="20%" align="right" style="background-color:${tdBgColor};padding:5px 0;line-height:1;">${"%.2f" % goal['current']}<br/><span style="font-size:13px;color:${mutedColor};">on ${"%.2f" % line['target']}</span>
</td>
<td width="20%" style="color:${tdPercentageColor};background-color:${tdBgColor};padding-right:15px;font-size:22px;" align="right"><strong>${goal['completeness'] | int}%</strong></td>
</tr>
<tr>
<td colspan="5" style="height:1px;background-color:#DADADA;"></td>
</tr>
% endfor
</tbody>
</table>
</td>
</tr>
</table>
% endfor
% endif
</td></tr>
</table>
<table cellspacing="0" cellpadding="0" width="100%" style="padding: 15px; color: #666666;margin-bottom: 5px;">
<tr>
<td align="center" style="min-width: 590px;">
<table width="580" border="0" cellpadding="0" style="min-width: 580px; font-size: 12px;">
<tr>
<td valign="middle" align="left" style="padding-top: 10px; padding-bottom: 10px;">
${company.name}<br/>
${company.phone or ''}
</td>
<td valign="middle" align="right" style="padding-top: 10px; padding-bottom: 10px;">
% if company.email:
<a href="mailto:${company.email}" style="text-decoration:none; color: #666666;">${company.email}</a><br/>
% endif
% if company.website:
<a href="${company.website}" style="text-decoration:none; color: #666666;">
${company.website}
</a>
% endif
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="center">
Powered by <a target="_blank" href="https://www.odoo.com">Odoo</a>.
</td>
</tr>
</table>
</td></tr>
</table>
</td>
</tr>
</table>
]]>
<br/><br/>
% endfor
% endif
]]></field>
</field>
</record>
<!-- goal definitions -->
+1 -1
View File
@@ -177,7 +177,7 @@
<div class="oe_kanban_global_click">
<field name="category_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<div class="o_kanban_image">
<img t-att-src="kanban_image('hr.employee', 'image_medium', record.id.value)"/>
<img t-att-src="kanban_image('hr.employee', 'image_medium', record.id.raw_value)"/>
<div class="o_employee_summary_icons">
<t t-if="record.message_needaction_counter.raw_value">
<span class='oe_kanban_mail_new' title='Unread Messages'><i class='fa fa-comments'/><t t-raw="record.message_needaction_counter.raw_value"/></span>
@@ -42,7 +42,7 @@
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<div class="o_kanban_image">
<img t-att-src="kanban_image('hr.employee', 'image_medium', record.id.value)"/>
<img t-att-src="kanban_image('hr.employee', 'image_medium', record.id.raw_value)"/>
</div>
<div class="oe_kanban_details">
<div id="textbox">
+1 -1
View File
@@ -44,7 +44,7 @@
<field name="name">Submitted</field>
<field name="res_model">hr.expense.sheet</field>
<field name="description">Expense report submitted, waiting approval</field>
<field name="default" eval="False"/>
<field name="default" eval="True"/>
</record>
<record id="mt_expense_paid" model="mail.message.subtype">
<field name="name">Paid</field>
+20 -10
View File
@@ -30,7 +30,8 @@ class HrExpense(models.Model):
company_id = fields.Many2one('res.company', string='Company', readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, default=lambda self: self.env.user.company_id)
currency_id = fields.Many2one('res.currency', string='Currency', readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, default=lambda self: self.env.user.company_id.currency_id)
analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', states={'post': [('readonly', True)], 'done': [('readonly', True)]}, oldname='analytic_account')
account_id = fields.Many2one('account.account', string='Account', states={'post': [('readonly', True)], 'done': [('readonly', True)]}, default=lambda self: self.env['ir.property'].get('property_account_expense_categ_id', 'product.category'))
account_id = fields.Many2one('account.account', string='Account', states={'post': [('readonly', True)], 'done': [('readonly', True)]}, default=lambda self: self.env['ir.property'].get('property_account_expense_categ_id', 'product.category'),
help="An expense account is expected")
description = fields.Text()
payment_mode = fields.Selection([("own_account", "Employee (to reimburse)"), ("company_account", "Company")], default='own_account', states={'done': [('readonly', True)], 'post': [('readonly', True)]}, string="Payment By")
attachment_number = fields.Integer(compute='_compute_attachment_number', string='Number of Attachments')
@@ -419,10 +420,15 @@ class HrExpenseSheet(models.Model):
@api.model
def create(self, vals):
# Add the followers at creation, so they can be notified
if vals.get('employee_id'):
employee = self.env['hr.employee'].browse(vals['employee_id'])
users = self._get_users_to_subscribe(employee=employee) - self.env.user
vals['message_follower_ids'] = []
for partner in users.mapped('partner_id'):
vals['message_follower_ids'] += self.env['mail.followers']._add_follower_command(self._name, [], {partner.id: None}, {})[0]
sheet = super(HrExpenseSheet, self).create(vals)
self.check_consistency()
if vals.get('employee_id'):
sheet._add_followers()
return sheet
@api.multi
@@ -457,16 +463,20 @@ class HrExpenseSheet(models.Model):
return 'hr_expense.mt_expense_paid'
return super(HrExpenseSheet, self)._track_subtype(init_values)
def _add_followers(self):
user_ids = []
employee = self.employee_id
def _get_users_to_subscribe(self, employee=False):
users = self.env['res.users']
employee = employee or self.employee_id
if employee.user_id:
user_ids.append(employee.user_id.id)
users |= employee.user_id
if employee.parent_id:
user_ids.append(employee.parent_id.user_id.id)
users |= employee.parent_id.user_id
if employee.department_id and employee.department_id.manager_id and employee.parent_id != employee.department_id.manager_id:
user_ids.append(employee.department_id.manager_id.user_id.id)
self.message_subscribe_users(user_ids=user_ids)
users |= employee.department_id.manager_id.user_id
return users
def _add_followers(self):
users = self._get_users_to_subscribe()
self.message_subscribe_users(user_ids=users.ids)
@api.onchange('employee_id')
def _onchange_employee_id(self):
+1 -1
View File
@@ -60,7 +60,7 @@
</group><group>
<field name="reference"/>
<field name="date"/>
<field name="account_id"/>
<field name="account_id" domain="[('internal_type', '=', 'other')]"/>
<field name="employee_id"/>
<field name="sheet_id" invisible="1"/>
<field name="currency_id" groups="base.group_multi_currency"/>
@@ -109,7 +109,7 @@ class HrHolidaySummaryReport(models.AbstractModel):
@api.model
def get_report_values(self, docids, data=None):
if not data.get('form'):
raise UserError(_("Some data are missing, this report cannot be printed."))
raise UserError(_("Form content is missing, this report cannot be printed."))
holidays_report = self.env['ir.actions.report']._get_report_from_name('hr_holidays.report_holidayssummary')
holidays = self.env['hr.holidays'].browse(self.ids)
@@ -31,7 +31,7 @@ class ContributionRegisterReport(models.AbstractModel):
@api.model
def get_report_values(self, docids, data=None):
if not data.get('form'):
raise UserError(_("Some data are missing, this report cannot be printed."))
raise UserError(_("Form content is missing, this report cannot be printed."))
register_ids = self.env.context.get('active_ids', [])
contrib_registers = self.env['hr.contribution.register'].browse(register_ids)
@@ -36,7 +36,7 @@
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<div class="o_kanban_image">
<img t-att-src="kanban_image('im_livechat.channel', 'image_medium', record.id.value)" class="img-responsive"/>
<img t-att-src="kanban_image('im_livechat.channel', 'image_medium', record.id.raw_value)" class="img-responsive"/>
</div>
<div class="oe_kanban_details">
<strong><field name="name"/></strong>
@@ -108,7 +108,7 @@
<div>
<a t-if="! read_only_mode" type="delete" style="position: absolute; right: 0; padding: 4px; diplay: inline-block">X</a>
<div class="oe_group_details" style="min-height: 40px">
<img t-att-src="kanban_image('res.users', 'image', record.id.value)"
<img t-att-src="kanban_image('res.users', 'image', record.id.raw_value)"
class="oe_avatar oe_kanban_avatar_smallbox" style="float:left; margin-right: 10px;"/>
<h4><field name="name"/></h4>
</div>
@@ -2928,6 +2928,7 @@
<field name="name">Coupons échus à l'encaissement</field>
<field name="code">5111</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/>
<field name="reconcile" eval="True"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
@@ -2935,6 +2936,7 @@
<field name="name">Chèques à encaisser</field>
<field name="code">5112</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/>
<field name="reconcile" eval="True"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
@@ -2942,6 +2944,7 @@
<field name="name">Effets à l'encaissement</field>
<field name="code">5113</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/>
<field name="reconcile" eval="True"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
@@ -2949,6 +2952,7 @@
<field name="name">Effets à l'escompte</field>
<field name="code">5114</field>
<field name="user_type_id" ref="account.data_account_type_liquidity"/>
<field name="reconcile" eval="True"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
@@ -95,7 +95,7 @@ class ReportHrSalaryEmployeeBymonth(models.AbstractModel):
@api.model
def get_report_values(self, docids, data=None):
if not data.get('form') or not self.env.context.get('active_model') or not self.env.context.get('active_id'):
raise UserError(_("Some data are missing, this report cannot be printed."))
raise UserError(_("Form content is missing, this report cannot be printed."))
model = self.env.context.get('active_model')
docs = self.env[model].browse(self.env.context.get('active_id'))
@@ -130,7 +130,7 @@ class EmployeesYearlySalaryReport(models.AbstractModel):
@api.model
def get_report_values(self, docids, data=None):
if not self.env.context.get('active_model') or not self.env.context.get('active_id'):
raise UserError(_("Some data are missing, this report cannot be printed."))
raise UserError(_("Form content is missing, this report cannot be printed."))
model = self.env.context.get('active_model')
docs = self.env[model].browse(self.env.context.get('active_id'))
+1 -1
View File
@@ -61,7 +61,7 @@
</t>
<t t-name="kanban-box">
<div class="oe_module_vignette oe_kanban_global_click">
<img t-att-src="kanban_image('mail.channel', 'image_medium', record.id.value)" class="oe_module_icon"/>
<img t-att-src="kanban_image('mail.channel', 'image_medium', record.id.raw_value)" class="oe_module_icon"/>
<div class="oe_module_desc">
<h4>#<field name="name"/></h4>
<p class="oe_module_name">
+3 -4
View File
@@ -60,15 +60,14 @@ class MailMail(models.Model):
self.ensure_one()
body = super(MailMail, self).send_get_mail_body(partner=partner)
links_blacklist = ['/unsubscribe_from_list']
if self.mailing_id and body and self.statistics_ids:
for match in re.findall(URL_REGEX, self.body_html):
href = match[0]
url = match[1]
if not [s for s in links_blacklist if s in href]:
parsed = urlparse.urlparse(url, scheme='http')
if parsed.scheme.startswith('http') and parsed.path.startswith('/r/'):
new_href = href.replace(url, url + '/m/' + str(self.statistics_ids[0].id))
body = body.replace(href, new_href)
+10
View File
@@ -39,3 +39,13 @@ class MailThread(models.AbstractModel):
message_ids = [x.strip() for x in decode_smtp_header(message['References']).split()]
self.env['mail.mail.statistics'].set_replied(mail_message_ids=message_ids)
return super(MailThread, self).message_route_process(message, message_dict, routes)
@api.multi
def message_post_with_template(self, template_id, **kwargs):
# avoid having message send through `message_post*` methods being implicitly considered as
# mass-mailing
no_massmail = self.with_context(
default_mass_mailing_name=False,
default_mass_mailing_id=False,
)
return super(MailThread, no_massmail).message_post_with_template(template_id, **kwargs)
+6 -5
View File
@@ -263,11 +263,12 @@ class Repair(models.Model):
return self.env.ref('mrp_repair.action_report_mrp_repair_order').report_action(self)
def action_repair_invoice_create(self):
self.action_invoice_create()
if self.invoice_method == 'b4repair':
self.action_repair_ready()
elif self.invoice_method == 'after_repair':
self.write({'state': 'done'})
for repair in self:
repair.action_invoice_create()
if repair.invoice_method == 'b4repair':
repair.action_repair_ready()
elif repair.invoice_method == 'after_repair':
repair.write({'state': 'done'})
return True
@api.multi
+1 -1
View File
@@ -140,7 +140,7 @@
<t t-set="installed" t-value="!record.module_id.value || (record.module_id.value &amp;&amp; record.module_state.raw_value === 'installed')"/>
<div t-attf-class="oe_kanban_global_click #{installed and record.website_published.raw_value ? 'oe_kanban_color_5' : 'oe_kanban_color_white'}">
<div class="o_kanban_image">
<img type="open" t-att-src="kanban_image('payment.acquirer', 'image_small', record.id.value)"/>
<img type="open" t-att-src="kanban_image('payment.acquirer', 'image_small', record.id.raw_value)"/>
</div>
<div class="o_payment_acquirer_desc">
<h3 class="mt4"><t t-esc="record.name.value"/></h3>
+2 -2
View File
@@ -256,14 +256,14 @@ class TxAuthorize(models.Model):
def authorize_s2s_capture_transaction(self):
self.ensure_one()
transaction = AuthorizeAPI(self.acquirer_id)
tree = transaction.capture(self.acquirer_reference, self.amount)
tree = transaction.capture(self.acquirer_reference or '', self.amount)
return self._authorize_s2s_validate_tree(tree)
@api.multi
def authorize_s2s_void_transaction(self):
self.ensure_one()
transaction = AuthorizeAPI(self.acquirer_id)
tree = transaction.void(self.acquirer_reference)
tree = transaction.void(self.acquirer_reference or '')
return self._authorize_s2s_validate_tree(tree)
@api.multi
+1 -1
View File
@@ -649,7 +649,7 @@ class PosOrder(models.Model):
return_picking = Picking.create(return_vals)
return_picking.message_post(body=message)
for line in order.lines.filtered(lambda l: l.product_id.type in ['product', 'consu'] and l.qty != 0):
for line in order.lines.filtered(lambda l: l.product_id.type in ['product', 'consu'] and not float_is_zero(l.qty, precision_digits=l.product_id.uom_id.rounding)):
moves |= Move.create({
'name': line.name,
'product_uom': line.product_id.uom_id.id,
+1 -1
View File
@@ -296,7 +296,7 @@ class PosSession(models.Model):
if not self.ids:
return {}
for session in self.filtered(lambda s: s.user_id.id != self.env.uid):
raise UserError(_("You cannot use the session of another users. This session is owned by %s. "
raise UserError(_("You cannot use the session of another user. This session is owned by %s. "
"Please first close this one to use this point of sale.") % session.user_id.name)
return {
'type': 'ir.actions.act_url',
+1 -1
View File
@@ -270,7 +270,7 @@ var DebugWidget = PosBaseWidget.extend({
this.$('.button.delete_unpaid_orders').click(function(){
self.gui.show_popup('confirm',{
'title': _t('Delete Unpaid Orders ?'),
'body': _t('This operation will permanently destroy all unpaid orders from all sessions that have been put in the local storage. You will lose all the data and exit the point of sale. This operation cannot be undone.'),
'body': _t('This operation will destroy all unpaid orders in the browser. You will lose all the unsaved data and exit the point of sale. This operation cannot be undone.'),
confirm: function(){
self.pos.db.remove_all_unpaid_orders();
window.location = '/';
@@ -33,7 +33,7 @@
<filter string="Invoiced" domain="[('state','=',('invoiced'))]"/>
<filter string="Not Invoiced" domain="[('state','=',('paid'))]"/>
<separator/>
<filter string="Year" name="year" domain="[('date','&lt;=', time.strftime('%%Y-12-31')),('date','&gt;=',time.strftime('%%Y-01-01'))]" help="POS ordered created during current year"/>
<filter string="Year" name="year" domain="[('date','&lt;=', time.strftime('%%Y-12-31')),('date','&gt;=',time.strftime('%%Y-01-01'))]" help="POS orders created during the current year"/>
<separator/>
<filter string="My Sales" help="My Sales" domain="[('user_id','=',uid)]"/>
<field name="config_id"/>
@@ -75,7 +75,7 @@
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<div class="o_kanban_image">
<img t-att-src="kanban_image('product.template', 'image_small', record.id.value)"/>
<img t-att-src="kanban_image('product.template', 'image_small', record.id.raw_value)"/>
</div>
<div class="oe_kanban_details">
<strong>
+1 -1
View File
@@ -349,7 +349,7 @@
<field name="attribute_value_ids" groups="product.group_product_variant"/>
</div>
<div class="o_kanban_image">
<img t-att-src="kanban_image('product.product', 'image_small', record.id.value)"/>
<img t-att-src="kanban_image('product.product', 'image_small', record.id.raw_value)"/>
</div>
<div class="oe_kanban_details">
<strong><field name="name"/></strong>
+4 -11
View File
@@ -12,27 +12,20 @@ class SaleOrderLine(models.Model):
@api.multi
def _compute_analytic(self, domain=None):
lines = {}
force_so_lines = self.env.context.get("force_so_lines")
if not domain:
if not self.ids and not force_so_lines:
return True
# To filter on analyic lines linked to an expense
expense_type_id = self.env.ref('account.data_account_type_expenses', raise_if_not_found=False)
expense_type_id = expense_type_id and expense_type_id.id
domain = [
('so_line', 'in', self.ids),
'|',
('amount', '<', 0),
'&',
('amount', '=', 0),
'|',
('move_id', '=', False),
('move_id.account_id.user_type_id', '=', expense_type_id)
]
domain = [('so_line', 'in', self.ids), ('amount', '<=', 0.0)]
data = self.env['account.analytic.line'].read_group(
domain,
['so_line', 'unit_amount', 'product_uom_id'], ['product_uom_id', 'so_line'], lazy=False
)
# If the unlinked analytic line was the last one on the SO line, the qty was not updated.
force_so_lines = self.env.context.get("force_so_lines")
if force_so_lines:
for line in force_so_lines:
lines.setdefault(line, 0.0)
@@ -113,6 +113,7 @@ class SaleAdvancePaymentInv(models.TransientModel):
'payment_term_id': order.payment_term_id.id,
'fiscal_position_id': order.fiscal_position_id.id or order.partner_id.property_account_position_id.id,
'team_id': order.team_id.id,
'user_id': order.user_id.id,
'comment': order.note,
})
invoice.compute_taxes()
+2 -13
View File
@@ -140,20 +140,9 @@ class SaleOrderLine(models.Model):
@api.multi
def _compute_analytic(self, domain=None):
if not domain:
if not domain and self.ids:
# To filter on analyic lines linked to an expense
expense_type_id = self.env.ref('account.data_account_type_expenses', raise_if_not_found=False)
expense_type_id = expense_type_id and expense_type_id.id
domain = [
('so_line', 'in', self.ids),
'|',
'|',
('amount', '<', 0.0),
('project_id', '!=', False),
'&',
('amount', '=', 0),
'|',
('move_id', '=', False),
('move_id.account_id.user_type_id', '=', expense_type_id)
]
domain = [('so_line', 'in', self.ids), '|', ('amount', '<=', 0.0), ('project_id', '!=', False)]
return super(SaleOrderLine, self)._compute_analytic(domain=domain)
+1 -1
View File
@@ -105,7 +105,7 @@
<div class="oe_kanban_global_click" style="position: relative">
<a t-if="! read_only_mode" type="delete" style="position: absolute; right: 0; padding: 4px; diplay: inline-block">X</a>
<div class="oe_module_vignette">
<img t-att-src="kanban_image('res.users', 'image_small', record.id.value)" class="oe_avatar oe_kanban_avatar_smallbox"/>
<img t-att-src="kanban_image('res.users', 'image_small', record.id.raw_value)" class="oe_avatar oe_kanban_avatar_smallbox"/>
<div class="oe_module_desc">
<field name="name"/>
</div>
+6
View File
@@ -343,6 +343,12 @@ class Product(models.Model):
def action_view_routes(self):
return self.mapped('product_tmpl_id').action_view_routes()
@api.multi
def write(self, values):
res = super(Product, self).write(values)
if 'active' in values and not values['active'] and self.mapped('orderpoint_ids').filtered(lambda r: r.active):
raise UserError(_('You still have some active reordering rules on this product. Please archive or delete them first.'))
return res
class ProductTemplate(models.Model):
_inherit = 'product.template'
@@ -53,7 +53,8 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
*/
on_attach_callback: function() {
this.is_in_DOM = true;
if (this.active_view && this.active_view.controller.on_attach_callback) {
var controller = this.active_view && this.active_view.controller;
if (controller && this.active_view.controller.on_attach_callback) {
this.active_view.controller.on_attach_callback();
}
},
@@ -579,10 +580,11 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
// Wrong default_* and search_default_* values will no give the expected result
// Wrong group_by values will simply fail and forbid rendering of the destination view
var ncontext = new Context(
_.object(_.reject(_.pairs(_.extend({}, action_data.context, self.env.context)), function(pair) {
_.object(_.reject(_.pairs(self.env.context), function(pair) {
return pair[0].match('^(?:(?:default_|search_default_|show_).+|.+_view_ref|group_by|group_by_no_leaf|active_id|active_ids)$') !== null;
}))
);
ncontext.add(action_data.context || {});
ncontext.add({active_model: self.env.modelName});
if (res_ids) {
ncontext.add({
@@ -65,9 +65,12 @@ var AbstractField = Widget.extend({
* @constructor
* @param {Widget} parent
* @param {string} name The field name defined in the model
* @param {Object} record A record object (result of the get method of a basic model)
* @param {Object} record A record object (result of the get method of
* a basic model)
* @param {Object} [options]
* @param {string} [options.mode=readonly] should be 'readonly' or 'edit'
* @param {string} [options.standalone=false] this flag indicates that the
* field does not have a controller to delegate to
*/
init: function (parent, name, record, options) {
this._super(parent);
@@ -157,6 +160,10 @@ var AbstractField = Widget.extend({
// calls to the format (resp. parse) function.
this.formatOptions = {};
this.parseOptions = {};
// standalone is a flag indicating that the field does not have a
// controller to delegate actions to
this.standalone = options.standalone;
},
/**
* When a field widget is appended to the DOM, its start method is called,
+11 -3
View File
@@ -29,12 +29,20 @@ var _t = core._t;
/**
* @todo Really? it returns a jqueryElement... We should try to move this to a
* module with dom helpers functions, such as web.dom, maybe. And replace this
* with a function that returns a string
* with a function that returns a string so we can get rid of the forceString.
*
* @param {boolean} value
* @returns {jQueryElement}
* @param {Object} [field]
* a description of the field (note: this parameter is ignored)
* @param {Object} [options] additional options
* @param {boolean} [options.forceString=false] if true, returns a string
* representation of the boolean rather than a jQueryElement
* @returns {jQueryElement | string}
*/
function formatBoolean(value) {
function formatBoolean(value, field, options) {
if (options && options.forceString) {
return value ? _t('True') : _t('False');
}
var $input = $('<input/>', {
type: 'checkbox',
}).prop({
@@ -223,21 +223,25 @@ var FieldMany2One = AbstractField.extend({
_quickCreate: function (name) {
var self = this;
var slowCreate = this._searchCreatePopup.bind(this, "form", false, this._createContext(name));
var action = function () {
return self._rpc({
model: self.field.relation,
method: 'name_create',
args: [name],
context: self.record.getContext(self.recordParams),
}).then(function (result) {
if (self.mode === "edit") {
self.reinitialize({id: result[0], display_name: result[1]});
}
}, slowCreate);
};
if (this.nodeOptions.quick_create) {
this.trigger_up('mutexify', {
action: function () {
return self._rpc({
model: self.field.relation,
method: 'name_create',
args: [name],
context: self.record.getContext(self.recordParams),
}).then(function (result) {
if (self.mode === "edit") {
self.reinitialize({id: result[0], display_name: result[1]});
}
}, slowCreate);
},
});
if (this.standalone) {
action();
} else {
this.trigger_up('mutexify', {action: action});
}
} else {
slowCreate();
}
@@ -15,6 +15,7 @@ odoo.define('web.AbstractModel', function (require) {
*/
var Class = require('web.Class');
var fieldUtils = require('web.field_utils');
var mixins = require('web.mixins');
var ServicesMixin = require('web.ServicesMixin');
@@ -63,6 +64,30 @@ var AbstractModel = Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
reload: function (params) {
return $.when();
},
/**
* Processes date(time) and selection field values sent by the server.
* Converts data(time) values to moment instances.
* Converts false values of selection fields to 0 if 0 is a valid key,
* because the server doesn't make a distinction between false and 0, and
* always sends false when value is 0.
*
* @param {Object} field the field description
* @param {*} value
* @returns {*} the processed value
*/
_parseServerValue: function (field, value) {
if (field.type === 'date' || field.type === 'datetime') {
// process date(time): convert into a moment instance
value = fieldUtils.parse[field.type](value, field, {isUTC: true});
} else if (field.type === 'selection' && value === false) {
// process selection: convert false to 0, if 0 is a valid key
var hasKey0 = _.find(field.selection, function (option) {
return option[0] === 0;
});
value = hasKey0 ? 0 : value;
}
return value;
},
});
return AbstractModel;
@@ -84,10 +84,9 @@ odoo.define('web.BasicModel', function (require) {
var AbstractModel = require('web.AbstractModel');
var concurrency = require('web.concurrency');
var core = require('web.core');
var Context = require('web.Context');
var core = require('web.core');
var Domain = require('web.Domain');
var fieldUtils = require('web.field_utils');
var session = require('web.session');
var _t = core._t;
@@ -2370,8 +2369,8 @@ var BasicModel = AbstractModel.extend({
* if given, this field's context is added to the context, instead of
* the element's context (except if options.full is true)
* @param {boolean} [options.full=false]
* if true and fieldName given in options, the element's context
* is added to the context
* if true or nor fieldName or additionalContext given in options,
* the element's context is added to the context
* @returns {Object} the evaluated context
*/
_getContext: function (element, options) {
@@ -2379,7 +2378,7 @@ var BasicModel = AbstractModel.extend({
var context = new Context(session.user_context);
context.set_eval_context(this._getEvalContext(element));
if (options.full || !options.fieldName) {
if (options.full || !(options.fieldName || options.additionalContext)) {
context.add(element.context);
}
if (options.fieldName) {
@@ -2656,8 +2655,14 @@ var BasicModel = AbstractModel.extend({
var type = params.type || ('domain' in params && 'list') || 'record';
var res_id, value;
var res_ids = params.res_ids || [];
var data = params.data || (type === 'record' ? {} : []);
if (type === 'record') {
res_id = params.res_id || (params.data && params.data.id) || _.uniqueId('virtual_');
res_id = params.res_id || (params.data && params.data.id);
if (res_id) {
data.id = res_id;
} else {
res_id = _.uniqueId('virtual_');
}
} else {
var isValueArray = params.value instanceof Array;
res_id = isValueArray ? params.value[0] : undefined;
@@ -2675,7 +2680,7 @@ var BasicModel = AbstractModel.extend({
aggregateValues: params.aggregateValues || {},
context: params.context || {},
count: params.count || res_ids.length,
data: params.data || (type === 'record' ? {} : []),
data: data,
domain: params.domain || [],
fields: fields,
fieldsInfo: params.fieldsInfo,
@@ -2940,30 +2945,6 @@ var BasicModel = AbstractModel.extend({
}
});
},
/**
* Processes date(time) and selection field values sent by the server.
* Converts data(time) values to moment instances.
* Converts false values of selection fields to 0 if 0 is a valid key,
* because the server doesn't make a distinction between false and 0, and
* always sends false when value is 0.
*
* @param {Object} field the field description
* @param {*} value
* @returns {*} the processed value
*/
_parseServerValue: function (field, value) {
if (field.type === 'date' || field.type === 'datetime') {
// process date(time): convert into a moment instance
value = fieldUtils.parse[field.type](value, field, {isUTC: true});
} else if (field.type === 'selection' && value === false) {
// process selection: convert false to 0, if 0 is a valid key
var hasKey0 = _.find(field.selection, function (option) {
return option[0] === 0;
});
value = hasKey0 ? 0 : value;
}
return value;
},
/**
* This method is quite important: it is supposed to perform the /onchange
* rpc and apply the result.
@@ -3087,7 +3068,8 @@ var BasicModel = AbstractModel.extend({
lazy: true,
})
.then(function (groups) {
var rawGroupBy = list.groupedBy[0].split(':')[0];
var groupByField = list.groupedBy[0];
var rawGroupBy = groupByField.split(':')[0];
var previousGroups = _.map(list.data, function (groupID) {
return self.localData[groupID];
});
@@ -3098,13 +3080,13 @@ var BasicModel = AbstractModel.extend({
_.each(groups, function (group) {
var aggregateValues = {};
_.each(group, function (value, key) {
if (_.contains(fields, key) && key !== list.groupedBy[0]) {
if (_.contains(fields, key) && key !== groupByField) {
aggregateValues[key] = value;
}
});
// When a view is grouped, we need to display the name of each group in
// the 'title'.
var value = group[rawGroupBy];
var value = group[groupByField];
if (list.fields[rawGroupBy].type === "selection") {
var choice = _.find(list.fields[rawGroupBy].selection, function (c) {
return c[0] === value;
@@ -406,12 +406,13 @@ return AbstractModel.extend({
});
}
return eventsDef.then(function (events) {
self.data.data = _.map(events, self._recordToCalendarEvent.bind(self));
return $.when(
self._loadColors(self.data, self.data.data),
self._loadRecordsToFilters(self.data, self.data.data)
);
});
self._parseServerData(events);
self.data.data = _.map(events, self._recordToCalendarEvent.bind(self));
return $.when(
self._loadColors(self.data, self.data.data),
self._loadRecordsToFilters(self.data, self.data.data)
);
});
});
},
/**
@@ -566,11 +567,24 @@ return AbstractModel.extend({
if (filter.filters.length && (filter.filters[0].avatar_model in to_read)) {
_.each(filter.filters, function (f) {
f.label = to_read[f.avatar_model][f.value];
});
});
}
});
});
},
/**
* parse the server values to javascript framwork
*
* @param {Object} data the server data to parse
*/
_parseServerData: function (data) {
var self = this;
_.each(data, function(event) {
_.each(self.fieldNames, function (fieldName) {
event[fieldName] = self._parseServerValue(self.fields[fieldName], event[fieldName]);
});
});
},
/**
* Transform OpenERP event object to fullcalendar event object
*/
@@ -583,11 +597,11 @@ return AbstractModel.extend({
attendees = [];
if (!all_day) {
date_start = fieldUtils.parse.datetime(evt[this.mapping.date_start]);
date_stop = this.mapping.date_stop ? fieldUtils.parse.datetime(evt[this.mapping.date_stop]) : null;
date_start = evt[this.mapping.date_start].clone();
date_stop = this.mapping.date_stop ? evt[this.mapping.date_stop].clone() : null;
} else {
date_start = fieldUtils.parse.datetime(evt[this.mapping.date_start].split(' ')[0]);
date_stop = this.mapping.date_stop ? fieldUtils.parse.datetime(evt[this.mapping.date_stop].split(' ')[0]) : null;
date_start = evt[this.mapping.date_start].clone().startOf('day');
date_stop = this.mapping.date_stop ? evt[this.mapping.date_stop].clone().startOf('day') : null;
}
if (!date_stop && date_delay) {
@@ -605,8 +619,8 @@ return AbstractModel.extend({
'record': evt,
'start': date_start,
'end': date_stop,
'r_start':date_start.clone(),
'r_end':date_stop.clone(),
'r_start': date_start,
'r_end': date_stop,
'title': the_title,
'allDay': this.mapping.all_day && evt[this.mapping.all_day] || false,
'id': evt.id,
@@ -288,7 +288,7 @@ return AbstractRenderer.extend({
if (field.type === "one2many" || field.type === "many2many") {
return field_utils.format[field.type]({data: record[fieldName]}, field);
} else {
return field_utils.format[field.type](record[fieldName], field);
return field_utils.format[field.type](record[fieldName], field, {forceString: true});
}
},
/**
@@ -60,12 +60,13 @@ return AbstractModel.extend({
this.modelName = params.modelName;
this.chart = {
data: [],
groupedBy: params.groupBys,
groupedBy: params.groupedBy.length ? params.groupedBy : params.groupBys,
measure: params.measure,
mode: params.mode,
domain: params.domain,
context: params.context,
};
this.defaultGroupedBy = params.groupedBy;
return this._loadGraph();
},
/**
@@ -88,11 +89,7 @@ return AbstractModel.extend({
this.chart.domain = params.domain;
}
if ('groupBy' in params) {
if (!params.groupBy.length) {
this.chart.groupedBy = this.initialGroupBys;
} else {
this.chart.groupedBy = params.groupBy;
}
this.chart.groupedBy = params.groupBy.length ? params.groupBy : this.defaultGroupedBy;
}
if ('measure' in params) {
this.chart.measure = params.measure;
@@ -164,9 +164,10 @@ var KanbanController = BasicController.extend({
_onAddColumn: function (event) {
var self = this;
this.model.createGroup(event.data.value, this.handle).then(function () {
self.update({}, {reload: false});
return self.update({}, {reload: false});
}).then(function () {
self._updateButtons();
self.trigger_up('scrollTo', {selector: '.o_column_quick_create'});
self.renderer.quickCreateToggleFold();
});
},
/**
@@ -192,7 +192,7 @@ var RecordQuickCreate = AbstractQuickCreate.extend({
var ColumnQuickCreate = AbstractQuickCreate.extend({
template: 'KanbanView.ColumnQuickCreate',
events: _.extend({}, AbstractQuickCreate.prototype.events, {
'click': '_onClick',
'click': 'toggleFold',
'click input': '_onInputClicked',
'focusout': '_onFocusout',
}),
@@ -213,6 +213,22 @@ var ColumnQuickCreate = AbstractQuickCreate.extend({
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Toggle fold/unfold the Column quick create widget
*/
toggleFold: function () {
this.folded = !this.folded;
this._update();
if (!this.folded) {
this.$input.focus();
this.trigger_up('scrollTo', {selector: '.o_column_quick_create'});
}
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
@@ -241,18 +257,6 @@ var ColumnQuickCreate = AbstractQuickCreate.extend({
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {MouseEvent} event
*/
_onClick: function () {
this.folded = !this.folded;
this._update();
if (!this.folded) {
this.$input.focus();
this.trigger_up('scrollTo', {selector: '.o_column_quick_create'});
}
},
/**
* @private
*/
@@ -112,6 +112,12 @@ var KanbanRenderer = BasicRenderer.extend({
addQuickCreate: function () {
this.widgets[0].addQuickCreate();
},
/**
* Toggle fold/unfold the Column quick create widget
*/
quickCreateToggleFold: function () {
this.quickCreate.toggleFold();
},
/**
* Removes a widget (record if ungrouped, column if grouped) from the view.
*
@@ -244,8 +250,8 @@ var KanbanRenderer = BasicRenderer.extend({
// Enable column quickcreate
if (this.columnOptions.group_creatable) {
var quickCreate = new ColumnQuickCreate(this);
quickCreate.appendTo(fragment);
this.quickCreate = new ColumnQuickCreate(this);
this.quickCreate.appendTo(fragment);
}
}
@@ -377,6 +377,10 @@ var ListRenderer = BasicRenderer.extend({
$cells.unshift($('<td>'));
}
var name = group.value === undefined ? _t('Undefined') : group.value;
var groupBy = this.state.groupedBy[groupLevel];
if (group.fields[groupBy.split(':')[0]].type !== 'boolean') {
name = name || _t('Undefined');
}
var $th = $('<th>')
.addClass('o_group_name')
.text(name + ' (' + group.count + ')');
@@ -229,11 +229,12 @@ var PivotModel = AbstractModel.extend({
this.data = {
domain: params.domain,
context: _.extend({}, session.user_context, params.context),
groupedBy: params.rowGroupBys,
groupedBy: params.groupedBy,
colGroupBys: params.colGroupBys,
measures: params.measures,
sorted_column: {},
};
this.defaultGroupedBy = params.groupedBy;
return this._loadData();
},
/**
@@ -250,7 +251,7 @@ var PivotModel = AbstractModel.extend({
this.data.domain = this.initialDomain;
}
if ('groupBy' in params) {
this.data.groupedBy = params.groupBy;
this.data.groupedBy = params.groupBy.length ? params.groupBy : this.defaultGroupedBy;
}
if (!this.data.has_data) {
return this._loadData();
+1 -1
View File
@@ -127,7 +127,7 @@
}
}
> .o_field_translate {
position: absolute;
.o-position-absolute(0, 5px);
width: 16px; // to have some space on the right
}
.o_input {
@@ -1,11 +1,13 @@
odoo.define('web.relational_fields_tests', function (require) {
"use strict";
var BasicModel = require('web.BasicModel');
var concurrency = require('web.concurrency');
var FormView = require('web.FormView');
var ListView = require('web.ListView');
var relationalFields = require('web.relational_fields');
var testUtils = require('web.test_utils');
var Widget = require('web.Widget');
var createView = testUtils.createView;
@@ -361,7 +363,6 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('many2one readonly fields with option "no_open"', function (assert) {
assert.expect(1);
@@ -564,10 +565,71 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('many2one field with option standalone', function (assert) {
assert.expect(3);
var done = assert.async();
var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
var fixture = $('#qunit-fixture');
var self = this;
var model = testUtils.createModel({
Model: BasicModel,
data: this.data,
});
model.makeRecord('coucou', [{
name: 'partner_id',
relation: 'partner',
type: 'many2one',
value: [1, 'first partner'],
}], {
partner_id: {
options: {
no_open: true,
},
},
}).then(function (recordID) {
var record = model.get(recordID);
var parent = new Widget();
testUtils.addMockEnvironment(parent, {
data: self.data,
mockRPC: function (route, args) {
assert.step(args.method);
return this._super.apply(this, arguments);
},
});
var relField = new relationalFields.FieldMany2One(parent,
'partner_id',
record,
{
mode: 'edit',
standalone: true,
});
relField.appendTo(fixture);
$('input.o_input').val('xyzzrot').trigger('input');
concurrency.delay(0).then(function () {
var $dropdown = $('input.o_input').autocomplete('widget');
$dropdown.find('.o_m2o_dropdown_option:contains(Create)')
.first().mouseenter().click();
assert.verifySteps(['name_search', 'name_create']);
parent.destroy();
model.destroy();
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
done();
});
});
});
// QUnit.test('onchange on a many2one to a different model', function (assert) {
// This test is commented because the mock server does not give the correct response.
// It should return a couple [id, display_name], but I don't know the logic used
// by the server, so it's hard to emulate it correctly
// This test is commented because the mock server does not give the correct response.
// It should return a couple [id, display_name], but I don't know the logic used
// by the server, so it's hard to emulate it correctly
// assert.expect(2);
// this.data.partner.records[0].product_id = 41;
@@ -995,14 +1057,16 @@ QUnit.module('relational_fields', {
assert.deepEqual(
args.kwargs.context,
{hey: "ho", hello: "world", test: "yop"},
'the field attr context should have been used for the '
+ 'RPC (evaluated and merged with the session one)');
'the field attr context should have been used for the ' +
'RPC (evaluated and merged with the session one)');
return $.when([]);
}
if (args.method === 'name_search' && args.model === 'partner') {
assert.deepEqual(args.kwargs.args, [['id', 'in', [12]]],
'the field attr domain should have been used for the RPC (and evaluated)');
assert.deepEqual(args.kwargs.context, {hey: 'ho', timmy: [[6, false, [12]]]},
'the field attr context should have been used for the RPC (and evaluated)');
return $.when([]);
}
return this._super.apply(this, arguments);
},
@@ -3652,6 +3716,34 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('id in one2many obtained in onchange is properly set', function (assert) {
assert.expect(1);
this.data.partner.onchanges.turtles = function (obj) {
obj.turtles = [
[5],
[1, 3, {turtle_foo: "kawa"}]
];
};
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="turtles">' +
'<tree>' +
'<field name="id"/>' +
'<field name="turtle_foo"/>' +
'</tree>' +
'</field>' +
'</form>',
});
assert.strictEqual(form.$('tr.o_data_row').text(), '3kawa',
"should have properly displayed id and foo field");
form.destroy();
});
QUnit.test('sub form view with a required field', function (assert) {
assert.expect(2);
this.data.partner.fields.foo.required = true;
+4 -20
View File
@@ -465,25 +465,7 @@ var MockServer = Class.extend({
_mockNameSearch: function (model, args, _kwargs) {
var str = args && typeof args[0] === 'string' ? args[0] : _kwargs.name;
var domain = (args && args[1]) || _kwargs.args || [];
var dom = domain[0];
var records = this.data[model].records;
if (dom) {
records = _.filter(records, function (record) {
var value = record[dom[0]];
if (value instanceof Array) {
value = value[0];
}
if (dom[1] === 'not in') {
return !_.contains(dom[2], value);
} else if (dom[1] === 'in') {
return _.contains(dom[2], value);
} else if (dom[1] === '==') {
return dom[2] == value;
} else if (dom[1] === '!=') {
return dom[2] != value;
}
});
}
var records = this._getRecords(model, domain);
if (str.length) {
records = _.filter(records, function (record) {
return record.display_name.indexOf(str) !== -1;
@@ -632,7 +614,9 @@ var MockServer = Class.extend({
var fieldName = groupByField.split(':')[0];
var aggregateFunction = groupByField.split(':')[1] || 'month';
if (fields[fieldName].type === 'date') {
if (aggregateFunction === 'day') {
if (!val) {
return false;
} else if (aggregateFunction === 'day') {
return moment(val).format('YYYY-MM-DD');
} else {
return moment(val).format('MMMM YYYY');
@@ -3,6 +3,7 @@ odoo.define('web.calendar_tests', function (require) {
var CalendarView = require('web.CalendarView');
var CalendarRenderer = require('web.CalendarRenderer');
var fieldUtils = require('web.field_utils');
var testUtils = require('web.test_utils');
var session = require('web.session');
@@ -431,6 +432,8 @@ QUnit.module('Views', {
'mode="week" '+
'readonly_form_view_id="1">'+
'<field name="name"/>'+
'<field name="start"/>'+
'<field name="allday"/>'+
'</calendar>',
archs: archs,
viewOptions: {
@@ -478,14 +481,14 @@ QUnit.module('Views', {
$('.modal button.btn:contains(Create)').trigger('click');
var $newevent = calendar.$('.fc-event:contains(new event)');
assert.strictEqual($newevent.text().replace(/[\s\n\r]+/g, ''), "08:00-10:00newevent",
"should display the new event with time and title");
assert.strictEqual($newevent.text().replace(/[\s\n\r]+/g, ''), "08:00-10:00newevent12/13/201608:00:00False",
"should display the new event with time, title and additional fields");
assert.deepEqual($newevent.data('fcSeg').event.record,
{
display_name: "new event",
start: "2016-12-13 06:00:00",
stop: "2016-12-13 08:00:00",
start: fieldUtils.parse.datetime("2016-12-13 06:00:00", this.data.event.fields.start, {isUTC: true}),
stop: fieldUtils.parse.datetime("2016-12-13 08:00:00", this.data.event.fields.stop, {isUTC: true}),
allday: false,
name: "new event",
id: 1
@@ -616,8 +619,8 @@ QUnit.module('Views', {
assert.deepEqual($newevent.data('fcSeg').event.record,
{
display_name: "new event",
start: "2016-12-13 06:00:00",
stop: "2016-12-13 08:00:00",
start: fieldUtils.parse.datetime("2016-12-13 06:00:00", this.data.event.fields.start, {isUTC: true}),
stop: fieldUtils.parse.datetime("2016-12-13 08:00:00", this.data.event.fields.stop, {isUTC: true}),
allday: false,
name: "new event",
id: 1
@@ -691,8 +694,8 @@ QUnit.module('Views', {
assert.deepEqual($newevent.data('fcSeg').event.record,
{
display_name: "new event",
start: "2016-12-14 00:00:00",
stop: "2016-12-15 00:00:00",
start: fieldUtils.parse.datetime("2016-12-14 00:00:00", this.data.event.fields.start, {isUTC: true}),
stop: fieldUtils.parse.datetime("2016-12-15 00:00:00", this.data.event.fields.stop, {isUTC: true}),
allday: true,
name: "new event",
id: 1
+4 -1
View File
@@ -3555,10 +3555,13 @@ QUnit.module('Views', {
'</sheet>' +
'</form>',
res_id: 2,
viewOptions: {
context: {some_context: true},
},
intercepts: {
execute_action: function (e) {
assert.deepEqual(e.data.action_data.context, {test: 2},
"button context should have been evaluated and given to the action");
"button context should have been evaluated and given to the action, without previous context");
},
},
});
@@ -344,6 +344,34 @@ QUnit.module('Views', {
done();
});
});
QUnit.test('correctly use group_by key from the context', function (assert) {
var done = assert.async();
assert.expect(2);
var graph = createView({
View: GraphView,
model: 'foo',
data: this.data,
arch: '<graph><field name="product_id" /></graph>',
groupBy: ['color_id'],
viewOptions: {
context: {
graph_measure: 'foo',
graph_mode: 'line',
},
},
});
return concurrency.delay(0).then(function () {
assert.strictEqual(graph.$('text:contains(xphone)').length, 0,
'should not contain a text element with product in legend');
assert.strictEqual(graph.$('text:contains(red)').length, 1,
'should contain a text element with color in legend');
graph.destroy();
done();
});
});
});
});
@@ -1626,6 +1626,35 @@ QUnit.module('Views', {
"column should contain 1 records");
kanban.destroy();
});
QUnit.test('don\'t fold column quick create after creation', function (assert) {
assert.expect(2);
var kanban = createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban on_create="quick_create">' +
'<field name="product_id"/>' +
'<templates><t t-name="kanban-box">' +
'<div><field name="foo"/></div>' +
'</t></templates>' +
'</kanban>',
groupBy: ['product_id'],
});
// add a new column
kanban.$('.o_kanban_group:first .o_kanban_quick_add').click();
var $quickCreate = kanban.$('.o_kanban_quick_create');
$quickCreate.find('input').val('new partner');
$quickCreate.find('button.o_kanban_add').click();
assert.strictEqual(this.data.partner.records.length, 5,
"should have created a 'new partner' column");
assert.strictEqual(kanban.$('.o_kanban_add:visible').length, 1,
"the add button should still be visible");
kanban.destroy();
});
});
});
@@ -2466,6 +2466,23 @@ QUnit.module('Views', {
list.destroy();
});
QUnit.test('list grouped by date:month', function (assert) {
assert.expect(1);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="date"/></tree>',
groupBy: ['date:month'],
});
assert.strictEqual(list.$('tbody').text(), "January 2017 (1)Undefined (3)",
"the group names should be correct");
list.destroy();
});
});
});
@@ -825,4 +825,38 @@ QUnit.module('Views', {
pivot.destroy();
});
QUnit.test('correctly use group_by key from the context', function (assert) {
assert.expect(7);
var pivot = createView({
View: PivotView,
model: 'partner',
data: this.data,
arch: '<pivot>' +
'<field name="customer" type="col" />' +
'<field name="foo" type="measure" />' +
'</pivot>',
groupBy: ['product_id'],
});
assert.strictEqual(pivot.$('thead .o_pivot_header_cell_opened').length, 1,
'column: should have one opened header');
assert.strictEqual(pivot.$('thead .o_pivot_header_cell_closed:contains(First)').length, 1,
'column: should display one closed header with "First"');
assert.strictEqual(pivot.$('thead .o_pivot_header_cell_closed:contains(Second)').length, 1,
'column: should display one closed header with "Second"');
assert.strictEqual(pivot.$('tbody .o_pivot_header_cell_opened').length, 1,
'row: should have one opened header');
assert.strictEqual(pivot.$('tbody .o_pivot_header_cell_closed:contains(xphone)').length, 1,
'row: should display one closed header with "xphone"');
assert.strictEqual(pivot.$('tbody .o_pivot_header_cell_closed:contains(xpad)').length, 1,
'row: should display one closed header with "xpad"');
assert.strictEqual(pivot.$('tbody tr:first td:nth(3)').text(), '32',
'selected measure should be foo, with total 32');
pivot.destroy();
});
});});
@@ -134,12 +134,18 @@ if(!$('.o_website_quote').length) {
case "h1":
var id = self.setElementId('quote_header_', el);
var text = self.extractText($(el));
if (!text) {
break;
}
last_li = $("<li>").append($('<a href="#'+id+'"/>').text(text)).appendTo(self.$el);
last_ul = false;
break;
case "h2":
var id = self.setElementId('quote_', el);
var text = self.extractText($(el));
if (!text) {
break;
}
if (last_li) {
if (!last_ul) {
last_ul = $("<ul class='nav'>").appendTo(last_li);
+1 -1
View File
@@ -174,7 +174,7 @@
<div class="products_pager">
<div class="row" style="width: 100%;">
<t t-call="website_sale.search">
<t t-set="search_class">pagination form-inline o_website_sale_search</t>
<t t-set="search_class" t-value="'pagination form-inline o_website_sale_search'"/>
</t>
<t t-call="website_sale.pricelist_list" />
<t t-call="website.pager" />
+14
View File
@@ -0,0 +1,14 @@
Finland, 2017-07-17
Oy Tawasta OS Technologies Ltd. agrees to the terms of the Odoo Corporate Contributor License Agreement v1.0.
We declare that we are authorized and able to make this agreement and sign this declaration.
Signed,
Jarmo Kortetjärvi jarmo.kortetjarvi@tawasta.fi https://github.com/jarmokortetjarvi
List of contributors:
Aleksi Savijoki aleksi.savijoki@tawasta.fi https://github.com/savijoki
Jarmo Kortetjärvi jarmo.kortetjarvi@tawasta.fi https://github.com/jarmokortetjarvi
+10
View File
@@ -0,0 +1,10 @@
India, 2017-07-04
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,
Rakesh Sindhav sindhav21591@gmail.com https://github.com/sindhav
+11
View File
@@ -0,0 +1,11 @@
Finland, 2017-07-19
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,
Timo Talvitie timo.talvitie@vizucom.com https://github.com/timotalvitie
+1 -1
View File
@@ -11,7 +11,7 @@ diff --git a/openacademy/models.py b/openacademy/models.py
+from datetime import timedelta
from odoo import models, fields, api, exceptions
class Session(models.Model):
class Course(models.Model):
@@ -55,6 +56,8 @@ class Session(models.Model):
attendee_ids = fields.Many2many('res.partner', string="Attendees")
+1 -1
View File
@@ -17,7 +17,7 @@ Index: addons/openacademy/views/openacademy.xml
===================================================================
--- addons.orig/openacademy/views/openacademy.xml 2014-08-28 14:21:58.627015606 +0200
+++ addons/openacademy/views/openacademy.xml 2014-08-28 14:21:58.623015606 +0200
@@ -169,11 +169,57 @@
@@ -168,11 +168,57 @@
</field>
</record>
+2 -2
View File
@@ -30,11 +30,11 @@ dictionary, where each key specifies module metadatum.
""",
# data files always loaded at installation
'data': [
'mymodule_view.xml',
'views/mymodule_view.xml',
],
# data files containing optionally loaded demonstration data
'demo': [
'demo_data.xml',
'demo/demo_data.xml',
],
}
+15 -2
View File
@@ -802,6 +802,19 @@ class IrModelFields(models.Model):
records = self.browse([fields_data.pop(name)['id'] for name in extra_names])
records.with_context(**{MODULE_UNINSTALL_FLAG: True}).unlink()
@tools.ormcache()
def _all_manual_field_data(self):
cr = self._cr
cr.execute("SELECT * FROM ir_model_fields WHERE state='manual'")
result = defaultdict(dict)
for row in cr.dictfetchall():
result[row['model']][row['name']] = row
return result
def _get_manual_field_data(self, model_name):
""" Return the given model's manual field data. """
return self._all_manual_field_data().get(model_name, {})
def _instanciate_attrs(self, field_data):
""" Return the parameters for a field instance for ``field_data``. """
attrs = {
@@ -830,7 +843,7 @@ class IrModelFields(models.Model):
if not self.pool.loaded and not (
field_data['relation'] in self.env and (
field_data['relation_field'] in self.env[field_data['relation']]._fields or
field_data['relation_field'] in self._existing_field_data(field_data['relation'])
field_data['relation_field'] in self._get_manual_field_data(field_data['relation'])
)):
return
attrs['comodel_name'] = field_data['relation']
@@ -858,7 +871,7 @@ class IrModelFields(models.Model):
def _add_manual_fields(self, model):
""" Add extra fields on model. """
fields_data = self._existing_field_data(model._name)
fields_data = self._get_manual_field_data(model._name)
for name, field_data in pycompat.items(fields_data):
if name not in model._fields and field_data['state'] == 'manual':
field = self._instanciate(field_data)
+1 -1
View File
@@ -444,7 +444,7 @@
<div class="o_kanban_tags_section oe_kanban_partner_categories"/>
<div class="o_kanban_image">
<t t-if="record.image_small.raw_value">
<img t-att-src="kanban_image('res.partner', 'image_small', record.id.value)"/>
<img t-att-src="kanban_image('res.partner', 'image_small', record.id.raw_value)"/>
</t>
<t t-if="!record.image_small.raw_value">
<t t-if="record.type.raw_value === 'delivery'">