[MERGE] forward port of branch 8.0 up to 83bd9ee
This commit is contained in:
@@ -179,7 +179,7 @@
|
||||
</div>
|
||||
</group>
|
||||
<group>
|
||||
<field name="date_invoice"/>
|
||||
<field name="date_invoice" on_change="onchange_payment_term_date_invoice(payment_term, date_invoice)" />
|
||||
<field name="date_due"/>
|
||||
<field domain="[('company_id', '=', company_id), ('type', '=', 'payable')]"
|
||||
name="account_id" groups="account.group_account_user"/>
|
||||
@@ -328,7 +328,7 @@
|
||||
<field name="fiscal_position" options="{'no_create': True}" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="date_invoice"/>
|
||||
<field name="date_invoice" on_change="onchange_payment_term_date_invoice(payment_term, date_invoice)" />
|
||||
<field name="journal_id" groups="account.group_account_user"
|
||||
on_change="onchange_journal_id(journal_id)" options="{'no_create': True}"/>
|
||||
<field domain="[('company_id', '=', company_id),('type','=', 'receivable')]"
|
||||
|
||||
@@ -68,7 +68,7 @@ access_report_aged_receivable,report.aged.receivable,model_report_aged_receivabl
|
||||
access_report_invoice_created,report.invoice.created,model_report_invoice_created,account.group_account_manager,1,1,1,1
|
||||
access_report_account_type_sales,report.account_type.sales,model_report_account_type_sales,account.group_account_manager,1,1,1,1
|
||||
access_report_account_sales,report.account.sales,model_report_account_sales,account.group_account_manager,1,1,1,1
|
||||
access_account_invoice_report,account.invoice.report,model_account_invoice_report,account.group_account_manager,1,1,1,1
|
||||
access_account_invoice_report,account.invoice.report,model_account_invoice_report,account.group_account_invoice,1,0,0,0
|
||||
access_res_partner_group_account_manager,res_partner group_account_manager,model_res_partner,account.group_account_manager,1,0,0,0
|
||||
access_account_invoice_accountant,account.invoice accountant,model_account_invoice,account.group_account_user,1,0,0,0
|
||||
access_account_tax_code_accountant,account.tax.code accountant,model_account_tax_code,account.group_account_user,1,1,1,1
|
||||
|
||||
|
@@ -131,9 +131,9 @@
|
||||
<strong>Comment:</strong>
|
||||
<span t-field="o.comment"/>
|
||||
</p>
|
||||
<p t-if="o.payment_term">
|
||||
<p t-if="o.payment_term.note">
|
||||
<strong>Payment Term:</strong>
|
||||
<span t-field="o.payment_term"/>
|
||||
<span t-field="o.payment_term.note"/>
|
||||
</p>
|
||||
<p t-if="o.fiscal_position">
|
||||
<strong>Fiscal Position:</strong>
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<field eval="2000.0" name="salvage_value"/>
|
||||
<field name="state">open</field>
|
||||
<field eval="12" name="method_period"/>
|
||||
<field eval="10" name="method_number"/>
|
||||
<field eval="5" name="method_number"/>
|
||||
<field name="name">CEO's Car</field>
|
||||
<field eval="12000.0" name="purchase_value"/>
|
||||
<field name="category_id" ref="account_asset_category_fixedassets0"/>
|
||||
|
||||
@@ -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")])
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<field name="journal_id" widget="selection" on_change="onchange_journal_voucher(line_ids, tax_id, amount, partner_id, journal_id, type)"/>
|
||||
<field name="type" required="1"/>
|
||||
<field name="name" colspan="2"/>
|
||||
<field name="company_id" widget="selection" groups="base.group_multi_company"/>
|
||||
<field name="company_id" widget="selection" on_change="onchange_company(partner_id, journal_id, currency_id, company_id)" groups="base.group_multi_company"/>
|
||||
<field name="reference"/>
|
||||
<field name="number"/>
|
||||
<field name="currency_id" groups="base.group_multi_currency"/>
|
||||
|
||||
@@ -437,7 +437,7 @@
|
||||
<field name="date" on_change="onchange_date(date, currency_id, payment_rate_currency_id, amount, company_id, context)"/>
|
||||
<field name="reference" string="Payment Ref" placeholder="e.g. 003/10"/>
|
||||
<field name="name" colspan="2" placeholder="e.g. Invoice SAJ/0042"/>
|
||||
<field name="company_id" widget="selection" groups="base.group_multi_company"/>
|
||||
<field name="company_id" widget="selection" on_change="onchange_company(partner_id, journal_id, currency_id, company_id)" groups="base.group_multi_company"/>
|
||||
|
||||
<field name="account_id"
|
||||
widget="selection"
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
<group>
|
||||
<group>
|
||||
<field name="type" invisible="True"/>
|
||||
<field name="company_id" widget="selection" groups="base.group_multi_company"/>
|
||||
<field name="company_id" widget="selection" on_change="onchange_company(partner_id, journal_id, currency_id, company_id)" groups="base.group_multi_company"/>
|
||||
<field name="partner_id" domain="[('customer','=',True)]" on_change="onchange_partner_id(partner_id, journal_id, amount, currency_id, type, date, context)" string="Customer" context="{'search_default_customer':1, 'show_address': 1}" options='{"always_reload": True}'/>
|
||||
<field name="account_id"
|
||||
domain="[('type','in', ['liquidity', 'receivable'])]"/>
|
||||
|
||||
@@ -240,7 +240,7 @@
|
||||
<field name="name">CRM - Meetings Gantt</field>
|
||||
<field name="model">calendar.event</field>
|
||||
<field name="arch" type="xml">
|
||||
<gantt date_delay="duration" date_start="date" string="Meetings"/>
|
||||
<gantt date_delay="duration" date_start="start" string="Meetings"/>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
<filter string="My Team" domain="[('team_id.user_id', '=', uid)]"/>
|
||||
<filter string="Unassigned" domain="[('user_id','=',False)]"/>
|
||||
<separator/>
|
||||
<filter string="To Do" name="current" domain="[('state','=','open')]"/>
|
||||
<filter string="To Do" name="current" domain="[('state','in',('open','pending'))]"/>
|
||||
<separator/>
|
||||
<filter string="New Mail" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<separator/>
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -398,7 +398,7 @@
|
||||
<field name="product_efficiency" groups="mrp.group_rounding_efficiency"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_stop"/>
|
||||
<field name="attribute_value_ids" widget="many2many_tags"/>
|
||||
<field name="attribute_value_ids" widget="many2many_tags" domain="[('product_ids.product_tmpl_id', '=', parent.product_tmpl_id)]"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<field name="view_template_id" ref="adyen_acquirer_button"/>
|
||||
<field name="environment">test</field>
|
||||
<field name="pre_msg"><![CDATA[
|
||||
<p>You will be redirected to the Adyen website after cliking on the payment button.</p>]]></field>
|
||||
<p>You will be redirected to the Adyen website after clicking on the payment button.</p>]]></field>
|
||||
<field name="adyen_merchant_account">dummy</field>
|
||||
<field name="adyen_skin_code">dummy</field>
|
||||
<field name="adyen_skin_hmac_key">dummy</field>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<field name="view_template_id" ref="buckaroo_acquirer_button"/>
|
||||
<field name="environment">test</field>
|
||||
<field name="pre_msg"><![CDATA[
|
||||
<p>You will be redirected to the Buckaroo website after cliking on the payment button.</p>]]></field>
|
||||
<p>You will be redirected to the Buckaroo website after clicking on the payment button.</p>]]></field>
|
||||
<field name="brq_websitekey">dummy</field>
|
||||
<field name="brq_secretkey">dummy</field>
|
||||
</record>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<field name="view_template_id" ref="ogone_acquirer_button"/>
|
||||
<field name="environment">test</field>
|
||||
<field name="pre_msg"><![CDATA[
|
||||
<p>You will be redirected to the Ogone website after cliking on the payment button.</p>]]></field>
|
||||
<p>You will be redirected to the Ogone website after clicking on the payment button.</p>]]></field>
|
||||
<field name='ogone_pspid'>dummy</field>
|
||||
<field name='ogone_userid'>dummy</field>
|
||||
<field name='ogone_password'>dummy</field>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<field name="view_template_id" ref="paypal_acquirer_button"/>
|
||||
<field name="environment">test</field>
|
||||
<field name="pre_msg"><![CDATA[
|
||||
<p>You will be redirected to the Paypal website after cliking on the payment button.</p>]]></field>
|
||||
<p>You will be redirected to the Paypal website after clicking on the payment button.</p>]]></field>
|
||||
<field name="paypal_email_account">dummy</field>
|
||||
<field name="paypal_seller_account">dummy</field>
|
||||
<field name="paypal_api_username">dummy</field>
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -1164,6 +1164,10 @@ td {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
@page {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
margin: 0;
|
||||
|
||||
@@ -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) );
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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(){
|
||||
|
||||
@@ -948,12 +948,12 @@
|
||||
<img t-att-src='image_url' />
|
||||
<t t-if="!product.to_weight">
|
||||
<span class="price-tag">
|
||||
<t t-esc="widget.format_currency(product.price)"/>
|
||||
<t t-esc="widget.format_currency(product.price,'Product Price')"/>
|
||||
</span>
|
||||
</t>
|
||||
<t t-if="product.to_weight">
|
||||
<span class="price-tag">
|
||||
<t t-esc="widget.format_currency(product.price)+'/Kg'"/>
|
||||
<t t-esc="widget.format_currency(product.price,'Product Price')+'/Kg'"/>
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
@@ -979,7 +979,7 @@
|
||||
</em>
|
||||
<t t-esc="line.get_unit().name" />
|
||||
at
|
||||
<t t-esc="widget.format_currency(line.get_unit_price())" />
|
||||
<t t-esc="widget.format_currency(line.get_unit_price(),'Product Price')" />
|
||||
/
|
||||
<t t-esc="line.get_unit().name" />
|
||||
</li>
|
||||
@@ -1075,7 +1075,7 @@
|
||||
<div class='paymentline-name'>
|
||||
<t t-esc="line.name"/>
|
||||
</div>
|
||||
<input class='paymentline-input' type="number" step="0.01" t-att-value="line.get_amount().toFixed(2)" />
|
||||
<input class='paymentline-input' type="number" step="0.01" t-att-value="line.get_amount_str()" />
|
||||
<span class='paymentline-delete'>
|
||||
<img src="/point_of_sale/static/src/img/search_reset.gif" />
|
||||
</span>
|
||||
@@ -1088,7 +1088,7 @@
|
||||
<t t-esc="line.name"/>
|
||||
</td>
|
||||
<td class="paymentline-amount pos-right-align">
|
||||
<input type="number" step="0.01" t-att-value="line.get_amount().toFixed(2)" />
|
||||
<input type="number" step="0.01" t-att-value="line.get_amount_str()" />
|
||||
<span class='delete-payment-line'><img src="/point_of_sale/static/src/img/search_reset.gif" /></span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -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<rule.min_quantity:
|
||||
|
||||
@@ -365,6 +365,14 @@ class product_attribute_value(osv.osv):
|
||||
'price_extra': value,
|
||||
}, context=context)
|
||||
|
||||
def name_get(self, cr, uid, ids, context=None):
|
||||
if context and not context.get('show_attribute', True):
|
||||
return super(product_attribute_value, self).name_get(cr, uid, ids, context=context)
|
||||
res = []
|
||||
for value in self.browse(cr, uid, ids, context=context):
|
||||
res.append([value.id, "%s: %s" % (value.attribute_id.name, value.name)])
|
||||
return res
|
||||
|
||||
_columns = {
|
||||
'sequence': fields.integer('Sequence', help="Determine the display order"),
|
||||
'name': fields.char('Value', translate=True, required=True),
|
||||
|
||||
@@ -246,7 +246,7 @@
|
||||
<field string="List of Variants" name="product_variant_count" widget="statinfo" />
|
||||
</button>
|
||||
</div>
|
||||
<field name="attribute_line_ids" widget="one2many_list">
|
||||
<field name="attribute_line_ids" widget="one2many_list" context="{'show_attribute': False}">
|
||||
<tree string="Variants" editable="bottom">
|
||||
<field name="attribute_id"/>
|
||||
<field name="value_ids" widget="many2many_tags" options="{'no_create_edit': True}" domain="[('attribute_id', '=', attribute_id)]" context="{'default_attribute_id': attribute_id}"/>
|
||||
|
||||
@@ -82,7 +82,9 @@
|
||||
<field name="domain_force">['|',
|
||||
('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]),
|
||||
]</field>
|
||||
<field name="groups" eval="[(4,ref('base.group_user'))]"/>
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
<filter string="Responsible" icon="terp-personal" domain="[]" context="{'group_by':'user_id'}"/>
|
||||
<filter string="Source" icon="terp-gtk-jump-to-rtl" domain="[]" context="{'group_by':'origin'}"/>
|
||||
<filter string="Status" icon="terp-stock_effects-object-colorize" domain="[]" context="{'group_by':'state'}"/>
|
||||
<filter string="Start Month" icon="terp-go-month" domain="[]" context="{'group_by':'date_start'}"/>
|
||||
<filter string="Ordering Date" icon="terp-go-month" domain="[]" context="{'group_by':'ordering_date'}"/>
|
||||
<filter string="End Month" icon="terp-go-month" domain="[]" context="{'group_by':'date_end'}"/>
|
||||
</group>
|
||||
</search>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="view_account_invoice_report_search_inherit">
|
||||
<field name="name">account.invoice.report.search</field>
|
||||
<field name="model">account.invoice.report</field>
|
||||
<field name="inherit_id" ref="account.view_account_invoice_report_search" />
|
||||
<field name="arch" type="xml">
|
||||
<filter name="user" position="after">
|
||||
<filter string="Sales Team" domain="[]"
|
||||
context="{'group_by':'section_id'}" groups="base.group_multi_salesteams" />
|
||||
</filter>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -2,4 +2,4 @@
|
||||
# vim:noet:
|
||||
|
||||
%:
|
||||
dh ${@}
|
||||
dh ${@} -Spython_distutils
|
||||
Vendored
+1
-1
@@ -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'
|
||||
] %}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
Vendored
+116
-84
@@ -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 =
|
||||
$('<button type="button" class="btn-clipboard">Copy</button>')
|
||||
.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<fromSheet.cssRules.length; ++i) {
|
||||
@@ -63,87 +178,4 @@ $(function () {
|
||||
}
|
||||
return root;
|
||||
}
|
||||
if ($('div.document-super').hasClass('stripe')) { (function () {
|
||||
var sheet = findSheet(/style\.css$/);
|
||||
if (!sheet) { return; }
|
||||
|
||||
// collect languages
|
||||
var languages = {};
|
||||
$('div.switchable').each(function () {
|
||||
var classes = this.className.split(/\s+/);
|
||||
for (var i = 0; i < classes.length; ++i) {
|
||||
var cls = classes[i];
|
||||
if (!/^highlight-/.test(cls)) { continue; }
|
||||
languages[cls.slice(10)] = true;
|
||||
}
|
||||
});
|
||||
|
||||
$(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;
|
||||
var sel = [
|
||||
'.stripe .only-', id, ', ',
|
||||
'.stripe .highlight-', id, ' > .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 = '<div class="zero-clipboard"><span class="btn-clipboard">Copy</span></div>';
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
Vendored
+16
-12
@@ -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;
|
||||
|
||||
+21
-13
@@ -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 {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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<String, String> info = (Map<String, String>)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
|
||||
|
||||
|
||||
+673
-5
@@ -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
|
||||
<Environment object ...>
|
||||
>>> records.env.user
|
||||
res.user(3)
|
||||
>>> records.env.cr
|
||||
<Cursor object ...)
|
||||
|
||||
When creating a recordset from an other recordset, the environment is
|
||||
inherited. The environment can be used to get an empty recordset in an
|
||||
other model, and query that model::
|
||||
|
||||
>>> 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 <reference/orm/domains>`, 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
|
||||
<reference/orm/oldapi>`::
|
||||
|
||||
>>> 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
|
||||
<reference/orm/fields>`.
|
||||
|
||||
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::
|
||||
|
||||
<field name="name" on_change="0"/>
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
+25
-18
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user