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/.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/.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/.po
source_file = addons/base_setup/i18n/base_setup.pot
@@ -892,6 +897,11 @@ file_filter = addons/website_sale/i18n/.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/.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/.po
source_file = addons/website_sale_delivery/i18n/website_sale_delivery.pot
@@ -912,6 +922,11 @@ file_filter = addons/website_sale_stock/i18n/.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/.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/.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 37ec02186e9..522d459a47b 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,
@@ -1578,6 +1577,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 f22ef71527c..1775eba10ad 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 @@
-
Exchange rates can be automatically updated once a day from Yahoo Finance or the European Central Bank. You can activate this feature in the bottom of the Accounting Settingsaccounting settings.
+
Exchange rates can be automatically updated once a day from Yahoo Finance or the European Central Bank. You can activate this feature in the bottom of the Accounting Settings.
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/delivery/models/delivery_grid.py b/addons/delivery/models/delivery_grid.py
index 49678160fec..8f5c0ab347e 100644
--- a/addons/delivery/models/delivery_grid.py
+++ b/addons/delivery/models/delivery_grid.py
@@ -58,7 +58,7 @@ class ProviderGrid(models.Model):
price_unit = order.company_id.currency_id.with_context(date=order.date_order).compute(price_unit, order.pricelist_id.currency_id)
return {'success': True,
- 'price': self.fixed_price,
+ 'price': price_unit,
'error_message': False,
'warning_message': False}
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 @@
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 @@
Coupons échus à l'encaissement5111
+
@@ -2935,6 +2936,7 @@
Chèques à encaisser5112
+
@@ -2942,6 +2944,7 @@
Effets à l'encaissement5113
+
@@ -2949,6 +2952,7 @@
Effets à l'escompte5114
+
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 @@
-
+
#
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/pad/py_etherpad/__init__.py b/addons/pad/py_etherpad/__init__.py
index d1040cec852..fc9721eb9c8 100644
--- a/addons/pad/py_etherpad/__init__.py
+++ b/addons/pad/py_etherpad/__init__.py
@@ -30,7 +30,7 @@ class EtherpadLiteClient:
params = arguments or {}
params['apikey'] = self.apiKey
- r = requests.get(url, data=params, timeout=self.TIMEOUT)
+ r = requests.post(url, data=params, timeout=self.TIMEOUT)
r.raise_for_status()
return self.handleResult(r.json())
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 @@
-
+
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 @@
-
+
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 @@
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 = $('', {
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,
@@ -2940,30 +2945,6 @@ var BasicModel = AbstractModel.extend({
}
});
},
- /**
- * Processes date(time) and selection field values sent by the server.
- * Converts data(time) values to moment instances.
- * Converts false values of selection fields to 0 if 0 is a valid key,
- * because the server doesn't make a distinction between false and 0, and
- * always sends false when value is 0.
- *
- * @param {Object} field the field description
- * @param {*} value
- * @returns {*} the processed value
- */
- _parseServerValue: function (field, value) {
- if (field.type === 'date' || field.type === 'datetime') {
- // process date(time): convert into a moment instance
- value = fieldUtils.parse[field.type](value, field, {isUTC: true});
- } else if (field.type === 'selection' && value === false) {
- // process selection: convert false to 0, if 0 is a valid key
- var hasKey0 = _.find(field.selection, function (option) {
- return option[0] === 0;
- });
- value = hasKey0 ? 0 : value;
- }
- return value;
- },
/**
* This method is quite important: it is supposed to perform the /onchange
* rpc and apply the result.
@@ -3087,7 +3068,8 @@ var BasicModel = AbstractModel.extend({
lazy: true,
})
.then(function (groups) {
- var rawGroupBy = list.groupedBy[0].split(':')[0];
+ var groupByField = list.groupedBy[0];
+ var rawGroupBy = groupByField.split(':')[0];
var previousGroups = _.map(list.data, function (groupID) {
return self.localData[groupID];
});
@@ -3098,13 +3080,13 @@ var BasicModel = AbstractModel.extend({
_.each(groups, function (group) {
var aggregateValues = {};
_.each(group, function (value, key) {
- if (_.contains(fields, key) && key !== list.groupedBy[0]) {
+ if (_.contains(fields, key) && key !== groupByField) {
aggregateValues[key] = value;
}
});
// When a view is grouped, we need to display the name of each group in
// the 'title'.
- var value = group[rawGroupBy];
+ var value = group[groupByField];
if (list.fields[rawGroupBy].type === "selection") {
var choice = _.find(list.fields[rawGroupBy].selection, function (c) {
return c[0] === value;
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,11 +567,24 @@ return AbstractModel.extend({
if (filter.filters.length && (filter.filters[0].avatar_model in to_read)) {
_.each(filter.filters, function (f) {
f.label = to_read[f.avatar_model][f.value];
- });
+ });
}
});
});
},
+ /**
+ * parse the server values to javascript framwork
+ *
+ * @param {Object} data the server data to parse
+ */
+ _parseServerData: function (data) {
+ var self = this;
+ _.each(data, function(event) {
+ _.each(self.fieldNames, function (fieldName) {
+ event[fieldName] = self._parseServerValue(self.fields[fieldName], event[fieldName]);
+ });
+ });
+ },
/**
* Transform OpenERP event object to fullcalendar event object
*/
@@ -583,11 +597,11 @@ return AbstractModel.extend({
attendees = [];
if (!all_day) {
- date_start = fieldUtils.parse.datetime(evt[this.mapping.date_start]);
- date_stop = this.mapping.date_stop ? fieldUtils.parse.datetime(evt[this.mapping.date_stop]) : null;
+ date_start = evt[this.mapping.date_start].clone();
+ date_stop = this.mapping.date_stop ? evt[this.mapping.date_stop].clone() : null;
} else {
- date_start = fieldUtils.parse.datetime(evt[this.mapping.date_start].split(' ')[0]);
- date_stop = this.mapping.date_stop ? fieldUtils.parse.datetime(evt[this.mapping.date_stop].split(' ')[0]) : null;
+ date_start = evt[this.mapping.date_start].clone().startOf('day');
+ date_stop = this.mapping.date_stop ? evt[this.mapping.date_stop].clone().startOf('day') : null;
}
if (!date_stop && date_delay) {
@@ -605,8 +619,8 @@ return AbstractModel.extend({
'record': evt,
'start': date_start,
'end': date_stop,
- 'r_start':date_start.clone(),
- 'r_end':date_stop.clone(),
+ 'r_start': date_start,
+ 'r_end': date_stop,
'title': the_title,
'allDay': this.mapping.all_day && evt[this.mapping.all_day] || false,
'id': evt.id,
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/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',
}),
@@ -213,6 +213,22 @@ var ColumnQuickCreate = AbstractQuickCreate.extend({
return this._super.apply(this, arguments);
},
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Toggle fold/unfold the Column quick create widget
+ */
+ toggleFold: function () {
+ this.folded = !this.folded;
+ this._update();
+ if (!this.folded) {
+ this.$input.focus();
+ this.trigger_up('scrollTo', {selector: '.o_column_quick_create'});
+ }
+ },
+
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
@@ -241,18 +257,6 @@ var ColumnQuickCreate = AbstractQuickCreate.extend({
// Handlers
//--------------------------------------------------------------------------
- /**
- * @private
- * @param {MouseEvent} event
- */
- _onClick: function () {
- this.folded = !this.folded;
- this._update();
- if (!this.folded) {
- this.$input.focus();
- this.trigger_up('scrollTo', {selector: '.o_column_quick_create'});
- }
- },
/**
* @private
*/
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
@@ -112,6 +112,12 @@ var KanbanRenderer = BasicRenderer.extend({
addQuickCreate: function () {
this.widgets[0].addQuickCreate();
},
+ /**
+ * Toggle fold/unfold the Column quick create widget
+ */
+ quickCreateToggleFold: function () {
+ this.quickCreate.toggleFold();
+ },
/**
* Removes a widget (record if ungrouped, column if grouped) from the view.
*
@@ -244,8 +250,8 @@ var KanbanRenderer = BasicRenderer.extend({
// Enable column quickcreate
if (this.columnOptions.group_creatable) {
- var quickCreate = new ColumnQuickCreate(this);
- quickCreate.appendTo(fragment);
+ this.quickCreate = new ColumnQuickCreate(this);
+ this.quickCreate.appendTo(fragment);
}
}
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($('
'));
}
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 = $('
')
.addClass('o_group_name')
.text(name + ' (' + group.count + ')');
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: '',
+ });
+
+ 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">'+
''+
+ ''+
+ ''+
'',
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', {
'' +
'',
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/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: '' +
+ '' +
+ '' +
+ '
' +
+ '' +
+ '',
+ 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: '',
+ 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/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 = $("
").append($('').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 = $("
diff --git a/addons/website_sale_delivery/controllers/main.py b/addons/website_sale_delivery/controllers/main.py
index cbb456d7968..e9403cb26bb 100644
--- a/addons/website_sale_delivery/controllers/main.py
+++ b/addons/website_sale_delivery/controllers/main.py
@@ -44,8 +44,14 @@ class WebsiteSaleDelivery(WebsiteSale):
has_stockable_products = any(line.product_id.type in ['consu', 'product'] for line in order.order_line)
if has_stockable_products:
+ if order.carrier_id and not order.delivery_rating_success:
+ values['errors'].append(
+ (_("Ouch, you cannot choose this carrier!"),
+ _("%s does not ship to your address, please choose another one.") % order.carrier_id.name))
+ order._remove_delivery_line()
+
delivery_carriers = order._get_delivery_methods()
- values['deliveries'] = delivery_carriers.sudo().with_context(order_id=order.id)
+ values['deliveries'] = delivery_carriers.sudo()
values['delivery_action_id'] = request.env.ref('delivery.action_delivery_carrier_form').id
return values
diff --git a/addons/website_sale_delivery/models/sale_order.py b/addons/website_sale_delivery/models/sale_order.py
index 996ae6eaf89..036d2a13a4f 100644
--- a/addons/website_sale_delivery/models/sale_order.py
+++ b/addons/website_sale_delivery/models/sale_order.py
@@ -2,8 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
-from odoo import api, fields, models, _
-from odoo.exceptions import ValidationError
+from odoo import api, fields, models
_logger = logging.getLogger(__name__)
@@ -61,7 +60,8 @@ class SaleOrder(models.Model):
self.write({'carrier_id': carrier.id})
if carrier:
self.get_delivery_price()
- self.set_delivery_line()
+ if self.delivery_rating_success:
+ self.set_delivery_line()
else:
self._remove_delivery_line()
@@ -85,8 +85,4 @@ class SaleOrder(models.Model):
values = super(SaleOrder, self)._cart_update(product_id, line_id, add_qty, set_qty, **kwargs)
- if add_qty or set_qty is not None:
- for sale_order in self:
- self._check_carrier_quotation()
-
return values
diff --git a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js
index 46caab18e0e..ff48ebd9dfd 100644
--- a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js
+++ b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js
@@ -1,3 +1,4 @@
+'use strict';
$(document).ready(function () {
// When choosing an delivery carrier, update the quotation and the acquirers
@@ -11,7 +12,7 @@ $(document).ready(function () {
var value = $(this).val();
var $provider_free = $("select[name='country_id']:not(.o_provider_restricted), select[name='state_id']:not(.o_provider_restricted)");
var $provider_restricted = $("select[name='country_id'].o_provider_restricted, select[name='state_id'].o_provider_restricted");
- if (value == 0) {
+ if (value === 0) {
// Ship to the same address : only show shipping countries available for billing
$provider_free.hide().attr('disabled', true);
$provider_restricted.show().attr('disabled', false).change();
diff --git a/addons/website_sale_delivery/views/website_sale_delivery_templates.xml b/addons/website_sale_delivery/views/website_sale_delivery_templates.xml
index a220f02591b..5c87f8d7e2c 100644
--- a/addons/website_sale_delivery/views/website_sale_delivery_templates.xml
+++ b/addons/website_sale_delivery/views/website_sale_delivery_templates.xml
@@ -25,20 +25,14 @@
-
Choose a delivery method
+
Choose a delivery method
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 @@
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 89d34000c49..4fa66406108 100644
--- a/odoo/addons/base/ir/ir_model.py
+++ b/odoo/addons/base/ir/ir_model.py
@@ -711,19 +711,12 @@ class IrModelFields(models.Model):
res.append((field.id, '%s (%s)' % (field.field_description, field.model)))
return res
- @tools.ormcache()
- def _all_field_data(self):
- """ Return all fields data, indexed by model name, then field name. """
- cr = self._cr
- cr.execute("SELECT * FROM ir_model_fields")
- result = defaultdict(dict)
- for row in cr.dictfetchall():
- result[row['model']][row['name']] = row
- return result
-
- def _get_field_data(self, model_name):
+ @tools.ormcache('model_name')
+ def _existing_field_data(self, model_name):
""" Return the given model's existing field data. """
- return self._all_field_data()[model_name]
+ cr = self._cr
+ cr.execute("SELECT * FROM ir_model_fields WHERE model=%s", [model_name])
+ return {row['name']: row for row in cr.dictfetchall()}
def _reflect_field_params(self, field):
""" Return the values to write to the database for the given field. """
@@ -753,7 +746,7 @@ class IrModelFields(models.Model):
def _reflect_field(self, field):
""" Reflect the given field and return its corresponding record. """
- fields_data = self._get_field_data(field.model_name)
+ fields_data = self._existing_field_data(field.model_name)
field_data = fields_data.get(field.name)
params = self._reflect_field_params(field)
@@ -801,7 +794,7 @@ class IrModelFields(models.Model):
if not self.pool._init:
# remove ir.model.fields that are not in self._fields
- fields_data = self._get_field_data(model._name)
+ fields_data = self._existing_field_data(model._name)
extra_names = set(fields_data) - set(model._fields)
if extra_names:
# add key MODULE_UNINSTALL_FLAG in context to (1) force the
@@ -809,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 = {
@@ -837,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._get_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']
@@ -865,7 +871,7 @@ class IrModelFields(models.Model):
def _add_manual_fields(self, model):
""" Add extra fields on model. """
- fields_data = self._get_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 @@
-
+
diff --git a/odoo/release.py b/odoo/release.py
index c563d9ff4cd..711cb318ad8 100644
--- a/odoo/release.py
+++ b/odoo/release.py
@@ -12,7 +12,7 @@ RELEASE_LEVELS_DISPLAY = {ALPHA: ALPHA,
# properly comparable using normal operarors, for example:
# (6,1,0,'beta',0) < (6,1,0,'candidate',1) < (6,1,0,'candidate',2)
# (6,1,0,'candidate',2) < (6,1,0,'final',0) < (6,1,2,'final',0)
-version_info = (11, 0, 0, ALPHA, 1, '')
+version_info = (10, 'saas~17', 0, FINAL, 0, '')
version = '.'.join(str(s) for s in version_info[:2]) + RELEASE_LEVELS_DISPLAY[version_info[3]] + str(version_info[4] or '') + version_info[5]
series = serie = major_version = '.'.join(str(s) for s in version_info[:2])