[MERGE] forward port of branch 9.0 up to 27058c4

This commit is contained in:
Christophe Simonis
2016-02-17 17:48:03 +01:00
56 changed files with 615 additions and 232 deletions
+4 -4
View File
@@ -1,7 +1,7 @@
[![Build Status](http://runbot.odoo.com/runbot/badge/flat/1/master.svg)](http://runbot.odoo.com/runbot)
[![Tech Doc](http://img.shields.io/badge/master-docs-8f8f8f.svg?style=flat)](http://www.odoo.com/documentation/master)
[![Help](http://img.shields.io/badge/master-help-8f8f8f.svg?style=flat)](https://www.odoo.com/forum/help-1)
[![Nightly Builds](http://img.shields.io/badge/master-nightly-8f8f8f.svg?style=flat)](http://nightly.odoo.com/)
[![Build Status](http://runbot.odoo.com/runbot/badge/flat/1/9.0.svg)](http://runbot.odoo.com/runbot)
[![Tech Doc](http://img.shields.io/badge/9.0-docs-8f8f8f.svg?style=flat)](http://www.odoo.com/documentation/9.0)
[![Help](http://img.shields.io/badge/9.0-help-8f8f8f.svg?style=flat)](https://www.odoo.com/forum/help-1)
[![Nightly Builds](http://img.shields.io/badge/9.0-nightly-8f8f8f.svg?style=flat)](http://nightly.odoo.com/)
Odoo
----
+16 -17
View File
@@ -653,25 +653,24 @@ class AccountTax(models.Model):
else:
tax_amount = currency.round(tax_amount)
if tax_amount:
if tax.price_include:
total_excluded -= tax_amount
base -= tax_amount
else:
total_included += tax_amount
if tax.price_include:
total_excluded -= tax_amount
base -= tax_amount
else:
total_included += tax_amount
if tax.include_base_amount:
base += tax_amount
if tax.include_base_amount:
base += tax_amount
taxes.append({
'id': tax.id,
'name': tax.name,
'amount': tax_amount,
'sequence': tax.sequence,
'account_id': tax.account_id.id,
'refund_account_id': tax.refund_account_id.id,
'analytic': tax.analytic,
})
taxes.append({
'id': tax.id,
'name': tax.name,
'amount': tax_amount,
'sequence': tax.sequence,
'account_id': tax.account_id.id,
'refund_account_id': tax.refund_account_id.id,
'analytic': tax.analytic,
})
return {
'taxes': sorted(taxes, key=lambda k: k['sequence']),
+11 -10
View File
@@ -623,16 +623,17 @@ class AccountInvoice(models.Model):
def tax_line_move_line_get(self):
res = []
for tax_line in self.tax_line_ids:
res.append({
'tax_line_id': tax_line.tax_id.id,
'type': 'tax',
'name': tax_line.name,
'price_unit': tax_line.amount,
'quantity': 1,
'price': tax_line.amount,
'account_id': tax_line.account_id.id,
'account_analytic_id': tax_line.account_analytic_id.id,
})
if tax_line.amount:
res.append({
'tax_line_id': tax_line.tax_id.id,
'type': 'tax',
'name': tax_line.name,
'price_unit': tax_line.amount,
'quantity': 1,
'price': tax_line.amount,
'account_id': tax_line.account_id.id,
'account_analytic_id': tax_line.account_analytic_id.id,
})
return res
def inv_line_characteristic_hashcode(self, invoice_line):
+43 -19
View File
@@ -535,7 +535,7 @@ class AccountMoveLine(models.Model):
# Return lines formatted
if len(pairs) > 0:
target_currency = self.currency_id or self.company_id.currency_id
target_currency = (self.currency_id and self.amount_currency) and self.currency_id or self.company_id.currency_id
lines = self.browse(list(pairs[0]))
return lines.prepare_move_lines_for_reconciliation_widget(target_currency=target_currency)
return []
@@ -630,7 +630,7 @@ class AccountMoveLine(models.Model):
'journal_name': line.journal_id.name,
'partner_id': line.partner_id.id,
'partner_name': line.partner_id.name,
'currency_id': line.currency_id.id or False,
'currency_id': (line.currency_id and line.amount_currency) and line.currency_id.id or False,
}
debit = line.debit
@@ -645,7 +645,7 @@ class AccountMoveLine(models.Model):
# Get right debit / credit:
target_currency = target_currency or company_currency
line_currency = line.currency_id or company_currency
line_currency = (line.currency_id and line.amount_currency) and line.currency_id or company_currency
amount_currency_str = ""
total_amount_currency_str = ""
if line_currency != company_currency:
@@ -976,20 +976,21 @@ class AccountMoveLine(models.Model):
vals['amount_currency'] = self.env['res.currency'].browse(vals['currency_id']).round(vals['amount_currency'] * (amount / res['total_excluded']))
# Create tax lines
for tax_vals in res['taxes']:
account_id = (amount > 0 and tax_vals['account_id'] or tax_vals['refund_account_id'])
if not account_id: account_id = vals['account_id']
tax_lines_vals.append({
'account_id': account_id,
'name': vals['name'] + ' ' + tax_vals['name'],
'tax_line_id': tax_vals['id'],
'move_id': vals['move_id'],
'date': vals['date'],
'partner_id': vals.get('partner_id'),
'ref': vals.get('ref'),
'statement_id': vals.get('statement_id'),
'debit': tax_vals['amount'] > 0 and tax_vals['amount'] or 0.0,
'credit': tax_vals['amount'] < 0 and -tax_vals['amount'] or 0.0,
})
if tax_vals['amount']:
account_id = (amount > 0 and tax_vals['account_id'] or tax_vals['refund_account_id'])
if not account_id: account_id = vals['account_id']
tax_lines_vals.append({
'account_id': account_id,
'name': vals['name'] + ' ' + tax_vals['name'],
'tax_line_id': tax_vals['id'],
'move_id': vals['move_id'],
'date': vals['date'],
'partner_id': vals.get('partner_id'),
'ref': vals.get('ref'),
'statement_id': vals.get('statement_id'),
'debit': tax_vals['amount'] > 0 and tax_vals['amount'] or 0.0,
'credit': tax_vals['amount'] < 0 and -tax_vals['amount'] or 0.0,
})
new_line = super(AccountMoveLine, self).create(vals)
for tax_line_vals in tax_lines_vals:
@@ -1220,6 +1221,7 @@ class AccountPartialReconcile(models.Model):
'move_id': move.id,
'currency_id': rec.currency_id.id,
'amount_currency': 0.0,
'partner_id': rec.debit_move_id.partner_id.id,
})
rec.env['account.move.line'].create({
'name': _('Currency exchange rate difference'),
@@ -1228,6 +1230,7 @@ class AccountPartialReconcile(models.Model):
'account_id': amount_diff > 0 and rec.company_id.currency_exchange_journal_id.default_debit_account_id.id or rec.company_id.currency_exchange_journal_id.default_credit_account_id.id,
'move_id': move.id,
'currency_id': rec.currency_id.id,
'partner_id': rec.debit_move_id.partner_id.id,
})
rec.env['account.partial.reconcile'].create({
'debit_move_id': amount_diff < 0 and line_to_reconcile.id or rec.debit_move_id.id,
@@ -1249,7 +1252,28 @@ class AccountPartialReconcile(models.Model):
def unlink(self):
""" When removing a link between entries, we need to revert the eventual journal entries we created to book the
fluctuation of the foreign currency's exchange rate.
We need also to reconcile together the origin currency difference line and its reversal in order to completly
cancel the currency difference entry on the partner account (otherwise it will still appear on the aged balance
for example).
"""
exchange_rate_entries = self.env['account.move'].search([('rate_diff_partial_rec_id', 'in', self.ids)])
exchange_rate_entries.reverse_moves()
return super(AccountPartialReconcile, self).unlink()
# revert the currency difference entry
reversed_moves = exchange_rate_entries.reverse_moves()
# find the origin currency difference line on the partner account and its newly created reversal, and store them in a list
pairs_to_rec = []
for rev_move in self.env['account.move'].browse(reversed_moves):
if not rev_move.rate_diff_partial_rec_id:
continue
origin_move = exchange_rate_entries.filtered(lambda x: x.rate_diff_partial_rec_id == rev_move.rate_diff_partial_rec_id)
for acm_line in rev_move.line_ids:
if acm_line.account_id.reconcile:
for origin_line in origin_move.line_ids:
if origin_line.account_id == acm_line.account_id and origin_line.debit == acm_line.credit and origin_line.credit == acm_line.debit:
to_rec = origin_line + acm_line
pairs_to_rec.append(to_rec)
# the call to super() had to be delayed in order to mark the move lines to reconcile together (to use 'rate_diff_partial_rec_id')
res = super(AccountPartialReconcile, self).unlink()
# now that the origin currency difference line is not reconciled anymore, we can reconcile it with its reversal entry to cancel it completly
for to_rec in pairs_to_rec:
to_rec.reconcile()
return res
+2
View File
@@ -77,6 +77,8 @@ class account_analytic_account(models.Model):
@api.model
def name_search(self, name='', args=None, operator='ilike', limit=100):
if operator not in ('ilike', 'like', '=', '=like', '=ilike'):
return super(account_analytic_account, self).name_search(name, args, operator, limit)
args = args or []
domain = ['|', ('code', operator, name), ('name', operator, name)]
partners = self.env['res.partner'].search([('name', operator, name)], limit=limit)
+25 -3
View File
@@ -6,6 +6,8 @@ import logging
import operator
import os
from openerp.tools.mimetypes import guess_mimetype
try:
from cStringIO import StringIO
except ImportError:
@@ -158,16 +160,36 @@ class ir_import(orm.TransientModel):
return fields
def _read_file(self, file_type, record, options):
# guess mimetype from file content
mimetype = guess_mimetype(record.file)
(file_extension, handler, req) = FILE_TYPE_DICT.get(mimetype, (None, None, None))
if handler:
try:
return getattr(self, '_read_' + file_extension)(record, options)
except Exception:
_logger.warn("Failed to read file '%s' (transient id %d) using guessed mimetype %s",
record.file_name or '<unknown>', record.id, mimetype)
# try reading with user-provided mimetype
(file_extension, handler, req) = FILE_TYPE_DICT.get(file_type, (None, None, None))
if handler:
return getattr(self, '_read_' + file_extension)(record, options)
try:
return getattr(self, '_read_' + file_extension)(record, options)
except Exception:
_logger.warn("Failed to read file '%s' (transient id %d) using user-provided mimetype %s",
record.file_name or '<unknown>', record.id, file_type)
# fallback on file extensions as mime types can be unreliable (e.g.
# software setting incorrect mime types, or non-installed software
# leading to browser not sending mime types)
if record.file_name:
p, ext = os.path.splitext(record.file_name)
if ext and EXTENSIONS.get(ext):
return getattr(self, '_read_' + ext[1:])(record, options)
if ext in EXTENSIONS:
try:
return getattr(self, '_read_' + ext[1:])(record, options)
except Exception:
_logger.warn("Failed to read file '%s' (transient id %s) using file extension",
record.file_name, record.id)
if req:
raise ImportError(_("Unable to load \"{extension}\" file: requires Python module \"{modname}\"").format(extension=file_extension, modname=req))
+3 -1
View File
@@ -3,7 +3,7 @@ import unittest
from openerp.tests.common import TransactionCase, can_import
from openerp.modules.module import get_module_resource
from openerp.tools import mute_logger
from .. import models
@@ -197,6 +197,7 @@ class test_preview(TransactionCase):
})
return Import, id
@mute_logger('openerp.addons.base_import.models')
def test_encoding(self):
Import, id = self.make_import()
result = Import.parse_preview(self.cr, self.uid, id, {
@@ -205,6 +206,7 @@ class test_preview(TransactionCase):
})
self.assertTrue('error' in result)
@mute_logger('openerp.addons.base_import.models')
def test_csv_errors(self):
Import, id = self.make_import()
+8 -10
View File
@@ -2,16 +2,14 @@
<openerp>
<data>
<act_window id="action_partner_deduplicate"
res_model="base.partner.merge.automatic.wizard"
src_model="res.partner"
target="new"
multi="True"
key2="client_action_multi"
view_mode="form"
name="Deduplicate Contact"
context="{'default_state': 'option'}"
/>
<record model="ir.actions.act_window" id="action_partner_deduplicate">
<field name="name">Deduplicate Contacts</field>
<field name="res_model">base.partner.merge.automatic.wizard</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="context">{'active_test': False}</field>
</record>
<record model='ir.ui.view' id='base_partner_merge_automatic_wizard_form'>
<field name='name'>base.partner.merge.automatic.wizard.form</field>
+23
View File
@@ -4,6 +4,22 @@
<menuitem id="menu_delivery" name="Delivery" parent="stock.menu_stock_config_settings" groups="stock.group_stock_manager" sequence="50"/>
<menuitem id="sale_menu_delivery" name="Delivery" parent="base.menu_sale_config" groups="base.group_sale_manager" sequence="12"/>
<record id="view_delivery_carrier_search" model="ir.ui.view">
<field name="name">delivery.carrier.search</field>
<field name="model">delivery.carrier</field>
<field name="arch" type="xml">
<search string="Delivery Carrier Search">
<field name="name" string="Carrier" />
<field name="partner_id" operator="child_of"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<group expand="0" string="Group By" >
<filter string="Price Computation" name="delivery_type" context="{'group_by':'delivery_type'}" />
</group>
</search>
</field>
</record>
<record id="view_delivery_carrier_tree" model="ir.ui.view">
<field name="name">delivery.carrier.tree</field>
<field name="model">delivery.carrier</field>
@@ -23,6 +39,13 @@
<field name="arch" type="xml">
<form string="Carrier">
<sheet>
<div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object"
class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button"
options='{"terminology": "archive"}'/>
</button>
</div>
<div class="oe_title" name="title">
<label for="name" string="Name" class="oe_edit_only"/>
<h1>
@@ -134,14 +134,16 @@
<field name="is_timesheet" invisible="1"/>
</tree>
<form string="Timesheet Activities">
<field name="date"/>
<field name="account_id"/>
<field name="name"/>
<field name="unit_amount" widget="float_time"/>
<field name="amount"/>
<field name="general_account_id"/>
<field name="user_id" required="1"/>
<field name="is_timesheet" invisible="1"/>
<group>
<field name="date"/>
<field name="account_id"/>
<field name="name"/>
<field name="unit_amount" widget="float_time"/>
<field name="amount"/>
<field name="general_account_id"/>
<field name="user_id" required="1"/>
<field name="is_timesheet" invisible="1"/>
</group>
</form>
</field>
</page>
@@ -21,7 +21,7 @@
<!-- Rule Category -->
<record id="base.main_company" model="res.company">
<field name="plafond_secu">2946</field>
<field name="plafond_secu">3218</field>
</record>
+26 -23
View File
@@ -7,6 +7,7 @@ import logging
from openerp import _, api, fields, models, SUPERUSER_ID
from openerp import tools
from openerp.exceptions import UserError, AccessError
from openerp.osv import expression
_logger = logging.getLogger(__name__)
@@ -147,34 +148,36 @@ class Message(models.Model):
#------------------------------------------------------
@api.model
def mark_all_as_read(self, channel_ids=None):
def mark_all_as_read(self, channel_ids=None, domain=None):
""" Remove all needactions of the current partner. If channel_ids is
given, restrict to messages written in one of those channels. """
partner_id = self.env.user.partner_id.id
# possibly horribly inefficient method:
# it does one db request for the search, and one for each message in
# the result set to remove the current user from the relation.
# domain = [('needaction_partner_ids', 'in', partner_id)]
# if channel_ids:
# domain += [('channel_ids', 'in', channel_ids)]
# unread_messages = self.search(domain)
# unread_messages.write({'needaction_partner_ids': [(3, partner_id)]})
if domain is None:
query = "DELETE FROM mail_message_res_partner_needaction_rel WHERE res_partner_id IN %s"
args = [(partner_id,)]
if channel_ids:
query += """
AND mail_message_id in
(SELECT mail_message_id
FROM mail_message_mail_channel_rel
WHERE mail_channel_id in %s)"""
args += [tuple(channel_ids)]
query += " RETURNING mail_message_id as id"
self._cr.execute(query, args)
self.invalidate_cache()
# a much faster way to do this is in pure sql:
query = "DELETE FROM mail_message_res_partner_needaction_rel WHERE res_partner_id IN %s"
args = [(partner_id,)]
if channel_ids:
query += """
AND mail_message_id in
(SELECT mail_message_id
FROM mail_message_mail_channel_rel
WHERE mail_channel_id in %s)"""
args += [tuple(channel_ids)]
query += " RETURNING mail_message_id as id"
self._cr.execute(query, args)
self.invalidate_cache()
ids = [m['id'] for m in self._cr.dictfetchall()]
else:
# not really efficient method: it does one db request for the
# search, and one for each message in the result set to remove the
# current user from the relation.
msg_domain = [('needaction_partner_ids', 'in', partner_id)]
if channel_ids:
msg_domain += [('channel_ids', 'in', channel_ids)]
unread_messages = self.search(expression.AND([msg_domain, domain]))
unread_messages.write({'needaction_partner_ids': [(3, partner_id)]})
ids = unread_messages.mapped('id')
ids = [m['id'] for m in self._cr.dictfetchall()]
notification = {'type': 'mark_as_read', 'message_ids': ids, 'channel_ids': channel_ids}
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), notification)
+9 -5
View File
@@ -148,7 +148,7 @@ function add_message (data, options) {
var msg = _.findWhere(messages, { id: data.id });
if (!msg) {
msg = make_message(data);
msg = chat_manager.make_message(data);
// Keep the array ordered by id when inserting the new message
messages.splice(_.sortedIndex(messages, msg, 'id'), 0, msg);
_.each(msg.channel_ids, function (channel_id) {
@@ -298,7 +298,7 @@ function add_channel (data, options) {
chat_manager.bus.trigger("channel_toggle_fold", channel);
}
} else {
channel = make_channel(data, options);
channel = chat_manager.make_channel(data, options);
channels.push(channel);
channels = _.sortBy(channels, function (channel) { return channel.name.toLowerCase(); });
if (!options.silent) {
@@ -677,6 +677,10 @@ function on_presence_notification (data) {
// Public interface
//----------------------------------------------------------------------------------
var chat_manager = {
// these two functions are exposed for extensibility purposes and shouldn't be called by other modules
make_message: make_message,
make_channel: make_channel,
post_message: function (data, options) {
options = options || {};
var msg = {
@@ -773,9 +777,9 @@ var chat_manager = {
return $.when();
}
},
mark_all_as_read: function (channel) {
if ((!channel && needaction_counter) || (channel && channel.needaction_counter)) {
return MessageModel.call('mark_all_as_read', channel ? [[channel.id]] : []);
mark_all_as_read: function (channel, domain) {
if ((channel.id === "channel_inbox" && needaction_counter) || (channel && channel.needaction_counter)) {
return MessageModel.call('mark_all_as_read', [], {channel_ids: channel.id !== "channel_inbox" ? [channel.id] : [], domain: domain});
}
return $.when();
},
+1 -1
View File
@@ -200,7 +200,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
self.$('.o_mail_chat_sidebar').slideToggle(200);
});
this.$buttons.on('click', '.o_mail_chat_button_mark_read', function () {
chat_manager.mark_all_as_read();
chat_manager.mark_all_as_read(self.channel, self.domain);
});
this.$buttons.on('click', '.o_mail_chat_button_unstar_all', chat_manager.unstar_all);
this.$buttons.on('click', '.o_mail_chat_button_new_message', this.on_click_new_message);
+17 -1
View File
@@ -2,12 +2,28 @@
import werkzeug
from openerp import http, SUPERUSER_ID
from openerp import http, SUPERUSER_ID, _
from openerp.http import request
class MassMailController(http.Controller):
@http.route(['/mail/mailing/<int:mailing_id>/unsubscribe'], type='http', website=True, auth='public')
def mailing(self, mailing_id, email=None, res_id=None, **post):
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
if mailing.exists():
res_ids = []
if mailing.mailing_model == 'mail.mass_mailing.contact':
contacts = request.env['mail.mass_mailing.contact'].sudo().search([
('email', '=', email),
('list_id', 'in', [mailing_list.id for mailing_list in mailing.contact_list_ids])
])
res_ids = contacts.ids
else:
res_ids = [res_id]
mailing.update_opt_out(mailing_id, email, res_ids, True)
return _('You have been unsubscribed successfully')
@http.route('/mail/track/<int:mail_id>/blank.gif', type='http', auth='none')
def track_mail_open(self, mail_id, **post):
""" Email tracking. """
+1
View File
@@ -1221,6 +1221,7 @@ class mrp_production(osv.osv):
move_id = stock_move.create(cr, uid, {
'name': production.name,
'date': production.date_planned,
'date_expected': production.date_planned,
'product_id': product.id,
'product_uom_qty': qty,
'product_uom': uom_id,
+10 -2
View File
@@ -264,7 +264,7 @@ class PaymentTxOgone(osv.Model):
'acquirer_id': tx.acquirer_id.id,
'acquirer_ref': alias
})
tx.partner_reference = alias
tx.write({'payment_method_id': ref})
return tx
@@ -294,7 +294,7 @@ class PaymentTxOgone(osv.Model):
'date_validate': datetime.datetime.strptime(data['TRXDATE'], '%m/%d/%y').strftime(DEFAULT_SERVER_DATE_FORMAT),
'acquirer_reference': data['PAYID'],
}
if data.get('ALIAS') and tx.partner_id and tx.type == 'form_save':
if data.get('ALIAS') and tx.partner_id and tx.type == 'form_save' and not tx.payment_method_id:
pm_id = self.pool['payment.method'].create(cr, uid, {
'partner_id': tx.partner_id.id,
'acquirer_id': tx.acquirer_id.id,
@@ -399,6 +399,14 @@ class PaymentTxOgone(osv.Model):
'date_validate': datetime.date.today().strftime(DEFAULT_SERVER_DATE_FORMAT),
'acquirer_reference': tree.get('PAYID'),
})
if tree.get('ALIAS') and tx.partner_id and tx.type == 'form_save' and not tx.payment_method_id:
pm = tx.env['payment.method'].create({
'partner_id': tx.partner_id.id,
'acquirer_id': tx.acquirer_id.id,
'acquirer_ref': tree.get('ALIAS'),
'name': tree.get('CARDNO'),
})
tx.write({'payment_method_id': pm.id})
if tx.callback_eval:
safe_eval(tx.callback_eval, {'self': tx})
return True
@@ -1961,6 +1961,7 @@ return {
NumpadWidget: NumpadWidget,
ProductScreenWidget: ProductScreenWidget,
ProductListWidget: ProductListWidget,
ClientListScreenWidget: ClientListScreenWidget,
};
});
+1 -1
View File
@@ -299,7 +299,7 @@ class product_pricelist_item(osv.osv):
'base': fields.selection([('list_price', 'Public Price'), ('standard_price', 'Cost'), ('pricelist', 'Other Pricelist')], string="Based on", required=True,
help='Base price for computation. \n Public Price: The base price will be the Sale/public Price. \n Cost Price : The base price will be the cost price. \n Other Pricelist : Computation of the base price based on another Pricelist.'),
'base_pricelist_id': fields.many2one('product.pricelist', 'Other Pricelist'),
'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', ondelete='cascade'),
'price_surcharge': fields.float('Price Surcharge',
digits_compute= dp.get_precision('Product Price'), help='Specify the fixed amount to add or substract(if negative) to the amount calculated with the discount.'),
'price_discount': fields.float('Price Discount', digits=(16,2)),
@@ -20,7 +20,7 @@ class sale_order_line(osv.osv):
"""Retrieve the price before applying the pricelist"""
item_obj = self.pool['product.pricelist.item']
product_obj = self.pool['product.product']
field_name = 'list_price'
field_name = 'lst_price'
currency_id = None
if res_dict.get(pricelist):
rule_id = res_dict[pricelist][1]
+1 -1
View File
@@ -102,7 +102,7 @@ class PurchaseOrder(models.Model):
"It's used to do the matching when you receive the "
"products as this reference is usually written on the "
"delivery order sent by your vendor.")
date_order = fields.Datetime('Order Date', required=True, states=READONLY_STATES, select=True, copy=False, default=fields.Datetime.now(),\
date_order = fields.Datetime('Order Date', required=True, states=READONLY_STATES, select=True, copy=False, default=fields.Datetime.now,\
help="Depicts the date where the Quotation should be validated and converted into a purchase order.")
date_approve = fields.Date('Approval Date', readonly=1, select=True, copy=False)
partner_id = fields.Many2one('res.partner', string='Vendor', required=True, states=READONLY_STATES, change_default=True, track_visibility='always')
+1 -1
View File
@@ -146,7 +146,7 @@
<button name="button_confirm" type="object" states="sent" string="Confirm Order" class="oe_highlight" id="bid_confirm"/>
<button name="button_approve" type="object" states='to approve' string="Approve Order" class="oe_highlight" groups="purchase.group_purchase_manager"/>
<button name="action_rfq_send" states="purchase" string="Send PO by Email" type="object" context="{'send_rfq':False}"/>
<button name="action_view_picking" string="Receive Products" type="object" attrs="{'invisible': [('state','not in', ('purchase','done'))]}"/>
<button name="action_view_picking" string="Receive Products" type="object" attrs="{'invisible': ['|', ('state','not in', ('purchase','done')), ('picking_count', '=', 0)]}"/>
<button name="button_draft" states="cancel" string="Set to Draft" type="object" />
<button name="button_cancel" states="draft,to approve,sent,purchase" string="Cancel" type="object" />
<button name="button_done" type="object" string="Set to Done" states="purchase"
+3 -8
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
from datetime import timedelta
from dateutil import rrule
from dateutil.relativedelta import relativedelta
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
@@ -330,14 +331,8 @@ class resource_calendar(osv.osv):
working_intervals = []
tz_info = fields.datetime.context_timestamp(cr, uid, work_dt, context=context).tzinfo
for calendar_working_day in self.get_attendances_for_weekday(cr, uid, id, start_dt, context=context):
if context and context.get('no_round_hours'):
min_from = int((calendar_working_day.hour_from - int(calendar_working_day.hour_from)) * 60)
min_to = int((calendar_working_day.hour_to - int(calendar_working_day.hour_to)) * 60)
dt_f = work_dt.replace(hour=int(calendar_working_day.hour_from), minute=min_from)
dt_t = work_dt.replace(hour=int(calendar_working_day.hour_to), minute=min_to)
else:
dt_f = work_dt.replace(hour=int(calendar_working_day.hour_from))
dt_t = work_dt.replace(hour=int(calendar_working_day.hour_to))
dt_f = work_dt.replace(hour=0, minute=0, second=0) + timedelta(seconds=(calendar_working_day.hour_from * 3600))
dt_t = work_dt.replace(hour=0, minute=0, second=0) + timedelta(seconds=(calendar_working_day.hour_to * 3600))
# adapt tz
working_interval = (
+29
View File
@@ -405,6 +405,35 @@ class TestResource(TestResourceCommon):
default_interval=(8, 16), context=context)
self.assertEqual(res, 32.0, 'resource_calendar: wrong get_working_hours computation')
self.att0_0_id = self.resource_attendance.create(
cr, uid, {
'name': 'Att0',
'dayofweek': '0',
'hour_from': 7.5,
'hour_to': 12.5,
'calendar_id': self.calendar_id,
}, context=context
)
self.att0_1_id = self.resource_attendance.create(
cr, uid, {
'name': 'Att0',
'dayofweek': '0',
'hour_from': 13,
'hour_to': 14,
'calendar_id': self.calendar_id,
}, context=context
)
date1 = datetime.strptime('2013-02-11 07:30:00', '%Y-%m-%d %H:%M:%S')
date2 = datetime.strptime('2013-02-11 14:00:00', '%Y-%m-%d %H:%M:%S')
res = self.resource_calendar.get_working_hours(
cr, uid, self.calendar_id,
date1,
date2,
compute_leaves=False, resource_id=self.resource1_id, context=context)
# 7h30 -> 12h30 = 5 / 13h -> 14h = 1 / -> 6h
self.assertEqual(res, 6, 'resource_calendar: wrong get_working_hours computation')
def test_50_calendar_schedule_days(self):
""" Testing calendar days scheduling """
cr, uid = self.cr, self.uid
+2 -2
View File
@@ -136,7 +136,7 @@ class SaleOrder(models.Model):
payment_term_id = fields.Many2one('account.payment.term', string='Payment Term', oldname='payment_term')
fiscal_position_id = fields.Many2one('account.fiscal.position', oldname='fiscal_position', string='Fiscal Position')
company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get('sale.order'))
team_id = fields.Many2one('crm.team', 'Sales Team', change_default=True, default=_get_default_team)
team_id = fields.Many2one('crm.team', 'Sales Team', change_default=True, default=_get_default_team, oldname='section_id')
procurement_group_id = fields.Many2one('procurement.group', 'Procurement Group', copy=False)
product_id = fields.Many2one('product.product', related='order_line.product_id', string='Product')
@@ -797,7 +797,7 @@ class AccountInvoice(models.Model):
def _get_default_team(self):
return self.env['crm.team']._get_default_team_id()
team_id = fields.Many2one('crm.team', string='Sales Team', default=_get_default_team)
team_id = fields.Many2one('crm.team', string='Sales Team', default=_get_default_team, oldname='section_id')
@api.multi
def confirm_paid(self):
@@ -128,7 +128,7 @@ class stock_history(osv.osv):
product_template_id,
SUM(quantity) as quantity,
date,
price_unit_on_quant,
SUM(price_unit_on_quant * quantity) / SUM(quantity) as price_unit_on_quant,
source,
serial_number
FROM
@@ -205,5 +205,5 @@ class stock_history(osv.osv):
dest_location.usage not in ('internal', 'transit'))
))
AS foo
GROUP BY move_id, location_id, company_id, product_id, product_categ_id, date, price_unit_on_quant, source, product_template_id, serial_number
GROUP BY move_id, location_id, company_id, product_id, product_categ_id, date, source, product_template_id, serial_number
)""")
+1 -1
View File
@@ -468,7 +468,7 @@ function autoresize ($textarea, options) {
var $fixed_text_area;
var min_height = (options && options.min_height) || 50;
if (!$fixed_text_area) {
$fixed_text_area = $('<textarea>').css({
$fixed_text_area = $('<textarea disabled>').css({
position: 'absolute',
opacity: 0,
height: 10,
@@ -1,23 +1,24 @@
# -*- coding: utf-8 -*-
from openerp import http, SUPERUSER_ID
from openerp.addons.mass_mailing.controllers.main import MassMailController
from openerp.http import request
class MassMailController(http.Controller):
class MassMailController(MassMailController):
@http.route(['/mail/mailing/<int:mailing_id>/unsubscribe'], type='http', website=True, auth='public')
def mailing(self, mailing_id, email=None, res_id=None, **post):
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
if mailing.exists():
if mailing.mailing_model == 'mail.mass_mailing.contact':
contacts = request.env['mail.mass_mailing.contact'].sudo().search([('email', 'ilike', email)])
contacts = request.env['mail.mass_mailing.contact'].sudo().search([('email', '=', email)])
return request.website.render('website_mass_mailing.page_unsubscribe', {
'contacts': contacts,
'email': email,
'mailing_id': mailing_id})
else:
mailing.update_opt_out(mailing_id, email, [res_id], True)
super(MassMailController, self).mailing(mailing_id, email=email, res_id=res_id, **post)
return request.website.render('website_mass_mailing.page_unsubscribed')
@http.route(['/mail/mailing/unsubscribe'], type='json', auth='none')
+2 -2
View File
@@ -277,9 +277,9 @@ class sale_order(osv.osv):
# create draft invoice if transaction is ok
if tx and tx.state == 'done':
if order.state in ['draft', 'sent']:
self.signal_workflow(cr, SUPERUSER_ID, [order.id], 'manual_invoice', context=context)
self.action_confirm(cr, SUPERUSER_ID, order.id, context=context)
message = _('Order payed by %s. Transaction: %s. Amount: %s.') % (tx.partner_id.name, tx.acquirer_reference, tx.amount)
self.message_post(cr, uid, order_id, body=message, type='comment', subtype='mt_comment', context=context)
self.message_post(cr, uid, order_id, body=message, context=context)
return True
return False
+1
View File
@@ -11,3 +11,4 @@ Yoshi Tashiro tashiro@roomsfor.hk https://github.com/yostashiro
List of contributors:
Yoshi Tashiro tashiro@roomsfor.hk https://github.com/yostashiro
Manami Hashi manami@roomsfor.hk https://github.com/manamirfhk
+1 -1
View File
@@ -8,4 +8,4 @@ declaration.
Signed,
Alejandro Santana tech@alejandrosantana.eu https://github.com/alejandrosantana
Alejandro Santana hi@alejandrosantana.eu https://github.com/alejandrosantana
+11
View File
@@ -0,0 +1,11 @@
France, 2016-02-11
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
MORIN Fabien fabien.morin@gmail.com https://github.com/fabienmorin
+11
View File
@@ -0,0 +1,11 @@
Portugal, 2016-01-30
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Kevin Chappell kevin.b.chappell@gmail.com https://github.com/kevinchappell
+5 -5
View File
@@ -1128,11 +1128,11 @@ their root element is ``<gantt>``.
.. code-block:: xml
<gantt string="Ideas" date_start="invent_date" color="inventor_id">
<level object="idea.idea" link="id" domain="[]">
<field name="inventor_id"/>
</level>
</gantt>
<gantt string="Ideas"
date_start="invent_date"
date_stop="date_finished"
progress="progress"
default_group_by="inventor_id" />
.. exercise:: Gantt charts
+1 -1
View File
@@ -208,7 +208,7 @@ field:
<field name="name">Res Partner Server Action</field>
<field name="model_id" ref="model_res_partner"/>
<field name="code">
print self, object
raise Warning(object.name)
</field>
</record>
-7
View File
@@ -280,13 +280,6 @@ customize the amout of logging output
In case of conflict between :option:`--log-level` and
:option:`--log-handler`, the latter is used
Advanced options
----------------
.. option:: --auto-reload
enable auto-reloading of python files and xml files without having to
restart the server. Requires pyinotify_.
.. _reference/cmdline/scaffold:
+27 -11
View File
@@ -862,6 +862,8 @@ Possible children of the view element are:
a :meth:`~openerp.models.Model.read` (except for date and datetime fields
that are `formatted according to user's locale
<https://github.com/odoo/odoo/blob/a678bd4e/addons/web_kanban/static/src/js/kanban_record.js#L102>`_)
``formats``
the :js:class:`web.formats` module to manipulate and convert values
``read_only_mode``
self-explanatory
@@ -1013,26 +1015,40 @@ take the following attributes:
and the end date will be set to the start date
``date_delay``
name of the field providing the duration of the event
``progress``
name of a field providing the completion percentage for the record's event,
between 0 and 100
``duration_unit``
one of ``minute``, ``hour`` (default), ``day``, ``week``, ``month``, ``year``
``default_group_by``
name of a field to group tasks by
``fold_last_level``
If a value is set, the last grouping level is folded
``round_dnd_dates``
enables rounding the task's start and end dates to the nearest scale marks
``type``
``gantt`` classic gantt view (default)
``consolidate`` values of the first children are consolidated in the gantt's task
``planning`` children are displayed in the gantt's task
``consolidation``
field name to display consolidation value in record cell
``consolidation_max``
dictionary with the "group by" field as key and the maximum consolidation
value that can be reached before displaying the cell in red
(e.g. ``{"user_id": 100}``)
.. warning::
The dictionnary definition must use double-quotes, ``{'user_id': 100}`` is
not a valid value
``consolidation_label``
string to display next to the consolidation value, if not specified, the label
of the consolidation field will be used
``fold_last_level``
If a value is set, the last grouping level is folded
``round_dnd_dates``
enables rounding the task's start and end dates to the nearest scale marks
.. ``progress``
name of a field providing the completion percentage for the record's event,
between 0 and 100
.. consolidation_exclude
.. consolidation_color
.. ``type``
``gantt`` classic gantt view (default)
``consolidate`` values of the first children are consolidated in the gantt's task
``planning`` children are displayed in the gantt's task
.. _reference/views/diagram:
+5 -3
View File
@@ -86,6 +86,7 @@ instance, here is a simple sequence of two activities defined in XML
<record id="trans_a_b" model="workflow.transition">
<field name="act_from" ref="activity_a"/>
<field name="act_to" ref="activity_b"/>
<field name="signal">signal_goto_b</signal>
</record>
A worfklow is always defined with respect to a particular model (the model is
@@ -107,9 +108,10 @@ is of kind ``function``, which means that the action ``print_a()`` is a method
call on the model ``test.workflow`` (the usual ``cr, uid, ids, context``
arguments are passed for you).
The transition between "a" and "b" does not specify any condition. This means
that the workflow instance immediately goes from "a" to "b" after "a" has been
processed, and thus also processes activity "b".
The transition between "a" and "b" specify a signal but not any condition. This
means the workflow instance will immediately transition from "a" to "b" when the
signal ``signal_goto_b`` is recieved and thus processes activity "b"'s action
``print_b()``.
Activities
----------
+5 -52
View File
@@ -8,61 +8,15 @@ import mimetypes
import os
import re
from openerp import tools
from openerp.tools.translate import _
from openerp import SUPERUSER_ID, tools
from openerp.exceptions import AccessError
from openerp.osv import fields,osv
from openerp import SUPERUSER_ID
from openerp.exceptions import UserError
from openerp.tools.translate import _
from openerp.tools.misc import ustr
from openerp.tools.mimetypes import guess_mimetype
_logger = logging.getLogger(__name__)
try:
import magic
except ImportError:
magic = None
# We define our own guess_mimetype implementation and if magic is available we
# use it instead.
# However there are 2 python libs named 'magic' with incompatible api.
# magic from pypi https://pypi.python.org/pypi/python-magic/
# magic from file(1) https://packages.debian.org/squeeze/python-magic
def guess_mimetype(bin_data):
# by default, guess the type using the magic number of file hex signature (like magic, but more limited)
# see http://www.filesignatures.net/ for file signatures
mapping = (
# pdf
('application/pdf', ['%PDF']),
# jpg, jpeg, png, gif
('image/jpeg', ['\xFF\xD8\xFF\xE0', '\xFF\xD8\xFF\xE2', '\xFF\xD8\xFF\xE3', '\xFF\xD8\xFF\xE1']),
('image/png', ['\x89\x50\x4E\x47\x0D\x0A\x1A\x0A']),
('image/gif', ['GIF87a', '\x47\x49\x46\x38\x37\x61', 'GIF89a', '\x47\x49\x46\x38\x39\x61']),
# docx, odt, doc, xls
('application/msword', ['PK\x03\x04', '\xCF\x11\xE0\xA1\xB1\x1A\xE1\x00', '\xEC\xA5\xC1\x00', '\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1', '\x0D\x44\x4F\x43']),
# zip, but will include jar, odt, ods, odp, docx, xlsx, pptx, apk
('application/zip', ['PK']),
('application/vnd.ms-excel', ['\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1', '\x09\x08\x10\x00\x00\x06\x05\x00', '\xFD\xFF\xFF\xFF\x10', '\xFD\xFF\xFF\xFF\x1F', '\xFD\xFF\xFF\xFF\x23', '\xFD\xFF\xFF\xFF\x28', '\xFD\xFF\xFF\xFF\x29']),
)
for mimetype, signatures in mapping:
for signature in signatures:
if bin_data.startswith(signature):
return mimetype
return 'application/octet-stream'
# if available adapt magic from pypi
if hasattr(magic,'from_buffer'):
guess_mimetype = lambda bin_data: magic.from_buffer(bin_data, mime=True)
# or if available adapt magic from file(1)
elif hasattr(magic,'open'):
ms = magic.open(magic.MAGIC_MIME_TYPE)
ms.load()
guess_mimetype = ms.buffer
class ir_attachment(osv.osv):
"""Attachments are used to link binary files or url to any openerp document.
@@ -292,7 +246,7 @@ class ir_attachment(osv.osv):
'file_size': fields.integer('File Size', readonly=True),
'checksum': fields.char("Checksum/SHA1", size=40, select=True, readonly=True),
'mimetype': fields.char('Mime Type', readonly=True),
'index_content': fields.text('Indexed Content', readonly=True),
'index_content': fields.text('Indexed Content', readonly=True, _prefetch=False),
'public': fields.boolean('Is public document'),
}
@@ -443,9 +397,8 @@ class ir_attachment(osv.osv):
# database allowed it. Helps avoid errors when concurrent transactions
# are deleting the same file, and some of the transactions are
# rolled back by PostgreSQL (due to concurrent updates detection).
to_delete = [a.store_fname
for a in self.browse(cr, uid, ids, context=context)
if a.store_fname]
to_delete = set([a.store_fname for a in self.browse(cr, uid, ids, context=context)
if a.store_fname])
res = super(ir_attachment, self).unlink(cr, uid, ids, context)
for file_path in to_delete:
self._file_delete(cr, uid, file_path)
+19 -12
View File
@@ -1339,21 +1339,28 @@ class AssetsBundle(object):
uid = openerp.SUPERUSER_ID
preprocessed = True
for atype in (SassStylesheetAsset, LessStylesheetAsset):
assets = [asset for asset in self.stylesheets if isinstance(asset, atype)]
outdated = False
assets = dict((asset.html_url % asset.url, asset) for asset in self.stylesheets if isinstance(asset, atype))
if assets:
assets_domain = [('name', 'in', [asset.html_url % asset.url for asset in assets])]
assets_domain = [('url', 'in', assets.keys())]
ira_ids = self.registry['ir.attachment'].search(self.cr, uid, assets_domain)
if len(ira_ids) != len(assets):
ira_records = self.registry['ir.attachment'].browse(self.cr, uid, ira_ids)
for ira_record in ira_records:
asset = assets[ira_record.url]
if asset.last_modified > Datetime.from_string(ira_record['__last_update']):
outdated = True
break
if asset._content is None:
asset._content = ira_record.datas and ira_record.datas.decode('base64').decode('utf8')
if any(asset._content is None for asset in assets.itervalues()):
outdated = True
if outdated:
if ira_ids:
self.registry['ir.attachment'].unlink(self.cr, uid, ira_ids)
preprocessed = False
else:
ira_records = self.registry['ir.attachment'].browse(self.cr, uid, ira_ids)
for ira_record in ira_records:
stylesheet = next(stylesheet for stylesheet in self.stylesheets if stylesheet.html_url % stylesheet.url == ira_record.url)
if stylesheet.last_modified > Datetime.from_string(ira_record['__last_update']):
self.registry['ir.attachment'].unlink(self.cr, self.uid, ira_ids)
preprocessed = False
break
stylesheet._content = ira_record.datas and ira_record.datas.decode('base64').decode('utf8')
return preprocessed
def preprocess_css(self, debug=False):
@@ -0,0 +1 @@
# -*- coding: utf-8 -*-
@@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
{
'name': 'test mimetypes-guessing',
'version': '0.1',
'category': 'Tests',
'description': """A module to generate exceptions.""",
}
@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import test_guess_mimetypes
@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
import os.path
import unittest
from openerp.tools.mimetypes import guess_mimetype
def contents(extension):
with open(os.path.join(
os.path.dirname(__file__),
'testfiles',
'case.{}'.format(extension)
), 'rb') as f:
return f.read()
class TestMimeGuessing(unittest.TestCase):
def test_doc(self):
self.assertEqual(
guess_mimetype(contents('doc')),
'application/msword'
)
def test_xls(self):
self.assertEqual(
guess_mimetype(contents('xls')),
'application/vnd.ms-excel'
)
def test_docx(self):
self.assertEqual(
guess_mimetype(contents('docx')),
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
)
def test_xlsx(self):
self.assertEqual(
guess_mimetype(contents('xlsx')),
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
def test_odt(self):
self.assertEqual(
guess_mimetype(contents('odt')),
'application/vnd.oasis.opendocument.text'
)
def test_ods(self):
self.assertEqual(
guess_mimetype(contents('ods')),
'application/vnd.oasis.opendocument.spreadsheet'
)
def test_zip(self):
self.assertEqual(
guess_mimetype(contents('zip')),
'application/zip'
)
def test_gif(self):
self.assertEqual(
guess_mimetype(contents('gif')),
'image/gif'
)
def test_jpeg(self):
self.assertEqual(
guess_mimetype(contents('jpg')),
'image/jpeg'
)
def test_unknown(self):
self.assertEqual(
guess_mimetype(contents('csv')),
'application/octet-stream'
)
@@ -0,0 +1,3 @@
a,b,c
1,2,3
4,5,6
1 a b c
2 1 2 3
3 4 5 6
Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

+30 -13
View File
@@ -13,6 +13,8 @@ from functools import wraps
import logging
import urlparse
import uuid
import psycopg2
import psycopg2.extras
import psycopg2.extensions
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT, ISOLATION_LEVEL_READ_COMMITTED, ISOLATION_LEVEL_REPEATABLE_READ
@@ -527,7 +529,11 @@ class ConnectionPool(object):
_logger.debug(('%r ' + msg), self, *args)
@locked
def borrow(self, dsn):
def borrow(self, connection_info):
"""
:param dict connection_info: dict of psql connection keywords
:rtype: PsycoConnection
"""
# free dead and leaked connections
for i, (cnx, _) in tools.reverse_enumerate(self._connections):
if cnx.closed:
@@ -541,7 +547,7 @@ class ConnectionPool(object):
_logger.info('%r: Free leaked connection to %r', self, cnx.dsn)
for i, (cnx, used) in enumerate(self._connections):
if not used and cnx._original_dsn == dsn:
if not used and cnx._original_dsn == connection_info:
try:
cnx.reset()
except psycopg2.OperationalError:
@@ -570,11 +576,13 @@ class ConnectionPool(object):
raise PoolError('The Connection Pool Is Full')
try:
result = psycopg2.connect(dsn=dsn, connection_factory=PsycoConnection)
result = psycopg2.connect(
connection_factory=PsycoConnection,
**connection_info)
except psycopg2.Error:
_logger.info('Connection to the database failed')
raise
result._original_dsn = dsn
result._original_dsn = connection_info
self._connections.append((result, True))
self._debug('Create new connection')
return result
@@ -639,8 +647,17 @@ class Connection(object):
except Exception:
return False
def dsn(db_or_uri):
"""parse the given `db_or_uri` and return a 2-tuple (dbname, uri)"""
def connection_info_for(db_or_uri):
""" parse the given `db_or_uri` and return a 2-tuple (dbname, connection_params)
Connection params are either a dictionary with a single key ``dsn``
containing a connection URI, or a dictionary containing connection
parameter keywords which psycopg2 can build a key/value connection string
(dsn) from
:param str db_or_uri: database name or postgres dsn
:rtype: (str, dict)
"""
if db_or_uri.startswith(('postgresql://', 'postgres://')):
# extract db from uri
us = urlparse.urlsplit(db_or_uri)
@@ -650,15 +667,15 @@ def dsn(db_or_uri):
db_name = us.username
else:
db_name = us.hostname
return db_name, db_or_uri
return db_name, {'dsn': db_or_uri}
_dsn = ''
connection_info = {'database': db_or_uri}
for p in ('host', 'port', 'user', 'password'):
cfg = tools.config['db_' + p]
if cfg:
_dsn += '%s=%s ' % (p, cfg)
connection_info[p] = cfg
return db_or_uri, '%sdbname=%s' % (_dsn, db_or_uri)
return db_or_uri, connection_info
_Pool = None
@@ -667,16 +684,16 @@ def db_connect(to, allow_uri=False):
if _Pool is None:
_Pool = ConnectionPool(int(tools.config['db_maxconn']))
db, uri = dsn(to)
db, info = connection_info_for(to)
if not allow_uri and db != to:
raise ValueError('URI connections not allowed')
return Connection(_Pool, db, uri)
return Connection(_Pool, db, info)
def close_db(db_name):
""" You might want to call openerp.modules.registry.RegistryManager.delete(db_name) along this function."""
global _Pool
if _Pool:
_Pool.close_all(dsn(db_name)[1])
_Pool.close_all(connection_info_for(db_name)[1])
def close_all():
global _Pool
+160
View File
@@ -0,0 +1,160 @@
# -*- coding: utf-8 -*-
"""
Mimetypes-related utilities
# TODO: reexport stdlib mimetypes?
"""
import collections
import io
import logging
import re
import zipfile
__all__ = ['guess_mimetype']
_logger = logging.getLogger(__name__)
# We define our own guess_mimetype implementation and if magic is available we
# use it instead.
# discriminants for zip-based file formats
_ooxml_dirs = {
'word/': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'pt/': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'xl/': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
}
def _check_ooxml(data):
with io.BytesIO(data) as f, zipfile.ZipFile(f) as z:
filenames = z.namelist()
# OOXML documents should have a [Content_Types].xml file for early
# check that we're interested in this thing at all
if '[Content_Types].xml' not in filenames:
return False
# then there is a directory whose name denotes the type of the file:
# word, pt (powerpoint) or xl (excel)
for dirname, mime in _ooxml_dirs.iteritems():
if any(entry.startswith(dirname) for entry in filenames):
return mime
return False
# checks that a string looks kinda sorta like a mimetype
_mime_validator = re.compile(r"""
[\w-]+ # type-name
/ # subtype separator
[\w-]+ # registration facet or subtype
(?:\.[\w-]+)* # optional faceted name
(?:\+[\w-]+)? # optional structured syntax specifier
""", re.VERBOSE)
def _check_open_container_format(data):
with io.BytesIO(data) as f, zipfile.ZipFile(f) as z:
# OCF zip files must contain a ``mimetype`` entry
if 'mimetype' not in z.namelist():
return False
# it holds the exact mimetype for the file
marcel = z.read('mimetype')
# check that it's not too long (RFC6838 § 4.2 restricts type and
# subtype to 127 characters each + separator, strongly recommends
# limiting them to 64 but does not require it) and that it looks a lot
# like a valid mime type
if len(marcel) < 256 and _mime_validator.match(marcel):
return marcel
return False
_xls_pattern = re.compile("""
\x09\x08\x10\x00\x00\x06\x05\x00
| \xFD\xFF\xFF\xFF(\x10|\x1F|\x20|"|\\#|\\(|\\))
""", re.VERBOSE)
_ppt_pattern = re.compile("""
\x00\x6E\x1E\xF0
| \x0F\x00\xE8\x03
| \xA0\x46\x1D\xF0
| \xFD\xFF\xFF\xFF(\x0E|\x1C|\x43)\x00\x00\x00
""", re.VERBOSE)
def _check_olecf(data):
""" Pre-OOXML Office formats are OLE Compound Files which all use the same
file signature ("magic bytes") and should have a subheader at offset 512
(0x200).
Subheaders taken from http://www.garykessler.net/library/file_sigs.html
according to which Mac office files *may* have different subheaders. We'll
ignore that.
"""
offset = 0x200
if data.startswith('\xEC\xA5\xC1\x00', offset):
return 'application/msword'
# the _xls_pattern stuff doesn't seem to work correctly (the test file
# only has a bunch of \xf* at offset 0x200), that apparently works
elif 'Microsoft Excel' in data:
return 'application/vnd.ms-excel'
elif _ppt_pattern.match(data, offset):
return 'application/vnd.ms-powerpoint'
return False
# for "master" formats with many subformats, discriminants is a list of
# functions, tried in order and the first non-falsy value returned is the
# selected mime type. If all functions return falsy values, the master
# mimetype is returned.
_Entry = collections.namedtuple('_Entry', ['mimetype', 'signatures', 'discriminants'])
_mime_mappings = (
# pdf
_Entry('application/pdf', ['%PDF'], []),
# jpg, jpeg, png, gif
_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'], []),
# 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
]),
# 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):
""" Attempts to guess the mime type of the provided binary data, similar
to but significantly more limited than libmagic
:param str bin_data: binary data to try and guess a mime type for
:returns: matched mimetype or ``application/octet-stream`` if none matched
"""
# by default, guess the type using the magic number of file hex signature (like magic, but more limited)
# see http://www.filesignatures.net/ for file signatures
for entry in _mime_mappings:
for signature in entry.signatures:
if bin_data.startswith(signature):
for discriminant in entry.discriminants:
try:
guess = discriminant(bin_data)
if guess: return guess
except Exception:
# log-and-next
_logger.getChild('guess_mimetype').warn(
"Sub-checker '%s' of type '%s' failed",
discriminant.__name__, entry.mimetype,
exc_info=True
)
# if no discriminant or no discriminant matches, return
# primary mime type
return entry.mimetype
return 'application/octet-stream'
try:
import magic
except ImportError:
magic = None
else:
# There are 2 python libs named 'magic' with incompatible api.
# magic from pypi https://pypi.python.org/pypi/python-magic/
if hasattr(magic,'from_buffer'):
guess_mimetype = lambda bin_data: magic.from_buffer(bin_data, mime=True)
# magic from file(1) https://packages.debian.org/squeeze/python-magic
elif hasattr(magic,'open'):
ms = magic.open(magic.MAGIC_MIME_TYPE)
ms.load()
guess_mimetype = ms.buffer