[MERGE] forward port of branch 8.0 up to c92e70b

This commit is contained in:
Christophe Simonis
2014-11-17 17:31:12 +01:00
53 changed files with 745 additions and 486 deletions
+14 -1
View File
@@ -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 -3
View File
@@ -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]) &lt;= 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
+1 -1
View File
@@ -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
+1 -5
View File
@@ -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
})
})
+20 -1
View File
@@ -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]
+4 -2
View File
@@ -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
+3 -3
View File
@@ -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": ''
+1 -1
View File
@@ -24,7 +24,7 @@
</div>
<div class="oe_suggested_item_content">
<a class="oe_suggestion_item_name" t-attf-href="#model=hr.employee&amp;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>
+1 -1
View File
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
10 access_account_fiscalyear_hr_user account.account.fiscalyear.user account.model_account_fiscalyear base.group_hr_user 1 1 1 1
11 access_hr_analytic_timesheet_user hr.analytic.timesheet.user model_hr_analytic_timesheet base.group_user 1 1 1 0
12 access_account_analytic_journal_user account.account.analytic.journal.user analytic.model_account_analytic_journal base.group_user 1 0 0 0
13 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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
3 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
4 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
5 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
6 access_hr_analytic_timesheet_system_user hr.analytic.timesheet.system.user model_hr_analytic_timesheet base.group_user 1 0 0 0
7 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
+1 -1
View File
@@ -25,7 +25,7 @@
</div>
<div class="oe_suggested_item_content">
<a class="oe_suggestion_item_name" t-attf-href="#model=mail.group&amp;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
View File
@@ -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;
}
+5 -2
View File
@@ -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;
+1 -1
View File
@@ -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.
+4 -4
View File
@@ -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)
+69 -41
View File
@@ -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):
+14 -1
View File
@@ -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
View File
@@ -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
+21 -5
View File
@@ -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:
+4 -10
View File
@@ -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>
+2 -2
View File
@@ -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
View File
@@ -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),
+2 -2
View File
@@ -1097,7 +1097,7 @@
</group>
<group string="Locations" groups="stock.group_locations">
<field name="location_id" domain="[('usage','&lt;&gt;','view')]"/>
<field name="location_dest_id" domain="[('usage','=','internal')]"/>
<field name="location_dest_id" domain="[('usage','&lt;&gt;','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)
+10 -2
View File
@@ -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)
-1
View File
@@ -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;
+1 -1
View File
@@ -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>
+1
View File
@@ -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)
+3 -2
View File
@@ -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
View File
@@ -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):
"""
+102 -13
View File
@@ -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
View File
@@ -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:
+1 -1
View File
@@ -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
View File
@@ -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
-8
View File
@@ -1,8 +0,0 @@
==============
Module Objects
==============
.. toctree::
:titlesonly:
modules/api_integration
+1 -1
View File
@@ -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
View File
@@ -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"
+3 -7
View File
@@ -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
+18
View File
@@ -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
View File
@@ -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
View File
@@ -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])
+2 -1
View File
@@ -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
+2 -3
View File
@@ -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):
+20
View File
@@ -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