[MERGE] forward port of branch 9.0 up to 27058c4
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
[](http://runbot.odoo.com/runbot)
|
[](http://runbot.odoo.com/runbot)
|
||||||
[](http://www.odoo.com/documentation/master)
|
[](http://www.odoo.com/documentation/9.0)
|
||||||
[](https://www.odoo.com/forum/help-1)
|
[](https://www.odoo.com/forum/help-1)
|
||||||
[](http://nightly.odoo.com/)
|
[](http://nightly.odoo.com/)
|
||||||
|
|
||||||
Odoo
|
Odoo
|
||||||
----
|
----
|
||||||
|
|||||||
@@ -653,25 +653,24 @@ class AccountTax(models.Model):
|
|||||||
else:
|
else:
|
||||||
tax_amount = currency.round(tax_amount)
|
tax_amount = currency.round(tax_amount)
|
||||||
|
|
||||||
if tax_amount:
|
if tax.price_include:
|
||||||
if tax.price_include:
|
total_excluded -= tax_amount
|
||||||
total_excluded -= tax_amount
|
base -= tax_amount
|
||||||
base -= tax_amount
|
else:
|
||||||
else:
|
total_included += tax_amount
|
||||||
total_included += tax_amount
|
|
||||||
|
|
||||||
if tax.include_base_amount:
|
if tax.include_base_amount:
|
||||||
base += tax_amount
|
base += tax_amount
|
||||||
|
|
||||||
taxes.append({
|
taxes.append({
|
||||||
'id': tax.id,
|
'id': tax.id,
|
||||||
'name': tax.name,
|
'name': tax.name,
|
||||||
'amount': tax_amount,
|
'amount': tax_amount,
|
||||||
'sequence': tax.sequence,
|
'sequence': tax.sequence,
|
||||||
'account_id': tax.account_id.id,
|
'account_id': tax.account_id.id,
|
||||||
'refund_account_id': tax.refund_account_id.id,
|
'refund_account_id': tax.refund_account_id.id,
|
||||||
'analytic': tax.analytic,
|
'analytic': tax.analytic,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'taxes': sorted(taxes, key=lambda k: k['sequence']),
|
'taxes': sorted(taxes, key=lambda k: k['sequence']),
|
||||||
|
|||||||
@@ -623,16 +623,17 @@ class AccountInvoice(models.Model):
|
|||||||
def tax_line_move_line_get(self):
|
def tax_line_move_line_get(self):
|
||||||
res = []
|
res = []
|
||||||
for tax_line in self.tax_line_ids:
|
for tax_line in self.tax_line_ids:
|
||||||
res.append({
|
if tax_line.amount:
|
||||||
'tax_line_id': tax_line.tax_id.id,
|
res.append({
|
||||||
'type': 'tax',
|
'tax_line_id': tax_line.tax_id.id,
|
||||||
'name': tax_line.name,
|
'type': 'tax',
|
||||||
'price_unit': tax_line.amount,
|
'name': tax_line.name,
|
||||||
'quantity': 1,
|
'price_unit': tax_line.amount,
|
||||||
'price': tax_line.amount,
|
'quantity': 1,
|
||||||
'account_id': tax_line.account_id.id,
|
'price': tax_line.amount,
|
||||||
'account_analytic_id': tax_line.account_analytic_id.id,
|
'account_id': tax_line.account_id.id,
|
||||||
})
|
'account_analytic_id': tax_line.account_analytic_id.id,
|
||||||
|
})
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def inv_line_characteristic_hashcode(self, invoice_line):
|
def inv_line_characteristic_hashcode(self, invoice_line):
|
||||||
|
|||||||
@@ -535,7 +535,7 @@ class AccountMoveLine(models.Model):
|
|||||||
|
|
||||||
# Return lines formatted
|
# Return lines formatted
|
||||||
if len(pairs) > 0:
|
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]))
|
lines = self.browse(list(pairs[0]))
|
||||||
return lines.prepare_move_lines_for_reconciliation_widget(target_currency=target_currency)
|
return lines.prepare_move_lines_for_reconciliation_widget(target_currency=target_currency)
|
||||||
return []
|
return []
|
||||||
@@ -630,7 +630,7 @@ class AccountMoveLine(models.Model):
|
|||||||
'journal_name': line.journal_id.name,
|
'journal_name': line.journal_id.name,
|
||||||
'partner_id': line.partner_id.id,
|
'partner_id': line.partner_id.id,
|
||||||
'partner_name': line.partner_id.name,
|
'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
|
debit = line.debit
|
||||||
@@ -645,7 +645,7 @@ class AccountMoveLine(models.Model):
|
|||||||
|
|
||||||
# Get right debit / credit:
|
# Get right debit / credit:
|
||||||
target_currency = target_currency or company_currency
|
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 = ""
|
amount_currency_str = ""
|
||||||
total_amount_currency_str = ""
|
total_amount_currency_str = ""
|
||||||
if line_currency != company_currency:
|
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']))
|
vals['amount_currency'] = self.env['res.currency'].browse(vals['currency_id']).round(vals['amount_currency'] * (amount / res['total_excluded']))
|
||||||
# Create tax lines
|
# Create tax lines
|
||||||
for tax_vals in res['taxes']:
|
for tax_vals in res['taxes']:
|
||||||
account_id = (amount > 0 and tax_vals['account_id'] or tax_vals['refund_account_id'])
|
if tax_vals['amount']:
|
||||||
if not account_id: account_id = vals['account_id']
|
account_id = (amount > 0 and tax_vals['account_id'] or tax_vals['refund_account_id'])
|
||||||
tax_lines_vals.append({
|
if not account_id: account_id = vals['account_id']
|
||||||
'account_id': account_id,
|
tax_lines_vals.append({
|
||||||
'name': vals['name'] + ' ' + tax_vals['name'],
|
'account_id': account_id,
|
||||||
'tax_line_id': tax_vals['id'],
|
'name': vals['name'] + ' ' + tax_vals['name'],
|
||||||
'move_id': vals['move_id'],
|
'tax_line_id': tax_vals['id'],
|
||||||
'date': vals['date'],
|
'move_id': vals['move_id'],
|
||||||
'partner_id': vals.get('partner_id'),
|
'date': vals['date'],
|
||||||
'ref': vals.get('ref'),
|
'partner_id': vals.get('partner_id'),
|
||||||
'statement_id': vals.get('statement_id'),
|
'ref': vals.get('ref'),
|
||||||
'debit': tax_vals['amount'] > 0 and tax_vals['amount'] or 0.0,
|
'statement_id': vals.get('statement_id'),
|
||||||
'credit': tax_vals['amount'] < 0 and -tax_vals['amount'] or 0.0,
|
'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)
|
new_line = super(AccountMoveLine, self).create(vals)
|
||||||
for tax_line_vals in tax_lines_vals:
|
for tax_line_vals in tax_lines_vals:
|
||||||
@@ -1220,6 +1221,7 @@ class AccountPartialReconcile(models.Model):
|
|||||||
'move_id': move.id,
|
'move_id': move.id,
|
||||||
'currency_id': rec.currency_id.id,
|
'currency_id': rec.currency_id.id,
|
||||||
'amount_currency': 0.0,
|
'amount_currency': 0.0,
|
||||||
|
'partner_id': rec.debit_move_id.partner_id.id,
|
||||||
})
|
})
|
||||||
rec.env['account.move.line'].create({
|
rec.env['account.move.line'].create({
|
||||||
'name': _('Currency exchange rate difference'),
|
'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,
|
'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,
|
'move_id': move.id,
|
||||||
'currency_id': rec.currency_id.id,
|
'currency_id': rec.currency_id.id,
|
||||||
|
'partner_id': rec.debit_move_id.partner_id.id,
|
||||||
})
|
})
|
||||||
rec.env['account.partial.reconcile'].create({
|
rec.env['account.partial.reconcile'].create({
|
||||||
'debit_move_id': amount_diff < 0 and line_to_reconcile.id or rec.debit_move_id.id,
|
'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):
|
def unlink(self):
|
||||||
""" When removing a link between entries, we need to revert the eventual journal entries we created to book the
|
""" 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.
|
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 = self.env['account.move'].search([('rate_diff_partial_rec_id', 'in', self.ids)])
|
||||||
exchange_rate_entries.reverse_moves()
|
# revert the currency difference entry
|
||||||
return super(AccountPartialReconcile, self).unlink()
|
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
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ class account_analytic_account(models.Model):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def name_search(self, name='', args=None, operator='ilike', limit=100):
|
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 []
|
args = args or []
|
||||||
domain = ['|', ('code', operator, name), ('name', operator, name)]
|
domain = ['|', ('code', operator, name), ('name', operator, name)]
|
||||||
partners = self.env['res.partner'].search([('name', operator, name)], limit=limit)
|
partners = self.env['res.partner'].search([('name', operator, name)], limit=limit)
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import logging
|
|||||||
import operator
|
import operator
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from openerp.tools.mimetypes import guess_mimetype
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from cStringIO import StringIO
|
from cStringIO import StringIO
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -158,16 +160,36 @@ class ir_import(orm.TransientModel):
|
|||||||
return fields
|
return fields
|
||||||
|
|
||||||
def _read_file(self, file_type, record, options):
|
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))
|
(file_extension, handler, req) = FILE_TYPE_DICT.get(file_type, (None, None, None))
|
||||||
if handler:
|
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.
|
# fallback on file extensions as mime types can be unreliable (e.g.
|
||||||
# software setting incorrect mime types, or non-installed software
|
# software setting incorrect mime types, or non-installed software
|
||||||
# leading to browser not sending mime types)
|
# leading to browser not sending mime types)
|
||||||
if record.file_name:
|
if record.file_name:
|
||||||
p, ext = os.path.splitext(record.file_name)
|
p, ext = os.path.splitext(record.file_name)
|
||||||
if ext and EXTENSIONS.get(ext):
|
if ext in EXTENSIONS:
|
||||||
return getattr(self, '_read_' + ext[1:])(record, options)
|
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:
|
if req:
|
||||||
raise ImportError(_("Unable to load \"{extension}\" file: requires Python module \"{modname}\"").format(extension=file_extension, modname=req))
|
raise ImportError(_("Unable to load \"{extension}\" file: requires Python module \"{modname}\"").format(extension=file_extension, modname=req))
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import unittest
|
|||||||
|
|
||||||
from openerp.tests.common import TransactionCase, can_import
|
from openerp.tests.common import TransactionCase, can_import
|
||||||
from openerp.modules.module import get_module_resource
|
from openerp.modules.module import get_module_resource
|
||||||
|
from openerp.tools import mute_logger
|
||||||
|
|
||||||
from .. import models
|
from .. import models
|
||||||
|
|
||||||
@@ -197,6 +197,7 @@ class test_preview(TransactionCase):
|
|||||||
})
|
})
|
||||||
return Import, id
|
return Import, id
|
||||||
|
|
||||||
|
@mute_logger('openerp.addons.base_import.models')
|
||||||
def test_encoding(self):
|
def test_encoding(self):
|
||||||
Import, id = self.make_import()
|
Import, id = self.make_import()
|
||||||
result = Import.parse_preview(self.cr, self.uid, id, {
|
result = Import.parse_preview(self.cr, self.uid, id, {
|
||||||
@@ -205,6 +206,7 @@ class test_preview(TransactionCase):
|
|||||||
})
|
})
|
||||||
self.assertTrue('error' in result)
|
self.assertTrue('error' in result)
|
||||||
|
|
||||||
|
@mute_logger('openerp.addons.base_import.models')
|
||||||
def test_csv_errors(self):
|
def test_csv_errors(self):
|
||||||
Import, id = self.make_import()
|
Import, id = self.make_import()
|
||||||
|
|
||||||
|
|||||||
@@ -2,16 +2,14 @@
|
|||||||
<openerp>
|
<openerp>
|
||||||
<data>
|
<data>
|
||||||
|
|
||||||
<act_window id="action_partner_deduplicate"
|
<record model="ir.actions.act_window" id="action_partner_deduplicate">
|
||||||
res_model="base.partner.merge.automatic.wizard"
|
<field name="name">Deduplicate Contacts</field>
|
||||||
src_model="res.partner"
|
<field name="res_model">base.partner.merge.automatic.wizard</field>
|
||||||
target="new"
|
<field name="view_type">form</field>
|
||||||
multi="True"
|
<field name="view_mode">form</field>
|
||||||
key2="client_action_multi"
|
<field name="target">new</field>
|
||||||
view_mode="form"
|
<field name="context">{'active_test': False}</field>
|
||||||
name="Deduplicate Contact"
|
</record>
|
||||||
context="{'default_state': 'option'}"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<record model='ir.ui.view' id='base_partner_merge_automatic_wizard_form'>
|
<record model='ir.ui.view' id='base_partner_merge_automatic_wizard_form'>
|
||||||
<field name='name'>base.partner.merge.automatic.wizard.form</field>
|
<field name='name'>base.partner.merge.automatic.wizard.form</field>
|
||||||
|
|||||||
@@ -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="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"/>
|
<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">
|
<record id="view_delivery_carrier_tree" model="ir.ui.view">
|
||||||
<field name="name">delivery.carrier.tree</field>
|
<field name="name">delivery.carrier.tree</field>
|
||||||
<field name="model">delivery.carrier</field>
|
<field name="model">delivery.carrier</field>
|
||||||
@@ -23,6 +39,13 @@
|
|||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form string="Carrier">
|
<form string="Carrier">
|
||||||
<sheet>
|
<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">
|
<div class="oe_title" name="title">
|
||||||
<label for="name" string="Name" class="oe_edit_only"/>
|
<label for="name" string="Name" class="oe_edit_only"/>
|
||||||
<h1>
|
<h1>
|
||||||
|
|||||||
@@ -134,14 +134,16 @@
|
|||||||
<field name="is_timesheet" invisible="1"/>
|
<field name="is_timesheet" invisible="1"/>
|
||||||
</tree>
|
</tree>
|
||||||
<form string="Timesheet Activities">
|
<form string="Timesheet Activities">
|
||||||
<field name="date"/>
|
<group>
|
||||||
<field name="account_id"/>
|
<field name="date"/>
|
||||||
<field name="name"/>
|
<field name="account_id"/>
|
||||||
<field name="unit_amount" widget="float_time"/>
|
<field name="name"/>
|
||||||
<field name="amount"/>
|
<field name="unit_amount" widget="float_time"/>
|
||||||
<field name="general_account_id"/>
|
<field name="amount"/>
|
||||||
<field name="user_id" required="1"/>
|
<field name="general_account_id"/>
|
||||||
<field name="is_timesheet" invisible="1"/>
|
<field name="user_id" required="1"/>
|
||||||
|
<field name="is_timesheet" invisible="1"/>
|
||||||
|
</group>
|
||||||
</form>
|
</form>
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</page>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<!-- Rule Category -->
|
<!-- Rule Category -->
|
||||||
|
|
||||||
<record id="base.main_company" model="res.company">
|
<record id="base.main_company" model="res.company">
|
||||||
<field name="plafond_secu">2946</field>
|
<field name="plafond_secu">3218</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import logging
|
|||||||
from openerp import _, api, fields, models, SUPERUSER_ID
|
from openerp import _, api, fields, models, SUPERUSER_ID
|
||||||
from openerp import tools
|
from openerp import tools
|
||||||
from openerp.exceptions import UserError, AccessError
|
from openerp.exceptions import UserError, AccessError
|
||||||
|
from openerp.osv import expression
|
||||||
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
@@ -147,34 +148,36 @@ class Message(models.Model):
|
|||||||
#------------------------------------------------------
|
#------------------------------------------------------
|
||||||
|
|
||||||
@api.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
|
""" Remove all needactions of the current partner. If channel_ids is
|
||||||
given, restrict to messages written in one of those channels. """
|
given, restrict to messages written in one of those channels. """
|
||||||
partner_id = self.env.user.partner_id.id
|
partner_id = self.env.user.partner_id.id
|
||||||
# possibly horribly inefficient method:
|
if domain is None:
|
||||||
# it does one db request for the search, and one for each message in
|
query = "DELETE FROM mail_message_res_partner_needaction_rel WHERE res_partner_id IN %s"
|
||||||
# the result set to remove the current user from the relation.
|
args = [(partner_id,)]
|
||||||
# domain = [('needaction_partner_ids', 'in', partner_id)]
|
if channel_ids:
|
||||||
# if channel_ids:
|
query += """
|
||||||
# domain += [('channel_ids', 'in', channel_ids)]
|
AND mail_message_id in
|
||||||
# unread_messages = self.search(domain)
|
(SELECT mail_message_id
|
||||||
# unread_messages.write({'needaction_partner_ids': [(3, partner_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:
|
ids = [m['id'] for m in self._cr.dictfetchall()]
|
||||||
query = "DELETE FROM mail_message_res_partner_needaction_rel WHERE res_partner_id IN %s"
|
else:
|
||||||
args = [(partner_id,)]
|
# not really efficient method: it does one db request for the
|
||||||
if channel_ids:
|
# search, and one for each message in the result set to remove the
|
||||||
query += """
|
# current user from the relation.
|
||||||
AND mail_message_id in
|
msg_domain = [('needaction_partner_ids', 'in', partner_id)]
|
||||||
(SELECT mail_message_id
|
if channel_ids:
|
||||||
FROM mail_message_mail_channel_rel
|
msg_domain += [('channel_ids', 'in', channel_ids)]
|
||||||
WHERE mail_channel_id in %s)"""
|
unread_messages = self.search(expression.AND([msg_domain, domain]))
|
||||||
args += [tuple(channel_ids)]
|
unread_messages.write({'needaction_partner_ids': [(3, partner_id)]})
|
||||||
query += " RETURNING mail_message_id as id"
|
ids = unread_messages.mapped('id')
|
||||||
self._cr.execute(query, args)
|
|
||||||
self.invalidate_cache()
|
|
||||||
|
|
||||||
ids = [m['id'] for m in self._cr.dictfetchall()]
|
|
||||||
notification = {'type': 'mark_as_read', 'message_ids': ids, 'channel_ids': channel_ids}
|
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)
|
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), notification)
|
||||||
|
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ function add_message (data, options) {
|
|||||||
var msg = _.findWhere(messages, { id: data.id });
|
var msg = _.findWhere(messages, { id: data.id });
|
||||||
|
|
||||||
if (!msg) {
|
if (!msg) {
|
||||||
msg = make_message(data);
|
msg = chat_manager.make_message(data);
|
||||||
// Keep the array ordered by id when inserting the new message
|
// Keep the array ordered by id when inserting the new message
|
||||||
messages.splice(_.sortedIndex(messages, msg, 'id'), 0, msg);
|
messages.splice(_.sortedIndex(messages, msg, 'id'), 0, msg);
|
||||||
_.each(msg.channel_ids, function (channel_id) {
|
_.each(msg.channel_ids, function (channel_id) {
|
||||||
@@ -298,7 +298,7 @@ function add_channel (data, options) {
|
|||||||
chat_manager.bus.trigger("channel_toggle_fold", channel);
|
chat_manager.bus.trigger("channel_toggle_fold", channel);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel = make_channel(data, options);
|
channel = chat_manager.make_channel(data, options);
|
||||||
channels.push(channel);
|
channels.push(channel);
|
||||||
channels = _.sortBy(channels, function (channel) { return channel.name.toLowerCase(); });
|
channels = _.sortBy(channels, function (channel) { return channel.name.toLowerCase(); });
|
||||||
if (!options.silent) {
|
if (!options.silent) {
|
||||||
@@ -677,6 +677,10 @@ function on_presence_notification (data) {
|
|||||||
// Public interface
|
// Public interface
|
||||||
//----------------------------------------------------------------------------------
|
//----------------------------------------------------------------------------------
|
||||||
var chat_manager = {
|
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) {
|
post_message: function (data, options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
var msg = {
|
var msg = {
|
||||||
@@ -773,9 +777,9 @@ var chat_manager = {
|
|||||||
return $.when();
|
return $.when();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mark_all_as_read: function (channel) {
|
mark_all_as_read: function (channel, domain) {
|
||||||
if ((!channel && needaction_counter) || (channel && channel.needaction_counter)) {
|
if ((channel.id === "channel_inbox" && needaction_counter) || (channel && channel.needaction_counter)) {
|
||||||
return MessageModel.call('mark_all_as_read', channel ? [[channel.id]] : []);
|
return MessageModel.call('mark_all_as_read', [], {channel_ids: channel.id !== "channel_inbox" ? [channel.id] : [], domain: domain});
|
||||||
}
|
}
|
||||||
return $.when();
|
return $.when();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
|||||||
self.$('.o_mail_chat_sidebar').slideToggle(200);
|
self.$('.o_mail_chat_sidebar').slideToggle(200);
|
||||||
});
|
});
|
||||||
this.$buttons.on('click', '.o_mail_chat_button_mark_read', function () {
|
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_unstar_all', chat_manager.unstar_all);
|
||||||
this.$buttons.on('click', '.o_mail_chat_button_new_message', this.on_click_new_message);
|
this.$buttons.on('click', '.o_mail_chat_button_new_message', this.on_click_new_message);
|
||||||
|
|||||||
@@ -2,12 +2,28 @@
|
|||||||
|
|
||||||
import werkzeug
|
import werkzeug
|
||||||
|
|
||||||
from openerp import http, SUPERUSER_ID
|
from openerp import http, SUPERUSER_ID, _
|
||||||
from openerp.http import request
|
from openerp.http import request
|
||||||
|
|
||||||
|
|
||||||
class MassMailController(http.Controller):
|
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')
|
@http.route('/mail/track/<int:mail_id>/blank.gif', type='http', auth='none')
|
||||||
def track_mail_open(self, mail_id, **post):
|
def track_mail_open(self, mail_id, **post):
|
||||||
""" Email tracking. """
|
""" Email tracking. """
|
||||||
|
|||||||
@@ -1221,6 +1221,7 @@ class mrp_production(osv.osv):
|
|||||||
move_id = stock_move.create(cr, uid, {
|
move_id = stock_move.create(cr, uid, {
|
||||||
'name': production.name,
|
'name': production.name,
|
||||||
'date': production.date_planned,
|
'date': production.date_planned,
|
||||||
|
'date_expected': production.date_planned,
|
||||||
'product_id': product.id,
|
'product_id': product.id,
|
||||||
'product_uom_qty': qty,
|
'product_uom_qty': qty,
|
||||||
'product_uom': uom_id,
|
'product_uom': uom_id,
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ class PaymentTxOgone(osv.Model):
|
|||||||
'acquirer_id': tx.acquirer_id.id,
|
'acquirer_id': tx.acquirer_id.id,
|
||||||
'acquirer_ref': alias
|
'acquirer_ref': alias
|
||||||
})
|
})
|
||||||
tx.partner_reference = alias
|
tx.write({'payment_method_id': ref})
|
||||||
|
|
||||||
return tx
|
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),
|
'date_validate': datetime.datetime.strptime(data['TRXDATE'], '%m/%d/%y').strftime(DEFAULT_SERVER_DATE_FORMAT),
|
||||||
'acquirer_reference': data['PAYID'],
|
'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, {
|
pm_id = self.pool['payment.method'].create(cr, uid, {
|
||||||
'partner_id': tx.partner_id.id,
|
'partner_id': tx.partner_id.id,
|
||||||
'acquirer_id': tx.acquirer_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),
|
'date_validate': datetime.date.today().strftime(DEFAULT_SERVER_DATE_FORMAT),
|
||||||
'acquirer_reference': tree.get('PAYID'),
|
'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:
|
if tx.callback_eval:
|
||||||
safe_eval(tx.callback_eval, {'self': tx})
|
safe_eval(tx.callback_eval, {'self': tx})
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -1961,6 +1961,7 @@ return {
|
|||||||
NumpadWidget: NumpadWidget,
|
NumpadWidget: NumpadWidget,
|
||||||
ProductScreenWidget: ProductScreenWidget,
|
ProductScreenWidget: ProductScreenWidget,
|
||||||
ProductListWidget: ProductListWidget,
|
ProductListWidget: ProductListWidget,
|
||||||
|
ClientListScreenWidget: ClientListScreenWidget,
|
||||||
};
|
};
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
'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.'),
|
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'),
|
'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',
|
'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.'),
|
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)),
|
'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"""
|
"""Retrieve the price before applying the pricelist"""
|
||||||
item_obj = self.pool['product.pricelist.item']
|
item_obj = self.pool['product.pricelist.item']
|
||||||
product_obj = self.pool['product.product']
|
product_obj = self.pool['product.product']
|
||||||
field_name = 'list_price'
|
field_name = 'lst_price'
|
||||||
currency_id = None
|
currency_id = None
|
||||||
if res_dict.get(pricelist):
|
if res_dict.get(pricelist):
|
||||||
rule_id = res_dict[pricelist][1]
|
rule_id = res_dict[pricelist][1]
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ class PurchaseOrder(models.Model):
|
|||||||
"It's used to do the matching when you receive the "
|
"It's used to do the matching when you receive the "
|
||||||
"products as this reference is usually written on the "
|
"products as this reference is usually written on the "
|
||||||
"delivery order sent by your vendor.")
|
"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.")
|
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)
|
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')
|
partner_id = fields.Many2one('res.partner', string='Vendor', required=True, states=READONLY_STATES, change_default=True, track_visibility='always')
|
||||||
|
|||||||
@@ -146,7 +146,7 @@
|
|||||||
<button name="button_confirm" type="object" states="sent" string="Confirm Order" class="oe_highlight" id="bid_confirm"/>
|
<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="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_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_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_cancel" states="draft,to approve,sent,purchase" string="Cancel" type="object" />
|
||||||
<button name="button_done" type="object" string="Set to Done" states="purchase"
|
<button name="button_done" type="object" string="Set to Done" states="purchase"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
from datetime import timedelta
|
||||||
from dateutil import rrule
|
from dateutil import rrule
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
|
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
|
||||||
@@ -330,14 +331,8 @@ class resource_calendar(osv.osv):
|
|||||||
working_intervals = []
|
working_intervals = []
|
||||||
tz_info = fields.datetime.context_timestamp(cr, uid, work_dt, context=context).tzinfo
|
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):
|
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'):
|
dt_f = work_dt.replace(hour=0, minute=0, second=0) + timedelta(seconds=(calendar_working_day.hour_from * 3600))
|
||||||
min_from = int((calendar_working_day.hour_from - int(calendar_working_day.hour_from)) * 60)
|
dt_t = work_dt.replace(hour=0, minute=0, second=0) + timedelta(seconds=(calendar_working_day.hour_to * 3600))
|
||||||
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))
|
|
||||||
|
|
||||||
# adapt tz
|
# adapt tz
|
||||||
working_interval = (
|
working_interval = (
|
||||||
|
|||||||
@@ -405,6 +405,35 @@ class TestResource(TestResourceCommon):
|
|||||||
default_interval=(8, 16), context=context)
|
default_interval=(8, 16), context=context)
|
||||||
self.assertEqual(res, 32.0, 'resource_calendar: wrong get_working_hours computation')
|
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):
|
def test_50_calendar_schedule_days(self):
|
||||||
""" Testing calendar days scheduling """
|
""" Testing calendar days scheduling """
|
||||||
cr, uid = self.cr, self.uid
|
cr, uid = self.cr, self.uid
|
||||||
|
|||||||
+2
-2
@@ -136,7 +136,7 @@ class SaleOrder(models.Model):
|
|||||||
payment_term_id = fields.Many2one('account.payment.term', string='Payment Term', oldname='payment_term')
|
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')
|
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'))
|
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)
|
procurement_group_id = fields.Many2one('procurement.group', 'Procurement Group', copy=False)
|
||||||
|
|
||||||
product_id = fields.Many2one('product.product', related='order_line.product_id', string='Product')
|
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):
|
def _get_default_team(self):
|
||||||
return self.env['crm.team']._get_default_team_id()
|
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
|
@api.multi
|
||||||
def confirm_paid(self):
|
def confirm_paid(self):
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ class stock_history(osv.osv):
|
|||||||
product_template_id,
|
product_template_id,
|
||||||
SUM(quantity) as quantity,
|
SUM(quantity) as quantity,
|
||||||
date,
|
date,
|
||||||
price_unit_on_quant,
|
SUM(price_unit_on_quant * quantity) / SUM(quantity) as price_unit_on_quant,
|
||||||
source,
|
source,
|
||||||
serial_number
|
serial_number
|
||||||
FROM
|
FROM
|
||||||
@@ -205,5 +205,5 @@ class stock_history(osv.osv):
|
|||||||
dest_location.usage not in ('internal', 'transit'))
|
dest_location.usage not in ('internal', 'transit'))
|
||||||
))
|
))
|
||||||
AS foo
|
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
|
||||||
)""")
|
)""")
|
||||||
|
|||||||
@@ -468,7 +468,7 @@ function autoresize ($textarea, options) {
|
|||||||
var $fixed_text_area;
|
var $fixed_text_area;
|
||||||
var min_height = (options && options.min_height) || 50;
|
var min_height = (options && options.min_height) || 50;
|
||||||
if (!$fixed_text_area) {
|
if (!$fixed_text_area) {
|
||||||
$fixed_text_area = $('<textarea>').css({
|
$fixed_text_area = $('<textarea disabled>').css({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
height: 10,
|
height: 10,
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from openerp import http, SUPERUSER_ID
|
from openerp import http, SUPERUSER_ID
|
||||||
|
from openerp.addons.mass_mailing.controllers.main import MassMailController
|
||||||
from openerp.http import request
|
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')
|
@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):
|
def mailing(self, mailing_id, email=None, res_id=None, **post):
|
||||||
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
|
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
|
||||||
if mailing.exists():
|
if mailing.exists():
|
||||||
if mailing.mailing_model == 'mail.mass_mailing.contact':
|
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', {
|
return request.website.render('website_mass_mailing.page_unsubscribe', {
|
||||||
'contacts': contacts,
|
'contacts': contacts,
|
||||||
'email': email,
|
'email': email,
|
||||||
'mailing_id': mailing_id})
|
'mailing_id': mailing_id})
|
||||||
else:
|
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')
|
return request.website.render('website_mass_mailing.page_unsubscribed')
|
||||||
|
|
||||||
@http.route(['/mail/mailing/unsubscribe'], type='json', auth='none')
|
@http.route(['/mail/mailing/unsubscribe'], type='json', auth='none')
|
||||||
|
|||||||
@@ -277,9 +277,9 @@ class sale_order(osv.osv):
|
|||||||
# create draft invoice if transaction is ok
|
# create draft invoice if transaction is ok
|
||||||
if tx and tx.state == 'done':
|
if tx and tx.state == 'done':
|
||||||
if order.state in ['draft', 'sent']:
|
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)
|
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 True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ Yoshi Tashiro tashiro@roomsfor.hk https://github.com/yostashiro
|
|||||||
List of contributors:
|
List of contributors:
|
||||||
|
|
||||||
Yoshi Tashiro tashiro@roomsfor.hk https://github.com/yostashiro
|
Yoshi Tashiro tashiro@roomsfor.hk https://github.com/yostashiro
|
||||||
|
Manami Hashi manami@roomsfor.hk https://github.com/manamirfhk
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ declaration.
|
|||||||
|
|
||||||
Signed,
|
Signed,
|
||||||
|
|
||||||
Alejandro Santana tech@alejandrosantana.eu https://github.com/alejandrosantana
|
Alejandro Santana hi@alejandrosantana.eu https://github.com/alejandrosantana
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -1128,11 +1128,11 @@ their root element is ``<gantt>``.
|
|||||||
|
|
||||||
.. code-block:: xml
|
.. code-block:: xml
|
||||||
|
|
||||||
<gantt string="Ideas" date_start="invent_date" color="inventor_id">
|
<gantt string="Ideas"
|
||||||
<level object="idea.idea" link="id" domain="[]">
|
date_start="invent_date"
|
||||||
<field name="inventor_id"/>
|
date_stop="date_finished"
|
||||||
</level>
|
progress="progress"
|
||||||
</gantt>
|
default_group_by="inventor_id" />
|
||||||
|
|
||||||
.. exercise:: Gantt charts
|
.. exercise:: Gantt charts
|
||||||
|
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ field:
|
|||||||
<field name="name">Res Partner Server Action</field>
|
<field name="name">Res Partner Server Action</field>
|
||||||
<field name="model_id" ref="model_res_partner"/>
|
<field name="model_id" ref="model_res_partner"/>
|
||||||
<field name="code">
|
<field name="code">
|
||||||
print self, object
|
raise Warning(object.name)
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|||||||
@@ -280,13 +280,6 @@ customize the amout of logging output
|
|||||||
In case of conflict between :option:`--log-level` and
|
In case of conflict between :option:`--log-level` and
|
||||||
:option:`--log-handler`, the latter is used
|
: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:
|
.. _reference/cmdline/scaffold:
|
||||||
|
|
||||||
|
|||||||
+27
-11
@@ -862,6 +862,8 @@ Possible children of the view element are:
|
|||||||
a :meth:`~openerp.models.Model.read` (except for date and datetime fields
|
a :meth:`~openerp.models.Model.read` (except for date and datetime fields
|
||||||
that are `formatted according to user's locale
|
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>`_)
|
<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``
|
``read_only_mode``
|
||||||
self-explanatory
|
self-explanatory
|
||||||
|
|
||||||
@@ -1013,26 +1015,40 @@ take the following attributes:
|
|||||||
and the end date will be set to the start date
|
and the end date will be set to the start date
|
||||||
``date_delay``
|
``date_delay``
|
||||||
name of the field providing the duration of the event
|
name of the field providing the duration of the event
|
||||||
``progress``
|
``duration_unit``
|
||||||
name of a field providing the completion percentage for the record's event,
|
one of ``minute``, ``hour`` (default), ``day``, ``week``, ``month``, ``year``
|
||||||
between 0 and 100
|
|
||||||
``default_group_by``
|
``default_group_by``
|
||||||
name of a field to group tasks by
|
name of a field to group tasks by
|
||||||
``fold_last_level``
|
``type``
|
||||||
If a value is set, the last grouping level is folded
|
``gantt`` classic gantt view (default)
|
||||||
``round_dnd_dates``
|
|
||||||
enables rounding the task's start and end dates to the nearest scale marks
|
``consolidate`` values of the first children are consolidated in the gantt's task
|
||||||
|
|
||||||
|
``planning`` children are displayed in the gantt's task
|
||||||
``consolidation``
|
``consolidation``
|
||||||
field name to display consolidation value in record cell
|
field name to display consolidation value in record cell
|
||||||
``consolidation_max``
|
``consolidation_max``
|
||||||
dictionary with the "group by" field as key and the maximum consolidation
|
dictionary with the "group by" field as key and the maximum consolidation
|
||||||
value that can be reached before displaying the cell in red
|
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_exclude
|
||||||
.. consolidation_color
|
.. 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:
|
.. _reference/views/diagram:
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ instance, here is a simple sequence of two activities defined in XML
|
|||||||
<record id="trans_a_b" model="workflow.transition">
|
<record id="trans_a_b" model="workflow.transition">
|
||||||
<field name="act_from" ref="activity_a"/>
|
<field name="act_from" ref="activity_a"/>
|
||||||
<field name="act_to" ref="activity_b"/>
|
<field name="act_to" ref="activity_b"/>
|
||||||
|
<field name="signal">signal_goto_b</signal>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
A worfklow is always defined with respect to a particular model (the model is
|
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``
|
call on the model ``test.workflow`` (the usual ``cr, uid, ids, context``
|
||||||
arguments are passed for you).
|
arguments are passed for you).
|
||||||
|
|
||||||
The transition between "a" and "b" does not specify any condition. This means
|
The transition between "a" and "b" specify a signal but not any condition. This
|
||||||
that the workflow instance immediately goes from "a" to "b" after "a" has been
|
means the workflow instance will immediately transition from "a" to "b" when the
|
||||||
processed, and thus also processes activity "b".
|
signal ``signal_goto_b`` is recieved and thus processes activity "b"'s action
|
||||||
|
``print_b()``.
|
||||||
|
|
||||||
Activities
|
Activities
|
||||||
----------
|
----------
|
||||||
|
|||||||
@@ -8,61 +8,15 @@ import mimetypes
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from openerp import tools
|
from openerp import SUPERUSER_ID, tools
|
||||||
from openerp.tools.translate import _
|
|
||||||
from openerp.exceptions import AccessError
|
from openerp.exceptions import AccessError
|
||||||
from openerp.osv import fields,osv
|
from openerp.osv import fields,osv
|
||||||
from openerp import SUPERUSER_ID
|
|
||||||
from openerp.exceptions import UserError
|
|
||||||
from openerp.tools.translate import _
|
from openerp.tools.translate import _
|
||||||
from openerp.tools.misc import ustr
|
from openerp.tools.misc import ustr
|
||||||
|
from openerp.tools.mimetypes import guess_mimetype
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_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):
|
class ir_attachment(osv.osv):
|
||||||
"""Attachments are used to link binary files or url to any openerp document.
|
"""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),
|
'file_size': fields.integer('File Size', readonly=True),
|
||||||
'checksum': fields.char("Checksum/SHA1", size=40, select=True, readonly=True),
|
'checksum': fields.char("Checksum/SHA1", size=40, select=True, readonly=True),
|
||||||
'mimetype': fields.char('Mime Type', 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'),
|
'public': fields.boolean('Is public document'),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,9 +397,8 @@ class ir_attachment(osv.osv):
|
|||||||
# database allowed it. Helps avoid errors when concurrent transactions
|
# database allowed it. Helps avoid errors when concurrent transactions
|
||||||
# are deleting the same file, and some of the transactions are
|
# are deleting the same file, and some of the transactions are
|
||||||
# rolled back by PostgreSQL (due to concurrent updates detection).
|
# rolled back by PostgreSQL (due to concurrent updates detection).
|
||||||
to_delete = [a.store_fname
|
to_delete = set([a.store_fname for a in self.browse(cr, uid, ids, context=context)
|
||||||
for a in self.browse(cr, uid, ids, context=context)
|
if a.store_fname])
|
||||||
if a.store_fname]
|
|
||||||
res = super(ir_attachment, self).unlink(cr, uid, ids, context)
|
res = super(ir_attachment, self).unlink(cr, uid, ids, context)
|
||||||
for file_path in to_delete:
|
for file_path in to_delete:
|
||||||
self._file_delete(cr, uid, file_path)
|
self._file_delete(cr, uid, file_path)
|
||||||
|
|||||||
@@ -1339,21 +1339,28 @@ class AssetsBundle(object):
|
|||||||
uid = openerp.SUPERUSER_ID
|
uid = openerp.SUPERUSER_ID
|
||||||
preprocessed = True
|
preprocessed = True
|
||||||
for atype in (SassStylesheetAsset, LessStylesheetAsset):
|
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:
|
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)
|
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
|
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
|
return preprocessed
|
||||||
|
|
||||||
def preprocess_css(self, debug=False):
|
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
|
||||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+30
-13
@@ -13,6 +13,8 @@ from functools import wraps
|
|||||||
import logging
|
import logging
|
||||||
import urlparse
|
import urlparse
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
import psycopg2
|
||||||
import psycopg2.extras
|
import psycopg2.extras
|
||||||
import psycopg2.extensions
|
import psycopg2.extensions
|
||||||
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT, ISOLATION_LEVEL_READ_COMMITTED, ISOLATION_LEVEL_REPEATABLE_READ
|
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)
|
_logger.debug(('%r ' + msg), self, *args)
|
||||||
|
|
||||||
@locked
|
@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
|
# free dead and leaked connections
|
||||||
for i, (cnx, _) in tools.reverse_enumerate(self._connections):
|
for i, (cnx, _) in tools.reverse_enumerate(self._connections):
|
||||||
if cnx.closed:
|
if cnx.closed:
|
||||||
@@ -541,7 +547,7 @@ class ConnectionPool(object):
|
|||||||
_logger.info('%r: Free leaked connection to %r', self, cnx.dsn)
|
_logger.info('%r: Free leaked connection to %r', self, cnx.dsn)
|
||||||
|
|
||||||
for i, (cnx, used) in enumerate(self._connections):
|
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:
|
try:
|
||||||
cnx.reset()
|
cnx.reset()
|
||||||
except psycopg2.OperationalError:
|
except psycopg2.OperationalError:
|
||||||
@@ -570,11 +576,13 @@ class ConnectionPool(object):
|
|||||||
raise PoolError('The Connection Pool Is Full')
|
raise PoolError('The Connection Pool Is Full')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = psycopg2.connect(dsn=dsn, connection_factory=PsycoConnection)
|
result = psycopg2.connect(
|
||||||
|
connection_factory=PsycoConnection,
|
||||||
|
**connection_info)
|
||||||
except psycopg2.Error:
|
except psycopg2.Error:
|
||||||
_logger.info('Connection to the database failed')
|
_logger.info('Connection to the database failed')
|
||||||
raise
|
raise
|
||||||
result._original_dsn = dsn
|
result._original_dsn = connection_info
|
||||||
self._connections.append((result, True))
|
self._connections.append((result, True))
|
||||||
self._debug('Create new connection')
|
self._debug('Create new connection')
|
||||||
return result
|
return result
|
||||||
@@ -639,8 +647,17 @@ class Connection(object):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def dsn(db_or_uri):
|
def connection_info_for(db_or_uri):
|
||||||
"""parse the given `db_or_uri` and return a 2-tuple (dbname, 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://')):
|
if db_or_uri.startswith(('postgresql://', 'postgres://')):
|
||||||
# extract db from uri
|
# extract db from uri
|
||||||
us = urlparse.urlsplit(db_or_uri)
|
us = urlparse.urlsplit(db_or_uri)
|
||||||
@@ -650,15 +667,15 @@ def dsn(db_or_uri):
|
|||||||
db_name = us.username
|
db_name = us.username
|
||||||
else:
|
else:
|
||||||
db_name = us.hostname
|
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'):
|
for p in ('host', 'port', 'user', 'password'):
|
||||||
cfg = tools.config['db_' + p]
|
cfg = tools.config['db_' + p]
|
||||||
if cfg:
|
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
|
_Pool = None
|
||||||
|
|
||||||
@@ -667,16 +684,16 @@ def db_connect(to, allow_uri=False):
|
|||||||
if _Pool is None:
|
if _Pool is None:
|
||||||
_Pool = ConnectionPool(int(tools.config['db_maxconn']))
|
_Pool = ConnectionPool(int(tools.config['db_maxconn']))
|
||||||
|
|
||||||
db, uri = dsn(to)
|
db, info = connection_info_for(to)
|
||||||
if not allow_uri and db != to:
|
if not allow_uri and db != to:
|
||||||
raise ValueError('URI connections not allowed')
|
raise ValueError('URI connections not allowed')
|
||||||
return Connection(_Pool, db, uri)
|
return Connection(_Pool, db, info)
|
||||||
|
|
||||||
def close_db(db_name):
|
def close_db(db_name):
|
||||||
""" You might want to call openerp.modules.registry.RegistryManager.delete(db_name) along this function."""
|
""" You might want to call openerp.modules.registry.RegistryManager.delete(db_name) along this function."""
|
||||||
global _Pool
|
global _Pool
|
||||||
if _Pool:
|
if _Pool:
|
||||||
_Pool.close_all(dsn(db_name)[1])
|
_Pool.close_all(connection_info_for(db_name)[1])
|
||||||
|
|
||||||
def close_all():
|
def close_all():
|
||||||
global _Pool
|
global _Pool
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user