How to browse or search One2many field in Odoo?

2020-07-13 10:57发布

问题:

I've extended the 'account.analytic.account' model with a One2many field that references a second model with a One2many field.

When I try to iterate through the second One2many field from the compute method it only lists records that have just been added. Previous records (which are visible on the interface) are not visible in code using the 'self' context until the parent record is saved.

example:

for line in self.One2manyField:
    #only gets here for records I add during current session, or all records if parent is saved
    #how can I see previously saved records? 

Here is the code:

1.) extended 'account.analytic.account' model

class account_analytic_account(models.Model):

    _inherit = ['account.analytic.account']

    service_location_ids = fields.One2many(comodel_name='contract.service.location', inverse_name='contract_id', copy=True)

2.) First referenced One2many model:

class Contract_Service_Location(models.Model):
    _name = 'contract.service.location'
    _description = 'Service Location Record'  

    #problem is here!
    #compute method for subtotal field
    @api.one    
    @api.depends('recurring_line_ids','recurring_line_ids.price_subtotal')
    def _compute_subtotal(self):
        total = 0.0

        #I tried to get previously saved ids, but returns nothing, until parent record is saved
        old_ids = self.env['contract.recurring.line'].search([('service_location_id', '=', self.id)]) 

        #this only works for new entries during same session, or until parent record is saved. Why?
        for line in self.recurring_line_ids:
            total = total + line.price_subtotal

        #set field
        self.price_subtotal = total

    contract_id = fields.Many2one(comodel_name='account.analytic.account')
    fiscal_position = fields.Many2one(comodel_name='account.fiscal.position', string='Default Taxes')
    partner_id = fields.Many2one(comodel_name='res.partner', string='Service Location', help='Optional seperate billing address from customer AND service locations',required=True)
    sequence = fields.Integer(string='Sequence', help="Gives the sequence order when displaying a list of sales order lines.")
    price_subtotal = fields.Float(compute='_compute_subtotal', string='Subtotal', digits_compute= dp.get_precision('Account'), readonly=True, store=True)
    pricelist_id = fields.Many2one(comodel_name='product.pricelist', string='Pricelist', required=True, help="Pricelist for current customer.", default=_get_default_pricelist)
    recurring_line_ids = fields.One2many(comodel_name='contract.recurring.line', inverse_name='service_location_id', copy=True)

3.) Second referenced One2many model:

class Contract_Recurring_Line(models.Model):
    _name = 'contract.recurring.line'
    _description = 'Recurring Service Location Line'


    @api.one
    @api.depends('price_unit', 'discount', 'product_uom_qty','product_uos_qty',
        'product_id', 'service_location_id.partner_id','service_location_id.pricelist_id')
    def _compute_subtotal(self):
        price = self.price_unit * (1 - (self.discount or 0.0) / 100.0)
        taxes = self.tax_id.compute_all(price, self.product_uom_qty, product=self.product_id, partner=self.service_location_id.partner_id)
        self.price_subtotal = taxes['total']
        if self.service_location_id:
            self.price_subtotal = self.service_location_id.pricelist_id.currency_id.round(self.price_subtotal)


    service_location_id = fields.Many2one(comodel_name='contract.service.location', required=True, ondelete='cascade', select=True)
    name = fields.Text('Description', required=True)
    product_id = fields.Many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True, ondelete='restrict')
    price_unit = fields.Float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'))
    price_subtotal = fields.Float(compute='_compute_subtotal', string='Subtotal',store=True, readonly=True, digits_compute= dp.get_precision('Account'))
    product_uom_qty = fields.Float('Quantity', default=float(1), digits_compute= dp.get_precision('Product UoS'))
    discount = fields.Float('Discount (%)', digits_compute= dp.get_precision('Discount'))

回答1:

Sadly OpenERP/Odoo only supports one level of Relationship in On-change methods, compute methods, and record modification tracking.

So out of the box, Parent/Child setups are allowed (as in FORMVIEW.one2manyLIST) but not Grandparent/Parent/Child (as in FORMVIEW.one2manyLIST.one2manyLIST).

An example of this would be:

  • Model A - Presented as service warranty form (grandparent model)
  • Model B - List of covered locations (parent model, with ref to grandparent)
  • Model C - List of services provided for each covered location (child model, with ref to parent)

Changes made to Model A will not save records of Model C, and on-change/compute methods on Model A cannot use fields on Model C

So, changes are lost and the problem above is experienced if changes are made on the second nested one2many field, or even if you attempt to read the nested one2many field.

I have created a solution that adds another level of tracking to the onchange/compute/write. You will need to modify the core of Odoo. I hope this changes in the future as Odoo SA has this listed on github as "wishlist".

Here is the code for Odoo 8.0. YMMV with other versions.

FIELDS.PY/_RelationalMulti Class. Replace the following method:

def convert_to_write(self, value, target=None, fnames=None):
    # remove/delete former records
    if target is None:
        set_ids = []
        result = [(6, 0, set_ids)]
        add_existing = lambda id: set_ids.append(id)
    else:
        tag = 2 if self.type == 'one2many' else 3
        result = [(tag, record.id) for record in target[self.name] - value]
        add_existing = lambda id: result.append((4, id))

    if fnames is None:
        # take all fields in cache, except the inverses of self
        fnames = set(value._fields) - set(MAGIC_COLUMNS)
        for invf in self.inverse_fields:
            fnames.discard(invf.name)

    # add new and existing records
    for record in value:
        if not record.id or record._dirty:
            values = dict((k, v) for k, v in record._cache.iteritems() if k in fnames)
            tempVal = {}
            for n in values:
                f = record._fields[n] #get field def
                if f.type == 'one2many':
                    subrec = record[n]
                    subfields = subrec._fields                    
                    tempVal[n] = f.convert_to_write(subrec,record)
                else:
                    val = {}
                    val[n] = values.get(n)
                    tempVal[n] = record._convert_to_write(val)[n]

            if tempVal:
                values = tempVal       

            #add to result       
            if not record.id:
                    result.append((0, 0, values))
            else:
                result.append((1, record.id, values))
        else:
            add_existing(record.id)

    return result

In MODELS.py/BaseModel Class, replace the following method:

@api.model
def new(self, values={}):
    """ new([values]) -> record

    Return a new record instance attached to the current environment and
    initialized with the provided ``value``. The record is *not* created
    in database, it only exists in memory.
    """
    record = self.browse([NewId()])
    record._cache.update(record._convert_to_cache(values, update=True))

    if record.env.in_onchange:
        # The cache update does not set inverse fields, so do it manually.
        # This is useful for computing a function field on secondary
        # records, if that field depends on the main record.
        for name in values:
            field = self._fields.get(name)
            if field:
                try:
                    for invf in field.inverse_fields:
                        invf._update(record[name], record)

                        #serarch this field for sub inverse fields
                        for ftmp in self[name]._fields: 

                                f = self[name]._fields.get(ftmp) 
                                if f and f != invf:                  
                                    for invf in f.inverse_fields:
                                        val = record[name]
                                        invf._update(record[name][ftmp], val)

                except:
                    pass

    return record