Fiscal Position:
diff --git a/addons/account_anglo_saxon/invoice.py b/addons/account_anglo_saxon/invoice.py
index de085e32e30..e4ea2adb748 100644
--- a/addons/account_anglo_saxon/invoice.py
+++ b/addons/account_anglo_saxon/invoice.py
@@ -34,114 +34,15 @@ class account_invoice_line(osv.osv):
def move_line_get(self, cr, uid, invoice_id, context=None):
res = super(account_invoice_line,self).move_line_get(cr, uid, invoice_id, context=context)
inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id, context=context)
- company_currency = inv.company_id.currency_id.id
- def get_price(cr, uid, inv, company_currency, i_line, price_unit):
- cur_obj = self.pool.get('res.currency')
- decimal_precision = self.pool.get('decimal.precision')
- if inv.currency_id.id != company_currency:
- price = cur_obj.compute(cr, uid, company_currency, inv.currency_id.id, price_unit * i_line.quantity, context={'date': inv.date_invoice})
- else:
- price = price_unit * i_line.quantity
- return round(price, decimal_precision.precision_get(cr, uid, 'Account'))
-
if inv.type in ('out_invoice','out_refund'):
for i_line in inv.invoice_line:
- if i_line.product_id and i_line.product_id.valuation == 'real_time':
- # debit account dacc will be the output account
- # first check the product, if empty check the category
- dacc = i_line.product_id.property_stock_account_output and i_line.product_id.property_stock_account_output.id
- if not dacc:
- dacc = i_line.product_id.categ_id.property_stock_account_output_categ and i_line.product_id.categ_id.property_stock_account_output_categ.id
- # in both cases the credit account cacc will be the expense account
- # first check the product, if empty check the category
- cacc = i_line.product_id.property_account_expense and i_line.product_id.property_account_expense.id
- if not cacc:
- cacc = i_line.product_id.categ_id.property_account_expense_categ and i_line.product_id.categ_id.property_account_expense_categ.id
- if dacc and cacc:
- price_unit = i_line.move_id and i_line.move_id.price_unit or i_line.product_id.standard_price
- res.append({
- 'type':'src',
- 'name': i_line.name[:64],
- 'price_unit':price_unit,
- 'quantity':i_line.quantity,
- 'price':get_price(cr, uid, inv, company_currency, i_line, price_unit),
- 'account_id':dacc,
- 'product_id':i_line.product_id.id,
- 'uos_id':i_line.uos_id.id,
- 'account_analytic_id': False,
- 'taxes':i_line.invoice_line_tax_id,
- })
-
- res.append({
- 'type':'src',
- 'name': i_line.name[:64],
- 'price_unit':price_unit,
- 'quantity':i_line.quantity,
- 'price': -1 * get_price(cr, uid, inv, company_currency, i_line, price_unit),
- 'account_id':cacc,
- 'product_id':i_line.product_id.id,
- 'uos_id':i_line.uos_id.id,
- 'account_analytic_id': False,
- 'taxes':i_line.invoice_line_tax_id,
- })
+ res.extend(self._anglo_saxon_sale_move_lines(cr, uid, i_line, res, context=context))
elif inv.type in ('in_invoice','in_refund'):
for i_line in inv.invoice_line:
- if i_line.product_id and i_line.product_id.valuation == 'real_time':
- if i_line.product_id.type != 'service':
- # get the price difference account at the product
- acc = i_line.product_id.property_account_creditor_price_difference and i_line.product_id.property_account_creditor_price_difference.id
- if not acc:
- # if not found on the product get the price difference account at the category
- acc = i_line.product_id.categ_id.property_account_creditor_price_difference_categ and i_line.product_id.categ_id.property_account_creditor_price_difference_categ.id
- a = None
-
- # oa will be the stock input account
- # first check the product, if empty check the category
- oa = i_line.product_id.property_stock_account_input and i_line.product_id.property_stock_account_input.id
- if not oa:
- oa = i_line.product_id.categ_id.property_stock_account_input_categ and i_line.product_id.categ_id.property_stock_account_input_categ.id
- if oa:
- # get the fiscal position
- fpos = i_line.invoice_id.fiscal_position or False
- a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, oa)
- diff_res = []
- decimal_precision = self.pool.get('decimal.precision')
- account_prec = decimal_precision.precision_get(cr, uid, 'Account')
- # calculate and write down the possible price difference between invoice price and product price
- for line in res:
- if line.get('invl_id', 0) == i_line.id and a == line['account_id']:
- uom = i_line.product_id.uos_id or i_line.product_id.uom_id
- valuation_price_unit = self.pool.get('product.uom')._compute_price(cr, uid, uom.id, i_line.product_id.standard_price, i_line.uos_id.id)
- if i_line.product_id.cost_method != 'standard' and i_line.purchase_line_id:
- #for average/fifo/lifo costing method, fetch real cost price from incomming moves
- stock_move_obj = self.pool.get('stock.move')
- valuation_stock_move = stock_move_obj.search(cr, uid, [('purchase_line_id', '=', i_line.purchase_line_id.id)], limit=1, context=context)
- if valuation_stock_move:
- valuation_price_unit = stock_move_obj.browse(cr, uid, valuation_stock_move[0], context=context).price_unit
- if inv.currency_id.id != company_currency:
- valuation_price_unit = self.pool.get('res.currency').compute(cr, uid, company_currency, inv.currency_id.id, valuation_price_unit, context={'date': inv.date_invoice})
- if valuation_price_unit != i_line.price_unit and line['price_unit'] == i_line.price_unit and acc:
- # price with discount and without tax included
- price_unit = self.pool['account.tax'].compute_all(cr, uid, line['taxes'],
- i_line.price_unit * (1-(i_line.discount or 0.0)/100.0), line['quantity'])['total']
- price_line = round(valuation_price_unit * line['quantity'], account_prec)
- price_diff = round(price_unit - price_line, account_prec)
- line.update({'price': price_line})
- diff_res.append({
- 'type': 'src',
- 'name': i_line.name[:64],
- 'price_unit': round(price_diff / line['quantity'], account_prec),
- 'quantity': line['quantity'],
- 'price': price_diff,
- 'account_id': acc,
- 'product_id': line['product_id'],
- 'uos_id': line['uos_id'],
- 'account_analytic_id': line['account_analytic_id'],
- 'taxes': line.get('taxes', []),
- })
- res += diff_res
+ res.extend(self._anglo_saxon_purchase_move_lines(cr, uid, i_line, res, context=context))
return res
+
def product_id_change(self, cr, uid, ids, product, uom_id, qty=0, name='', type='out_invoice', partner_id=False, fposition_id=False, price_unit=False, currency_id=False, company_id=None, context=None):
fiscal_pool = self.pool.get('account.fiscal.position')
res = super(account_invoice_line, self).product_id_change(cr, uid, ids, product, uom_id, qty, name, type, partner_id, fposition_id, price_unit, currency_id, company_id, context)
@@ -163,6 +64,132 @@ class account_invoice_line(osv.osv):
res['value'].update({'account_id':a})
return res
+ def _get_price(self, cr, uid, inv, company_currency, i_line, price_unit):
+ cur_obj = self.pool.get('res.currency')
+ decimal_precision = self.pool.get('decimal.precision')
+ if inv.currency_id.id != company_currency:
+ price = cur_obj.compute(cr, uid, company_currency, inv.currency_id.id, price_unit * i_line.quantity, context={'date': inv.date_invoice})
+ else:
+ price = price_unit * i_line.quantity
+ return round(price, decimal_precision.precision_get(cr, uid, 'Account'))
+
+ def _anglo_saxon_sale_move_lines(self, cr, uid, i_line, res, context=None):
+ """Return the additional move lines for sales invoices and refunds.
+
+ i_line: An account.invoice.line object.
+ res: The move line entries produced so far by the parent move_line_get.
+ """
+ inv = i_line.invoice_id
+ company_currency = inv.company_id.currency_id.id
+
+ if i_line.product_id and i_line.product_id.valuation == 'real_time':
+ # debit account dacc will be the output account
+ # first check the product, if empty check the category
+ dacc = i_line.product_id.property_stock_account_output and i_line.product_id.property_stock_account_output.id
+ if not dacc:
+ dacc = i_line.product_id.categ_id.property_stock_account_output_categ and i_line.product_id.categ_id.property_stock_account_output_categ.id
+ # in both cases the credit account cacc will be the expense account
+ # first check the product, if empty check the category
+ cacc = i_line.product_id.property_account_expense and i_line.product_id.property_account_expense.id
+ if not cacc:
+ cacc = i_line.product_id.categ_id.property_account_expense_categ and i_line.product_id.categ_id.property_account_expense_categ.id
+ if dacc and cacc:
+ price_unit = i_line.move_id and i_line.move_id.price_unit or i_line.product_id.standard_price
+ return [
+ {
+ 'type':'src',
+ 'name': i_line.name[:64],
+ 'price_unit':price_unit,
+ 'quantity':i_line.quantity,
+ 'price':self._get_price(cr, uid, inv, company_currency, i_line, price_unit),
+ 'account_id':dacc,
+ 'product_id':i_line.product_id.id,
+ 'uos_id':i_line.uos_id.id,
+ 'account_analytic_id': False,
+ 'taxes':i_line.invoice_line_tax_id,
+ },
+
+ {
+ 'type':'src',
+ 'name': i_line.name[:64],
+ 'price_unit':price_unit,
+ 'quantity':i_line.quantity,
+ 'price': -1 * self._get_price(cr, uid, inv, company_currency, i_line, price_unit),
+ 'account_id':cacc,
+ 'product_id':i_line.product_id.id,
+ 'uos_id':i_line.uos_id.id,
+ 'account_analytic_id': False,
+ 'taxes':i_line.invoice_line_tax_id,
+ },
+ ]
+ return []
+
+
+ def _anglo_saxon_purchase_move_lines(self, cr, uid, i_line, res, context=None):
+ """Return the additional move lines for purchase invoices and refunds.
+
+ i_line: An account.invoice.line object.
+ res: The move line entries produced so far by the parent move_line_get.
+ """
+ inv = i_line.invoice_id
+ company_currency = inv.company_id.currency_id.id
+ if i_line.product_id and i_line.product_id.valuation == 'real_time':
+ if i_line.product_id.type != 'service':
+ # get the price difference account at the product
+ acc = i_line.product_id.property_account_creditor_price_difference and i_line.product_id.property_account_creditor_price_difference.id
+ if not acc:
+ # if not found on the product get the price difference account at the category
+ acc = i_line.product_id.categ_id.property_account_creditor_price_difference_categ and i_line.product_id.categ_id.property_account_creditor_price_difference_categ.id
+ a = None
+
+ # oa will be the stock input account
+ # first check the product, if empty check the category
+ oa = i_line.product_id.property_stock_account_input and i_line.product_id.property_stock_account_input.id
+ if not oa:
+ oa = i_line.product_id.categ_id.property_stock_account_input_categ and i_line.product_id.categ_id.property_stock_account_input_categ.id
+ if oa:
+ # get the fiscal position
+ fpos = i_line.invoice_id.fiscal_position or False
+ a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, oa)
+ diff_res = []
+ decimal_precision = self.pool.get('decimal.precision')
+ account_prec = decimal_precision.precision_get(cr, uid, 'Account')
+ # calculate and write down the possible price difference between invoice price and product price
+ for line in res:
+ if line.get('invl_id', 0) == i_line.id and a == line['account_id']:
+ uom = i_line.product_id.uos_id or i_line.product_id.uom_id
+ valuation_price_unit = self.pool.get('product.uom')._compute_price(cr, uid, uom.id, i_line.product_id.standard_price, i_line.uos_id.id)
+ if i_line.product_id.cost_method != 'standard' and i_line.purchase_line_id:
+ #for average/fifo/lifo costing method, fetch real cost price from incomming moves
+ stock_move_obj = self.pool.get('stock.move')
+ valuation_stock_move = stock_move_obj.search(cr, uid, [('purchase_line_id', '=', i_line.purchase_line_id.id)], limit=1, context=context)
+ if valuation_stock_move:
+ valuation_price_unit = stock_move_obj.browse(cr, uid, valuation_stock_move[0], context=context).price_unit
+ if inv.currency_id.id != company_currency:
+ valuation_price_unit = self.pool.get('res.currency').compute(cr, uid, company_currency, inv.currency_id.id, valuation_price_unit, context={'date': inv.date_invoice})
+ if valuation_price_unit != i_line.price_unit and line['price_unit'] == i_line.price_unit and acc:
+ # price with discount and without tax included
+ price_unit = self.pool['account.tax'].compute_all(cr, uid, line['taxes'],
+ i_line.price_unit * (1-(i_line.discount or 0.0)/100.0), line['quantity'])['total']
+ price_line = round(valuation_price_unit * line['quantity'], account_prec)
+ price_diff = round(price_unit - price_line, account_prec)
+ line.update({'price': price_line})
+ diff_res.append({
+ 'type': 'src',
+ 'name': i_line.name[:64],
+ 'price_unit': round(price_diff / line['quantity'], account_prec),
+ 'quantity': line['quantity'],
+ 'price': price_diff,
+ 'account_id': acc,
+ 'product_id': line['product_id'],
+ 'uos_id': line['uos_id'],
+ 'account_analytic_id': line['account_analytic_id'],
+ 'taxes': line.get('taxes', []),
+ })
+ return diff_res
+ return []
+
+
class account_invoice(osv.osv):
_inherit = "account.invoice"
diff --git a/addons/account_asset/account_asset.py b/addons/account_asset/account_asset.py
index 728ae7f7d4a..7aa97bb012e 100644
--- a/addons/account_asset/account_asset.py
+++ b/addons/account_asset/account_asset.py
@@ -390,7 +390,7 @@ class account_asset_depreciation_line(osv.osv):
created_move_ids = []
asset_ids = []
for line in self.browse(cr, uid, ids, context=context):
- depreciation_date = context.get('depreciation_date') or time.strftime('%Y-%m-%d')
+ depreciation_date = context.get('depreciation_date') or line.depreciation_date or time.strftime('%Y-%m-%d')
period_ids = period_obj.find(cr, uid, depreciation_date, context=context)
company_currency = line.asset_id.company_id.currency_id.id
current_currency = line.asset_id.currency_id.id
diff --git a/addons/account_asset/account_asset_demo.xml b/addons/account_asset/account_asset_demo.xml
index 5b1a4b03be6..ec18c4a5df6 100644
--- a/addons/account_asset/account_asset_demo.xml
+++ b/addons/account_asset/account_asset_demo.xml
@@ -41,7 +41,7 @@
open
-
+ CEO's Car
diff --git a/addons/account_asset/test/account_asset_demo.yml b/addons/account_asset/test/account_asset_demo.yml
index c0871d05268..56d4d7ce8ac 100644
--- a/addons/account_asset/test/account_asset_demo.yml
+++ b/addons/account_asset/test/account_asset_demo.yml
@@ -6,4 +6,46 @@
category_id: account_asset_category_sale
-
!record {model: account.asset.asset, id: account_asset_asset_vehicles0}:
- method_number: 10
+ method_number: 5
+-
+ !record {model: account.fiscalyear, id: data_fiscalyear_plus1}:
+ company_id: base.main_company
+ date_start: !eval "'%s-01-01' %(datetime.now().year+1)"
+ date_stop: !eval "'%s-12-31' %(datetime.now().year+1)"
+ name: !eval "'Fiscal Year X %s' %(datetime.now().year+1)"
+ code: !eval "'FY%s' %(datetime.now().year+1)"
+-
+ !record {model: account.fiscalyear, id: data_fiscalyear_plus2}:
+ company_id: base.main_company
+ date_start: !eval "'%s-01-01' %(datetime.now().year+2)"
+ date_stop: !eval "'%s-12-31' %(datetime.now().year+2)"
+ name: !eval "'Fiscal Year X %s' %(datetime.now().year+2)"
+ code: !eval "'FY%s' %(datetime.now().year+2)"
+-
+ !record {model: account.fiscalyear, id: data_fiscalyear_plus3}:
+ company_id: base.main_company
+ date_start: !eval "'%s-01-01' %(datetime.now().year+3)"
+ date_stop: !eval "'%s-12-31' %(datetime.now().year+3)"
+ name: !eval "'Fiscal Year X %s' %(datetime.now().year+3)"
+ code: !eval "'FY%s' %(datetime.now().year+3)"
+-
+ !record {model: account.fiscalyear, id: data_fiscalyear_plus4}:
+ company_id: base.main_company
+ date_start: !eval "'%s-01-01' %(datetime.now().year+4)"
+ date_stop: !eval "'%s-12-31' %(datetime.now().year+4)"
+ name: !eval "'Fiscal Year X %s' %(datetime.now().year+4)"
+ code: !eval "'FY%s' %(datetime.now().year+4)"
+-
+ !record {model: account.fiscalyear, id: data_fiscalyear_plus5}:
+ company_id: base.main_company
+ date_start: !eval "'%s-01-01' %(datetime.now().year+5)"
+ date_stop: !eval "'%s-12-31' %(datetime.now().year+5)"
+ name: !eval "'Fiscal Year X %s' %(datetime.now().year+5)"
+ code: !eval "'FY%s' %(datetime.now().year+5)"
+-
+ !python {model: account.fiscalyear}: |
+ self.create_period(cr, uid, [ref("data_fiscalyear_plus1")])
+ self.create_period(cr, uid, [ref("data_fiscalyear_plus2")])
+ self.create_period(cr, uid, [ref("data_fiscalyear_plus3")])
+ self.create_period(cr, uid, [ref("data_fiscalyear_plus4")])
+ self.create_period(cr, uid, [ref("data_fiscalyear_plus5")])
diff --git a/addons/account_voucher/account_voucher.py b/addons/account_voucher/account_voucher.py
index 9f14828d426..22bce5306be 100644
--- a/addons/account_voucher/account_voucher.py
+++ b/addons/account_voucher/account_voucher.py
@@ -69,8 +69,10 @@ class account_voucher(osv.osv):
invoice_pool = self.pool.get('account.invoice')
journal_pool = self.pool.get('account.journal')
if context.get('invoice_id', False):
- currency_id = invoice_pool.browse(cr, uid, context['invoice_id'], context=context).currency_id.id
- journal_id = journal_pool.search(cr, uid, [('currency', '=', currency_id)], limit=1)
+ invoice = invoice_pool.browse(cr, uid, context['invoice_id'], context=context)
+ journal_id = journal_pool.search(cr, uid, [
+ ('currency', '=', invoice.currency_id.id), ('company_id', '=', invoice.company_id.id)
+ ], limit=1, context=context)
return journal_id and journal_id[0] or False
if context.get('journal_id', False):
return context.get('journal_id')
@@ -878,7 +880,13 @@ class account_voucher(osv.osv):
currency_id = journal.currency.id
else:
currency_id = journal.company_id.currency_id.id
- vals['value'].update({'currency_id': currency_id, 'payment_rate_currency_id': currency_id})
+
+ period_id = self.pool['account.period'].find(cr, uid, context=dict(context, company_id=company_id))
+ vals['value'].update({
+ 'currency_id': currency_id,
+ 'payment_rate_currency_id': currency_id,
+ 'period_id' : period_id
+ })
#in case we want to register the payment directly from an invoice, it's confusing to allow to switch the journal
#without seeing that the amount is expressed in the journal currency, and not in the invoice currency. So to avoid
#this common mistake, we simply reset the amount to 0 if the currency is not the invoice currency.
@@ -891,6 +899,18 @@ class account_voucher(osv.osv):
vals[key].update(res[key])
return vals
+ def onchange_company(self, cr, uid, ids, partner_id, journal_id, currency_id, company_id, context=None):
+ """
+ If the company changes, check that the journal is in the right company.
+ If not, fetch a new journal.
+ """
+ journal_pool = self.pool['account.journal']
+ journal = journal_pool.browse(cr, uid, journal_id, context=context)
+ if journal.company_id.id != company_id:
+ # can not guess type of journal, better remove it
+ return {'value': {'journal_id': False}}
+ return {}
+
def button_proforma_voucher(self, cr, uid, ids, context=None):
self.signal_workflow(cr, uid, ids, 'proforma_voucher')
return {'type': 'ir.actions.act_window_close'}
diff --git a/addons/account_voucher/account_voucher_view.xml b/addons/account_voucher/account_voucher_view.xml
index 2b0508a7542..24c566481d8 100644
--- a/addons/account_voucher/account_voucher_view.xml
+++ b/addons/account_voucher/account_voucher_view.xml
@@ -55,7 +55,7 @@
-
+
diff --git a/addons/account_voucher/voucher_payment_receipt_view.xml b/addons/account_voucher/voucher_payment_receipt_view.xml
index 047f69ab6a6..3c5fa4b8bc4 100644
--- a/addons/account_voucher/voucher_payment_receipt_view.xml
+++ b/addons/account_voucher/voucher_payment_receipt_view.xml
@@ -437,7 +437,7 @@
-
+
-
+
diff --git a/addons/calendar/calendar_view.xml b/addons/calendar/calendar_view.xml
index 6ac9f54e36b..4bdfa5e0762 100644
--- a/addons/calendar/calendar_view.xml
+++ b/addons/calendar/calendar_view.xml
@@ -240,7 +240,7 @@
CRM - Meetings Ganttcalendar.event
-
+
diff --git a/addons/crm/crm_phonecall_view.xml b/addons/crm/crm_phonecall_view.xml
index 7ae15f49cad..c1e877b2294 100644
--- a/addons/crm/crm_phonecall_view.xml
+++ b/addons/crm/crm_phonecall_view.xml
@@ -198,7 +198,7 @@
-
+
diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py
index ec4db70bd96..caecb47f564 100644
--- a/addons/mail/mail_mail.py
+++ b/addons/mail/mail_mail.py
@@ -75,7 +75,7 @@ class mail_mail(osv.Model):
def default_get(self, cr, uid, fields, context=None):
# protection for `default_type` values leaking from menu action context (e.g. for invoices)
# To remove when automatic context propagation is removed in web client
- if context and context.get('default_type') and context.get('default_type') not in self._fields['type'].selection:
+ if context and context.get('default_type') and context.get('default_type') not in self._all_columns['type'].column.selection:
context = dict(context, default_type=None)
return super(mail_mail, self).default_get(cr, uid, fields, context=context)
diff --git a/addons/mrp/mrp_view.xml b/addons/mrp/mrp_view.xml
index b9e5aafdcf5..1b3f4234cad 100644
--- a/addons/mrp/mrp_view.xml
+++ b/addons/mrp/mrp_view.xml
@@ -398,7 +398,7 @@
-
+
diff --git a/addons/payment_adyen/data/adyen.xml b/addons/payment_adyen/data/adyen.xml
index ec1808f5b7f..7d774d45064 100644
--- a/addons/payment_adyen/data/adyen.xml
+++ b/addons/payment_adyen/data/adyen.xml
@@ -9,7 +9,7 @@
testYou will be redirected to the Adyen website after cliking on the payment button.
]]>
+
You will be redirected to the Adyen website after clicking on the payment button.
]]>
dummydummydummy
diff --git a/addons/payment_buckaroo/data/buckaroo.xml b/addons/payment_buckaroo/data/buckaroo.xml
index 9d100a59191..e0e3ad8b7c5 100644
--- a/addons/payment_buckaroo/data/buckaroo.xml
+++ b/addons/payment_buckaroo/data/buckaroo.xml
@@ -9,7 +9,7 @@
testYou will be redirected to the Buckaroo website after cliking on the payment button.]]>
+
You will be redirected to the Buckaroo website after clicking on the payment button.
]]>
dummydummy
diff --git a/addons/payment_ogone/data/ogone.xml b/addons/payment_ogone/data/ogone.xml
index e24aace7ffd..bb9c9a59eea 100644
--- a/addons/payment_ogone/data/ogone.xml
+++ b/addons/payment_ogone/data/ogone.xml
@@ -9,7 +9,7 @@
testYou will be redirected to the Ogone website after cliking on the payment button.]]>
+
You will be redirected to the Ogone website after clicking on the payment button.
]]>
dummydummydummy
diff --git a/addons/payment_paypal/data/paypal.xml b/addons/payment_paypal/data/paypal.xml
index d66dee8a3b3..746af24126b 100644
--- a/addons/payment_paypal/data/paypal.xml
+++ b/addons/payment_paypal/data/paypal.xml
@@ -9,7 +9,7 @@
testYou will be redirected to the Paypal website after cliking on the payment button.]]>
+
You will be redirected to the Paypal website after clicking on the payment button.
diff --git a/addons/product/pricelist.py b/addons/product/pricelist.py
index 35be803ea39..34a87b8beaf 100644
--- a/addons/product/pricelist.py
+++ b/addons/product/pricelist.py
@@ -24,6 +24,7 @@ import time
from openerp import tools
from openerp.osv import fields, osv
from openerp.tools.translate import _
+from openerp.exceptions import except_orm
import openerp.addons.decimal_precision as dp
@@ -251,8 +252,11 @@ class product_pricelist(osv.osv):
price = False
rule_id = False
for rule in items:
- if 'uom' in context:
- qty_in_product_uom = product_uom_obj._compute_qty(cr, uid, context['uom'], qty, product.uom_id.id or product.uos_id.id)
+ if 'uom' in context and product.uom_id and context['uom'] != product.uom_id.id:
+ try:
+ qty_in_product_uom = product_uom_obj._compute_qty(cr, uid, context['uom'], qty, product.uom_id.id, dict(context.items() + [('raise-exception', False)]))
+ except except_orm:
+ qty_in_product_uom = qty
else:
qty_in_product_uom = qty
if rule.min_quantity and qty_in_product_uom
-
+
diff --git a/addons/project/security/project_security.xml b/addons/project/security/project_security.xml
index 649f518de84..444a9d03b40 100644
--- a/addons/project/security/project_security.xml
+++ b/addons/project/security/project_security.xml
@@ -82,7 +82,9 @@
['|',
('project_id.privacy_visibility', 'in', ['public', 'employees']),
'&',
- ('project_id.privacy_visibility', '=', 'followers'),
+ '|',
+ ('project_id', '=', False),
+ ('project_id.privacy_visibility', '=', 'followers'),
('message_follower_ids', 'in', [user.partner_id.id]),
]
diff --git a/addons/purchase_requisition/purchase_requisition_view.xml b/addons/purchase_requisition/purchase_requisition_view.xml
index 718c9719d02..e510d8afbeb 100644
--- a/addons/purchase_requisition/purchase_requisition_view.xml
+++ b/addons/purchase_requisition/purchase_requisition_view.xml
@@ -164,7 +164,7 @@
-
+
diff --git a/addons/sale/__openerp__.py b/addons/sale/__openerp__.py
index 78d7feabadc..18f9088eaf0 100644
--- a/addons/sale/__openerp__.py
+++ b/addons/sale/__openerp__.py
@@ -74,6 +74,7 @@ The Dashboard for the Sales Manager will include
'sales_team_view.xml',
'res_partner_view.xml',
'report/sale_report_view.xml',
+ 'report/invoice_report_view.xml',
'edi/sale_order_action_data.xml',
'res_config_view.xml',
'views/report_saleorder.xml',
diff --git a/addons/sale/report/invoice_report_view.xml b/addons/sale/report/invoice_report_view.xml
new file mode 100644
index 00000000000..73d50ea98da
--- /dev/null
+++ b/addons/sale/report/invoice_report_view.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ account.invoice.report.search
+ account.invoice.report
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addons/stock/static/src/js/widgets.js b/addons/stock/static/src/js/widgets.js
index 7fcfd051a0f..f9b1b48f799 100644
--- a/addons/stock/static/src/js/widgets.js
+++ b/addons/stock/static/src/js/widgets.js
@@ -479,7 +479,7 @@ function openerp_picking_widgets(instance){
var picking = pickings[i];
self.pickings_by_type[picking.picking_type_id[0]].push(picking);
self.pickings_by_id[picking.id] = picking;
- self.picking_search_string += '' + picking.id + ':' + picking.name.toUpperCase() + '\n'
+ self.picking_search_string += '' + picking.id + ':' + (picking.name ? picking.name.toUpperCase(): '') + '\n';
}
});
diff --git a/addons/stock_picking_wave/stock_picking_wave.py b/addons/stock_picking_wave/stock_picking_wave.py
index 0082d500d98..48779f6882c 100644
--- a/addons/stock_picking_wave/stock_picking_wave.py
+++ b/addons/stock_picking_wave/stock_picking_wave.py
@@ -4,6 +4,7 @@ from openerp.tools.translate import _
class stock_picking_wave(osv.osv):
_name = "stock.picking.wave"
+ _description = "Picking Wave"
_order = "name desc"
_columns = {
'name': fields.char('Picking Wave Name', required=True, help='Name of the picking wave', copy=False),
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index dd4d32aff35..296f4f1b9bc 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -1042,7 +1042,8 @@ class Binary(http.Controller):
@http.route('/web/binary/image', type='http', auth="public")
def image(self, model, id, field, **kw):
last_update = '__last_update'
- Model = request.session.model(model)
+ Model = request.registry[model]
+ cr, uid, context = request.cr, request.uid, request.context
headers = [('Content-Type', 'image/png')]
etag = request.httprequest.headers.get('If-None-Match')
hashed_session = hashlib.md5(request.session_id).hexdigest()
@@ -1055,15 +1056,15 @@ class Binary(http.Controller):
if not id and hashed_session == etag:
return werkzeug.wrappers.Response(status=304)
else:
- date = Model.read([id], [last_update], request.context)[0].get(last_update)
+ date = Model.read(cr, uid, [id], [last_update], context)[0].get(last_update)
if hashlib.md5(date).hexdigest() == etag:
return werkzeug.wrappers.Response(status=304)
if not id:
- res = Model.default_get([field], request.context).get(field)
+ res = Model.default_get(cr, uid, [field], context).get(field)
image_base64 = res
else:
- res = Model.read([id], [last_update, field], request.context)[0]
+ res = Model.read(cr, uid, [id], [last_update, field], context)[0]
retag = hashlib.md5(res.get(last_update)).hexdigest()
image_base64 = res.get(field)
@@ -1109,14 +1110,15 @@ class Binary(http.Controller):
:param str filename_field: field holding the file's name, if any
:returns: :class:`werkzeug.wrappers.Response`
"""
- Model = request.session.model(model)
+ Model = request.registry[model]
+ cr, uid, context = request.cr, request.uid, request.context
fields = [field]
if filename_field:
fields.append(filename_field)
if id:
- res = Model.read([int(id)], fields, request.context)[0]
+ res = Model.read(cr, uid, [int(id)], fields, context)[0]
else:
- res = Model.default_get(fields, request.context)
+ res = Model.default_get(cr, uid, fields, context)
filecontent = base64.b64decode(res.get(field, ''))
if not filecontent:
return request.not_found()
diff --git a/addons/web/static/src/js/formats.js b/addons/web/static/src/js/formats.js
index 0ec366d44ce..3dbacc98527 100644
--- a/addons/web/static/src/js/formats.js
+++ b/addons/web/static/src/js/formats.js
@@ -304,13 +304,13 @@ instance.web.auto_date_to_str = function(value, type) {
* performs a half up rounding with arbitrary precision, correcting for float loss of precision
* See the corresponding float_round() in server/tools/float_utils.py for more info
* @param {Number} the value to be rounded
- * @param {Number} a non zero precision parameter. eg: 0.01 rounds to two digits.
+ * @param {Number} a precision parameter. eg: 0.01 rounds to two digits.
*/
instance.web.round_precision = function(value, precision){
- if(!value){
+ if (!value) {
return 0;
- }else if(!precision){
- throw new Error('round_precision(...): Cannot round value: '+value+' with a precision of zero (or undefined)');
+ } else if (!precision || precision < 0) {
+ precision = 1;
}
var normalized_value = value / precision;
var epsilon_magnitude = Math.log(Math.abs(normalized_value))/Math.log(2);
diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js
index 48e2c99703b..20cba67c5a1 100644
--- a/addons/web/static/src/js/view_form.js
+++ b/addons/web/static/src/js/view_form.js
@@ -5732,7 +5732,7 @@ instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
* Options on attribute ; "blockui" {Boolean} block the UI or not
* during the file is uploading
*/
-instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({
+instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
template: "FieldBinaryFileUploader",
init: function(field_manager, node) {
this._super(field_manager, node);
@@ -5747,8 +5747,7 @@ instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractFie
this.fileupload_id = _.uniqueId('oe_fileupload_temp');
$(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
},
- start: function() {
- this._super(this);
+ initialize_content: function() {
this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
},
// WARNING: duplicated in 4 other M2M widgets
diff --git a/addons/web_kanban/static/src/css/kanban.css b/addons/web_kanban/static/src/css/kanban.css
index 4bcdab30a00..c64535526e0 100644
--- a/addons/web_kanban/static/src/css/kanban.css
+++ b/addons/web_kanban/static/src/css/kanban.css
@@ -6,6 +6,9 @@
background: white;
height: 100%;
}
+.openerp .oe_kanban_view.oe_background_grey > table.oe_kanban_groups {
+ background-color: #eee;
+}
.openerp .oe_kanban_view .oe_kanban_column_higlight {
background: #eeddf6 !important;
}
@@ -80,10 +83,6 @@
.openerp .oe_kanban_view .oe_kanban_content div:first-child {
margin-right: 16px;
}
-.openerp .oe_kanban_view .oe_kanban_button_new {
- color: white;
- background: #DC5F59;
-}
.openerp .oe_kanban_view .oe_kanban_groups {
height: inherit;
}
@@ -311,18 +310,6 @@
-webkit-border-radius: 4px;
margin-bottom: 5px;
}
-.openerp .oe_kanban_view .oe_kanban_box_header {
- border-bottom: 1px solid #CCC;
-}
-.openerp .oe_kanban_view .oe_kanban_title {
- font-size: 95%;
- font-weight: bold;
- padding: 0 4px 0 4px;
-}
-.openerp .oe_kanban_view .oe_kanban_small {
- font-size: 80%;
- font-weight: normal;
-}
.openerp .oe_kanban_view .oe_kanban_show_more {
clear: both;
text-align: center;
@@ -363,9 +350,6 @@
.openerp .oe_kanban_view .oe_kanban_color_border {
border-color: #CCCCCC;
}
-.openerp .oe_kanban_view .oe_kanban_color_border {
- border-color: #CCCCCC;
-}
.openerp .oe_kanban_view .oe_kanban_tooltip ul, .openerp .oe_kanban_view ul.oe_kanban_tooltip {
padding: 0 0 4px 0;
margin: 5px 0 0 15px;
@@ -605,7 +589,6 @@
-ms-writing-mode: lr-tb !important;
background: #f0eeee;
}
-
.openerp_ie .oe_kanban_view.oe_kanban_grouped .oe_kanban_group_header {
height: 1%;
}
diff --git a/addons/web_kanban/static/src/css/kanban.sass b/addons/web_kanban/static/src/css/kanban.sass
index 951ed9166fa..2d23a8e6d2e 100644
--- a/addons/web_kanban/static/src/css/kanban.sass
+++ b/addons/web_kanban/static/src/css/kanban.sass
@@ -54,6 +54,9 @@
//background: url(data:image/pngbase64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAKElEQVQIHWP8DwTv379nAAFBQUEGhnfv3oHEwADEZgJLIRGMIClkLQCr3x2Htp/lLwAAAABJRU5ErkJggg==)
background: white
height: 100%
+ &.oe_background_grey
+ > table.oe_kanban_groups
+ background-color: #eee
.oe_kanban_column_higlight
background: #eeddf6 !important
.oe_view_nocontent
@@ -475,7 +478,6 @@
.oe_kanban_column, .oe_kanban_group_header
padding: 0px
background: #ffffff
-
.openerp
.oe_popup_form
.oe_kanban_buttons
diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py
index 0f0226e375f..1ccd27235fe 100644
--- a/addons/website_forum/models/forum.py
+++ b/addons/website_forum/models/forum.py
@@ -324,7 +324,7 @@ class Post(models.Model):
for post in self:
if post.closed_reason_id in (reason_offensive, reason_spam):
_logger.info('Upvoting user <%s>, reopening spam/offensive question',
- post.create_uid.login)
+ post.create_uid)
# TODO: in master, consider making this a tunable karma parameter
post.create_uid.sudo().add_karma(post.forum_id.karma_gen_question_downvote * -5)
@@ -340,7 +340,7 @@ class Post(models.Model):
if reason_id in (reason_offensive, reason_spam):
for post in self:
_logger.info('Downvoting user <%s> for posting spam/offensive contents',
- post.create_uid.login)
+ post.create_uid)
# TODO: in master, consider making this a tunable karma parameter
post.create_uid.sudo().add_karma(post.forum_id.karma_gen_question_downvote * 5)
diff --git a/debian/rules b/debian/rules
index 2538f9f61e2..ae227e8947a 100755
--- a/debian/rules
+++ b/debian/rules
@@ -2,4 +2,4 @@
# vim:noet:
%:
- dh ${@}
\ No newline at end of file
+ dh ${@} -Spython_distutils
\ No newline at end of file
diff --git a/doc/_themes/odoodoc/layout.html b/doc/_themes/odoodoc/layout.html
index 8abb3fd7b69..7685d8af25f 100644
--- a/doc/_themes/odoodoc/layout.html
+++ b/doc/_themes/odoodoc/layout.html
@@ -6,7 +6,7 @@
'_static/bootstrap/js/tooltip.js',
'_static/bootstrap/js/collapse.js',
'_static/bootstrap/js/dropdown.js',
- '_static/ZeroClipboard.min.js',
+ '_static/zeroclipboard-2.1.6/ZeroClipboard.min.js',
'_static/app.js'
] %}
diff --git a/doc/_themes/odoodoc/static/ZeroClipboard.min.js b/doc/_themes/odoodoc/static/ZeroClipboard.min.js
deleted file mode 100644
index 56402340713..00000000000
--- a/doc/_themes/odoodoc/static/ZeroClipboard.min.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/*!
-* ZeroClipboard
-* The ZeroClipboard library provides an easy way to copy text to the clipboard using an invisible Adobe Flash movie and a JavaScript interface.
-* Copyright (c) 2014 Jon Rohan, James M. Greene
-* Licensed MIT
-* http://zeroclipboard.org/
-* v1.3.5
-*/
-!function(a){"use strict";function b(a){return a.replace(/,/g,".").replace(/[^0-9\.]/g,"")}function c(a){return parseFloat(b(a))>=10}var d,e={bridge:null,version:"0.0.0",disabled:null,outdated:null,ready:null},f={},g=0,h={},i=0,j={},k=null,l=null,m=function(){var a,b,c,d,e="ZeroClipboard.swf";if(document.currentScript&&(d=document.currentScript.src));else{var f=document.getElementsByTagName("script");if("readyState"in f[0])for(a=f.length;a--&&("interactive"!==f[a].readyState||!(d=f[a].src)););else if("loading"===document.readyState)d=f[f.length-1].src;else{for(a=f.length;a--;){if(c=f[a].src,!c){b=null;break}if(c=c.split("#")[0].split("?")[0],c=c.slice(0,c.lastIndexOf("/")+1),null==b)b=c;else if(b!==c){b=null;break}}null!==b&&(d=b)}}return d&&(d=d.split("#")[0].split("?")[0],e=d.slice(0,d.lastIndexOf("/")+1)+e),e}(),n=function(){var a=/\-([a-z])/g,b=function(a,b){return b.toUpperCase()};return function(c){return c.replace(a,b)}}(),o=function(b,c){var d,e,f;return a.getComputedStyle?d=a.getComputedStyle(b,null).getPropertyValue(c):(e=n(c),d=b.currentStyle?b.currentStyle[e]:b.style[e]),"cursor"!==c||d&&"auto"!==d||(f=b.tagName.toLowerCase(),"a"!==f)?d:"pointer"},p=function(b){b||(b=a.event);var c;this!==a?c=this:b.target?c=b.target:b.srcElement&&(c=b.srcElement),K.activate(c)},q=function(a,b,c){a&&1===a.nodeType&&(a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent&&a.attachEvent("on"+b,c))},r=function(a,b,c){a&&1===a.nodeType&&(a.removeEventListener?a.removeEventListener(b,c,!1):a.detachEvent&&a.detachEvent("on"+b,c))},s=function(a,b){if(!a||1!==a.nodeType)return a;if(a.classList)return a.classList.contains(b)||a.classList.add(b),a;if(b&&"string"==typeof b){var c=(b||"").split(/\s+/);if(1===a.nodeType)if(a.className){for(var d=" "+a.className+" ",e=a.className,f=0,g=c.length;g>f;f++)d.indexOf(" "+c[f]+" ")<0&&(e+=" "+c[f]);a.className=e.replace(/^\s+|\s+$/g,"")}else a.className=b}return a},t=function(a,b){if(!a||1!==a.nodeType)return a;if(a.classList)return a.classList.contains(b)&&a.classList.remove(b),a;if(b&&"string"==typeof b||void 0===b){var c=(b||"").split(/\s+/);if(1===a.nodeType&&a.className)if(b){for(var d=(" "+a.className+" ").replace(/[\n\t]/g," "),e=0,f=c.length;f>e;e++)d=d.replace(" "+c[e]+" "," ");a.className=d.replace(/^\s+|\s+$/g,"")}else a.className=""}return a},u=function(){var a,b,c,d=1;return"function"==typeof document.body.getBoundingClientRect&&(a=document.body.getBoundingClientRect(),b=a.right-a.left,c=document.body.offsetWidth,d=Math.round(b/c*100)/100),d},v=function(b,c){var d={left:0,top:0,width:0,height:0,zIndex:B(c)-1};if(b.getBoundingClientRect){var e,f,g,h=b.getBoundingClientRect();"pageXOffset"in a&&"pageYOffset"in a?(e=a.pageXOffset,f=a.pageYOffset):(g=u(),e=Math.round(document.documentElement.scrollLeft/g),f=Math.round(document.documentElement.scrollTop/g));var i=document.documentElement.clientLeft||0,j=document.documentElement.clientTop||0;d.left=h.left+e-i,d.top=h.top+f-j,d.width="width"in h?h.width:h.right-h.left,d.height="height"in h?h.height:h.bottom-h.top}return d},w=function(a,b){var c=null==b||b&&b.cacheBust===!0&&b.useNoCache===!0;return c?(-1===a.indexOf("?")?"?":"&")+"noCache="+(new Date).getTime():""},x=function(b){var c,d,e,f=[],g=[],h=[];if(b.trustedOrigins&&("string"==typeof b.trustedOrigins?g.push(b.trustedOrigins):"object"==typeof b.trustedOrigins&&"length"in b.trustedOrigins&&(g=g.concat(b.trustedOrigins))),b.trustedDomains&&("string"==typeof b.trustedDomains?g.push(b.trustedDomains):"object"==typeof b.trustedDomains&&"length"in b.trustedDomains&&(g=g.concat(b.trustedDomains))),g.length)for(c=0,d=g.length;d>c;c++)if(g.hasOwnProperty(c)&&g[c]&&"string"==typeof g[c]){if(e=E(g[c]),!e)continue;if("*"===e){h=[e];break}h.push.apply(h,[e,"//"+e,a.location.protocol+"//"+e])}return h.length&&f.push("trustedOrigins="+encodeURIComponent(h.join(","))),"string"==typeof b.jsModuleId&&b.jsModuleId&&f.push("jsModuleId="+encodeURIComponent(b.jsModuleId)),f.join("&")},y=function(a,b,c){if("function"==typeof b.indexOf)return b.indexOf(a,c);var d,e=b.length;for("undefined"==typeof c?c=0:0>c&&(c=e+c),d=c;e>d;d++)if(b.hasOwnProperty(d)&&b[d]===a)return d;return-1},z=function(a){if("string"==typeof a)throw new TypeError("ZeroClipboard doesn't accept query strings.");return a.length?a:[a]},A=function(b,c,d,e){e?a.setTimeout(function(){b.apply(c,d)},0):b.apply(c,d)},B=function(a){var b,c;return a&&("number"==typeof a&&a>0?b=a:"string"==typeof a&&(c=parseInt(a,10))&&!isNaN(c)&&c>0&&(b=c)),b||("number"==typeof N.zIndex&&N.zIndex>0?b=N.zIndex:"string"==typeof N.zIndex&&(c=parseInt(N.zIndex,10))&&!isNaN(c)&&c>0&&(b=c)),b||0},C=function(a,b){if(a&&b!==!1&&"undefined"!=typeof console&&console&&(console.warn||console.log)){var c="`"+a+"` is deprecated. See docs for more info:\n https://github.com/zeroclipboard/zeroclipboard/blob/master/docs/instructions.md#deprecations";console.warn?console.warn(c):console.log(c)}},D=function(){var a,b,c,d,e,f,g=arguments[0]||{};for(a=1,b=arguments.length;b>a;a++)if(null!=(c=arguments[a]))for(d in c)if(c.hasOwnProperty(d)){if(e=g[d],f=c[d],g===f)continue;void 0!==f&&(g[d]=f)}return g},E=function(a){if(null==a||""===a)return null;if(a=a.replace(/^\s+|\s+$/g,""),""===a)return null;var b=a.indexOf("//");a=-1===b?a:a.slice(b+2);var c=a.indexOf("/");return a=-1===c?a:-1===b||0===c?null:a.slice(0,c),a&&".swf"===a.slice(-4).toLowerCase()?null:a||null},F=function(){var a=function(a,b){var c,d,e;if(null!=a&&"*"!==b[0]&&("string"==typeof a&&(a=[a]),"object"==typeof a&&"length"in a))for(c=0,d=a.length;d>c;c++)if(a.hasOwnProperty(c)&&(e=E(a[c]))){if("*"===e){b.length=0,b.push("*");break}-1===y(e,b)&&b.push(e)}},b={always:"always",samedomain:"sameDomain",never:"never"};return function(c,d){var e,f=d.allowScriptAccess;if("string"==typeof f&&(e=f.toLowerCase())&&/^always|samedomain|never$/.test(e))return b[e];var g=E(d.moviePath);null===g&&(g=c);var h=[];a(d.trustedOrigins,h),a(d.trustedDomains,h);var i=h.length;if(i>0){if(1===i&&"*"===h[0])return"always";if(-1!==y(c,h))return 1===i&&c===g?"sameDomain":"always"}return"never"}}(),G=function(a){if(null==a)return[];if(Object.keys)return Object.keys(a);var b=[];for(var c in a)a.hasOwnProperty(c)&&b.push(c);return b},H=function(a){if(a)for(var b in a)a.hasOwnProperty(b)&&delete a[b];return a},I=function(){try{return document.activeElement}catch(a){}return null},J=function(){var a=!1;if("boolean"==typeof e.disabled)a=e.disabled===!1;else{if("function"==typeof ActiveXObject)try{new ActiveXObject("ShockwaveFlash.ShockwaveFlash")&&(a=!0)}catch(b){}!a&&navigator.mimeTypes["application/x-shockwave-flash"]&&(a=!0)}return a},K=function(a,b){return this instanceof K?(this.id=""+g++,h[this.id]={instance:this,elements:[],handlers:{}},a&&this.clip(a),"undefined"!=typeof b&&(C("new ZeroClipboard(elements, options)",N.debug),K.config(b)),this.options=K.config(),"boolean"!=typeof e.disabled&&(e.disabled=!J()),e.disabled===!1&&e.outdated!==!0&&null===e.bridge&&(e.outdated=!1,e.ready=!1,O()),void 0):new K(a,b)};K.prototype.setText=function(a){return a&&""!==a&&(f["text/plain"]=a,e.ready===!0&&e.bridge&&"function"==typeof e.bridge.setText?e.bridge.setText(a):e.ready=!1),this},K.prototype.setSize=function(a,b){return e.ready===!0&&e.bridge&&"function"==typeof e.bridge.setSize?e.bridge.setSize(a,b):e.ready=!1,this};var L=function(a){e.ready===!0&&e.bridge&&"function"==typeof e.bridge.setHandCursor?e.bridge.setHandCursor(a):e.ready=!1};K.prototype.destroy=function(){this.unclip(),this.off(),delete h[this.id]};var M=function(){var a,b,c,d=[],e=G(h);for(a=0,b=e.length;b>a;a++)c=h[e[a]].instance,c&&c instanceof K&&d.push(c);return d};K.version="1.3.5";var N={swfPath:m,trustedDomains:a.location.host?[a.location.host]:[],cacheBust:!0,forceHandCursor:!1,zIndex:999999999,debug:!0,title:null,autoActivate:!0};K.config=function(a){"object"==typeof a&&null!==a&&D(N,a);{if("string"!=typeof a||!a){var b={};for(var c in N)N.hasOwnProperty(c)&&(b[c]="object"==typeof N[c]&&null!==N[c]?"length"in N[c]?N[c].slice(0):D({},N[c]):N[c]);return b}if(N.hasOwnProperty(a))return N[a]}},K.destroy=function(){K.deactivate();for(var a in h)if(h.hasOwnProperty(a)&&h[a]){var b=h[a].instance;b&&"function"==typeof b.destroy&&b.destroy()}var c=P(e.bridge);c&&c.parentNode&&(c.parentNode.removeChild(c),e.ready=null,e.bridge=null)},K.activate=function(a){d&&(t(d,N.hoverClass),t(d,N.activeClass)),d=a,s(a,N.hoverClass),Q();var b=N.title||a.getAttribute("title");if(b){var c=P(e.bridge);c&&c.setAttribute("title",b)}var f=N.forceHandCursor===!0||"pointer"===o(a,"cursor");L(f)},K.deactivate=function(){var a=P(e.bridge);a&&(a.style.left="0px",a.style.top="-9999px",a.removeAttribute("title")),d&&(t(d,N.hoverClass),t(d,N.activeClass),d=null)};var O=function(){var b,c,d=document.getElementById("global-zeroclipboard-html-bridge");if(!d){var f=K.config();f.jsModuleId="string"==typeof k&&k||"string"==typeof l&&l||null;var g=F(a.location.host,N),h=x(f),i=N.moviePath+w(N.moviePath,N),j=' ';d=document.createElement("div"),d.id="global-zeroclipboard-html-bridge",d.setAttribute("class","global-zeroclipboard-container"),d.style.position="absolute",d.style.left="0px",d.style.top="-9999px",d.style.width="15px",d.style.height="15px",d.style.zIndex=""+B(N.zIndex),document.body.appendChild(d),d.innerHTML=j}b=document["global-zeroclipboard-flash-bridge"],b&&(c=b.length)&&(b=b[c-1]),e.bridge=b||d.children[0].lastElementChild},P=function(a){for(var b=/^OBJECT|EMBED$/,c=a&&a.parentNode;c&&b.test(c.nodeName)&&c.parentNode;)c=c.parentNode;return c||null},Q=function(){if(d){var a=v(d,N.zIndex),b=P(e.bridge);b&&(b.style.top=a.top+"px",b.style.left=a.left+"px",b.style.width=a.width+"px",b.style.height=a.height+"px",b.style.zIndex=a.zIndex+1),e.ready===!0&&e.bridge&&"function"==typeof e.bridge.setSize?e.bridge.setSize(a.width,a.height):e.ready=!1}return this};K.prototype.on=function(a,b){var c,d,f,g={},i=h[this.id]&&h[this.id].handlers;if("string"==typeof a&&a)f=a.toLowerCase().split(/\s+/);else if("object"==typeof a&&a&&"undefined"==typeof b)for(c in a)a.hasOwnProperty(c)&&"string"==typeof c&&c&&"function"==typeof a[c]&&this.on(c,a[c]);if(f&&f.length){for(c=0,d=f.length;d>c;c++)a=f[c].replace(/^on/,""),g[a]=!0,i[a]||(i[a]=[]),i[a].push(b);g.noflash&&e.disabled&&T.call(this,"noflash",{}),g.wrongflash&&e.outdated&&T.call(this,"wrongflash",{flashVersion:e.version}),g.load&&e.ready&&T.call(this,"load",{flashVersion:e.version})}return this},K.prototype.off=function(a,b){var c,d,e,f,g,i=h[this.id]&&h[this.id].handlers;if(0===arguments.length)f=G(i);else if("string"==typeof a&&a)f=a.split(/\s+/);else if("object"==typeof a&&a&&"undefined"==typeof b)for(c in a)a.hasOwnProperty(c)&&"string"==typeof c&&c&&"function"==typeof a[c]&&this.off(c,a[c]);if(f&&f.length)for(c=0,d=f.length;d>c;c++)if(a=f[c].toLowerCase().replace(/^on/,""),g=i[a],g&&g.length)if(b)for(e=y(b,g);-1!==e;)g.splice(e,1),e=y(b,g,e);else i[a].length=0;return this},K.prototype.handlers=function(a){var b,c=null,d=h[this.id]&&h[this.id].handlers;if(d){if("string"==typeof a&&a)return d[a]?d[a].slice(0):null;c={};for(b in d)d.hasOwnProperty(b)&&d[b]&&(c[b]=d[b].slice(0))}return c};var R=function(b,c,d,e){var f=h[this.id]&&h[this.id].handlers[b];if(f&&f.length){var g,i,j,k=c||this;for(g=0,i=f.length;i>g;g++)j=f[g],c=k,"string"==typeof j&&"function"==typeof a[j]&&(j=a[j]),"object"==typeof j&&j&&"function"==typeof j.handleEvent&&(c=j,j=j.handleEvent),"function"==typeof j&&A(j,c,d,e)}return this};K.prototype.clip=function(a){a=z(a);for(var b=0;bd;d++)f=h[c[d]].instance,f&&f instanceof K&&g.push(f);return g};N.hoverClass="zeroclipboard-is-hover",N.activeClass="zeroclipboard-is-active",N.trustedOrigins=null,N.allowScriptAccess=null,N.useNoCache=!0,N.moviePath="ZeroClipboard.swf",K.detectFlashSupport=function(){return C("ZeroClipboard.detectFlashSupport",N.debug),J()},K.dispatch=function(a,b){if("string"==typeof a&&a){var c=a.toLowerCase().replace(/^on/,"");if(c)for(var e=d&&N.autoActivate===!0?S(d):M(),f=0,g=e.length;g>f;f++)T.call(e[f],c,b)}},K.prototype.setHandCursor=function(a){return C("ZeroClipboard.prototype.setHandCursor",N.debug),a="boolean"==typeof a?a:!!a,L(a),N.forceHandCursor=a,this},K.prototype.reposition=function(){return C("ZeroClipboard.prototype.reposition",N.debug),Q()},K.prototype.receiveEvent=function(a,b){if(C("ZeroClipboard.prototype.receiveEvent",N.debug),"string"==typeof a&&a){var c=a.toLowerCase().replace(/^on/,"");c&&T.call(this,c,b)}},K.prototype.setCurrent=function(a){return C("ZeroClipboard.prototype.setCurrent",N.debug),K.activate(a),this},K.prototype.resetBridge=function(){return C("ZeroClipboard.prototype.resetBridge",N.debug),K.deactivate(),this},K.prototype.setTitle=function(a){if(C("ZeroClipboard.prototype.setTitle",N.debug),a=a||N.title||d&&d.getAttribute("title")){var b=P(e.bridge);b&&b.setAttribute("title",a)}return this},K.setDefaults=function(a){C("ZeroClipboard.setDefaults",N.debug),K.config(a)},K.prototype.addEventListener=function(a,b){return C("ZeroClipboard.prototype.addEventListener",N.debug),this.on(a,b)},K.prototype.removeEventListener=function(a,b){return C("ZeroClipboard.prototype.removeEventListener",N.debug),this.off(a,b)},K.prototype.ready=function(){return C("ZeroClipboard.prototype.ready",N.debug),e.ready===!0};var T=function(a,g){a=a.toLowerCase().replace(/^on/,"");var h=g&&g.flashVersion&&b(g.flashVersion)||null,i=d,j=!0;switch(a){case"load":if(h){if(!c(h))return T.call(this,"onWrongFlash",{flashVersion:h}),void 0;e.outdated=!1,e.ready=!0,e.version=h}break;case"wrongflash":h&&!c(h)&&(e.outdated=!0,e.ready=!1,e.version=h);break;case"mouseover":s(i,N.hoverClass);break;case"mouseout":N.autoActivate===!0&&K.deactivate();break;case"mousedown":s(i,N.activeClass);break;case"mouseup":t(i,N.activeClass);break;case"datarequested":if(i){var k=i.getAttribute("data-clipboard-target"),l=k?document.getElementById(k):null;if(l){var m=l.value||l.textContent||l.innerText;m&&this.setText(m)}else{var n=i.getAttribute("data-clipboard-text");n&&this.setText(n)}}j=!1;break;case"complete":H(f),i&&i!==I()&&i.focus&&i.focus()}var o=i,p=[this,g];return R.call(this,a,o,p,j)};"function"==typeof define&&define.amd?define(["require","exports","module"],function(a,b,c){return k=c&&c.id||null,K}):"object"==typeof module&&module&&"object"==typeof module.exports&&module.exports&&"function"==typeof a.require?(l=module.id||null,module.exports=K):a.ZeroClipboard=K}(function(){return this}());
\ No newline at end of file
diff --git a/doc/_themes/odoodoc/static/ZeroClipboard.swf b/doc/_themes/odoodoc/static/ZeroClipboard.swf
deleted file mode 100644
index 55ccf9608ed..00000000000
Binary files a/doc/_themes/odoodoc/static/ZeroClipboard.swf and /dev/null differ
diff --git a/doc/_themes/odoodoc/static/app.js b/doc/_themes/odoodoc/static/app.js
index a03d3b2ad27..47c33f41d23 100644
--- a/doc/_themes/odoodoc/static/app.js
+++ b/doc/_themes/odoodoc/static/app.js
@@ -25,7 +25,122 @@ $(function () {
});
}, 100);
- // lang switcher
+ /*
+ for clipboard:
+ * add per-language setup code to document, hidden
+ * adds button to each switchable language block except when they're setup
+ stuff because fuck'em
+ * per-language, add clipboard hook to prefix with setup bit on-copy
+ * setup bit is... ?
+ * actually not all blocks because we don't want to add the setup bits to
+ the setup bits, so that's kinda shit
+ */
+
+ document.addEventListener('copy', copyCode);
+
+ ZeroClipboard.config({
+ swfPath: '../_static/zeroclipboard-2.1.6/ZeroClipboard.swf',
+ flashLoadTimeout: 3e3,
+ });
+ var zc = new ZeroClipboard();
+ zc.on('ready', function () {
+ var $highlighted = $('.switchable:not(.setup) .highlight').addClass('with-btn-clipboard');
+ var $clipboard_buttons =
+ $('')
+ .on('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', function () {
+ $(this).removeClass('active');
+ })
+ .prependTo($highlighted);
+ zc.clip($clipboard_buttons);
+ });
+ zc.on('copy', function (e) {
+ // yellow flash
+ $(e.target).addClass('active');
+ copyCode(e);
+ });
+
+ // stripe page stuff
+ if ($('div.document-super').hasClass('stripe')) { (function () {
+ // iterate on highlighted PL blocks (but not results because that'd
+ // be gross), extract all switchable PLs in the document and add
+ // clipboard-copy buttons
+ var languages = {};
+ $('div.switchable').each(function () {
+ var language = getHighlightLanguage(this);
+ if (language) {
+ languages[language] = true;
+ }
+ });
+
+ // if can't find CSS where base rule lives something's probably
+ // broken, bail
+ var sheet = findSheet(/style\.css$/);
+ if (!sheet) { return; }
+ // build PL switcher UI and hook toggle event
+ $(buildSwitcher(Object.keys(languages)))
+ .prependTo('div.documentwrapper')
+ .on('click', 'li', function (e) {
+ $(e.target).addClass('active')
+ .siblings().removeClass('active');
+ var id = e.target.textContent;
+ var lastIndex = sheet.cssRules.length - 1;
+ var content = sheet.cssRules[lastIndex].style.cssText;
+ // change rule in CSS because why not (also can add new
+ // languages without having to e.g. change CSS or anything)
+ var sel = [
+ '.stripe .only-', id, ', ',
+ '.stripe .highlight-', id, ' > .highlight'
+ ].join('');
+ sheet.deleteRule(lastIndex);
+ sheet.insertRule(sel + '{' + content + '}', lastIndex);
+ });
+ })(); }
+
+
+ function copyCode(e) {
+ // works for both C-c and "Copy" button if copy button is injected
+ // inside highlighted code section
+ var target = $(e.target).closest('.switchable:not(.setup)').get(0);
+ // not in a switchable
+ if (!target) { return; }
+ var lang = getHighlightLanguage(target);
+ if (!lang) {
+ // switchable without highlight (e.g. language-specific notes),
+ // don't munge
+ return;
+ }
+
+ // get generic setup code
+ var setup_selector = '.setupcode.highlight-' + lang + ' pre';
+ var setup = document.querySelector(setup_selector).textContent;
+
+ // prepend setup code to current snippet, get all of current snippet
+ // in case only part of it was selected. Ensure we don't get e.g.
+ // button text around snippet itself
+ var data = setup + '\n' + target.querySelector('pre').textContent;
+ // sane browsers & ZeroClipboard
+ e.clipboardData.setData('text/plain', data);
+ // MSIE
+ e.clipboardData.setData('Text', data);
+
+ // no preventDefault on ZC event
+ e.preventDefault && e.preventDefault();
+ }
+ /**
+ * @param {Node} node highlight node to get the language of
+ * @returns {String|null} either the highlight language or null
+ */
+ function getHighlightLanguage(node) {
+ var classes = node.className.split(/\s+/);
+ for (var i = 0; i < classes.length; ++i) {
+ var cls = classes[i];
+ if (/^highlight-/.test(cls)) {
+ return cls.slice(10);
+ }
+ }
+ return null;
+ }
+ // programming language switcher
function findSheet(pattern, fromSheet) {
if (fromSheet) {
for(var i=0; i .highlight'
- ].join('');
- sheet.deleteRule(lastIndex);
- sheet.insertRule(sel + '{' + content + '}', lastIndex);
- });
- })(); }
-
- // Config ZeroClipboard
- ZeroClipboard.config({
- moviePath: '_static/ZeroClipboard.swf',
- hoverClass: 'btn-clipboard-hover'
- });
-
- // Insert copy to clipboard button before .highlight or .example
- $('.highlight-html, .highlight-scss').each(function () {
- var highlight = $(this);
- var previous = highlight.prev();
- var btnHtml = '
Copy
';
-
- if (previous.hasClass('example')) {
- previous.before(btnHtml.replace(/btn-clipboard/, 'btn-clipboard with-example'));
- } else {
- highlight.before(btnHtml);
- }
- });
- var zeroClipboard = new ZeroClipboard($('.btn-clipboard'));
- var htmlBridge = $('#global-zeroclipboard-html-bridge');
-
- // Handlers for ZeroClipboard
- zeroClipboard.on('load', function () {
- htmlBridge
- .data('placement', 'top')
- .attr('title', 'Copy to clipboard')
- .tooltip();
- });
-
- // Copy to clipboard
- zeroClipboard.on('dataRequested', function (client) {
- var highlight = $(this).parent().nextAll('.highlight').first();
- client.setText(highlight.text());
- });
-
- // Notify copy success and reset tooltip title
- zeroClipboard.on('complete', function () {
- htmlBridge
- .attr('title', 'Copied!')
- .tooltip('fixTitle')
- .tooltip('show')
- .attr('title', 'Copy to clipboard')
- .tooltip('fixTitle');
- });
-
- // Notify copy failure
- zeroClipboard.on('noflash wrongflash', function () {
- htmlBridge.attr('title', 'Flash required')
- .tooltip('fixTitle')
- .tooltip('show');
- });
});
diff --git a/doc/_themes/odoodoc/static/style.css b/doc/_themes/odoodoc/static/style.css
index fba330944dd..19f5270109e 100644
--- a/doc/_themes/odoodoc/static/style.css
+++ b/doc/_themes/odoodoc/static/style.css
@@ -6653,23 +6653,20 @@ div.section > h2 {
word-break: normal;
word-wrap: normal;
}
+.switchable .highlight.with-btn-clipboard pre {
+ padding-right: 50px;
+}
/*
* ZeroClipboard styles
*/
-.zero-clipboard {
+.highlight {
position: relative;
- display: none;
-}
-@media (min-width: 768px) {
- .zero-clipboard {
- display: block;
- }
}
.btn-clipboard {
position: absolute;
top: 0;
right: 0;
- z-index: 10;
+ z-index: 3;
display: block;
padding: 5px 8px;
font-size: 12px;
@@ -6679,10 +6676,14 @@ div.section > h2 {
border: 1px solid #e1e1e8;
border-radius: 0 4px 0 4px;
}
-.btn-clipboard-hover {
- color: #fff;
- background-color: #a24689;
- border-color: #a24689;
+.btn-clipboard:focus {
+ outline: none;
+}
+.btn-clipboard.active {
+ background-color: #ffffad;
+ -webkit-transition: background-color 0.5s linear;
+ -o-transition: background-color 0.5s linear;
+ transition: background-color 0.5s linear;
}
img.align-center {
display: block;
@@ -6756,6 +6757,9 @@ td.field-body > ul {
width: 0;
border-left: 1px solid #777777;
}
+ .stripe .highlight pre {
+ white-space: pre-wrap;
+ }
}
.stripe .switcher {
color: white;
diff --git a/doc/_themes/odoodoc/static/style.less b/doc/_themes/odoodoc/static/style.less
index 71076470e7b..719fa6f10e3 100644
--- a/doc/_themes/odoodoc/static/style.less
+++ b/doc/_themes/odoodoc/static/style.less
@@ -469,27 +469,26 @@ div.section > h2 {
padding: 4px;
font-size: 75%;
- // code block lines should not wrap
+ // don't break lines within words
word-break: normal;
word-wrap: normal;
}
+.switchable .highlight.with-btn-clipboard pre {
+ // avoid copy button overlapping with even long lines
+ padding-right: 50px;
+}
/*
* ZeroClipboard styles
*/
-
-.zero-clipboard {
+.highlight {
position: relative;
- display: none;
- @media (min-width: @screen-sm-min) {
- display: block;
- }
}
.btn-clipboard {
position: absolute;
top: 0;
right: 0;
- z-index: 10;
+ z-index: 3;
display: block;
padding: 5px 8px;
font-size: 12px;
@@ -498,11 +497,15 @@ div.section > h2 {
background-color: #fff;
border: 1px solid #e1e1e8;
border-radius: 0 4px 0 4px;
-}
-.btn-clipboard-hover {
- color: #fff;
- background-color: @brand-primary;
- border-color: @brand-primary;
+
+ &:focus {
+ outline: none;
+ }
+
+ &.active {
+ background-color: #ffffad;
+ .transition(background-color .5s linear);
+ }
}
// rST styles
@@ -592,6 +595,11 @@ td.field-body {
border-left: 1px solid @gray-light;
}
}
+
+ .highlight pre {
+ // but allow additional line-breaks between "words" to avoid overflow
+ white-space: pre-wrap;
+ }
}
.switcher {
diff --git a/doc/_themes/odoodoc/static/zeroclipboard-2.1.6/ZeroClipboard.min.js b/doc/_themes/odoodoc/static/zeroclipboard-2.1.6/ZeroClipboard.min.js
new file mode 100644
index 00000000000..365737f559a
--- /dev/null
+++ b/doc/_themes/odoodoc/static/zeroclipboard-2.1.6/ZeroClipboard.min.js
@@ -0,0 +1,9 @@
+/*!
+ * ZeroClipboard
+ * The ZeroClipboard library provides an easy way to copy text to the clipboard using an invisible Adobe Flash movie and a JavaScript interface.
+ * Copyright (c) 2014 Jon Rohan, James M. Greene
+ * Licensed MIT
+ * http://zeroclipboard.org/
+ * v2.1.6
+ */
+!function(a,b){"use strict";var c,d,e=a,f=e.document,g=e.navigator,h=e.setTimeout,i=e.encodeURIComponent,j=e.ActiveXObject,k=e.Error,l=e.Number.parseInt||e.parseInt,m=e.Number.parseFloat||e.parseFloat,n=e.Number.isNaN||e.isNaN,o=e.Math.round,p=e.Date.now,q=e.Object.keys,r=e.Object.defineProperty,s=e.Object.prototype.hasOwnProperty,t=e.Array.prototype.slice,u=function(){var a=function(a){return a};if("function"==typeof e.wrap&&"function"==typeof e.unwrap)try{var b=f.createElement("div"),c=e.unwrap(b);1===b.nodeType&&c&&1===c.nodeType&&(a=e.unwrap)}catch(d){}return a}(),v=function(a){return t.call(a,0)},w=function(){var a,c,d,e,f,g,h=v(arguments),i=h[0]||{};for(a=1,c=h.length;c>a;a++)if(null!=(d=h[a]))for(e in d)s.call(d,e)&&(f=i[e],g=d[e],i!==g&&g!==b&&(i[e]=g));return i},x=function(a){var b,c,d,e;if("object"!=typeof a||null==a)b=a;else if("number"==typeof a.length)for(b=[],c=0,d=a.length;d>c;c++)s.call(a,c)&&(b[c]=x(a[c]));else{b={};for(e in a)s.call(a,e)&&(b[e]=x(a[e]))}return b},y=function(a,b){for(var c={},d=0,e=b.length;e>d;d++)b[d]in a&&(c[b[d]]=a[b[d]]);return c},z=function(a,b){var c={};for(var d in a)-1===b.indexOf(d)&&(c[d]=a[d]);return c},A=function(a){if(a)for(var b in a)s.call(a,b)&&delete a[b];return a},B=function(a,b){if(a&&1===a.nodeType&&a.ownerDocument&&b&&(1===b.nodeType&&b.ownerDocument&&b.ownerDocument===a.ownerDocument||9===b.nodeType&&!b.ownerDocument&&b===a.ownerDocument))do{if(a===b)return!0;a=a.parentNode}while(a);return!1},C=function(a){var b;return"string"==typeof a&&a&&(b=a.split("#")[0].split("?")[0],b=a.slice(0,a.lastIndexOf("/")+1)),b},D=function(a){var b,c;return"string"==typeof a&&a&&(c=a.match(/^(?:|[^:@]*@|.+\)@(?=http[s]?|file)|.+?\s+(?: at |@)(?:[^:\(]+ )*[\(]?)((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/),c&&c[1]?b=c[1]:(c=a.match(/\)@((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/),c&&c[1]&&(b=c[1]))),b},E=function(){var a,b;try{throw new k}catch(c){b=c}return b&&(a=b.sourceURL||b.fileName||D(b.stack)),a},F=function(){var a,c,d;if(f.currentScript&&(a=f.currentScript.src))return a;if(c=f.getElementsByTagName("script"),1===c.length)return c[0].src||b;if("readyState"in c[0])for(d=c.length;d--;)if("interactive"===c[d].readyState&&(a=c[d].src))return a;return"loading"===f.readyState&&(a=c[c.length-1].src)?a:(a=E())?a:b},G=function(){var a,c,d,e=f.getElementsByTagName("script");for(a=e.length;a--;){if(!(d=e[a].src)){c=null;break}if(d=C(d),null==c)c=d;else if(c!==d){c=null;break}}return c||b},H=function(){var a=C(F())||G()||"";return a+"ZeroClipboard.swf"},I={bridge:null,version:"0.0.0",pluginType:"unknown",disabled:null,outdated:null,unavailable:null,deactivated:null,overdue:null,ready:null},J="11.0.0",K={},L={},M=null,N={ready:"Flash communication is established",error:{"flash-disabled":"Flash is disabled or not installed","flash-outdated":"Flash is too outdated to support ZeroClipboard","flash-unavailable":"Flash is unable to communicate bidirectionally with JavaScript","flash-deactivated":"Flash is too outdated for your browser and/or is configured as click-to-activate","flash-overdue":"Flash communication was established but NOT within the acceptable time limit"}},O={swfPath:H(),trustedDomains:a.location.host?[a.location.host]:[],cacheBust:!0,forceEnhancedClipboard:!1,flashLoadTimeout:3e4,autoActivate:!0,bubbleEvents:!0,containerId:"global-zeroclipboard-html-bridge",containerClass:"global-zeroclipboard-container",swfObjectId:"global-zeroclipboard-flash-bridge",hoverClass:"zeroclipboard-is-hover",activeClass:"zeroclipboard-is-active",forceHandCursor:!1,title:null,zIndex:999999999},P=function(a){if("object"==typeof a&&null!==a)for(var b in a)if(s.call(a,b))if(/^(?:forceHandCursor|title|zIndex|bubbleEvents)$/.test(b))O[b]=a[b];else if(null==I.bridge)if("containerId"===b||"swfObjectId"===b){if(!cb(a[b]))throw new Error("The specified `"+b+"` value is not valid as an HTML4 Element ID");O[b]=a[b]}else O[b]=a[b];{if("string"!=typeof a||!a)return x(O);if(s.call(O,a))return O[a]}},Q=function(){return{browser:y(g,["userAgent","platform","appName"]),flash:z(I,["bridge"]),zeroclipboard:{version:Fb.version,config:Fb.config()}}},R=function(){return!!(I.disabled||I.outdated||I.unavailable||I.deactivated)},S=function(a,b){var c,d,e,f={};if("string"==typeof a&&a)e=a.toLowerCase().split(/\s+/);else if("object"==typeof a&&a&&"undefined"==typeof b)for(c in a)s.call(a,c)&&"string"==typeof c&&c&&"function"==typeof a[c]&&Fb.on(c,a[c]);if(e&&e.length){for(c=0,d=e.length;d>c;c++)a=e[c].replace(/^on/,""),f[a]=!0,K[a]||(K[a]=[]),K[a].push(b);if(f.ready&&I.ready&&Fb.emit({type:"ready"}),f.error){var g=["disabled","outdated","unavailable","deactivated","overdue"];for(c=0,d=g.length;d>c;c++)if(I[g[c]]===!0){Fb.emit({type:"error",name:"flash-"+g[c]});break}}}return Fb},T=function(a,b){var c,d,e,f,g;if(0===arguments.length)f=q(K);else if("string"==typeof a&&a)f=a.split(/\s+/);else if("object"==typeof a&&a&&"undefined"==typeof b)for(c in a)s.call(a,c)&&"string"==typeof c&&c&&"function"==typeof a[c]&&Fb.off(c,a[c]);if(f&&f.length)for(c=0,d=f.length;d>c;c++)if(a=f[c].toLowerCase().replace(/^on/,""),g=K[a],g&&g.length)if(b)for(e=g.indexOf(b);-1!==e;)g.splice(e,1),e=g.indexOf(b,e);else g.length=0;return Fb},U=function(a){var b;return b="string"==typeof a&&a?x(K[a])||null:x(K)},V=function(a){var b,c,d;return a=db(a),a&&!jb(a)?"ready"===a.type&&I.overdue===!0?Fb.emit({type:"error",name:"flash-overdue"}):(b=w({},a),ib.call(this,b),"copy"===a.type&&(d=pb(L),c=d.data,M=d.formatMap),c):void 0},W=function(){if("boolean"!=typeof I.ready&&(I.ready=!1),!Fb.isFlashUnusable()&&null===I.bridge){var a=O.flashLoadTimeout;"number"==typeof a&&a>=0&&h(function(){"boolean"!=typeof I.deactivated&&(I.deactivated=!0),I.deactivated===!0&&Fb.emit({type:"error",name:"flash-deactivated"})},a),I.overdue=!1,nb()}},X=function(){Fb.clearData(),Fb.blur(),Fb.emit("destroy"),ob(),Fb.off()},Y=function(a,b){var c;if("object"==typeof a&&a&&"undefined"==typeof b)c=a,Fb.clearData();else{if("string"!=typeof a||!a)return;c={},c[a]=b}for(var d in c)"string"==typeof d&&d&&s.call(c,d)&&"string"==typeof c[d]&&c[d]&&(L[d]=c[d])},Z=function(a){"undefined"==typeof a?(A(L),M=null):"string"==typeof a&&s.call(L,a)&&delete L[a]},$=function(a){return"undefined"==typeof a?x(L):"string"==typeof a&&s.call(L,a)?L[a]:void 0},_=function(a){if(a&&1===a.nodeType){c&&(xb(c,O.activeClass),c!==a&&xb(c,O.hoverClass)),c=a,wb(a,O.hoverClass);var b=a.getAttribute("title")||O.title;if("string"==typeof b&&b){var d=mb(I.bridge);d&&d.setAttribute("title",b)}var e=O.forceHandCursor===!0||"pointer"===yb(a,"cursor");Cb(e),Bb()}},ab=function(){var a=mb(I.bridge);a&&(a.removeAttribute("title"),a.style.left="0px",a.style.top="-9999px",a.style.width="1px",a.style.top="1px"),c&&(xb(c,O.hoverClass),xb(c,O.activeClass),c=null)},bb=function(){return c||null},cb=function(a){return"string"==typeof a&&a&&/^[A-Za-z][A-Za-z0-9_:\-\.]*$/.test(a)},db=function(a){var b;if("string"==typeof a&&a?(b=a,a={}):"object"==typeof a&&a&&"string"==typeof a.type&&a.type&&(b=a.type),b){!a.target&&/^(copy|aftercopy|_click)$/.test(b.toLowerCase())&&(a.target=d),w(a,{type:b.toLowerCase(),target:a.target||c||null,relatedTarget:a.relatedTarget||null,currentTarget:I&&I.bridge||null,timeStamp:a.timeStamp||p()||null});var e=N[a.type];return"error"===a.type&&a.name&&e&&(e=e[a.name]),e&&(a.message=e),"ready"===a.type&&w(a,{target:null,version:I.version}),"error"===a.type&&(/^flash-(disabled|outdated|unavailable|deactivated|overdue)$/.test(a.name)&&w(a,{target:null,minimumVersion:J}),/^flash-(outdated|unavailable|deactivated|overdue)$/.test(a.name)&&w(a,{version:I.version})),"copy"===a.type&&(a.clipboardData={setData:Fb.setData,clearData:Fb.clearData}),"aftercopy"===a.type&&(a=qb(a,M)),a.target&&!a.relatedTarget&&(a.relatedTarget=eb(a.target)),a=fb(a)}},eb=function(a){var b=a&&a.getAttribute&&a.getAttribute("data-clipboard-target");return b?f.getElementById(b):null},fb=function(a){if(a&&/^_(?:click|mouse(?:over|out|down|up|move))$/.test(a.type)){var c=a.target,d="_mouseover"===a.type&&a.relatedTarget?a.relatedTarget:b,g="_mouseout"===a.type&&a.relatedTarget?a.relatedTarget:b,h=Ab(c),i=e.screenLeft||e.screenX||0,j=e.screenTop||e.screenY||0,k=f.body.scrollLeft+f.documentElement.scrollLeft,l=f.body.scrollTop+f.documentElement.scrollTop,m=h.left+("number"==typeof a._stageX?a._stageX:0),n=h.top+("number"==typeof a._stageY?a._stageY:0),o=m-k,p=n-l,q=i+o,r=j+p,s="number"==typeof a.movementX?a.movementX:0,t="number"==typeof a.movementY?a.movementY:0;delete a._stageX,delete a._stageY,w(a,{srcElement:c,fromElement:d,toElement:g,screenX:q,screenY:r,pageX:m,pageY:n,clientX:o,clientY:p,x:o,y:p,movementX:s,movementY:t,offsetX:0,offsetY:0,layerX:0,layerY:0})}return a},gb=function(a){var b=a&&"string"==typeof a.type&&a.type||"";return!/^(?:(?:before)?copy|destroy)$/.test(b)},hb=function(a,b,c,d){d?h(function(){a.apply(b,c)},0):a.apply(b,c)},ib=function(a){if("object"==typeof a&&a&&a.type){var b=gb(a),c=K["*"]||[],d=K[a.type]||[],f=c.concat(d);if(f&&f.length){var g,h,i,j,k,l=this;for(g=0,h=f.length;h>g;g++)i=f[g],j=l,"string"==typeof i&&"function"==typeof e[i]&&(i=e[i]),"object"==typeof i&&i&&"function"==typeof i.handleEvent&&(j=i,i=i.handleEvent),"function"==typeof i&&(k=w({},a),hb(i,j,[k],b))}return this}},jb=function(a){var b=a.target||c||null,e="swf"===a._source;delete a._source;var f=["flash-disabled","flash-outdated","flash-unavailable","flash-deactivated","flash-overdue"];switch(a.type){case"error":-1!==f.indexOf(a.name)&&w(I,{disabled:"flash-disabled"===a.name,outdated:"flash-outdated"===a.name,unavailable:"flash-unavailable"===a.name,deactivated:"flash-deactivated"===a.name,overdue:"flash-overdue"===a.name,ready:!1});break;case"ready":var g=I.deactivated===!0;w(I,{disabled:!1,outdated:!1,unavailable:!1,deactivated:!1,overdue:g,ready:!g});break;case"beforecopy":d=b;break;case"copy":var h,i,j=a.relatedTarget;!L["text/html"]&&!L["text/plain"]&&j&&(i=j.value||j.outerHTML||j.innerHTML)&&(h=j.value||j.textContent||j.innerText)?(a.clipboardData.clearData(),a.clipboardData.setData("text/plain",h),i!==h&&a.clipboardData.setData("text/html",i)):!L["text/plain"]&&a.target&&(h=a.target.getAttribute("data-clipboard-text"))&&(a.clipboardData.clearData(),a.clipboardData.setData("text/plain",h));break;case"aftercopy":Fb.clearData(),b&&b!==vb()&&b.focus&&b.focus();break;case"_mouseover":Fb.focus(b),O.bubbleEvents===!0&&e&&(b&&b!==a.relatedTarget&&!B(a.relatedTarget,b)&&kb(w({},a,{type:"mouseenter",bubbles:!1,cancelable:!1})),kb(w({},a,{type:"mouseover"})));break;case"_mouseout":Fb.blur(),O.bubbleEvents===!0&&e&&(b&&b!==a.relatedTarget&&!B(a.relatedTarget,b)&&kb(w({},a,{type:"mouseleave",bubbles:!1,cancelable:!1})),kb(w({},a,{type:"mouseout"})));break;case"_mousedown":wb(b,O.activeClass),O.bubbleEvents===!0&&e&&kb(w({},a,{type:a.type.slice(1)}));break;case"_mouseup":xb(b,O.activeClass),O.bubbleEvents===!0&&e&&kb(w({},a,{type:a.type.slice(1)}));break;case"_click":d=null,O.bubbleEvents===!0&&e&&kb(w({},a,{type:a.type.slice(1)}));break;case"_mousemove":O.bubbleEvents===!0&&e&&kb(w({},a,{type:a.type.slice(1)}))}return/^_(?:click|mouse(?:over|out|down|up|move))$/.test(a.type)?!0:void 0},kb=function(a){if(a&&"string"==typeof a.type&&a){var b,c=a.target||null,d=c&&c.ownerDocument||f,g={view:d.defaultView||e,canBubble:!0,cancelable:!0,detail:"click"===a.type?1:0,button:"number"==typeof a.which?a.which-1:"number"==typeof a.button?a.button:d.createEvent?0:1},h=w(g,a);c&&d.createEvent&&c.dispatchEvent&&(h=[h.type,h.canBubble,h.cancelable,h.view,h.detail,h.screenX,h.screenY,h.clientX,h.clientY,h.ctrlKey,h.altKey,h.shiftKey,h.metaKey,h.button,h.relatedTarget],b=d.createEvent("MouseEvents"),b.initMouseEvent&&(b.initMouseEvent.apply(b,h),b._source="js",c.dispatchEvent(b)))}},lb=function(){var a=f.createElement("div");return a.id=O.containerId,a.className=O.containerClass,a.style.position="absolute",a.style.left="0px",a.style.top="-9999px",a.style.width="1px",a.style.height="1px",a.style.zIndex=""+Db(O.zIndex),a},mb=function(a){for(var b=a&&a.parentNode;b&&"OBJECT"===b.nodeName&&b.parentNode;)b=b.parentNode;return b||null},nb=function(){var a,b=I.bridge,c=mb(b);if(!b){var d=ub(e.location.host,O),g="never"===d?"none":"all",h=sb(O),i=O.swfPath+rb(O.swfPath,O);c=lb();var j=f.createElement("div");c.appendChild(j),f.body.appendChild(c);var k=f.createElement("div"),l="activex"===I.pluginType;k.innerHTML='',b=k.firstChild,k=null,u(b).ZeroClipboard=Fb,c.replaceChild(b,j)}return b||(b=f[O.swfObjectId],b&&(a=b.length)&&(b=b[a-1]),!b&&c&&(b=c.firstChild)),I.bridge=b||null,b},ob=function(){var a=I.bridge;if(a){var b=mb(a);b&&("activex"===I.pluginType&&"readyState"in a?(a.style.display="none",function c(){if(4===a.readyState){for(var d in a)"function"==typeof a[d]&&(a[d]=null);a.parentNode&&a.parentNode.removeChild(a),b.parentNode&&b.parentNode.removeChild(b)}else h(c,10)}()):(a.parentNode&&a.parentNode.removeChild(a),b.parentNode&&b.parentNode.removeChild(b))),I.ready=null,I.bridge=null,I.deactivated=null}},pb=function(a){var b={},c={};if("object"==typeof a&&a){for(var d in a)if(d&&s.call(a,d)&&"string"==typeof a[d]&&a[d])switch(d.toLowerCase()){case"text/plain":case"text":case"air:text":case"flash:text":b.text=a[d],c.text=d;break;case"text/html":case"html":case"air:html":case"flash:html":b.html=a[d],c.html=d;break;case"application/rtf":case"text/rtf":case"rtf":case"richtext":case"air:rtf":case"flash:rtf":b.rtf=a[d],c.rtf=d}return{data:b,formatMap:c}}},qb=function(a,b){if("object"!=typeof a||!a||"object"!=typeof b||!b)return a;var c={};for(var d in a)if(s.call(a,d)){if("success"!==d&&"data"!==d){c[d]=a[d];continue}c[d]={};var e=a[d];for(var f in e)f&&s.call(e,f)&&s.call(b,f)&&(c[d][b[f]]=e[f])}return c},rb=function(a,b){var c=null==b||b&&b.cacheBust===!0;return c?(-1===a.indexOf("?")?"?":"&")+"noCache="+p():""},sb=function(a){var b,c,d,f,g="",h=[];if(a.trustedDomains&&("string"==typeof a.trustedDomains?f=[a.trustedDomains]:"object"==typeof a.trustedDomains&&"length"in a.trustedDomains&&(f=a.trustedDomains)),f&&f.length)for(b=0,c=f.length;c>b;b++)if(s.call(f,b)&&f[b]&&"string"==typeof f[b]){if(d=tb(f[b]),!d)continue;if("*"===d){h.length=0,h.push(d);break}h.push.apply(h,[d,"//"+d,e.location.protocol+"//"+d])}return h.length&&(g+="trustedOrigins="+i(h.join(","))),a.forceEnhancedClipboard===!0&&(g+=(g?"&":"")+"forceEnhancedClipboard=true"),"string"==typeof a.swfObjectId&&a.swfObjectId&&(g+=(g?"&":"")+"swfObjectId="+i(a.swfObjectId)),g},tb=function(a){if(null==a||""===a)return null;if(a=a.replace(/^\s+|\s+$/g,""),""===a)return null;var b=a.indexOf("//");a=-1===b?a:a.slice(b+2);var c=a.indexOf("/");return a=-1===c?a:-1===b||0===c?null:a.slice(0,c),a&&".swf"===a.slice(-4).toLowerCase()?null:a||null},ub=function(){var a=function(a){var b,c,d,e=[];if("string"==typeof a&&(a=[a]),"object"!=typeof a||!a||"number"!=typeof a.length)return e;for(b=0,c=a.length;c>b;b++)if(s.call(a,b)&&(d=tb(a[b]))){if("*"===d){e.length=0,e.push("*");break}-1===e.indexOf(d)&&e.push(d)}return e};return function(b,c){var d=tb(c.swfPath);null===d&&(d=b);var e=a(c.trustedDomains),f=e.length;if(f>0){if(1===f&&"*"===e[0])return"always";if(-1!==e.indexOf(b))return 1===f&&b===d?"sameDomain":"always"}return"never"}}(),vb=function(){try{return f.activeElement}catch(a){return null}},wb=function(a,b){if(!a||1!==a.nodeType)return a;if(a.classList)return a.classList.contains(b)||a.classList.add(b),a;if(b&&"string"==typeof b){var c=(b||"").split(/\s+/);if(1===a.nodeType)if(a.className){for(var d=" "+a.className+" ",e=a.className,f=0,g=c.length;g>f;f++)d.indexOf(" "+c[f]+" ")<0&&(e+=" "+c[f]);a.className=e.replace(/^\s+|\s+$/g,"")}else a.className=b}return a},xb=function(a,b){if(!a||1!==a.nodeType)return a;if(a.classList)return a.classList.contains(b)&&a.classList.remove(b),a;if("string"==typeof b&&b){var c=b.split(/\s+/);if(1===a.nodeType&&a.className){for(var d=(" "+a.className+" ").replace(/[\n\t]/g," "),e=0,f=c.length;f>e;e++)d=d.replace(" "+c[e]+" "," ");a.className=d.replace(/^\s+|\s+$/g,"")}}return a},yb=function(a,b){var c=e.getComputedStyle(a,null).getPropertyValue(b);return"cursor"!==b||c&&"auto"!==c||"A"!==a.nodeName?c:"pointer"},zb=function(){var a,b,c,d=1;return"function"==typeof f.body.getBoundingClientRect&&(a=f.body.getBoundingClientRect(),b=a.right-a.left,c=f.body.offsetWidth,d=o(b/c*100)/100),d},Ab=function(a){var b={left:0,top:0,width:0,height:0};if(a.getBoundingClientRect){var c,d,g,h=a.getBoundingClientRect();"pageXOffset"in e&&"pageYOffset"in e?(c=e.pageXOffset,d=e.pageYOffset):(g=zb(),c=o(f.documentElement.scrollLeft/g),d=o(f.documentElement.scrollTop/g));var i=f.documentElement.clientLeft||0,j=f.documentElement.clientTop||0;b.left=h.left+c-i,b.top=h.top+d-j,b.width="width"in h?h.width:h.right-h.left,b.height="height"in h?h.height:h.bottom-h.top}return b},Bb=function(){var a;if(c&&(a=mb(I.bridge))){var b=Ab(c);w(a.style,{width:b.width+"px",height:b.height+"px",top:b.top+"px",left:b.left+"px",zIndex:""+Db(O.zIndex)})}},Cb=function(a){I.ready===!0&&(I.bridge&&"function"==typeof I.bridge.setHandCursor?I.bridge.setHandCursor(a):I.ready=!1)},Db=function(a){if(/^(?:auto|inherit)$/.test(a))return a;var b;return"number"!=typeof a||n(a)?"string"==typeof a&&(b=Db(l(a,10))):b=a,"number"==typeof b?b:"auto"},Eb=function(a){function b(a){var b=a.match(/[\d]+/g);return b.length=3,b.join(".")}function c(a){return!!a&&(a=a.toLowerCase())&&(/^(pepflashplayer\.dll|libpepflashplayer\.so|pepperflashplayer\.plugin)$/.test(a)||"chrome.plugin"===a.slice(-13))}function d(a){a&&(i=!0,a.version&&(l=b(a.version)),!l&&a.description&&(l=b(a.description)),a.filename&&(k=c(a.filename)))}var e,f,h,i=!1,j=!1,k=!1,l="";if(g.plugins&&g.plugins.length)e=g.plugins["Shockwave Flash"],d(e),g.plugins["Shockwave Flash 2.0"]&&(i=!0,l="2.0.0.11");else if(g.mimeTypes&&g.mimeTypes.length)h=g.mimeTypes["application/x-shockwave-flash"],e=h&&h.enabledPlugin,d(e);else if("undefined"!=typeof a){j=!0;try{f=new a("ShockwaveFlash.ShockwaveFlash.7"),i=!0,l=b(f.GetVariable("$version"))}catch(n){try{f=new a("ShockwaveFlash.ShockwaveFlash.6"),i=!0,l="6.0.21"}catch(o){try{f=new a("ShockwaveFlash.ShockwaveFlash"),i=!0,l=b(f.GetVariable("$version"))}catch(p){j=!1}}}}I.disabled=i!==!0,I.outdated=l&&m(l)c;c++)a=e[c].replace(/^on/,""),f[a]=!0,g[a]||(g[a]=[]),g[a].push(b);if(f.ready&&I.ready&&this.emit({type:"ready",client:this}),f.error){var h=["disabled","outdated","unavailable","deactivated","overdue"];for(c=0,d=h.length;d>c;c++)if(I[h[c]]){this.emit({type:"error",name:"flash-"+h[c],client:this});break}}}return this},Nb=function(a,b){var c,d,e,f,g,h=Hb[this.id]&&Hb[this.id].handlers;if(0===arguments.length)f=q(h);else if("string"==typeof a&&a)f=a.split(/\s+/);else if("object"==typeof a&&a&&"undefined"==typeof b)for(c in a)s.call(a,c)&&"string"==typeof c&&c&&"function"==typeof a[c]&&this.off(c,a[c]);if(f&&f.length)for(c=0,d=f.length;d>c;c++)if(a=f[c].toLowerCase().replace(/^on/,""),g=h[a],g&&g.length)if(b)for(e=g.indexOf(b);-1!==e;)g.splice(e,1),e=g.indexOf(b,e);else g.length=0;return this},Ob=function(a){var b=null,c=Hb[this.id]&&Hb[this.id].handlers;return c&&(b="string"==typeof a&&a?c[a]?c[a].slice(0):[]:x(c)),b},Pb=function(a){if(Ub.call(this,a)){"object"==typeof a&&a&&"string"==typeof a.type&&a.type&&(a=w({},a));var b=w({},db(a),{client:this});Vb.call(this,b)}return this},Qb=function(a){a=Wb(a);for(var b=0;b0,d=!a.target||c&&-1!==b.indexOf(a.target),e=a.relatedTarget&&c&&-1!==b.indexOf(a.relatedTarget),f=a.client&&a.client===this;return d||e||f?!0:!1},Vb=function(a){if("object"==typeof a&&a&&a.type){var b=gb(a),c=Hb[this.id]&&Hb[this.id].handlers["*"]||[],d=Hb[this.id]&&Hb[this.id].handlers[a.type]||[],f=c.concat(d);if(f&&f.length){var g,h,i,j,k,l=this;for(g=0,h=f.length;h>g;g++)i=f[g],j=l,"string"==typeof i&&"function"==typeof e[i]&&(i=e[i]),"object"==typeof i&&i&&"function"==typeof i.handleEvent&&(j=i,i=i.handleEvent),"function"==typeof i&&(k=w({},a),hb(i,j,[k],b))}return this}},Wb=function(a){return"string"==typeof a&&(a=[]),"number"!=typeof a.length?[a]:a},Xb=function(a){if(a&&1===a.nodeType){var b=function(a){(a||(a=e.event))&&("js"!==a._source&&(a.stopImmediatePropagation(),a.preventDefault()),delete a._source)},c=function(c){(c||(c=e.event))&&(b(c),Fb.focus(a))};a.addEventListener("mouseover",c,!1),a.addEventListener("mouseout",b,!1),a.addEventListener("mouseenter",b,!1),a.addEventListener("mouseleave",b,!1),a.addEventListener("mousemove",b,!1),Kb[a.zcClippingId]={mouseover:c,mouseout:b,mouseenter:b,mouseleave:b,mousemove:b}}},Yb=function(a){if(a&&1===a.nodeType){var b=Kb[a.zcClippingId];if("object"==typeof b&&b){for(var c,d,e=["move","leave","enter","out","over"],f=0,g=e.length;g>f;f++)c="mouse"+e[f],d=b[c],"function"==typeof d&&a.removeEventListener(c,d,!1);delete Kb[a.zcClippingId]}}};Fb._createClient=function(){Lb.apply(this,v(arguments))},Fb.prototype.on=function(){return Mb.apply(this,v(arguments))},Fb.prototype.off=function(){return Nb.apply(this,v(arguments))},Fb.prototype.handlers=function(){return Ob.apply(this,v(arguments))},Fb.prototype.emit=function(){return Pb.apply(this,v(arguments))},Fb.prototype.clip=function(){return Qb.apply(this,v(arguments))},Fb.prototype.unclip=function(){return Rb.apply(this,v(arguments))},Fb.prototype.elements=function(){return Sb.apply(this,v(arguments))},Fb.prototype.destroy=function(){return Tb.apply(this,v(arguments))},Fb.prototype.setText=function(a){return Fb.setData("text/plain",a),this},Fb.prototype.setHtml=function(a){return Fb.setData("text/html",a),this},Fb.prototype.setRichText=function(a){return Fb.setData("application/rtf",a),this},Fb.prototype.setData=function(){return Fb.setData.apply(this,v(arguments)),this},Fb.prototype.clearData=function(){return Fb.clearData.apply(this,v(arguments)),this},Fb.prototype.getData=function(){return Fb.getData.apply(this,v(arguments))},"function"==typeof define&&define.amd?define(function(){return Fb}):"object"==typeof module&&module&&"object"==typeof module.exports&&module.exports?module.exports=Fb:a.ZeroClipboard=Fb}(function(){return this||window}());
diff --git a/doc/_themes/odoodoc/static/zeroclipboard-2.1.6/ZeroClipboard.swf b/doc/_themes/odoodoc/static/zeroclipboard-2.1.6/ZeroClipboard.swf
new file mode 100644
index 00000000000..d4e2561b366
Binary files /dev/null and b/doc/_themes/odoodoc/static/zeroclipboard-2.1.6/ZeroClipboard.swf differ
diff --git a/doc/glossary.rst b/doc/glossary.rst
index 278098a5923..29cf6c4c7e6 100644
--- a/doc/glossary.rst
+++ b/doc/glossary.rst
@@ -13,6 +13,9 @@
``account.invoice_graph``). From within a module, the
:samp:`{module}.` prefix can be left out.
+ Sometimes referred to as "xml id" or ``xml_id`` as XML-based
+ :ref:`reference/data` make extensive use of them.
+
format string
inspired by `jinja variables`_, format strings allow more easily
mixing literal content and computed content (expressions): content
diff --git a/doc/modules/api_integration.rst b/doc/modules/api_integration.rst
index f6a51f0f9b8..21d0163ba46 100644
--- a/doc/modules/api_integration.rst
+++ b/doc/modules/api_integration.rst
@@ -22,13 +22,74 @@ easily available over XML-RPC_ and accessible from a variety of languages.
Connection and authentication
=============================
+.. kinda gross because it duplicates existing bits
+
+.. rst-class:: setupcode hidden
+
+ .. code-block:: python
+
+ import xmlrpclib
+ info = xmlrpclib.ServerProxy('https://demo.odoo.com/start').start()
+ url, db, username, password = \
+ info['host'], info['database'], info['user'], info['password']
+ common = xmlrpclib.ServerProxy('{}/xmlrpc/2/common'.format(url))
+ uid = common.authenticate(db, username, password, {})
+ models = xmlrpclib.ServerProxy('{}/xmlrpc/2/object'.format(url))
+
+ .. code-block:: ruby
+
+ require "xmlrpc/client"
+ info = XMLRPC::Client.new2('https://demo.odoo.com/start').call('start')
+ url, db, username, password = \
+ info['host'], info['database'], info['user'], info['password']
+ common = XMLRPC::Client.new2("#{url}/xmlrpc/2/common")
+ uid = common.call('authenticate', db, username, password, {})
+ models = XMLRPC::Client.new2("#{url}/xmlrpc/2/object").proxy
+
+ .. code-block:: php
+
+ require_once('ripcord.php');
+ $info = ripcord::client('https://demo.odoo.com/start')->start();
+ list($url, $db, $username, $password) =
+ array($info['host'], $info['database'], $info['user'], $info['password']);
+ $common = ripcord::client("$url/xmlrpc/2/common");
+ $uid = $common->authenticate($db, $username, $password, array());
+ $models = ripcord::client("$url/xmlrpc/2/object");
+
+ .. code-block:: java
+
+ final XmlRpcClient client = new XmlRpcClient();
+
+ final XmlRpcClientConfigImpl start_config = new XmlRpcClientConfigImpl();
+ start_config.setServerURL(new URL("https://demo.odoo.com/start"));
+ final Map info = (Map)client.execute(
+ start_config, "start", Collections.emptyList());
+
+ final String url = info.get("host"),
+ db = info.get("database"),
+ username = info.get("user"),
+ password = info.get("password");
+
+ final XmlRpcClientConfigImpl common_config = new XmlRpcClientConfigImpl();
+ common_config.setServerURL(new URL(String.format("%s/xmlrpc/2/common", url)));
+
+ int uid = (int)client.execute(
+ common_config, "authenticate", Arrays.asList(
+ db, username, password, Collections.emptyMap()));
+
+ final XmlRpcClient models = new XmlRpcClient() {{
+ setConfig(new XmlRpcClientConfigImpl() {{
+ setServerURL(new URL(String.format("%s/xmlrpc/2/object", url)));
+ }});
+ }};
+
Configuration
-------------
If you already have an Odoo server installed, you can just use its
parameters
-.. rst-class:: switchable
+.. rst-class:: switchable setup
.. code-block:: python
@@ -61,7 +122,7 @@ parameters
To make exploration simpler, you can also ask https://demo.odoo.com for a test
database:
-.. rst-class:: switchable
+.. rst-class:: switchable setup
.. code-block:: python
@@ -134,7 +195,7 @@ authentication itself is done through the ``authenticate`` function and
returns a user identifier (``uid``) used in authenticated calls instead of
the login.
-.. rst-class:: switchable
+.. rst-class:: switchable setup
.. code-block:: python
@@ -166,7 +227,7 @@ the login.
"protocol_version": 1,
}
-.. rst-class:: switchable
+.. rst-class:: switchable setup
.. code-block:: python
@@ -209,7 +270,7 @@ For instance to see if we can read the ``res.partner`` model we can call
``raise_exception`` passed by keyword (in order to get a true/false result
rather than true/error):
-.. rst-class:: switchable
+.. rst-class:: switchable setup
.. code-block:: python
diff --git a/doc/reference/orm.rst b/doc/reference/orm.rst
index 03548d1e399..94aa18b4863 100644
--- a/doc/reference/orm.rst
+++ b/doc/reference/orm.rst
@@ -4,10 +4,678 @@
ORM
===
+Recordsets
+==========
+
+.. versionadded:: 8.0
+
+Interaction with models and records is performed through recordsets, a sorted
+set of records of the same model.
+
+.. warning:: contrary to what the name implies, it is currently possible for
+ recordsets to contain duplicates. This may change in the future.
+
+Methods defined on a model are executed on a recordset, and their ``self`` is
+a recordset::
+
+ class AModel(Model):
+ _name = 'a.model'
+ def a_method(self):
+ # self can be anywhere between 0 records and all records in the
+ # database
+ self.do_operation()
+
+Iterating on a recordset will yield new sets of *a single record*
+("singletons"), much like iterating on a Python string yields strings of a
+single characters::
+
+ def do_operation(self):
+ print self # => a.model(1, 2, 3, 4, 5)
+ for record in self:
+ print record # => a.model(1), then a.model(2), then a.model(3), ...
+
+Field access
+------------
+
+Recordsets provide an "Active Record" interface: model fields can be read and
+written directly from the record, but only on singletons (single-record
+recordsets). Setting a field's value triggers an update to the database::
+
+ >>> record.name
+ Example Name
+ >>> record.company_id.name
+ Company Name
+ >>> record.name = "Bob"
+
+Trying to read or write a field on multiple records will raise an error.
+
+Accessing a relational field (:class:`~openerp.fields.Many2one`,
+:class:`~openerp.fields.One2many`, :class:`~openerp.fields.Many2many`)
+*always* returns a recordset, empty if the field is not set.
+
+.. danger::
+
+ each assignment to a field triggers a database update, when setting
+ multiple fields at the same time or setting fields on multiple records
+ (to the same value), use :meth:`~openerp.models.Model.write`::
+
+ # 3 * len(records) database updates
+ for record in records:
+ record.a = 1
+ record.b = 2
+ record.c = 3
+
+ # len(records) database updates
+ for record in records:
+ record.write({'a': 1, 'b': 2, 'c': 3})
+
+ # 1 database update
+ records.write({'a': 1, 'b': 2, 'c': 3})
+
+Set operations
+--------------
+
+Recordsets are immutable, but sets of the same model can be combined using
+various set operations, returning new recordsets. Set operations do *not*
+preserve order.
+
+.. addition preserves order but can introduce duplicates
+
+* ``record in set`` returns whether ``record`` (which must be a 1-element
+ recordset) is present in ``set``. ``record not in set`` is the inverse
+ operation
+* ``set1 | set2`` returns the union of the two recordsets, a new recordset
+ containing all records present in either source
+* ``set1 & set2`` returns the intersection of two recordsets, a new recordset
+ containing only records present in both sources
+* ``set1 - set2`` returns a new recordset containing only records of ``set1``
+ which are *not* in ``set2``
+
+Other recordset operations
+--------------------------
+
+Recordsets are iterable so the usual Python tools are available for
+transformation (:func:`python:map`, :func:`python:sorted`,
+:func:`~python:itertools.ifilter`, ...) however these return either a
+:class:`python:list` or an :term:`python:iterator`, removing the ability to
+call methods on their result, or to use set operations.
+
+Recordsets therefore provide these operations returning recordsets themselves
+(when possible):
+
+:meth:`~openerp.models.Model.filtered`
+ returns a recordset containing only records satisfying the provided
+ predicate function. The predicate can also be a string to filter by a
+ field being true or false::
+
+ # only keep records whose company is the current user's
+ records.filtered(lambda r: r.company_id == user.company_id)
+
+ # only keep records whose partner is a company
+ records.filtered("partner_id.is_company")
+
+:meth:`~openerp.models.Model.sorted`
+ returns a recordset sorted by the provided key function. If no key
+ is provided, use the model's default sort order::
+
+ # sort records by name
+ records.sorted(key=lambda r: r.name)
+
+:meth:`~openerp.models.Model.mapped`
+ applies the provided function to each record in the recordset, returns
+ a recordset if the results are recordsets::
+
+ # returns a list of summing two fields for each record in the set
+ records.mapped(lambda r: r.field1 + r.field2)
+
+ The provided function can be a string to get field values::
+
+ # returns a list of names
+ records.mapped('name')
+
+ # returns a recordset of partners
+ record.mapped('partner_id')
+
+ # returns the union of all partner banks, with duplicates removed
+ record.mapped('partner_id.bank_ids')
+
+Environment
+===========
+
+The :class:`~openerp.api.Environment` stores various contextual data used by
+the ORM: the database cursor (for database queries), the current user
+(for access rights checking) and the current context (storing arbitrary
+metadata). The environment also stores caches.
+
+All recordsets have an environment, which is immutable, can be accessed
+using :attr:`~openerp.models.Model.env` and gives access to the current user
+(:attr:`~openerp.api.Environment.user`), the cursor
+(:attr:`~openerp.api.Environment.cr`) or the context
+(:attr:`~openerp.api.Environment.context`)::
+
+ >>> records.env
+
+ >>> records.env.user
+ res.user(3)
+ >>> records.env.cr
+ >> self.env['res.partner']
+ res.partner
+ >>> self.env['res.partner'].search([['is_company', '=', True], ['customer', '=', True]])
+ res.partner(7, 18, 12, 14, 17, 19, 8, 31, 26, 16, 13, 20, 30, 22, 29, 15, 23, 28, 74)
+
+Altering the environment
+------------------------
+
+The environment can be customized from a recordset. This returns a new
+version of the recordset using the altered environment.
+
+:meth:`~openerp.models.Model.sudo`
+ creates a new environment with the provided user set, uses the
+ administrator if none is provided (to bypass access rights/rules in safe
+ contexts), returns a copy of the recordset it is called on using the
+ new environment::
+
+ # create partner object as administrator
+ env['res.partner'].sudo().create({'name': "A Partner"})
+
+ # list partners visible by the "public" user
+ public = env.ref('base.public_user')
+ env['res.partner'].sudo(public).search([])
+
+:meth:`~openerp.models.Model.with_context`
+ #. can take a single positional parameter, which replaces the current
+ environment's context
+ #. can take any number of parameters by keyword, which are added to either
+ the current environment's context or the context set during step 1
+
+ ::
+
+ # look for partner, or create one with specified timezone if none is
+ # found
+ env['res.partner'].with_context(tz=a_tz).find_or_create(email_address)
+
+:meth:`~openerp.models.Model.with_env`
+ replaces the existing environment entirely
+
+Common ORM methods
+==================
+
+.. maybe these clarifications/examples should be in the APIDoc?
+
+:meth:`~openerp.models.Model.search`
+ Takes a :ref:`search domain `, returns a recordset
+ of matching records. Can return a subset of matching records (``offset``
+ and ``limit`` parameters) and be ordered (``order`` parameter)::
+
+ >>> # searches the current model
+ >>> self.search([('is_company', '=', True), ('customer', '=', True)])
+ res.partner(7, 18, 12, 14, 17, 19, 8, 31, 26, 16, 13, 20, 30, 22, 29, 15, 23, 28, 74)
+ >>> self.search([('is_company', '=', True)], limit=1).name
+ 'Agrolait'
+
+ .. tip:: to just check if any record matches a domain, or count the number
+ of records which do, use
+ :meth:`~openerp.models.Model.search_count`
+:meth:`~openerp.models.Model.create`
+ Takes a number of field values, and returns a recordset containing the
+ record created::
+
+ >>> self.create({'name': "New Name"})
+ res.partner(78)
+
+:meth:`~openerp.models.Model.write`
+ Takes a number of field values, writes them to all the records in its
+ recordset. Does not return anything::
+
+ self.write({'name': "Newer Name"})
+
+:meth:`~openerp.models.Model.browse`
+ Takes a database id or a list of ids and returns a recordset, useful when
+ record ids are obtained from outside Odoo (e.g. round-trip through
+ external system) or :ref:`when calling methods in the old API
+ `::
+
+ >>> self.browse([7, 18, 12])
+ res.partner(7, 18, 12])
+
+:meth:`~openerp.models.Model.exists`
+ Returns a new recordset containing only the records which exist in the
+ database. Can be used to check whether a record (e.g. obtained externally)
+ still exists::
+
+ if not record.exists():
+ raise Exception("The record has been deleted")
+
+ or after calling a method which could have removed some records::
+
+ records.may_remove_some()
+ # only keep records which were not deleted
+ records = records.exists()
+
+:meth:`~openerp.api.Environment.ref`
+ Environment method returning the record matching a provided
+ :term:`external id`::
+
+ >>> env.ref('base.group_public')
+ res.groups(2)
+
+:meth:`~openerp.models.Model.ensure_one`
+ checks that the recordset is a singleton (only contains a single record),
+ raises an error otherwise::
+
+ records.ensure_one()
+ # is equivalent to but clearer than:
+ assert len(records) == 1, "Expected singleton"
+
+Creating Models
+===============
+
+Model fields are defined as attributes on the model itself::
+
+ from openerp import models, fields
+ class AModel(Model):
+ _name = 'a.model.name'
+
+ field1 = fields.Char()
+
+.. warning:: this means you can not define a field and a method with the same
+ name, they will conflict
+
+By default, the field's label (user-visible name) is a capitalized version of
+the field name, this can be overridden with the ``string`` parameter::
+
+ field2 = fields.Integer(string="an other field")
+
+For the various field types and parameters, see :ref:`the fields reference
+`.
+
+Default values are defined as parameters on fields, either a value::
+
+ a_field = fields.Char(default="a value")
+
+or a function called to compute the default value, which should return that
+value::
+
+ a_field = fields.Char(default=compute_default_value)
+ def compute_default_value(self):
+ return self.get_value()
+
+Computed fields
+---------------
+
+Fields can be computed (instead of read straight from the database) using the
+``compute`` parameter. **It must assign the computed value to the field**. If
+it uses the values of other *fields*, it should specify those fields using
+:func:`~openerp.api.depends`::
+
+ from openerp import api
+ total = fields.Float(compute='_compute_total')
+
+ @api.depends('value', 'tax')
+ def _compute_total(self):
+ for record in self:
+ record.total = record.value + record.value * record.tax
+
+* dependencies can be dotted paths when using sub-fields::
+
+ @api.depends('line_ids.value')
+ def _compute_total(self):
+ for record in self:
+ record.total = sum(line.value for line in record.line_ids)
+
+* computed fields are not stored by default, they are computed and
+ returned when requested. Setting ``store=True`` will store them in the
+ database and automatically enable searching
+* searching on a computed field can also be enabled by setting the ``search``
+ parameter. The value is a method name returning a
+ :ref:`reference/orm/domains`::
+
+ upper_name = field.Char(compute='_compute_upper', search='_search_upper')
+
+ def _search_upper(self, operator, value):
+ if operator == 'like':
+ operator = 'ilike'
+ return [('name', operator, value)]
+
+* to allow *setting* values on a computed field, use the ``inverse``
+ parameter. It is the name of a function reversing the computation and
+ setting the relevant fields::
+
+ document = fields.Char(compute='_get_document', inverse='_set_document')
+
+ def _get_document(self):
+ for record in self:
+ with open(record.get_document_path) as f:
+ record.document = f.read()
+ def _set_document(self):
+ for record in self:
+ if not record.document: continue
+ with open(record.get_document_path()) as f:
+ f.write(record.document)
+
+* multiple fields can be computed at the same time by the same method, just
+ use the same method on all fields and set all of them::
+
+ discount_value = fields.Float(compute='_apply_discount')
+ total = fields.Float(compute='_apply_discount')
+
+ @depends('value', 'discount')
+ def _apply_discount(self):
+ for record in self:
+ # compute actual discount from discount percentage
+ discount = self.value * self.discount
+ self.discount_value = discount
+ self.total = self.value - discount
+
+Related fields
+''''''''''''''
+
+A special case of computed fields are *related* (proxy) fields, which provide
+the value of a sub-field on the current record. They are defined by setting
+the ``related`` parameter and like regular computed fields they can be
+stored::
+
+ nickname = fields.Char(related='user_id.partner_id.name', store=True)
+
+onchange: updating UI on the fly
+--------------------------------
+
+When a user changes a field's value in a form (but hasn't saved the form yet),
+it can be useful to automatically update other fields based on that value
+e.g. updating a final total when the tax is changed or a new invoice line is
+added.
+
+* computed fields are automatically checked and recomputed, they do not need
+ an ``onchange``
+* for non-computed fields, the :func:`~openerp.api.onchange` decorator is used
+ to provide new field values::
+
+ @api.onchange('field1', 'field2') # if these fields are changed, call method
+ def check_change(self):
+ if self.field1 < self.field2:
+ self.field3 = True
+
+ the changes performed during the method are then sent to the client program
+ and become visible to the user
+
+* Both computed fields and new-API onchanges are automatically called by the
+ client without having to add them in views
+* It is possible to suppress the trigger from a specific field by adding
+ ``on_change="0"`` in a view::
+
+
+
+ will not trigger any interface update when the field is edited by the user,
+ even if there are function fields or explicit onchange depending on that
+ field.
+
+.. note::
+
+ ``onchange`` methods work on virtual records assignment on these records
+ is not written to the database, just used to know which value to send back
+ to the client
+
+.. _reference/orm/oldapi:
+
+Old API compatibility
+---------------------
+
+Odoo is currently transitioning from an older (less regular) API, it can be
+necessary to manually bridge from one to the other manually:
+
+* RPC layers (both XML-RPC and JSON-RPC) are expressed in terms of the old
+ API, methods expressed purely in the new API are not available over RPC
+* overridable methods may be called from older pieces of code still written
+ in the old API style
+
+The big differences between the old and new APIs are:
+
+* values of the :class:`~openerp.api.Environment` (cursor, user id and
+ context) are passed explicitly to methods instead
+* record data (:attr:`~openerp.models.Model.ids`) are passed explicitly to
+ methods, and possibly not passed at all
+* methods tend to work on lists of ids instead of recordsets
+
+By default, methods are assumed to use the new API style and are not callable
+from the old API style.
+
+.. tip:: calls from the new API to the old API are bridged
+ :class: aphorism
+
+ when using the new API style, calls to methods defined using the old API
+ are automatically converted on-the-fly, there should be no need to do
+ anything special::
+
+ >>> # method in the old API style
+ >>> def old_method(self, cr, uid, ids, context=None):
+ ... print ids
+
+ >>> # method in the new API style
+ >>> def new_method(self):
+ ... # system automatically infers how to call the old-style
+ ... # method from the new-style method
+ ... self.old_method()
+
+ >>> env[model].browse([1, 2, 3, 4]).new_method()
+ [1, 2, 3, 4]
+
+Two decorators can expose a new-style method to the old API:
+
+:func:`~openerp.api.model`
+ the method is exposed as not using ids, its recordset will generally be
+ empty. Its "old API" signature is ``cr, uid, *arguments, context``::
+
+ @api.model
+ def some_method(foo):
+ pass
+ # can be called as
+ old_style_model.some_method(cr, uid, a_value, context=context)
+
+:func:`~openerp.api.multi`
+ the method is exposed as taking a list of ids (possibly empty), its
+ "old API" signature is ``cr, uid, ids, *arguments, context``::
+
+ @api.multi
+ def some_method(foo):
+ pass
+ # can be called as
+ old_style_model.some_method(cr, uid, [id1, id2], a_value, context=context)
+
+Because new-style APIs tend to return recordsets and old-style APIs tend to
+return lists of ids, there is also a decorator managing this:
+
+:func:`~openerp.api.returns`
+ the function is assumed to return a recordset, the first parameter should
+ be the name of the recordset's model or ``self`` (for the current model).
+
+ No effect if the method is called in new API style, but transforms the
+ recordset into a list of ids when called from the old API style::
+
+ >>> @api.multi
+ ... @api.returns('self')
+ ... def some_method():
+ ... return self
+ >>> new_style_model = env['a.model'].browse(1, 2, 3)
+ >>> new_style_model.some_method()
+ a.model(1, 2, 3)
+ >>> old_style_model = pool['a.model']
+ >>> old_style_model.some_method(cr, uid, [1, 2, 3], context=context)
+ [1, 2, 3]
+
+Porting from the old API
+------------------------
+
+* methods still written in the old API should be automatically bridged by the
+ ORM, no need to switch to the old API, just call them as if they were a new
+ API method. See :ref:`reference/orm/oldapi/bridging` for more details.
+* ``search`` returns a recordset, no point in e.g. browsing its result
+* ``fields.related`` and ``fields.function`` are replaced by using a normal
+ field type with either a ``related`` or a ``compute`` parameter
+* ``depends`` on field compute methods **must be complete**, it must list
+ **all** the fields and sub-fields which the compute method uses. It is
+ better to have too many dependencies (will recompute the field in cases
+ where that is not needed) than not enough (will forget to recompute the
+ field and then values will be incorrect)
+* **remove** all ``onchange`` methods on computed fields. Computed fields are
+ automatically re-computed when one of their dependencies is changed, and
+ that is used to auto-generate ``onchange`` by the client
+* the decorators :func:`~openerp.api.model` and :func:`~openerp.api.multi` are
+ for bridging *when calling from the old API context*, for internal or pure
+ new-api (e.g. compute) they are useless
+* remove :attr:`~openerp.models.Model._default`, replace by ``default=``
+ parameter on corresponding fields
+* if a field's ``string`` is the titlecased version of the field name::
+
+ name = fields.Char(string="Name")
+
+ it is useless and should be removed
+* ``multi`` does not do anything on new API fields use the same ``compute``
+ methods on all relevant fields for the same result
+* provide ``compute``, ``inverse`` and ``search`` methods by name (as a
+ string), this makes them overridable (removes the need for an intermediate
+ "trampoline" function)
+* double check that all fields and methods have different names, there is no
+ warning in case of collision (because Python handles it before Odoo sees
+ anything)
+* the normal new-api import is ``from openerp import fields, models``. If
+ compatibility decorators are necessary, use ``from openerp import api,
+ fields, models``
+* avoid the :func:`~openerp.api.one` decorator, it probably does not do what
+ you expect
+* remove explicit definition of :attr:`~openerp.models.Model.create_uid`,
+ :attr:`~openerp.models.Model.create_date`,
+ :attr:`~openerp.models.Model.write_uid` and
+ :attr:`~openerp.models.Model.write_date` fields: they are now created as
+ regular "legitimate" fields, and can be read and written like any other
+ field out-of-the-box
+* when straight conversion is impossible (semantics can not be bridged) or the
+ "old API" version is not desirable and could be improved for the new API, it
+ is possible to use completely different "old API" and "new API"
+ implementations for the same method name using :func:`~openerp.api.v7` and
+ :func:`~openerp.api.v8`. The method should first be defined using the
+ old-API style and decorated with :func:`~openerp.api.v7`, it should then be
+ re-defined using the exact same name but the new-API style and decorated
+ with :func:`~openerp.api.v8`. Calls from an old-API context will be
+ dispatched to the first implementation and calls from a new-API context will
+ be dispatched to the second implementation. One implementation can call (and
+ frequently does) call the other by switching context.
+
+ .. danger:: using these decorators makes methods extremely difficult to
+ override and harder to understand and document
+* uses of :attr:`~openerp.models.Model._columns` or
+ :attr:`~openerp.models.Model._all_columns` should be replaced by
+ :attr:`~openerp.models.Model._fields`, which provides access to instances of
+ new-style :class:`openerp.fields.Field` instances (rather than old-style
+ :class:`openerp.osv.fields._column`).
+
+ Non-stored computed fields created using the new API style are *not*
+ available in :attr:`~openerp.models.Model._columns` and can only be
+ inspected through :attr:`~openerp.models.Model._fields`
+* reassigning ``self`` in a method is probably unnecessary and may break
+ translation introspection
+* :class:`~openerp.api.Environment` objects rely on some threadlocal state,
+ which has to be set up before using them. It is necessary to do so using the
+ :meth:`openerp.api.Environment.manage` context manager when trying to use
+ the new API in contexts where it hasn't been set up yet, such as new threads
+ or a Python interactive environment::
+
+ >>> from openerp import api, modules
+ >>> r = modules.registry.RegistryManager.get('test')
+ >>> cr = r.cursor()
+ >>> env = api.Environment(cr, 1, {})
+ Traceback (most recent call last):
+ ...
+ AttributeError: environments
+ >>> with api.Environment.manage():
+ ... env = api.Environment(cr, 1, {})
+ ... print env['res.partner'].browse(1)
+ ...
+ res.partner(1,)
+
+.. _reference/orm/oldapi/bridging:
+
+Automatic bridging of old API methods
+'''''''''''''''''''''''''''''''''''''
+
+When models are initialized, all methods are automatically scanned and bridged
+if they look like models declared in the old API style. This bridging makes
+them transparently callable from new-API-style methods.
+
+Methods are matched as "old-API style" if their second positional parameter
+(after ``self``) is called either ``cr`` or ``cursor``. The system also
+recognizes the third positional parameter being called ``uid`` or ``user`` and
+the fourth being called ``id`` or ``ids``. It also recognizes the presence of
+any parameter called ``context``.
+
+When calling such methods from a new API context, the system will
+automatically fill matched parameters from the current
+:class:`~openerp.api.Environment` (for :attr:`~openerp.api.Environment.cr`,
+:attr:`~openerp.api.Environment.user` and
+:attr:`~openerp.api.Environment.context`) or the current recordset (for ``id``
+and ``ids``).
+
+In the rare cases where it is necessary, the bridging can be customized by
+decorating the old-style method:
+
+* disabling it entirely, by decorating a method with
+ :func:`~openerp.api.noguess` there will be no bridging and methods will be
+ called the exact same way from the new and old API styles
+* defining the bridge explicitly, this is mostly for methods which are matched
+ incorrectly (because parameters are named in unexpected ways):
+
+ :func:`~openerp.api.cr`
+ will automatically prepend the current cursor to explicitly provided
+ parameters, positionally
+ :func:`~openerp.api.cr_uid`
+ will automatically prepend the current cursor and user's id to explictly
+ provided parameters
+ :func:`~openerp.api.cr_uid_ids`
+ will automatically prepend the current cursor, user's id and recordset's
+ ids to explicitly provided parameters
+ :func:`~openerp.api.cr_uid_id`
+ will loop over the current recordset and call the method once for each
+ record, prepending the current cursor, user's id and record's id to
+ explicitly provided parameters.
+
+ .. danger:: the result of this wrapper is *always a list* when calling
+ from a new-API context
+
+ All of these methods have a ``_context``-suffixed version
+ (e.g. :func:`~openerp.api.cr_uid_context`) which also passes the current
+ context *by keyword*.
+* dual implementations using :func:`~openerp.api.v7` and
+ :func:`~openerp.api.v8` will be ignored as they provide their own "bridging"
+
+Low-level SQL
+-------------
+
+The :attr:`~openerp.api.Environment.cr` attribute on environments is the
+cursor for the current database transaction and allows executing SQL directly,
+either for queries which are difficult to express using the ORM (e.g. complex
+joins) or for performance reasons::
+
+ self.env.cr.execute("some_sql", param1, param2, param3)
+
+Because models use the same cursor and the :class:`~openerp.api.Environment`
+holds various caches, these caches must be invalidated when *altering* the
+database in raw SQL, or further uses of models may become incoherent. It is
+necessary to clear caches when using ``CREATE``, ``UPDATE`` or ``DELETE`` in
+SQL, but not ``SELECT`` (which simply reads the database).
+
+Clearing caches can be performed using the
+:meth:`~openerp.api.Environment.invalidate_all` method of the
+:class:`~openerp.api.Environment` object.
+
.. _reference/orm/model:
-Model
-=====
+Model Reference
+===============
.. - can't get autoattribute to import docstrings, so use regular attribute
- no autoclassmethod
@@ -121,7 +789,7 @@ Model
.. automethod:: exists
.. automethod:: filtered
.. automethod:: sorted
- .. automethod:: update
+ .. automethod:: mapped
.. rubric:: Environment swapping
@@ -234,7 +902,8 @@ Method decorators
=================
.. automodule:: openerp.api
- :members: one, multi, model, depends, constrains, onchange, returns
+ :members: one, multi, model, depends, constrains, onchange, returns,
+ v7, v8
.. _reference/orm/fields:
@@ -499,4 +1168,3 @@ Domain criteria can be combined using logical operators in *prefix* form:
(name is 'ABC')
AND (language is NOT english)
AND (country is Belgium OR Germany)
-
diff --git a/openerp/addons/base/ir/ir_translation.py b/openerp/addons/base/ir/ir_translation.py
index a16933b7f63..d52513c87d9 100644
--- a/openerp/addons/base/ir/ir_translation.py
+++ b/openerp/addons/base/ir/ir_translation.py
@@ -400,7 +400,7 @@ class ir_translation(osv.osv):
main_lang = 'en_US'
translatable_fields = []
for k, f in trans_model._fields.items():
- if f.translate:
+ if getattr(f, 'translate', False):
if f.inherited:
parent_id = trans_model.read(cr, uid, [id], [f.related[0]], context=context)[0][f.related[0]][0]
translatable_fields.append({'name': k, 'id': parent_id, 'model': f.base_field.model})
@@ -434,7 +434,7 @@ class ir_translation(osv.osv):
if field:
f = trans_model._fields[field]
action['context'] = {
- 'search_default_name': "%s,%s" % (f.base_field.model, field)
+ 'search_default_name': "%s,%s" % (f.base_field.model_name, field)
}
return action
diff --git a/openerp/addons/test_inherit/models.py b/openerp/addons/test_inherit/models.py
index 1a386a28274..422d59fd323 100644
--- a/openerp/addons/test_inherit/models.py
+++ b/openerp/addons/test_inherit/models.py
@@ -69,4 +69,7 @@ class daughter(models.Model):
# simply redeclare the field without adding any option
template_id = fields.Many2one()
+ # change the default value of an inherited field
+ name = fields.Char(default='Baz')
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/addons/test_inherit/tests/test_inherit.py b/openerp/addons/test_inherit/tests/test_inherit.py
index e9be815d216..6909882b2c6 100644
--- a/openerp/addons/test_inherit/tests/test_inherit.py
+++ b/openerp/addons/test_inherit/tests/test_inherit.py
@@ -17,19 +17,25 @@ class test_inherits(common.TransactionCase):
def test_field_extension(self):
""" check the extension of a field in an inherited model """
- # the field mother.name should inherit required=True, and have "Bar" as
- # a default value
mother = self.env['test.inherit.mother']
+ daughter = self.env['test.inherit.daughter']
+
+ # the field mother.name must have required=True and "Bar" as default
field = mother._fields['name']
self.assertTrue(field.required)
-
self.assertEqual(field.default(mother), "Bar")
- self.assertEqual(mother.default_get(['name']), {'name': "Bar"})
self.assertEqual(mother._defaults.get('name'), "Bar")
+ self.assertEqual(mother.default_get(['name']), {'name': "Bar"})
- # the field daughter.template_id should inherit
- # model_name='test.inherit.mother', string='Template', required=True
- daughter = self.env['test.inherit.daughter']
+ # the field daughter.name must have required=False and "Baz" as default
+ field = daughter._fields['name']
+ self.assertFalse(field.required)
+ self.assertEqual(field.default(mother), "Baz")
+ self.assertEqual(daughter._defaults.get('name'), "Baz")
+ self.assertEqual(daughter.default_get(['name']), {'name': "Baz"})
+
+ # the field daughter.template_id should have
+ # comodel_name='test.inherit.mother', string='Template', required=True
field = daughter._fields['template_id']
self.assertEqual(field.comodel_name, 'test.inherit.mother')
self.assertEqual(field.string, "Template")
diff --git a/openerp/fields.py b/openerp/fields.py
index 2d5e0c2d6a4..58838bf0531 100644
--- a/openerp/fields.py
+++ b/openerp/fields.py
@@ -347,30 +347,36 @@ class Field(object):
# traverse the class hierarchy upwards, and take the first field
# definition with a default or _defaults for self
for klass in cls.__mro__:
- field = klass.__dict__.get(name, self)
- if not isinstance(field, type(self)):
- return # klass contains another value overridden by self
+ if name in klass.__dict__:
+ field = klass.__dict__[name]
+ if not isinstance(field, type(self)):
+ # klass contains another value overridden by self
+ return
- if 'default' in field._attrs:
- # take the default in field, and adapt it for cls._defaults
- value = field._attrs['default']
- if callable(value):
- self.default = value
- cls._defaults[name] = lambda model, cr, uid, context: \
- self.convert_to_write(value(model.browse(cr, uid, [], context)))
- else:
- self.default = lambda recs: value
- cls._defaults[name] = value
- return
+ if 'default' in field._attrs:
+ # take the default in field, and adapt it for cls._defaults
+ value = field._attrs['default']
+ if callable(value):
+ from openerp import api
+ self.default = value
+ cls._defaults[name] = api.model(
+ lambda recs: self.convert_to_write(value(recs))
+ )
+ else:
+ self.default = lambda recs: value
+ cls._defaults[name] = value
+ return
defaults = klass.__dict__.get('_defaults') or {}
if name in defaults:
# take the value from _defaults, and adapt it for self.default
value = defaults[name]
- value_func = value if callable(value) else lambda *args: value
+ if callable(value):
+ func = lambda recs: value(recs._model, recs._cr, recs._uid, recs._context)
+ else:
+ func = lambda recs: value
self.default = lambda recs: self.convert_to_cache(
- value_func(recs._model, recs._cr, recs._uid, recs._context),
- recs, validate=False,
+ func(recs), recs, validate=False,
)
cls._defaults[name] = value
return
@@ -617,7 +623,8 @@ class Field(object):
def _description_string(self, env):
if self.string and env.lang:
- name = "%s,%s" % (self.model_name, self.name)
+ field = self.base_field
+ name = "%s,%s" % (field.model_name, field.name)
trans = env['ir.translation']._get_source(name, 'field', env.lang)
return trans or self.string
return self.string
diff --git a/openerp/models.py b/openerp/models.py
index 2e3fbe3b527..8c2e5ad0bd1 100644
--- a/openerp/models.py
+++ b/openerp/models.py
@@ -4679,6 +4679,8 @@ class BaseModel(object):
trans_obj = self.pool.get('ir.translation')
for field_name, field in self._fields.iteritems():
+ if not field.copy:
+ continue
# removing the lang to compare untranslated values
context_wo_lang = dict(context, lang=None)
old_record, new_record = self.browse(cr, uid, [old_id, new_id], context=context_wo_lang)