diff options
| author | Christophe Simonis <chs@odoo.com> | 2016-08-26 16:59:45 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2016-08-26 16:59:45 +0200 |
| commit | de0d503421f0eb7ee14e98f2a627d11fd79aa7fd (patch) | |
| tree | 1ac0adbeaf08ebbe13f969b940afbae3c9d581b2 | |
| parent | c8551260696f8979d67a5ae2d48d3cfca1128581 (diff) | |
| parent | f0c8224175159d4fad8f511ede16cec7824d58c4 (diff) | |
[MERGE] forward port branch saas-11 up to f0c8224
33 files changed, 313 insertions, 83 deletions
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 4f8b8e2b4bf..eaec3b34422 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -705,7 +705,7 @@ class AccountInvoice(models.Model): will be grouped together if the journal has the 'group line' option. Of course a module can add fields to invoice lines that would need to be tested too before merging lines or not.""" - return "%s-%s-%s-%s-%s" % ( + return "%s-%s-%s-%s-%s-%s" % ( invoice_line['account_id'], invoice_line.get('tax_line_id', 'False'), invoice_line.get('product_id', 'False'), @@ -1163,7 +1163,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 b4d5f7ec837..1c9c07ab107 100644 --- a/addons/account_asset/views/account_asset_view.xml +++ b/addons/account_asset/views/account_asset_view.xml @@ -131,11 +131,7 @@ <field name="value" widget="monetary" options="{'currency_field': 'currency_id'}" help="Gross value of asset"/> <field name="salvage_value" widget="monetary" options="{'currency_field': 'currency_id'}" attrs="{'invisible': [('type','=','sale')]}"/> <field name="value_residual" widget="monetary" options="{'currency_field': 'currency_id'}"/> - <div> - <label for="partner_id" string="Customer" attrs="{'invisible': [('type','==','purchase')]}"/> - <label for="partner_id" string="Vendor" attrs="{'invisible': [('type','!=','purchase')]}"/> - </div> - <field name="partner_id" string="Partner" nolabel="1"/> + <field name="partner_id" string="Vendor" domain="[('supplier', '=', True)]"/> <field name="invoice_id" string="Invoice" options="{'no_create': True}"/> </group> </group> diff --git a/addons/account_budget/account_budget.py b/addons/account_budget/account_budget.py index 727c7c0db89..d67a73565e6 100644 --- a/addons/account_budget/account_budget.py +++ b/addons/account_budget/account_budget.py @@ -95,7 +95,6 @@ class crossovered_budget_lines(osv.osv): def _prac_amt(self, cr, uid, ids, context=None): res = {} - result = 0.0 if context is None: context = {} for line in self.browse(cr, uid, ids, context=context): @@ -109,8 +108,10 @@ class crossovered_budget_lines(osv.osv): "between to_date(%s,'yyyy-mm-dd') AND to_date(%s,'yyyy-mm-dd')) AND " "general_account_id=ANY(%s)", (line.analytic_account_id.id, date_from, date_to,acc_ids,)) result = cr.fetchone()[0] - if result is None: - result = 0.00 + if result is None: + result = 0.0 + else: + result = 0.0 res[line.id] = result return res 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/views/hr_holidays_views.xml b/addons/hr_holidays/views/hr_holidays_views.xml index b4f6f65bb5f..dbcf30303fc 100644 --- a/addons/hr_holidays/views/hr_holidays_views.xml +++ b/addons/hr_holidays/views/hr_holidays_views.xml @@ -252,7 +252,7 @@ <field name="view_type">form</field> <field name="view_mode">calendar</field> <field name="context">{'search_default_year': 1, 'search_default_group_employee': 1}</field> - <field name="domain">[('holiday_type','=','employee'), ('type', '=', 'remove')]</field> + <field name="domain">[('holiday_type','=','employee'), ('type', '=', 'remove'), ('state', '!=', 'refuse')]</field> <field name="search_view_id" ref="view_hr_holidays_filter"/> </record> diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js index 34d687ba8dc..d37e9dcf282 100644 --- a/addons/mail/static/src/js/chat_manager.js +++ b/addons/mail/static/src/js/chat_manager.js @@ -214,6 +214,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; } @@ -654,7 +670,7 @@ var chat_manager = { // post a message in a channel or execute a command return ChannelModel.call(data.command ? 'execute_command' : 'message_post', [options.channel_id], _.extend(msg, { message_type: 'comment', - content_subtype: 'html', + content_subtype: 'plaintext', subtype: 'mail.mt_comment', command: data.command, })); diff --git a/addons/mail/static/src/js/thread.js b/addons/mail/static/src/js/thread.js index 17b21c39a5b..ea593d21dfa 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 e6d05648324..c3c43bcdd5e 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_id.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 a1fe670f56f..e85dd257cf2 100644 --- a/addons/payment_paypal/models/__init__.py +++ b/addons/payment_paypal/models/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- +import account_invoice import paypal 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/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index cc3c459dac8..071591ec7c7 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -223,7 +223,7 @@ class PosOrder(models.Model): }) if data_type == 'product': - key = ('product', values['partner_id'], (values['product_id'], values['name']), values['analytic_account_id'], values['debit'] > 0) + key = ('product', values['partner_id'], (values['product_id'], tuple(values['tax_ids'][0][2]), values['name']), values['analytic_account_id'], values['debit'] > 0) elif data_type == 'tax': key = ('tax', values['partner_id'], values['tax_line_id'], values['debit'] > 0) elif data_type == 'counter_part': @@ -287,7 +287,7 @@ class PosOrder(models.Model): 'analytic_account_id': self._prepare_analytic_account(line), 'credit': ((amount > 0) and amount) or 0.0, 'debit': ((amount < 0) and -amount) or 0.0, - 'tax_ids': [(6, 0, line.tax_ids.ids)], + 'tax_ids': [(6, 0, line.tax_ids_after_fiscal_position.ids)], 'partner_id': partner_id }) 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/views/stock_pack_operation_views.xml b/addons/stock/views/stock_pack_operation_views.xml index 3a0aab844e2..5f7e46850e6 100644 --- a/addons/stock/views/stock_pack_operation_views.xml +++ b/addons/stock/views/stock_pack_operation_views.xml @@ -25,8 +25,8 @@ <field name="location_dest_id"/> </group> <group string="Packages" groups="stock.group_tracking_lot"> - <field name="package_id" attrs="{'required': [('product_id', '=', False)]}" context="{'location_id': location_id}"/> - <field name="result_package_id" context="{'location_id': location_dest_id}"/> + <field name="package_id" attrs="{'required': [('product_id', '=', False)]}" domain="[('location_id', '=', location_id)]" context="{'location_id': location_id}"/> + <field name="result_package_id" domain="[('location_id', '=', location_dest_id)]" context="{'location_id': location_dest_id}"/> </group> </group> </form> @@ -115,4 +115,4 @@ </form> </field> </record> -</odoo>
\ No newline at end of file +</odoo> diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml index cebdddcbb99..7947f60b14b 100644 --- a/addons/stock/views/stock_picking_views.xml +++ b/addons/stock/views/stock_picking_views.xml @@ -320,7 +320,7 @@ <field name="picking_type_entire_packs" invisible="1"/> <field name="pack_operation_pack_ids" options="{'reload_on_button': True}" attrs="{'invisible': [('pack_operation_pack_ids', '=', []), ('picking_type_entire_packs', '=', False)]}" context="{'default_picking_id': id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id, 'default_picking_id': id}" groups="stock.group_tracking_lot"> <tree editable="bottom" decoration-muted="qty_done>0"> - <field name="package_id" required="1" string="Package To Move"/> + <field name="package_id" required="1" string="Package To Move" domain="[('location_id', '=', location_id)]"/> <field name="location_id" domain="[('id', 'child_of', parent.location_id)]" invisible="1"/> <field name="from_loc" groups="stock.group_stock_multi_locations,stock.group_tracking_lot"/> <field name="to_loc" groups="stock.group_stock_multi_locations,stock.group_tracking_lot"/> diff --git a/addons/stock_account/stock_account.py b/addons/stock_account/stock_account.py index 798e8fdda5d..1b59afdc74b 100644 --- a/addons/stock_account/stock_account.py +++ b/addons/stock_account/stock_account.py @@ -309,7 +309,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: @@ -380,12 +384,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 aa4900e25b6..f991d5eca07 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -33,7 +33,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__) @@ -412,10 +412,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, @@ -915,6 +911,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/<string:xmlid>', '/web/content/<string:xmlid>/<string:filename>', @@ -973,8 +974,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/views/website_templates.xml b/addons/website/views/website_templates.xml index b79ac75eed5..b28c06aa68f 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -913,9 +913,8 @@ Sitemap: <t t-esc="url_root"/>sitemap.xml "widget": "contact", "fields": ["name", "address", "phone", "mobile", "fax", "email"]}'/> </address> - <a t-att-href="res_company.google_map_link()" target="_BLANK"> - <img class="thumbnail img-responsive" t-att-src="res_company.google_map_img()" /> - </a> + <!-- FORWARD-PORT UP TO SAAS-12 --> + <a t-att-href="res_company.google_map_link()" target="_BLANK">Visualize on Google Maps</a> </template> <template id="contactus" name="Contact us" page="True"> diff --git a/addons/website_blog/controllers/main.py b/addons/website_blog/controllers/main.py index b7f85a12fc9..0aedea2352f 100644 --- a/addons/website_blog/controllers/main.py +++ b/addons/website_blog/controllers/main.py @@ -124,10 +124,11 @@ class WebsiteBlog(http.Controller): blog_post_ids = blog_post_obj.search(cr, uid, domain, order="published_date desc, create_date desc", context=context) 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_event/views/website_event_templates.xml b/addons/website_event/views/website_event_templates.xml index 7d1e8932fde..151affec228 100644 --- a/addons/website_event/views/website_event_templates.xml +++ b/addons/website_event/views/website_event_templates.xml @@ -334,9 +334,8 @@ <h4>Where</h4> </div> <div class="panel-body"> - <a t-att-href="event.google_map_link()" target="_BLANK"> - <img t-att-src="event.google_map_img()" width="100%%"/> - </a> + <!-- FORWARD-PORT UP TO SAAS-12 --> + <a t-att-href="event.google_map_link()" target="_BLANK">Visualize on Google Maps</a> <div itemprop="location" class="mt16 mb8" t-field="event.address_id" t-options='{ "widget": "contact", "fields": ["address", "phone", "mobile", "fax", "email"] @@ -457,8 +456,7 @@ <strong> Ticket Type #<t t-raw="counter_type"/>: <t t-esc="ticket['name']"/> ( <t t-if="ticket['price'] > 0"> - <t t-esc="ticket['price']"/> - <t t-esc="website.pricelist_id.currency_id.symbol"/> + <t t-esc="ticket['price']" t-esc-options='{"widget": "monetary", "display_currency": "website.pricelist_id.currency_id"}'/> </t> <t t-if="ticket['price'] == 0">Free</t>) </strong> diff --git a/addons/website_portal/controllers/main.py b/addons/website_portal/controllers/main.py index 4f7abc91413..0d5c60ba86d 100644 --- a/addons/website_portal/controllers/main.py +++ b/addons/website_portal/controllers/main.py @@ -64,6 +64,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 343e737a734..2afd5e40546 100644 --- a/addons/website_quote/views/website_quotation.xml +++ b/addons/website_quote/views/website_quotation.xml @@ -451,7 +451,7 @@ <small t-field="quotation.state"/> <div t-ignore="true" class="pull-right css_editable_mode_hidden hidden-print"> <ul class="list-inline"> - <li groups="base.group_website_publisher"> + <li groups="base.group_sale_salesman"> <a t-if="quotation.state not in ('manual')" class="btn btn-info" t-att-href="'/web#return_label=Website&model=%s&id=%s&action=%s&view_type=form' % (quotation._name, quotation.id, action)">Edit Quote</a> <a t-if="quotation.state in ('manual')" class=" btn btn-info" t-att-href="'/web#return_label=Website&model=%s&id=%s&action=%s&view_type=form' % (quotation._name, quotation.id, action)">Back to Sale Order</a> </li> 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 6e6cb250312..4907ce131a7 100644 --- a/openerp/addons/base/ir/ir_http.py +++ b/openerp/addons/base/ir/ir_http.py @@ -21,7 +21,8 @@ import werkzeug.utils import odoo from odoo import api, http, models, tools, SUPERUSER_ID from odoo.exceptions import AccessDenied, AccessError -from odoo.http import request, STATIC_CACHE +from odoo.http import request, STATIC_CACHE, content_disposition +from odoo.tools.mimetypes import guess_mimetype from odoo.modules.module import get_resource_path, get_module_path _logger = logging.getLogger(__name__) @@ -210,16 +211,7 @@ class IrHttp(models.AbstractModel): return self._routing_map def content_disposition(self, filename): - filename = 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 @@ -305,7 +297,8 @@ class IrHttp(models.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 @@ -318,7 +311,6 @@ class IrHttp(models.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/res/res_currency_view.xml b/openerp/addons/base/res/res_currency_view.xml index c272ec67d1d..decc23c63a6 100644 --- a/openerp/addons/base/res/res_currency_view.xml +++ b/openerp/addons/base/res/res_currency_view.xml @@ -146,7 +146,6 @@ <field name="view_mode">tree,form</field> <field name="search_view_id" ref="view_currency_search"/> <field name="context">{'active_test': False}</field> - <field name="target">new</field> </record> <!-- We override the view res_company here because the action wasn't defined before --> diff --git a/openerp/addons/base/res/res_users.py b/openerp/addons/base/res/res_users.py index c4849cc1fd0..4a2f44034d4 100644 --- a/openerp/addons/base/res/res_users.py +++ b/openerp/addons/base/res/res_users.py @@ -683,8 +683,7 @@ class GroupsView(models.Model): @api.multi def write(self, values): res = super(GroupsView, self).write(values) - if 'category_id' in values: - self._update_user_groups_view() + self._update_user_groups_view() # ir_values.get_actions() depends on action records self.env['ir.values'].clear_caches() return res diff --git a/openerp/addons/base/tests/__init__.py b/openerp/addons/base/tests/__init__.py index fbb2375f3c1..06c06cbbc5d 100644 --- a/openerp/addons/base/tests/__init__.py +++ b/openerp/addons/base/tests/__init__.py @@ -8,12 +8,14 @@ import test_float 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 @@ -23,7 +25,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/http.py b/openerp/http.py index d837131d52d..45db589aa28 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 @@ -1809,6 +1810,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: |
