diff --git a/addons/account/account_invoice_view.xml b/addons/account/account_invoice_view.xml index 0d6eaedbe0d..f424558884c 100644 --- a/addons/account/account_invoice_view.xml +++ b/addons/account/account_invoice_view.xml @@ -179,7 +179,7 @@ - + @@ -328,7 +328,7 @@ - + Comment:

-

+

Payment Term: - +

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 Gantt calendar.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 @@ test You 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.

]]>
dummy dummy dummy 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 @@ test You 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.

]]>
dummy dummy 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 @@ test You 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.

]]>
dummy dummy dummy 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 @@ test You 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.

]]>
dummy dummy dummy diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index 3926b18c0a6..91e2cd14d09 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -680,7 +680,7 @@ class pos_order(osv.osv): 'date_order': fields.datetime('Order Date', readonly=True, select=True), 'user_id': fields.many2one('res.users', 'Salesman', help="Person who uses the the cash register. It can be a reliever, a student or an interim employee."), 'amount_tax': fields.function(_amount_all, string='Taxes', digits_compute=dp.get_precision('Account'), multi='all'), - 'amount_total': fields.function(_amount_all, string='Total', multi='all'), + 'amount_total': fields.function(_amount_all, string='Total', digits_compute=dp.get_precision('Account'), multi='all'), 'amount_paid': fields.function(_amount_all, string='Paid', states={'draft': [('readonly', False)]}, readonly=True, digits_compute=dp.get_precision('Account'), multi='all'), 'amount_return': fields.function(_amount_all, string='Returned', digits_compute=dp.get_precision('Account'), multi='all'), 'lines': fields.one2many('pos.order.line', 'order_id', 'Order Lines', states={'draft': [('readonly', False)]}, readonly=True, copy=True), @@ -1314,8 +1314,8 @@ class pos_order_line(osv.osv): 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True, change_default=True), 'price_unit': fields.float(string='Unit Price', digits_compute=dp.get_precision('Account')), 'qty': fields.float('Quantity', digits_compute=dp.get_precision('Product UoS')), - 'price_subtotal': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal w/o Tax', store=True), - 'price_subtotal_incl': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal', store=True), + 'price_subtotal': fields.function(_amount_line_all, multi='pos_order_line_amount', digits_compute=dp.get_precision('Account'), string='Subtotal w/o Tax', store=True), + 'price_subtotal_incl': fields.function(_amount_line_all, multi='pos_order_line_amount', digits_compute=dp.get_precision('Account'), string='Subtotal', store=True), 'discount': fields.float('Discount (%)', digits_compute=dp.get_precision('Account')), 'order_id': fields.many2one('pos.order', 'Order Ref', ondelete='cascade'), 'create_date': fields.datetime('Creation Date', readonly=True), diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index db0b0227809..2f647bbcb15 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -1164,6 +1164,10 @@ td { word-wrap: break-word; } +@page { + margin: 0; +} + @media print { body { margin: 0; diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index ba5f66f4635..c746d12c8fc 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -131,14 +131,23 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal { model: 'res.users', fields: ['name','company_id'], - domain: function(self){ return [['id','=',self.session.uid]]; }, + ids: function(self){ return [self.session.uid]; }, loaded: function(self,users){ self.user = users[0]; }, },{ model: 'res.company', fields: [ 'currency_id', 'email', 'website', 'company_registry', 'vat', 'name', 'phone', 'partner_id' , 'country_id'], - domain: function(self){ return [['id','=',self.user.company_id[0]]]; }, + ids: function(self){ return [self.user.company_id[0]] }, loaded: function(self,companies){ self.company = companies[0]; }, },{ + model: 'decimal.precision', + fields: ['name','digits'], + loaded: function(self,dps){ + self.dp = {}; + for (var i = 0; i < dps.length; i++) { + self.dp[dps[i].name] = dps[i].digits; + } + }, + },{ model: 'product.uom', fields: [], domain: null, @@ -222,19 +231,25 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal },{ model: 'stock.location', fields: [], - domain: function(self){ return [['id','=', self.config.stock_location_id[0]]]; }, + ids: function(self){ return [self.config.stock_location_id[0]]; }, loaded: function(self, locations){ self.shop = locations[0]; }, },{ model: 'product.pricelist', fields: ['currency_id'], - domain: function(self){ return [['id','=',self.config.pricelist_id[0]]]; }, + ids: function(self){ return [self.config.pricelist_id[0]]; }, loaded: function(self, pricelists){ self.pricelist = pricelists[0]; }, },{ model: 'res.currency', fields: ['symbol','position','rounding','accuracy'], - domain: function(self){ return [['id','=',self.pricelist.currency_id[0]]]; }, + ids: function(self){ return [self.pricelist.currency_id[0]]; }, loaded: function(self, currencies){ self.currency = currencies[0]; + if (self.currency.rounding > 0) { + self.currency.decimals = Math.ceil(Math.log(1.0 / self.currency.rounding) / Math.log(10)); + } else { + self.currency.decimals = 0; + } + }, },{ model: 'product.packaging', @@ -363,11 +378,17 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal var fields = typeof model.fields === 'function' ? model.fields(self,tmp) : model.fields; var domain = typeof model.domain === 'function' ? model.domain(self,tmp) : model.domain; var context = typeof model.context === 'function' ? model.context(self,tmp) : model.context; + var ids = typeof model.ids === 'function' ? model.ids(self,tmp) : model.ids; progress += progress_step; + if( model.model ){ - new instance.web.Model(model.model).query(fields).filter(domain).context(context).all() - .then(function(result){ + if (model.ids) { + var records = new instance.web.Model(model.model).call('read',[ids,fields],context); + } else { + var records = new instance.web.Model(model.model).query(fields).filter(domain).context(context).all() + } + records.then(function(result){ try{ // catching exceptions in model.loaded(...) $.when(model.loaded(self,result,tmp)) .then(function(){ load_model(index + 1); }, @@ -684,8 +705,13 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal var quant = parseFloat(quantity) || 0; var unit = this.get_unit(); if(unit){ - this.quantity = round_pr(quant, unit.rounding); - this.quantityStr = this.quantity.toFixed(Math.ceil(Math.log(1.0 / unit.rounding) / Math.log(10))); + if (unit.rounding) { + this.quantity = round_pr(quant, unit.rounding); + this.quantityStr = this.quantity.toFixed(Math.ceil(Math.log(1.0 / unit.rounding) / Math.log(10))); + } else { + this.quantity = round_pr(quant, 1); + this.quantityStr = this.quantity.toFixed(0); + } }else{ this.quantity = quant; this.quantityStr = '' + this.quantity; @@ -779,12 +805,11 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal }, // changes the base price of the product for this orderline set_unit_price: function(price){ - this.price = round_di(parseFloat(price) || 0, 2); + this.price = round_di(parseFloat(price) || 0, this.pos.dp['Product Price']); this.trigger('change',this); }, get_unit_price: function(){ - var rounding = this.pos.currency.rounding; - return round_pr(this.price,rounding); + return this.price; }, get_display_price: function(){ var rounding = this.pos.currency.rounding; @@ -864,16 +889,20 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal this.cashregister = options.cashregister; this.name = this.cashregister.journal_id[1]; this.selected = false; + this.pos = options.pos; }, //sets the amount of money on this payment line set_amount: function(value){ - this.amount = round_di(parseFloat(value) || 0, 2); + this.amount = round_di(parseFloat(value) || 0, this.pos.currency.decimals); this.trigger('change:amount',this); }, // returns the amount of money on this paymentline get_amount: function(){ return this.amount; }, + get_amount_str: function(){ + return this.amount.toFixed(this.pos.currency.decimals); + }, set_selected: function(selected){ if(this.selected !== selected){ this.selected = selected; @@ -999,7 +1028,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal }, addPaymentline: function(cashregister) { var paymentLines = this.get('paymentLines'); - var newPaymentline = new module.Paymentline({},{cashregister:cashregister}); + var newPaymentline = new module.Paymentline({},{cashregister:cashregister, pos:this.pos}); if(cashregister.journal.type !== 'cash'){ newPaymentline.set_amount( Math.max(this.getDueLeft(),0) ); } diff --git a/addons/point_of_sale/static/src/js/widget_base.js b/addons/point_of_sale/static/src/js/widget_base.js index 8820cb8c9f3..f0764d2f43d 100644 --- a/addons/point_of_sale/static/src/js/widget_base.js +++ b/addons/point_of_sale/static/src/js/widget_base.js @@ -1,5 +1,6 @@ function openerp_pos_basewidget(instance, module){ //module is instance.point_of_sale + var round_di = instance.web.round_decimals; var round_pr = instance.web.round_precision // This is a base class for all Widgets in the POS. It exposes relevant data to the @@ -17,36 +18,30 @@ function openerp_pos_basewidget(instance, module){ //module is instance.point_of options = options || {}; this.pos = options.pos || (parent ? parent.pos : undefined); this.pos_widget = options.pos_widget || (parent ? parent.pos_widget : undefined); - this.build_currency_template(); }, - build_currency_template: function(){ + format_currency: function(amount,precision){ + var currency = (this.pos && this.pos.currency) ? this.pos.currency : {symbol:'$', position: 'after', rounding: 0.01, decimals: 2}; + var decimals = currency.decimals; - if(this.pos && this.pos.currency){ - this.currency = this.pos.currency; - }else{ - this.currency = {symbol: '$', position: 'after', rounding: 0.01}; + if (precision && (typeof this.pos.dp[precision]) !== undefined) { + decimals = this.pos.dp[precision]; } - var decimals = Math.max(0,Math.ceil(Math.log(1.0 / this.currency.rounding) / Math.log(10))); - this.format_currency_no_symbol = function(amount){ amount = round_pr(amount,this.currency.rounding); amount = amount.toFixed(decimals); return amount; }; - this.format_currency = function(amount){ - if(typeof amount === 'number'){ - amount = Math.round(amount*100)/100; - amount = amount.toFixed(decimals); - } - if(this.currency.position === 'after'){ - return amount + ' ' + (this.currency.symbol || ''); - }else{ - return (this.currency.symbol || '') + ' ' + amount; - } - }; + if (typeof amount === 'number') { + amount = round_di(amount,decimals).toFixed(decimals); + } + if (currency.position === 'after') { + return amount + ' ' + (currency.symbol || ''); + } else { + return (currency.symbol || '') + ' ' + amount; + } }, show: function(){ this.$el.removeClass('oe_hidden'); diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js index 2c199f87ef6..ff3b1795e32 100644 --- a/addons/point_of_sale/static/src/js/widgets.js +++ b/addons/point_of_sale/static/src/js/widgets.js @@ -862,7 +862,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa $('.oe_web_client').off(); $('.openerp_webclient_container').off(); - self.build_currency_template(); self.renderElement(); self.$('.neworder-button').click(function(){ diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index 5988532f410..362f1a2da73 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -948,12 +948,12 @@ - + - + @@ -979,7 +979,7 @@ at - + / @@ -1075,7 +1075,7 @@
- + @@ -1088,7 +1088,7 @@ - + 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='"+(l?'':"")+'',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)