A functional field is a field whose value is calculated by a function (rather
than being stored in the database).
fnct parameter
If
method is True, the signature of the method must be:
def fnct(self, cr, uid, ids, field_name, arg, context):
otherwise (if it is a global function), its signature must be:
def fnct(cr, table, ids, field_name, arg, context):
Either way, it must return a dictionary of values of the form
{id’_1_’: value’_1_’, id’_2_’: value’_2_’,...}.
The values of the returned dictionary must be of the type specified by the type
argument in the field declaration.
If
multi is set, then
field_name is replaced by
field_names: a list
of the field names that should be calculated. Each value in the returned
dictionary is also a dictionary from field name to value. For example, if the
fields
‘name’, and
‘age’ are both based on the
vital_statistics function,
then the return value of
vital_statistics might look like this when
ids is
[1, 2, 5]:
{
1: {'name': 'Bob', 'age': 23},
2: {'name': 'Sally', 'age', 19},
5: {'name': 'Ed', 'age': 62}
}
fnct_search parameter
If method is true, the signature of the method must be:
def fnct(self, cr, uid, obj, name, args, context):
otherwise (if it is a global function), it should be:
def fnct(cr, uid, obj, name, args, context):
The return value is a list containing 3-part tuples which are used in search function:
return [('id','in',[1,3,5])]
obj is the same as
self, and
name receives the field name.
args is a list
of 3-part tuples containing search criteria for this field, although the search
function may be called separately for each tuple.
Example
Suppose we create a contract object which is :
class hr_contract(osv.osv):
_name = 'hr.contract'
_description = 'Contract'
_columns = {
'name' : fields.char('Contract Name', size=30, required=True),
'employee_id' : fields.many2one('hr.employee', 'Employee', required=True),
'function' : fields.many2one('res.partner.function', 'Function'),
}
hr_contract()
If we want to add a field that retrieves the function of an employee
by looking its current contract, we use a functional field. The object
hr_employee is inherited this way:
class hr_employee(osv.osv):
_name = "hr.employee"
_description = "Employee"
_inherit = "hr.employee"
_columns = {
'contract_ids' : fields.one2many('hr.contract', 'employee_id', 'Contracts'),
'function' : fields.function(
_get_cur_function_id,
type='many2one',
obj="res.partner.function",
method=True,
string='Contract Function'),
}
hr_employee()
Note
three points
- type =’many2one’ is because the function field must create
a many2one field; function is declared as a many2one in hr_contract also.
- obj =”res.partner.function” is used to specify that the
object to use for the many2one field is res.partner.function.
- We called our method _get_cur_function_id because its role
is to return a dictionary whose keys are ids of employees, and whose
corresponding values are ids of the function of those employees. The
code of this method is:
def _get_cur_function_id(self, cr, uid, ids, field_name, arg, context):
for i in ids:
#get the id of the current function of the employee of identifier "i"
sql_req= """
SELECT f.id AS func_id
FROM hr_contract c
LEFT JOIN res_partner_function f ON (f.id = c.function)
WHERE
(c.employee_id = %d)
""" % (i,)
cr.execute(sql_req)
sql_res = cr.dictfetchone()
if sql_res: #The employee has one associated contract
res[i] = sql_res['func_id']
else:
#res[i] must be set to False and not to None because of XML:RPC
# "cannot marshal None unless allow_none is enabled"
res[i] = False
return res
The id of the function is retrieved using a SQL query. Note that if the query
returns no result, the value of sql_res[‘func_id’] will be None. We force the
False value in this case value because XML:RPC (communication between the server
and the client) doesn’t allow to transmit this value.
store Parameter
It will calculate the field and store the result in the table. The field will be
recalculated when certain fields are changed on other objects. It uses the
following syntax:
store = {
'object_name': (
function_name,
['field_name1', 'field_name2'],
priority)
}
It will call function function_name when any changes are written to fields in the
list [‘field1’,’field2’] on object ‘object_name’. The function should have the
following signature:
def function_name(self, cr, uid, ids, context=None):
Where
ids will be the ids of records in the other object’s table that have
changed values in the watched fields. The function should return a list of ids
of records in its own table that should have the field recalculated. That list
will be sent as a parameter for the main function of the field.
Here’s an example from the membership module:
'membership_state':
fields.function(
_membership_state,
method=True,
string='Current membership state',
type='selection',
selection=STATE,
store={
'account.invoice': (_get_invoice_partner, ['state'], 10),
'membership.membership_line': (_get_partner_id,['state'], 10),
'res.partner': (
lambda self, cr, uid, ids, c={}: ids,
['free_member'],
10)
}),
Property Fields
-
Declaring a property
A property is a special field: fields.property.
class res_partner(osv.osv):
_name = "res.partner"
_inherit = "res.partner"
_columns = {
'property_product_pricelist':
fields.property(
'product.pricelist',
type='many2one',
relation='product.pricelist',
string="Sale Pricelist",
method=True,
group_name="Pricelists Properties"),
}
Then you have to create the default value in a .XML file for this property:
<record model="ir.property" id="property_product_pricelist">
<field name="name">property_product_pricelist</field>
<field name="fields_id" search="[('model','=','res.partner'),
('name','=','property_product_pricelist')]"/>
<field name="value" eval="'product.pricelist,'+str(list0)"/>
</record>
Tip
if the default value points to a resource from another module, you can use the ref function like this:
<field name=”value” eval=“‘product.pricelist,’+str(ref(‘module.data_id’))”/>
Putting properties in forms
To add properties in forms, just put the <properties/> tag in
your form. This will automatically add all properties fields that are
related to this object. The system will add properties depending on your
rights. (some people will be able to change a specific property, others
won’t).
Properties are displayed by section, depending on the group_name attribute. (It is rendered in the client like a separator tag).
How does this work ?
The fields.property class inherits from fields.function and overrides
the read and write method. The type of this field is many2one, so in
the form a property is represented like a many2one function.
But the value of a property is stored in the ir.property class/table
as a complete record. The stored value is a field of type reference (not
many2one) because each property may point to a different object. If you
edit properties values (from the administration menu), these are
represented like a field of type reference.
When you read a property, the program gives you the property attached
to the instance of object you are reading. If this object has no value,
the system will give you the default property.
The definition of a property is stored in the ir.model.fields class
like any other fields. In the definition of the property, you can add
groups that are allowed to change to property.
Using properties or normal fields
When you want to add a new feature, you will have to choose to
implement it as a property or as normal field. Use a normal field when
you inherit from an object and want to extend this object. Use a
property when the new feature is not related to the object but to an
external concept.
Here are a few tips to help you choose between a normal field or a property:
Normal fields extend the object, adding more features or data.
A property is a concept that is attached to an object and have special features:
- Different value for the same property depending on the company
- Rights management per field
- It’s a link between resources (many2one)
Example 1: Account Receivable
The default “Account Receivable” for a specific partner is implemented as a property because:
- This is a concept related to the account chart and not to the
partner, so it is an account property that is visible on a partner form.
Rights have to be managed on this fields for accountants, these are not
the same rights that are applied to partner objects. So you have
specific rights just for this field of the partner form: only
accountants may change the account receivable of a partner.
- This is a multi-company field: the same partner may have different
account receivable values depending on the company the user belongs to.
In a multi-company system, there is one account chart per company. The
account receivable of a partner depends on the company it placed the
sale order.
- The default account receivable is the same for all partners and is
configured from the general property menu (in administration).
Note
One interesting thing is that properties avoid
“spaghetti” code. The account module depends on the partner (base)
module. But you can install the partner (base) module without the
accounting module. If you add a field that points to an account in the
partner object, both objects will depend on each other. It’s much more
difficult to maintain and code (for instance, try to remove a table when
both tables are pointing to each others.)
Example 2: Product Times
The product expiry module implements all delays related to products:
removal date, product usetime, ... This module is very useful for food
industries.
This module inherits from the product.product object and adds new fields to it:
class product_product(osv.osv):
_inherit = 'product.product'
_name = 'product.product'
_columns = {
'life_time': fields.integer('Product lifetime'),
'use_time': fields.integer('Product usetime'),
'removal_time': fields.integer('Product removal time'),
'alert_time': fields.integer('Product alert time'),
}
product_product()
This module adds simple fields to the product.product object. We did not use properties because:
- We extend a product, the life_time field is a concept related to a product, not to another object.
- We do not need a right management per field, the different delays are managed by the same people that manage all products.