summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2017-07-20 11:48:49 +0200
committerChristophe Simonis <chs@odoo.com>2017-07-20 11:48:49 +0200
commitfa3e89064a503c95ab2b283e5d5b39ca182f8f47 (patch)
treed638f0cb99fc973169d670ff0bafd1b9474b4771
parent0ae679e433ee4c5ca32ae1302d87d7fb33e0c78d (diff)
parent104dfe3c07d36a6487f045f31d0999cc429cb5bb (diff)
[MERGE] forward port branch saas-16 up to 104dfe3c07d
-rw-r--r--.tx/config15
-rw-r--r--addons/account/models/account_bank_statement.py2
-rw-r--r--addons/account/models/account_invoice.py15
-rw-r--r--addons/account/models/company.py10
-rw-r--r--addons/account/report/account_aged_partner_balance.py2
-rw-r--r--addons/account/report/account_balance.py2
-rw-r--r--addons/account/report/account_general_ledger.py2
-rw-r--r--addons/account/report/account_journal.py2
-rw-r--r--addons/account/report/account_partner_ledger.py2
-rw-r--r--addons/account/report/account_report_financial.py2
-rw-r--r--addons/account/views/web_planner_data.xml2
-rw-r--r--addons/account_bank_statement_import/account_bank_statement_import.py2
-rw-r--r--addons/fleet/views/fleet_vehicle_model_views.xml2
-rw-r--r--addons/fleet/views/fleet_vehicle_views.xml4
-rw-r--r--addons/gamification/data/goal_base.xml302
-rw-r--r--addons/hr/views/hr_views.xml2
-rw-r--r--addons/hr_attendance/views/hr_employee_view.xml2
-rw-r--r--addons/hr_expense/data/hr_expense_data.xml2
-rw-r--r--addons/hr_expense/models/hr_expense.py30
-rw-r--r--addons/hr_expense/views/hr_expense_views.xml2
-rw-r--r--addons/hr_holidays/report/holidays_summary_report.py2
-rw-r--r--addons/hr_payroll/report/report_contribution_register.py2
-rw-r--r--addons/im_livechat/views/im_livechat_channel_views.xml4
-rw-r--r--addons/l10n_fr/data/account_chart_template_data.xml4
-rw-r--r--addons/l10n_in_hr_payroll/report/report_hr_salary_employee_bymonth.py2
-rw-r--r--addons/l10n_in_hr_payroll/report/report_hr_yearly_salary_detail.py2
-rw-r--r--addons/mail/views/mail_channel_views.xml2
-rw-r--r--addons/mass_mailing/models/mail_mail.py7
-rw-r--r--addons/mass_mailing/models/mail_thread.py10
-rw-r--r--addons/mrp_repair/models/mrp_repair.py11
-rw-r--r--addons/payment/views/payment_views.xml2
-rw-r--r--addons/payment_authorize/models/payment.py4
-rw-r--r--addons/point_of_sale/models/pos_order.py2
-rw-r--r--addons/point_of_sale/models/pos_session.py2
-rw-r--r--addons/point_of_sale/static/src/js/chrome.js2
-rw-r--r--addons/point_of_sale/views/pos_order_report_view.xml2
-rw-r--r--addons/product/views/product_template_views.xml2
-rw-r--r--addons/product/views/product_views.xml2
-rw-r--r--addons/sale/models/sale_analytic.py15
-rw-r--r--addons/sale/wizard/sale_make_invoice_advance.py1
-rw-r--r--addons/sale_timesheet/models/sale_order.py15
-rw-r--r--addons/sales_team/views/crm_team_views.xml2
-rw-r--r--addons/stock/models/product.py6
-rw-r--r--addons/web/static/src/js/chrome/view_manager.js6
-rw-r--r--addons/web/static/src/js/fields/abstract_field.js9
-rw-r--r--addons/web/static/src/js/fields/field_utils.js14
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js32
-rw-r--r--addons/web/static/src/js/views/abstract_model.js25
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js50
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_model.js40
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_renderer.js2
-rw-r--r--addons/web/static/src/js/views/graph/graph_model.js9
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_controller.js5
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_quick_create.js30
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_renderer.js10
-rw-r--r--addons/web/static/src/js/views/list/list_renderer.js4
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_model.js5
-rw-r--r--addons/web/static/src/less/list_view.less2
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js104
-rw-r--r--addons/web/static/tests/helpers/mock_server.js24
-rw-r--r--addons/web/static/tests/views/calendar_tests.js19
-rw-r--r--addons/web/static/tests/views/form_tests.js5
-rw-r--r--addons/web/static/tests/views/graph_tests.js28
-rw-r--r--addons/web/static/tests/views/kanban_tests.js29
-rw-r--r--addons/web/static/tests/views/list_tests.js17
-rw-r--r--addons/web/static/tests/views/pivot_tests.js34
-rw-r--r--addons/website_quote/static/src/js/website_quotation.js6
-rw-r--r--addons/website_sale/views/templates.xml2
-rw-r--r--doc/cla/corporate/tawasta.md14
-rw-r--r--doc/cla/individual/sindhav.md10
-rw-r--r--doc/cla/individual/timotalvitie.md11
-rw-r--r--doc/howtos/backend/exercise-calendar2
-rw-r--r--doc/howtos/backend/exercise-kanban2
-rw-r--r--doc/reference/module.rst4
-rw-r--r--odoo/addons/base/ir/ir_model.py17
-rw-r--r--odoo/addons/base/res/res_partner_view.xml2
76 files changed, 792 insertions, 288 deletions
diff --git a/.tx/config b/.tx/config
index 674a24a43df..c2a261b582b 100644
--- a/.tx/config
+++ b/.tx/config
@@ -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
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
index 85bed6a05ad..5f1ae94b93a 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -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
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index 24e8abc3049..79e8e18fd1b 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -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"
diff --git a/addons/account/models/company.py b/addons/account/models/company.py
index 44eee5432ae..584c1e44c99 100644
--- a/addons/account/models/company.py
+++ b/addons/account/models/company.py
@@ -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):
diff --git a/addons/account/report/account_aged_partner_balance.py b/addons/account/report/account_aged_partner_balance.py
index 3380f876d63..fd2fef466fb 100644
--- a/addons/account/report/account_aged_partner_balance.py
+++ b/addons/account/report/account_aged_partner_balance.py
@@ -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')
diff --git a/addons/account/report/account_balance.py b/addons/account/report/account_balance.py
index e8280c90128..fd4ae5d0379 100644
--- a/addons/account/report/account_balance.py
+++ b/addons/account/report/account_balance.py
@@ -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', []))
diff --git a/addons/account/report/account_general_ledger.py b/addons/account/report/account_general_ledger.py
index 1bd983a68ae..c817d300664 100644
--- a/addons/account/report/account_general_ledger.py
+++ b/addons/account/report/account_general_ledger.py
@@ -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', []))
diff --git a/addons/account/report/account_journal.py b/addons/account/report/account_journal.py
index 0bb9bcedb99..7ae49486fbf 100644
--- a/addons/account/report/account_journal.py
+++ b/addons/account/report/account_journal.py
@@ -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')
diff --git a/addons/account/report/account_partner_ledger.py b/addons/account/report/account_partner_ledger.py
index b3e0b03b6ba..a0438540d61 100644
--- a/addons/account/report/account_partner_ledger.py
+++ b/addons/account/report/account_partner_ledger.py
@@ -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'] = {}
diff --git a/addons/account/report/account_report_financial.py b/addons/account/report/account_report_financial.py
index b0abf69de3d..803abe9b890 100644
--- a/addons/account/report/account_report_financial.py
+++ b/addons/account/report/account_report_financial.py
@@ -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'))
diff --git a/addons/account/views/web_planner_data.xml b/addons/account/views/web_planner_data.xml
index 690d160f23e..d07827629c5 100644
--- a/addons/account/views/web_planner_data.xml
+++ b/addons/account/views/web_planner_data.xml
@@ -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">
diff --git a/addons/account_bank_statement_import/account_bank_statement_import.py b/addons/account_bank_statement_import/account_bank_statement_import.py
index 07ab4c7dc6e..9056b7af916 100644
--- a/addons/account_bank_statement_import/account_bank_statement_import.py
+++ b/addons/account_bank_statement_import/account_bank_statement_import.py
@@ -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
diff --git a/addons/fleet/views/fleet_vehicle_model_views.xml b/addons/fleet/views/fleet_vehicle_model_views.xml
index da4027f01d7..20d66fd232b 100644
--- a/addons/fleet/views/fleet_vehicle_model_views.xml
+++ b/addons/fleet/views/fleet_vehicle_model_views.xml
@@ -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">
diff --git a/addons/fleet/views/fleet_vehicle_views.xml b/addons/fleet/views/fleet_vehicle_views.xml
index 699d06a877b..4355823971e 100644
--- a/addons/fleet/views/fleet_vehicle_views.xml
+++ b/addons/fleet/views/fleet_vehicle_views.xml
@@ -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> \ No newline at end of file
+</odoo>
diff --git a/addons/gamification/data/goal_base.xml b/addons/gamification/data/goal_base.xml
index 06aede03c49..3b983ebd256 100644
--- a/addons/gamification/data/goal_base.xml
+++ b/addons/gamification/data/goal_base.xml
@@ -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 -->
diff --git a/addons/hr/views/hr_views.xml b/addons/hr/views/hr_views.xml
index e910db920e6..c5392a8f55d 100644
--- a/addons/hr/views/hr_views.xml
+++ b/addons/hr/views/hr_views.xml
@@ -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>
diff --git a/addons/hr_attendance/views/hr_employee_view.xml b/addons/hr_attendance/views/hr_employee_view.xml
index 25b307b1c0d..b187f370f60 100644
--- a/addons/hr_attendance/views/hr_employee_view.xml
+++ b/addons/hr_attendance/views/hr_employee_view.xml
@@ -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">
diff --git a/addons/hr_expense/data/hr_expense_data.xml b/addons/hr_expense/data/hr_expense_data.xml
index b922a38d43c..100282658a8 100644
--- a/addons/hr_expense/data/hr_expense_data.xml
+++ b/addons/hr_expense/data/hr_expense_data.xml
@@ -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>
diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py
index 70f44d413fb..cdcf25b491f 100644
--- a/addons/hr_expense/models/hr_expense.py
+++ b/addons/hr_expense/models/hr_expense.py
@@ -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):
diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml
index d00772c22c2..20d3fb741e1 100644
--- a/addons/hr_expense/views/hr_expense_views.xml
+++ b/addons/hr_expense/views/hr_expense_views.xml
@@ -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"/>
diff --git a/addons/hr_holidays/report/holidays_summary_report.py b/addons/hr_holidays/report/holidays_summary_report.py
index e9e166dc3a7..9d1def98339 100644
--- a/addons/hr_holidays/report/holidays_summary_report.py
+++ b/addons/hr_holidays/report/holidays_summary_report.py
@@ -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)
diff --git a/addons/hr_payroll/report/report_contribution_register.py b/addons/hr_payroll/report/report_contribution_register.py
index 27b0612e5f1..5023b1213e3 100644
--- a/addons/hr_payroll/report/report_contribution_register.py
+++ b/addons/hr_payroll/report/report_contribution_register.py
@@ -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)
diff --git a/addons/im_livechat/views/im_livechat_channel_views.xml b/addons/im_livechat/views/im_livechat_channel_views.xml
index 0513e7472ef..e5af9e925f2 100644
--- a/addons/im_livechat/views/im_livechat_channel_views.xml
+++ b/addons/im_livechat/views/im_livechat_channel_views.xml
@@ -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>
diff --git a/addons/l10n_fr/data/account_chart_template_data.xml b/addons/l10n_fr/data/account_chart_template_data.xml
index 59fb761390d..aea92e3e02f 100644
--- a/addons/l10n_fr/data/account_chart_template_data.xml
+++ b/addons/l10n_fr/data/account_chart_template_data.xml
@@ -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>
diff --git a/addons/l10n_in_hr_payroll/report/report_hr_salary_employee_bymonth.py b/addons/l10n_in_hr_payroll/report/report_hr_salary_employee_bymonth.py
index 16e17b2fbba..c1c426c1574 100644
--- a/addons/l10n_in_hr_payroll/report/report_hr_salary_employee_bymonth.py
+++ b/addons/l10n_in_hr_payroll/report/report_hr_salary_employee_bymonth.py
@@ -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'))
diff --git a/addons/l10n_in_hr_payroll/report/report_hr_yearly_salary_detail.py b/addons/l10n_in_hr_payroll/report/report_hr_yearly_salary_detail.py
index 38cee62d220..43e00295f33 100644
--- a/addons/l10n_in_hr_payroll/report/report_hr_yearly_salary_detail.py
+++ b/addons/l10n_in_hr_payroll/report/report_hr_yearly_salary_detail.py
@@ -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'))
diff --git a/addons/mail/views/mail_channel_views.xml b/addons/mail/views/mail_channel_views.xml
index f319b86122e..d31c585eb39 100644
--- a/addons/mail/views/mail_channel_views.xml
+++ b/addons/mail/views/mail_channel_views.xml
@@ -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">
diff --git a/addons/mass_mailing/models/mail_mail.py b/addons/mass_mailing/models/mail_mail.py
index 1772cdaf1ff..e5072060fd5 100644
--- a/addons/mass_mailing/models/mail_mail.py
+++ b/addons/mass_mailing/models/mail_mail.py
@@ -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)
diff --git a/addons/mass_mailing/models/mail_thread.py b/addons/mass_mailing/models/mail_thread.py
index 8e515a664f8..ee5fb45f6eb 100644
--- a/addons/mass_mailing/models/mail_thread.py
+++ b/addons/mass_mailing/models/mail_thread.py
@@ -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)
diff --git a/addons/mrp_repair/models/mrp_repair.py b/addons/mrp_repair/models/mrp_repair.py
index a098d0b08fa..0fab1440d12 100644
--- a/addons/mrp_repair/models/mrp_repair.py
+++ b/addons/mrp_repair/models/mrp_repair.py
@@ -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
diff --git a/addons/payment/views/payment_views.xml b/addons/payment/views/payment_views.xml
index 82606809f5d..c626657bc42 100644
--- a/addons/payment/views/payment_views.xml
+++ b/addons/payment/views/payment_views.xml
@@ -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>
diff --git a/addons/payment_authorize/models/payment.py b/addons/payment_authorize/models/payment.py
index dfa181b2e81..184f07cdc58 100644
--- a/addons/payment_authorize/models/payment.py
+++ b/addons/payment_authorize/models/payment.py
@@ -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
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index 482ca1d809a..f848a64cbe1 100644
--- a/addons/point_of_sale/models/pos_order.py
+++ b/addons/point_of_sale/models/pos_order.py
@@ -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,
diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py
index 67d9ca973a0..d9f38720fd4 100644
--- a/addons/point_of_sale/models/pos_session.py
+++ b/addons/point_of_sale/models/pos_session.py
@@ -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',
diff --git a/addons/point_of_sale/static/src/js/chrome.js b/addons/point_of_sale/static/src/js/chrome.js
index 150a6930835..3d95af17a61 100644
--- a/addons/point_of_sale/static/src/js/chrome.js
+++ b/addons/point_of_sale/static/src/js/chrome.js
@@ -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 = '/';
diff --git a/addons/point_of_sale/views/pos_order_report_view.xml b/addons/point_of_sale/views/pos_order_report_view.xml
index 1ca45a9f5ce..f15704b891a 100644
--- a/addons/point_of_sale/views/pos_order_report_view.xml
+++ b/addons/point_of_sale/views/pos_order_report_view.xml
@@ -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"/>
diff --git a/addons/product/views/product_template_views.xml b/addons/product/views/product_template_views.xml
index f3316c537d9..96c3bfe333f 100644
--- a/addons/product/views/product_template_views.xml
+++ b/addons/product/views/product_template_views.xml
@@ -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>
diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml
index eef7edbe839..af0981744d1 100644
--- a/addons/product/views/product_views.xml
+++ b/addons/product/views/product_views.xml
@@ -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>
diff --git a/addons/sale/models/sale_analytic.py b/addons/sale/models/sale_analytic.py
index cad770f842d..733bd74bc2a 100644
--- a/addons/sale/models/sale_analytic.py
+++ b/addons/sale/models/sale_analytic.py
@@ -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)
diff --git a/addons/sale/wizard/sale_make_invoice_advance.py b/addons/sale/wizard/sale_make_invoice_advance.py
index 8fe7dc11f33..8f3884c032f 100644
--- a/addons/sale/wizard/sale_make_invoice_advance.py
+++ b/addons/sale/wizard/sale_make_invoice_advance.py
@@ -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()
diff --git a/addons/sale_timesheet/models/sale_order.py b/addons/sale_timesheet/models/sale_order.py
index 0254c3dbe0e..0db9b0c0632 100644
--- a/addons/sale_timesheet/models/sale_order.py
+++ b/addons/sale_timesheet/models/sale_order.py
@@ -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)
diff --git a/addons/sales_team/views/crm_team_views.xml b/addons/sales_team/views/crm_team_views.xml
index 27b14712f61..30bed47bd91 100644
--- a/addons/sales_team/views/crm_team_views.xml
+++ b/addons/sales_team/views/crm_team_views.xml
@@ -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>
diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py
index 557e9a5b8f2..6b43afdcaaa 100644
--- a/addons/stock/models/product.py
+++ b/addons/stock/models/product.py
@@ -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'
diff --git a/addons/web/static/src/js/chrome/view_manager.js b/addons/web/static/src/js/chrome/view_manager.js
index 9b431d3e57a..a4a1d65f7b6 100644
--- a/addons/web/static/src/js/chrome/view_manager.js
+++ b/addons/web/static/src/js/chrome/view_manager.js
@@ -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({
diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js
index 4ae4eec9c05..fa2f37a123f 100644
--- a/addons/web/static/src/js/fields/abstract_field.js
+++ b/addons/web/static/src/js/fields/abstract_field.js
@@ -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,
diff --git a/addons/web/static/src/js/fields/field_utils.js b/addons/web/static/src/js/fields/field_utils.js
index ac4c06454ea..0a44a52e315 100644
--- a/addons/web/static/src/js/fields/field_utils.js
+++ b/addons/web/static/src/js/fields/field_utils.js
@@ -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({
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 14512c02912..7522bb3bc37 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -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();
}
diff --git a/addons/web/static/src/js/views/abstract_model.js b/addons/web/static/src/js/views/abstract_model.js
index 27d13a12b06..4ea6b217e65 100644
--- a/addons/web/static/src/js/views/abstract_model.js
+++ b/addons/web/static/src/js/views/abstract_model.js
@@ -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;
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 5fb77204ec7..75a0ad02971 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -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,
@@ -2941,30 +2946,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;
diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js
index ded7938605f..73868a93fdb 100644
--- a/addons/web/static/src/js/views/calendar/calendar_model.js
+++ b/addons/web/static/src/js/views/calendar/calendar_model.js
@@ -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,12 +567,25 @@ 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
*/
_recordToCalendarEvent: function (evt) {
@@ -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,
diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js
index a4930c87d37..b61236131c6 100644
--- a/addons/web/static/src/js/views/calendar/calendar_renderer.js
+++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js
@@ -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});
}
},
/**
diff --git a/addons/web/static/src/js/views/graph/graph_model.js b/addons/web/static/src/js/views/graph/graph_model.js
index 5948e07a54f..8a7f5da6d5d 100644
--- a/addons/web/static/src/js/views/graph/graph_model.js
+++ b/addons/web/static/src/js/views/graph/graph_model.js
@@ -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;
diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js
index b5c60fe9ab3..2592e3d96ec 100644
--- a/addons/web/static/src/js/views/kanban/kanban_controller.js
+++ b/addons/web/static/src/js/views/kanban/kanban_controller.js
@@ -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();
});
},
/**
diff --git a/addons/web/static/src/js/views/kanban/kanban_quick_create.js b/addons/web/static/src/js/views/kanban/kanban_quick_create.js
index 5e40352b26d..e81a290bf2d 100644
--- a/addons/web/static/src/js/views/kanban/kanban_quick_create.js
+++ b/addons/web/static/src/js/views/kanban/kanban_quick_create.js
@@ -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',
}),
@@ -214,6 +214,22 @@ var ColumnQuickCreate = AbstractQuickCreate.extend({
},
//--------------------------------------------------------------------------
+ // 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
//--------------------------------------------------------------------------
@@ -243,18 +259,6 @@ var ColumnQuickCreate = AbstractQuickCreate.extend({
/**
* @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
*/
_onFocusout: function () {
var hasFocus = this.$(':focus').length > 0;
diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer.js b/addons/web/static/src/js/views/kanban/kanban_renderer.js
index 87275260b01..ea2f0270017 100644
--- a/addons/web/static/src/js/views/kanban/kanban_renderer.js
+++ b/addons/web/static/src/js/views/kanban/kanban_renderer.js
@@ -113,6 +113,12 @@ var KanbanRenderer = BasicRenderer.extend({
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.
*
* @param {Widget} widget the instance of the widget to remove
@@ -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);
}
}
diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js
index a917781b580..802f2f03a09 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -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 + ')');
diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js
index 49c00663d47..d74af7fee22 100644
--- a/addons/web/static/src/js/views/pivot/pivot_model.js
+++ b/addons/web/static/src/js/views/pivot/pivot_model.js
@@ -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();
diff --git a/addons/web/static/src/less/list_view.less b/addons/web/static/src/less/list_view.less
index a09b8e7ac0c..edb4c184408 100644
--- a/addons/web/static/src/less/list_view.less
+++ b/addons/web/static/src/less/list_view.less
@@ -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 {
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index aacbb744252..1a7badd353f 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -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;
diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js
index cab291ec012..06b85dd8f9e 100644
--- a/addons/web/static/tests/helpers/mock_server.js
+++ b/addons/web/static/tests/helpers/mock_server.js
@@ -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');
diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js
index 011843eefee..dfc42c6f746 100644
--- a/addons/web/static/tests/views/calendar_tests.js
+++ b/addons/web/static/tests/views/calendar_tests.js
@@ -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
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index 09ddcc20d46..183e5958b62 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -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");
},
},
});
diff --git a/addons/web/static/tests/views/graph_tests.js b/addons/web/static/tests/views/graph_tests.js
index 7a7d22d96d6..33b2fd8ffbd 100644
--- a/addons/web/static/tests/views/graph_tests.js
+++ b/addons/web/static/tests/views/graph_tests.js
@@ -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();
+ });
+ });
+
});
});
diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js
index 89ec104fc41..dd0a52868f6 100644
--- a/addons/web/static/tests/views/kanban_tests.js
+++ b/addons/web/static/tests/views/kanban_tests.js
@@ -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();
+ });
});
});
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index 09109d92b84..e3d8acfe84a 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -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();
+ });
});
});
diff --git a/addons/web/static/tests/views/pivot_tests.js b/addons/web/static/tests/views/pivot_tests.js
index c3acdb52399..267437845b0 100644
--- a/addons/web/static/tests/views/pivot_tests.js
+++ b/addons/web/static/tests/views/pivot_tests.js
@@ -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();
+ });
+
});});
diff --git a/addons/website_quote/static/src/js/website_quotation.js b/addons/website_quote/static/src/js/website_quotation.js
index afe84e5a57c..22b6ca94231 100644
--- a/addons/website_quote/static/src/js/website_quotation.js
+++ b/addons/website_quote/static/src/js/website_quotation.js
@@ -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);
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 3ef9a5acd12..18f3bd4957f 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -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" />
diff --git a/doc/cla/corporate/tawasta.md b/doc/cla/corporate/tawasta.md
new file mode 100644
index 00000000000..130e6adf0f2
--- /dev/null
+++ b/doc/cla/corporate/tawasta.md
@@ -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
diff --git a/doc/cla/individual/sindhav.md b/doc/cla/individual/sindhav.md
new file mode 100644
index 00000000000..876d54b2475
--- /dev/null
+++ b/doc/cla/individual/sindhav.md
@@ -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
+
diff --git a/doc/cla/individual/timotalvitie.md b/doc/cla/individual/timotalvitie.md
new file mode 100644
index 00000000000..1268e38225e
--- /dev/null
+++ b/doc/cla/individual/timotalvitie.md
@@ -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
diff --git a/doc/howtos/backend/exercise-calendar b/doc/howtos/backend/exercise-calendar
index 60774310884..f63422c95a1 100644
--- a/doc/howtos/backend/exercise-calendar
+++ b/doc/howtos/backend/exercise-calendar
@@ -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")
diff --git a/doc/howtos/backend/exercise-kanban b/doc/howtos/backend/exercise-kanban
index 68001ea87a8..14899f46a3d 100644
--- a/doc/howtos/backend/exercise-kanban
+++ b/doc/howtos/backend/exercise-kanban
@@ -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>
diff --git a/doc/reference/module.rst b/doc/reference/module.rst
index 76200fbc9a1..aecb8de8a8f 100644
--- a/doc/reference/module.rst
+++ b/doc/reference/module.rst
@@ -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',
],
}
diff --git a/odoo/addons/base/ir/ir_model.py b/odoo/addons/base/ir/ir_model.py
index 31828750dfe..4fa66406108 100644
--- a/odoo/addons/base/ir/ir_model.py
+++ b/odoo/addons/base/ir/ir_model.py
@@ -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)
diff --git a/odoo/addons/base/res/res_partner_view.xml b/odoo/addons/base/res/res_partner_view.xml
index 245081fc68b..b191cf845ec 100644
--- a/odoo/addons/base/res/res_partner_view.xml
+++ b/odoo/addons/base/res/res_partner_view.xml
@@ -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'">