[MERGE] forward port of branch 8.0 up to c92e70b
This commit is contained in:
@@ -1154,6 +1154,19 @@ class account_move(osv.osv):
|
||||
_description = "Account Entry"
|
||||
_order = 'id desc'
|
||||
|
||||
def account_assert_balanced(self, cr, uid, context=None):
|
||||
cr.execute("""\
|
||||
SELECT move_id
|
||||
FROM account_move_line
|
||||
WHERE state = 'valid'
|
||||
GROUP BY move_id
|
||||
HAVING abs(sum(debit) - sum(credit)) > 0.00001
|
||||
""")
|
||||
assert len(cr.fetchall()) == 0, \
|
||||
"For all Journal Items, the state is valid implies that the sum " \
|
||||
"of credits equals the sum of debits"
|
||||
return True
|
||||
|
||||
def account_move_prepare(self, cr, uid, journal_id, date=False, ref='', company_id=False, context=None):
|
||||
'''
|
||||
Prepares and returns a dictionary of values, ready to be passed to create() based on the parameters received.
|
||||
@@ -2023,7 +2036,7 @@ class account_tax(osv.osv):
|
||||
data['tax_amount']=quantity
|
||||
# data['amount'] = quantity
|
||||
elif tax.type=='code':
|
||||
localdict = {'price_unit':cur_price_unit, 'product':product, 'partner':partner}
|
||||
localdict = {'price_unit':cur_price_unit, 'product':product, 'partner':partner, 'quantity': quantity}
|
||||
exec tax.python_compute in localdict
|
||||
amount = localdict['result']
|
||||
data['amount'] = amount
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<assert model="account.move" search="[]" string="For all Journal Items, the state is valid implies that the sum of credits equals the sum of debits">
|
||||
<test expr="not len(line_id) or line_id[0].state != 'valid' or (sum([l.debit - l.credit for l in line_id]) <= 0.00001)"/>
|
||||
</assert>
|
||||
<function name="account_assert_balanced" model="account.move"/>
|
||||
</data>
|
||||
</openerp>
|
||||
|
||||
@@ -125,6 +125,9 @@ class account_voucher(osv.osv):
|
||||
journal_pool = self.pool.get('account.journal')
|
||||
journal_id = context.get('journal_id', False)
|
||||
if journal_id:
|
||||
if isinstance(journal_id, (list, tuple)):
|
||||
# sometimes journal_id is a pair (id, display_name)
|
||||
journal_id = journal_id[0]
|
||||
journal = journal_pool.browse(cr, uid, journal_id, context=context)
|
||||
if journal.currency:
|
||||
return journal.currency.id
|
||||
|
||||
@@ -789,7 +789,7 @@ class crm_lead(format_address, osv.osv):
|
||||
partner_id = self._create_lead_partner(cr, uid, lead, context)
|
||||
self.pool['res.partner'].write(cr, uid, partner_id, {'team_id': lead.team_id and lead.team_id.id or False})
|
||||
if partner_id:
|
||||
lead.write({'partner_id': partner_id}, context=context)
|
||||
lead.write({'partner_id': partner_id})
|
||||
partner_ids[lead.id] = partner_id
|
||||
return partner_ids
|
||||
|
||||
|
||||
@@ -53,10 +53,6 @@ class sale_order(osv.Model):
|
||||
result['value']['carrier_id'] = dtype
|
||||
return result
|
||||
|
||||
def _prepare_order_picking(self, cr, uid, order, context=None):
|
||||
result = super(sale_order, self)._prepare_order_picking(cr, uid, order, context=context)
|
||||
result.update(carrier_id=order.carrier_id.id)
|
||||
return result
|
||||
|
||||
def _delivery_unset(self, cr, uid, ids, context=None):
|
||||
sale_obj = self.pool['sale.order.line']
|
||||
@@ -92,4 +88,4 @@ class sale_order(osv.Model):
|
||||
'price_unit': grid_obj.get_price(cr, uid, grid.id, order, time.strftime('%Y-%m-%d'), context),
|
||||
'tax_id': [(6, 0, taxes_ids)],
|
||||
'is_delivery': True
|
||||
})
|
||||
})
|
||||
@@ -120,9 +120,10 @@ class stock_picking(osv.osv):
|
||||
}
|
||||
|
||||
def _create_invoice_from_picking(self, cr, uid, picking, vals, context=None):
|
||||
invoice_obj = self.pool.get('account.invoice')
|
||||
invoice_line_obj = self.pool.get('account.invoice.line')
|
||||
invoice_id = super(stock_picking, self)._create_invoice_from_picking(cr, uid, picking, vals, context=context)
|
||||
invoice = self.browse(cr, uid, invoice_id, context=context)
|
||||
invoice = invoice_obj.browse(cr, uid, invoice_id, context=context)
|
||||
invoice_line = self._prepare_shipping_invoice_line(cr, uid, picking, invoice, context=context)
|
||||
if invoice_line:
|
||||
invoice_line_obj.create(cr, uid, invoice_line)
|
||||
@@ -170,6 +171,24 @@ class stock_move(osv.osv):
|
||||
'weight_uom_id': fields.many2one('product.uom', 'Unit of Measure', required=True,readonly="1",help="Unit of Measure (Unit of Measure) is the unit of measurement for Weight",),
|
||||
}
|
||||
|
||||
def action_confirm(self, cr, uid, ids, context=None):
|
||||
"""
|
||||
Pass the carrier to the picking from the sales order
|
||||
(Should also work in case of Phantom BoMs when on explosion the original move is deleted)
|
||||
"""
|
||||
procs_to_check = []
|
||||
for move in self.browse(cr, uid, ids, context=context):
|
||||
if move.procurement_id and move.procurement_id.sale_line_id and move.procurement_id.sale_line_id.order_id.carrier_id:
|
||||
procs_to_check += [move.procurement_id]
|
||||
res = super(stock_move, self).action_confirm(cr, uid, ids, context=context)
|
||||
pick_obj = self.pool.get("stock.picking")
|
||||
for proc in procs_to_check:
|
||||
pickings = list(set([x.picking_id.id for x in proc.move_ids if x.picking_id and not x.picking_id.carrier_id]))
|
||||
if pickings:
|
||||
pick_obj.write(cr, uid, pickings, {'carrier_id': proc.sale_line_id.order_id.carrier_id.id}, context=context)
|
||||
return res
|
||||
|
||||
|
||||
def _get_default_uom(self, cr, uid, context=None):
|
||||
uom_categ_id = self.pool.get('ir.model.data').xmlid_to_res_id(cr, uid, 'product.product_uom_categ_kgm')
|
||||
return self.pool.get('product.uom').search(cr, uid, [('category_id', '=', uom_categ_id),('factor','=',1)])[0]
|
||||
|
||||
@@ -426,8 +426,10 @@ class fleet_vehicle_odometer(osv.Model):
|
||||
res = {}
|
||||
for record in self.browse(cr, uid, ids, context=context):
|
||||
name = record.vehicle_id.name
|
||||
if record.date:
|
||||
name = name+ ' / '+ str(record.date)
|
||||
if not name:
|
||||
name = record.date
|
||||
elif record.date:
|
||||
name += ' / '+ record.date
|
||||
res[record.id] = name
|
||||
return res
|
||||
|
||||
|
||||
@@ -494,7 +494,7 @@ class google_calendar(osv.AbstractModel):
|
||||
|
||||
all_my_attendees = self.pool['calendar.attendee'].search(cr, uid, [('partner_id', '=', current_user.partner_id.id)], context=context)
|
||||
self.pool['calendar.attendee'].write(cr, uid, all_my_attendees, {'oe_synchro_date': False, 'google_internal_event_id': False}, context=context)
|
||||
current_user.write(reset_data, context=context)
|
||||
current_user.write(reset_data)
|
||||
return True
|
||||
|
||||
def synchronize_events_cron(self, cr, uid, context=None):
|
||||
@@ -552,7 +552,7 @@ class google_calendar(osv.AbstractModel):
|
||||
lastSync = False
|
||||
_logger.info("[%s] Calendar Synchro - MODE FULL SYNCHRO FORCED" % user_to_sync)
|
||||
else:
|
||||
current_user.write({'google_calendar_cal_id': current_google}, context=context)
|
||||
current_user.write({'google_calendar_cal_id': current_google})
|
||||
lastSync = False
|
||||
_logger.info("[%s] Calendar Synchro - MODE FULL SYNCHRO - NEW CAL ID" % user_to_sync)
|
||||
|
||||
@@ -562,7 +562,7 @@ class google_calendar(osv.AbstractModel):
|
||||
|
||||
res = self.update_events(cr, uid, lastSync, context)
|
||||
|
||||
current_user.write({'google_calendar_last_sync_date': ask_time}, context=context)
|
||||
current_user.write({'google_calendar_last_sync_date': ask_time})
|
||||
return {
|
||||
"status": res and "need_refresh" or "no_new_event_form_google",
|
||||
"url": ''
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</div>
|
||||
<div class="oe_suggested_item_content">
|
||||
<a class="oe_suggestion_item_name" t-attf-href="#model=hr.employee&id=#{result.id}"><t t-esc="result.name"/></a>
|
||||
<a class="oe_suggestion_remove_item oe_suggestion_employee oe_e" t-attf-id="{result.id}">X</a>
|
||||
<a class="oe_suggestion_remove_item oe_suggestion_employee oe_e" t-att-id="result.id">X</a>
|
||||
<br/>
|
||||
<button class="oe_suggestion_follow" t-att-id="result.id">Follow</button>
|
||||
</div>
|
||||
|
||||
@@ -493,7 +493,7 @@ class hr_applicant(osv.Model):
|
||||
address_id = self.pool.get('res.partner').address_get(cr, uid, [applicant.partner_id.id], ['contact'])['contact']
|
||||
contact_name = self.pool.get('res.partner').name_get(cr, uid, [applicant.partner_id.id])[0][1]
|
||||
if applicant.job_id and (applicant.partner_name or contact_name):
|
||||
applicant.job_id.write({'no_of_hired_employee': applicant.job_id.no_of_hired_employee + 1}, context=context)
|
||||
applicant.job_id.write({'no_of_hired_employee': applicant.job_id.no_of_hired_employee + 1})
|
||||
create_ctx = dict(context, mail_broadcast=True)
|
||||
emp_id = hr_employee.create(cr, uid, {'name': applicant.partner_name or contact_name,
|
||||
'job_id': applicant.job_id.id,
|
||||
|
||||
@@ -10,3 +10,4 @@ access_product_uom_hr_timesheet,product.uom.hr.timesheet,product.model_product_u
|
||||
access_account_fiscalyear_hr_user,account.account.fiscalyear.user,account.model_account_fiscalyear,base.group_hr_user,1,1,1,1
|
||||
access_hr_analytic_timesheet_user,hr.analytic.timesheet.user,model_hr_analytic_timesheet,base.group_user,1,1,1,0
|
||||
access_account_analytic_journal_user,account.account.analytic.journal.user,analytic.model_account_analytic_journal,base.group_user,1,0,0,0
|
||||
access_hr_timesheet_report,hr.timesheet.report,model_hr_timesheet_report,base.group_hr_manager,1,1,0,0
|
||||
|
||||
|
@@ -3,6 +3,5 @@ access_hr_timesheet_sheet_sheet_user,hr_timesheet_sheet.sheet.user,model_hr_time
|
||||
access_hr_timesheet_sheet_sheet_system_employee,hr_timesheet_sheet.sheet.system.employee,model_hr_timesheet_sheet_sheet,base.group_user,1,1,1,0
|
||||
access_hr_timesheet_sheet_sheet_day,hr_timesheet_sheet.sheet.day,model_hr_timesheet_sheet_sheet_day,base.group_hr_user,1,1,1,1
|
||||
access_hr_timesheet_sheet_sheet_account,hr_timesheet_sheet.sheet.account,model_hr_timesheet_sheet_sheet_account,base.group_hr_user,1,1,1,1
|
||||
access_hr_timesheet_report,hr.timesheet.report,model_hr_timesheet_report,base.group_hr_manager,1,1,0,0
|
||||
access_hr_analytic_timesheet_system_user,hr.analytic.timesheet.system.user,model_hr_analytic_timesheet,base.group_user,1,0,0,0
|
||||
access_hr_timesheet_sheet_sheet_day,hr.timesheet.sheet.sheet.day.user,model_hr_timesheet_sheet_sheet_day,base.group_user,1,1,1,0
|
||||
|
||||
|
@@ -25,7 +25,7 @@
|
||||
</div>
|
||||
<div class="oe_suggested_item_content">
|
||||
<a class="oe_suggestion_item_name" t-attf-href="#model=mail.group&id=#{result.id}"><t t-esc="result.name"/></a>
|
||||
<a class="oe_suggestion_remove_item oe_suggestion_group oe_e" t-attf-id="{result.id}">X</a>
|
||||
<a class="oe_suggestion_remove_item oe_suggestion_group oe_e" t-att-id="result.id">X</a>
|
||||
<br/>
|
||||
<button t-att-id="result.id" class="oe_suggestion_join">Join Group</button>
|
||||
</div>
|
||||
|
||||
+6
-1
@@ -334,6 +334,11 @@ class mrp_bom(osv.osv):
|
||||
res['value'].update({'product_uom': product.uom_id.id})
|
||||
return res
|
||||
|
||||
def unlink(self, cr, uid, ids, context=None):
|
||||
if self.pool['mrp.production'].search(cr, uid, [('bom_id', 'in', ids), ('state', 'not in', ['done', 'cancel'])], context=context):
|
||||
raise osv.except_osv(_('Warning!'), _('You can not delete a Bill of Material with running manufacturing orders.\nPlease close or cancel it first.'))
|
||||
return super(mrp_bom, self).unlink(cr, uid, ids, context=context)
|
||||
|
||||
def onchange_product_tmpl_id(self, cr, uid, ids, product_tmpl_id, product_qty=0, context=None):
|
||||
""" Changes UoM and name if product_id changes.
|
||||
@param product_id: Changed product_id
|
||||
@@ -1200,7 +1205,7 @@ class mrp_production(osv.osv):
|
||||
self._make_service_procurement(cr, uid, line, context=context)
|
||||
if stock_moves:
|
||||
self.pool.get('stock.move').action_confirm(cr, uid, stock_moves, context=context)
|
||||
production.write({'state': 'confirmed'}, context=context)
|
||||
production.write({'state': 'confirmed'})
|
||||
return 0
|
||||
|
||||
def action_assign(self, cr, uid, ids, context=None):
|
||||
|
||||
@@ -25,5 +25,11 @@
|
||||
<field name="global" eval="True" />
|
||||
<field name="domain_force">[('company_id','child_of',[user.company_id.id])]</field>
|
||||
</record>
|
||||
<record id="rule_pos_order_report_multi_company" model="ir.rule">
|
||||
<field name="name">Point Of Sale Order Analysis multi-company</field>
|
||||
<field name="model_id" ref="model_report_pos_order"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
|
||||
@@ -1169,6 +1169,9 @@ td {
|
||||
}
|
||||
|
||||
@media print {
|
||||
* {
|
||||
color: black !important;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -811,10 +811,13 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
|
||||
get_unit_price: function(){
|
||||
return this.price;
|
||||
},
|
||||
get_display_price: function(){
|
||||
get_base_price: function(){
|
||||
var rounding = this.pos.currency.rounding;
|
||||
return round_pr(round_pr(this.get_unit_price() * this.get_quantity(),rounding) * (1- this.get_discount()/100.0),rounding);
|
||||
},
|
||||
get_display_price: function(){
|
||||
return this.get_base_price();
|
||||
},
|
||||
get_price_without_tax: function(){
|
||||
return this.get_all_prices().priceWithoutTax;
|
||||
},
|
||||
@@ -830,7 +833,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
|
||||
get_all_prices: function(){
|
||||
var self = this;
|
||||
var currency_rounding = this.pos.currency.rounding;
|
||||
var base = round_pr(this.get_quantity() * this.get_unit_price() * (1.0 - (this.get_discount() / 100.0)), currency_rounding);
|
||||
var base = this.get_base_price();
|
||||
var totalTax = base;
|
||||
var totalNoTax = base;
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
Customize access to your OpenERP database to external users by creating portals.
|
||||
================================================================================
|
||||
A portal defines a specific user menu and access rights for its members. This
|
||||
menu can ben seen by portal members, public users and any other user that
|
||||
menu can be seen by portal members, public users and any other user that
|
||||
have the access to technical features (e.g. the administrator).
|
||||
Also, each portal member is linked to a specific partner.
|
||||
|
||||
|
||||
@@ -847,7 +847,7 @@ class product_product(osv.osv):
|
||||
context['uom'], value, uom.id)
|
||||
value = value - product.price_extra
|
||||
|
||||
return product.write({'list_price': value}, context=context)
|
||||
return product.write({'list_price': value})
|
||||
|
||||
def _get_partner_code_name(self, cr, uid, ids, product, partner_id, context=None):
|
||||
for supinfo in product.seller_ids:
|
||||
@@ -898,8 +898,8 @@ class product_product(osv.osv):
|
||||
res = self.write(cr, uid, [id], {'image_variant': image}, context=context)
|
||||
product = self.browse(cr, uid, id, context=context)
|
||||
if not product.product_tmpl_id.image:
|
||||
product.write({'image_variant': None}, context=context)
|
||||
product.product_tmpl_id.write({'image': image}, context=context)
|
||||
product.write({'image_variant': None})
|
||||
product.product_tmpl_id.write({'image': image})
|
||||
return res
|
||||
|
||||
def _get_price_extra(self, cr, uid, ids, name, args, context=None):
|
||||
@@ -1062,7 +1062,7 @@ class product_product(osv.osv):
|
||||
if not limit or len(ids) < limit:
|
||||
# we may underrun the limit because of dupes in the results, that's fine
|
||||
limit2 = (limit - len(ids)) if limit else False
|
||||
ids.update(self.search(cr, user, args + [('name', operator, name)], limit=limit2, context=context))
|
||||
ids.update(self.search(cr, user, args + [('name', operator, name), ('id', 'not in', list(ids))], limit=limit2, context=context))
|
||||
ids = list(ids)
|
||||
elif not ids and operator in expression.NEGATIVE_TERM_OPERATORS:
|
||||
ids = self.search(cr, user, args + ['&', ('default_code', operator, name), ('name', operator, name)], limit=limit, context=context)
|
||||
|
||||
@@ -103,50 +103,57 @@ class project_work(osv.osv):
|
||||
res['product_uom_id'] = emp.product_id.uom_id.id
|
||||
return res
|
||||
|
||||
def create(self, cr, uid, vals, *args, **kwargs):
|
||||
timesheet_obj = self.pool.get('hr.analytic.timesheet')
|
||||
task_obj = self.pool.get('project.task')
|
||||
uom_obj = self.pool.get('product.uom')
|
||||
def _create_analytic_entries(self, cr, uid, vals, context):
|
||||
"""Create the hr analytic timesheet from project task work"""
|
||||
timesheet_obj = self.pool['hr.analytic.timesheet']
|
||||
task_obj = self.pool['project.task']
|
||||
|
||||
vals_line = {}
|
||||
timeline_id = False
|
||||
acc_id = False
|
||||
|
||||
task_obj = task_obj.browse(cr, uid, vals['task_id'], context=context)
|
||||
result = self.get_user_related_details(cr, uid, vals.get('user_id', uid))
|
||||
vals_line['name'] = '%s: %s' % (tools.ustr(task_obj.name), tools.ustr(vals['name'] or '/'))
|
||||
vals_line['user_id'] = vals['user_id']
|
||||
vals_line['product_id'] = result['product_id']
|
||||
if vals.get('date'):
|
||||
vals_line['date' ] = vals['date'][:10]
|
||||
|
||||
# Calculate quantity based on employee's product's uom
|
||||
vals_line['unit_amount'] = vals['hours']
|
||||
|
||||
default_uom = self.pool['res.users'].browse(cr, uid, uid, context=context).company_id.project_time_mode_id.id
|
||||
if result['product_uom_id'] != default_uom:
|
||||
vals_line['unit_amount'] = self.pool['product.uom']._compute_qty(cr, uid, default_uom, vals['hours'], result['product_uom_id'])
|
||||
acc_id = task_obj.project_id and task_obj.project_id.analytic_account_id.id or acc_id
|
||||
if acc_id:
|
||||
vals_line['account_id'] = acc_id
|
||||
res = timesheet_obj.on_change_account_id(cr, uid, False, acc_id)
|
||||
if res.get('value'):
|
||||
vals_line.update(res['value'])
|
||||
vals_line['general_account_id'] = result['general_account_id']
|
||||
vals_line['journal_id'] = result['journal_id']
|
||||
vals_line['amount'] = 0.0
|
||||
vals_line['product_uom_id'] = result['product_uom_id']
|
||||
amount = vals_line['unit_amount']
|
||||
prod_id = vals_line['product_id']
|
||||
unit = False
|
||||
timeline_id = timesheet_obj.create(cr, uid, vals=vals_line, context=context)
|
||||
|
||||
# Compute based on pricetype
|
||||
amount_unit = timesheet_obj.on_change_unit_amount(cr, uid, timeline_id,
|
||||
prod_id, amount, False, unit, vals_line['journal_id'], context=context)
|
||||
if amount_unit and 'amount' in amount_unit.get('value',{}):
|
||||
updv = { 'amount': amount_unit['value']['amount'] }
|
||||
timesheet_obj.write(cr, uid, [timeline_id], updv, context=context)
|
||||
|
||||
return timeline_id
|
||||
|
||||
def create(self, cr, uid, vals, *args, **kwargs):
|
||||
context = kwargs.get('context', {})
|
||||
if not context.get('no_analytic_entry',False):
|
||||
task_obj = task_obj.browse(cr, uid, vals['task_id'])
|
||||
result = self.get_user_related_details(cr, uid, vals.get('user_id', uid))
|
||||
vals_line['name'] = '%s: %s' % (tools.ustr(task_obj.name), tools.ustr(vals['name'] or '/'))
|
||||
vals_line['user_id'] = vals['user_id']
|
||||
vals_line['product_id'] = result['product_id']
|
||||
vals_line['date'] = vals['date'][:10]
|
||||
|
||||
# Calculate quantity based on employee's product's uom
|
||||
vals_line['unit_amount'] = vals['hours']
|
||||
|
||||
default_uom = self.pool.get('res.users').browse(cr, uid, uid).company_id.project_time_mode_id.id
|
||||
if result['product_uom_id'] != default_uom:
|
||||
vals_line['unit_amount'] = uom_obj._compute_qty(cr, uid, default_uom, vals['hours'], result['product_uom_id'])
|
||||
acc_id = task_obj.project_id and task_obj.project_id.analytic_account_id.id or False
|
||||
if acc_id:
|
||||
vals_line['account_id'] = acc_id
|
||||
res = timesheet_obj.on_change_account_id(cr, uid, False, acc_id)
|
||||
if res.get('value'):
|
||||
vals_line.update(res['value'])
|
||||
vals_line['general_account_id'] = result['general_account_id']
|
||||
vals_line['journal_id'] = result['journal_id']
|
||||
vals_line['amount'] = 0.0
|
||||
vals_line['product_uom_id'] = result['product_uom_id']
|
||||
amount = vals_line['unit_amount']
|
||||
prod_id = vals_line['product_id']
|
||||
unit = False
|
||||
context = dict(context, recompute=True)
|
||||
timeline_id = timesheet_obj.create(cr, uid, vals_line, context=context)
|
||||
|
||||
# Compute based on pricetype
|
||||
amount_unit = timesheet_obj.on_change_unit_amount(cr, uid, timeline_id,
|
||||
prod_id, amount, False, unit, vals_line['journal_id'], context=context)
|
||||
if amount_unit and 'amount' in amount_unit.get('value',{}):
|
||||
updv = { 'amount': amount_unit['value']['amount'] }
|
||||
timesheet_obj.write(cr, uid, [timeline_id], updv, context=context)
|
||||
vals['hr_analytic_timesheet_id'] = timeline_id
|
||||
vals['hr_analytic_timesheet_id'] = self._create_analytic_entries(cr, uid, vals, context=context)
|
||||
return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
|
||||
|
||||
def write(self, cr, uid, ids, vals, context=None):
|
||||
@@ -234,6 +241,10 @@ class task(osv.osv):
|
||||
def write(self, cr, uid, ids, vals, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
task_work_obj = self.pool['project.task.work']
|
||||
acc_id = False
|
||||
missing_analytic_entries = {}
|
||||
|
||||
if vals.get('project_id',False) or vals.get('name',False):
|
||||
vals_line = {}
|
||||
hr_anlytic_timesheet = self.pool.get('hr.analytic.timesheet')
|
||||
@@ -245,6 +256,16 @@ class task(osv.osv):
|
||||
if len(task_obj.work_ids):
|
||||
for task_work in task_obj.work_ids:
|
||||
if not task_work.hr_analytic_timesheet_id:
|
||||
if acc_id :
|
||||
# missing timesheet activities to generate
|
||||
missing_analytic_entries[task_work.id] = {
|
||||
'name' : task_work.name,
|
||||
'user_id' : task_work.user_id.id,
|
||||
'date' : task_work.date and task_work.date[:10] or False,
|
||||
'account_id': acc_id,
|
||||
'hours' : task_work.hours,
|
||||
'task_id' : task_obj.id
|
||||
}
|
||||
continue
|
||||
line_id = task_work.hr_analytic_timesheet_id.id
|
||||
if vals.get('project_id',False):
|
||||
@@ -252,7 +273,14 @@ class task(osv.osv):
|
||||
if vals.get('name',False):
|
||||
vals_line['name'] = '%s: %s' % (tools.ustr(vals['name']), tools.ustr(task_work.name) or '/')
|
||||
hr_anlytic_timesheet.write(cr, uid, [line_id], vals_line, {})
|
||||
return super(task,self).write(cr, uid, ids, vals, context)
|
||||
|
||||
res = super(task,self).write(cr, uid, ids, vals, context)
|
||||
|
||||
for task_work_id, analytic_entry in missing_analytic_entries.items():
|
||||
timeline_id = task_work_obj._create_analytic_entries(cr, uid, analytic_entry, context=context)
|
||||
task_work_obj.write(cr, uid, task_work_id, {'hr_analytic_timesheet_id' : timeline_id}, context=context)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
class res_partner(osv.osv):
|
||||
|
||||
@@ -150,7 +150,20 @@ class purchase_order(osv.osv):
|
||||
|
||||
def _get_picking_in(self, cr, uid, context=None):
|
||||
obj_data = self.pool.get('ir.model.data')
|
||||
return obj_data.get_object_reference(cr, uid, 'stock','picking_type_in') and obj_data.get_object_reference(cr, uid, 'stock','picking_type_in')[1] or False
|
||||
type_obj = self.pool.get('stock.picking.type')
|
||||
user_obj = self.pool.get('res.users')
|
||||
company_id = user_obj.browse(cr, uid, uid, context=context).company_id.id
|
||||
pick_type = obj_data.get_object_reference(cr, uid, 'stock', 'picking_type_in') and obj_data.get_object_reference(cr, uid, 'stock', 'picking_type_in')[1] or False
|
||||
if pick_type:
|
||||
type = type_obj.browse(cr, uid, pick_type, context=context)
|
||||
if type and type.warehouse_id and type.warehouse_id.company_id.id == company_id:
|
||||
return pick_type
|
||||
types = type_obj.search(cr, uid, [('code', '=', 'incoming'), ('warehouse_id.company_id', '=', company_id)], context=context)
|
||||
if not types:
|
||||
types = type_obj.search(cr, uid, [('code', '=', 'incoming'), ('warehouse_id', '=', False)], context=context)
|
||||
if not types:
|
||||
raise osv.except_osv(_('Error!'), _("Make sure you have at least an incoming picking type defined"))
|
||||
return types[0]
|
||||
|
||||
def _get_picking_ids(self, cr, uid, ids, field_names, args, context=None):
|
||||
res = {}
|
||||
|
||||
+3
-2
@@ -701,7 +701,7 @@ class sale_order(osv.osv):
|
||||
vals = self._prepare_procurement_group(cr, uid, order, context=context)
|
||||
if not order.procurement_group_id:
|
||||
group_id = self.pool.get("procurement.group").create(cr, uid, vals, context=context)
|
||||
order.write({'procurement_group_id': group_id}, context=context)
|
||||
order.write({'procurement_group_id': group_id})
|
||||
|
||||
for line in order.order_line:
|
||||
#Try to fix exception procurement (possible when after a shipping exception the user choose to recreate)
|
||||
@@ -711,10 +711,11 @@ class sale_order(osv.osv):
|
||||
line.refresh()
|
||||
#run again procurement that are in exception in order to trigger another move
|
||||
proc_ids += [x.id for x in line.procurement_ids if x.state in ('exception', 'cancel')]
|
||||
procurement_obj.reset_to_confirmed(cr, uid, proc_ids, context=context)
|
||||
elif sale_line_obj.need_procurement(cr, uid, [line.id], context=context):
|
||||
if (line.state == 'done') or not line.product_id:
|
||||
continue
|
||||
vals = self._prepare_order_line_procurement(cr, uid, order, line, group_id=group_id, context=context)
|
||||
vals = self._prepare_order_line_procurement(cr, uid, order, line, group_id=order.procurement_group_id.id, context=context)
|
||||
proc_id = procurement_obj.create(cr, uid, vals, context=context)
|
||||
proc_ids.append(proc_id)
|
||||
#Confirm procurement order such that rules will be applied on it
|
||||
|
||||
@@ -61,17 +61,33 @@ class picking(osv.osv):
|
||||
}
|
||||
|
||||
|
||||
class stock_move(osv.osv):
|
||||
_inherit = "stock.move"
|
||||
|
||||
def action_confirm(self, cr, uid, ids, context=None):
|
||||
"""
|
||||
Pass the invoice type to the picking from the sales order
|
||||
(Should also work in case of Phantom BoMs when on explosion the original move is deleted, similar to carrier_id on delivery)
|
||||
"""
|
||||
procs_to_check = []
|
||||
for move in self.browse(cr, uid, ids, context=context):
|
||||
if move.procurement_id and move.procurement_id.sale_line_id and move.procurement_id.sale_line_id.order_id.invoice_type_id:
|
||||
procs_to_check += [move.procurement_id]
|
||||
res = super(stock_move, self).action_confirm(cr, uid, ids, context=context)
|
||||
pick_obj = self.pool.get("stock.picking")
|
||||
for proc in procs_to_check:
|
||||
pickings = list(set([x.picking_id.id for x in proc.move_ids if x.picking_id and not x.picking_id.invoice_type_id]))
|
||||
if pickings:
|
||||
pick_obj.write(cr, uid, pickings, {'invoice_type_id': proc.sale_line_id.order_id.invoice_type_id.id}, context=context)
|
||||
return res
|
||||
|
||||
|
||||
class sale(osv.osv):
|
||||
_inherit = "sale.order"
|
||||
_columns = {
|
||||
'invoice_type_id': fields.many2one('sale_journal.invoice.type', 'Invoice Type', help="Generate invoice based on the selected option.")
|
||||
}
|
||||
|
||||
def _prepare_order_picking(self, cr, uid, order, context=None):
|
||||
result = super(sale,self)._prepare_order_picking(cr, uid, order, context=context)
|
||||
result.update(invoice_type_id=order.invoice_type_id and order.invoice_type_id.id or False)
|
||||
return result
|
||||
|
||||
def onchange_partner_id(self, cr, uid, ids, part, context=None):
|
||||
result = super(sale, self).onchange_partner_id(cr, uid, ids, part, context=context)
|
||||
if part:
|
||||
|
||||
@@ -38,17 +38,11 @@
|
||||
<xpath expr="//field[@name='order_line']/tree//field[@name='product_uom_qty']" position="replace">
|
||||
<field context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom}"
|
||||
name="product_uom_qty"
|
||||
on_change="product_id_change_with_wh(parent.pricelist_id,product_id,product_uom_qty,product_uom,product_uos_qty,product_uos,name,parent.partner_id, False, False, parent.date_order, product_packaging, parent.fiscal_position, True, parent.warehouse_id, context)"/>
|
||||
on_change="product_id_change_with_wh(parent.pricelist_id,product_id,product_uom_qty,product_uom,product_uos_qty,False,name,parent.partner_id, False, False, parent.date_order, product_packaging, parent.fiscal_position, True, parent.warehouse_id, context)"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='order_line']/form//field[@name='product_id']" position="replace">
|
||||
<field name="product_id"
|
||||
context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom}"
|
||||
groups="base.group_user"
|
||||
on_change="product_id_change_with_wh(parent.pricelist_id,product_id,product_uom_qty,product_uom,product_uos_qty,product_uos,name,parent.partner_id, False, True, parent.date_order, product_packaging, parent.fiscal_position, False, parent.warehouse_id, context)"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='order_line']/form//field[@name='product_uom_qty']" position="replace">
|
||||
<field context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom}"
|
||||
name="product_uom_qty" class="oe_inline"
|
||||
<xpath expr="//field[@name='order_line']/form//field[@name='product_uos_qty']" position="replace">
|
||||
<field context="{'partner_id':parent.partner_id, 'quantity':product_uos_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom}"
|
||||
name="product_uos_qty" class="oe_inline"
|
||||
on_change="product_id_change_with_wh(parent.pricelist_id,product_id,product_uom_qty,product_uom,product_uos_qty,product_uos,name,parent.partner_id, False, False, parent.date_order, product_packaging, parent.fiscal_position, True, parent.warehouse_id, context)"/>
|
||||
</xpath>
|
||||
|
||||
|
||||
@@ -273,7 +273,7 @@ class procurement_order(osv.osv):
|
||||
@param context: A standard dictionary for contextual values
|
||||
@return: Dictionary of values
|
||||
'''
|
||||
super(procurement_order, self).run_scheduler(cr, uid, use_new_cursor=use_new_cursor, context=context)
|
||||
super(procurement_order, self).run_scheduler(cr, uid, use_new_cursor=use_new_cursor, company_id=company_id, context=context)
|
||||
if context is None:
|
||||
context = {}
|
||||
try:
|
||||
@@ -283,7 +283,7 @@ class procurement_order(osv.osv):
|
||||
move_obj = self.pool.get('stock.move')
|
||||
|
||||
#Minimum stock rules
|
||||
self._procure_orderpoint_confirm(cr, SUPERUSER_ID, use_new_cursor=False, company_id=company_id, context=context)
|
||||
self._procure_orderpoint_confirm(cr, SUPERUSER_ID, use_new_cursor=use_new_cursor, company_id=company_id, context=context)
|
||||
|
||||
#Search all confirmed stock_moves and try to assign them
|
||||
confirmed_ids = move_obj.search(cr, uid, [('state', '=', 'confirmed')], limit=None, order='priority desc, date_expected asc', context=context)
|
||||
|
||||
+21
-7
@@ -2640,13 +2640,27 @@ class stock_inventory(osv.osv):
|
||||
def prepare_inventory(self, cr, uid, ids, context=None):
|
||||
inventory_line_obj = self.pool.get('stock.inventory.line')
|
||||
for inventory in self.browse(cr, uid, ids, context=context):
|
||||
#clean the existing inventory lines before redoing an inventory proposal
|
||||
# If there are inventory lines already (e.g. from import), respect those and set their theoretical qty
|
||||
line_ids = [line.id for line in inventory.line_ids]
|
||||
inventory_line_obj.unlink(cr, uid, line_ids, context=context)
|
||||
#compute the inventory lines and create them
|
||||
vals = self._get_inventory_lines(cr, uid, inventory, context=context)
|
||||
for product_line in vals:
|
||||
inventory_line_obj.create(cr, uid, product_line, context=context)
|
||||
if not line_ids:
|
||||
#compute the inventory lines and create them
|
||||
vals = self._get_inventory_lines(cr, uid, inventory, context=context)
|
||||
for product_line in vals:
|
||||
inventory_line_obj.create(cr, uid, product_line, context=context)
|
||||
else:
|
||||
# On import calculate theoretical quantity
|
||||
quant_obj = self.pool.get("stock.quant")
|
||||
for line in inventory.line_ids:
|
||||
dom = [('company_id', '=', line.company_id.id), ('location_id', 'child_of', line.location_id.id), ('lot_id', '=', line.prod_lot_id.id),
|
||||
('product_id','=', line.product_id.id), ('owner_id', '=', line.partner_id.id)]
|
||||
if line.package_id:
|
||||
dom += [('package_id', '=', line.package_id.id)]
|
||||
quants = quant_obj.search(cr, uid, dom, context=context)
|
||||
tot_qty = 0
|
||||
for quant in quant_obj.browse(cr, uid, quants, context=context):
|
||||
tot_qty += quant.qty
|
||||
inventory_line_obj.write(cr, uid, [line.id], {'theoretical_qty': tot_qty}, context=context)
|
||||
|
||||
return self.write(cr, uid, ids, {'state': 'confirm', 'date': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
|
||||
|
||||
def _get_inventory_lines(self, cr, uid, inventory, context=None):
|
||||
@@ -2712,7 +2726,7 @@ class stock_inventory_line(osv.osv):
|
||||
'company_id': fields.related('inventory_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, select=True, readonly=True),
|
||||
'prod_lot_id': fields.many2one('stock.production.lot', 'Serial Number', domain="[('product_id','=',product_id)]"),
|
||||
'state': fields.related('inventory_id', 'state', type='char', string='Status', readonly=True),
|
||||
'theoretical_qty': fields.float('Theoretical Quantity', readonly=True),
|
||||
'theoretical_qty': fields.float('Theoretical Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), readonly=True),
|
||||
'partner_id': fields.many2one('res.partner', 'Owner'),
|
||||
'product_name': fields.related('product_id', 'name', type='char', string='Product Name', store={
|
||||
'product.product': (_get_product_name_change, ['name', 'default_code'], 20),
|
||||
|
||||
@@ -1097,7 +1097,7 @@
|
||||
</group>
|
||||
<group string="Locations" groups="stock.group_locations">
|
||||
<field name="location_id" domain="[('usage','<>','view')]"/>
|
||||
<field name="location_dest_id" domain="[('usage','=','internal')]"/>
|
||||
<field name="location_dest_id" domain="[('usage','<>','view')]"/>
|
||||
</group>
|
||||
<group name="quants_grp" string="Reserved Quants" colspan="4" groups="base.group_no_one">
|
||||
<field name="reserved_quant_ids"/>
|
||||
@@ -1658,7 +1658,7 @@
|
||||
<field name="model">stock.quant</field>
|
||||
<field eval="10" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Quants">
|
||||
<form string="Quants" create="false" edit="false">
|
||||
<div class="oe_right oe_button_box">
|
||||
<button name="action_view_quant_history" type="object" string="Quant History"/>
|
||||
</div>
|
||||
|
||||
@@ -133,7 +133,7 @@ class stock_transfer_details_items(models.TransientModel):
|
||||
packop_id = fields.Many2one('stock.pack.operation', 'Operation')
|
||||
product_id = fields.Many2one('product.product', 'Product')
|
||||
product_uom_id = fields.Many2one('product.uom', 'Product Unit of Measure')
|
||||
quantity = fields.Float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), default = 1.0)
|
||||
quantity = fields.Float('Quantity', digits=dp.get_precision('Product Unit of Measure'), default = 1.0)
|
||||
package_id = fields.Many2one('stock.quant.package', 'Source package', domain="['|', ('location_id', 'child_of', sourceloc_id), ('location_id','=',False)]")
|
||||
lot_id = fields.Many2one('stock.production.lot', 'Lot/Serial Number')
|
||||
sourceloc_id = fields.Many2one('stock.location', 'Source Location', required=True)
|
||||
|
||||
@@ -282,17 +282,25 @@ class stock_move(osv.osv):
|
||||
|
||||
def product_price_update_before_done(self, cr, uid, ids, context=None):
|
||||
product_obj = self.pool.get('product.product')
|
||||
tmpl_dict = {}
|
||||
for move in self.browse(cr, uid, ids, context=context):
|
||||
#adapt standard price on incomming moves if the product cost_method is 'average'
|
||||
if (move.location_id.usage == 'supplier') and (move.product_id.cost_method == 'average'):
|
||||
product = move.product_id
|
||||
product_avail = product.qty_available
|
||||
if product.qty_available <= 0:
|
||||
prod_tmpl_id = move.product_id.product_tmpl_id.id
|
||||
qty_available = move.product_id.product_tmpl_id.qty_available
|
||||
if tmpl_dict.get(prod_tmpl_id):
|
||||
product_avail = qty_available + tmpl_dict[prod_tmpl_id]
|
||||
else:
|
||||
tmpl_dict[prod_tmpl_id] = 0
|
||||
product_avail = qty_available
|
||||
if product_avail <= 0:
|
||||
new_std_price = move.price_unit
|
||||
else:
|
||||
# Get the standard price
|
||||
amount_unit = product.standard_price
|
||||
new_std_price = ((amount_unit * product_avail) + (move.price_unit * move.product_qty)) / (product_avail + move.product_qty)
|
||||
tmpl_dict[prod_tmpl_id] += move.product_qty
|
||||
# Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products
|
||||
product_obj.write(cr, SUPERUSER_ID, [product.id], {'standard_price': new_std_price}, context=context)
|
||||
|
||||
|
||||
@@ -4372,7 +4372,6 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({
|
||||
if (!this.fields_view || !this.editable()){
|
||||
return true;
|
||||
}
|
||||
this.o2m._dirty_flag = true;
|
||||
var r;
|
||||
return _.every(this.records.records, function(record){
|
||||
r = record;
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
<template id="layout_logo_show" inherit_id="website.layout" customize_show="True" name="Show Logo">
|
||||
<xpath expr="//header//a[@class='navbar-brand']" position="replace">
|
||||
<a href="/" class="navbar-brand logo">
|
||||
<img src="/logo.png"/>
|
||||
<img src="/logo.png" t-att-alt="'Logo of %s' % res_company.name" t-att-title="res_company.name"/>
|
||||
</a>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
@@ -212,7 +212,7 @@
|
||||
t-att-title="attachment.name"/>
|
||||
</t>
|
||||
<t t-if="attachment.file_type != 'webimage'">
|
||||
<img t-att-src="'/mail/static/src/img/mimetypes/' + attachment.file_type + '.png'"
|
||||
<img t-att-src="'/mail/static/src/img/mimetypes/' + (attachment.file_type or 'disk') + '.png'"
|
||||
class='oe_attachment_webimage'
|
||||
t-att-title="attachment.name"/>
|
||||
</t>
|
||||
|
||||
@@ -179,6 +179,7 @@ class website_sale(http.Controller):
|
||||
if search:
|
||||
post["search"] = search
|
||||
if category:
|
||||
category = pool['product.public.category'].browse(cr, uid, int(category), context=context)
|
||||
url = "/shop/category/%s" % slug(category)
|
||||
|
||||
style_obj = pool['product.style']
|
||||
|
||||
@@ -63,7 +63,7 @@ class SaleOrder(orm.Model):
|
||||
if not order:
|
||||
return False
|
||||
if all(line.product_id.type == "service" for line in order.website_order_line):
|
||||
order.write({'carrier_id': None}, context=context)
|
||||
order.write({'carrier_id': None})
|
||||
self.pool['sale.order']._delivery_unset(cr, SUPERUSER_ID, [order.id], context=context)
|
||||
return True
|
||||
else:
|
||||
@@ -81,11 +81,11 @@ class SaleOrder(orm.Model):
|
||||
if grid_id:
|
||||
carrier_id = delivery_id
|
||||
break
|
||||
order.write({'carrier_id': carrier_id}, context=context)
|
||||
order.write({'carrier_id': carrier_id})
|
||||
if carrier_id:
|
||||
order.delivery_set(context=context)
|
||||
order.delivery_set()
|
||||
else:
|
||||
order._delivery_unset(context=context)
|
||||
order._delivery_unset()
|
||||
|
||||
return bool(carrier_id)
|
||||
|
||||
|
||||
Vendored
+3
-2
@@ -32,7 +32,7 @@
|
||||
<div class="sphinxsidebar">
|
||||
<div class="sphinxsidebarwrapper">
|
||||
{{ toctree(maxdepth=4, collapse=False, includehidden=True,
|
||||
main_navbar=False, titles_only=False) }}
|
||||
navbar='side', titles_only=False) }}
|
||||
{% if github_link %}
|
||||
<p><a href="{{ github_link(mode='edit') }}" class="github">
|
||||
Edit on GitHub
|
||||
@@ -72,7 +72,8 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<nav class="collapse navbar-collapse navbar-main" role="navigation">
|
||||
{{ toctree(titles_only=True, maxdepth=2, includehidden=True, collapse=False) }}
|
||||
{{ toctree(titles_only=True, maxdepth=2, includehidden=True,
|
||||
collapse=False, navbar='main') }}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
+19
-24
@@ -1,7 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sphinx.roles
|
||||
import sphinx.environment
|
||||
from sphinx.builders.html import StandaloneHTMLBuilder
|
||||
from sphinx.writers.html import HTMLTranslator
|
||||
from docutils.writers.html4css1 import HTMLTranslator as DocutilsTranslator
|
||||
|
||||
@@ -9,27 +8,17 @@ def patch():
|
||||
# navify toctree (oh god)
|
||||
@monkey(sphinx.environment.BuildEnvironment)
|
||||
def resolve_toctree(old_resolve, self, *args, **kwargs):
|
||||
""" If main_navbar, bootstrapify TOC to yield a navbar
|
||||
""" If navbar, bootstrapify TOC to yield a navbar
|
||||
|
||||
"""
|
||||
main_navbar = kwargs.pop('main_navbar', False)
|
||||
navbar = kwargs.pop('navbar', None)
|
||||
toc = old_resolve(self, *args, **kwargs)
|
||||
if toc is None:
|
||||
return None
|
||||
|
||||
navbarify(toc[0], main_navbar=main_navbar)
|
||||
navbarify(toc[0], navbar=navbar)
|
||||
return toc
|
||||
|
||||
@monkey(StandaloneHTMLBuilder)
|
||||
def _get_local_toctree(old_local, self, *args, **kwargs):
|
||||
""" _get_local_toctree generates a documentation toctree for the local
|
||||
document (?), called from handle_page
|
||||
"""
|
||||
# so can call toctree(main_navbar=False)
|
||||
d = {'main_navbar': True}
|
||||
d.update(kwargs)
|
||||
return old_local(self, *args, **d)
|
||||
|
||||
# monkeypatch visit_table to remove border and add .table
|
||||
HTMLTranslator.visit_table = visit_table
|
||||
# disable colspec crap
|
||||
@@ -37,11 +26,18 @@ def patch():
|
||||
# copy data- attributes straight from source to dest
|
||||
HTMLTranslator.starttag = starttag_data
|
||||
|
||||
def navbarify(node, main_navbar=False):
|
||||
# add classes to toplevel
|
||||
if not main_navbar:
|
||||
navify([node])
|
||||
else:
|
||||
def navbarify(node, navbar=None):
|
||||
"""
|
||||
:param node: toctree node to navbarify
|
||||
:param navbar: Whether this toctree is a 'main' navbar, a 'side' navbar or
|
||||
not a navbar at all
|
||||
"""
|
||||
if navbar == 'side':
|
||||
for n in node.traverse():
|
||||
if n.tagname == 'bullet_list':
|
||||
n['classes'].append('nav')
|
||||
elif navbar == 'main':
|
||||
# add classes to just toplevel
|
||||
node['classes'].extend(['nav', 'navbar-nav', 'navbar-right'])
|
||||
for list_item in node.children:
|
||||
# bullet_list
|
||||
@@ -52,6 +48,10 @@ def navbarify(node, main_navbar=False):
|
||||
# list_item
|
||||
# compact_paragraph
|
||||
# reference
|
||||
# no bullet_list.list_item -> don't dropdownify
|
||||
if not list_item.children[1].children:
|
||||
return
|
||||
|
||||
list_item['classes'].append('dropdown')
|
||||
# list_item.compact_paragraph.reference
|
||||
link = list_item.children[0].children[0]
|
||||
@@ -59,11 +59,6 @@ def navbarify(node, main_navbar=False):
|
||||
link.attributes['data-toggle'] = 'dropdown'
|
||||
# list_item.bullet_list
|
||||
list_item.children[1]['classes'].append('dropdown-menu')
|
||||
def navify(nodes):
|
||||
for node in nodes:
|
||||
if node.tagname == 'bullet_list':
|
||||
node['classes'].append('nav')
|
||||
navify(node.children)
|
||||
|
||||
def visit_table(self, node):
|
||||
"""
|
||||
|
||||
Vendored
+102
-13
@@ -5902,8 +5902,10 @@ button.close {
|
||||
.panel-body:after,
|
||||
.modal-footer:before,
|
||||
.modal-footer:after,
|
||||
.document-super:before,
|
||||
.document-super:after,
|
||||
.document-super:not(.stripe):before,
|
||||
.document-super:not(.stripe):after,
|
||||
.document-super.stripe:before,
|
||||
.document-super.stripe:after,
|
||||
.document:before,
|
||||
.document:after {
|
||||
content: " ";
|
||||
@@ -5924,7 +5926,8 @@ button.close {
|
||||
.pager:after,
|
||||
.panel-body:after,
|
||||
.modal-footer:after,
|
||||
.document-super:after,
|
||||
.document-super:not(.stripe):after,
|
||||
.document-super.stripe:after,
|
||||
.document:after {
|
||||
clear: both;
|
||||
}
|
||||
@@ -6181,35 +6184,53 @@ body {
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
}
|
||||
.document-super {
|
||||
.document-super:not(.stripe) {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.document-super {
|
||||
.document-super:not(.stripe) {
|
||||
width: 750px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.document-super {
|
||||
.document-super:not(.stripe) {
|
||||
width: 970px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.document-super {
|
||||
.document-super:not(.stripe) {
|
||||
width: 1170px;
|
||||
}
|
||||
}
|
||||
.document-super > .navbar-header,
|
||||
.document-super > .navbar-collapse {
|
||||
.document-super:not(.stripe) > .navbar-header,
|
||||
.document-super:not(.stripe) > .navbar-collapse {
|
||||
margin-right: -15px;
|
||||
margin-left: -15px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.document-super > .navbar-header,
|
||||
.document-super > .navbar-collapse {
|
||||
.document-super:not(.stripe) > .navbar-header,
|
||||
.document-super:not(.stripe) > .navbar-collapse {
|
||||
margin-right: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
.document-super.stripe {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
.document-super.stripe > .navbar-header,
|
||||
.document-super.stripe > .navbar-collapse {
|
||||
margin-right: -15px;
|
||||
margin-left: -15px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.document-super.stripe > .navbar-header,
|
||||
.document-super.stripe > .navbar-collapse {
|
||||
margin-right: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
@@ -6296,7 +6317,7 @@ body {
|
||||
-o-transition: all 0.3s ease-out;
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
.docs-nav .navbar-nav > li > a:after {
|
||||
.docs-nav .navbar-nav > li.dropdown > a:after {
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
@@ -6307,7 +6328,7 @@ body {
|
||||
border-right: 4px solid transparent;
|
||||
border-left: 4px solid transparent;
|
||||
}
|
||||
.docs-nav .navbar-nav > li .dropdown-menu > li.current > a.current {
|
||||
.docs-nav .navbar-nav > li.dropdown .dropdown-menu > li.current > a.current {
|
||||
background-color: #a24689;
|
||||
color: white;
|
||||
}
|
||||
@@ -6709,6 +6730,74 @@ td.field-body > ul {
|
||||
.descclassname {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.index-tree ul,
|
||||
.index-tree li {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.index-tree > ul > li {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.index-tree > ul > li > a {
|
||||
font-family: Lato, Arial, sans-serif;
|
||||
font-weight: 500;
|
||||
line-height: 1.1;
|
||||
color: inherit;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
.index-tree > ul > li > a small,
|
||||
.index-tree > ul > li > a .small {
|
||||
font-weight: normal;
|
||||
line-height: 1;
|
||||
color: #777777;
|
||||
}
|
||||
.index-tree > ul > li > a small,
|
||||
.index-tree > ul > li > a .small {
|
||||
font-size: 65%;
|
||||
}
|
||||
.index-tree > ul > li > ul {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.index-tree > ul > li > ul > li {
|
||||
width: 50%;
|
||||
}
|
||||
.index-tree > ul > li > ul > li a {
|
||||
font-family: Lato, Arial, sans-serif;
|
||||
font-weight: 500;
|
||||
line-height: 1.1;
|
||||
color: inherit;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 18px;
|
||||
padding-left: 10%;
|
||||
padding-right: 10%;
|
||||
}
|
||||
.index-tree > ul > li > ul > li a small,
|
||||
.index-tree > ul > li > ul > li a .small {
|
||||
font-weight: normal;
|
||||
line-height: 1;
|
||||
color: #777777;
|
||||
}
|
||||
.index-tree > ul > li > ul > li a small,
|
||||
.index-tree > ul > li > ul > li a .small {
|
||||
font-size: 75%;
|
||||
}
|
||||
.index-tree > ul > li a {
|
||||
display: block;
|
||||
color: #a24689 !important;
|
||||
padding: 0.3em 0;
|
||||
margin: 3px !important;
|
||||
}
|
||||
.index-tree > ul > li a:hover {
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
.stripe .section {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
+58
-15
@@ -25,9 +25,12 @@ body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.document-super {
|
||||
.document-super:not(.stripe) {
|
||||
.container();
|
||||
}
|
||||
.document-super.stripe {
|
||||
.container-fluid();
|
||||
}
|
||||
|
||||
.document {
|
||||
.make-row();
|
||||
@@ -89,20 +92,22 @@ body {
|
||||
opacity: 0;
|
||||
.transition(all 0.3s ease-out);
|
||||
}
|
||||
> a:after {
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin-left: 5px;
|
||||
vertical-align: middle;
|
||||
border-top: 4px solid @gray-light;
|
||||
border-right: 4px solid transparent;
|
||||
border-left: 4px solid transparent;
|
||||
}
|
||||
.dropdown-menu > li.current > a.current {
|
||||
background-color: @brand-primary;
|
||||
color: white;
|
||||
&.dropdown {
|
||||
> a:after {
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin-left: 5px;
|
||||
vertical-align: middle;
|
||||
border-top: 4px solid @gray-light;
|
||||
border-right: 4px solid transparent;
|
||||
border-left: 4px solid transparent;
|
||||
}
|
||||
.dropdown-menu > li.current > a.current {
|
||||
background-color: @brand-primary;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* version switcher */
|
||||
@@ -547,6 +552,44 @@ td.field-body {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
|
||||
.index-tree {
|
||||
ul, li {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
> ul > li {
|
||||
margin-bottom: 0.5em;
|
||||
> a {
|
||||
.h3();
|
||||
text-align: center;
|
||||
}
|
||||
> ul {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
> li {
|
||||
width: 50%;
|
||||
a {
|
||||
.h4();
|
||||
padding-left: 10%;
|
||||
padding-right: 10%;
|
||||
}
|
||||
}
|
||||
}
|
||||
a {
|
||||
display: block;
|
||||
color: @link-color !important;
|
||||
padding: 0.3em 0;
|
||||
margin: 3px !important;
|
||||
&:hover {
|
||||
background-color: @gray-lighter;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// STRIPE-STYLE PAGES
|
||||
.stripe {
|
||||
.section {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
:classes: stripe
|
||||
|
||||
===========
|
||||
Odoo as API
|
||||
Web Service
|
||||
===========
|
||||
|
||||
Odoo is mostly extended internally via modules, but much of its features and
|
||||
@@ -250,8 +250,8 @@ the login.
|
||||
Calling methods
|
||||
===============
|
||||
|
||||
The second — and most generally useful — is ``xmlrpc/2/object`` which is used
|
||||
to call methods of odoo models via the ``execute_kw`` RPC function.
|
||||
The second endpoint is ``xmlrpc/2/object``, is used to call methods of odoo
|
||||
models via the ``execute_kw`` RPC function.
|
||||
|
||||
Each call to ``execute_kw`` takes the following parameters:
|
||||
|
||||
@@ -1384,7 +1384,7 @@ access rights are limited.
|
||||
|
||||
Here is an example of a rule that prevents the deletion of leads that are not
|
||||
in state ``cancel``. Notice that the value of the field ``groups`` must follow
|
||||
the same convention as the method ``write`` of the ORM.
|
||||
the same convention as the method :meth:`~openerp.models.Model.write` of the ORM.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
+5
-26
@@ -4,38 +4,17 @@ odoo developer documentation
|
||||
|
||||
Welcome to the Odoo developer documentation.
|
||||
|
||||
This documentation is incomplete and may contain errors, if you wish to
|
||||
contribute, every page should have a :guilabel:`View on Github` link:
|
||||
|
||||
.. image:: images/view-on-github.*
|
||||
:align: center
|
||||
|
||||
Through this link you can edit documents and submit changes for review using
|
||||
`github's web interface
|
||||
<https://help.github.com/articles/editing-files-in-your-repository/>`_.
|
||||
Contributions are welcome and appreciated.
|
||||
|
||||
.. todo:: what's the documentation's license?
|
||||
|
||||
The documentation is currently organized in four sections:
|
||||
|
||||
* :doc:`tutorials`, aimed at introducing the primary areas of developing Odoo
|
||||
modules
|
||||
* :doc:`reference`, which ought be the complete and canonical documentation
|
||||
for Odoo subsystems
|
||||
* :doc:`modules`, documenting useful specialized modules and integration
|
||||
methods (and currently empty)
|
||||
|
||||
.. hidden toctree w/o titlesonly otherwise the titlesonly "sticks" to
|
||||
in-document toctrees and we can't have a toctree showing both "sibling"
|
||||
pages and current document sections
|
||||
|
||||
.. rst-class:: index-tree
|
||||
.. titlesonly breaks level 3 (~in-document) toc of left navbar, so use
|
||||
maxdepth instead
|
||||
.. toctree::
|
||||
:hidden:
|
||||
:maxdepth: 2
|
||||
|
||||
tutorials
|
||||
reference
|
||||
modules
|
||||
api_integration
|
||||
|
||||
.. ifconfig:: todo_include_todos
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
==============
|
||||
Module Objects
|
||||
==============
|
||||
|
||||
.. toctree::
|
||||
:titlesonly:
|
||||
|
||||
modules/api_integration
|
||||
@@ -71,7 +71,7 @@ Scaffolding is available via the :command:`odoo.py scaffold` subcommand.
|
||||
.. option:: -t <template>
|
||||
|
||||
a template directory, files are passed through jinja2_ then copied to
|
||||
the :option:`destination` directory
|
||||
the ``destination`` directory
|
||||
|
||||
.. option:: name
|
||||
|
||||
|
||||
+167
-165
@@ -421,10 +421,31 @@ added.
|
||||
is not written to the database, just used to know which value to send back
|
||||
to the client
|
||||
|
||||
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/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:
|
||||
@@ -508,170 +529,6 @@ return lists of ids, there is also a decorator managing this:
|
||||
>>> 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 Reference
|
||||
@@ -1168,3 +1025,148 @@ 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)
|
||||
|
||||
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.
|
||||
* :meth:`~openerp.models.Model.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
|
||||
* :func:`~openerp.api.depends` on ``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
|
||||
* the ``multi=`` parameter 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"
|
||||
|
||||
@@ -228,12 +228,7 @@ class ir_model(osv.osv):
|
||||
_module = False
|
||||
_custom = True
|
||||
|
||||
obj = CustomModel._build_model(self.pool, cr)
|
||||
obj._rec_name = CustomModel._rec_name = (
|
||||
'x_name' if 'x_name' in obj._columns else
|
||||
list(obj._columns)[0] if obj._columns else
|
||||
'id'
|
||||
)
|
||||
CustomModel._build_model(self.pool, cr)
|
||||
|
||||
class ir_model_fields(osv.osv):
|
||||
_name = 'ir.model.fields'
|
||||
@@ -501,6 +496,7 @@ class ir_model_fields(osv.osv):
|
||||
# we want to change the key of field in obj._fields and obj._columns
|
||||
field = obj._pop_field(rename[1])
|
||||
obj._add_field(rename[2], field)
|
||||
self.pool.setup_models(cr, partial=(not self.pool.ready))
|
||||
|
||||
if patches:
|
||||
# We have to update _columns of the model(s) and then call their
|
||||
@@ -521,11 +517,11 @@ class ir_model_fields(osv.osv):
|
||||
obj._add_field(field_name, field.new(**attrs))
|
||||
|
||||
# update database schema
|
||||
self.pool.setup_models(cr, partial=(not self.pool.ready))
|
||||
obj._auto_init(cr, ctx)
|
||||
obj._auto_end(cr, ctx) # actually create FKs!
|
||||
|
||||
if column_rename or patches:
|
||||
self.pool.setup_models(cr, partial=(not self.pool.ready))
|
||||
RegistryManager.signal_registry_change(cr.dbname)
|
||||
|
||||
return res
|
||||
|
||||
@@ -185,6 +185,9 @@ class TestNewFields(common.TransactionCase):
|
||||
with self.assertRaises(Exception):
|
||||
self.env['test_new_api.message'].create({'discussion': discussion.id, 'body': 'Whatever'})
|
||||
|
||||
# make sure that assertRaises() does not leave fields to recompute
|
||||
self.assertFalse(self.env.has_todo())
|
||||
|
||||
# put back oneself into discussion participants: now we can create
|
||||
# messages in discussion
|
||||
discussion.participants += self.env.user
|
||||
|
||||
@@ -815,6 +815,24 @@ class Environment(object):
|
||||
env.computed.clear()
|
||||
env.dirty.clear()
|
||||
|
||||
def clear(self):
|
||||
""" Clear all record caches, and discard all fields to recompute.
|
||||
This may be useful when recovering from a failed ORM operation.
|
||||
"""
|
||||
self.invalidate_all()
|
||||
self.all.todo.clear()
|
||||
|
||||
@contextmanager
|
||||
def clear_upon_failure(self):
|
||||
""" Context manager that clears the environments (caches and fields to
|
||||
recompute) upon exception.
|
||||
"""
|
||||
try:
|
||||
yield
|
||||
except Exception:
|
||||
self.clear()
|
||||
raise
|
||||
|
||||
def field_todo(self, field):
|
||||
""" Check whether `field` must be recomputed, and returns a recordset
|
||||
with all records to recompute for `field`.
|
||||
|
||||
+6
-4
@@ -426,6 +426,7 @@ class Field(object):
|
||||
# put invalidation triggers on model dependencies
|
||||
for dep_model_name, field_names in model._depends.iteritems():
|
||||
dep_model = env[dep_model_name]
|
||||
dep_model._setup_fields()
|
||||
for field_name in field_names:
|
||||
field = dep_model._fields[field_name]
|
||||
field._triggers.add((self, None))
|
||||
@@ -444,8 +445,8 @@ class Field(object):
|
||||
recs = env[self.model_name]
|
||||
fields = []
|
||||
for name in self.related:
|
||||
recs._setup_fields()
|
||||
field = recs._fields[name]
|
||||
field.setup(env)
|
||||
recs = recs[name]
|
||||
fields.append(field)
|
||||
|
||||
@@ -551,6 +552,7 @@ class Field(object):
|
||||
env = model.env
|
||||
head, tail = path1[0], path1[1:]
|
||||
|
||||
model._setup_fields()
|
||||
if head == '*':
|
||||
# special case: add triggers on all fields of model (except self)
|
||||
fields = set(model._fields.itervalues()) - set([self])
|
||||
@@ -563,8 +565,6 @@ class Field(object):
|
||||
self.recursive = True
|
||||
continue
|
||||
|
||||
field.setup(env)
|
||||
|
||||
#_logger.debug("Add trigger on %s to recompute %s", field, self)
|
||||
field._triggers.add((self, '.'.join(path0 or ['id'])))
|
||||
|
||||
@@ -1648,7 +1648,9 @@ class One2many(_RelationalMulti):
|
||||
|
||||
if self.inverse_name:
|
||||
# link self to its inverse field and vice-versa
|
||||
invf = env[self.comodel_name]._fields[self.inverse_name]
|
||||
comodel = env[self.comodel_name]
|
||||
comodel._setup_fields()
|
||||
invf = comodel._fields[self.inverse_name]
|
||||
# In some rare cases, a `One2many` field can link to `Int` field
|
||||
# (res_model/res_id pattern). Only inverse the field if this is
|
||||
# a `Many2one` field.
|
||||
|
||||
+102
-105
@@ -516,7 +516,7 @@ class BaseModel(object):
|
||||
"""
|
||||
def add(name, field):
|
||||
""" add `field` with the given `name` if it does not exist yet """
|
||||
if name not in cls._columns and name not in cls._fields:
|
||||
if name not in cls._fields:
|
||||
cls._add_field(name, field)
|
||||
|
||||
# cyclic import
|
||||
@@ -700,50 +700,46 @@ class BaseModel(object):
|
||||
pool._store_function[model].sort(key=lambda x: x[4])
|
||||
|
||||
@classmethod
|
||||
def _init_manual_fields(cls, pool, cr):
|
||||
def _init_manual_fields(cls, cr):
|
||||
# Check whether the query is already done
|
||||
if pool.fields_by_model is not None:
|
||||
manual_fields = pool.fields_by_model.get(cls._name, [])
|
||||
if cls.pool.fields_by_model is not None:
|
||||
manual_fields = cls.pool.fields_by_model.get(cls._name, [])
|
||||
else:
|
||||
cr.execute('SELECT * FROM ir_model_fields WHERE model=%s AND state=%s', (cls._name, 'manual'))
|
||||
manual_fields = cr.dictfetchall()
|
||||
|
||||
for field in manual_fields:
|
||||
if field['name'] in cls._columns:
|
||||
if field['name'] in cls._fields:
|
||||
continue
|
||||
attrs = {
|
||||
'manual': True,
|
||||
'string': field['field_description'],
|
||||
'required': bool(field['required']),
|
||||
'readonly': bool(field['readonly']),
|
||||
'domain': eval(field['domain']) if field['domain'] else None,
|
||||
'size': field['size'] or None,
|
||||
'ondelete': field['on_delete'],
|
||||
'translate': (field['translate']),
|
||||
'manual': True,
|
||||
'_prefetch': False,
|
||||
#'select': int(field['select_level'])
|
||||
}
|
||||
if field['serialization_field_id']:
|
||||
cr.execute('SELECT name FROM ir_model_fields WHERE id=%s', (field['serialization_field_id'],))
|
||||
attrs.update({'serialization_field': cr.fetchone()[0], 'type': field['ttype']})
|
||||
if field['ttype'] in ['many2one', 'one2many', 'many2many']:
|
||||
attrs.update({'relation': field['relation']})
|
||||
cls._columns[field['name']] = fields.sparse(**attrs)
|
||||
elif field['ttype'] == 'selection':
|
||||
cls._columns[field['name']] = fields.selection(eval(field['selection']), **attrs)
|
||||
elif field['ttype'] == 'reference':
|
||||
cls._columns[field['name']] = fields.reference(selection=eval(field['selection']), **attrs)
|
||||
# FIXME: ignore field['serialization_field_id']
|
||||
if field['ttype'] in ('char', 'text', 'html'):
|
||||
attrs['translate'] = bool(field['translate'])
|
||||
attrs['size'] = field['size'] or None
|
||||
elif field['ttype'] in ('selection', 'reference'):
|
||||
attrs['selection'] = eval(field['selection'])
|
||||
elif field['ttype'] == 'many2one':
|
||||
cls._columns[field['name']] = fields.many2one(field['relation'], **attrs)
|
||||
attrs['comodel_name'] = field['relation']
|
||||
attrs['ondelete'] = field['on_delete']
|
||||
attrs['domain'] = eval(field['domain']) if field['domain'] else None
|
||||
elif field['ttype'] == 'one2many':
|
||||
cls._columns[field['name']] = fields.one2many(field['relation'], field['relation_field'], **attrs)
|
||||
attrs['comodel_name'] = field['relation']
|
||||
attrs['inverse_name'] = field['relation_field']
|
||||
attrs['domain'] = eval(field['domain']) if field['domain'] else None
|
||||
elif field['ttype'] == 'many2many':
|
||||
attrs['comodel_name'] = field['relation']
|
||||
_rel1 = field['relation'].replace('.', '_')
|
||||
_rel2 = field['model'].replace('.', '_')
|
||||
_rel_name = 'x_%s_%s_%s_rel' % (_rel1, _rel2, field['name'])
|
||||
cls._columns[field['name']] = fields.many2many(field['relation'], _rel_name, 'id1', 'id2', **attrs)
|
||||
else:
|
||||
cls._columns[field['name']] = getattr(fields, field['ttype'])(**attrs)
|
||||
attrs['relation'] = 'x_%s_%s_%s_rel' % (_rel1, _rel2, field['name'])
|
||||
attrs['column1'] = 'id1'
|
||||
attrs['column2'] = 'id2'
|
||||
attrs['domain'] = eval(field['domain']) if field['domain'] else None
|
||||
cls._add_field(field['name'], Field.by_type[field['ttype']](**attrs))
|
||||
|
||||
@classmethod
|
||||
def _init_constraints_onchanges(cls):
|
||||
@@ -756,12 +752,8 @@ class BaseModel(object):
|
||||
cls._onchange_methods = defaultdict(list)
|
||||
for attr, func in getmembers(cls, callable):
|
||||
if hasattr(func, '_constrains'):
|
||||
if not all(name in cls._fields for name in func._constrains):
|
||||
_logger.warning("@constrains%r parameters must be field names", func._constrains)
|
||||
cls._constraint_methods.append(func)
|
||||
if hasattr(func, '_onchange'):
|
||||
if not all(name in cls._fields for name in func._onchange):
|
||||
_logger.warning("@onchange%r parameters must be field names", func._onchange)
|
||||
for name in func._onchange:
|
||||
cls._onchange_methods[name].append(func)
|
||||
|
||||
@@ -815,39 +807,17 @@ class BaseModel(object):
|
||||
cls._fields = {}
|
||||
above = cls.__bases__[0]
|
||||
for attr, field in getmembers(above, Field.__instancecheck__):
|
||||
if not field.inherited:
|
||||
cls._add_field(attr, field.new())
|
||||
cls._add_field(attr, field.new())
|
||||
|
||||
# introduce magic fields
|
||||
cls._add_magic_fields()
|
||||
|
||||
# register stuff about low-level function fields and custom fields
|
||||
cls._init_function_fields(pool, cr)
|
||||
cls._init_manual_fields(pool, cr)
|
||||
|
||||
# process _inherits
|
||||
cls._inherits_check()
|
||||
cls._inherits_reload()
|
||||
|
||||
# register constraints and onchange methods
|
||||
cls._init_constraints_onchanges()
|
||||
|
||||
# check defaults
|
||||
for k in cls._defaults:
|
||||
assert k in cls._fields, \
|
||||
"Model %s has a default for non-existing field %s" % (cls._name, k)
|
||||
|
||||
# restart columns
|
||||
for column in cls._columns.itervalues():
|
||||
column.restart()
|
||||
|
||||
# validate rec_name
|
||||
if cls._rec_name:
|
||||
assert cls._rec_name in cls._fields, \
|
||||
"Invalid rec_name %s for model %s" % (cls._rec_name, cls._name)
|
||||
elif 'name' in cls._fields:
|
||||
cls._rec_name = 'name'
|
||||
|
||||
# prepare ormcache, which must be shared by all instances of the model
|
||||
cls._ormcache = {}
|
||||
|
||||
@@ -2902,47 +2872,34 @@ class BaseModel(object):
|
||||
# Update objects that uses this one to update their _inherits fields
|
||||
#
|
||||
|
||||
@classmethod
|
||||
def _inherits_reload_src(cls):
|
||||
""" Recompute the _inherit_fields mapping on each _inherits'd child model."""
|
||||
for model in cls.pool.values():
|
||||
if cls._name in model._inherits:
|
||||
model._inherits_reload()
|
||||
|
||||
@classmethod
|
||||
def _inherits_reload(cls):
|
||||
""" Recompute the _inherit_fields mapping.
|
||||
""" Recompute the _inherit_fields mapping, and inherited fields. """
|
||||
struct = {}
|
||||
fields = {}
|
||||
for parent_model, parent_field in cls._inherits.iteritems():
|
||||
parent = cls.pool[parent_model]
|
||||
# old-api struct for _inherit_fields
|
||||
for name, column in parent._columns.iteritems():
|
||||
struct[name] = (parent_model, parent_field, column, parent_model)
|
||||
for name, source in parent._inherit_fields.iteritems():
|
||||
struct[name] = (parent_model, parent_field, source[2], source[3])
|
||||
# new-api fields for _fields
|
||||
for name, field in parent._fields.iteritems():
|
||||
fields[name] = field.new(
|
||||
inherited=True,
|
||||
related=(parent_field, name),
|
||||
related_sudo=False,
|
||||
)
|
||||
|
||||
This will also call itself on each inherits'd child model.
|
||||
|
||||
"""
|
||||
res = {}
|
||||
for table in cls._inherits:
|
||||
other = cls.pool[table]
|
||||
for col in other._columns.keys():
|
||||
res[col] = (table, cls._inherits[table], other._columns[col], table)
|
||||
for col in other._inherit_fields.keys():
|
||||
res[col] = (table, cls._inherits[table], other._inherit_fields[col][2], other._inherit_fields[col][3])
|
||||
cls._inherit_fields = res
|
||||
# old-api stuff
|
||||
cls._inherit_fields = struct
|
||||
cls._all_columns = cls._get_column_infos()
|
||||
|
||||
# interface columns with new-style fields
|
||||
for attr, column in cls._columns.items():
|
||||
if attr not in cls._fields:
|
||||
cls._add_field(attr, column.to_field())
|
||||
|
||||
# interface inherited fields with new-style fields (note that the
|
||||
# reverse order is for being consistent with _all_columns above)
|
||||
for parent_model, parent_field in reversed(cls._inherits.items()):
|
||||
for attr, field in cls.pool[parent_model]._fields.iteritems():
|
||||
if attr not in cls._fields:
|
||||
cls._add_field(attr, field.new(
|
||||
inherited=True,
|
||||
related=(parent_field, attr),
|
||||
related_sudo=False,
|
||||
))
|
||||
|
||||
cls._inherits_reload_src()
|
||||
# add inherited fields that are not redefined locally
|
||||
for name, field in fields.iteritems():
|
||||
if name not in cls._fields:
|
||||
cls._add_field(name, field)
|
||||
|
||||
@classmethod
|
||||
def _get_column_infos(cls):
|
||||
@@ -2982,34 +2939,74 @@ class BaseModel(object):
|
||||
@api.model
|
||||
def _prepare_setup_fields(self):
|
||||
""" Prepare the setup of fields once the models have been loaded. """
|
||||
for field in self._fields.itervalues():
|
||||
field.reset()
|
||||
type(self)._setup_done = False
|
||||
for name, field in self._fields.items():
|
||||
if field.inherited:
|
||||
del self._fields[name]
|
||||
else:
|
||||
field.reset()
|
||||
|
||||
@api.model
|
||||
def _setup_fields(self, partial=False):
|
||||
def _setup_fields(self):
|
||||
""" Setup the fields (dependency triggers, etc). """
|
||||
for field in self._fields.itervalues():
|
||||
try:
|
||||
field.setup(self.env)
|
||||
except Exception:
|
||||
if not partial:
|
||||
raise
|
||||
cls = type(self)
|
||||
if cls._setup_done:
|
||||
return
|
||||
cls._setup_done = True
|
||||
|
||||
# update columns (fields may have changed), and column_infos
|
||||
for name, field in self._fields.iteritems():
|
||||
# first make sure that parent models are all set up
|
||||
for parent in self._inherits:
|
||||
self.env[parent]._setup_fields()
|
||||
|
||||
# retrieve custom fields
|
||||
if not self._context.get('_setup_fields_partial'):
|
||||
cls._init_manual_fields(self._cr)
|
||||
|
||||
# retrieve inherited fields
|
||||
cls._inherits_check()
|
||||
cls._inherits_reload()
|
||||
|
||||
# set up fields
|
||||
for field in cls._fields.itervalues():
|
||||
field.setup(self.env)
|
||||
|
||||
# update columns (fields may have changed)
|
||||
for name, field in cls._fields.iteritems():
|
||||
if field.column:
|
||||
self._columns[name] = field.to_column()
|
||||
self._inherits_reload()
|
||||
cls._columns[name] = field.to_column()
|
||||
|
||||
# group fields by compute to determine field.computed_fields
|
||||
fields_by_compute = defaultdict(list)
|
||||
for field in self._fields.itervalues():
|
||||
for field in cls._fields.itervalues():
|
||||
if field.compute:
|
||||
field.computed_fields = fields_by_compute[field.compute]
|
||||
field.computed_fields.append(field)
|
||||
else:
|
||||
field.computed_fields = []
|
||||
|
||||
# check constraints
|
||||
for func in cls._constraint_methods:
|
||||
if not all(name in cls._fields for name in func._constrains):
|
||||
_logger.warning("@constrains%r parameters must be field names", func._constrains)
|
||||
for name in cls._onchange_methods:
|
||||
if name not in cls._fields:
|
||||
func = cls._onchange_methods[name]
|
||||
_logger.warning("@onchange%r parameters must be field names", func._onchange)
|
||||
|
||||
# check defaults
|
||||
for name in cls._defaults:
|
||||
assert name in cls._fields, \
|
||||
"Model %s has a default for nonexiting field %s" % (cls._name, name)
|
||||
|
||||
# validate rec_name
|
||||
if cls._rec_name:
|
||||
assert cls._rec_name in cls._fields, \
|
||||
"Invalid rec_name %s for model %s" % (cls._rec_name, cls._name)
|
||||
elif 'name' in cls._fields:
|
||||
cls._rec_name = 'name'
|
||||
elif 'x_name' in cls._fields:
|
||||
cls._rec_name = 'x_name'
|
||||
|
||||
def fields_get(self, cr, user, allfields=None, context=None, write_access=True):
|
||||
""" fields_get([fields])
|
||||
|
||||
|
||||
@@ -164,8 +164,9 @@ class Registry(Mapping):
|
||||
|
||||
# do the actual setup from a clean state
|
||||
self._m2m = {}
|
||||
context = {'_setup_fields_partial': partial}
|
||||
for model in self.models.itervalues():
|
||||
model._setup_fields(cr, SUPERUSER_ID, partial=partial)
|
||||
model._setup_fields(cr, SUPERUSER_ID, context=context)
|
||||
|
||||
def clear_caches(self):
|
||||
""" Clear the caches
|
||||
|
||||
@@ -136,9 +136,8 @@ class _column(object):
|
||||
for key, val in args.iteritems():
|
||||
setattr(self, key, val)
|
||||
|
||||
# prefetch only if self._classic_write, not self.groups, and not
|
||||
# self.deprecated
|
||||
if not self._classic_write or self.deprecated:
|
||||
# prefetch only if _classic_write, not deprecated and not manual
|
||||
if not self._classic_write or self.deprecated or self.manual:
|
||||
self._prefetch = False
|
||||
|
||||
def new(self, **args):
|
||||
|
||||
@@ -16,6 +16,7 @@ import time
|
||||
import unittest2
|
||||
import urllib2
|
||||
import xmlrpclib
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import werkzeug
|
||||
@@ -104,6 +105,20 @@ class BaseCase(unittest2.TestCase):
|
||||
module, xid = xid.split('.')
|
||||
return self.registry('ir.model.data').get_object(self.cr, self.uid, module, xid)
|
||||
|
||||
@contextmanager
|
||||
def _assertRaises(self, exception):
|
||||
""" Context manager that clears the environment upon failure. """
|
||||
with super(BaseCase, self).assertRaises(exception) as cm:
|
||||
with self.env.clear_upon_failure():
|
||||
yield cm
|
||||
|
||||
def assertRaises(self, exception, func=None, *args, **kwargs):
|
||||
if func:
|
||||
with self._assertRaises(exception):
|
||||
func(*args, **kwargs)
|
||||
else:
|
||||
return self._assertRaises(exception)
|
||||
|
||||
|
||||
class TransactionCase(BaseCase):
|
||||
""" TestCase in which each test method is run in its own transaction,
|
||||
@@ -120,6 +135,8 @@ class TransactionCase(BaseCase):
|
||||
self.env = api.Environment(self.cr, self.uid, {})
|
||||
|
||||
def tearDown(self):
|
||||
# rollback and close the cursor, and reset the environments
|
||||
self.env.reset()
|
||||
self.cr.rollback()
|
||||
self.cr.close()
|
||||
|
||||
@@ -139,9 +156,12 @@ class SingleTransactionCase(BaseCase):
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
# rollback and close the cursor, and reset the environments
|
||||
cls.env.reset()
|
||||
cls.cr.rollback()
|
||||
cls.cr.close()
|
||||
|
||||
|
||||
class RedirectHandler(urllib2.HTTPRedirectHandler):
|
||||
"""
|
||||
HTTPRedirectHandler is predicated upon HTTPErrorProcessor being used and
|
||||
|
||||
Reference in New Issue
Block a user