diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 5bae62f5fba..74c3d2ca30a 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -1132,7 +1132,10 @@ class AccountInvoiceLine(models.Model): doc = etree.XML(res['arch']) for node in doc.xpath("//field[@name='product_id']"): if self._context['type'] in ('in_invoice', 'in_refund'): - node.set('domain', "[('purchase_ok', '=', True)]") + # Hack to fix the stable version 8.0 -> saas-12 + # purchase_ok will be moved from purchase to product in master #13271 + if 'purchase_ok' in self.env['product.template']._fields: + node.set('domain', "[('purchase_ok', '=', True)]") else: node.set('domain', "[('sale_ok', '=', True)]") res['arch'] = etree.tostring(doc) diff --git a/addons/account_asset/views/account_asset_view.xml b/addons/account_asset/views/account_asset_view.xml index cdbabc75d8d..8910a4115bd 100644 --- a/addons/account_asset/views/account_asset_view.xml +++ b/addons/account_asset/views/account_asset_view.xml @@ -123,11 +123,7 @@ -
-
- + diff --git a/addons/board/static/src/less/dashboard.less b/addons/board/static/src/less/dashboard.less index b0b41e65e34..36f0727c0ac 100644 --- a/addons/board/static/src/less/dashboard.less +++ b/addons/board/static/src/less/dashboard.less @@ -139,5 +139,4 @@ // Favorites menu in control panel .o_add_to_dashboard { display: none; // hidden by default - .o-search-options-dropdown-custom-li; } diff --git a/addons/hr_holidays/hr_holidays_view.xml b/addons/hr_holidays/hr_holidays_view.xml index 0954d70179a..83a4edd65d2 100644 --- a/addons/hr_holidays/hr_holidays_view.xml +++ b/addons/hr_holidays/hr_holidays_view.xml @@ -250,7 +250,7 @@ form calendar {'search_default_year': 1, 'search_default_group_employee': 1} - [('holiday_type','=','employee'), ('type', '=', 'remove')] + [('holiday_type','=','employee'), ('type', '=', 'remove'), ('state', '!=', 'refuse')] diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js index 28138fbeaab..8710c7195a3 100644 --- a/addons/mail/static/src/js/chat_manager.js +++ b/addons/mail/static/src/js/chat_manager.js @@ -206,6 +206,22 @@ function make_message (data) { a.url = '/web/content/' + a.id + '?download=true'; }); + // format date to the local only once by message + // can not be done in preprocess, since it alter the original value + if (msg.tracking_value_ids && msg.tracking_value_ids.length) { + _.each(msg.tracking_value_ids, function(f) { + if (_.contains(['date', 'datetime'], f.field_type)) { + var format = (f.field_type === 'date') ? 'LL' : 'LLL'; + if (f.old_value) { + f.old_value = moment.utc(f.old_value).local().format(format); + } + if (f.new_value) { + f.new_value = moment.utc(f.new_value).local().format(format); + } + } + }); + } + return msg; } @@ -623,7 +639,7 @@ var chat_manager = { // post a message in a channel return ChannelModel.call('message_post', [options.channel_id], _.extend(msg, { message_type: 'comment', - content_subtype: 'html', + content_subtype: 'plaintext', subtype: 'mail.mt_comment', })); } diff --git a/addons/mail/static/src/js/thread.js b/addons/mail/static/src/js/thread.js index 55acfc00f9d..00f7a44f1b1 100644 --- a/addons/mail/static/src/js/thread.js +++ b/addons/mail/static/src/js/thread.js @@ -234,20 +234,6 @@ var Thread = Widget.extend({ msg.day = msg.date.format('LL'); } - if (message.tracking_value_ids && message.tracking_value_ids.length) { - _.each(message.tracking_value_ids, function(f) { - if (_.contains(['date', 'datetime'], f.field_type)) { - var format = (f.field_type === 'date') ? 'LL' : 'LLL'; - if (f.old_value) { - f.old_value = moment.utc(f.old_value).local().format(format); - } - if (f.new_value) { - f.new_value = moment.utc(f.new_value).local().format(format); - } - } - }); - } - if (_.contains(this.expanded_msg_ids, message.id)) { msg.expanded = true; } diff --git a/addons/mass_mailing/static/src/js/mass_mailing.js b/addons/mass_mailing/static/src/js/mass_mailing.js index 3785cf87070..e2bcb8a09ad 100644 --- a/addons/mass_mailing/static/src/js/mass_mailing.js +++ b/addons/mass_mailing/static/src/js/mass_mailing.js @@ -1,6 +1,7 @@ odoo.define('mass_mailing.mass_mailing', function (require) { var core = require('web.core'); +var FieldTextHtml = require('web_editor.backend').FieldTextHtml; var KanbanRecord = require('web_kanban.Record'); var KanbanView = require('web_kanban.KanbanView'); @@ -25,4 +26,21 @@ KanbanView.include({ }, }); +FieldTextHtml.include({ + get_datarecord: function() { + /* Avoid extremely long URIs by whitelisting fields in the datarecord + that get set as a get parameter */ + var datarecord = this._super(); + if (this.view.model === 'mail.mass_mailing') { + // these fields can potentially get very long, let's remove them + var blacklist = ['mailing_domain', 'contact_list_ids']; + for (var k in blacklist) { + delete datarecord[blacklist[k]]; + } + delete datarecord[this.name]; + } + return datarecord; + }, +}); + }); diff --git a/addons/mrp/report/bom_structure.py b/addons/mrp/report/bom_structure.py index 2618de3994b..f3ee9173ade 100644 --- a/addons/mrp/report/bom_structure.py +++ b/addons/mrp/report/bom_structure.py @@ -15,12 +15,12 @@ class bom_structure(report_sxw.rml_parse): def get_children(self, object, level=0): result = [] - def _get_rec(object, level): + def _get_rec(object, level, qty=1.0): for l in object: res = {} res['pname'] = l.product_id.name_get()[0][1] res['pcode'] = l.product_id.default_code - res['pqty'] = l.product_qty + res['pqty'] = l.product_qty * qty res['uname'] = l.product_uom.name res['level'] = level res['code'] = l.bom_id.code @@ -28,7 +28,7 @@ class bom_structure(report_sxw.rml_parse): if l.child_line_ids: if level<6: level += 1 - _get_rec(l.child_line_ids,level) + _get_rec(l.child_line_ids, level, qty=res['pqty']) if level>0 and level<6: level -= 1 return result diff --git a/addons/payment_paypal/models/__init__.py b/addons/payment_paypal/models/__init__.py index 7af8e269d79..4729fc79543 100644 --- a/addons/payment_paypal/models/__init__.py +++ b/addons/payment_paypal/models/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import account_invoice import paypal import res_company diff --git a/addons/payment_paypal/models/account_invoice.py b/addons/payment_paypal/models/account_invoice.py new file mode 100644 index 00000000000..7b7f3e377b7 --- /dev/null +++ b/addons/payment_paypal/models/account_invoice.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +from openerp import models, fields, api + +import werkzeug.urls + +class AccountInvoice(models.Model): + _inherit = 'account.invoice' + + paypal_url = fields.Char('Paypal Url', compute='_compute_paypal_url') + + @api.depends('type', 'number', 'company_id', 'currency_id') + def _compute_paypal_url(self): + for inv in self: + if inv.type == 'out_invoice' and inv.company_id.paypal_account: + params = { + "cmd": "_xclick", + "business": inv.company_id.paypal_account, + "item_name": "%s Invoice %s" % (inv.company_id.name, inv.number or ''), + "invoice": inv.number, + "amount": inv.residual, + "currency_code": inv.currency_id.name, + "button_subtype": "services", + "no_note": "1", + "bn": "Odoo_Invoice_PayNow_" + inv.currency_id.name, + } + inv.paypal_url = "https://www.paypal.com/cgi-bin/webscr?" + werkzeug.url_encode(params) diff --git a/addons/sale/sales_team.py b/addons/sale/sales_team.py index 309419eb97f..a234f867cf4 100644 --- a/addons/sale/sales_team.py +++ b/addons/sale/sales_team.py @@ -35,7 +35,8 @@ class crm_team(osv.Model): ('state', 'in', ['open', 'paid']), ('team_id', '=', team), ('date', '<=', date.today()), - ('date', '>=', date.today().replace(day=1)) + ('date', '>=', date.today().replace(day=1)), + ('type', 'in', ['out_invoice', 'out_refund']), ] invoices = obj_inv.search_read(cr, uid, domain, ['amount_untaxed_signed'], context=context) res[team] = sum([inv['amount_untaxed_signed'] for inv in invoices]) diff --git a/addons/stock/stock_view.xml b/addons/stock/stock_view.xml index c1e6f634613..b442cc1abb7 100644 --- a/addons/stock/stock_view.xml +++ b/addons/stock/stock_view.xml @@ -747,8 +747,8 @@ - - + + @@ -959,7 +959,7 @@ - + diff --git a/addons/stock_account/stock_account.py b/addons/stock_account/stock_account.py index 6a54df1ac6a..d3fe752db76 100644 --- a/addons/stock_account/stock_account.py +++ b/addons/stock_account/stock_account.py @@ -303,7 +303,11 @@ class stock_quant(osv.osv): debit_value = currency_obj.round(cr, uid, move.company_id.currency_id, valuation_amount * qty) #check that all data is correct if move.company_id.currency_id.is_zero(debit_value): - raise UserError(_("The found valuation amount for product %s is zero. Which means there is probably a configuration error. Check the costing method and the standard price") % (move.product_id.name,)) + if move.product_id.cost_method == 'standard': + raise UserError(_("The found valuation amount for product %s is zero. Which means there is probably a configuration error. Check the costing method and the standard price") % (move.product_id.name,)) + else: + return [] + credit_value = debit_value if move.product_id.cost_method == 'average' and move.company_id.anglo_saxon_accounting: @@ -374,12 +378,13 @@ class stock_quant(osv.osv): move_obj = self.pool.get('account.move') for cost, qty in quant_cost_qty.items(): move_lines = self._prepare_account_move_line(cr, uid, move, qty, cost, credit_account_id, debit_account_id, context=context) - date = context.get('force_period_date', fields.date.context_today(self, cr, uid, context=context)) - new_move = move_obj.create(cr, uid, {'journal_id': journal_id, - 'line_ids': move_lines, - 'date': date, - 'ref': move.picking_id.name}, context=context) - move_obj.post(cr, uid, [new_move], context=context) + if move_lines: + date = context.get('force_period_date', fields.date.context_today(self, cr, uid, context=context)) + new_move = move_obj.create(cr, uid, {'journal_id': journal_id, + 'line_ids': move_lines, + 'date': date, + 'ref': move.picking_id.name}, context=context) + move_obj.post(cr, uid, [new_move], context=context) #def _reconcile_single_negative_quant(self, cr, uid, to_solve_quant, quant, quant_neg, qty, context=None): # move = self._get_latest_move(cr, uid, to_solve_quant, context=context) diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 616b7352053..674eebdea6a 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -34,7 +34,7 @@ from openerp.tools.translate import _ from openerp.tools import ustr from openerp.tools.misc import str2bool, xlwt from openerp import http -from openerp.http import request, serialize_exception as _serialize_exception +from openerp.http import request, serialize_exception as _serialize_exception, content_disposition from openerp.exceptions import AccessError _logger = logging.getLogger(__name__) @@ -415,10 +415,6 @@ def xml2json_from_elementtree(el, preserve_whitespaces=False): res["children"] = kids return res -def content_disposition(filename): - return request.registry['ir.http'].content_disposition(filename) - - def binary_content(xmlid=None, model='ir.attachment', id=None, field='datas', unique=False, filename=None, filename_field='datas_fname', download=False, mimetype=None, default_mimetype='application/octet-stream', env=None): return request.registry['ir.http'].binary_content( xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename, filename_field=filename_field, @@ -941,6 +937,11 @@ class Binary(http.Controller): addons_path = http.addons_manifest['web']['addons_path'] return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', image), 'rb').read() + def force_contenttype(self, headers, contenttype='image/png'): + dictheaders = dict(headers) + dictheaders['Content-Type'] = contenttype + return dictheaders.items() + @http.route(['/web/content', '/web/content/', '/web/content//', @@ -999,8 +1000,15 @@ class Binary(http.Controller): if height > 500: height = 500 content = openerp.tools.image_resize_image(base64_source=content, size=(width or None, height or None), encoding='base64', filetype='PNG') + # resize force png as filetype + headers = self.force_contenttype(headers, contenttype='image/png') + + if content: + image_base64 = base64.b64decode(content) + else: + image_base64 = self.placeholder(image='placeholder.png') # could return (contenttype, content) in master + headers = self.force_contenttype(headers, contenttype='image/png') - image_base64 = content and base64.b64decode(content) or self.placeholder() headers.append(('Content-Length', len(image_base64))) response = request.make_response(image_base64, headers) response.status_code = status diff --git a/addons/web_editor/static/src/js/backend.js b/addons/web_editor/static/src/js/backend.js index 38f25e42103..468fab0c728 100644 --- a/addons/web_editor/static/src/js/backend.js +++ b/addons/web_editor/static/src/js/backend.js @@ -220,9 +220,12 @@ var FieldTextHtml = widget.extend({ this.$translate = $(); return def; }, + get_datarecord: function() { + return this.view.get_fields_values(); + }, get_url: function (_attr) { var src = this.options.editor_url || "/web_editor/field/html"; - var datarecord = this.view.get_fields_values(); + var datarecord = this.get_datarecord(); var attr = { 'model': this.view.model, @@ -269,7 +272,6 @@ var FieldTextHtml = widget.extend({ delete datarecord[this.name]; src += "&datarecord="+ encodeURIComponent(JSON.stringify(datarecord)); - return src; }, initialize_content: function () { @@ -384,4 +386,9 @@ core.form_widget_registry .add('html', FieldTextHtmlSimple) .add('html_frame', FieldTextHtml); +return { + FieldTextHtmlSimple: FieldTextHtmlSimple, + FieldTextHtml: FieldTextHtml, +}; + }); diff --git a/addons/web_kanban/static/src/js/kanban_widgets.js b/addons/web_kanban/static/src/js/kanban_widgets.js index 1c6e5532aa5..a29105ed9b5 100644 --- a/addons/web_kanban/static/src/js/kanban_widgets.js +++ b/addons/web_kanban/static/src/js/kanban_widgets.js @@ -139,7 +139,7 @@ var KanbanSelection = AbstractField.extend({ var current_state = _.find(this.states, function(state) { return state.name === self.get('value'); - }); + }) || {state_class: ''}; self.$el = $(QWeb.render("KanbanSelection", { current_state_class: current_state.state_class, diff --git a/addons/website_blog/controllers/main.py b/addons/website_blog/controllers/main.py index 5c0db09e5bc..3e1d2d90f72 100644 --- a/addons/website_blog/controllers/main.py +++ b/addons/website_blog/controllers/main.py @@ -132,10 +132,11 @@ class WebsiteBlog(http.Controller): blog_posts = blog_post_obj.browse(cr, uid, blog_post_ids, context=context) pager = request.website.pager( - url=blog_url(), + url=request.httprequest.path, total=len(blog_posts), page=page, step=self._blog_post_per_page, + url_args=opt, ) pager_begin = (page - 1) * self._blog_post_per_page pager_end = page * self._blog_post_per_page diff --git a/addons/website_blog/models/website_blog.py b/addons/website_blog/models/website_blog.py index 352338981e5..3fe593064f5 100644 --- a/addons/website_blog/models/website_blog.py +++ b/addons/website_blog/models/website_blog.py @@ -249,7 +249,7 @@ class BlogPost(osv.Model): 'type': 'ir.actions.act_url', 'url': '/blog/%s/post/%s' % (post.blog_id.id, post.id), 'target': 'self', - 'res_id': self.id, + 'res_id': post.id, } def _notification_get_recipient_groups(self, cr, uid, ids, message, recipients, context=None): diff --git a/addons/website_event/views/website_event.xml b/addons/website_event/views/website_event.xml index 3f43bce838a..a7a87d37d17 100644 --- a/addons/website_event/views/website_event.xml +++ b/addons/website_event/views/website_event.xml @@ -451,8 +451,7 @@ Ticket Type #: ( - - + Free) diff --git a/addons/website_portal/controllers/main.py b/addons/website_portal/controllers/main.py index dc900002be7..cf8e818fc3b 100644 --- a/addons/website_portal/controllers/main.py +++ b/addons/website_portal/controllers/main.py @@ -37,6 +37,16 @@ class website_account(http.Controller): values.update(post) if not error: post.update({'zip': post.pop('zipcode', '')}) + if partner.type == "contact": + address_fields = { + 'city': post.pop('city'), + 'street': post.pop('street'), + 'street2': post.pop('street2'), + 'zip': post.pop('zip'), + 'country_id': post.pop('country_id'), + 'state_id': post.pop('state_id') + } + partner.commercial_partner_id.write(address_fields) partner.sudo().write(post) if redirect: return request.redirect(redirect) diff --git a/addons/website_quote/views/website_quotation.xml b/addons/website_quote/views/website_quotation.xml index d7f5ce792c2..ebef3ecead8 100644 --- a/addons/website_quote/views/website_quotation.xml +++ b/addons/website_quote/views/website_quotation.xml @@ -396,7 +396,7 @@
    -
  • +
  • Edit Quote Back to Sale Order
  • diff --git a/doc/cla/corporate/pine-view-buildings.md b/doc/cla/corporate/pine-view-buildings.md new file mode 100644 index 00000000000..78caa110a70 --- /dev/null +++ b/doc/cla/corporate/pine-view-buildings.md @@ -0,0 +1,15 @@ +USA, 08/13/2016 + +Pine View Buildings agrees to the terms of the Odoo Corporate Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Freeman Helmuth freeman@pineview.biz https://github.com/busyfritz + +List of contributors: + +Freeman Helmuth freeman@pineview.biz https://github.com/busyfritz diff --git a/openerp/addons/base/ir/ir_http.py b/openerp/addons/base/ir/ir_http.py index 34bb0035d1a..d8ece72e1b1 100644 --- a/openerp/addons/base/ir/ir_http.py +++ b/openerp/addons/base/ir/ir_http.py @@ -22,7 +22,8 @@ import openerp import openerp.exceptions import openerp.models from openerp import http -from openerp.http import request, STATIC_CACHE +from openerp.tools.mimetypes import guess_mimetype +from openerp.http import request, STATIC_CACHE, content_disposition from openerp.modules.module import get_resource_path, get_module_path from openerp.osv import osv, orm @@ -209,16 +210,7 @@ class ir_http(osv.AbstractModel): return self._routing_map def content_disposition(self, filename): - filename = openerp.tools.ustr(filename) - escaped = urllib2.quote(filename.encode('utf8')) - browser = request.httprequest.user_agent.browser - version = int((request.httprequest.user_agent.version or '0').split('.')[0]) - if browser == 'msie' and version < 9: - return "attachment; filename=%s" % escaped - elif browser == 'safari' and version < 537: - return u"attachment; filename=%s" % filename.encode('ascii', 'replace') - else: - return "attachment; filename*=UTF-8''%s" % escaped + return content_disposition(filename) def binary_content(self, xmlid=None, model='ir.attachment', id=None, field='datas', unique=False, filename=None, filename_field='datas_fname', download=False, mimetype=None, default_mimetype='application/octet-stream', env=None): """ Get file, attachment or downloadable content @@ -304,7 +296,8 @@ class ir_http(osv.AbstractModel): attach_mimetype = env['ir.attachment'].search_read(domain=[('res_model', '=', model), ('res_id', '=', id), ('res_field', '=', field)], fields=['mimetype'], limit=1) mimetype = attach_mimetype and attach_mimetype[0]['mimetype'] if not mimetype: - mimetype = default_mimetype + mimetype = guess_mimetype(base64.b64decode(content), default=default_mimetype) + headers += [('Content-Type', mimetype), ('X-Content-Type-Options', 'nosniff')] # cache @@ -317,7 +310,6 @@ class ir_http(osv.AbstractModel): # content-disposition default name if download: headers.append(('Content-Disposition', self.content_disposition(filename))) - return (status, headers, content) diff --git a/openerp/addons/base/tests/__init__.py b/openerp/addons/base/tests/__init__.py index e9414be9011..2325c11aa8e 100644 --- a/openerp/addons/base/tests/__init__.py +++ b/openerp/addons/base/tests/__init__.py @@ -7,12 +7,14 @@ import test_expression import test_func import test_ir_actions import test_ir_attachment +import test_ir_http import test_ir_filters import test_ir_sequence import test_ir_sequence_date_range import test_ir_values import test_mail import test_menu +import test_mimetypes import test_orm import test_ormcache import test_osv @@ -21,7 +23,7 @@ import test_res_config import test_res_lang import test_search import test_translate -#import test_uninstall +#import test_uninstall # loop import test_user_has_group import test_view_validation import test_views diff --git a/openerp/addons/base/tests/test_ir_http.py b/openerp/addons/base/tests/test_ir_http.py new file mode 100644 index 00000000000..649cd069816 --- /dev/null +++ b/openerp/addons/base/tests/test_ir_http.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- + +from openerp.tests import common +import openerp + +GIF = "R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=" + + +class test_ir_http_mimetype(common.TransactionCase): + + def test_ir_http_mimetype_attachment(self): + """ Test mimetype for attachment """ + attachment = self.env['ir.attachment'].create({ + 'datas': GIF, + 'name': 'Test mimetype gif', + 'datas_fname': 'file.gif'}) + + status, headers, content = self.env['ir.http'].binary_content( + id=attachment.id, + mimetype=None, + default_mimetype='application/octet-stream', + env=self.env + ) + mimetype = dict(headers).get('Content-Type') + self.assertEqual(mimetype, 'image/gif') + + def test_ir_http_mimetype_attachment_name(self): + """ Test mimetype for attachment with bad name""" + attachment = self.env['ir.attachment'].create({ + 'datas': GIF, + 'name': 'Test mimetype gif with png name', + 'datas_fname': 'file.png'}) + + status, headers, content = self.env['ir.http'].binary_content( + id=attachment.id, + mimetype=None, + default_mimetype='application/octet-stream', + env=self.env + ) + mimetype = dict(headers).get('Content-Type') + # TODO: fix and change it in master, should be image/gif + self.assertEqual(mimetype, 'image/png') + + def test_ir_http_mimetype_basic_field(self): + """ Test mimetype for classic field """ + partner = self.env['res.partner'].create({ + 'image': GIF, + 'name': 'Test mimetype basic field', + }) + + status, headers, content = self.env['ir.http'].binary_content( + model='res.partner', + id=partner.id, + field='image', + default_mimetype='application/octet-stream', + env=self.env + ) + mimetype = dict(headers).get('Content-Type') + self.assertEqual(mimetype, 'image/gif') + + def test_ir_http_mimetype_computed_field(self): + """ Test mimetype for computed field wich resize picture""" + prop = self.env['ir.property'].create({ + 'fields_id': self.env['ir.model.fields'].search([], limit=1).id, + 'name': "Property binary", + 'value_binary': GIF, + 'type': 'binary', + }) + + resized = openerp.tools.image_get_resized_images(prop.value_binary, return_big=True, avoid_resize_medium=True)['image_small'] + # Simul computed field which resize and that is not attachement=True (E.G. on product) + prop.write({'value_binary': resized}) + status, headers, content = self.env['ir.http'].binary_content( + model='ir.property', + id=prop.id, + field='value_binary', + default_mimetype='application/octet-stream', + env=self.env + ) + mimetype = dict(headers).get('Content-Type') + self.assertEqual(mimetype, 'image/gif') diff --git a/openerp/addons/base/tests/test_mimetypes.py b/openerp/addons/base/tests/test_mimetypes.py new file mode 100644 index 00000000000..0d3ce6b71fc --- /dev/null +++ b/openerp/addons/base/tests/test_mimetypes.py @@ -0,0 +1,52 @@ +import base64 +import unittest + +from openerp.tools.mimetypes import guess_mimetype + +PNG = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC' +GIF = "R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=" +BMP = """Qk1+AAAAAAAAAHoAAABsAAAAAQAAAAEAAAABABgAAAAAAAQAAAATCwAAEwsAAAAAAAAAAAAAQkdScwAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAD///8A""" +JPG = """/9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAP +//////////////////////////////////////////////////////////////////////////////////////2wBDAf/////// +///////////////////////////////////////////////////////////////////////////////wgARCAABAAEDAREAAhEB +AxEB/8QAFAABAAAAAAAAAAAAAAAAAAAAAv/EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhADEAAAAUf/xAAUEAEAAAAAAAA +AAAAAAAAAAAAA/9oACAEBAAEFAn//xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oACAEDAQE/AX//xAAUEQEAAAAAAAAAAAAAAAAAAA +AA/9oACAECAQE/AX//xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAY/An//xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBA +AE/IX//2gAMAwEAAgADAAAAEB//xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oACAEDAQE/EH//xAAUEQEAAAAAAAAAAAAAAAAAAAAA +/9oACAECAQE/EH//xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAE/EH//2Q==""" + + +class test_guess_mimetype(unittest.TestCase): + + def test_default_mimetype_empty(self): + mimetype = guess_mimetype('') + self.assertEqual(mimetype, 'application/octet-stream') + + def test_default_mimetype(self): + mimetype = guess_mimetype('', default='test') + self.assertEqual(mimetype, 'test') + + def test_mimetype_png(self): + content = base64.b64decode(PNG) + mimetype = guess_mimetype(content, default='test') + self.assertEqual(mimetype, 'image/png') + + def test_mimetype_bmp(self): + content = base64.b64decode(BMP) + mimetype = guess_mimetype(content, default='test') + self.assertEqual(mimetype, 'image/bmp') + + def test_mimetype_jpg(self): + content = base64.b64decode(JPG) + mimetype = guess_mimetype(content, default='test') + self.assertEqual(mimetype, 'image/jpeg') + + def test_mimetype_gif(self): + content = base64.b64decode(GIF) + mimetype = guess_mimetype(content, default='test') + self.assertEqual(mimetype, 'image/gif') + + +if __name__ == '__main__': + unittest.main() diff --git a/openerp/addons/base/tests/test_uninstall.py b/openerp/addons/base/tests/test_uninstall.py index 8bb658d61ba..a99240d703b 100644 --- a/openerp/addons/base/tests/test_uninstall.py +++ b/openerp/addons/base/tests/test_uninstall.py @@ -4,7 +4,7 @@ import unittest import openerp from openerp import SUPERUSER_ID -import common +from openerp.tests import common ADMIN_USER_ID = common.ADMIN_USER_ID diff --git a/openerp/http.py b/openerp/http.py index df12582c90f..b34f7d72a90 100644 --- a/openerp/http.py +++ b/openerp/http.py @@ -20,6 +20,7 @@ import sys import threading import time import traceback +import urllib2 import urlparse import warnings from zlib import adler32 @@ -1808,6 +1809,18 @@ def send_file(filepath_or_fp, mimetype=None, as_attachment=False, filename=None, rv.headers.pop('x-sendfile', None) return rv +def content_disposition(filename): + filename = openerp.tools.ustr(filename) + escaped = urllib2.quote(filename.encode('utf8')) + browser = request.httprequest.user_agent.browser + version = int((request.httprequest.user_agent.version or '0').split('.')[0]) + if browser == 'msie' and version < 9: + return "attachment; filename=%s" % escaped + elif browser == 'safari' and version < 537: + return u"attachment; filename=%s" % filename.encode('ascii', 'replace') + else: + return "attachment; filename*=UTF-8''%s" % escaped + #---------------------------------------------------------- # RPC controller #---------------------------------------------------------- diff --git a/openerp/tools/mimetypes.py b/openerp/tools/mimetypes.py index cf5a9debc90..581dcb27bd6 100644 --- a/openerp/tools/mimetypes.py +++ b/openerp/tools/mimetypes.py @@ -103,10 +103,11 @@ _Entry = collections.namedtuple('_Entry', ['mimetype', 'signatures', 'discrimina _mime_mappings = ( # pdf _Entry('application/pdf', ['%PDF'], []), - # jpg, jpeg, png, gif + # jpg, jpeg, png, gif, bmp _Entry('image/jpeg', ['\xFF\xD8\xFF\xE0', '\xFF\xD8\xFF\xE2', '\xFF\xD8\xFF\xE3', '\xFF\xD8\xFF\xE1'], []), _Entry('image/png', ['\x89PNG\r\n\x1A\n'], []), _Entry('image/gif', ['GIF87a', 'GIF89a'], []), + _Entry('image/bmp', ['BM'], []), # OLECF files in general (Word, Excel, PPT, default to word because why not?) _Entry('application/msword', ['\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1', '\x0D\x44\x4F\x43'], [ _check_olecf @@ -114,7 +115,7 @@ _mime_mappings = ( # zip, but will include jar, odt, ods, odp, docx, xlsx, pptx, apk _Entry('application/zip', ['PK\x03\x04'], [_check_ooxml, _check_open_container_format]), ) -def guess_mimetype(bin_data): +def guess_mimetype(bin_data, default='application/octet-stream'): """ Attempts to guess the mime type of the provided binary data, similar to but significantly more limited than libmagic @@ -140,7 +141,7 @@ def guess_mimetype(bin_data): # if no discriminant or no discriminant matches, return # primary mime type return entry.mimetype - return 'application/octet-stream' + return default try: