diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 3265887f3cd..26577eaade2 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -361,7 +361,7 @@ class AccountInvoice(models.Model): return res @api.model - def fields_view_get(self, view_id=None, view_type=False, toolbar=False, submenu=False): + def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): def get_view_id(xid, name): try: return self.env.ref('account.' + xid) diff --git a/addons/hr/models/res_users.py b/addons/hr/models/res_users.py index f759aba30f5..ddbcc95c234 100644 --- a/addons/hr/models/res_users.py +++ b/addons/hr/models/res_users.py @@ -31,6 +31,7 @@ class User(models.Model): return self.env['hr.employee'].with_context(ctx).search([('user_id', '=', self.id)]) @api.multi + @api.returns('self', lambda value: value.id) def message_post(self, **kwargs): """ Redirect the posting of message on res.users to the related employees. This is done because when giving the context of Chatter on the diff --git a/addons/hr_attendance/models/hr_employee.py b/addons/hr_attendance/models/hr_employee.py index a4dac9883ea..dba3d2a3ba4 100644 --- a/addons/hr_attendance/models/hr_employee.py +++ b/addons/hr_attendance/models/hr_employee.py @@ -88,9 +88,6 @@ class HrEmployee(models.Model): self.ensure_one() action_message = self.env.ref('hr_attendance.hr_attendance_action_greeting_message').read()[0] action_message['previous_attendance_change_date'] = self.last_attendance_id and (self.last_attendance_id.check_out or self.last_attendance_id.check_in) or False - if action_message['previous_attendance_change_date']: - action_message['previous_attendance_change_date'] = \ - fields.Datetime.to_string(fields.Datetime.context_timestamp(self, fields.Datetime.from_string(action_message['previous_attendance_change_date']))) action_message['employee_name'] = self.name action_message['next_action'] = next_action diff --git a/addons/l10n_fr_fec/wizard/account_fr_fec.py b/addons/l10n_fr_fec/wizard/account_fr_fec.py index 604ffc4577b..215ed49cb4e 100644 --- a/addons/l10n_fr_fec/wizard/account_fr_fec.py +++ b/addons/l10n_fr_fec/wizard/account_fr_fec.py @@ -185,14 +185,17 @@ class AccountFrFec(models.TransientModel): unaffected_earnings_amount = float(unaffected_earnings_results[11].replace(',', '.')) - float(unaffected_earnings_results[12].replace(',', '.')) listrow_amount = current_amount + unaffected_earnings_amount if listrow_amount > 0: - listrow[11] = str(listrow_amount) - listrow[12] = '0.00' + listrow[11] = str(listrow_amount).replace('.', ',') + listrow[12] = '0,00' else: - listrow[11] = '0.00' - listrow[12] = str(listrow_amount) + listrow[11] = '0,00' + listrow[12] = str(-listrow_amount).replace('.', ',') w.writerow([s.encode("utf-8") for s in listrow]) #if the unaffected earnings account wasn't in the selection yet: add it manually - if not unaffected_earnings_line and unaffected_earnings_results and unaffected_earnings_results[11] != '0,00' and unaffected_earnings_results[12] != '0,00': + if (not unaffected_earnings_line + and unaffected_earnings_results + and (unaffected_earnings_results[11] != '0,00' + or unaffected_earnings_results[12] != '0,00')): #search an unaffected earnings account unaffected_earnings_account = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_unaffected_earnings').id)], limit=1) if unaffected_earnings_account: diff --git a/addons/mail/models/res_users.py b/addons/mail/models/res_users.py index 1f95c02f078..19630150a75 100644 --- a/addons/mail/models/res_users.py +++ b/addons/mail/models/res_users.py @@ -77,6 +77,7 @@ class Users(models.Model): return self.partner_id.id @api.multi + @api.returns('self', lambda value: value.id) def message_post(self, **kwargs): """ Redirect the posting of message on res.users as a private discussion. This is done because when giving the context of Chatter on the diff --git a/addons/membership/models/partner.py b/addons/membership/models/partner.py index 9b863bf4cce..8ee9bee2f5a 100644 --- a/addons/membership/models/partner.py +++ b/addons/membership/models/partner.py @@ -151,7 +151,7 @@ class Partner(models.Model): @api.one @api.constrains('associate_member') - def _check_recursion(self): + def _check_recursion_associate_member(self): level = 100 while self: self = self.associate_member diff --git a/addons/product_margin/models/product_product.py b/addons/product_margin/models/product_product.py index ef79a2d1714..fa65e79b38e 100644 --- a/addons/product_margin/models/product_product.py +++ b/addons/product_margin/models/product_product.py @@ -85,12 +85,9 @@ class ProductProduct(models.Model): date_from = self.env.context.get('date_from', time.strftime('%Y-01-01')) date_to = self.env.context.get('date_to', time.strftime('%Y-12-31')) invoice_state = self.env.context.get('invoice_state', 'open_paid') - if 'date_from' in field_names: - res[val.id]['date_from'] = date_from - if 'date_to' in field_names: - res[val.id]['date_to'] = date_to - if 'invoice_state' in field_names: - res[val.id]['invoice_state'] = invoice_state + res[val.id]['date_from'] = date_from + res[val.id]['date_to'] = date_to + res[val.id]['invoice_state'] = invoice_state invoice_types = () states = () if invoice_state == 'paid': @@ -136,14 +133,10 @@ class ProductProduct(models.Model): res[val.id]['normal_cost'] = val.standard_price * res[val.id]['purchase_num_invoiced'] res[val.id]['purchase_gap'] = res[val.id]['normal_cost'] - res[val.id]['total_cost'] - if 'total_margin' in field_names: - res[val.id]['total_margin'] = res[val.id]['turnover'] - res[val.id]['total_cost'] - if 'expected_margin' in field_names: - res[val.id]['expected_margin'] = res[val.id]['sale_expected'] - res[val.id]['normal_cost'] - if 'total_margin_rate' in field_names: - res[val.id]['total_margin_rate'] = res[val.id]['turnover'] and res[val.id]['total_margin'] * 100 / res[val.id]['turnover'] or 0.0 - if 'expected_margin_rate' in field_names: - res[val.id]['expected_margin_rate'] = res[val.id]['sale_expected'] and res[val.id]['expected_margin'] * 100 / res[val.id]['sale_expected'] or 0.0 + res[val.id]['total_margin'] = res[val.id]['turnover'] - res[val.id]['total_cost'] + res[val.id]['expected_margin'] = res[val.id]['sale_expected'] - res[val.id]['normal_cost'] + res[val.id]['total_margin_rate'] = res[val.id]['turnover'] and res[val.id]['total_margin'] * 100 / res[val.id]['turnover'] or 0.0 + res[val.id]['expected_margin_rate'] = res[val.id]['sale_expected'] and res[val.id]['expected_margin'] * 100 / res[val.id]['sale_expected'] or 0.0 for k, v in res[val.id].items(): setattr(val, k, v) return res diff --git a/addons/sale/models/sale_analytic.py b/addons/sale/models/sale_analytic.py index 45a2df1688a..acf3fd48682 100644 --- a/addons/sale/models/sale_analytic.py +++ b/addons/sale/models/sale_analytic.py @@ -90,11 +90,8 @@ class AccountAnalyticLine(models.Model): result = dict(vals or {}) so_line = result.get('so_line', False) or self.so_line if not so_line and self.account_id and self.product_id and (self.product_id.expense_policy != 'no'): - order = self.env['sale.order'].search([('project_id', '=', self.account_id.id)], limit=1) - if order and order.state != 'sale': - raise UserError(_('The Sales Order %s linked to the Analytic Account must be validated before registering expenses.') % order.name) - - order = self.env['sale.order'].search([('project_id', '=', self.account_id.id), ('state', '=', 'sale')], limit=1) + order_in_sale = self.env['sale.order'].search([('project_id', '=', self.account_id.id), ('state', '=', 'sale')], limit=1) + order = order_in_sale or self.env['sale.order'].search([('project_id', '=', self.account_id.id)], limit=1) if not order: return result price = self._get_invoice_price(order) @@ -106,6 +103,8 @@ class AccountAnalyticLine(models.Model): if so_lines: result.update({'so_line': so_lines[0].id}) else: + if order.state != 'sale': + raise UserError(_('The Sales Order %s linked to the Analytic Account must be validated before registering expenses.') % order.name) order_line_vals = self._get_sale_order_line_vals(order, price) if order_line_vals: so_line = self.env['sale.order.line'].create(order_line_vals) diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index 331c1f86ed8..4706bbc6b45 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -196,13 +196,13 @@ class StockMove(models.Model): new_std_price = move.get_price_unit() else: # Get the standard price - amount_unit = std_price_update.get(move.product_id.id) or move.product_id.standard_price + amount_unit = std_price_update.get((move.company_id.id, move.product_id.id)) or move.product_id.standard_price new_std_price = ((amount_unit * product_tot_qty_available) + (move.get_price_unit() * move.product_qty)) / (product_tot_qty_available + move.product_qty) tmpl_dict[move.product_id.id] += move.product_qty # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products - move.product_id.with_context(force_company=move.company_id.id).write({'standard_price': new_std_price}) - std_price_update[move.product_id.id] = new_std_price + move.product_id.with_context(force_company=move.company_id.id).sudo().write({'standard_price': new_std_price}) + std_price_update[move.company_id.id, move.product_id.id] = new_std_price @api.multi def product_price_update_after_done(self): diff --git a/addons/web/static/lib/qweb/qweb2.js b/addons/web/static/lib/qweb/qweb2.js index d5449dae6d9..1ea73e6278e 100644 --- a/addons/web/static/lib/qweb/qweb2.js +++ b/addons/web/static/lib/qweb/qweb2.js @@ -153,14 +153,15 @@ var QWeb2 = { foreach: function(context, enu, as, old_dict, callback) { if (enu != null) { var index, jlen, cur; - var size, new_dict = this.extend({}, old_dict); + var new_dict = this.extend({}, old_dict); new_dict[as + "_all"] = enu; var as_value = as + "_value", as_index = as + "_index", as_first = as + "_first", as_last = as + "_last", as_parity = as + "_parity"; - if (size = enu.length) { + if (enu instanceof Array) { + var size = enu.length; new_dict[as + "_size"] = size; for (index = 0, jlen = enu.length; index < jlen; index++) { cur = enu[index]; diff --git a/addons/web/static/src/less/form_view_layout.less b/addons/web/static/src/less/form_view_layout.less index fc5522035e5..7e108b6af2c 100644 --- a/addons/web/static/src/less/form_view_layout.less +++ b/addons/web/static/src/less/form_view_layout.less @@ -168,7 +168,9 @@ // Flex fields .o_form_field_many2one, .o_form_field_radio, .o_form_field_many2manytags, .o_form_field_percent_pie, .o_form_field_monetary, .o_form_field_binary_file { + @media (min-width: @screen-sm-min) { .o-inline-flex-display(); + } > span, > button { .o-flex(0, 0, auto); } diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index dcae38f7159..65cc369005c 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -191,9 +191,12 @@ class Website(Home): 'name': "/sitemap-%d.xml" % current_website.id, }) else: + # TODO: in master/saas-15, move current_website_id in template directly + pages_with_website = map(lambda p: "%d-%d" % (current_website.id, p), range(1, pages + 1)) + # Sitemaps must be split in several smaller files with a sitemap index content = View.render_template('website.sitemap_index_xml', { - 'pages': range(1, pages + 1), + 'pages': pages_with_website, 'url_root': request.httprequest.url_root, }) create_sitemap('/sitemap-%d.xml' % current_website.id, content) diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py index b7018705ada..4ecee90fd5e 100644 --- a/addons/website/models/ir_qweb.py +++ b/addons/website/models/ir_qweb.py @@ -23,6 +23,12 @@ class QWeb(models.AbstractModel): 'img': 'src', } + def _get_asset(self, xmlid, options, css=True, js=True, debug=False, async=False, values=None): + website = getattr(request, 'website', None) if request else None + if website and website.cdn_activated: + values = dict(values, url_for=website.get_cdn_url) + return super(QWeb, self)._get_asset(xmlid, options, css, js, debug, async, values) + def _website_build_attribute(self, tagName, name, value, options, values): """ Compute the value of an attribute while rendering the template. """ if name == self.URL_ATTRS.get(tagName) and values.get('url_for'): diff --git a/addons/website_blog/models/website_blog.py b/addons/website_blog/models/website_blog.py index 46e90fe4cd7..3f2e8292e50 100644 --- a/addons/website_blog/models/website_blog.py +++ b/addons/website_blog/models/website_blog.py @@ -33,6 +33,7 @@ class Blog(models.Model): return res @api.multi + @api.returns('self', lambda value: value.id) def message_post(self, parent_id=False, subtype=None, **kwargs): """ Temporary workaround to avoid spam. If someone replies on a channel through the 'Presentation Published' email, it should be considered as a diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py index 84f9ed7c117..d780e803e40 100644 --- a/addons/website_forum/models/forum.py +++ b/addons/website_forum/models/forum.py @@ -799,6 +799,7 @@ class Post(models.Model): return groups @api.multi + @api.returns('self', lambda value: value.id) def message_post(self, message_type='notification', subtype=None, **kwargs): question_followers = self.env['res.partner'] if self.ids and message_type == 'comment': # user comments have a restriction on karma diff --git a/addons/website_portal_sale/controllers/main.py b/addons/website_portal_sale/controllers/main.py index e421a9ce8b0..c931b839177 100644 --- a/addons/website_portal_sale/controllers/main.py +++ b/addons/website_portal_sale/controllers/main.py @@ -29,7 +29,7 @@ class website_account(website_account): invoice_count = Invoice.search_count([ ('type', 'in', ['out_invoice', 'out_refund']), ('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]), - ('state', 'in', ['open', 'paid', 'cancelled']) + ('state', 'in', ['open', 'paid', 'cancel']) ]) response.qcontext.update({ diff --git a/addons/website_slides/models/slides.py b/addons/website_slides/models/slides.py index 91211ea79f4..bd2a544a599 100644 --- a/addons/website_slides/models/slides.py +++ b/addons/website_slides/models/slides.py @@ -165,6 +165,7 @@ class Channel(models.Model): return res @api.multi + @api.returns('self', lambda value: value.id) def message_post(self, parent_id=False, subtype=None, **kwargs): """ Temporary workaround to avoid spam. If someone replies on a channel through the 'Presentation Published' email, it should be considered as a diff --git a/doc/cla/corporate/versada.md b/doc/cla/corporate/versada.md index 0d9e95d3c0f..90636f37bde 100644 --- a/doc/cla/corporate/versada.md +++ b/doc/cla/corporate/versada.md @@ -17,4 +17,4 @@ Kiril Vangelovski kiril@versada.eu https://github.com/kirca Naglis Jonaitis naglis@versada.eu https://github.com/naglis Paulius Sladkevičius paulius@versada.eu https://github.com/paulius-sladkevicius Tomas Račys tomas@versada.eu https://github.com/taezaz -Andrius Preimantas andrius@verasda.eu https://github.com/andrius-preimantas +Andrius Preimantas andrius@versada.eu https://github.com/andrius-preimantas diff --git a/doc/cla/individual/igallart.md b/doc/cla/individual/igallart.md new file mode 100644 index 00000000000..3bf5a8f735c --- /dev/null +++ b/doc/cla/individual/igallart.md @@ -0,0 +1,11 @@ +Spain, 2017-02-27 + +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, + +Isaac Gallart Bochons igallart@puntsistemes.es https://github.com/igallart diff --git a/odoo/addons/base/ir/ir_cron.py b/odoo/addons/base/ir/ir_cron.py index cb7a61b2b25..a6f29ee0d95 100644 --- a/odoo/addons/base/ir/ir_cron.py +++ b/odoo/addons/base/ir/ir_cron.py @@ -152,20 +152,20 @@ class ir_cron(models.Model): """ db = odoo.sql_db.db_connect(db_name) threading.current_thread().dbname = db_name - cr = db.cursor() jobs = [] try: - # Make sure the database we poll has the same version as the code of base - cr.execute("SELECT 1 FROM ir_module_module WHERE name=%s AND latest_version=%s", ('base', BASE_VERSION)) - if cr.fetchone(): - # Careful to compare timestamps with 'UTC' - everything is UTC as of v6.1. - cr.execute("""SELECT * FROM ir_cron - WHERE numbercall != 0 - AND active AND nextcall <= (now() at time zone 'UTC') - ORDER BY priority""") - jobs = cr.dictfetchall() - else: - _logger.warning('Skipping database %s as its base version is not %s.', db_name, BASE_VERSION) + with db.cursor() as cr: + # Make sure the database we poll has the same version as the code of base + cr.execute("SELECT 1 FROM ir_module_module WHERE name=%s AND latest_version=%s", ('base', BASE_VERSION)) + if cr.fetchone(): + # Careful to compare timestamps with 'UTC' - everything is UTC as of v6.1. + cr.execute("""SELECT * FROM ir_cron + WHERE numbercall != 0 + AND active AND nextcall <= (now() at time zone 'UTC') + ORDER BY priority""") + jobs = cr.dictfetchall() + else: + _logger.warning('Skipping database %s as its base version is not %s.', db_name, BASE_VERSION) except psycopg2.ProgrammingError, e: if e.pgcode == '42P01': # Class 42 — Syntax Error or Access Rule Violation; 42P01: undefined_table @@ -175,8 +175,6 @@ class ir_cron(models.Model): raise except Exception: _logger.warning('Exception in cron:', exc_info=True) - finally: - cr.close() for job in jobs: lock_cr = db.cursor() diff --git a/odoo/addons/base/ir/ir_qweb/assetsbundle.py b/odoo/addons/base/ir/ir_qweb/assetsbundle.py index 72b8a527e2b..d6b7705423a 100644 --- a/odoo/addons/base/ir/ir_qweb/assetsbundle.py +++ b/odoo/addons/base/ir/ir_qweb/assetsbundle.py @@ -175,8 +175,7 @@ class AssetsBundle(object): ] # force bundle invalidation on other workers - if 'xml' not in tools.config['dev_mode']: - self.env['ir.qweb']._get_asset.clear_cache(self.env['ir.qweb']) + self.env['ir.qweb'].clear_caches() return ira.sudo().search(domain).unlink() diff --git a/odoo/models.py b/odoo/models.py index 0a31ce1d044..06d40e9e21d 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -2866,6 +2866,11 @@ class BaseModel(object): if field.compute: cls._field_computed[field] = group = groups[field.compute] group.append(field) + for fields in groups.itervalues(): + compute_sudo = fields[0].compute_sudo + if not all(field.compute_sudo == compute_sudo for field in fields): + _logger.warning("%s: inconsistent 'compute_sudo' for computed fields: %s", + self._name, ", ".join(field.name for field in fields)) @api.model def _setup_complete(self): diff --git a/odoo/service/server.py b/odoo/service/server.py index b44640edbd6..03d49ee017b 100644 --- a/odoo/service/server.py +++ b/odoo/service/server.py @@ -217,8 +217,12 @@ class ThreadedServer(CommonServer): _logger.debug('cron%d polling for jobs', number) for db_name, registry in registries.iteritems(): while registry.ready: - acquired = odoo.addons.base.ir.ir_cron.ir_cron._acquire_job(db_name) - if not acquired: + try: + acquired = odoo.addons.base.ir.ir_cron.ir_cron._acquire_job(db_name) + if not acquired: + break + except Exception: + _logger.warning('cron%d encountered an Exception:', number, exc_info=True) break def cron_spawn(self):