Filter a Many2One field with a domain that uses another Many2One field

Hi everyone,

I’m creating a module allowing to manage Vehicles. It allows to create a Mission, choose the vehicles category to use for that mission, and for each choosen category, to create a MissionOrder (the MissionOrder makes the link between the Mission, the Driver doing the mission, and the vehicle to use).

In the form allowing to create a new MissionOrder, I’m trying to filter the field truck (which is the vehicle to use for the mission), so that the user can choose only between available vehicles in the right category. The field is a Many2One field with a domain, but no matter how I write the domain, I always end up with a javascript error from Sao.
The error is :

TypeError: Cannot use 'in' operator to search for 'truck_category' in null

My models are defined this way :

class VehicleCategory(ModelSQL, ModelView):
    'VehicleCategory'
    __name__ = 'transport.vehicle_category'
    _rec_name = 'code'
    # Relations with other models
    vehicles = fields.One2Many('transport.vehicle', 'category', 'Vehicles')
    parent = fields.Many2One('transport.vehicle_category', 'Parent Category', ondelete='RESTRICT', required=False, select=True)
    subcategories = fields.One2Many('transport.vehicle_category', 'parent', 'Subcategories')
    # Model properties
    code = fields.Char('Code', required=True)
    label = fields.Char('Label', required=True)
    
    # Add a unicity constraint on the field "code"
    @classmethod
    def __setup__(cls):
        super().__setup__()
        t = cls.__table__()
        cls._sql_constraints += [
            ('code_uniq', Unique(t, t.code), 'The Code must be unique.'),
        ]

class Vehicle(ModelSQL, ModelView):
    'Vehicle'
    __name__ = 'transport.vehicle'
    _rec_name = 'license_number'
    # Relations with other models
    category = fields.Many2One('transport.vehicle_category', 'Category', ondelete='RESTRICT', required=True)
    # Model properties
    license_number = fields.Char('License Number', required=True)
    label = fields.Char('Label', required=True)
    
    # Add a unicity constraint on the field "license_number"
    @classmethod
    def __setup__(cls):
        super().__setup__()
        t = cls.__table__()
        cls._sql_constraints += [
            ('license_number_uniq', Unique(t, t.license_number), 'The license number must be unique.'),
        ]

    
class Driver(ModelSQL, ModelView):
    'Driver'
    __name__ = 'transport.driver'
    _rec_name = 'code'
    # Relations with other models
    employee = fields.Many2One('company.employee', 'Employee', ondelete='RESTRICT', required=True)
    # Model properties
    code = fields.Char('Code', required=True)
    license_number = fields.Char('License Number', required=True)
    
    # Add a unicity constraint on the field "code"
    @classmethod
    def __setup__(cls):
        super().__setup__()
        t = cls.__table__()
        cls._sql_constraints += [
            ('code_uniq', Unique(t, t.code), 'The Code must be unique.'),
        ]

class Mission(ModelSQL, ModelView):
    'Mission'
    __name__ = 'transport.mission'
    _rec_name = 'description'
    # Model properties
    identifier = fields.Char('Identifier', required=True)
    date = fields.Date('Date', required=True)
    start_time = fields.Time('Start Time', required=True)
    end_time = fields.Time('End Time', required=True)
    vehicle_categories = fields.One2Many('transport.mission_vehicle_category', 'mission', 'Vehicle Categories')

class MissionVehicleCategory(ModelSQL, ModelView):
    'MissionVehicleCategory'
    __name__ = 'transport.mission_vehicle_category'
    _rec_name = 'rec_name'
    # Relations with other models
    mission = fields.Many2One('transport.mission', 'Mission', ondelete='CASCADE', required=True)
    truck_category = fields.Many2One('transport.vehicle_category', 'Truck Type', ondelete='RESTRICT', required=True,
        domain=['AND',
            ('code','not in', ['REM','SR']),
            ['OR',
                ('parent', '=', None),
                ('parent.code','not in', ['REM','SR'])
            ]
        ]
    )
    trailer_category = fields.Many2One('transport.vehicle_category', 'Trailer Type', ondelete='RESTRICT', required=False,
        domain=['OR',
            ('code', 'in', ['REM','SR']),
            ('parent.code', 'in', ['REM','SR'])
        ]
    )
    mission_orders = fields.One2Many('transport.mission_order', 'mission_vehicle_category', 'Mission orders')
    # Model properties
    number = fields.Integer('Number', required=True)

    # Override get_rec_name to display a proper name for the MissionVehicleCategory
    def get_rec_name(self, name):
        name = self.truck_category.rec_name
        if self.trailer_category:
            name += " - " + self.trailer_category.rec_name
        name += ' (x%d)' % (self.number)
        return name

class MissionOrder(ModelSQL, ModelView):
    'MissionOrder'
    __name__ = 'transport.mission_order'
    _rec_name = 'label'
    # Relations with other models
    mission_vehicle_category = fields.Many2One('transport.mission_vehicle_category', 'Mission Vehicle Category',
        ondelete='SET NULL', required=False)
    driver = fields.Many2One('transport.driver', 'Driver', ondelete='RESTRICT', required=True,
        domain=[('id', 'not in', Eval('unavailable_drivers'))], depends=['unavailable_drivers']
    )
    unavailable_drivers = fields.Function(
        fields.Many2Many('transport.mission_order-transport.driver', 'mission_order', 'driver', 'Unavailable drivers'),
        'getter_unavailable_drivers'
    )
    truck = fields.Many2One('transport.vehicle', 'Truck', ondelete='RESTRICT', required=False,
        domain=['AND',
            ('category.code', 'not in', ['REM','SR']),
            ('category.parent.code', 'not in', ['REM','SR']),
            ('category.code','=', Eval('mission_vehicle_category', {}).get('truck_category', 0)),
            ('id', 'not in', Eval('unavailable_vehicles'))
        ], depends=['unavailable_vehicles', 'mission_vehicle_category']
    )
    trailer = fields.Many2One('transport.vehicle', 'Trailer', ondelete='RESTRICT', required=False,
        domain=['OR',
            ('category.code', 'in', ['REM','SR']),
            ('category.parent.code', 'in', ['REM','SR']),
            ('id', 'not in', Eval('unavailable_vehicles'))
        ], depends=['unavailable_vehicles']
    )
    unavailable_vehicles = fields.Function(
        fields.Many2Many('transport.mission_order-transport.vehicle', 'mission_order', 'vehicle', 'Unavailable vehicles'),
        'getter_unavailable_vehicles'
    )
    # Model properties
    label = fields.Char('Label', required=True)
    comment = fields.Text('Comment', required=False)
    client = fields.Many2One('party.party', 'Client', ondelete='RESTRICT', required=False)
    
    def getter_unavailable_drivers(self, name):
        f = open("/modules/transport/transport.log", "w+")
        f.write("getter_unavailable_drivers : \n")
        if not self.start_date or not self.end_date:
            return None
        missionOrder = Pool().get('transport.mission_order')
        missionOrderTable = missionOrder.__table__()

        cursor = Transaction().connection.cursor()
        where = (
            (missionOrderTable.start_date.__ge__(self.start_date) & missionOrderTable.start_date.__le__(self.end_date))
            | (missionOrderTable.end_date.__ge__(self.start_date) & missionOrderTable.end_date.__le__(self.end_date))
        ) & missionOrderTable.id.__ne__(self.id)
        cursor.execute(*missionOrderTable.select(
            missionOrderTable.driver,
            where=where
        ))
        result = [driver[0] for driver in cursor.fetchall()]
        for driver in cursor.fetchall():
            f.write("Driver ID : %s\n" % driver)
        f.close()
        return result

    def getter_unavailable_vehicles(self, name):
        f = open("/modules/transport/transport.log", "w+")
        f.write("getter_unavailable_vehicles : \n")
        if not self.start_date or not self.end_date:
            return None
        missionOrder = Pool().get('transport.mission_order')
        missionOrderTable = missionOrder.__table__()

        cursor = Transaction().connection.cursor()
        where = (
            (missionOrderTable.start_date.__ge__(self.start_date) & missionOrderTable.start_date.__le__(self.end_date))
            | (missionOrderTable.end_date.__ge__(self.start_date) & missionOrderTable.end_date.__le__(self.end_date))
        ) & missionOrderTable.id.__ne__(self.id)
        cursor.execute(*missionOrderTable.select(
            missionOrderTable.truck, missionOrderTable.trailer,
            where=where
        ))
        result = []
        for truck, trailer in cursor.fetchall():
            result.append(truck)
            result.append(trailer)
            f.write("Truck ID : %s\n" % truck)
            f.write("Trailer ID : %s\n" % trailer)
        f.close()
        return result

class MissionOrderDriverRelation(ModelSQL):
    'MissionOrder - Driver relation'
    __name__ = 'transport.mission_order-transport.driver'

    mission_order = fields.Many2One('transport.mission_order', 'Mission Order', required=True, ondelete='CASCADE')
    driver = fields.Many2One('transport.driver', 'Driver', required=True, ondelete='CASCADE')

class MissionOrderVehicleRelation(ModelSQL):
    'MissionOrder - Vehicle relation'
    __name__ = 'transport.mission_order-transport.vehicle'

    mission_order = fields.Many2One('transport.mission_order', 'Mission Order', required=True, ondelete='CASCADE')
    vehicle = fields.Many2One('transport.vehicle', 'Vehicle', required=True, ondelete='CASCADE')

I have created the form views for all the models. I can create a new mission, and add a MissionVehicleCategory to it. After it is saved, I’m trying to add a MissionOrder to the MissionVehicleCategory. A new window appear with the MissionOrder form, but I got this javascript error from Sao that I mentionned earlier.

The problematic field is truck, in the model MissionOrder :

truck = fields.Many2One('transport.vehicle', 'Truck', ondelete='RESTRICT', required=False,
        domain=['AND',
            ('category.code', 'not in', ['REM','SR']),
            ('category.parent.code', 'not in', ['REM','SR']),
            ('category.code','=', Eval('mission_vehicle_category', {}).get('truck_category', 0)),
            ('id', 'not in', Eval('unavailable_vehicles'))
        ], depends=['unavailable_vehicles', 'mission_vehicle_category']
    )

More specifically, the third clause of the domain does not work. If I comment this line :
('category.code','=', Eval('mission_vehicle_category', {}).get('truck_category', 0)),
I don’t have the error (but the vehicles are not filtered properly when the user choose a vehicle for the field “truck”.
I have also tried to replace this clause with :
('category.code','=', Eval('mission_vehicle_category.truck_category', 0)),
But I get another javascript error :

TypeError: Cannot use 'in' operator to search for 'truck_category' in 11

When debugging, I can see that mission_vehicle_category is null when doing the Eval.

What am I doing wrong ?
Thanks in advance for any help.

Here is a screen capture of the window that appears after I click the add MissionOrder button.
It is the MissionOrder form, where I want the field “truck” to be filtered with only vehicles from the right category.

Hello,

AFAIK, you have to create a dedicated function field, and use this one in your domain.

truck_category = fields.Function(fields.Integer('Truck Category',
        'getter_truck_category')

def getter_truck_category(self, name):
    # Naive version
    return self.mission_vehicle_category.truck_category

Then your domain can be

[('category.code', '=', Eval('truck_category'))]
1 Like

To be precise the client can not evaluate attributes of Many2One field because it has not read them. It can only evaluation what is on the screen.

Thank you both for your help. I was able to (mostly) make the field work with my domain. This is the code I ended up with :
Fields :

    truck_category_code = fields.Function(fields.Char('Truck Category Code'), 'getter_truck_category_code')
    trailer_category_code = fields.Function(fields.Char('Trailer Category Code'), 'getter_trailer_category_code')
    truck = fields.Many2One('transport.vehicle', 'Truck', ondelete='RESTRICT', required=False,
        domain=['AND',
            ('category.code', 'not in', ['REM','SR']),
            ('category.parent.code', 'not in', ['REM','SR']),
            ('category.code',If(Equal(Eval('truck_category_code'), ''), '!=', '='), Eval('truck_category_code', '')),
            ('id', 'not in', Eval('unavailable_vehicles'))
        ], depends=['unavailable_vehicles', 'truck_category_code']
    )
    trailer = fields.Many2One('transport.vehicle', 'Trailer', ondelete='RESTRICT', required=False,
        domain=['AND',
            ('category.code',If(Equal(Eval('trailer_category_code'), ''), '!=', '='), Eval('trailer_category_code', '')),
            ['OR',
                ('category.code', 'in', ['REM','SR']),
                ('category.parent.code', 'in', ['REM','SR'])
            ],
            ('id', 'not in', Eval('unavailable_vehicles'))
        ], depends=['unavailable_vehicles', 'trailer_category_code']
    )

Getter :

    def getter_truck_category_code(self, name):
        if (self.mission_vehicle_category and self.mission_vehicle_category.truck_category):
            return self.mission_vehicle_category.truck_category.code
        else:
            return ''
    def getter_trailer_category_code(self, name):
        if (self.mission_vehicle_category and self.mission_vehicle_category.trailer_category):
            return self.mission_vehicle_category.trailer_category.code
        else:
            return ''

Now when I save the MissionOrder, and display it again in the form, I can select the truck and trailer in a properly fiiltered list. I still have a problem when the MissionOrder is not saved (when I first display the MissionOrder form to create a new MissionOrder).
Tryton automatically fills the field mission_vehicle_category with the right value when I save the MissionOrder. But before that, the domain cannot be applied properly : the domain uses the getter described above, but the field mission_vehicle_category has not been saved and so the getter returns a default value, not the actual code of the truck (or trailer) category. As a result, when the user is first selecting a truck and a trailer, the list is not filtered properly.

Is there a way to dynamically update the domain in Sao so that it properly filters the list before the MissionOrder is saved ?

Also, I am currently not able to display the field mission_vehicle_category in the MissionOrder form. As you can see in my second screenshot in my previous post, there is a blank space above the truck field (to the left of the Driver field). I suspect this is where the field mission_vehicle_category should be displayed, but it does not appear. The field was previously marked as readonly because I thought it would be displayed in a disabled state, but now the field is marked readonly=False, it still does not appear.

mission_vehicle_category = fields.Many2One('transport.mission_vehicle_category', 'Mission Vehicle Category',
        ondelete='SET NULL', required=False, readonly=False)

Declaration of the form view :

        <record model="ir.ui.view" id="mission_order_view_form">
            <field name="model">transport.mission_order</field>
            <field name="type">form</field>
            <field name="name">mission_order_form</field>
        </record>

And below is the actual code of the form view :

<?xml version="1.0"?>
<form>
    <label name="mission_vehicle_category"/>
    <field name="mission_vehicle_category"/>
    <label name="driver"/>
    <field name="driver"/>
    <label name="truck"/>
    <field name="truck"/>
    <label name="trailer"/>
    <field name="trailer"/>
    <label name="label"/>
    <field name="label"/>
    <label name="comment"/>
    <field name="comment"/>
    <label name="start_date"/>
    <field name="start_date"/>
    <label name="end_date"/>
    <field name="end_date"/>
    <label name="client"/>
    <field name="client"/>
    <label name="client_number"/>
    <field name="client_number"/>
    <label name="material"/>
    <field name="material"/>
    <label name="unavailable_drivers"/>
    <field name="unavailable_drivers"/>
</form>

Thanks again for your help.

You must use an on_change_with_<field> with the proper @depends.
The nice think is that you can reuse this method as getter for the Function field.

Thanks for the assist. I will try that and let you know if I manage to make it work.

Do you have any idea why the field mission_vehicle_category is not appearing in the form ?

Because it is the Many2One of the One2Many from which the record is created. So its value is fixed and so the client never display it.

Hi everyone,

After some trials and errors, I was finally able to make everything work as intended, by using on_change_with_<field> method. I also needed an on_change_<field> for my field mission_vehicle_category to properly initialize everything.

Thanks for the help.

Here is the code of the class MissionOrder for any future reader :

class MissionOrder(ModelSQL, ModelView):
    'MissionOrder'
    __name__ = 'transport.mission_order'
    _rec_name = 'label'
    # Relations with other models
    mission_vehicle_category = fields.Many2One('transport.mission_vehicle_category', 'Mission Vehicle Category',
        ondelete='SET NULL', required=False, readonly=False, states = {'invisible':True})
    truck_category_code = fields.Function(
        fields.Char('Truck Category Code', depends=['mission_vehicle_category']),
        'getter_truck_category_code'
    )
    trailer_category_code = fields.Function(
        fields.Char('Trailer Category Code', depends=['mission_vehicle_category']),
        'getter_trailer_category_code'
    )
    driver = fields.Many2One('transport.driver', 'Driver', ondelete='RESTRICT', required=True,
        domain=[('id', 'not in', Eval('unavailable_drivers'))], depends=['unavailable_drivers']
    )
    unavailable_drivers = fields.Function(
        fields.Many2Many('transport.mission_order-transport.driver', 'mission_order', 'driver', 'Unavailable drivers', depends=['mission_vehicle_category']),
        'on_change_with_unavailable_drivers'
    )
    truck = fields.Many2One('transport.vehicle', 'Truck', ondelete='RESTRICT', required=False,
        domain=['AND',
            ('category.code', 'not in', ['REM','SR']),
            ('category.parent.code', 'not in', ['REM','SR']),
            ['OR',
                ('category.code',If(Equal(Eval('truck_category_code'), ''), '!=', '='), Eval('truck_category_code', '')),
                ('category.parent.code',If(Equal(Eval('truck_category_code'), ''), '!=', '='), Eval('truck_category_code', '')),
            ],
            ('id', 'not in', Eval('unavailable_vehicles'))
        ], depends=['unavailable_vehicles', 'truck_category_code']
    )
    trailer = fields.Many2One('transport.vehicle', 'Trailer', ondelete='RESTRICT', required=False,
        domain=['AND',
            ['OR',
                ('category.code',If(Equal(Eval('trailer_category_code'), ''), '!=', '='), Eval('trailer_category_code', '')),
                ('category.parent.code',If(Equal(Eval('trailer_category_code'), ''), '!=', '='), Eval('trailer_category_code', '')),
            ],
            ['OR',
                ('category.code', 'in', ['REM','SR']),
                ('category.parent.code', 'in', ['REM','SR'])
            ],
            ('id', 'not in', Eval('unavailable_vehicles'))
        ], depends=['unavailable_vehicles', 'trailer_category_code']
    )
    unavailable_vehicles = fields.Function(
        fields.Many2Many('transport.mission_order-transport.vehicle', 'mission_order', 'vehicle', 'Unavailable vehicles', depends=['mission_vehicle_category']),
        'on_change_with_unavailable_vehicles'
    )
    # Model properties
    label = fields.Char('Label', required=True)
    comment = fields.Text('Comment', required=False)
    start_date = fields.DateTime('Start Date', required=True, depends=['mission_vehicle_category'])
    end_date = fields.DateTime('End Date', required=True, depends=['mission_vehicle_category'])
    #repetition_type = fields.Selection([('-1', 'No repetition'), ('0', 'Day'), ('1', 'Week'), ('2', 'Month'), ('3', 'Year')], 'Repetition Type')
    #number_of_repetition = fields.Integer('Number of repetition', required=False)
    #repeated_until = fields.DateTime('Repeated until', required=False)
    #repetition_end_type = fields.Selection([('0', 'No repetition'), ('1', 'Fixed Number'), ('2', 'Fixed Date'), ('255', 'Perpetual')], 'End Type')
    client = fields.Many2One('party.party', 'Client', ondelete='RESTRICT', required=False)
    client_number = fields.Char('Client Number', required=False)
    #loading_address # A définir
    #unloading_address # A définir
    material = fields.Char('Material', required=False)

    ########################
    # DEFAULT VALUES
    ########################
    # Default value of the field start_date
    @classmethod
    def default_start_date(self, name=None):
        # transportLog("default_start_date.\n")
        timezone = pytz.timezone("Europe/Paris")
        utc = pytz.timezone("UTC")
        today = date.today()
        return timezone.localize(datetime(today.year, today.month, today.day, 7, 0, 0, 0)).astimezone(utc)
    # Default value of the field end_date
    @classmethod
    def default_end_date(self, name=None):
        # transportLog("default_end_date.\n")
        timezone = pytz.timezone("Europe/Paris")
        utc = pytz.timezone("UTC")
        today = date.today()
        return timezone.localize(datetime(today.year, today.month, today.day, 17, 0, 0, 0)).astimezone(utc)

    ########################
    # GETTER AND ON CHANGE WITH
    ########################
    @fields.depends('mission_vehicle_category')
    def on_change_mission_vehicle_category(self):
        # transportLog("on_change_mission_vehicle_category.\n")
        # Update start date and end date
        if (self.mission_vehicle_category and self.mission_vehicle_category.mission):
            timezone = pytz.timezone("Europe/Paris")
            utc = pytz.timezone("UTC")
            date = self.mission_vehicle_category.mission.date
            start_time = self.mission_vehicle_category.mission.start_time
            end_time = self.mission_vehicle_category.mission.end_time
            start_date = datetime(date.year, date.month, date.day, start_time.hour, start_time.minute, start_time.second, 0)
            end_date = datetime(date.year, date.month, date.day, end_time.hour, end_time.minute, end_time.second, 0)
            self.start_date = timezone.localize(start_date).astimezone(utc)
            self.end_date = timezone.localize(end_date).astimezone(utc)
            # self.start_date = datetime.combine(self.mission_vehicle_category.mission.date, self.mission_vehicle_category.mission.start_time)
            # self.end_date = datetime.combine(self.mission_vehicle_category.mission.date, self.mission_vehicle_category.mission.end_time)
        # Update truck category code
        if (self.mission_vehicle_category and self.mission_vehicle_category.truck_category):
            self.truck_category_code = self.mission_vehicle_category.truck_category.code
        else:
            self.truck_category_code = ''
        # Update trailer category code
        if (self.mission_vehicle_category and self.mission_vehicle_category.trailer_category):
            self.trailer_category_code = self.mission_vehicle_category.trailer_category.code
        else:
            self.trailer_category_code = ''
        self.unavailable_drivers = self.retrieve_unavailable_drivers(self)
        self.unavailable_vehicles = self.retrieve_unavailable_vehicles(self)

    def getter_truck_category_code(self, name=None):
        # transportLog("getter_truck_category_code.\n")
        if (self.mission_vehicle_category and self.mission_vehicle_category.truck_category):
            return self.mission_vehicle_category.truck_category.code
        else:
            return ''

    def getter_trailer_category_code(self, name=None):
        # transportLog("getter_trailer_category_code.\n")
        if (self.mission_vehicle_category and self.mission_vehicle_category.trailer_category):
            return self.mission_vehicle_category.trailer_category.code
        else:
            return ''

    @fields.depends('start_date', 'end_date')
    def on_change_with_unavailable_drivers(self, name=None):
        # transportLog("on_change_with_unavailable_drivers.\n")
        return self.retrieve_unavailable_drivers(self)
    
    def retrieve_unavailable_drivers(self, name=None):
        if not self.start_date or not self.end_date:
            return None
        missionOrder = Pool().get('transport.mission_order')
        missionOrderTable = missionOrder.__table__()

        cursor = Transaction().connection.cursor()
        where = (
            (missionOrderTable.start_date.__ge__(self.start_date) & missionOrderTable.start_date.__le__(self.end_date))
            | (missionOrderTable.end_date.__ge__(self.start_date) & missionOrderTable.end_date.__le__(self.end_date))
        ) & missionOrderTable.id.__ne__(self.id)
        cursor.execute(*missionOrderTable.select(
            missionOrderTable.driver,
            where=where
        ))
        result = [driver[0] for driver in cursor.fetchall()]
        return result

    @fields.depends('start_date', 'end_date')
    def on_change_with_unavailable_vehicles(self, name=None):
        # transportLog("on_change_with_unavailable_vehicles.\n")
        return self.retrieve_unavailable_vehicles(self)
    
    def retrieve_unavailable_vehicles(self, name=None):
        if not self.start_date or not self.end_date:
            return None
        missionOrder = Pool().get('transport.mission_order')
        missionOrderTable = missionOrder.__table__()

        cursor = Transaction().connection.cursor()
        where = (
            (missionOrderTable.start_date.__ge__(self.start_date) & missionOrderTable.start_date.__le__(self.end_date))
            | (missionOrderTable.end_date.__ge__(self.start_date) & missionOrderTable.end_date.__le__(self.end_date))
        ) & missionOrderTable.id.__ne__(self.id)
        cursor.execute(*missionOrderTable.select(
            missionOrderTable.truck, missionOrderTable.trailer,
            where=where
        ))
        result = []
        for truck, trailer in cursor.fetchall():
            result.append(truck)
            result.append(trailer)
        return result