diff --git a/addons/account/report/account_invoice_report.py b/addons/account/report/account_invoice_report.py index 7e50e500732..3f4c6f0f8a7 100644 --- a/addons/account/report/account_invoice_report.py +++ b/addons/account/report/account_invoice_report.py @@ -148,8 +148,8 @@ class account_invoice_report(osv.osv): ai.partner_bank_id, SUM(CASE WHEN ai.type::text = ANY (ARRAY['out_refund'::character varying::text, 'in_invoice'::character varying::text]) - THEN (- ail.quantity) / u.factor - ELSE ail.quantity / u.factor + THEN (- ail.quantity) / u.factor * u2.factor + ELSE ail.quantity / u.factor * u2.factor END) AS product_qty, SUM(CASE WHEN ai.type::text = ANY (ARRAY['out_refund'::character varying::text, 'in_invoice'::character varying::text]) @@ -197,6 +197,7 @@ class account_invoice_report(osv.osv): LEFT JOIN product_product pr ON pr.id = ail.product_id left JOIN product_template pt ON pt.id = pr.product_tmpl_id LEFT JOIN product_uom u ON u.id = ail.uos_id + LEFT JOIN product_uom u2 ON u.id = pt.uom_id """ return from_str diff --git a/addons/account/report/report_vat.py b/addons/account/report/report_vat.py index a86a180391c..70302b9d411 100644 --- a/addons/account/report/report_vat.py +++ b/addons/account/report/report_vat.py @@ -91,18 +91,18 @@ class tax_report(report_sxw.rml_parse, common_report_header): i = 0 top_result = [] while i < len(res): - res_dict = { 'code': res[i][1].code, - 'name': res[i][1].name, + res_dict = { 'code': res[i][1]['code'], + 'name': res[i][1]['name'], 'debit': 0, 'credit': 0, - 'tax_amount': res[i][1].sum_period, + 'tax_amount': res[i][1]['sum_period'], 'type': 1, 'level': res[i][0], 'pos': 0 } top_result.append(res_dict) - res_general = self._get_general(res[i][1].id, period_list, company_id, based_on, context=context) + res_general = self._get_general(res[i][1]['id'], period_list, company_id, based_on, context=context) ind_general = 0 while ind_general < len(res_general): res_general[ind_general]['type'] = 2 @@ -171,7 +171,7 @@ class tax_report(report_sxw.rml_parse, common_report_header): def _get_codes(self, based_on, company_id, parent=False, level=0, period_list=None, context=None): obj_tc = self.pool.get('account.tax.code') - ids = obj_tc.search(self.cr, self.uid, [('parent_id','=',parent),('company_id','=',company_id)], order='sequence', context=context) + ids = obj_tc.search(self.cr, self.uid, [('parent_id','=',parent),('company_id','=',company_id)], context=context) res = [] for code in obj_tc.browse(self.cr, self.uid, ids, {'based_on': based_on}): @@ -192,13 +192,20 @@ class tax_report(report_sxw.rml_parse, common_report_header): obj_tc = self.pool.get('account.tax.code') for account in account_list: ids = obj_tc.search(self.cr, self.uid, [('id','=', account[1].id)], context=context) - sum_tax_add = 0 + code = {} for period_ind in period_list: context2 = dict(context, period_id=period_ind, based_on=based_on) - for code in obj_tc.browse(self.cr, self.uid, ids, context=context2): - sum_tax_add = sum_tax_add + code.sum_period + record = obj_tc.browse(self.cr, self.uid, ids, context=context2)[0] + if not code: + code = { + 'id': record.id, + 'name': record.name, + 'code': record.code, + 'sequence': record.sequence, + 'sum_period': 0, + } + code['sum_period'] += record.sum_period - code.sum_period = sum_tax_add res.append((account[0], code)) return res diff --git a/addons/base_vat/base_vat.py b/addons/base_vat/base_vat.py index 27e7607effa..e734336b441 100644 --- a/addons/base_vat/base_vat.py +++ b/addons/base_vat/base_vat.py @@ -92,6 +92,10 @@ class res_partner(osv.osv): getattr(vatnumber, check_func_name, None) if not check_func: # No VAT validation available, default to check that the country code exists + if country_code.upper() == 'EU': + # Foreign companies that trade with non-enterprises in the EU + # may have a VATIN starting with "EU" instead of a country code. + return True res_country = self.pool.get('res.country') return bool(res_country.search(cr, uid, [('code', '=ilike', country_code)], context=context)) return check_func(vat_number) diff --git a/addons/crm/res_partner.py b/addons/crm/res_partner.py index c1c04064ae3..844e755f953 100644 --- a/addons/crm/res_partner.py +++ b/addons/crm/res_partner.py @@ -30,7 +30,11 @@ class res_partner(osv.osv): # the user may not have access rights for opportunities or meetings try: for partner in self.browse(cr, uid, ids, context): - opp_ids = self.pool['crm.lead'].search(cr, uid, [('partner_id', '=', partner.id), ('type', '=', 'opportunity'), ('probability', '<', '100')], context=context) + if partner.is_company: + operator = 'child_of' + else: + operator = '=' + opp_ids = self.pool['crm.lead'].search(cr, uid, [('partner_id', operator, partner.id), ('type', '=', 'opportunity'), ('probability', '<', '100')], context=context) res[partner.id] = { 'opportunity_count': len(opp_ids), 'meeting_count': len(partner.meeting_ids), diff --git a/addons/delivery/delivery.py b/addons/delivery/delivery.py index 84f092f1454..59574c7e0d9 100644 --- a/addons/delivery/delivery.py +++ b/addons/delivery/delivery.py @@ -128,6 +128,7 @@ class delivery_carrier(osv.osv): grid_id = grid_pool.search(cr, uid, [('carrier_id', '=', record.id)], context=context) if grid_id and not (record.normal_price or record.free_if_more_than): grid_pool.unlink(cr, uid, grid_id, context=context) + grid_id = None # Check that float, else 0.0 is False if not (isinstance(record.normal_price,float) or record.free_if_more_than): diff --git a/addons/hr_evaluation/hr_evaluation_view.xml b/addons/hr_evaluation/hr_evaluation_view.xml index 115f9773987..b91af7b4653 100644 --- a/addons/hr_evaluation/hr_evaluation_view.xml +++ b/addons/hr_evaluation/hr_evaluation_view.xml @@ -94,7 +94,7 @@ - + @@ -364,6 +364,7 @@ hr.evaluation.interview form + {"search_default_todo":1,"search_default_user_id":uid} diff --git a/addons/mail/mail_followers.py b/addons/mail/mail_followers.py index a8949954986..bfe605e07d0 100644 --- a/addons/mail/mail_followers.py +++ b/addons/mail/mail_followers.py @@ -18,6 +18,9 @@ # along with this program. If not, see # ############################################################################## + +import threading + from openerp.osv import osv, fields from openerp import tools, SUPERUSER_ID from openerp.tools.translate import _ @@ -217,7 +220,14 @@ class mail_notification(osv.Model): } mail_values.update(custom_values) email_ids.append(self.pool.get('mail.mail').create(cr, uid, mail_values, context=context)) - if force_send and len(chunks) < 2: # for more than 50 followers, use the queue system + # NOTE: + # 1. for more than 50 followers, use the queue system + # 2. do not send emails immediately if the registry is not loaded, + # to prevent sending email during a simple update of the database + # using the command-line. + if force_send and len(chunks) < 2 and \ + (not self.pool._init or + getattr(threading.currentThread(), 'testing', False)): self.pool.get('mail.mail').send(cr, uid, email_ids, context=context) return True diff --git a/addons/mail/mail_template.py b/addons/mail/mail_template.py index 7525fe2fd81..360239bffd2 100644 --- a/addons/mail/mail_template.py +++ b/addons/mail/mail_template.py @@ -61,7 +61,7 @@ def format_tz(pool, cr, uid, dt, tz=False, format=False, context=None): fdate = ts.strftime(format_date) ftime = ts.strftime(format_time) - return "%s %s (%s)" % (fdate, ftime, tz) + return "%s %s%s" % (fdate, ftime, (' (%s)' % tz) if tz else '') try: # We use a jinja2 sandboxed environment to render mako templates. @@ -191,7 +191,7 @@ class mail_template(osv.osv): user = self.pool.get('res.users').browse(cr, uid, uid, context=context) records = self.pool[model].browse(cr, uid, res_ids, context=context) or [None] variables = { - 'format_tz': lambda dt, tz=False, format=False: format_tz(self.pool, cr, uid, dt, tz, format, context), + 'format_tz': lambda dt, tz=False, format=False, context=context: format_tz(self.pool, cr, uid, dt, tz, format, context), 'user': user, 'ctx': context, # context kw would clash with mako internals } diff --git a/addons/mrp_repair/mrp_repair.py b/addons/mrp_repair/mrp_repair.py index 2ef40504c49..08dca1c4ca4 100644 --- a/addons/mrp_repair/mrp_repair.py +++ b/addons/mrp_repair/mrp_repair.py @@ -140,7 +140,7 @@ class mrp_repair(osv.osv): \n* The \'Cancelled\' status is used when user cancel repair order.'), 'location_id': fields.many2one('stock.location', 'Current Location', select=True, required=True, readonly=True, states={'draft': [('readonly', False)], 'confirmed': [('readonly', True)]}), 'location_dest_id': fields.many2one('stock.location', 'Delivery Location', readonly=True, required=True, states={'draft': [('readonly', False)], 'confirmed': [('readonly', True)]}), - 'lot_id': fields.many2one('stock.production.lot', 'Repaired Lot', domain="[('product_id','=', product_id)]", help="Products repaired are all belonging to this lot"), + 'lot_id': fields.many2one('stock.production.lot', 'Repaired Lot', domain="[('product_id','=', product_id)]", help="Products repaired are all belonging to this lot", oldname="prodlot_id"), 'guarantee_limit': fields.date('Warranty Expiration', help="The warranty expiration limit is computed as: last move date + warranty defined on selected product. If the current date is below the warranty expiration limit, each operation and fee you will add will be set as 'not to invoiced' by default. Note that you can change manually afterwards.", states={'confirmed': [('readonly', True)]}), 'operations': fields.one2many('mrp.repair.line', 'repair_id', 'Operation Lines', readonly=True, states={'draft': [('readonly', False)]}, copy=True), 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', help='Pricelist of the selected partner.'), diff --git a/addons/point_of_sale/point_of_sale_view.xml b/addons/point_of_sale/point_of_sale_view.xml index 9ea04a12d5b..3bf95d8c785 100644 --- a/addons/point_of_sale/point_of_sale_view.xml +++ b/addons/point_of_sale/point_of_sale_view.xml @@ -26,7 +26,7 @@ - + diff --git a/addons/point_of_sale/static/src/js/devices.js b/addons/point_of_sale/static/src/js/devices.js index 53398639e7a..d788de75fc8 100644 --- a/addons/point_of_sale/static/src/js/devices.js +++ b/addons/point_of_sale/static/src/js/devices.js @@ -373,11 +373,14 @@ openerp.point_of_sale.load_devices = function load_devices(instance,module){ //m scale_read: function(){ var self = this; var ret = new $.Deferred(); + if (self.use_debug_weight) { + return (new $.Deferred()).resolve({weight:this.debug_weight, unit:'Kg', info:'ok'}); + } this.message('scale_read',{}) .then(function(weight){ - ret.resolve(self.use_debug_weight ? self.debug_weight : weight); + ret.resolve(weight); }, function(){ //failed to read weight - ret.resolve(self.use_debug_weight ? self.debug_weight : {weight:0.0, unit:'Kg', info:'ok'}); + ret.resolve({weight:0.0, unit:'Kg', info:'ok'}); }); return ret; }, diff --git a/addons/portal_sale/portal_sale_data.xml b/addons/portal_sale/portal_sale_data.xml index cfacd6bd917..77cb4f3b2dc 100644 --- a/addons/portal_sale/portal_sale_data.xml +++ b/addons/portal_sale/portal_sale_data.xml @@ -26,7 +26,7 @@   REFERENCES
  Order number: ${object.name}
  Order total: ${object.amount_total} ${object.pricelist_id.currency_id.name}
-   Order date: ${object.date_order}
+   Order date: ${format_tz(object.date_order, tz=user.tz, context={'lang':object.partner_id.lang})}
% if object.validity_date and object.state in ('draft', 'sent'):   Expiration date: ${object.validity_date}
% endif diff --git a/addons/product/product.py b/addons/product/product.py index 2609540bb27..85aab0a52ef 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -122,7 +122,7 @@ class product_uom(osv.osv): _order = "name" _columns = { 'name': fields.char('Unit of Measure', required=True, translate=True), - 'category_id': fields.many2one('product.uom.categ', 'Product Category', required=True, ondelete='cascade', + 'category_id': fields.many2one('product.uom.categ', 'Unit of Measure Category', required=True, ondelete='cascade', help="Conversion between Units of Measure can only occur if they belong to the same category. The conversion will be made based on the ratios."), 'factor': fields.float('Ratio', required=True, digits=0, # force NUMERIC with unlimited precision help='How much bigger or smaller this unit is compared to the reference Unit of Measure for this category:\n'\ @@ -492,7 +492,7 @@ class product_template(osv.osv): def _get_product_variant_count(self, cr, uid, ids, name, arg, context=None): res = {} - for product in self.browse(cr, uid, ids): + for product in self.browse(cr, uid, ids, context=context): res[product.id] = len(product.product_variant_ids) return res diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index c6018d2a359..70b9c87121e 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -1565,6 +1565,13 @@ class product_product(osv.Model): for product_id in ids } + def action_view_purchases(self, cr, uid, ids, context=None): + if isinstance(ids, (int, long)): + ids = [ids] + result = self.pool['product.template']._get_act_window_dict(cr, uid, 'purchase.action_purchase_line_product_tree', context=context) + result['domain'] = "[('product_id','in',[" + ','.join(map(str, ids)) + "])]" + return result + _columns = { 'purchase_count': fields.function(_purchase_count, string='# Purchases', type='integer'), } diff --git a/addons/purchase/purchase_view.xml b/addons/purchase/purchase_view.xml index f0fc23cd0a5..ff5ca396609 100644 --- a/addons/purchase/purchase_view.xml +++ b/addons/purchase/purchase_view.xml @@ -623,7 +623,7 @@ - {'search_default_product_id': active_id, 'default_product_id': active_id} + {} Purchases purchase.order.line @@ -665,7 +665,7 @@ - diff --git a/addons/purchase/views/report_purchaseorder.xml b/addons/purchase/views/report_purchaseorder.xml index 99cc7222100..d1285c1d71b 100644 --- a/addons/purchase/views/report_purchaseorder.xml +++ b/addons/purchase/views/report_purchaseorder.xml @@ -11,19 +11,18 @@
-

VAT:

-

VAT:

+

VAT:

diff --git a/addons/purchase/views/report_purchasequotation.xml b/addons/purchase/views/report_purchasequotation.xml index 771e12e9283..2a386788b51 100644 --- a/addons/purchase/views/report_purchasequotation.xml +++ b/addons/purchase/views/report_purchasequotation.xml @@ -12,18 +12,17 @@
-

VAT:

-

VAT:

+

VAT:

diff --git a/addons/sale/edi/sale_order_action_data.xml b/addons/sale/edi/sale_order_action_data.xml index 66735b3198b..d5e75363f82 100644 --- a/addons/sale/edi/sale_order_action_data.xml +++ b/addons/sale/edi/sale_order_action_data.xml @@ -39,7 +39,7 @@   REFERENCES
  Order number: ${object.name}
  Order total: ${object.amount_total} ${object.pricelist_id.currency_id.name}
-   Order date: ${object.date_order}
+   Order date: ${format_tz(object.date_order, tz=user.tz, context={'lang':object.partner_id.lang})}
% if object.validity_date and object.state in ('draft', 'sent'):   Expiration date: ${object.validity_date}
% endif diff --git a/addons/sale/sale.py b/addons/sale/sale.py index d8b7bad4733..3a7703a2276 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -1239,15 +1239,23 @@ class product_product(osv.Model): _inherit = 'product.product' def _sales_count(self, cr, uid, ids, field_name, arg, context=None): - SaleOrderLine = self.pool['sale.order.line'] - return { - product_id: SaleOrderLine.search_count(cr,uid, [('product_id', '=', product_id)], context=context) - for product_id in ids - } + r = dict.fromkeys(ids, 0) + domain = [ + ('state', 'in', ['waiting_date','progress','manual', 'shipping_except', 'invoice_except', 'done']), + ('product_id', 'in', ids), + ] + for group in self.pool['sale.report'].read_group(cr, uid, domain, ['product_id','product_uom_qty'], ['product_id'], context=context): + r[group['product_id'][0]] = group['product_uom_qty'] + return r + + def action_view_sales(self, cr, uid, ids, context=None): + result = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'sale.action_order_line_product_tree', raise_if_not_found=True) + result = self.pool['ir.actions.act_window'].read(cr, uid, [result], context=context)[0] + result['domain'] = "[('product_id','in',[" + ','.join(map(str, ids)) + "])]" + return result _columns = { 'sales_count': fields.function(_sales_count, string='# Sales', type='integer'), - } class product_template(osv.Model): diff --git a/addons/sale/sale_view.xml b/addons/sale/sale_view.xml index 753c3316388..246356b42c2 100644 --- a/addons/sale/sale_view.xml +++ b/addons/sale/sale_view.xml @@ -504,7 +504,7 @@ - {'search_default_product_id': active_id, 'default_product_id': active_id} + {} Sales Order Lines sale.order.line @@ -516,7 +516,7 @@ - diff --git a/addons/sales_team/sales_team.py b/addons/sales_team/sales_team.py index 4f215448b1a..8b78b85573e 100644 --- a/addons/sales_team/sales_team.py +++ b/addons/sales_team/sales_team.py @@ -53,7 +53,7 @@ class crm_team(osv.Model): month_begin = date.today().replace(day=1) team_result = [{ 'value': 0, - 'tooltip': (month_begin + relativedelta.relativedelta(months=-i)).strftime('%B %Y'), + 'tooltip': tools.ustr((month_begin + relativedelta.relativedelta(months=-i)).strftime('%B %Y')), } for i in range(self._period_number - 1, -1, -1)] group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context) pattern = tools.DEFAULT_SERVER_DATE_FORMAT if obj.fields_get(cr, uid, groupby_field)[groupby_field]['type'] == 'date' else tools.DEFAULT_SERVER_DATETIME_FORMAT diff --git a/addons/stock_account/stock.py b/addons/stock_account/stock.py index b965f6689d7..486c94d797f 100644 --- a/addons/stock_account/stock.py +++ b/addons/stock_account/stock.py @@ -293,8 +293,8 @@ class stock_picking(osv.osv): else: invoice = invoice_obj.browse(cr, uid, invoices[key], context=context) if not invoice.origin or invoice_vals['origin'] not in invoice.origin.split(', '): - origin = filter(None, [invoice.origin, invoice_vals['origin']]) - invoice.write({'origin': ', '.join(origin)}) + invoice_origin = filter(None, [invoice.origin, invoice_vals['origin']]) + invoice.write({'origin': ', '.join(invoice_origin)}) invoice_line_vals = move_obj._get_invoice_line_vals(cr, uid, move, partner, inv_type, context=context) invoice_line_vals['invoice_id'] = invoices[key] diff --git a/addons/web/__openerp__.py b/addons/web/__openerp__.py index a4000856afe..646e80f432e 100644 --- a/addons/web/__openerp__.py +++ b/addons/web/__openerp__.py @@ -17,4 +17,5 @@ This module provides the core of the OpenERP Web Client. 'qweb' : [ "static/src/xml/*.xml", ], + 'bootstrap': True, # load translations for login screen } diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index cec9ae94837..7bea7f50111 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -1108,7 +1108,7 @@ class Binary(http.Controller): content_type = res['file_type'] else: res = Model.default_get(cr, uid, fields, context) - filecontent = base64.b64decode(res.get(field, '')) + filecontent = base64.b64decode(res.get(field) or '') if not filecontent: return request.not_found() else: @@ -1136,7 +1136,7 @@ class Binary(http.Controller): if filename_field: fields.append(filename_field) if data: - res = { field: data } + res = {field: data, filename_field: jdata.get('filename', None)} elif id: fields.append('file_type') res = Model.read([int(id)], fields, context)[0] @@ -1144,7 +1144,7 @@ class Binary(http.Controller): content_type = res['file_type'] else: res = Model.default_get(fields, context) - filecontent = base64.b64decode(res.get(field, '')) + filecontent = base64.b64decode(res.get(field) or '') if not filecontent: raise ValueError(_("No content found for field '%s' on '%s:%s'") % (field, model, id)) diff --git a/addons/web/static/src/js/formats.js b/addons/web/static/src/js/formats.js index 3dbacc98527..9c35d66c20e 100644 --- a/addons/web/static/src/js/formats.js +++ b/addons/web/static/src/js/formats.js @@ -390,4 +390,9 @@ instance.web.normalize_format_table = { }; var _normalize_format_cache = {}; +instance.web.float_is_zero = function(value, decimals){ + epsilon = Math.pow(10, -decimals); + return Math.abs(instance.web.round_precision(value, epsilon)) < epsilon; +}; + })(); diff --git a/addons/web/static/src/js/openerpframework.js b/addons/web/static/src/js/openerpframework.js index 004ec2cb93a..eba992e328f 100644 --- a/addons/web/static/src/js/openerpframework.js +++ b/addons/web/static/src/js/openerpframework.js @@ -478,6 +478,11 @@ openerp.PropertiesMixin = _.extend({}, openerp.EventDispatcherMixin, { var tmp = self.__getterSetterInternalMap[key]; if (tmp === val) return; + if (self.field && self.field.type === 'float' && tmp && val){ + var precision = self.field.digits ? self.field.digits[1] : 2; + if (openerp.web.float_is_zero(tmp - val, precision)) + return; + } changed = true; self.__getterSetterInternalMap[key] = val; if (! options.silent) diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index 19e6659296d..04eebc2c0a1 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -1233,12 +1233,13 @@ instance.web.search.SelectionField = instance.web.search.Field.extend(/** @lends var results = _(this.attrs.selection).chain() .filter(function (sel) { var value = sel[0], label = sel[1]; - if (!value) { return false; } + if (value === undefined || !label) { return false; } return label.toLowerCase().indexOf(needle.toLowerCase()) !== -1; }) .map(function (sel) { return { label: _.escape(sel[1]), + indent: true, facet: facet_from(self, sel) }; }).value(); diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index c06c50ceea4..cc3bdc25990 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -5671,14 +5671,17 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance. } else { instance.web.blockUI(); var c = instance.webclient.crashmanager; + var filename_fieldname = this.node.attrs.filename; + var filename_field = this.view.fields && this.view.fields[filename_fieldname]; this.session.get_file({ url: '/web/binary/saveas_ajax', data: {data: JSON.stringify({ model: this.view.dataset.model, id: (this.view.datarecord.id || ''), field: this.name, - filename_field: (this.node.attrs.filename || ''), + filename_field: (filename_fieldname || ''), data: instance.web.form.is_bin_size(value) ? null : value, + filename: filename_field ? filename_field.get('value') : null, context: this.view.dataset.get_context() })}, complete: instance.web.unblockUI, diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js index 4333731b93e..0e446fb323e 100644 --- a/addons/web/static/src/js/view_list.js +++ b/addons/web/static/src/js/view_list.js @@ -2230,7 +2230,7 @@ instance.web.list.Column = instance.web.Class.extend({ } if (attrs.invisible) { return ''; } - if (!row_data[this.id]) { + if (!(row_data[this.id] && row_data[this.id].value)) { return options.value_if_empty === undefined ? '' : options.value_if_empty; diff --git a/addons/web/static/src/js/view_list_editable.js b/addons/web/static/src/js/view_list_editable.js index 77559504060..82dd5433dc0 100644 --- a/addons/web/static/src/js/view_list_editable.js +++ b/addons/web/static/src/js/view_list_editable.js @@ -649,7 +649,7 @@ var form = this.editor.form; var last_field = _(form.fields_order).chain() .map(function (name) { return form.fields[name]; }) - .filter(function (field) { return field.$el.is(':visible'); }) + .filter(function (field) { return field.$el.is(':visible') && !field.get('effective_readonly'); }) .last() .value(); // tabbed from last field in form diff --git a/addons/web_calendar/static/src/js/web_calendar.js b/addons/web_calendar/static/src/js/web_calendar.js index 43d0249ce88..f78717e4e22 100644 --- a/addons/web_calendar/static/src/js/web_calendar.js +++ b/addons/web_calendar/static/src/js/web_calendar.js @@ -702,25 +702,33 @@ openerp.web_calendar = function(instance) { } if (!self.useContacts) { // If we use all peoples displayed in the current month as filter in sidebars - var filter_value; var filter_item; self.now_filter_ids = []; + var color_field = self.fields[self.color_field]; _.each(events, function (e) { - filter_value = e[self.color_field][0]; - if (!self.all_filters[e[self.color_field][0]]) { + var key,val = null; + if (self.color_field.type == "selection") { + key = e[self.color_field]; + val = _.find( self.color_field.selection, function(name){ return name[0] === key;}); + } + else { + key = e[self.color_field][0]; + val = e[self.color_field]; + } + if (!self.all_filters[key]) { filter_item = { - value: filter_value, - label: e[self.color_field][1], - color: self.get_color(filter_value), + value: key, + label: val[1], + color: self.get_color(key), avatar_model: (_.str.toBoolElse(self.avatar_filter, true) ? self.avatar_filter : false ), is_checked: true }; - self.all_filters[e[self.color_field][0]] = filter_item; + self.all_filters[key] = filter_item; } - if (! _.contains(self.now_filter_ids, filter_value)) { - self.now_filter_ids.push(filter_value); + if (! _.contains(self.now_filter_ids, key)) { + self.now_filter_ids.push(key); } }); @@ -729,7 +737,8 @@ openerp.web_calendar = function(instance) { self.sidebar.filter.set_filters(); events = $.map(events, function (e) { - if (_.contains(self.now_filter_ids,e[self.color_field][0]) && self.all_filters[e[self.color_field][0]].is_checked) { + var key = self.color_field.type == "selection" ? e[self.color_field] : e[self.color_field][0]; + if (_.contains(self.now_filter_ids, key) && self.all_filters[key].is_checked) { return e; } return null; @@ -751,10 +760,7 @@ openerp.web_calendar = function(instance) { }); } } - - } - var all_attendees = $.map(events, function (e) { return e[self.attendee_people]; }); all_attendees = _.chain(all_attendees).flatten().uniq().value(); @@ -1391,7 +1397,9 @@ openerp.web_calendar = function(instance) { }); instance.web_calendar.SidebarFilter = instance.web.Widget.extend({ events: { - 'change input:checkbox': 'filter_click' + 'change input:checkbox': 'filter_click', + 'click span.color_filter': 'select_previous', + }, init: function(parent, view) { this._super(parent); @@ -1422,10 +1430,13 @@ openerp.web_calendar = function(instance) { if (self.view.all_filters[0] && e.target.value == self.view.all_filters[0].value) { self.view.all_filters[0].is_checked = e.target.checked; } else { - self.view.all_filters[parseInt(e.target.value)].is_checked = e.target.checked; + self.view.all_filters[e.target.value].is_checked = e.target.checked; } self.view.$calendar.fullCalendar('refetchEvents'); }, + select_previous: function(e) { + $(e.target).siblings('input').trigger('click'); + } }); }; diff --git a/addons/web_calendar/static/src/xml/web_fullcalendar.xml b/addons/web_calendar/static/src/xml/web_fullcalendar.xml index acadc7434b7..2350111988a 100644 --- a/addons/web_calendar/static/src/xml/web_fullcalendar.xml +++ b/addons/web_calendar/static/src/xml/web_fullcalendar.xml @@ -27,7 +27,7 @@
- + @@ -35,7 +35,7 @@ - +
diff --git a/addons/website_blog/controllers/main.py b/addons/website_blog/controllers/main.py index 66fb70974cf..ddf899d90dd 100644 --- a/addons/website_blog/controllers/main.py +++ b/addons/website_blog/controllers/main.py @@ -213,17 +213,12 @@ class WebsiteBlog(http.Controller): tags = tag_obj.browse(cr, uid, tag_obj.search(cr, uid, [], context=context), context=context) # Find next Post - visited_blogs = request.httprequest.cookies.get('visited_blogs') or '' - visited_ids = filter(None, visited_blogs.split(',')) - visited_ids = map(lambda x: int(x), visited_ids) - if blog_post.id not in visited_ids: - visited_ids.append(blog_post.id) - next_post_id = blog_post_obj.search(cr, uid, [ - ('id', 'not in', visited_ids), - ], order='ranking desc', limit=1, context=context) - if not next_post_id: - next_post_id = blog_post_obj.search(cr, uid, [('id', '!=', blog.id)], order='ranking desc', limit=1, context=context) - next_post = next_post_id and blog_post_obj.browse(cr, uid, next_post_id[0], context=context) or False + all_post_ids = blog_post_obj.search(cr, uid, [('blog_id', '=', blog.id)], context=context) + # should always return at least the current post + current_blog_post_index = all_post_ids.index(blog_post.id) + next_post_id = all_post_ids[0 if current_blog_post_index == len(all_post_ids) - 1 \ + else current_blog_post_index + 1] + next_post = next_post_id and blog_post_obj.browse(cr, uid, next_post_id, context=context) or False values = { 'tags': tags, @@ -240,7 +235,6 @@ class WebsiteBlog(http.Controller): 'comments': comments, } response = request.website.render("website_blog.blog_post_complete", values) - response.set_cookie('visited_blogs', ','.join(map(str, visited_ids))) request.session[request.session_id] = request.session.get(request.session_id, []) if not (blog_post.id in request.session[request.session_id]): diff --git a/addons/website_blog/models/website_blog.py b/addons/website_blog/models/website_blog.py index 75b6fc991f9..bd20d311b22 100644 --- a/addons/website_blog/models/website_blog.py +++ b/addons/website_blog/models/website_blog.py @@ -138,6 +138,8 @@ class BlogPost(osv.Model): :return result: (html, mappin), where html is the updated html with ID and mapping is a list of (old_ID, new_ID), where old_ID is None is the paragraph is a new one. """ + + existing_attributes = [] mapping = [] if not html: return html, mapping @@ -145,7 +147,6 @@ class BlogPost(osv.Model): tags = ['p'] if attribute is None: attribute = 'data-unique-id' - counter = 0 # form a tree root = lxml.html.fragment_fromstring(html, create_parent='div') @@ -156,17 +157,22 @@ class BlogPost(osv.Model): # - img src -> check URL # - a href -> check URL for node in root.iter(): - if not node.tag in tags: + if node.tag not in tags: continue ancestor_tags = [parent.tag for parent in node.iterancestors()] - if ancestor_tags: - ancestor_tags.pop() - ancestor_tags.append('counter_%s' % counter) - new_attribute = '/'.join(reversed(ancestor_tags)) + old_attribute = node.get(attribute) - node.set(attribute, new_attribute) - mapping.append((old_attribute, counter)) - counter += 1 + new_attribute = old_attribute + if old_attribute in existing_attributes: + if ancestor_tags: + ancestor_tags.pop() + counter = random.randint(10000, 99999) + ancestor_tags.append('counter_%s' % counter) + new_attribute = '/'.join(reversed(ancestor_tags)) + node.set(attribute, new_attribute) + + existing_attributes.append(new_attribute) + mapping.append((old_attribute, new_attribute)) html = lxml.html.tostring(root, pretty_print=False, method='html') # this is ugly, but lxml/etree tostring want to put everything in a 'div' that breaks the editor -> remove that @@ -179,12 +185,18 @@ class BlogPost(osv.Model): content = self.browse(cr, uid, id, context=context).content if content is False: return content + content, mapping = self.html_tag_nodes(content, attribute='data-chatter-id', tags=['p'], context=context) - for old_attribute, new_attribute in mapping: - if not old_attribute: - continue - msg_ids = self.pool['mail.message'].search(cr, SUPERUSER_ID, [('path', '=', old_attribute)], context=context) - self.pool['mail.message'].write(cr, SUPERUSER_ID, msg_ids, {'path': new_attribute}, context=context) + if id: # not creating + existing = [x[0] for x in mapping if x[0]] + msg_ids = self.pool['mail.message'].search(cr, SUPERUSER_ID, [ + ('res_id', '=', id), + ('model', '=', self._name), + ('path', 'not in', existing), + ('path', '!=', False) + ], context=context) + self.pool['mail.message'].unlink(cr, SUPERUSER_ID, msg_ids, context=context) + return content if isinstance(ids, (int, long)): @@ -218,12 +230,13 @@ class BlogPost(osv.Model): def write(self, cr, uid, ids, vals, context=None): if 'content' in vals: - vals['content'] = self._postproces_content(cr, uid, None, vals['content'], context=context) + vals['content'] = self._postproces_content(cr, uid, ids[0], vals['content'], context=context) result = super(BlogPost, self).write(cr, uid, ids, vals, context) self._check_for_publication(cr, uid, ids, vals, context=context) return result + class Website(osv.Model): _inherit = "website" diff --git a/addons/website_forum/static/src/js/website_forum.js b/addons/website_forum/static/src/js/website_forum.js index c6d4240dfa0..97ff6cf2b91 100644 --- a/addons/website_forum/static/src/js/website_forum.js +++ b/addons/website_forum/static/src/js/website_forum.js @@ -92,7 +92,7 @@ $('.comment_delete').on('click', function (ev) { ev.preventDefault(); var $link = $(ev.currentTarget); - openerp.jsonRpc($link.data('href'), 'call', {}).then(function (data) { + openerp.jsonRpc($link.parent('form').attr('action'), 'call', {}).then(function (data) { $link.parents('.comment').first().remove(); }); }); diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index e4f9181e48e..858e6afad94 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -123,14 +123,17 @@ class website_sale(http.Controller): cr, uid, context, pool = request.cr, request.uid, request.context, request.registry currency_obj = pool['res.currency'] attribute_value_ids = [] + visible_attrs = set(l.attribute_id.id + for l in product.attribute_line_ids + if len(l.value_ids) > 1) if request.website.pricelist_id.id != context['pricelist']: website_currency_id = request.website.currency_id.id currency_id = self.get_pricelist().currency_id.id for p in product.product_variant_ids: price = currency_obj.compute(cr, uid, website_currency_id, currency_id, p.lst_price) - attribute_value_ids.append([p.id, [v.id for v in p.attribute_value_ids if len(v.attribute_id.value_ids) > 1], p.price, price]) + attribute_value_ids.append([p.id, [v.id for v in p.attribute_value_ids if v.attribute_id.id in visible_attrs], p.price, price]) else: - attribute_value_ids = [[p.id, [v.id for v in p.attribute_value_ids if len(v.attribute_id.value_ids) > 1], p.price, p.lst_price] + attribute_value_ids = [[p.id, [v.id for v in p.attribute_value_ids if v.attribute_id.id in visible_attrs], p.price, p.lst_price] for p in product.product_variant_ids] return attribute_value_ids @@ -837,13 +840,20 @@ class website_sale(http.Controller): # send the email if email_act and email_act.get('context'): + composer_obj = request.registry['mail.compose.message'] composer_values = {} email_ctx = email_act['context'] - public_id = request.website.user_id.id - if uid == public_id: + template_values = [ + email_ctx.get('default_template_id'), + email_ctx.get('default_composition_mode'), + email_ctx.get('default_model'), + email_ctx.get('default_res_id'), + ] + composer_values.update(composer_obj.onchange_template_id(cr, SUPERUSER_ID, None, *template_values, context=context).get('value', {})) + if not composer_values.get('email_from') and uid == request.website.user_id.id: composer_values['email_from'] = request.website.user_id.company_id.email - composer_id = request.registry['mail.compose.message'].create(cr, SUPERUSER_ID, composer_values, context=email_ctx) - request.registry['mail.compose.message'].send_mail(cr, SUPERUSER_ID, [composer_id], context=email_ctx) + composer_id = composer_obj.create(cr, SUPERUSER_ID, composer_values, context=email_ctx) + composer_obj.send_mail(cr, SUPERUSER_ID, [composer_id], context=email_ctx) # clean context and session, then redirect to the confirmation page request.website.sale_reset(context=context) diff --git a/doc/_themes/odoodoc/layout.html b/doc/_themes/odoodoc/layout.html index af62f44bbb4..b0244e191b3 100644 --- a/doc/_themes/odoodoc/layout.html +++ b/doc/_themes/odoodoc/layout.html @@ -26,7 +26,7 @@ {%- endblock -%} {%- block content -%} -
+
{{ super() }}
{%- endblock -%} @@ -34,6 +34,7 @@ {# sidebar to the left, except sidebar1 is *outside* div.document where sidebar2 is inside, so inject sidebar into document block #} {%- block document -%} + {%- if render_sidebar -%}
{{ toctree(maxdepth=4, collapse=False, includehidden=True, @@ -45,6 +46,7 @@ {% endif %}
+ {%- endif -%} {{ super() }} {%- endblock -%} {%- block sidebar1 -%}{%- endblock -%} diff --git a/doc/_themes/odoodoc/static/style.css b/doc/_themes/odoodoc/static/style.css index 7fc2d3dcb95..f7624440f81 100644 --- a/doc/_themes/odoodoc/static/style.css +++ b/doc/_themes/odoodoc/static/style.css @@ -6247,6 +6247,18 @@ body { } @media (min-width: 992px) { .documentwrapper { + float: left; + width: 100%; + } +} +.with-sidebar .documentwrapper { + position: relative; + min-height: 1px; + padding-left: 15px; + padding-right: 15px; +} +@media (min-width: 992px) { + .with-sidebar .documentwrapper { float: left; width: 75%; } @@ -6611,6 +6623,9 @@ div.section > h2 { padding-top: 20px; margin-top: 0; } +.body { + position: relative; +} .body > .section > .section { margin-bottom: 60px; } diff --git a/doc/_themes/odoodoc/static/style.less b/doc/_themes/odoodoc/static/style.less index 480e43c5cb8..763fe6027d3 100644 --- a/doc/_themes/odoodoc/static/style.less +++ b/doc/_themes/odoodoc/static/style.less @@ -37,6 +37,9 @@ body { } .documentwrapper { + .make-md-column(12); +} +.with-sidebar .documentwrapper { .make-md-column(9); } .sphinxsidebar { @@ -403,8 +406,12 @@ div.section > h2 { margin-top: 0; } // ~docs-section -.body > .section > .section { - margin-bottom: 60px; +.body { + // otherwise main section right column content may not be visible + position: relative; + > .section > .section { + margin-bottom: 60px; + } } .literal:extend(code) {} diff --git a/doc/howtos/website.rst b/doc/howtos/website.rst index 6ec9fddc76c..c3f575ef25d 100644 --- a/doc/howtos/website.rst +++ b/doc/howtos/website.rst @@ -19,29 +19,19 @@ Modules customize the behavior of an Odoo installation, either by adding new behaviors or by altering existing ones (including behaviors added by other modules). -First let's create a *module directory* which will contain a single module in -our case but may store multiple related (a project's) or not really related -(a company's) modules: +:ref:`Odoo's scaffolding ` can setup a basic +module to quickly get started, simply invoke: .. code-block:: console - $ mkdir my-modules + $ ./odoo.py scaffold Academy my-modules -then let's create the module's own directory: - -.. code-block:: console - - $ mkdir my-modules/academy - -An Odoo module is a valid `Python package -`_ so it needs an -empty ``__init__.py`` file. - -Finally the mark of an Odoo module is the -:ref:`manifest file `, a Python dictionary describing -various module metadata. +This will automatically create a ``my-modules`` *module directory* with an +``academy`` module inside. The directory can be an existing module directory +if you want, but the module name must be unique for the directory. .. patch:: + :hidden: A demonstration module ====================== @@ -52,30 +42,25 @@ Although it does absolutely nothing yet we can install it: * start the Odoo server - .. code-block:: console + .. code-block:: console - $ ./odoo.py --addons-path addons,my-modules + $ ./odoo.py --addons-path addons,my-modules * go to http://localhost:8069 * create a new database including demonstration data -* to go :menuselection:`Settings --> Modules --> Installed Modules` +* to go :menuselection:`Settings --> Modules --> Local Modules` * in the top-right corner remove the *Installed* filter and search for *academy* * click the :guilabel:`Install` button for the *Academy* module -.. seealso:: - - * In a production development setting, modules should generally be created - using :ref:`Odoo's scaffolding ` rather than by - hand - To the browser ============== :ref:`Controllers ` interpret browser requests and send data back. -Add a simple controller and import it (so Odoo can find it): +Add a simple controller and ensure it is imported by ``__init__.py`` (so +Odoo can find it): .. patch:: @@ -85,8 +70,8 @@ Shut down your server (:kbd:`^C`) then restart it: $ ./odoo.py --addons-path addons,my-modules -and open a page to http://localhost:8069/academy/, you should see your "page" -appear: +and open a page to http://localhost:8069/academy/academy/, you should see your +"page" appear: .. figure:: website/helloworld.png @@ -100,8 +85,8 @@ display logic. Odoo allows any Python templating system, but provides its own :ref:`QWeb ` templating system which integrates with other Odoo features. -Let's create an XML file for our first template, register the template in the -manifest and alter the controller to use our template: +Create a template and ensure the template file is registered in the +``__openerp__.py`` manifest, and alter the controller to use our template: .. patch:: @@ -109,7 +94,7 @@ The templates iterates (``t-foreach``) on all the teachers (passed through the *template context*), and prints each teacher in its own paragraph. Finally restart Odoo and update the module's data (to install the template) -by going to :menuselection:`Settings --> Modules --> Installed Modules --> +by going to :menuselection:`Settings --> Modules --> Local Modules --> Academy` and clicking :guilabel:`Upgrade`. .. tip:: @@ -121,7 +106,7 @@ Academy` and clicking :guilabel:`Upgrade`. $ odoo.py --addons-path addons,my-modules -d academy -u academy -Going to http://localhost:8069/academy/ should now result in: +Going to http://localhost:8069/academy/academy/ should now result in: .. image:: website/basic-list.png @@ -137,7 +122,8 @@ thereof, so we're now going to move our data to the database. Defining the data model ----------------------- -First define an Odoo model file and import it: +Define a teacher model, and ensure it is imported from ``__init__.py`` so it +is correctly loaded: .. patch:: @@ -165,7 +151,7 @@ Demonstration data The second step is to add some demonstration data to the system so it's possible to test it easily. This is done by adding a ``demo`` -:ref:`data file ` to the manifest: +:ref:`data file `, which must be linked from the manifest: .. patch:: @@ -194,8 +180,8 @@ The last step is to alter model and template to use our demonstration data: Restart the server and update the module (in order to update the manifest and templates and load the demo file) then navigate to -http://localhost:8069/academy/. The page should look little different: names -should simply be prefixed by a number (the database identifier for the +http://localhost:8069/academy/academy/. The page should look little different: +names should simply be prefixed by a number (the database identifier for the teacher). Website support @@ -216,9 +202,9 @@ integration and a few other services (e.g. default styling, theming) via the .. patch:: After restarting the server while updating the module (in order to update the -manifest and template) access http://localhost:8069/academy/ should yield a -nicer looking page with branding and a number of built-in page elements -(top-level menu, footer, …) +manifest and template) access http://localhost:8069/academy/academy/ should +yield a nicer looking page with branding and a number of built-in page +elements (top-level menu, footer, …) .. image:: website/layout.png @@ -396,7 +382,7 @@ let's also add views so we can see and edit a course's teacher: It should also be possible to create new courses directly from a teacher's page, or to see all the courses a teacher gives, so add -:class:`the inverse relationship ` to the *teachers* model: .. patch:: diff --git a/doc/howtos/website/basic-acl b/doc/howtos/website/basic-acl index f07581f4d22..fa2483909e2 100644 --- a/doc/howtos/website/basic-acl +++ b/doc/howtos/website/basic-acl @@ -1,19 +1,23 @@ # HG changeset patch # Parent 91c0cc5b319e7bf240d359817b8bd044769a4f5c -diff -r 91c0cc5b319e -r c77aa2c3341b academy/__openerp__.py ---- a/academy/__openerp__.py Mon Aug 11 16:49:10 2014 +0200 -+++ b/academy/__openerp__.py Mon Aug 11 16:50:01 2014 +0200 -@@ -8,6 +8,7 @@ - 'depends': ['base'], - # data files which are always installed +# Parent 0a9ec16d98785205f25868bc23485569a1444cf3 + +diff --git a/academy/__openerp__.py b/academy/__openerp__.py +--- a/academy/__openerp__.py ++++ b/academy/__openerp__.py +@@ -24,7 +24,7 @@ + + # always loaded 'data': [ -+ 'ir.model.access.csv', +- # 'security/ir.model.access.csv', ++ 'security/ir.model.access.csv', 'templates.xml', ], - } -diff -r 91c0cc5b319e -r c77aa2c3341b academy/ir.model.access.csv ---- /dev/null Thu Jan 01 00:00:00 1970 +0000 -+++ b/academy/ir.model.access.csv Mon Aug 11 16:50:01 2014 +0200 -@@ -0,0 +1,2 @@ -+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink + # only loaded in demonstration mode +diff --git a/academy/security/ir.model.access.csv b/academy/security/ir.model.access.csv +--- a/academy/security/ir.model.access.csv ++++ b/academy/security/ir.model.access.csv +@@ -1,2 +1,2 @@ + id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +-access_academy_academy,academy.academy,model_academy_academy,,1,0,0,0 +access_academy_teachers,access_academy_teachers,model_academy_teachers,,1,0,0,0 diff --git a/doc/howtos/website/basic-controller b/doc/howtos/website/basic-controller index 96f67b46508..7b29c58f013 100644 --- a/doc/howtos/website/basic-controller +++ b/doc/howtos/website/basic-controller @@ -1,19 +1,22 @@ # HG changeset patch # Parent 78b4476f35cbef86ee4f858daf843e3b932fb9fa -diff -r 78b4476f35cb -r d5ad2ad84a5d academy/__init__.py ---- a/academy/__init__.py Mon Aug 11 16:09:21 2014 +0200 -+++ b/academy/__init__.py Mon Aug 11 16:12:38 2014 +0200 -@@ -0,0 +1,1 @@ -+from . import controllers -diff -r 78b4476f35cb -r d5ad2ad84a5d academy/controllers.py ---- /dev/null Thu Jan 01 00:00:00 1970 +0000 -+++ b/academy/controllers.py Mon Aug 11 16:12:38 2014 +0200 -@@ -0,0 +1,8 @@ -+# -*- coding: utf-8 -*- -+from openerp import http -+ +# Parent 0a4f5e3206a0738201a40d2d1a88f41fdbbc98bf + +diff --git a/academy/controllers.py b/academy/controllers.py +--- a/academy/controllers.py ++++ b/academy/controllers.py +@@ -1,10 +1,10 @@ + # -*- coding: utf-8 -*- + from openerp import http + +-# class Academy(http.Controller): +-# @http.route('/academy/academy/', auth='public') +-# def index(self, **kw): +-# return "Hello, world" +class Academy(http.Controller): -+ @http.route('/academy/', auth='public') -+ def index(self): -+ return "Hello, world!" -+ ++ @http.route('/academy/academy/', auth='public') ++ def index(self, **kw): ++ return "Hello, world" + + # @http.route('/academy/academy/objects/', auth='public') + # def list(self, **kw): diff --git a/doc/howtos/website/basic-model b/doc/howtos/website/basic-model index 2e2f7677bbe..4086c82b13d 100644 --- a/doc/howtos/website/basic-model +++ b/doc/howtos/website/basic-model @@ -1,19 +1,18 @@ # HG changeset patch # Parent 3e800beb7db0f9f05bfd4db275c86e8903d4f127 -diff -r 3e800beb7db0 -r 91c0cc5b319e academy/__init__.py ---- a/academy/__init__.py Mon Aug 11 16:36:15 2014 +0200 -+++ b/academy/__init__.py Mon Aug 11 16:49:10 2014 +0200 -@@ -1,1 +1,2 @@ - from . import controllers -+from . import models -diff -r 3e800beb7db0 -r 91c0cc5b319e academy/models.py ---- /dev/null Thu Jan 01 00:00:00 1970 +0000 -+++ b/academy/models.py Mon Aug 11 16:49:10 2014 +0200 -@@ -0,0 +1,7 @@ -+from openerp import fields -+from openerp import models -+ +# Parent 4082801768b28c2871920e41d23b7879763e763e + +diff --git a/academy/models.py b/academy/models.py +--- a/academy/models.py ++++ b/academy/models.py +@@ -2,7 +2,7 @@ + + from openerp import models, fields, api + +-# class academy(models.Model): +-# _name = 'academy.academy' +class Teachers(models.Model): + _name = 'academy.teachers' -+ + +-# name = fields.Char() + name = fields.Char() diff --git a/doc/howtos/website/biography-esc b/doc/howtos/website/biography-esc index 2cb00789537..345623aa903 100644 --- a/doc/howtos/website/biography-esc +++ b/doc/howtos/website/biography-esc @@ -1,21 +1,23 @@ # HG changeset patch # Parent 4814709fe1c52515f5108623c2c8f0bce57ffac0 -diff -r 4814709fe1c5 -r 216904cc7636 academy/models.py ---- a/academy/models.py Tue Aug 12 17:18:59 2014 +0200 -+++ b/academy/models.py Tue Aug 12 17:24:44 2014 +0200 -@@ -5,3 +5,4 @@ class Teachers(models.Model): +# Parent 926b3fdd1bba025d9543bb85ac40159b392ba13c + +diff --git a/academy/models.py b/academy/models.py +--- a/academy/models.py ++++ b/academy/models.py +@@ -6,3 +6,4 @@ class Teachers(models.Model): _name = 'academy.teachers' name = fields.Char() + biography = fields.Html() -diff -r 4814709fe1c5 -r 216904cc7636 academy/templates.xml ---- a/academy/templates.xml Tue Aug 12 17:18:59 2014 +0200 -+++ b/academy/templates.xml Tue Aug 12 17:24:44 2014 +0200 +diff --git a/academy/templates.xml b/academy/templates.xml +--- a/academy/templates.xml ++++ b/academy/templates.xml @@ -21,6 +21,7 @@ -
-
-

-+
-
-
-
+
+
+

++
+
+
+
diff --git a/doc/howtos/website/biography-field b/doc/howtos/website/biography-field index c8905d09307..0fb14a2e565 100644 --- a/doc/howtos/website/biography-field +++ b/doc/howtos/website/biography-field @@ -1,16 +1,18 @@ # HG changeset patch # Parent 216904cc7636679adbf1a5a211070e739c59c3a8 -diff -r 216904cc7636 -r d5b8a2de3e35 academy/templates.xml ---- a/academy/templates.xml Tue Aug 12 17:24:44 2014 +0200 -+++ b/academy/templates.xml Tue Aug 12 17:25:15 2014 +0200 +# Parent edeaf1ebff99c9b3d7cbe1925ca3d2ece519ba52 + +diff --git a/academy/templates.xml b/academy/templates.xml +--- a/academy/templates.xml ++++ b/academy/templates.xml @@ -20,8 +20,8 @@ -
-
-
--

--
-+

-+
-
-

-
+
+
+
+-

+-
++

++
+
+

+
diff --git a/doc/howtos/website/chatter-backend b/doc/howtos/website/chatter-backend index 31f4a7f2305..8598be05bae 100644 --- a/doc/howtos/website/chatter-backend +++ b/doc/howtos/website/chatter-backend @@ -1,9 +1,11 @@ # HG changeset patch # Parent 957395d27f63dfdb2ea0dacdbe72bc35e724ddcc -diff -r 957395d27f63 -r 6106208621f0 academy/models.py ---- a/academy/models.py Wed Aug 13 15:06:30 2014 +0200 -+++ b/academy/models.py Wed Aug 13 16:32:26 2014 +0200 -@@ -11,6 +11,7 @@ class Teachers(models.Model): +# Parent 7156498d3cffb6128e73da2e351f2b5af5f7be63 + +diff --git a/academy/models.py b/academy/models.py +--- a/academy/models.py ++++ b/academy/models.py +@@ -12,6 +12,7 @@ class Teachers(models.Model): class Courses(models.Model): _name = 'academy.courses' @@ -11,9 +13,9 @@ diff -r 957395d27f63 -r 6106208621f0 academy/models.py name = fields.Char() teacher_id = fields.Many2one('academy.teachers', string="Teacher") -diff -r 957395d27f63 -r 6106208621f0 academy/views.xml ---- a/academy/views.xml Wed Aug 13 15:06:30 2014 +0200 -+++ b/academy/views.xml Wed Aug 13 16:32:26 2014 +0200 +diff --git a/academy/views.xml b/academy/views.xml +--- a/academy/views.xml ++++ b/academy/views.xml @@ -60,6 +60,10 @@
- - -+ -+ - + + # @http.route('/academy/academy/objects/', auth='public') +diff --git a/academy/templates.xml b/academy/templates.xml +--- a/academy/templates.xml ++++ b/academy/templates.xml +@@ -12,6 +12,18 @@ +
+ + ++ + + + diff --git a/doc/howtos/website/course-m2o b/doc/howtos/website/course-m2o index de624eb5452..8daabaedb7d 100644 --- a/doc/howtos/website/course-m2o +++ b/doc/howtos/website/course-m2o @@ -1,17 +1,11 @@ # HG changeset patch # Parent c3705f93fcea86a279f2c31204cc0c2914e784d8 +# Parent 8f2d190d879cdf6464bd61f7994b8b582befcafd -diff --git a/academy/ir.model.access.csv b/academy/ir.model.access.csv ---- a/academy/ir.model.access.csv -+++ b/academy/ir.model.access.csv -@@ -1,2 +1,3 @@ - id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink - access_academy_teachers,access_academy_teachers,model_academy_teachers,,1,0,0,0 -+access_academy_courses,access_academy_courses,model_academy_courses,,1,0,0,0 diff --git a/academy/models.py b/academy/models.py --- a/academy/models.py +++ b/academy/models.py -@@ -6,3 +6,9 @@ class Teachers(models.Model): +@@ -7,3 +7,9 @@ class Teachers(models.Model): name = fields.Char() biography = fields.Html() @@ -21,3 +15,11 @@ diff --git a/academy/models.py b/academy/models.py + + name = fields.Char() + teacher_id = fields.Many2one('academy.teachers', string="Teacher") +diff --git a/academy/security/ir.model.access.csv b/academy/security/ir.model.access.csv +--- a/academy/security/ir.model.access.csv ++++ b/academy/security/ir.model.access.csv +@@ -1,2 +1,3 @@ + id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink + access_academy_teachers,access_academy_teachers,model_academy_teachers,,1,0,0,0 ++access_academy_courses,access_academy_courses,model_academy_courses,,1,0,0,0 +\ No newline at end of file diff --git a/doc/howtos/website/course-no-price b/doc/howtos/website/course-no-price index c09a355040f..5a67a9ae892 100644 --- a/doc/howtos/website/course-no-price +++ b/doc/howtos/website/course-no-price @@ -1,16 +1,21 @@ # HG changeset patch # Parent 1708c59c9731207e06ca84529c839c571e622481 -diff -r 1708c59c9731 -r c706c2ec2832 academy/templates.xml ---- a/academy/templates.xml Thu Aug 14 14:53:52 2014 +0200 -+++ b/academy/templates.xml Thu Aug 14 16:08:10 2014 +0200 -@@ -28,4 +28,10 @@ -
- - +# Parent ac472417df8770ce9b6922968aa86779a86be35c + +diff --git a/academy/templates.xml b/academy/templates.xml +--- a/academy/templates.xml ++++ b/academy/templates.xml +@@ -28,6 +28,13 @@ +
+ + + -+ - ++ ++ + + + diff --git a/doc/howtos/website/course-product b/doc/howtos/website/course-product index 5b45a3f5009..ce433605baf 100644 --- a/doc/howtos/website/course-product +++ b/doc/howtos/website/course-product @@ -1,16 +1,17 @@ # HG changeset patch # Parent 2ee7212e5de4702dff08d9b5e4268e7dc261d038 +# Parent 4ca7b848bb60c3573de7ed41a28b4b65186a5725 diff --git a/academy/__openerp__.py b/academy/__openerp__.py --- a/academy/__openerp__.py +++ b/academy/__openerp__.py -@@ -11,6 +11,7 @@ - 'ir.model.access.csv', +@@ -27,6 +27,7 @@ + 'security/ir.model.access.csv', 'templates.xml', 'views.xml', + 'data.xml', ], - # data files which are only installed in "demonstration mode" + # only loaded in demonstration mode 'demo': [ diff --git a/academy/data.xml b/academy/data.xml new file mode 100644 @@ -26,47 +27,41 @@ new file mode 100644 diff --git a/academy/demo.xml b/academy/demo.xml --- a/academy/demo.xml +++ b/academy/demo.xml -@@ -8,4 +8,29 @@ - - Lester Vaughn - +@@ -9,5 +9,30 @@ + + Lester Vaughn + + -+ -+ Course 0 -+ -+ -+ True -+ 0 -+ service -+ -+ -+ Course 1 -+ -+ -+ True -+ 0 -+ service -+ -+ -+ Course 2 -+ -+ -+ True -+ 0 -+ service -+ - -diff --git a/academy/ir.model.access.csv b/academy/ir.model.access.csv ---- a/academy/ir.model.access.csv -+++ b/academy/ir.model.access.csv -@@ -1,3 +1,2 @@ - id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink - access_academy_teachers,access_academy_teachers,model_academy_teachers,,1,0,0,0 --access_academy_courses,access_academy_courses,model_academy_courses,,1,0,0,0 ++ ++ Course 0 ++ ++ ++ True ++ 0 ++ service ++ ++ ++ Course 1 ++ ++ ++ True ++ 0 ++ service ++ ++ ++ Course 2 ++ ++ ++ True ++ 0 ++ service ++ + + diff --git a/academy/models.py b/academy/models.py --- a/academy/models.py +++ b/academy/models.py -@@ -7,11 +7,9 @@ class Teachers(models.Model): +@@ -8,11 +8,9 @@ class Teachers(models.Model): name = fields.Char() biography = fields.Html() @@ -80,6 +75,16 @@ diff --git a/academy/models.py b/academy/models.py - name = fields.Char() teacher_id = fields.Many2one('academy.teachers', string="Teacher") +diff --git a/academy/security/ir.model.access.csv b/academy/security/ir.model.access.csv +--- a/academy/security/ir.model.access.csv ++++ b/academy/security/ir.model.access.csv +@@ -1,3 +1,2 @@ + id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +-access_academy_teachers,access_academy_teachers,model_academy_teachers,,1,0,0,0 +-access_academy_courses,access_academy_courses,model_academy_courses,,1,0,0,0 +\ No newline at end of file ++access_academy_teachers,access_academy_teachers,model_academy_teachers,,1,0,0,0 +\ No newline at end of file diff --git a/academy/views.xml b/academy/views.xml --- a/academy/views.xml +++ b/academy/views.xml diff --git a/doc/howtos/website/course-products-sidebar b/doc/howtos/website/course-products-sidebar index 09045a5ab0b..03de4bdb1de 100644 --- a/doc/howtos/website/course-products-sidebar +++ b/doc/howtos/website/course-products-sidebar @@ -1,14 +1,18 @@ # HG changeset patch # Parent c706c2ec28328a65a8aee6a7ba7d06896468c1e3 -diff -r c706c2ec2832 -r f85b481451bb academy/templates.xml ---- a/academy/templates.xml Thu Aug 14 16:08:10 2014 +0200 -+++ b/academy/templates.xml Thu Aug 14 16:30:49 2014 +0200 -@@ -34,4 +34,8 @@ - product.price > 0 - - +# Parent 053262442acdbe3c803c29ce35002ec69452c5fd + +diff --git a/academy/templates.xml b/academy/templates.xml +--- a/academy/templates.xml ++++ b/academy/templates.xml +@@ -35,6 +35,10 @@ + + + ++ ++ ++ + -+ -+ -+ - + + + diff --git a/doc/howtos/website/data-retrieval-orm b/doc/howtos/website/data-retrieval-orm index 0f34f1aa0d0..514777beb14 100644 --- a/doc/howtos/website/data-retrieval-orm +++ b/doc/howtos/website/data-retrieval-orm @@ -1,29 +1,30 @@ # HG changeset patch # Parent f0a3629abb17ca9f4dddd8885321d9ca5de14b55 -diff -r f0a3629abb17 -r 7ae0db4c2ddf academy/controllers.py ---- a/academy/controllers.py Tue Aug 12 11:21:05 2014 +0200 -+++ b/academy/controllers.py Tue Aug 12 11:24:14 2014 +0200 -@@ -4,7 +4,7 @@ from openerp import http +# Parent 4c1c3d77ba8ad35dc759405a0db39a6eae943b0f + +diff --git a/academy/controllers.py b/academy/controllers.py +--- a/academy/controllers.py ++++ b/academy/controllers.py +@@ -4,8 +4,9 @@ from openerp import http class Academy(http.Controller): - @http.route('/academy/', auth='public') - def index(self): + @http.route('/academy/academy/', auth='public') + def index(self, **kw): + Teachers = http.request.env['academy.teachers'] return http.request.render('academy.index', { - 'teachers': ["Diana Padilla", "Jody Caroll", "Lester Vaughn"], -+ 'teachers': Teachers.search([]), ++ 'teachers': Teachers.search([]) }) -- -diff -r f0a3629abb17 -r 7ae0db4c2ddf academy/templates.xml ---- a/academy/templates.xml Tue Aug 12 11:21:05 2014 +0200 -+++ b/academy/templates.xml Tue Aug 12 11:24:14 2014 +0200 -@@ -2,7 +2,7 @@ - -- -\ No newline at end of file -+ + + # @http.route('/academy/academy/objects/', auth='public') +diff --git a/academy/templates.xml b/academy/templates.xml +--- a/academy/templates.xml ++++ b/academy/templates.xml +@@ -3,7 +3,7 @@ + + diff --git a/doc/howtos/website/demo-data b/doc/howtos/website/demo-data index e7694add228..c8fb7d849ad 100644 --- a/doc/howtos/website/demo-data +++ b/doc/howtos/website/demo-data @@ -1,29 +1,42 @@ # HG changeset patch # Parent c77aa2c3341b9ab3a4e9ed05874e8a278fe69453 -diff -r c77aa2c3341b -r f0a3629abb17 academy/__openerp__.py ---- a/academy/__openerp__.py Mon Aug 11 16:50:01 2014 +0200 -+++ b/academy/__openerp__.py Tue Aug 12 11:21:05 2014 +0200 -@@ -11,4 +11,8 @@ - 'ir.model.access.csv', - 'templates.xml', - ], -+ # data files which are only installed in "demonstration mode" -+ 'demo': [ -+ 'demo.xml', -+ ], - } -diff -r c77aa2c3341b -r f0a3629abb17 academy/demo.xml ---- /dev/null Thu Jan 01 00:00:00 1970 +0000 -+++ b/academy/demo.xml Tue Aug 12 11:21:05 2014 +0200 -@@ -0,0 +1,11 @@ -+ -+ -+ Diana Padilla -+ -+ -+ Jody Carroll -+ -+ -+ Lester Vaughn -+ -+ +# Parent f416ab6c60f9cb3a803d30ff4dbff880eaa44a65 + +diff --git a/academy/demo.xml b/academy/demo.xml +--- a/academy/demo.xml ++++ b/academy/demo.xml +@@ -1,25 +1,13 @@ + + +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- ++ ++ Diana Padilla ++ ++ ++ Jody Carroll ++ ++ ++ Lester Vaughn ++ + + diff --git a/doc/howtos/website/field-format b/doc/howtos/website/field-format index 77b68adadb3..2fbaa541f8c 100644 --- a/doc/howtos/website/field-format +++ b/doc/howtos/website/field-format @@ -1,14 +1,16 @@ # HG changeset patch # Parent f3ad3f919b49bc70c87e1500ec0d39f741243b3d -diff -r f3ad3f919b49 -r c3d04f578b76 academy/templates.xml ---- a/academy/templates.xml Tue Aug 12 17:30:53 2014 +0200 -+++ b/academy/templates.xml Tue Aug 12 17:34:31 2014 +0200 +# Parent 07a68419287382d7dd9d600c8b813c3c37a88864 + +diff --git a/academy/templates.xml b/academy/templates.xml +--- a/academy/templates.xml ++++ b/academy/templates.xml @@ -21,7 +21,7 @@ -
-
-

--

Last modified:

-+

Last modified:

-
-
-

+
+
+

+-

Last modified:

++

Last modified:

+
+
+

diff --git a/doc/howtos/website/field-raw b/doc/howtos/website/field-raw index c09378c4383..e1b4ef76abb 100644 --- a/doc/howtos/website/field-raw +++ b/doc/howtos/website/field-raw @@ -1,13 +1,15 @@ # HG changeset patch # Parent d5b8a2de3e35c26c29f85ce500d41ccd2373c508 -diff -r d5b8a2de3e35 -r f3ad3f919b49 academy/templates.xml ---- a/academy/templates.xml Tue Aug 12 17:25:15 2014 +0200 -+++ b/academy/templates.xml Tue Aug 12 17:30:53 2014 +0200 +# Parent 65d27383542ef9214ff9e2c308aa54b042cff2d9 + +diff --git a/academy/templates.xml b/academy/templates.xml +--- a/academy/templates.xml ++++ b/academy/templates.xml @@ -21,6 +21,7 @@ -
-
-

-+

Last modified:

-
-
-

+
+
+

++

Last modified:

+
+
+

diff --git a/doc/howtos/website/field-widget b/doc/howtos/website/field-widget index 7be20b5f343..667cb8a65f1 100644 --- a/doc/howtos/website/field-widget +++ b/doc/howtos/website/field-widget @@ -1,14 +1,16 @@ # HG changeset patch # Parent c3d04f578b762612422e09777e0ac5b3863d6ea6 -diff -r c3d04f578b76 -r c8cc9e66a270 academy/templates.xml ---- a/academy/templates.xml Tue Aug 12 17:34:31 2014 +0200 -+++ b/academy/templates.xml Wed Aug 13 10:11:49 2014 +0200 +# Parent 0bc39fe6579c4e48b61e77bc970da2e1784f1a67 + +diff --git a/academy/templates.xml b/academy/templates.xml +--- a/academy/templates.xml ++++ b/academy/templates.xml @@ -21,7 +21,7 @@ -
-
-

--

Last modified:

-+

Last modified:

-
-
-

+
+
+

+-

Last modified:

++

Last modified:

+
+
+

diff --git a/doc/howtos/website/module-empty b/doc/howtos/website/module-empty index c1f9032f57c..0bbb26dd5b6 100644 --- a/doc/howtos/website/module-empty +++ b/doc/howtos/website/module-empty @@ -1,19 +1,153 @@ # HG changeset patch # Parent 0000000000000000000000000000000000000000 +# Parent 0000000000000000000000000000000000000000 diff --git a/academy/__init__.py b/academy/__init__.py new file mode 100644 +--- /dev/null ++++ b/academy/__init__.py +@@ -0,0 +1,3 @@ ++# -*- coding: utf-8 -*- ++import controllers ++import models diff --git a/academy/__openerp__.py b/academy/__openerp__.py new file mode 100644 --- /dev/null +++ b/academy/__openerp__.py -@@ -0,0 +1,9 @@ +@@ -0,0 +1,34 @@ ++# -*- coding: utf-8 -*- +{ -+ # The human-readable name of your module, displayed in the interface + 'name': "Academy", -+ # A more extensive description ++ ++ 'summary': """ ++ Short (1 phrase/line) summary of the module's purpose, used as ++ subtitle on modules listing or apps.openerp.com""", ++ + 'description': """ ++ Long description of module's purpose + """, -+ # Which modules must be installed for this one to work ++ ++ 'author': "Your Company", ++ 'website': "http://www.yourcompany.com", ++ ++ # Categories can be used to filter modules in modules listing ++ # Check https://github.com/odoo/odoo/blob/master/openerp/addons/base/module/module_data.xml ++ # for the full list ++ 'category': 'Uncategorized', ++ 'version': '0.1', ++ ++ # any module necessary for this one to work correctly + 'depends': ['base'], ++ ++ # always loaded ++ 'data': [ ++ # 'security/ir.model.access.csv', ++ 'templates.xml', ++ ], ++ # only loaded in demonstration mode ++ 'demo': [ ++ 'demo.xml', ++ ], +} +diff --git a/academy/controllers.py b/academy/controllers.py +new file mode 100644 +--- /dev/null ++++ b/academy/controllers.py +@@ -0,0 +1,20 @@ ++# -*- coding: utf-8 -*- ++from openerp import http ++ ++# class Academy(http.Controller): ++# @http.route('/academy/academy/', auth='public') ++# def index(self, **kw): ++# return "Hello, world" ++ ++# @http.route('/academy/academy/objects/', auth='public') ++# def list(self, **kw): ++# return http.request.render('academy.listing', { ++# 'root': '/academy/academy', ++# 'objects': http.request.env['academy.academy'].search([]), ++# }) ++ ++# @http.route('/academy/academy/objects//', auth='public') ++# def object(self, obj, **kw): ++# return http.request.render('academy.object', { ++# 'object': obj ++# }) +diff --git a/academy/demo.xml b/academy/demo.xml +new file mode 100644 +--- /dev/null ++++ b/academy/demo.xml +@@ -0,0 +1,25 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +diff --git a/academy/models.py b/academy/models.py +new file mode 100644 +--- /dev/null ++++ b/academy/models.py +@@ -0,0 +1,8 @@ ++# -*- coding: utf-8 -*- ++ ++from openerp import models, fields, api ++ ++# class academy(models.Model): ++# _name = 'academy.academy' ++ ++# name = fields.Char() +diff --git a/academy/security/ir.model.access.csv b/academy/security/ir.model.access.csv +new file mode 100644 +--- /dev/null ++++ b/academy/security/ir.model.access.csv +@@ -0,0 +1,2 @@ ++id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink ++access_academy_academy,academy.academy,model_academy_academy,,1,0,0,0 +diff --git a/academy/templates.xml b/academy/templates.xml +new file mode 100644 +--- /dev/null ++++ b/academy/templates.xml +@@ -0,0 +1,22 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ diff --git a/doc/howtos/website/product-dependency b/doc/howtos/website/product-dependency index 700c57af721..1998b876984 100644 --- a/doc/howtos/website/product-dependency +++ b/doc/howtos/website/product-dependency @@ -1,14 +1,16 @@ # HG changeset patch # Parent 6106208621f01d5aea67727f09adf5c5b2a38642 -diff -r 6106208621f0 -r f882c3b2e7af academy/__openerp__.py ---- a/academy/__openerp__.py Wed Aug 13 16:32:26 2014 +0200 -+++ b/academy/__openerp__.py Thu Aug 14 11:44:51 2014 +0200 -@@ -5,7 +5,7 @@ - 'description': """ - """, - # Which modules must be installed for this one to work +# Parent ae97cfb61fd0e1a47727a9d8ed4a702116254eff + +diff --git a/academy/__openerp__.py b/academy/__openerp__.py +--- a/academy/__openerp__.py ++++ b/academy/__openerp__.py +@@ -20,7 +20,7 @@ + 'version': '0.1', + + # any module necessary for this one to work correctly - 'depends': ['website'], + 'depends': ['website_sale'], - # data files which are always installed + + # always loaded 'data': [ - 'ir.model.access.csv', diff --git a/doc/howtos/website/routing-basic b/doc/howtos/website/routing-basic index 15c19c632a5..933b6540b27 100644 --- a/doc/howtos/website/routing-basic +++ b/doc/howtos/website/routing-basic @@ -1,14 +1,19 @@ # HG changeset patch # Parent 0795247fa6cbb63238941f3dd26a8b6144fa71ee -diff -r 0795247fa6cb -r e9d9740fa185 academy/controllers.py ---- a/academy/controllers.py Tue Aug 12 11:27:14 2014 +0200 -+++ b/academy/controllers.py Tue Aug 12 16:51:29 2014 +0200 -@@ -8,3 +8,8 @@ class Academy(http.Controller): - return http.request.render('academy.index', { - 'teachers': Teachers.search([]), +# Parent f66ff336f8ed20e022660bd74a43af1b44a1f275 + +diff --git a/academy/controllers.py b/academy/controllers.py +--- a/academy/controllers.py ++++ b/academy/controllers.py +@@ -9,6 +9,11 @@ class Academy(http.Controller): + 'teachers': Teachers.search([]) }) -+ + + @http.route('/academy//', auth='public', website=True) + def teacher(self, name): + return '

{}

'.format(name) + ++ + # @http.route('/academy/academy/objects/', auth='public') + # def list(self, **kw): + # return http.request.render('academy.listing', { diff --git a/doc/howtos/website/routing-converter b/doc/howtos/website/routing-converter index 578b4c31b35..bebdc7d9ff7 100644 --- a/doc/howtos/website/routing-converter +++ b/doc/howtos/website/routing-converter @@ -1,10 +1,12 @@ # HG changeset patch # Parent e9d9740fa1852c80541c6b5b94280c8ec74cc4bb -diff -r e9d9740fa185 -r 122340f43602 academy/controllers.py ---- a/academy/controllers.py Tue Aug 12 16:51:29 2014 +0200 -+++ b/academy/controllers.py Tue Aug 12 16:52:38 2014 +0200 -@@ -9,7 +9,7 @@ class Academy(http.Controller): - 'teachers': Teachers.search([]), +# Parent cf501446e2f8d7a059ef7bd16866ed03c8f2a04f + +diff --git a/academy/controllers.py b/academy/controllers.py +--- a/academy/controllers.py ++++ b/academy/controllers.py +@@ -9,9 +9,9 @@ class Academy(http.Controller): + 'teachers': Teachers.search([]) }) - @http.route('/academy//', auth='public', website=True) @@ -14,3 +16,5 @@ diff -r e9d9740fa185 -r 122340f43602 academy/controllers.py + def teacher(self, id): + return '

{} ({})

'.format(id, type(id).__name__) + + # @http.route('/academy/academy/objects/', auth='public') diff --git a/doc/howtos/website/teacher-links b/doc/howtos/website/teacher-links index 51945ab27b9..e6c6e8284a0 100644 --- a/doc/howtos/website/teacher-links +++ b/doc/howtos/website/teacher-links @@ -1,26 +1,27 @@ # HG changeset patch # Parent 91ffd360f3ffd44acb605c09753498b3d5f3f210 +# Parent 976a92eff9104d355cb86a0b41e883a19e75e923 diff --git a/academy/templates.xml b/academy/templates.xml --- a/academy/templates.xml +++ b/academy/templates.xml -@@ -5,7 +5,9 @@ -
-
- --

-+

-+ -+

-
-
-
+@@ -6,7 +6,9 @@ +
+
+ +-

++

++ ++

+
+
+
@@ -18,7 +20,7 @@ -
-
-
--

-+

-
-
-
+
+
+
+-

++

+
+
+
diff --git a/doc/howtos/website/teachers-menu b/doc/howtos/website/teachers-menu index 45483e1caba..117b8d16317 100644 --- a/doc/howtos/website/teachers-menu +++ b/doc/howtos/website/teachers-menu @@ -1,19 +1,22 @@ # HG changeset patch # Parent c8cc9e66a2701dad6589ff1d950e1cb7738d0854 -diff -r c8cc9e66a270 -r 0201053e995d academy/__openerp__.py ---- a/academy/__openerp__.py Wed Aug 13 10:11:49 2014 +0200 -+++ b/academy/__openerp__.py Wed Aug 13 11:04:49 2014 +0200 -@@ -10,6 +10,7 @@ +# Parent 44a09b8865d418cda8b7c46dfb70fadc86e22184 + +diff --git a/academy/__openerp__.py b/academy/__openerp__.py +--- a/academy/__openerp__.py ++++ b/academy/__openerp__.py +@@ -26,6 +26,7 @@ 'data': [ - 'ir.model.access.csv', + 'security/ir.model.access.csv', 'templates.xml', + 'views.xml', ], - # data files which are only installed in "demonstration mode" + # only loaded in demonstration mode 'demo': [ -diff -r c8cc9e66a270 -r 0201053e995d academy/views.xml ---- /dev/null Thu Jan 01 00:00:00 1970 +0000 -+++ b/academy/views.xml Wed Aug 13 11:04:49 2014 +0200 +diff --git a/academy/views.xml b/academy/views.xml +new file mode 100644 +--- /dev/null ++++ b/academy/views.xml @@ -0,0 +1,13 @@ + + diff --git a/doc/howtos/website/template b/doc/howtos/website/template index c088eb246a4..1647c16c1ba 100644 --- a/doc/howtos/website/template +++ b/doc/howtos/website/template @@ -1,39 +1,42 @@ # HG changeset patch # Parent d5ad2ad84a5db0668e4e08fefb6e3f628c2e14d1 -diff -r d5ad2ad84a5d -r 3e800beb7db0 academy/__openerp__.py ---- a/academy/__openerp__.py Mon Aug 11 16:12:38 2014 +0200 -+++ b/academy/__openerp__.py Mon Aug 11 16:36:15 2014 +0200 -@@ -6,4 +6,8 @@ - """, - # Which modules must be installed for this one to work - 'depends': ['base'], -+ # data files which are always installed -+ 'data': [ -+ 'templates.xml', -+ ], - } -diff -r d5ad2ad84a5d -r 3e800beb7db0 academy/controllers.py ---- a/academy/controllers.py Mon Aug 11 16:12:38 2014 +0200 -+++ b/academy/controllers.py Mon Aug 11 16:36:15 2014 +0200 -@@ -4,5 +4,7 @@ from openerp import http +# Parent e65c9826721cb2421131399bc9223e03b482d38f + +diff --git a/academy/controllers.py b/academy/controllers.py +--- a/academy/controllers.py ++++ b/academy/controllers.py +@@ -4,7 +4,9 @@ from openerp import http class Academy(http.Controller): - @http.route('/academy/', auth='public') - def index(self): -- return "Hello, world!" + @http.route('/academy/academy/', auth='public') + def index(self, **kw): +- return "Hello, world" + return http.request.render('academy.index', { + 'teachers': ["Diana Padilla", "Jody Caroll", "Lester Vaughn"], + }) -diff -r d5ad2ad84a5d -r 3e800beb7db0 academy/templates.xml ---- /dev/null Thu Jan 01 00:00:00 1970 +0000 -+++ b/academy/templates.xml Mon Aug 11 16:36:15 2014 +0200 -@@ -0,0 +1,8 @@ -+ -+ -+ -\ No newline at end of file + # @http.route('/academy/academy/objects/', auth='public') + # def list(self, **kw): +diff --git a/academy/templates.xml b/academy/templates.xml +--- a/academy/templates.xml ++++ b/academy/templates.xml +@@ -1,14 +1,11 @@ + + +- +- +- +- +- +- +- +- +- ++ + + + diff --git a/doc/howtos/website/website-support b/doc/howtos/website/website-support index d3c1a932b87..33b401c0144 100644 --- a/doc/howtos/website/website-support +++ b/doc/howtos/website/website-support @@ -1,47 +1,50 @@ # HG changeset patch # Parent 7ae0db4c2ddf5d0f7f58db2af8976fcec905cc4e -diff -r 7ae0db4c2ddf -r 0795247fa6cb academy/__openerp__.py ---- a/academy/__openerp__.py Tue Aug 12 11:24:14 2014 +0200 -+++ b/academy/__openerp__.py Tue Aug 12 11:27:14 2014 +0200 -@@ -5,7 +5,7 @@ - 'description': """ - """, - # Which modules must be installed for this one to work +# Parent 06f959831b14dd5d2e0a57d3e14bf919e18f8e3a + +diff --git a/academy/__openerp__.py b/academy/__openerp__.py +--- a/academy/__openerp__.py ++++ b/academy/__openerp__.py +@@ -20,7 +20,7 @@ + 'version': '0.1', + + # any module necessary for this one to work correctly - 'depends': ['base'], + 'depends': ['website'], - # data files which are always installed + + # always loaded 'data': [ - 'ir.model.access.csv', -diff -r 7ae0db4c2ddf -r 0795247fa6cb academy/controllers.py ---- a/academy/controllers.py Tue Aug 12 11:24:14 2014 +0200 -+++ b/academy/controllers.py Tue Aug 12 11:27:14 2014 +0200 +diff --git a/academy/controllers.py b/academy/controllers.py +--- a/academy/controllers.py ++++ b/academy/controllers.py @@ -2,7 +2,7 @@ from openerp import http class Academy(http.Controller): -- @http.route('/academy/', auth='public') -+ @http.route('/academy/', auth='public', website=True) - def index(self): +- @http.route('/academy/academy/', auth='public') ++ @http.route('/academy/academy/', auth='public', website=True) + def index(self, **kw): Teachers = http.request.env['academy.teachers'] return http.request.render('academy.index', { -diff -r 7ae0db4c2ddf -r 0795247fa6cb academy/templates.xml ---- a/academy/templates.xml Tue Aug 12 11:24:14 2014 +0200 -+++ b/academy/templates.xml Tue Aug 12 11:27:14 2014 +0200 -@@ -1,8 +1,14 @@ - - - +diff --git a/academy/templates.xml b/academy/templates.xml +--- a/academy/templates.xml ++++ b/academy/templates.xml +@@ -1,9 +1,15 @@ + + + + diff --git a/doc/reference/actions.rst b/doc/reference/actions.rst index fe2dfb6d68c..a0698be5516 100644 --- a/doc/reference/actions.rst +++ b/doc/reference/actions.rst @@ -370,10 +370,36 @@ server actions: .. _reference/actions/report: -Report Actions -============== +Report Actions (``ir.actions.report.xml``) +========================================== -.. todo:: sle-odoo +Triggers the printing of a report + +``name`` (mandatory) + only useful as a mnemonic/description of the report when looking for one + in a list of some sort +``model`` (mandatory) + the model your report will be about +``report_type`` (mandatory) + either ``qweb-pdf`` for PDF reports or ``qweb-html`` for HTML +``report_name`` + the name of your report (which will be the name of the PDF output) +``groups_id`` + :class:`~openerp.fields.Many2many` field to the groups allowed to view/use + the current report +``paperformat_id`` + :class:`~openerp.fields.Many2one` field to the paper format you wish to + use for this report (if not specified, the company format will be used) +``attachment_use`` + if set to ``True``, the report is only generated once the first time it is + requested, and re-printed from the stored report afterwards instead of + being re-generated every time. + + Can be used for reports which must only be generated once (e.g. for legal + reasons) +``attachment`` + python expression that defines the name of the report; the record is + accessible as the variable ``object`` .. _reference/actions/client: diff --git a/doc/reference/reports.rst b/doc/reference/reports.rst index 7dfbfb1c8c7..6f33100aedd 100644 --- a/doc/reference/reports.rst +++ b/doc/reference/reports.rst @@ -1,37 +1,126 @@ -.. _reference/reports: +.. highlight:: xml ============ QWeb Reports ============ -Paper Formats -------------- +Reports are written in HTML/QWeb, like all regular views in Odoo. You can use +the usual :ref:`QWeb control flow tools `. The PDF rendering +itself is performed by wkhtmltopdf_. -There is a model called Paper Format allowing to define details specific to -the PDF output. These details include margins, header line, ... Everything -related to the printed pdf. Defining a paper format is not mandatory as there -is a default one set on the company. If you want a specific report to be -associated to a specific paper format , just link the ir.actions.report.xml to -it. +If you want to create a report on a certain model, you will need to define +this :ref:`reference/reports/report` and the +:ref:`reference/reports/templates` it will use. If you wish, you can also +specify a specific :ref:`reference/reports/paper_formats` for this +report. Finally, if you need access to more than your model, you can define a +:ref:`reference/reports/custom_reports` class that gives you access to more +models and records in the template. -Expressions used in Odoo report templates ------------------------------------------ +.. _reference/reports/report: -There are some magic variables used in the report rendering. The main ones are -the following: +Report +====== + +Every report must be declared by a :ref:`report action +`. + +For simplicity, a shortcut ```` element is available to define a +report, rather than have to set up :ref:`the action +` and its surroundings manually. That ```` +can take the following attributes: + +``id`` + the generated record's :term:`external id` +``name`` (mandatory) + only useful as a mnemonic/description of the report when looking for one + in a list of some sort +``model`` (mandatory) + the model your report will be about +``report_type`` (mandatory) + either ``qweb-pdf`` for PDF reports or ``qweb-html`` for HTML +``report_name`` + the name of your report (which will be the name of the PDF output) +``groups`` + :class:`~openerp.fields.Many2many` field to the groups allowed to view/use + the current report +``attachment_use`` + if set to True, the report will be stored as an attachment of the record + using the name generated by the ``attachment`` expression; you can use + this if you need your report to be generated only once (for legal reasons, + for example) +``attachment`` + python expression that defines the name of the report; the record is + acessible as the variable ``object`` + +.. warning:: + + The paper format cannot currently be declared via the ```` + shortcut, it must be added afterwards using a ```` extension on the + report action itself:: + + + + + +Example:: + + + +.. _reference/reports/templates: + +Report template +=============== + + +Minimal viable template +----------------------- + +A minimal template would look like:: + + + +Calling ``external_layout`` will add the default header and footer on your +report. The PDF body will be the content inside the ``
``. The template's ``id`` must be the name specified in the +report declaration; for example ``account.report_invoice`` for the above +report. Since this is a QWeb template, you can access all the fields of the +``docs`` objects received by the template. + +There are some specific variables accessible in reports, mainly: ``docs`` records for the current report ``doc_ids`` list of ids for the ``docs`` records ``doc_model`` - model for teh ``docs`` records + model for the ``docs`` records ``time`` - a reference to time_ from the Python standard library + a reference to :mod:`python:time` from the Python standard library ``translate_doc`` a function to translate a part of a report. It must be used as follow: - .. code-block:: xml + :: @@ -39,36 +128,182 @@ the following: ``user`` ``res.user`` record for the user printing the report ``res_company`` - record the current ``user``'s company + record for the current ``user``'s company -Custom report -------------- +If you wish to access other records/models in the template, you will need +:ref:`a custom report `. -A generic report use the default rendering context, containing the magic -variables as explained before. If you want a new rendering context containing -anything you want to process your data Odoo AbstractModel, a custom module is -needed. These reports are called "particular report". +Translatable Templates +---------------------- -For a particular report, you have to write an Odoo Model containing a -render_html method. Classically, this method returns a call to the original -**QWeb render** with a **custom rendering context**. +If you wish to translate reports (to the language of a partner, for example), +you need to define two templates: + +* The main report template +* The translatable document + +You can then call translate_doc from your main template to obtain the +translated document. If you wish to see the details of the translation in the +backend, you can go to :menuselection:`Settings --> Reports --> Report --> + --> Search associated QWeb views --> --> +Associated translations`. + +For example, let's look at the Sale Order report from the Sale module:: + + + + + + + + +The main template calls translate_doc with ``partner_id.lang`` as a parameter, +which means it uses :ref:`a custom report model +` to access a ``res.partner`` record. + +Barcodes +-------- + +Barcodes are images returned by a controller and can easily be embedded in +reports thanks to the QWeb syntax: + +.. code-block:: html + + + +More parameters can be passed as a query string + +.. code-block:: html + + + + +Useful Remarks +-------------- +* Twitter Bootstrap and FontAwesome classes can be used in your report + template +* Local CSS can be put directly in the template + +* Global CSS can be inserted in the main report layout by inheriting its + template and inserting your CSS:: + + + +.. _reference/reports/paper_formats: + +Paper Format +============ + +Paper formats are records of ``report.paperformat`` and can contain the +following attributes: + +``name`` (mandatory) + only useful as a mnemonic/description of the report when looking for one + in a list of some sort +``description`` + a small description of your format +``format`` + either a predefined format (A0 to A9, B0 to B10, Legal, Letter, + Tabloid,...) or ``custom``; A4 by default. You cannot use a non-custom + format if you define the page dimensions. +``dpi`` + output DPI; 90 by default +``margin_top``, ``margin_bottom``, ``margin_left``, ``margin_right`` + margin sizes in mm +``page_height``, ``page_width`` + page dimensions in mm +``orientation`` + Landscape or Portrait +``header_line`` + boolean to display a header line +``header_spacing`` + header spacing in mm + +Example:: + + + French Bank Check + + custom + 80 + 175 + Portrait + 3 + 3 + 3 + 3 + + 3 + 80 + + +.. _reference/reports/custom_reports: + +Custom Reports +============== + +The report model has a default ``get_html`` function that looks for a model +named :samp:`report.{module.report_name}`. If it exists, it will use it to +call the QWeb engine; otherwise a generic function will be used. If you wish +to customize your reports by including more things in the template (like +records of others models, for example), you can define this model, overwrite +the function ``render_html`` and pass objects in the ``docargs`` dictionnary: .. code-block:: python from openerp import api, models - class ParticularReport(models.AbstractModel): - _name = 'report.<>' + _name = 'report.module.report_name' @api.multi def render_html(self, data=None): report_obj = self.env['report'] - report = report_obj._get_report_from_name('<>') + report = report_obj._get_report_from_name('module.report_name') docargs = { 'doc_ids': self._ids, 'doc_model': report.model, 'docs': self, } - return report_obj.render('<>', docargs) + return report_obj.render('module.report_name', docargs) -.. _time: https://docs.python.org/2/library/time.html +Reports are web pages +===================== + +Reports are dynamically generated by the report module and can be accessed +directly via URL: + +For example, you can access a Sale Order report in html mode by going to +\http:///report/html/sale.report_saleorder/38 + +Or you can access the pdf version at +\http:///report/pdf/sale.report_saleorder/38 + +.. _wkhtmltopdf: http://wkhtmltopdf.org diff --git a/doc/setup/install.rst b/doc/setup/install.rst index 7fb83ea4667..4221eb15225 100644 --- a/doc/setup/install.rst +++ b/doc/setup/install.rst @@ -316,7 +316,7 @@ Source installation requires manually installing dependencies: they require compilation. - add ``pypiwin32`` because it's needed under windows. - Then use pip to install install the dependencies using the following + Then use pip to install the dependencies using the following command from a cmd.exe prompt (replace ``\YourOdooPath`` by the actual path where you downloaded Odoo): diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py index bca23a9bbcd..6fd21c97d95 100644 --- a/openerp/addons/base/ir/ir_actions.py +++ b/openerp/addons/base/ir/ir_actions.py @@ -591,17 +591,15 @@ class ir_actions_server(osv.osv): 'condition': 'True', 'type': 'ir.actions.server', 'sequence': 5, - 'code': """# You can use the following variables: -# - self: ORM model of the record on which the action is triggered + 'code': """# Available locals: +# - time, datetime, dateutil: Python libraries +# - env: Odoo Environement +# - model: Model of the record on which the action is triggered # - object: Record on which the action is triggered if there is one, otherwise None -# - pool: ORM model pool (i.e. self.pool) -# - cr: database cursor -# - uid: current user id -# - context: current context -# - time: Python time module # - workflow: Workflow engine # - log : log(message), function to log debug information in logging table -# If you plan to return an action, assign: action = {...}""", +# - Warning: Warning Exception to use with raise +# To return an action, assign: action = {...}""", 'use_relational_model': 'base', 'use_create': 'new', 'use_write': 'current', @@ -971,18 +969,30 @@ class ir_actions_server(osv.osv): eval_context = super(ir_actions_server, self)._get_eval_context(cr, uid, action=action, context=context) obj_pool = self.pool[action.model_id.model] + env = openerp.api.Environment(cr, uid, context) + model = env[action.model_id.model] obj = None if context.get('active_model') == action.model_id.model and context.get('active_id'): - obj = obj_pool.browse(cr, uid, context['active_id'], context=context) + obj = model.browse(context['active_id']) eval_context.update({ - 'self': obj_pool, + # orm + 'env': env, + 'model': model, + 'workflow': workflow, + # Exceptions + 'Warning': openerp.exceptions.Warning, + # record + # TODO: When porting to master move badly named obj and object to + # deprecated and define record (active_id) and records (active_ids) 'object': obj, 'obj': obj, + # Deprecated use env or model instead + 'self': obj_pool, 'pool': self.pool, 'cr': cr, 'context': context, - 'workflow': workflow, - 'Warning': openerp.exceptions.Warning, + 'user': env.user, + # helpers 'log': log, }) return eval_context diff --git a/openerp/addons/base/ir/ir_attachment.py b/openerp/addons/base/ir/ir_attachment.py index 002e428acff..8beb80e3a3b 100644 --- a/openerp/addons/base/ir/ir_attachment.py +++ b/openerp/addons/base/ir/ir_attachment.py @@ -362,7 +362,7 @@ class ir_attachment(osv.osv): ima.check(cr, uid, model, mode) self.pool[model].check_access_rule(cr, uid, existing_ids, mode, context=context) if require_employee: - if not self.pool['res.users'].has_group(cr, uid, 'base.group_user'): + if not uid == SUPERUSER_ID and not self.pool['res.users'].has_group(cr, uid, 'base.group_user'): raise AccessError(_("Sorry, you are not allowed to access this document.")) def _search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None): diff --git a/openerp/addons/base/ir/ir_mail_server.py b/openerp/addons/base/ir/ir_mail_server.py index 3cb8405c647..ca411f5a70b 100644 --- a/openerp/addons/base/ir/ir_mail_server.py +++ b/openerp/addons/base/ir/ir_mail_server.py @@ -363,6 +363,27 @@ class ir_mail_server(osv.osv): msg.attach(part) return msg + def _get_default_bounce_address(self, cr, uid, context=None): + '''Compute the default bounce address. + + The default bounce address is used to set the envelop address if no + envelop address is provided in the message. It is formed by properly + joining the parameters "mail.catchall.alias" and + "mail.catchall.domain". + + If "mail.catchall.alias" is not set it defaults to "postmaster-odoo". + + If "mail.catchall.domain" is not set, return None. + + ''' + get_param = self.pool['ir.config_parameter'].get_param + postmaster = get_param(cr, uid, 'mail.bounce.alias', + default='postmaster-odoo', + context=context,) + domain = get_param(cr, uid, 'mail.catchall.domain', context=context) + if postmaster and domain: + return '%s@%s' % (postmaster, domain) + def send_email(self, cr, uid, message, mail_server_id=None, smtp_server=None, smtp_port=None, smtp_user=None, smtp_password=None, smtp_encryption=None, smtp_debug=False, context=None): @@ -378,8 +399,9 @@ class ir_mail_server(osv.osv): and fails if not found. :param message: the email.message.Message to send. The envelope sender will be extracted from the - ``Return-Path`` or ``From`` headers. The envelope recipients will be - extracted from the combined list of ``To``, ``CC`` and ``BCC`` headers. + ``Return-Path`` (if present), or will be set to the default bounce address. + The envelope recipients will be extracted from the combined list of ``To``, + ``CC`` and ``BCC`` headers. :param mail_server_id: optional id of ir.mail_server to use for sending. overrides other smtp_* arguments. :param smtp_server: optional hostname of SMTP server to use :param smtp_encryption: optional TLS mode, one of 'none', 'starttls' or 'ssl' (see ir.mail_server fields for explanation) @@ -390,7 +412,14 @@ class ir_mail_server(osv.osv): :return: the Message-ID of the message that was just sent, if successfully sent, otherwise raises MailDeliveryException and logs root cause. """ - smtp_from = message['Return-Path'] or message['From'] + # Use the default bounce address **only if** no Return-Path was + # provided by caller. Caller may be using Variable Envelope Return + # Path (VERP) to detect no-longer valid email addresses. + smtp_from = message['Return-Path'] + if not smtp_from: + smtp_from = self._get_default_bounce_address(cr, uid, context=context) + if not smtp_from: + smtp_from = message['From'] assert smtp_from, "The Return-Path or From header is required for any outbound email" # The email's "Envelope From" (Return-Path), and all recipient addresses must only contain ASCII characters. diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 87223741f8d..65ff60b66db 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -1068,6 +1068,7 @@ class view(osv.osv): if values is None: values = dict() qcontext = dict( + env=api.Environment(cr, uid, context), keep_query=keep_query, request=request, # might be unbound if we're not in an httprequest context debug=request.debug if request else False, diff --git a/openerp/addons/base/res/res_currency.py b/openerp/addons/base/res/res_currency.py index 8f7b1c1835b..2eebec90767 100644 --- a/openerp/addons/base/res/res_currency.py +++ b/openerp/addons/base/res/res_currency.py @@ -301,3 +301,22 @@ class res_currency_rate(osv.osv): 'name': lambda *a: time.strftime('%Y-%m-%d 00:00:00'), } _order = "name desc" + + def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=80): + if operator in ['=', '!=']: + try: + date_format = '%Y-%m-%d' + if context.get('lang'): + lang_obj = self.pool['res.lang'] + lang_ids = lang_obj.search(cr, user, [('code', '=', context['lang'])], context=context) + if lang_ids: + date_format = lang_obj.browse(cr, user, lang_ids[0], context=context).date_format + name = time.strftime('%Y-%m-%d', time.strptime(name, date_format)) + except ValueError: + try: + args.append(('rate', operator, float(name))) + except ValueError: + return [] + name = '' + operator = 'ilike' + return super(res_currency_rate, self).name_search(cr, user, name, args=args, operator=operator, context=context, limit=limit) diff --git a/openerp/fields.py b/openerp/fields.py index ca308bcad5d..71d077143fa 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -317,8 +317,9 @@ class Field(object): attrs['copy'] = attrs.get('copy', False) attrs['readonly'] = attrs.get('readonly', not attrs.get('inverse')) if attrs.get('related'): - # by default, related fields are not stored + # by default, related fields are not stored and not copied attrs['store'] = attrs.get('store', False) + attrs['copy'] = attrs.get('copy', False) # fix for function fields overridden by regular columns if not isinstance(attrs.get('column'), (NoneType, fields.function)): diff --git a/openerp/http.py b/openerp/http.py index 439be25c4ff..bdd009bfedf 100644 --- a/openerp/http.py +++ b/openerp/http.py @@ -398,14 +398,18 @@ def route(route=None, **kw): response = f(*args, **kw) if isinstance(response, Response) or f.routing_type == 'json': return response - elif isinstance(response, werkzeug.wrappers.BaseResponse): + + if isinstance(response, basestring): + return Response(response) + + if isinstance(response, werkzeug.exceptions.HTTPException): + response = response.get_response() + if isinstance(response, werkzeug.wrappers.BaseResponse): response = Response.force_type(response) response.set_default() return response - elif isinstance(response, basestring): - return Response(response) - else: - _logger.warn(" returns an invalid response type for an http request" % (f.__module__, f.__name__)) + + _logger.warn(" returns an invalid response type for an http request" % (f.__module__, f.__name__)) return response response_wrap.routing = routing response_wrap.original_func = f diff --git a/openerp/models.py b/openerp/models.py index c3d47da103b..5937aaee264 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -188,7 +188,7 @@ def get_pg_type(f, type_override=None): elif issubclass(field_type, (fields.char, fields.reference)): pg_type = ('varchar', pg_varchar(f.size)) elif issubclass(field_type, fields.selection): - if (isinstance(f.selection, list) and isinstance(f.selection[0][0], int))\ + if (f.selection and isinstance(f.selection, list) and isinstance(f.selection[0][0], int))\ or getattr(f, 'size', None) == -1: pg_type = ('int4', 'INTEGER') else: @@ -2504,7 +2504,7 @@ class BaseModel(object): ('numeric', 'float', get_pg_type(f)[1], '::'+get_pg_type(f)[1]), ('float8', 'float', get_pg_type(f)[1], '::'+get_pg_type(f)[1]), ] - if f_pg_type == 'varchar' and f._type == 'char' and f_pg_size and (f.size is None or f_pg_size < f.size): + if f_pg_type == 'varchar' and f._type in ('char', 'selection') and f_pg_size and (f.size is None or f_pg_size < f.size): try: with cr.savepoint(): cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" TYPE %s' % (self._table, k, pg_varchar(f.size))) diff --git a/openerp/modules/module.py b/openerp/modules/module.py index 952fe5bad5e..5df069506c5 100644 --- a/openerp/modules/module.py +++ b/openerp/modules/module.py @@ -31,6 +31,7 @@ import re import sys import time import unittest +import threading from os.path import join as opj import unittest2 @@ -461,6 +462,7 @@ def run_unit_tests(module_name, dbname, position=runs_at_install): global current_test current_test = module_name mods = get_test_modules(module_name) + threading.currentThread().testing = True r = True for m in mods: tests = unwrap_suite(unittest2.TestLoader().loadTestsFromModule(m)) @@ -478,6 +480,7 @@ def run_unit_tests(module_name, dbname, position=runs_at_install): _logger.error("Module %s: %d failures, %d errors", module_name, len(result.failures), len(result.errors)) current_test = None + threading.currentThread().testing = False return r def unwrap_suite(test): diff --git a/openerp/tools/config.py b/openerp/tools/config.py index 1d0be528cbe..9b90c36394a 100644 --- a/openerp/tools/config.py +++ b/openerp/tools/config.py @@ -48,8 +48,7 @@ class MyOption (optparse.Option, object): self.my_default = attrs.pop('my_default', None) super(MyOption, self).__init__(*opts, **attrs) -DEFAULT_LOG_HANDLER = [':INFO'] - +DEFAULT_LOG_HANDLER = ':INFO' def _get_default_datadir(): home = os.path.expanduser('~') if os.path.isdir(home): @@ -62,6 +61,20 @@ def _get_default_datadir(): # No "version" kwarg as session and filestore paths are shared against series return func(appname=release.product_name, appauthor=release.author) +def _deduplicate_loggers(loggers): + """ Avoid saving multiple logging levels for the same loggers to a save + file, that just takes space and the list can potentially grow unbounded + if for some odd reason people use :option`odoo.py --save`` all the time. + """ + # dict(iterable) -> the last item of iterable for any given key wins, + # which is what we want and expect. Output order should not matter as + # there are no duplicates within the output sequence + return ( + '{}:{}'.format(logger, level) + for logger, level in dict(it.split(':') for it in loggers).iteritems() + ) + + class configmanager(object): def __init__(self, fname=None): """Constructor. @@ -160,7 +173,7 @@ class configmanager(object): group.add_option("--logfile", dest="logfile", help="file where the server log will be stored") group.add_option("--logrotate", dest="logrotate", action="store_true", my_default=False, help="enable logfile rotation") group.add_option("--syslog", action="store_true", dest="syslog", my_default=False, help="Send the log to the syslog server") - group.add_option('--log-handler', action="append", default=DEFAULT_LOG_HANDLER, my_default=DEFAULT_LOG_HANDLER, metavar="PREFIX:LEVEL", help='setup a handler at LEVEL for a given PREFIX. An empty PREFIX indicates the root logger. This option can be repeated. Example: "openerp.orm:DEBUG" or "werkzeug:CRITICAL" (default: ":INFO")') + group.add_option('--log-handler', action="append", default=[], my_default=DEFAULT_LOG_HANDLER, metavar="PREFIX:LEVEL", help='setup a handler at LEVEL for a given PREFIX. An empty PREFIX indicates the root logger. This option can be repeated. Example: "openerp.orm:DEBUG" or "werkzeug:CRITICAL" (default: ":INFO")') group.add_option('--log-request', action="append_const", dest="log_handler", const="openerp.http.rpc.request:DEBUG", help='shortcut for --log-handler=openerp.http.rpc.request:DEBUG') group.add_option('--log-response', action="append_const", dest="log_handler", const="openerp.http.rpc.response:DEBUG", help='shortcut for --log-handler=openerp.http.rpc.response:DEBUG') group.add_option('--log-web', action="append_const", dest="log_handler", const="openerp.http:DEBUG", help='shortcut for --log-handler=openerp.http:DEBUG') @@ -369,14 +382,14 @@ class configmanager(object): 'email_from', 'smtp_server', 'smtp_user', 'smtp_password', 'db_maxconn', 'import_partial', 'addons_path', 'xmlrpc', 'syslog', 'without_demo', - 'dbfilter', 'log_handler', 'log_level', 'log_db', + 'dbfilter', 'log_level', 'log_db', 'geoip_database', ] for arg in keys: # Copy the command-line argument (except the special case for log_handler, due to # action=append requiring a real default, so we cannot use the my_default workaround) - if getattr(opt, arg) and getattr(opt, arg) != DEFAULT_LOG_HANDLER: + if getattr(opt, arg): self.options[arg] = getattr(opt, arg) # ... or keep, but cast, the config file value. elif isinstance(self.options[arg], basestring) and self.casts[arg].type in optparse.Option.TYPE_CHECKER: @@ -384,6 +397,7 @@ class configmanager(object): if isinstance(self.options['log_handler'], basestring): self.options['log_handler'] = self.options['log_handler'].split(',') + self.options['log_handler'].extend(opt.log_handler) # if defined but None take the configfile value keys = [ @@ -566,6 +580,8 @@ class configmanager(object): continue if opt in ('log_level',): p.set('options', opt, loglevelnames.get(self.options[opt], self.options[opt])) + elif opt == 'log_handler': + p.set('options', opt, ','.join(_deduplicate_loggers(self.options[opt]))) else: p.set('options', opt, self.options[opt]) diff --git a/openerp/tools/safe_eval.py b/openerp/tools/safe_eval.py index e30a3554364..4eef310f68c 100644 --- a/openerp/tools/safe_eval.py +++ b/openerp/tools/safe_eval.py @@ -32,6 +32,7 @@ condition/math builtins. # - safe_eval in tryton http://hg.tryton.org/hgwebdir.cgi/trytond/rev/bbb5f73319ad from opcode import HAVE_ARGUMENT, opmap, opname +from psycopg2 import OperationalError from types import CodeType import logging @@ -321,6 +322,10 @@ def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=Fal raise except openerp.exceptions.AccessError: raise + except OperationalError: + # Do not hide PostgreSQL low-level exceptions, to let the auto-replay + # of serialized transactions work its magic + raise except openerp.exceptions.MissingError: raise except Exception, e: