This commit adapts the business code to changes introduced by the parent commit in order to keep the same behaviour as before. All readonly=False fields will have to be checked afterwards to confirm that the business case requires write access to the source field.
378 lines
19 KiB
Python
378 lines
19 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.exceptions import UserError
|
|
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
|
|
class FleetVehicleCost(models.Model):
|
|
_name = 'fleet.vehicle.cost'
|
|
_description = 'Cost related to a vehicle'
|
|
_order = 'date desc, vehicle_id asc'
|
|
|
|
name = fields.Char(related='vehicle_id.name', string='Name', store=True, readonly=False)
|
|
vehicle_id = fields.Many2one('fleet.vehicle', 'Vehicle', required=True, help='Vehicle concerned by this log')
|
|
cost_subtype_id = fields.Many2one('fleet.service.type', 'Type', help='Cost type purchased with this cost')
|
|
amount = fields.Float('Total Price')
|
|
cost_type = fields.Selection([
|
|
('contract', 'Contract'),
|
|
('services', 'Services'),
|
|
('fuel', 'Fuel'),
|
|
('other', 'Other')
|
|
], 'Category of the cost', default="other", help='For internal purpose only', required=True)
|
|
parent_id = fields.Many2one('fleet.vehicle.cost', 'Parent', help='Parent cost to this current cost')
|
|
cost_ids = fields.One2many('fleet.vehicle.cost', 'parent_id', 'Included Services', copy=True)
|
|
odometer_id = fields.Many2one('fleet.vehicle.odometer', 'Odometer', help='Odometer measure of the vehicle at the moment of this log')
|
|
odometer = fields.Float(compute="_get_odometer", inverse='_set_odometer', string='Odometer Value',
|
|
help='Odometer measure of the vehicle at the moment of this log')
|
|
odometer_unit = fields.Selection(related='vehicle_id.odometer_unit', string="Unit", readonly=True)
|
|
date = fields.Date(help='Date when the cost has been executed')
|
|
contract_id = fields.Many2one('fleet.vehicle.log.contract', 'Contract', help='Contract attached to this cost')
|
|
auto_generated = fields.Boolean('Automatically Generated', readonly=True)
|
|
description = fields.Char("Cost Description")
|
|
|
|
def _get_odometer(self):
|
|
for record in self:
|
|
if record.odometer_id:
|
|
record.odometer = record.odometer_id.value
|
|
|
|
def _set_odometer(self):
|
|
for record in self:
|
|
if not record.odometer:
|
|
raise UserError(_('Emptying the odometer value of a vehicle is not allowed.'))
|
|
odometer = self.env['fleet.vehicle.odometer'].create({
|
|
'value': record.odometer,
|
|
'date': record.date or fields.Date.context_today(record),
|
|
'vehicle_id': record.vehicle_id.id
|
|
})
|
|
self.odometer_id = odometer
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
for data in vals_list:
|
|
# make sure that the data are consistent with values of parent and contract records given
|
|
if 'parent_id' in data and data['parent_id']:
|
|
parent = self.browse(data['parent_id'])
|
|
data['vehicle_id'] = parent.vehicle_id.id
|
|
data['date'] = parent.date
|
|
data['cost_type'] = parent.cost_type
|
|
if 'contract_id' in data and data['contract_id']:
|
|
contract = self.env['fleet.vehicle.log.contract'].browse(data['contract_id'])
|
|
data['vehicle_id'] = contract.vehicle_id.id
|
|
data['cost_subtype_id'] = contract.cost_subtype_id.id
|
|
data['cost_type'] = contract.cost_type
|
|
if 'odometer' in data and not data['odometer']:
|
|
# if received value for odometer is 0, then remove it from the
|
|
# data as it would result to the creation of a
|
|
# odometer log with 0, which is to be avoided
|
|
del data['odometer']
|
|
return super(FleetVehicleCost, self).create(vals_list)
|
|
|
|
|
|
class FleetVehicleLogContract(models.Model):
|
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
|
_inherits = {'fleet.vehicle.cost': 'cost_id'}
|
|
_name = 'fleet.vehicle.log.contract'
|
|
_description = 'Contract information on a vehicle'
|
|
_order = 'state desc,expiration_date'
|
|
|
|
def compute_next_year_date(self, strdate):
|
|
oneyear = relativedelta(years=1)
|
|
start_date = fields.Date.from_string(strdate)
|
|
return fields.Date.to_string(start_date + oneyear)
|
|
|
|
@api.model
|
|
def default_get(self, default_fields):
|
|
res = super(FleetVehicleLogContract, self).default_get(default_fields)
|
|
contract = self.env.ref('fleet.type_contract_leasing', raise_if_not_found=False)
|
|
res.update({
|
|
'date': fields.Date.context_today(self),
|
|
'cost_subtype_id': contract and contract.id or False,
|
|
'cost_type': 'contract'
|
|
})
|
|
return res
|
|
|
|
name = fields.Text(compute='_compute_contract_name', store=True)
|
|
active = fields.Boolean(default=True)
|
|
user_id = fields.Many2one('res.users', 'Responsible', default=lambda self: self.env.user, index=True)
|
|
start_date = fields.Date('Contract Start Date', default=fields.Date.context_today,
|
|
help='Date when the coverage of the contract begins')
|
|
expiration_date = fields.Date('Contract Expiration Date', default=lambda self:
|
|
self.compute_next_year_date(fields.Date.context_today(self)),
|
|
help='Date when the coverage of the contract expirates (by default, one year after begin date)')
|
|
days_left = fields.Integer(compute='_compute_days_left', string='Warning Date')
|
|
insurer_id = fields.Many2one('res.partner', 'Vendor')
|
|
purchaser_id = fields.Many2one('res.partner', 'Driver', default=lambda self: self.env.user.partner_id.id,
|
|
help='Person to which the contract is signed for')
|
|
ins_ref = fields.Char('Contract Reference', size=64, copy=False)
|
|
state = fields.Selection([
|
|
('futur', 'Incoming'),
|
|
('open', 'In Progress'),
|
|
('diesoon', 'Expiring Soon'),
|
|
('expired', 'Expired'),
|
|
('closed', 'Closed')
|
|
], 'Status', default='open', readonly=True,
|
|
help='Choose whether the contract is still valid or not',
|
|
track_visibility="onchange",
|
|
copy=False)
|
|
notes = fields.Text('Terms and Conditions', help='Write here all supplementary information relative to this contract', copy=False)
|
|
cost_generated = fields.Float('Recurring Cost Amount', track_visibility="onchange",
|
|
help="Costs paid at regular intervals, depending on the cost frequency. "
|
|
"If the cost frequency is set to unique, the cost will be logged at the start date")
|
|
cost_frequency = fields.Selection([
|
|
('no', 'No'),
|
|
('daily', 'Daily'),
|
|
('weekly', 'Weekly'),
|
|
('monthly', 'Monthly'),
|
|
('yearly', 'Yearly')
|
|
], 'Recurring Cost Frequency', default='no', help='Frequency of the recuring cost', required=True)
|
|
generated_cost_ids = fields.One2many('fleet.vehicle.cost', 'contract_id', 'Generated Costs')
|
|
sum_cost = fields.Float(compute='_compute_sum_cost', string='Indicative Costs Total')
|
|
cost_id = fields.Many2one('fleet.vehicle.cost', 'Cost', required=True, ondelete='cascade')
|
|
# we need to keep this field as a related with store=True because the graph view doesn't support
|
|
# (1) to address fields from inherited table
|
|
# (2) fields that aren't stored in database
|
|
cost_amount = fields.Float(related='cost_id.amount', string='Amount', store=True, readonly=False)
|
|
odometer = fields.Float(string='Odometer at creation',
|
|
help='Odometer measure of the vehicle at the moment of the contract creation')
|
|
|
|
@api.depends('vehicle_id', 'cost_subtype_id', 'date')
|
|
def _compute_contract_name(self):
|
|
for record in self:
|
|
name = record.vehicle_id.name
|
|
if record.cost_subtype_id.name:
|
|
name += ' / ' + record.cost_subtype_id.name
|
|
if record.date:
|
|
name += ' / ' + str(record.date)
|
|
record.name = name
|
|
|
|
@api.depends('expiration_date', 'state')
|
|
def _compute_days_left(self):
|
|
"""return a dict with as value for each contract an integer
|
|
if contract is in an open state and is overdue, return 0
|
|
if contract is in a closed state, return -1
|
|
otherwise return the number of days before the contract expires
|
|
"""
|
|
for record in self:
|
|
if (record.expiration_date and (record.state == 'open' or record.state == 'expired')):
|
|
today = fields.Date.from_string(fields.Date.today())
|
|
renew_date = fields.Date.from_string(record.expiration_date)
|
|
diff_time = (renew_date - today).days
|
|
record.days_left = diff_time > 0 and diff_time or 0
|
|
else:
|
|
record.days_left = -1
|
|
|
|
@api.depends('cost_ids.amount')
|
|
def _compute_sum_cost(self):
|
|
for contract in self:
|
|
contract.sum_cost = sum(contract.cost_ids.mapped('amount'))
|
|
|
|
@api.onchange('vehicle_id')
|
|
def _onchange_vehicle(self):
|
|
if self.vehicle_id:
|
|
self.odometer_unit = self.vehicle_id.odometer_unit
|
|
|
|
@api.multi
|
|
def write(self, vals):
|
|
res = super(FleetVehicleLogContract, self).write(vals)
|
|
if vals.get('expiration_date') or vals.get('user_id'):
|
|
self.activity_reschedule(['fleet.mail_act_fleet_contract_to_renew'], date_deadline=vals.get('expiration_date'), new_user_id=vals.get('user_id'))
|
|
return res
|
|
|
|
@api.multi
|
|
def contract_close(self):
|
|
for record in self:
|
|
record.state = 'closed'
|
|
|
|
@api.multi
|
|
def contract_open(self):
|
|
for record in self:
|
|
record.state = 'open'
|
|
|
|
@api.multi
|
|
def act_renew_contract(self):
|
|
assert len(self.ids) == 1, "This operation should only be done for 1 single contract at a time, as it it suppose to open a window as result"
|
|
for element in self:
|
|
# compute end date
|
|
startdate = fields.Date.from_string(element.start_date)
|
|
enddate = fields.Date.from_string(element.expiration_date)
|
|
diffdate = (enddate - startdate)
|
|
default = {
|
|
'date': fields.Date.context_today(self),
|
|
'start_date': fields.Date.to_string(fields.Date.from_string(element.expiration_date) + relativedelta(days=1)),
|
|
'expiration_date': fields.Date.to_string(enddate + diffdate),
|
|
}
|
|
newid = element.copy(default).id
|
|
return {
|
|
'name': _("Renew Contract"),
|
|
'view_mode': 'form',
|
|
'view_id': self.env.ref('fleet.fleet_vehicle_log_contract_view_form').id,
|
|
'view_type': 'tree,form',
|
|
'res_model': 'fleet.vehicle.log.contract',
|
|
'type': 'ir.actions.act_window',
|
|
'domain': '[]',
|
|
'res_id': newid,
|
|
'context': {'active_id': newid},
|
|
}
|
|
|
|
@api.model
|
|
def scheduler_manage_auto_costs(self):
|
|
# This method is called by a cron task
|
|
# It creates costs for contracts having the "recurring cost" field setted, depending on their frequency
|
|
# For example, if a contract has a reccuring cost of 200 with a weekly frequency, this method creates a cost of 200 on the
|
|
# first day of each week, from the date of the last recurring costs in the database to today
|
|
# If the contract has not yet any recurring costs in the database, the method generates the recurring costs from the start_date to today
|
|
# The created costs are associated to a contract thanks to the many2one field contract_id
|
|
# If the contract has no start_date, no cost will be created, even if the contract has recurring costs
|
|
VehicleCost = self.env['fleet.vehicle.cost']
|
|
deltas = {
|
|
'yearly': relativedelta(years=+1),
|
|
'monthly': relativedelta(months=+1),
|
|
'weekly': relativedelta(weeks=+1),
|
|
'daily': relativedelta(days=+1)
|
|
}
|
|
contracts = self.env['fleet.vehicle.log.contract'].search([('state', '!=', 'closed')], offset=0, limit=None, order=None)
|
|
for contract in contracts:
|
|
if not contract.start_date or contract.cost_frequency == 'no':
|
|
continue
|
|
found = False
|
|
startdate = contract.start_date
|
|
if contract.generated_cost_ids:
|
|
last_autogenerated_cost = VehicleCost.search([
|
|
('contract_id', '=', contract.id),
|
|
('auto_generated', '=', True)
|
|
], offset=0, limit=1, order='date desc')
|
|
if last_autogenerated_cost:
|
|
found = True
|
|
startdate = last_autogenerated_cost.date
|
|
if found:
|
|
startdate += deltas.get(contract.cost_frequency)
|
|
today = fields.Date.context_today(self)
|
|
while (startdate <= today) & (startdate <= contract.expiration_date):
|
|
data = {
|
|
'amount': contract.cost_generated,
|
|
'date': fields.Date.context_today(self),
|
|
'vehicle_id': contract.vehicle_id.id,
|
|
'cost_subtype_id': contract.cost_subtype_id.id,
|
|
'contract_id': contract.id,
|
|
'auto_generated': True
|
|
}
|
|
self.env['fleet.vehicle.cost'].create(data)
|
|
startdate += deltas.get(contract.cost_frequency)
|
|
return True
|
|
|
|
@api.model
|
|
def scheduler_manage_contract_expiration(self):
|
|
# This method is called by a cron task
|
|
# It manages the state of a contract, possibly by posting a message on the vehicle concerned and updating its status
|
|
date_today = fields.Date.from_string(fields.Date.today())
|
|
in_fifteen_days = fields.Date.to_string(date_today + relativedelta(days=+15))
|
|
nearly_expired_contracts = self.search([('state', '=', 'open'), ('expiration_date', '<', in_fifteen_days)])
|
|
|
|
nearly_expired_contracts.write({'state': 'diesoon'})
|
|
for contract in nearly_expired_contracts.filtered(lambda contract: contract.user_id):
|
|
contract.activity_schedule(
|
|
'fleet.mail_act_fleet_contract_to_renew', contract.expiration_date,
|
|
user_id=contract.user_id.id)
|
|
|
|
expired_contracts = self.search([('state', '!=', 'expired'), ('expiration_date', '<',fields.Date.today() )])
|
|
expired_contracts.write({'state': 'expired'})
|
|
|
|
futur_contracts = self.search([('state', 'not in', ['futur', 'closed']), ('start_date', '>', fields.Date.today())])
|
|
futur_contracts.write({'state': 'futur'})
|
|
|
|
now_running_contracts = self.search([('state', '=', 'futur'), ('start_date', '<=', fields.Date.today())])
|
|
now_running_contracts.write({'state': 'open'})
|
|
|
|
def run_scheduler(self):
|
|
self.scheduler_manage_auto_costs()
|
|
self.scheduler_manage_contract_expiration()
|
|
|
|
|
|
class FleetVehicleLogFuel(models.Model):
|
|
_name = 'fleet.vehicle.log.fuel'
|
|
_description = 'Fuel log for vehicles'
|
|
_inherits = {'fleet.vehicle.cost': 'cost_id'}
|
|
|
|
@api.model
|
|
def default_get(self, default_fields):
|
|
res = super(FleetVehicleLogFuel, self).default_get(default_fields)
|
|
service = self.env.ref('fleet.type_service_refueling', raise_if_not_found=False)
|
|
res.update({
|
|
'date': fields.Date.context_today(self),
|
|
'cost_subtype_id': service and service.id or False,
|
|
'cost_type': 'fuel'
|
|
})
|
|
return res
|
|
|
|
liter = fields.Float()
|
|
price_per_liter = fields.Float()
|
|
purchaser_id = fields.Many2one('res.partner', 'Purchaser', domain="['|',('customer','=',True),('employee','=',True)]")
|
|
inv_ref = fields.Char('Invoice Reference', size=64)
|
|
vendor_id = fields.Many2one('res.partner', 'Vendor', domain="[('supplier','=',True)]")
|
|
notes = fields.Text()
|
|
cost_id = fields.Many2one('fleet.vehicle.cost', 'Cost', required=True, ondelete='cascade')
|
|
# we need to keep this field as a related with store=True because the graph view doesn't support
|
|
# (1) to address fields from inherited table
|
|
# (2) fields that aren't stored in database
|
|
cost_amount = fields.Float(related='cost_id.amount', string='Amount', store=True, readonly=False)
|
|
|
|
@api.onchange('vehicle_id')
|
|
def _onchange_vehicle(self):
|
|
if self.vehicle_id:
|
|
self.odometer_unit = self.vehicle_id.odometer_unit
|
|
self.purchaser_id = self.vehicle_id.driver_id.id
|
|
|
|
@api.onchange('liter', 'price_per_liter', 'amount')
|
|
def _onchange_liter_price_amount(self):
|
|
# need to cast in float because the value receveid from web client maybe an integer (Javascript and JSON do not
|
|
# make any difference between 3.0 and 3). This cause a problem if you encode, for example, 2 liters at 1.5 per
|
|
# liter => total is computed as 3.0, then trigger an onchange that recomputes price_per_liter as 3/2=1 (instead
|
|
# of 3.0/2=1.5)
|
|
# If there is no change in the result, we return an empty dict to prevent an infinite loop due to the 3 intertwine
|
|
# onchange. And in order to verify that there is no change in the result, we have to limit the precision of the
|
|
# computation to 2 decimal
|
|
liter = float(self.liter)
|
|
price_per_liter = float(self.price_per_liter)
|
|
amount = float(self.amount)
|
|
if liter > 0 and price_per_liter > 0 and round(liter * price_per_liter, 2) != amount:
|
|
self.amount = round(liter * price_per_liter, 2)
|
|
elif amount > 0 and liter > 0 and round(amount / liter, 2) != price_per_liter:
|
|
self.price_per_liter = round(amount / liter, 2)
|
|
elif amount > 0 and price_per_liter > 0 and round(amount / price_per_liter, 2) != liter:
|
|
self.liter = round(amount / price_per_liter, 2)
|
|
|
|
|
|
class FleetVehicleLogServices(models.Model):
|
|
_name = 'fleet.vehicle.log.services'
|
|
_inherits = {'fleet.vehicle.cost': 'cost_id'}
|
|
_description = 'Services for vehicles'
|
|
|
|
@api.model
|
|
def default_get(self, default_fields):
|
|
res = super(FleetVehicleLogServices, self).default_get(default_fields)
|
|
service = self.env.ref('fleet.type_service_service_8', raise_if_not_found=False)
|
|
res.update({
|
|
'date': fields.Date.context_today(self),
|
|
'cost_subtype_id': service and service.id or False,
|
|
'cost_type': 'services'
|
|
})
|
|
return res
|
|
|
|
purchaser_id = fields.Many2one('res.partner', 'Purchaser', domain="['|',('customer','=',True),('employee','=',True)]")
|
|
inv_ref = fields.Char('Invoice Reference')
|
|
vendor_id = fields.Many2one('res.partner', 'Vendor', domain="[('supplier','=',True)]")
|
|
# we need to keep this field as a related with store=True because the graph view doesn't support
|
|
# (1) to address fields from inherited table and (2) fields that aren't stored in database
|
|
cost_amount = fields.Float(related='cost_id.amount', string='Amount', store=True, readonly=False)
|
|
notes = fields.Text()
|
|
cost_id = fields.Many2one('fleet.vehicle.cost', 'Cost', required=True, ondelete='cascade')
|
|
|
|
@api.onchange('vehicle_id')
|
|
def _onchange_vehicle(self):
|
|
if self.vehicle_id:
|
|
self.odometer_unit = self.vehicle_id.odometer_unit
|
|
self.purchaser_id = self.vehicle_id.driver_id.id
|