[MERGE] hr_payroll: Payroll module refactoring for salary configurator

Thanks to:

- Nicolas Michaux for the design part and his expertise.
- Laura Piraux for all her advices, reviews, ideas and kindness.
- Thibaut Delavallée for the technical advices.
- My mother for her pies.
- Martin Trigaux that did nothing.
This commit is contained in:
Yannick Tivisse
2017-04-26 16:16:44 +02:00
48 changed files with 5671 additions and 4476 deletions
+3 -1
View File
@@ -31,7 +31,9 @@ Main Features
'data': [
'security/fleet_security.xml',
'security/ir.model.access.csv',
'views/fleet_view.xml',
'views/fleet_vehicle_model_views.xml',
'views/fleet_vehicle_views.xml',
'views/fleet_vehicle_cost_views.xml',
'views/fleet_board_view.xml',
'data/fleet_cars_data.xml',
'data/fleet_data.xml',
+6 -1
View File
@@ -1 +1,6 @@
import fleet
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import fleet_vehicle
import fleet_vehicle_cost
import fleet_vehicle_model
-658
View File
@@ -1,658 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, tools, _
from odoo.exceptions import UserError
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)
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')
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)
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
def create(self, data):
#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(data)
class FleetVehicleTag(models.Model):
_name = 'fleet.vehicle.tag'
name = fields.Char(required=True, translate=True)
color = fields.Integer('Color Index', default=10)
_sql_constraints = [('name_uniq', 'unique (name)', "Tag name already exists !")]
class FleetVehicleState(models.Model):
_name = 'fleet.vehicle.state'
_order = 'sequence asc'
name = fields.Char(required=True)
sequence = fields.Integer(help="Used to order the note stages")
_sql_constraints = [('fleet_state_name_unique', 'unique(name)', 'State name already exists')]
class FleetVehicleModel(models.Model):
_name = 'fleet.vehicle.model'
_description = 'Model of a vehicle'
_order = 'name asc'
name = fields.Char('Model name', required=True)
brand_id = fields.Many2one('fleet.vehicle.model.brand', 'Make', required=True, help='Make of the vehicle')
vendors = fields.Many2many('res.partner', 'fleet_vehicle_model_vendors', 'model_id', 'partner_id', string='Vendors')
image = fields.Binary(related='brand_id.image', string="Logo")
image_medium = fields.Binary(related='brand_id.image_medium', string="Logo (medium)")
image_small = fields.Binary(related='brand_id.image_small', string="Logo (small)")
@api.multi
@api.depends('name', 'brand_id')
def name_get(self):
res = []
for record in self:
name = record.name
if record.brand_id.name:
name = record.brand_id.name + '/' + name
res.append((record.id, name))
return res
@api.onchange('brand_id')
def _onchange_brand(self):
if self.brand_id:
self.image_medium = self.brand_id.image
else:
self.image_medium = False
class FleetVehicleModelBrand(models.Model):
_name = 'fleet.vehicle.model.brand'
_description = 'Brand model of the vehicle'
_order = 'name asc'
name = fields.Char('Make', required=True)
image = fields.Binary("Logo", attachment=True,
help="This field holds the image used as logo for the brand, limited to 1024x1024px.")
image_medium = fields.Binary("Medium-sized image", attachment=True,
help="Medium-sized logo of the brand. It is automatically "
"resized as a 128x128px image, with aspect ratio preserved. "
"Use this field in form views or some kanban views.")
image_small = fields.Binary("Small-sized image", attachment=True,
help="Small-sized logo of the brand. It is automatically "
"resized as a 64x64px image, with aspect ratio preserved. "
"Use this field anywhere a small image is required.")
@api.model
def create(self, vals):
tools.image_resize_images(vals)
return super(FleetVehicleModelBrand, self).create(vals)
@api.multi
def write(self, vals):
tools.image_resize_images(vals)
return super(FleetVehicleModelBrand, self).write(vals)
class FleetVehicle(models.Model):
_inherit = 'mail.thread'
_name = 'fleet.vehicle'
_description = 'Information on a vehicle'
_order = 'license_plate asc'
def _get_default_state(self):
state = self.env.ref('fleet.vehicle_state_active', raise_if_not_found=False)
return state and state.id or False
name = fields.Char(compute="_compute_vehicle_name", store=True)
active = fields.Boolean(default=True)
company_id = fields.Many2one('res.company', 'Company')
license_plate = fields.Char(required=True, help='License plate number of the vehicle (i = plate number for a car)')
vin_sn = fields.Char('Chassis Number', help='Unique number written on the vehicle motor (VIN/SN number)', copy=False)
driver_id = fields.Many2one('res.partner', 'Driver', help='Driver of the vehicle')
model_id = fields.Many2one('fleet.vehicle.model', 'Model', required=True, help='Model of the vehicle')
log_fuel = fields.One2many('fleet.vehicle.log.fuel', 'vehicle_id', 'Fuel Logs')
log_services = fields.One2many('fleet.vehicle.log.services', 'vehicle_id', 'Services Logs')
log_contracts = fields.One2many('fleet.vehicle.log.contract', 'vehicle_id', 'Contracts')
cost_count = fields.Integer(compute="_compute_count_all", string="Costs")
contract_count = fields.Integer(compute="_compute_count_all", string='Contracts')
service_count = fields.Integer(compute="_compute_count_all", string='Services')
fuel_logs_count = fields.Integer(compute="_compute_count_all", string='Fuel Logs')
odometer_count = fields.Integer(compute="_compute_count_all", string='Odometer')
acquisition_date = fields.Date('Acquisition Date', required=False, help='Date when the vehicle has been bought')
color = fields.Char(help='Color of the vehicle')
state_id = fields.Many2one('fleet.vehicle.state', 'State', default=_get_default_state, help='Current state of the vehicle', ondelete="set null")
location = fields.Char(help='Location of the vehicle (garage, ...)')
seats = fields.Integer('Seats Number', help='Number of seats of the vehicle')
doors = fields.Integer('Doors Number', help='Number of doors of the vehicle', default=5)
tag_ids = fields.Many2many('fleet.vehicle.tag', 'fleet_vehicle_vehicle_tag_rel', 'vehicle_tag_id', 'tag_id', 'Tags', copy=False)
odometer = fields.Float(compute='_get_odometer', inverse='_set_odometer', string='Last Odometer', help='Odometer measure of the vehicle at the moment of this log')
odometer_unit = fields.Selection([('kilometers', 'Kilometers'), ('miles', 'Miles')],
'Odometer Unit', default='kilometers', help='Unit of the odometer ', required=True)
transmission = fields.Selection([('manual', 'Manual'), ('automatic', 'Automatic')], 'Transmission', help='Transmission Used by the vehicle')
fuel_type = fields.Selection([('gasoline', 'Gasoline'), ('diesel', 'Diesel'), ('electric', 'Electric'), ('hybrid', 'Hybrid')], 'Fuel Type', help='Fuel Used by the vehicle')
horsepower = fields.Integer()
horsepower_tax = fields.Float('Horsepower Taxation')
power = fields.Integer('Power', help='Power in kW of the vehicle')
co2 = fields.Float('CO2 Emissions', help='CO2 emissions of the vehicle')
image = fields.Binary(related='model_id.image', string="Logo")
image_medium = fields.Binary(related='model_id.image_medium', string="Logo (medium)")
image_small = fields.Binary(related='model_id.image_small', string="Logo (small)")
contract_renewal_due_soon = fields.Boolean(compute='_compute_contract_reminder', search='_search_contract_renewal_due_soon', string='Has Contracts to renew', multi='contract_info')
contract_renewal_overdue = fields.Boolean(compute='_compute_contract_reminder', search='_search_get_overdue_contract_reminder', string='Has Contracts Overdue', multi='contract_info')
contract_renewal_name = fields.Text(compute='_compute_contract_reminder', string='Name of contract to renew soon', multi='contract_info')
contract_renewal_total = fields.Text(compute='_compute_contract_reminder', string='Total of contracts due or overdue minus one', multi='contract_info')
car_value = fields.Float(help='Value of the bought vehicle')
@api.depends('model_id', 'license_plate')
def _compute_vehicle_name(self):
for record in self:
record.name = record.model_id.brand_id.name + '/' + record.model_id.name + '/' + record.license_plate
def _get_odometer(self):
FleetVehicalOdometer = self.env['fleet.vehicle.odometer']
for record in self:
vehicle_odometer = FleetVehicalOdometer.search([('vehicle_id', '=', record.id)], limit=1, order='value desc')
if vehicle_odometer:
record.odometer = vehicle_odometer.value
else:
record.odometer = 0
def _set_odometer(self):
for record in self:
if record.odometer:
date = fields.Date.context_today(record)
data = {'value': record.odometer, 'date': date, 'vehicle_id': record.id}
self.env['fleet.vehicle.odometer'].create(data)
def _compute_count_all(self):
Odometer = self.env['fleet.vehicle.odometer']
LogFuel = self.env['fleet.vehicle.log.fuel']
LogService = self.env['fleet.vehicle.log.services']
LogContract = self.env['fleet.vehicle.log.contract']
Cost = self.env['fleet.vehicle.cost']
for record in self:
record.odometer_count = Odometer.search_count([('vehicle_id', '=', record.id)])
record.fuel_logs_count = LogFuel.search_count([('vehicle_id', '=', record.id)])
record.service_count = LogService.search_count([('vehicle_id', '=', record.id)])
record.contract_count = LogContract.search_count([('vehicle_id', '=', record.id)])
record.cost_count = Cost.search_count([('vehicle_id', '=', record.id), ('parent_id', '=', False)])
@api.depends('log_contracts')
def _compute_contract_reminder(self):
for record in self:
overdue = False
due_soon = False
total = 0
name = ''
for element in record.log_contracts:
if element.state in ('open', 'toclose') and element.expiration_date:
current_date_str = fields.Date.context_today(record)
due_time_str = element.expiration_date
current_date = fields.Date.from_string(current_date_str)
due_time = fields.Date.from_string(due_time_str)
diff_time = (due_time - current_date).days
if diff_time < 0:
overdue = True
total += 1
if diff_time < 15 and diff_time >= 0:
due_soon = True
total += 1
if overdue or due_soon:
log_contract = self.env['fleet.vehicle.log.contract'].search([('vehicle_id', '=', record.id), ('state', 'in', ('open', 'toclose'))],
limit=1, order='expiration_date asc')
if log_contract:
#we display only the name of the oldest overdue/due soon contract
name = log_contract.cost_subtype_id.name
record.contract_renewal_overdue = overdue
record.contract_renewal_due_soon = due_soon
record.contract_renewal_total = total - 1 # we remove 1 from the real total for display purposes
record.contract_renewal_name = name
def _search_contract_renewal_due_soon(self, operator, value):
res = []
assert operator in ('=', '!=', '<>') and value in (True, False), 'Operation not supported'
if (operator == '=' and value is True) or (operator in ('<>', '!=') and value is False):
search_operator = 'in'
else:
search_operator = 'not in'
today = fields.Date.context_today(self)
datetime_today = fields.Datetime.from_string(today)
limit_date = fields.Datetime.to_string(datetime_today + relativedelta(days=+15))
self.env.cr.execute("""SELECT cost.vehicle_id,
count(contract.id) AS contract_number
FROM fleet_vehicle_cost cost
LEFT JOIN fleet_vehicle_log_contract contract ON contract.cost_id = cost.id
WHERE contract.expiration_date IS NOT NULL
AND contract.expiration_date > %s
AND contract.expiration_date < %s
AND contract.state IN ('open', 'toclose')
GROUP BY cost.vehicle_id""", (today, limit_date))
res_ids = [x[0] for x in self.env.cr.fetchall()]
res.append(('id', search_operator, res_ids))
return res
def _search_get_overdue_contract_reminder(self, operator, value):
res = []
assert operator in ('=', '!=', '<>') and value in (True, False), 'Operation not supported'
if (operator == '=' and value is True) or (operator in ('<>', '!=') and value is False):
search_operator = 'in'
else:
search_operator = 'not in'
today = fields.Date.context_today(self)
self.env.cr.execute('''SELECT cost.vehicle_id,
count(contract.id) AS contract_number
FROM fleet_vehicle_cost cost
LEFT JOIN fleet_vehicle_log_contract contract ON contract.cost_id = cost.id
WHERE contract.expiration_date IS NOT NULL
AND contract.expiration_date < %s
AND contract.state IN ('open', 'toclose')
GROUP BY cost.vehicle_id ''', (today,))
res_ids = [x[0] for x in self.env.cr.fetchall()]
res.append(('id', search_operator, res_ids))
return res
@api.onchange('model_id')
def _onchange_model(self):
if self.model_id:
self.image_medium = self.model_id.image
else:
self.image_medium = False
@api.model
def create(self, data):
vehicle = super(FleetVehicle, self.with_context(mail_create_nolog=True)).create(data)
vehicle.message_post(body=_('%s %s has been added to the fleet!') % (vehicle.model_id.name, vehicle.license_plate))
return vehicle
@api.multi
def write(self, vals):
"""
This function write an entry in the openchatter whenever we change important information
on the vehicle like the model, the drive, the state of the vehicle or its license plate
"""
for vehicle in self:
changes = []
if 'model_id' in vals and vehicle.model_id.id != vals['model_id']:
value = self.env['fleet.vehicle.model'].browse(vals['model_id']).name
oldmodel = vehicle.model_id.name or _('None')
changes.append(_("Model: from '%s' to '%s'") % (oldmodel, value))
if 'driver_id' in vals and vehicle.driver_id.id != vals['driver_id']:
value = self.env['res.partner'].browse(vals['driver_id']).name
olddriver = (vehicle.driver_id.name) or _('None')
changes.append(_("Driver: from '%s' to '%s'") % (olddriver, value))
if 'state_id' in vals and vehicle.state_id.id != vals['state_id']:
value = self.env['fleet.vehicle.state'].browse(vals['state_id']).name
oldstate = vehicle.state_id.name or _('None')
changes.append(_("State: from '%s' to '%s'") % (oldstate, value))
if 'license_plate' in vals and vehicle.license_plate != vals['license_plate']:
old_license_plate = vehicle.license_plate or _('None')
changes.append(_("License Plate: from '%s' to '%s'") % (old_license_plate, vals['license_plate']))
if len(changes) > 0:
self.message_post(body=", ".join(changes))
return super(FleetVehicle, self).write(vals)
@api.multi
def return_action_to_open(self):
""" This opens the xml view specified in xml_id for the current vehicle """
self.ensure_one()
xml_id = self.env.context.get('xml_id')
if xml_id:
res = self.env['ir.actions.act_window'].for_xml_id('fleet', xml_id)
res.update(
context=dict(self.env.context, default_vehicle_id=self.id, group_by=False),
domain=[('vehicle_id', '=', self.id)]
)
return res
return False
@api.multi
def act_show_log_cost(self):
""" This opens log view to view and add new log for this vehicle, groupby default to only show effective costs
@return: the costs log view
"""
self.ensure_one()
res = self.env['ir.actions.act_window'].for_xml_id('fleet', 'fleet_vehicle_costs_action')
res.update(
context=dict(self.env.context, default_vehicle_id=self.id, search_default_parent_false=True),
domain=[('vehicle_id', '=', self.id)]
)
return res
class FleetVehicleOdometer(models.Model):
_name = 'fleet.vehicle.odometer'
_description = 'Odometer log for a vehicle'
_order = 'date desc'
name = fields.Char(compute='_compute_vehicle_log_name', store=True)
date = fields.Date(default=fields.Date.context_today)
value = fields.Float('Odometer Value', group_operator="max")
vehicle_id = fields.Many2one('fleet.vehicle', 'Vehicle', required=True)
unit = fields.Selection(related='vehicle_id.odometer_unit', string="Unit", readonly=True)
@api.depends('vehicle_id', 'date')
def _compute_vehicle_log_name(self):
for record in self:
name = record.vehicle_id.name
if not name:
name = record.date
elif record.date:
name += ' / ' + record.date
self.name = name
@api.onchange('vehicle_id')
def _onchange_vehicle(self):
if self.vehicle_id:
self.unit = self.vehicle_id.odometer_unit
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')
cost_amount = fields.Float(related='cost_id.amount', string='Amount', store=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
@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)
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
class FleetServiceType(models.Model):
_name = 'fleet.service.type'
_description = 'Type of services available on a vehicle'
name = fields.Char(required=True, translate=True)
category = fields.Selection([('contract', 'Contract'), ('service', 'Service'), ('both', 'Both')], 'Category',
required=True, help='Choose wheter the service refer to contracts, vehicle services or both')
class FleetVehicleLogContract(models.Model):
_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)
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', 'Contractor', 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([('open', 'In Progress'), ('toclose', 'To Close'), ('closed', 'Terminated')],
'Status', default='open', readonly=True, help='Choose wheter the contract is still valid or not',
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', 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')
cost_amount = fields.Float(related='cost_id.amount', string='Amount', store=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
@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 += ' / ' + 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 == 'toclose')):
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 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
last_cost_date = 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
last_cost_date = last_autogenerated_cost.date
startdate = fields.Date.from_string(last_cost_date)
if found:
startdate += deltas.get(contract.cost_frequency)
today = fields.Date.from_string(fields.Date.context_today(self))
while (startdate <= today) & (startdate <= fields.Date.from_string(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.context_today(self))
limit_date = fields.Date.to_string(date_today + relativedelta(days=+15))
contracts = self.search([('state', '=', 'open'), ('expiration_date', '<', limit_date)])
res = {}
for contract in contracts:
if contract.vehicle_id.id in res:
res[contract.vehicle_id.id] += 1
else:
res[contract.vehicle_id.id] = 1
Vehicle = self.env['fleet.vehicle']
for vehicle, value in res.items():
Vehicle.browse(vehicle).message_post(body=_('%s contract(s) need(s) to be renewed and/or closed!') % value)
return contracts.write({'state': 'toclose'})
@api.model
def run_scheduler(self):
self.scheduler_manage_auto_costs()
self.scheduler_manage_contract_expiration()
+312
View File
@@ -0,0 +1,312 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _
class FleetVehicle(models.Model):
_inherit = 'mail.thread'
_name = 'fleet.vehicle'
_description = 'Information on a vehicle'
_order = 'license_plate asc'
def _get_default_state(self):
state = self.env.ref('fleet.vehicle_state_active', raise_if_not_found=False)
return state and state.id or False
name = fields.Char(compute="_compute_vehicle_name", store=True)
active = fields.Boolean(default=True)
company_id = fields.Many2one('res.company', 'Company')
license_plate = fields.Char(required=True, help='License plate number of the vehicle (i = plate number for a car)')
vin_sn = fields.Char('Chassis Number', help='Unique number written on the vehicle motor (VIN/SN number)', copy=False)
driver_id = fields.Many2one('res.partner', 'Driver', help='Driver of the vehicle')
model_id = fields.Many2one('fleet.vehicle.model', 'Model', required=True, help='Model of the vehicle')
log_fuel = fields.One2many('fleet.vehicle.log.fuel', 'vehicle_id', 'Fuel Logs')
log_services = fields.One2many('fleet.vehicle.log.services', 'vehicle_id', 'Services Logs')
log_contracts = fields.One2many('fleet.vehicle.log.contract', 'vehicle_id', 'Contracts')
cost_count = fields.Integer(compute="_compute_count_all", string="Costs")
contract_count = fields.Integer(compute="_compute_count_all", string='Contracts')
service_count = fields.Integer(compute="_compute_count_all", string='Services')
fuel_logs_count = fields.Integer(compute="_compute_count_all", string='Fuel Logs')
odometer_count = fields.Integer(compute="_compute_count_all", string='Odometer')
acquisition_date = fields.Date('Immatriculation Date', required=False, help='Date when the vehicle has been immatriculated')
color = fields.Char(help='Color of the vehicle')
state_id = fields.Many2one('fleet.vehicle.state', 'State', default=_get_default_state, help='Current state of the vehicle', ondelete="set null")
location = fields.Char(help='Location of the vehicle (garage, ...)')
seats = fields.Integer('Seats Number', help='Number of seats of the vehicle')
doors = fields.Integer('Doors Number', help='Number of doors of the vehicle', default=5)
tag_ids = fields.Many2many('fleet.vehicle.tag', 'fleet_vehicle_vehicle_tag_rel', 'vehicle_tag_id', 'tag_id', 'Tags', copy=False)
odometer = fields.Float(compute='_get_odometer', inverse='_set_odometer', string='Last Odometer',
help='Odometer measure of the vehicle at the moment of this log')
odometer_unit = fields.Selection([
('kilometers', 'Kilometers'),
('miles', 'Miles')
], 'Odometer Unit', default='kilometers', help='Unit of the odometer ', required=True)
transmission = fields.Selection([('manual', 'Manual'), ('automatic', 'Automatic')], 'Transmission', help='Transmission Used by the vehicle')
fuel_type = fields.Selection([
('gasoline', 'Gasoline'),
('diesel', 'Diesel'),
('electric', 'Electric'),
('hybrid', 'Hybrid')
], 'Fuel Type', help='Fuel Used by the vehicle')
horsepower = fields.Integer()
horsepower_tax = fields.Float('Horsepower Taxation')
power = fields.Integer('Power', help='Power in kW of the vehicle')
co2 = fields.Float('CO2 Emissions', help='CO2 emissions of the vehicle')
image = fields.Binary(related='model_id.image', string="Logo")
image_medium = fields.Binary(related='model_id.image_medium', string="Logo (medium)")
image_small = fields.Binary(related='model_id.image_small', string="Logo (small)")
contract_renewal_due_soon = fields.Boolean(compute='_compute_contract_reminder', search='_search_contract_renewal_due_soon',
string='Has Contracts to renew', multi='contract_info')
contract_renewal_overdue = fields.Boolean(compute='_compute_contract_reminder', search='_search_get_overdue_contract_reminder',
string='Has Contracts Overdue', multi='contract_info')
contract_renewal_name = fields.Text(compute='_compute_contract_reminder', string='Name of contract to renew soon', multi='contract_info')
contract_renewal_total = fields.Text(compute='_compute_contract_reminder', string='Total of contracts due or overdue minus one',
multi='contract_info')
car_value = fields.Float(string="Catalog Value (VAT incl.)", help='Value of the bought vehicle')
residual_value = fields.Float()
_sql_constraints = [
('driver_id_unique', 'UNIQUE(driver_id)', 'Only one car can be assigned to the same employee!')
]
@api.depends('model_id', 'license_plate')
def _compute_vehicle_name(self):
for record in self:
record.name = record.model_id.brand_id.name + '/' + record.model_id.name + '/' + record.license_plate
def _get_odometer(self):
FleetVehicalOdometer = self.env['fleet.vehicle.odometer']
for record in self:
vehicle_odometer = FleetVehicalOdometer.search([('vehicle_id', '=', record.id)], limit=1, order='value desc')
if vehicle_odometer:
record.odometer = vehicle_odometer.value
else:
record.odometer = 0
def _set_odometer(self):
for record in self:
if record.odometer:
date = fields.Date.context_today(record)
data = {'value': record.odometer, 'date': date, 'vehicle_id': record.id}
self.env['fleet.vehicle.odometer'].create(data)
def _compute_count_all(self):
Odometer = self.env['fleet.vehicle.odometer']
LogFuel = self.env['fleet.vehicle.log.fuel']
LogService = self.env['fleet.vehicle.log.services']
LogContract = self.env['fleet.vehicle.log.contract']
Cost = self.env['fleet.vehicle.cost']
for record in self:
record.odometer_count = Odometer.search_count([('vehicle_id', '=', record.id)])
record.fuel_logs_count = LogFuel.search_count([('vehicle_id', '=', record.id)])
record.service_count = LogService.search_count([('vehicle_id', '=', record.id)])
record.contract_count = LogContract.search_count([('vehicle_id', '=', record.id), ('state', '=', 'open')])
record.cost_count = Cost.search_count([('vehicle_id', '=', record.id), ('parent_id', '=', False)])
@api.depends('log_contracts')
def _compute_contract_reminder(self):
for record in self:
overdue = False
due_soon = False
total = 0
name = ''
for element in record.log_contracts:
if element.state in ('open', 'toclose') and element.expiration_date:
current_date_str = fields.Date.context_today(record)
due_time_str = element.expiration_date
current_date = fields.Date.from_string(current_date_str)
due_time = fields.Date.from_string(due_time_str)
diff_time = (due_time - current_date).days
if diff_time < 0:
overdue = True
total += 1
if diff_time < 15 and diff_time >= 0:
due_soon = True
total += 1
if overdue or due_soon:
log_contract = self.env['fleet.vehicle.log.contract'].search([
('vehicle_id', '=', record.id),
('state', 'in', ('open', 'toclose'))
], limit=1, order='expiration_date asc')
if log_contract:
# we display only the name of the oldest overdue/due soon contract
name = log_contract.cost_subtype_id.name
record.contract_renewal_overdue = overdue
record.contract_renewal_due_soon = due_soon
record.contract_renewal_total = total - 1 # we remove 1 from the real total for display purposes
record.contract_renewal_name = name
def _search_contract_renewal_due_soon(self, operator, value):
res = []
assert operator in ('=', '!=', '<>') and value in (True, False), 'Operation not supported'
if (operator == '=' and value is True) or (operator in ('<>', '!=') and value is False):
search_operator = 'in'
else:
search_operator = 'not in'
today = fields.Date.context_today(self)
datetime_today = fields.Datetime.from_string(today)
limit_date = fields.Datetime.to_string(datetime_today + relativedelta(days=+15))
self.env.cr.execute("""SELECT cost.vehicle_id,
count(contract.id) AS contract_number
FROM fleet_vehicle_cost cost
LEFT JOIN fleet_vehicle_log_contract contract ON contract.cost_id = cost.id
WHERE contract.expiration_date IS NOT NULL
AND contract.expiration_date > %s
AND contract.expiration_date < %s
AND contract.state IN ('open', 'toclose')
GROUP BY cost.vehicle_id""", (today, limit_date))
res_ids = [x[0] for x in self.env.cr.fetchall()]
res.append(('id', search_operator, res_ids))
return res
def _search_get_overdue_contract_reminder(self, operator, value):
res = []
assert operator in ('=', '!=', '<>') and value in (True, False), 'Operation not supported'
if (operator == '=' and value is True) or (operator in ('<>', '!=') and value is False):
search_operator = 'in'
else:
search_operator = 'not in'
today = fields.Date.context_today(self)
self.env.cr.execute('''SELECT cost.vehicle_id,
count(contract.id) AS contract_number
FROM fleet_vehicle_cost cost
LEFT JOIN fleet_vehicle_log_contract contract ON contract.cost_id = cost.id
WHERE contract.expiration_date IS NOT NULL
AND contract.expiration_date < %s
AND contract.state IN ('open', 'toclose')
GROUP BY cost.vehicle_id ''', (today,))
res_ids = [x[0] for x in self.env.cr.fetchall()]
res.append(('id', search_operator, res_ids))
return res
@api.onchange('model_id')
def _onchange_model(self):
if self.model_id:
self.image_medium = self.model_id.image
else:
self.image_medium = False
@api.model
def create(self, data):
vehicle = super(FleetVehicle, self.with_context(mail_create_nolog=True)).create(data)
vehicle.message_post(body=_('%s %s has been added to the fleet!') % (vehicle.model_id.name, vehicle.license_plate))
return vehicle
@api.multi
def write(self, vals):
"""
This function write an entry in the openchatter whenever we change important information
on the vehicle like the model, the drive, the state of the vehicle or its license plate
"""
for vehicle in self:
changes = []
if 'model_id' in vals and vehicle.model_id.id != vals['model_id']:
value = self.env['fleet.vehicle.model'].browse(vals['model_id']).name
oldmodel = vehicle.model_id.name or _('None')
changes.append(_("Model: from '%s' to '%s'") % (oldmodel, value))
if 'driver_id' in vals and vehicle.driver_id.id != vals['driver_id']:
value = self.env['res.partner'].browse(vals['driver_id']).name
olddriver = (vehicle.driver_id.name) or _('None')
changes.append(_("Driver: from '%s' to '%s'") % (olddriver, value))
if 'state_id' in vals and vehicle.state_id.id != vals['state_id']:
value = self.env['fleet.vehicle.state'].browse(vals['state_id']).name
oldstate = vehicle.state_id.name or _('None')
changes.append(_("State: from '%s' to '%s'") % (oldstate, value))
if 'license_plate' in vals and vehicle.license_plate != vals['license_plate']:
old_license_plate = vehicle.license_plate or _('None')
changes.append(_("License Plate: from '%s' to '%s'") % (old_license_plate, vals['license_plate']))
if len(changes) > 0:
self.message_post(body=", ".join(changes))
return super(FleetVehicle, self).write(vals)
@api.multi
def return_action_to_open(self):
""" This opens the xml view specified in xml_id for the current vehicle """
self.ensure_one()
xml_id = self.env.context.get('xml_id')
if xml_id:
res = self.env['ir.actions.act_window'].for_xml_id('fleet', xml_id)
res.update(
context=dict(self.env.context, default_vehicle_id=self.id, group_by=False),
domain=[('vehicle_id', '=', self.id)]
)
return res
return False
@api.multi
def act_show_log_cost(self):
""" This opens log view to view and add new log for this vehicle, groupby default to only show effective costs
@return: the costs log view
"""
self.ensure_one()
res = self.env['ir.actions.act_window'].for_xml_id('fleet', 'fleet_vehicle_costs_action')
res.update(
context=dict(self.env.context, default_vehicle_id=self.id, search_default_parent_false=True),
domain=[('vehicle_id', '=', self.id)]
)
return res
class FleetVehicleOdometer(models.Model):
_name = 'fleet.vehicle.odometer'
_description = 'Odometer log for a vehicle'
_order = 'date desc'
name = fields.Char(compute='_compute_vehicle_log_name', store=True)
date = fields.Date(default=fields.Date.context_today)
value = fields.Float('Odometer Value', group_operator="max")
vehicle_id = fields.Many2one('fleet.vehicle', 'Vehicle', required=True)
unit = fields.Selection(related='vehicle_id.odometer_unit', string="Unit", readonly=True)
driver_id = fields.Many2one(related="vehicle_id.driver_id", string="Driver")
@api.depends('vehicle_id', 'date')
def _compute_vehicle_log_name(self):
for record in self:
name = record.vehicle_id.name
if not name:
name = record.date
elif record.date:
name += ' / ' + record.date
self.name = name
@api.onchange('vehicle_id')
def _onchange_vehicle(self):
if self.vehicle_id:
self.unit = self.vehicle_id.odometer_unit
class FleetVehicleState(models.Model):
_name = 'fleet.vehicle.state'
_order = 'sequence asc'
name = fields.Char(required=True)
sequence = fields.Integer(help="Used to order the note stages")
_sql_constraints = [('fleet_state_name_unique', 'unique(name)', 'State name already exists')]
class FleetVehicleTag(models.Model):
_name = 'fleet.vehicle.tag'
name = fields.Char(required=True, translate=True)
color = fields.Integer('Color Index', default=10)
_sql_constraints = [('name_uniq', 'unique (name)', "Tag name already exists !")]
class FleetServiceType(models.Model):
_name = 'fleet.service.type'
_description = 'Type of services available on a vehicle'
name = fields.Char(required=True, translate=True)
category = fields.Selection([
('contract', 'Contract'),
('service', 'Service'),
('both', 'Both')
], 'Category', required=True, help='Choose wheter the service refer to contracts, vehicle services or both')
+358
View File
@@ -0,0 +1,358 @@
# -*- 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)
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
def create(self, data):
# 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(data)
class FleetVehicleLogContract(models.Model):
_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)
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', 'Contractor', 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([
('open', 'In Progress'),
('toclose', 'To Close'),
('closed', 'Terminated')
], 'Status', default='open', readonly=True, help='Choose wheter the contract is still valid or not',
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',
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)
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 += ' / ' + 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 == 'toclose')):
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 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
last_cost_date = 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
last_cost_date = last_autogenerated_cost.date
startdate = fields.Date.from_string(last_cost_date)
if found:
startdate += deltas.get(contract.cost_frequency)
today = fields.Date.from_string(fields.Date.context_today(self))
while (startdate <= today) & (startdate <= fields.Date.from_string(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.context_today(self))
limit_date = fields.Date.to_string(date_today + relativedelta(days=+15))
contracts = self.search([('state', '=', 'open'), ('expiration_date', '<', limit_date)])
res = {}
for contract in contracts:
if contract.vehicle_id.id in res:
res[contract.vehicle_id.id] += 1
else:
res[contract.vehicle_id.id] = 1
Vehicle = self.env['fleet.vehicle']
for vehicle, value in res.items():
Vehicle.browse(vehicle).message_post(body=_('%s contract(s) need(s) to be renewed and/or closed!') % value)
return contracts.write({'state': 'toclose'})
@api.model
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)
@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)
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
@@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, tools
class FleetVehicleModel(models.Model):
_name = 'fleet.vehicle.model'
_description = 'Model of a vehicle'
_order = 'name asc'
name = fields.Char('Model name', required=True)
brand_id = fields.Many2one('fleet.vehicle.model.brand', 'Make', required=True, help='Make of the vehicle')
vendors = fields.Many2many('res.partner', 'fleet_vehicle_model_vendors', 'model_id', 'partner_id', string='Vendors')
image = fields.Binary(related='brand_id.image', string="Logo")
image_medium = fields.Binary(related='brand_id.image_medium', string="Logo (medium)")
image_small = fields.Binary(related='brand_id.image_small', string="Logo (small)")
@api.multi
@api.depends('name', 'brand_id')
def name_get(self):
res = []
for record in self:
name = record.name
if record.brand_id.name:
name = record.brand_id.name + '/' + name
res.append((record.id, name))
return res
@api.onchange('brand_id')
def _onchange_brand(self):
if self.brand_id:
self.image_medium = self.brand_id.image
else:
self.image_medium = False
class FleetVehicleModelBrand(models.Model):
_name = 'fleet.vehicle.model.brand'
_description = 'Brand model of the vehicle'
_order = 'name asc'
name = fields.Char('Make', required=True)
image = fields.Binary("Logo", attachment=True,
help="This field holds the image used as logo for the brand, limited to 1024x1024px.")
image_medium = fields.Binary("Medium-sized image", attachment=True,
help="Medium-sized logo of the brand. It is automatically "
"resized as a 128x128px image, with aspect ratio preserved. "
"Use this field in form views or some kanban views.")
image_small = fields.Binary("Small-sized image", attachment=True,
help="Small-sized logo of the brand. It is automatically "
"resized as a 64x64px image, with aspect ratio preserved. "
"Use this field anywhere a small image is required.")
@api.model
def create(self, vals):
tools.image_resize_images(vals)
return super(FleetVehicleModelBrand, self).create(vals)
@api.multi
def write(self, vals):
tools.image_resize_images(vals)
return super(FleetVehicleModelBrand, self).write(vals)
@@ -0,0 +1,512 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id='fleet_vehicle_cost_view_tree' model='ir.ui.view'>
<field name="name">fleet.vehicle.cost.tree</field>
<field name="model">fleet.vehicle.cost</field>
<field name="arch" type="xml">
<tree string="Vehicle Costs">
<field name="vehicle_id"/>
<field name="description"/>
<field name="cost_subtype_id"/>
<field name="date"/>
</tree>
</field>
</record>
<record id='fleet_vehicle_cost_view_pivot' model='ir.ui.view'>
<field name="name">fleet.vehicle.cost.pivot</field>
<field name="model">fleet.vehicle.cost</field>
<field name="arch" type="xml">
<pivot string="Vehicles costs">
<field name="vehicle_id" type="row"/>
<field name="cost_type" type="row"/>
<field name="date" interval="year" type="col"/>
<field name="amount" type="measure"/>
</pivot>
</field>
</record>
<record id='fleet_vehicle_cost_view_graph' model='ir.ui.view'>
<field name="name">fleet.vehicle.cost.graph</field>
<field name="model">fleet.vehicle.cost</field>
<field name="arch" type="xml">
<graph string="Vehicles costs">
<field name="vehicle_id"/>
<field name="date" interval="year"/>
<field name="amount" type="measure"/>
</graph>
</field>
</record>
<record id='fleet_vehicle_cost_indicative_view_pivot' model='ir.ui.view'>
<field name="name">fleet.vehicle.cost.pivot</field>
<field name="model">fleet.vehicle.cost</field>
<field name="arch" type="xml">
<pivot string="Vehicles costs" >
<field name="vehicle_id" type="row"/>
<field name="cost_type" type="col"/>
<field name="amount" type="measure"/>
</pivot>
</field>
</record>
<record id='fleet_vehicle_cost_indicative_view_graph' model='ir.ui.view'>
<field name="name">fleet.vehicle.cost.graph</field>
<field name="model">fleet.vehicle.cost</field>
<field name="arch" type="xml">
<graph string="Vehicles costs">
<field name="parent_id" type="row"/>
<field name="cost_type" type="col"/>
<field name="amount" type="measure"/>
</graph>
</field>
</record>
<record id='fleet_vehicle_costs_view_search' model='ir.ui.view'>
<field name="name">fleet.vehicle.cost.search</field>
<field name="model">fleet.vehicle.cost</field>
<field name="arch" type="xml">
<search string="Vehicles costs" >
<field name="vehicle_id" />
<field name="cost_subtype_id"/>
<field name="date"/>
<field name="parent_id"/>
<filter name="parent_false" domain="[('parent_id','=',False)]" string="Effective Costs"/>
<filter name="parent_true" domain="[('parent_id','!=',False)]" string="Indicative Costs"/>
<group expand="1" string="Group By">
<filter name="groupby_cost_type" context="{'group_by': 'cost_type'}" string="Cost Type"/>
<filter name="groupby_cost_subtype_id" context="{'group_by': 'cost_subtype_id'}" string="Cost Subtype"/>
<filter name="groupby_vehicle_id" context="{'group_by': 'vehicle_id'}" string="Vehicle"/>
<filter name="groupby_parent_id" context="{'group_by': 'parent_id'}" string="Parent"/>
<separator/>
<filter name="groupby_date" context="{'group_by': 'date:month'}" string="Month" help="Vehicle Costs by Month"/>
<filter name="groupby_year" context="{'group_by': 'date:year'}" string="Year"/>
</group>
</search>
</field>
</record>
<record id='fleet_vehicle_costs_view_form' model='ir.ui.view'>
<field name="name">fleet.vehicle.cost.form</field>
<field name="model">fleet.vehicle.cost</field>
<field name="arch" type="xml">
<form string="Vehicle costs">
<sheet>
<group col="2" string="Cost Details">
<group>
<field name="vehicle_id" />
<field name="cost_subtype_id"/>
<field name="amount"/>
<field name="description"/>
</group>
<group>
<field name="date"/>
<field name="parent_id" groups="base.group_no_one"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="fleet_vehicle_costs_view_graph" model="ir.ui.view">
<field name="name">fleet.vehicle.cost.graph</field>
<field name="model">fleet.vehicle.cost</field>
<field name="arch" type="xml">
<graph string="Costs Per Month" stacked="True">
<field name="date" type="row"/>
<field name="vehicle_id" type="col"/>
<field name="amount" type="measure"/>
</graph>
</field>
</record>
<record id='fleet_vehicle_costs_action' model='ir.actions.act_window'>
<field name="name">Vehicle Costs</field>
<field name="res_model">fleet.vehicle.cost</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,graph</field>
<field name="context">{"search_default_parent_false" : True, "search_default_groupby_vehicle_id" : True,}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new cost.
</p><p>
Odoo helps you managing the costs for your different
vehicles. Costs are created automatically from services,
contracts (fixed or recurring) and fuel logs.
</p>
</field>
</record>
<menuitem action="fleet_vehicle_costs_action" parent="fleet_vehicles" id="fleet_vehicle_costs_menu" groups="fleet_group_manager"/>
<record id='fleet_vehicle_log_contract_view_form' model='ir.ui.view'>
<field name="name">fleet.vehicle.log_contract.form</field>
<field name="model">fleet.vehicle.log.contract</field>
<field name="arch" type="xml">
<form string="Contract logs">
<header>
<button modifiers="{'invisible': [['state', '=', 'closed']]}" name="contract_close" states="open" type="object" class="oe_highlight" string="Terminate Contract" groups="fleet.fleet_group_manager"/>
<button modifiers="{'invisible': [['state', '=', 'closed']]}" name="contract_close" states="toclose" type="object" class="oe_highlight" string="Terminate Contract" groups="fleet.fleet_group_manager"/>
<button modifiers="{'invisible': [['state', 'not in', ['closed']]]}" name="contract_open" states="closed" type="object" class="oe_highlight" string="Set Contract In Progress" groups="fleet.fleet_group_manager"/>
<button class="oe_highlight" name="act_renew_contract" type="object" string="Renew Contract" help="Create a new contract automatically with all the same informations except for the date that will start at the end of current contract" groups="fleet.fleet_group_manager"/>
<field name="state" widget="statusbar" />
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
</button>
</div>
<group col="2">
<group string="Contract details">
<field name="vehicle_id"/>
<field name="cost_subtype_id" required="1" domain="['|',('category','=','contract'),('category','=','both')]"/>
<field name="amount" string="Activation Cost" help="Cost that is paid only once at the creation of the contract"/>
<label for="cost_generated"/>
<div>
<field name="cost_generated" class="oe_inline" attrs="{'invisible': [('cost_frequency','=','no')]}" />
<field name="cost_frequency" class="oe_inline" />
</div>
</group>
<group string="Odometer details">
<label for="odometer"/>
<div class="o_row">
<field name="odometer"/>
<field name="odometer_unit"/>
</div>
</group>
</group>
<group col="2">
<group>
<field name="date" string="Invoice Date"/>
<field name="start_date"/>
<field name="expiration_date" />
</group>
<group>
<field name="insurer_id" context="{'default_supplier': True}"/>
<field name="purchaser_id" />
<field name="ins_ref" />
</group>
</group>
<notebook>
<page string="Included Services">
<group>
<field name="cost_ids" context="{'vehicle_id': vehicle_id}" nolabel="1">
<tree editable="bottom">
<field name="cost_subtype_id" string="Service" domain="[('category','=','service')]"/>
<field name="description"/>
<field name="amount" sum="Price" string="Indicative Cost" />
</tree>
</field>
</group>
<div class="oe_right">
<group>
<field name="sum_cost" string="Indicative Costs Total"/>
</group>
</div>
</page>
<page string="Generated Recurring Costs">
<group>
<field name="generated_cost_ids" context="{'vehicle_id': vehicle_id}" nolabel="1" sum="amount">
<tree editable="bottom" >
<field name="date" />
<field name="amount" sum="amount"/>
</tree>
</field>
</group>
</page>
</notebook>
<group string="Terms and Conditions">
<field name="notes" nolabel="1" placeholder="Write here all other information relative to this contract" />
</group>
</sheet>
</form>
</field>
</record>
<act_window
id="act_renew_contract"
name="Renew Contract"
res_model="fleet.vehicle.log.contract"
src_model="fleet.vehicle.log.contract"
view_mode="form"
view_type="form"
/>
<record id='fleet_vehicle_log_contract_view_tree' model='ir.ui.view'>
<field name="name">fleet.vehicle.log.contract.tree</field>
<field name="model">fleet.vehicle.log.contract</field>
<field name="arch" type="xml">
<tree string="Contract logs" decoration-warning="days_left>0 and days_left&lt;15" decoration-danger="days_left==0" decoration-muted="state=='closed'">
<field name="active" invisible="1"/>
<field name="start_date" />
<field name="expiration_date" />
<field name="days_left" invisible="1"/>
<field name="vehicle_id" />
<field name="cost_subtype_id"/>
<field name="insurer_id" />
<field name="amount" string="Activation Cost"/>
<field name="cost_generated"/>
<field name="cost_frequency"/>
<field name="state" />
</tree>
</field>
</record>
<record id="fleet_vehicle_log_contract_view_graph" model="ir.ui.view">
<field name="name">fleet.vehicle.log.contract.graph</field>
<field name="model">fleet.vehicle.log.contract</field>
<field name="arch" type="xml">
<graph string="Contract Costs Per Month" type="bar" stacked="True">
<field name="date" type="row"/>
<field name="vehicle_id" type="row"/>
<field name="cost_amount" type="measure"/>
</graph>
</field>
</record>
<record id="fleet_vehicle_log_contract_view_search" model="ir.ui.view">
<field name="name">fleet.vehicle.log.contract.search</field>
<field name="model">fleet.vehicle.log.contract</field>
<field name="arch" type="xml">
<search string="Vehicles Contracts">
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<filter string="In Progress" name="open" domain="[('state', '=', 'open')]"/>
</search>
</field>
</record>
<record id='fleet_vehicle_log_contract_action' model='ir.actions.act_window'>
<field name="name">Vehicles Contracts</field>
<field name="res_model">fleet.vehicle.log.contract</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,graph</field>
<field name="context">{'search_default_open': 1}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new contract.
</p><p>
Manage all your contracts (leasing, insurances, etc.) with
their related services, costs. Odoo will automatically warn
you when some contracts have to be renewed.
</p><p>
Each contract (e.g.: leasing) may include several services
(reparation, insurances, periodic maintenance).
</p>
</field>
</record>
<menuitem action="fleet_vehicle_log_contract_action" parent="fleet_vehicles" id="fleet_vehicle_log_contract_menu" groups="fleet_group_user"/>
<record id='fleet_vehicle_log_fuel_view_form' model='ir.ui.view'>
<field name="name">fleet.vehicle.log.fuel.form</field>
<field name="model">fleet.vehicle.log.fuel</field>
<field name="arch" type="xml">
<form string="Fuel Logs">
<sheet>
<group>
<group string="Vehicle Details">
<field name="vehicle_id"/>
</group>
<group string="Refueling Details">
<field name="liter"/>
<field name="price_per_liter"/>
<field name="amount"/>
</group>
</group>
<group>
<group string="Odometer Details">
<label for="odometer"/>
<div class="o_row">
<field name="odometer"/>
<field name="odometer_unit"/>
</div>
</group>
<group string="Additional Details">
<field name="date" />
<field name="purchaser_id" />
<field name="inv_ref" />
<field name="vendor_id" context="{'default_supplier': True}"/>
</group>
</group>
<field name="notes" placeholder="Write here any other information"/>
</sheet>
</form>
</field>
</record>
<record id='fleet_vehicle_log_fuel_view_tree' model='ir.ui.view'>
<field name="name">fleet.vehicle.log.fuel.tree</field>
<field name="model">fleet.vehicle.log.fuel</field>
<field name="arch" type="xml">
<tree string="Fuel Logs">
<field name="date" />
<field name="vehicle_id" />
<field name="odometer" invisible="1"/>
<field name="odometer_unit" invisible="1"/>
<field name="purchaser_id" />
<field name="inv_ref" invisible="1"/>
<field name="vendor_id" invisible="1"/>
<field name="liter" />
<field name="price_per_liter" invisible="1"/>
<field name="amount" sum="Price"/>
</tree>
</field>
</record>
<record id='fleet_vehicle_log_fuel_view_search' model='ir.ui.view'>
<field name="name">fleet.vehicle.log.fuel.search</field>
<field name="model">fleet.vehicle.log.fuel</field>
<field name="arch" type="xml">
<search string="Vehicles Fuel Logs" >
<field name="vehicle_id" />
<field name="date"/>
<field name="purchaser_id" />
<field name="liter" />
<field name="amount" sum="Price"/>
<group expand="0" string="Group By">
<filter name="groupby_vehicle" context="{'group_by' : 'vehicle_id'}" string="Vehicle"/>
</group>
</search>
</field>
</record>
<record id="fleet_vehicle_log_fuel_view_graph" model="ir.ui.view">
<field name="name">fleet.vehicle.log.fuel.graph</field>
<field name="model">fleet.vehicle.log.fuel</field>
<field name="arch" type="xml">
<graph string="Fuel Costs Per Month">
<field name="vehicle_id" type="row"/>
<field name="cost_amount" type="measure"/>
</graph>
</field>
</record>
<record id='fleet_vehicle_log_fuel_action' model='ir.actions.act_window'>
<field name="name">Vehicles Fuel Logs</field>
<field name="res_model">fleet.vehicle.log.fuel</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,graph</field>
<field name="context">{"search_default_groupby_vehicle" : True}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new fuel log.
</p><p>
Here you can add refuelling entries for all vehicles. You can
also filter logs of a particular vehicle using the search
field.
</p>
</field>
</record>
<menuitem action="fleet_vehicle_log_fuel_action" parent="fleet_vehicles" id="fleet_vehicle_log_fuel_menu" groups="fleet_group_user"/>
<record id='fleet_vehicle_log_services_view_form' model='ir.ui.view'>
<field name="name">fleet.vehicle.log.services.form</field>
<field name="model">fleet.vehicle.log.services</field>
<field name="arch" type="xml">
<form string="Services Logs">
<sheet>
<group col="2">
<group string="Services Details">
<field name="vehicle_id"/>
<field name="cost_subtype_id" string="Service Type" domain="['|',('category','=','service'),('category','=','both')]" required="1"/>
<field name="amount"/>
</group>
<group string="Odometer Details">
<label for="odometer"/>
<div class="o_row">
<field name="odometer"/>
<field name="odometer_unit"/>
</div>
</group>
</group>
<group col="2">
<group string="Additional Details">
<field name="date" />
<field name="purchaser_id" />
<field name="vendor_id" context="{'default_supplier': True}"/>
<field name="inv_ref" />
</group>
</group>
<group string="Included Services">
<field name="cost_ids" nolabel="1">
<tree string="Included Services" editable="bottom">
<field name="cost_subtype_id" string="Service" domain="[('category','=','service')]"/>
<field name="amount" sum="Price" string="Indicative Cost"/>
</tree>
</field>
</group>
<group string="Notes">
<field nolabel="1" name="notes" placeholder="Write here any other information related to the service completed."/>
</group>
</sheet>
</form>
</field>
</record>
<record id='fleet_vehicle_log_services_view_tree' model='ir.ui.view'>
<field name="name">fleet.vehicle.log.services.tree</field>
<field name="model">fleet.vehicle.log.services</field>
<field name="arch" type="xml">
<tree string="Services Logs">
<field name="date" />
<field name="vehicle_id" />
<field name="cost_subtype_id"/>
<field name="purchaser_id"/>
<field name="vendor_id" />
<field name="inv_ref" />
<field name="notes" />
<field name="amount" sum="Total"/>
</tree>
</field>
</record>
<record id="fleet_vehicle_log_services_view_graph" model="ir.ui.view">
<field name="name">fleet.vehicle.log.services.graph</field>
<field name="model">fleet.vehicle.log.services</field>
<field name="arch" type="xml">
<graph string="Services Costs Per Month" stacked="True">
<field name="date" type="row"/>
<field name="vehicle_id" type="row"/>
<field name="cost_amount" type="measure"/>
</graph>
</field>
</record>
<record id='fleet_vehicle_log_services_view_search' model='ir.ui.view'>
<field name="name">fleet.vehicle.log.services.search</field>
<field name="model">fleet.vehicle.log.services</field>
<field name="arch" type="xml">
<search string="Services Logs" >
<field name="vehicle_id"/>
<field name="cost_subtype_id"/>
</search>
</field>
</record>
<record id='fleet_vehicle_log_services_action' model='ir.actions.act_window'>
<field name="name">Vehicles Services Logs</field>
<field name="res_model">fleet.vehicle.log.services</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,graph</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new service entry.
</p><p>
Odoo helps you keeping track of all the services done
on your vehicle. Services can be of many type: occasional
repair, fixed maintenance, etc.
</p>
</field>
</record>
<menuitem action="fleet_vehicle_log_services_action" parent="fleet_vehicles" id="fleet_vehicle_log_services_menu" groups="fleet_group_user"/>
</odoo>
@@ -0,0 +1,158 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id='fleet_vehicle_model_view_form' model='ir.ui.view'>
<field name="name">fleet.vehicle.model.form</field>
<field name="model">fleet.vehicle.model</field>
<field name="arch" type="xml">
<form string="Model">
<sheet>
<field name="image_medium" widget='image' class="oe_avatar"/>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name" placeholder="e.g. Model S"/>
</h1>
<label for="brand_id" class="oe_edit_only"/>
<h2>
<field name="brand_id" placeholder="e.g. Tesla"/>
</h2>
</div>
<notebook>
<page string="Vendors" name="vendors">
<field name="vendors" widget="many2many_kanban">
<kanban quick_create="false" create="true">
<field name="name"/>
<templates>
<t t-name="kanban-box">
<div style="position: relative">
<a t-if="! read_only_mode" type="delete" style="position: absolute; right: 0; padding: 4px; diplay: inline-block">X</a>
<div class="oe_module_vignette">
<div class="oe_module_desc">
<field name="name"/>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id='fleet_vehicle_model_view_tree' model='ir.ui.view'>
<field name="name">fleet.vehicle.model.tree</field>
<field name="model">fleet.vehicle.model</field>
<field name="arch" type="xml">
<tree string="Models">
<field name="brand_id" />
<field name="name" />
</tree>
</field>
</record>
<record id='fleet_vehicle_model_view_search' model='ir.ui.view'>
<field name="name">fleet.vehicle.model.search</field>
<field name="model">fleet.vehicle.model</field>
<field name="arch" type="xml">
<search string="Vehicles costs" >
<field name="brand_id" />
<group expand="1" string="Group By">
<filter name="groupby_brand" context="{'group_by' : 'brand_id'}" string="Make"/>
</group>
</search>
</field>
</record>
<record id='fleet_vehicle_model_action' model='ir.actions.act_window'>
<field name="name">Vehicle Model</field>
<field name="res_model">fleet.vehicle.model</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="context">{"search_default_groupby_brand" : True,}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new model.
</p><p>
You can define several models (e.g. A3, A4) for each make (Audi).
</p>
</field>
</record>
<menuitem name="Fleet" id="menu_root" sequence="115" groups="fleet_group_user" web_icon="fleet,static/description/icon.png"/>
<menuitem name="Configuration" parent="menu_root" id="fleet_configuration" sequence="100" groups="fleet_group_manager"/>
<menuitem action="fleet_vehicle_model_action" parent="fleet_configuration" id="fleet_vehicle_model_menu"/>
<record id='fleet_vehicle_model_brand_view_tree' model='ir.ui.view'>
<field name="name">fleet.vehicle.model.brand.tree</field>
<field name="model">fleet.vehicle.model.brand</field>
<field name="arch" type="xml">
<tree string="Model Make">
<field name="name" />
</tree>
</field>
</record>
<record id='fleet_vehicle_model_brand_view_form' model='ir.ui.view'>
<field name="name">fleet.vehicle.model.brand.form</field>
<field name="model">fleet.vehicle.model.brand</field>
<field name="arch" type="xml">
<form string="Model Make">
<group>
<div>
<field name="image_medium" widget="image" class="oe_avatar"/>
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name"/>
</h1>
</div>
</group>
</form>
</field>
</record>
<record id='fleet_vehicle_model_brand_view_kanban' model='ir.ui.view'>
<field name="name">fleet.vehicle.model.brandkanban</field>
<field name="model">fleet.vehicle.model.brand</field>
<field name="arch" type="xml">
<kanban>
<field name="id"/>
<field name="name" />
<field name="image" />
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_vignette oe_semantic_html_override">
<a type="open" href="#" class="oe_kanban_action">
<img t-att-src="kanban_image('fleet.vehicle.model.brand', 'image_small', record.id.value)" class="img-responsive" style="width:100px;"/>
</a>
<div class="text-center">
<h4 class="oe_partner_heading">
<a type="open">
<field name="name"/>
</a>
</h4>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id='fleet_vehicle_model_brand_action' model='ir.actions.act_window'>
<field name="name">Model make of Vehicle</field>
<field name="res_model">fleet.vehicle.model.brand</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new make.
</p>
</field>
</record>
<menuitem action="fleet_vehicle_model_brand_action" parent="fleet_configuration" id="fleet_vehicle_model_brand_menu"/>
</odoo>
+400
View File
@@ -0,0 +1,400 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id='fleet_vehicle_view_form' model='ir.ui.view'>
<field name="name">fleet.vehicle.form</field>
<field name="model">fleet.vehicle</field>
<field name="arch" type="xml">
<form string="Vehicle">
<header>
<field name="state_id" widget="statusbar" clickable="True" />
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="return_action_to_open"
type="object"
class="oe_stat_button"
icon="fa-book"
context="{'xml_id':'fleet_vehicle_log_contract_action', 'search_default_open': 1}"
help="show the contract for this vehicle">
<field name="contract_count" widget="statinfo" string="Contracts"/>
</button>
<button name="act_show_log_cost"
type="object"
class="oe_stat_button"
icon="fa-usd"
help="show all the costs for this vehicle"
groups="fleet.fleet_group_manager">
<field name="cost_count" widget="statinfo" string="Costs"/>
</button>
<button name="return_action_to_open"
type="object"
class="oe_stat_button"
icon="fa-wrench"
context="{'xml_id':'fleet_vehicle_log_services_action'}"
help="show the services logs for this vehicle" >
<field name="service_count" widget="statinfo" string="Services"/>
</button>
<button name="return_action_to_open"
type="object"
class="oe_stat_button"
icon="fa-flask"
context="{'xml_id':'fleet_vehicle_log_fuel_action'}"
help="show the fuel logs for this vehicle" >
<field name="fuel_logs_count" widget="statinfo" string="Fuel"/>
</button>
<button name="return_action_to_open"
type="object"
class="oe_stat_button"
icon="fa-tachometer"
context="{'xml_id':'fleet_vehicle_odometer_action'}"
help="show the odometer logs for this vehicle" >
<field name="odometer_count" widget="statinfo" string="Odometer"/>
</button>
<button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
</button>
</div>
<field name="image_medium" widget='image' class="oe_avatar"/>
<div class="oe_title">
<label for="model_id" class="oe_edit_only"/>
<h1>
<field name="model_id" placeholder="e.g. Model S"/>
</h1>
<label for="license_plate" class="oe_edit_only"/>
<h2>
<field name="license_plate" class="oe_inline" placeholder="e.g. PAE 326"/>
</h2>
<label for="tag_ids" class="oe_edit_only"/>
<field name="tag_ids" widget="many2many_tags" options="{'no_create_edit': True}"/>
</div>
<group col="2" string="General Properties">
<group >
<field name="driver_id" />
<field name="location" />
<field name="vin_sn" />
<field name="company_id" groups="base.group_multi_company"/>
</group>
<group >
<label for="odometer"/>
<div class="o_row">
<field name="odometer"/>
<field name="odometer_unit"/>
</div>
<field name="acquisition_date" />
<field name="car_value" />
<field name="residual_value"/>
</group>
</group>
<group col="2">
<group string="Additional Properties">
<field name="seats" />
<field name="doors" />
<field name="color" />
</group>
<group string="Engine Options">
<field name="transmission" />
<field name="fuel_type" />
<label for="co2"/>
<div class="o_row" name="co2">
<field name="co2"/><span>g/km</span>
</div>
<field name="horsepower" />
<field name="horsepower_tax" />
<label for="power"/>
<div class="o_row">
<field name="power"/><span>kW</span>
</div>
</group>
</group>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread" options='{"thread_level": 1}'/>
</div>
</form>
</field>
</record>
<record id='fleet_vehicle_view_tree' model='ir.ui.view'>
<field name="name">fleet.vehicle.tree</field>
<field name="model">fleet.vehicle</field>
<field name="arch" type="xml">
<tree string="Vehicle" decoration-warning="contract_renewal_due_soon and not contract_renewal_overdue" decoration-danger="contract_renewal_overdue">
<field name="active" invisible="1"/>
<field name="license_plate" />
<field name="model_id" />
<field name="driver_id" />
<field name="vin_sn" />
<field name="acquisition_date" />
<field name="state_id"/>
<field name="odometer"/>
<field name="odometer_unit" />
<field name="contract_renewal_due_soon" invisible="1"/>
<field name="contract_renewal_overdue" invisible="1" />
<field name="contract_renewal_total" invisible="1"/>
</tree>
</field>
</record>
<record id="fleet_vehicle_view_search" model="ir.ui.view">
<field name="name">fleet.vehicle.search</field>
<field name="model">fleet.vehicle</field>
<field name="arch" type="xml">
<search string="All vehicles">
<field name="name" filter_domain="['|', ('name','ilike',self), ('license_plate','ilike',self)]" string="Vehicle"/>
<field name="driver_id"/>
<field name="tag_ids"/>
<field name="location"/>
<field name="state_id" />
<filter name="alert_true" domain="['|',('contract_renewal_due_soon','=',True),('contract_renewal_overdue','=',True)]" string="Has Alert(s)"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<group expand="1" string="Group By">
<filter name="groupby_status" context="{'group_by' : 'state_id'}" string="Status"/>
<filter name="groupby_model" context="{'group_by' : 'model_id'}" string="Model"/>
</group>
</search>
</field>
</record>
<record id='fleet_vehicle_view_kanban' model='ir.ui.view'>
<field name="name">fleet.vehicle.kanban</field>
<field name="model">fleet.vehicle</field>
<field name="arch" type="xml">
<kanban>
<field name="license_plate" />
<field name="model_id" />
<field name="driver_id" />
<field name="location" />
<field name="state_id" />
<field name="id" />
<field name="image" />
<field name="tag_ids" />
<field name="contract_renewal_due_soon" />
<field name="contract_renewal_overdue" />
<field name="contract_renewal_name" />
<field name="contract_renewal_total" />
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<div class="o_kanban_tags_section">
<field name="tag_ids"/>
</div>
<div class="o_kanban_image">
<img t-att-src="kanban_image('fleet.vehicle', 'image_small', record.id.value)"/>
</div>
<div class="oe_kanban_details">
<strong>
<field name="license_plate"/> : <field name="model_id"/>
</strong>
<ul>
<li>
<t t-if="record.driver_id.raw_value"><field name="driver_id"/></t>
</li>
<li>
<t t-if="record.location.raw_value"><field name="location"/></t>
</li>
</ul>
<div class="o_kanban_button">
<a t-if="record.contract_renewal_due_soon.raw_value and !record.contract_renewal_overdue.raw_value" data-type="object" data-name="return_action_to_open" href="#" class="oe_kanban_action btn btn-sm btn-warning" data-context='{"xml_id":"fleet_vehicle_log_contract_action"}'>
<field name="contract_renewal_name"/> <t t-if="record.contract_renewal_total.raw_value > 0"> and <field name="contract_renewal_total" /> other(s)</t>
</a>
<a t-if="record.contract_renewal_overdue.raw_value" data-type="object" data-name="return_action_to_open" href="#" class="oe_kanban_action btn btn-sm btn-danger" data-context='{"xml_id":"fleet_vehicle_log_contract_action"}'>
<field name="contract_renewal_name"/> <t t-if="record.contract_renewal_total.raw_value > 0"> and <field name="contract_renewal_total" /> other(s)</t>
</a>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id='fleet_vehicle_action' model='ir.actions.act_window'>
<field name="name">Vehicles</field>
<field name="res_model">fleet.vehicle</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new vehicle.
</p><p>
You will be able to manage your fleet by keeping track of the
contracts, services, fixed and recurring costs, odometers and
fuel logs associated to each vehicle.
</p><p>
Odoo will warn you when services or contract have to be
renewed.
</p>
</field>
</record>
<menuitem name="Vehicles" parent="menu_root" id="fleet_vehicles" sequence="2" groups="fleet_group_user"/>
<menuitem action="fleet_vehicle_action" parent="fleet_vehicles" id="fleet_vehicle_menu" groups="fleet_group_user"/>
<record id='fleet_vehicle_odometer_view_form' model='ir.ui.view'>
<field name="name">fleet.vehicle.odometer.form</field>
<field name="model">fleet.vehicle.odometer</field>
<field name="arch" type="xml">
<form string="Odometer Logs">
<sheet>
<group>
<field name="vehicle_id"/>
<div>
<field name="value" class="oe_inline"/>
<field name="unit" class="oe_inline"/>
</div>
<field name="date" />
</group>
</sheet>
</form>
</field>
</record>
<record id='fleet_vehicle_odometer_view_tree' model='ir.ui.view'>
<field name="name">fleet.vehicle.odometer.tree</field>
<field name="model">fleet.vehicle.odometer</field>
<field name="arch" type="xml">
<tree string="Odometer Logs" editable="top">
<field name="date" />
<field name="vehicle_id"/>
<field name="driver_id"/>
<field name="value" />
<field name="unit" />
</tree>
</field>
</record>
<record id='fleet_vehicle_odometer_view_search' model='ir.ui.view'>
<field name="name">fleet.vehicle.odometer.search</field>
<field name="model">fleet.vehicle.odometer</field>
<field name="arch" type="xml">
<search string="Vehicles odometers" >
<field name="vehicle_id" />
<field name="value"/>
<field name="unit"/>
<field name="date"/>
<group expand="0" string="Group By">
<filter name="groupby_vehicle" context="{'group_by' : 'vehicle_id'}" string="Vehicle"/>
</group>
</search>
</field>
</record>
<record id="fleet_vehicle_odometer_view_graph" model="ir.ui.view">
<field name="name">fleet.vehicle.odometer.graph</field>
<field name="model">fleet.vehicle.odometer</field>
<field name="arch" type="xml">
<graph string="Odometer Values Per Vehicle">
<field name="vehicle_id" type="row"/>
<field name="value" type="measure"/>
</graph>
</field>
</record>
<record id='fleet_vehicle_odometer_action' model='ir.actions.act_window'>
<field name="name">Vehicles Odometer</field>
<field name="res_model">fleet.vehicle.odometer</field>
<field name="view_type">form</field>
<field name="view_mode">tree,graph</field>
<field name="context">{"search_default_groupby_vehicle" : True}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new odometer log.
</p>
<p>
Here you can add various odometer entries for all vehicles.
You can also show odometer value for a particular vehicle using
the search field.
</p>
</field>
</record>
<menuitem action="fleet_vehicle_odometer_action" parent="fleet_vehicles" id="fleet_vehicle_odometer_menu" groups="fleet_group_user"/>
<record id='fleet_vehicle_service_types_view_tree' model='ir.ui.view'>
<field name="name">fleet.service.type.tree</field>
<field name="model">fleet.service.type</field>
<field name="arch" type="xml">
<tree string="Service types" editable="top">
<field name="name" />
<field name="category"/>
</tree>
</field>
</record>
<record id='fleet_vehicle_service_types_action' model='ir.actions.act_window'>
<field name="name">Service Types</field>
<field name="res_model">fleet.service.type</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new type of service.
</p><p>
Each service can used in contracts, as a standalone service or both.
</p>
</field>
</record>
<menuitem action="fleet_vehicle_service_types_action" parent="fleet_configuration" id="fleet_vehicle_service_types_menu"/>
<record id='fleet_vehicle_state_view_tree' model='ir.ui.view'>
<field name="name">fleet.vehicle.state.tree</field>
<field name="model">fleet.vehicle.state</field>
<field name="arch" type="xml">
<tree string="State" editable="bottom">
<field name="sequence" widget="handler"/>
<field name="name" />
</tree>
</field>
</record>
<record id='fleet_vehicle_state_action' model='ir.actions.act_window'>
<field name="name">Vehicle Status</field>
<field name="res_model">fleet.vehicle.state</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a vehicle status.
</p><p>
You can customize available status to track the evolution of
each vehicle. Example: Active, Being Repaired, Sold.
</p>
</field>
</record>
<menuitem action="fleet_vehicle_state_action" parent="fleet_configuration" id="fleet_vehicle_state_menu" groups="fleet_group_manager"/>
<record id="fleet_vehicle_tag_view_view_form" model="ir.ui.view">
<field name="name">fleet.vehicle.tag.form</field>
<field name="model">fleet.vehicle.tag</field>
<field name="arch" type="xml">
<form string="Vehicle Tags">
<group>
<field name="name"/>
</group>
</form>
</field>
</record>
<record id="fleet_vehicle_tag_action" model="ir.actions.act_window">
<field name="name">Vehicle Tags</field>
<field name="res_model">fleet.vehicle.tag</field>
<field name="view_type">form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to add a new tag.
</p>
</field>
</record>
<menuitem id="fleet_vehicle_tag_menu" parent="fleet_configuration" action="fleet_vehicle_tag_action" sequence="112"/>
</odoo>
File diff suppressed because it is too large Load Diff
+7 -3
View File
@@ -118,13 +118,13 @@ class Employee(models.Model):
('male', 'Male'),
('female', 'Female'),
('other', 'Other')
], groups="hr.group_hr_user")
], groups="hr.group_hr_user", default="male")
marital = fields.Selection([
('single', 'Single'),
('married', 'Married'),
('married', 'Married (or similar)'),
('widower', 'Widower'),
('divorced', 'Divorced')
], string='Marital Status', groups="hr.group_hr_user")
], string='Marital Status', groups="hr.group_hr_user", default='single')
birthday = fields.Date('Date of Birth', groups="hr.group_hr_user")
ssnid = fields.Char('SSN No', help='Social Security Number', groups="hr.group_hr_user")
sinid = fields.Char('SIN No', help='Social Insurance Number', groups="hr.group_hr_user")
@@ -135,6 +135,10 @@ class Employee(models.Model):
domain="[('partner_id', '=', address_home_id)]",
groups="hr.group_hr_user",
help='Employee bank salary account')
permit_no = fields.Char('Work Permit No')
visa_no = fields.Char('Visa No')
visa_expire = fields.Date('Visa Expire Date')
# image: all image fields are base64 encoded and PIL-supported
image = fields.Binary(
"Photo", default=_default_image, attachment=True,
+5
View File
@@ -95,6 +95,11 @@
<group string="Birth">
<field name="birthday"/>
</group>
<group string="Work Permit" name="work_permit">
<field name="visa_no"/>
<field name="permit_no"/>
<field name="visa_expire"/>
</group>
</group>
</page>
<page name="hr_settings" string="HR Settings" groups="hr.group_hr_user">
+9 -7
View File
@@ -54,29 +54,31 @@ class Contract(models.Model):
_inherit = ['mail.thread']
name = fields.Char('Contract Reference', required=True)
employee_id = fields.Many2one('hr.employee', string='Employee', required=True)
employee_id = fields.Many2one('hr.employee', string='Employee')
department_id = fields.Many2one('hr.department', string="Department")
type_id = fields.Many2one('hr.contract.type', string="Contract Type", required=True, default=lambda self: self.env['hr.contract.type'].search([], limit=1))
job_id = fields.Many2one('hr.job', string='Job Title')
date_start = fields.Date('Start Date', required=True, default=fields.Date.today)
date_end = fields.Date('End Date')
trial_date_start = fields.Date('Trial Start Date')
trial_date_end = fields.Date('Trial End Date')
trial_date_end = fields.Date('End of Trial Period')
resource_calendar_id = fields.Many2one(
'resource.calendar', 'Working Schedule',
default=lambda self: self.env['res.company']._company_default_get().resource_calendar_id.id)
wage = fields.Float('Wage', digits=(16, 2), required=True, help="Basic Salary of the employee")
wage = fields.Monetary('Wage', digits=(16, 2), required=True, help="The monthly gross wage of the employee.")
advantages = fields.Text('Advantages')
notes = fields.Text('Notes')
permit_no = fields.Char('Work Permit No')
visa_no = fields.Char('Visa No')
visa_expire = fields.Date('Visa Expire Date')
state = fields.Selection([
('draft', 'New'),
('open', 'Running'),
('pending', 'To Renew'),
('close', 'Expired'),
('cancel', 'Cancelled')
], string='Status', track_visibility='onchange', help='Status of the contract', default='draft')
company_id = fields.Many2one('res.company', default=lambda self: self.env.user.company_id)
currency_id = fields.Many2one(string="Currency", related='company_id.currency_id')
permit_no = fields.Char('Work Permit No', related="employee_id.permit_no")
visa_no = fields.Char('Visa No', related="employee_id.visa_no")
visa_expire = fields.Date('Visa Expire Date', related="employee_id.visa_expire")
@api.onchange('employee_id')
def _onchange_employee_id(self):
+24 -24
View File
@@ -82,7 +82,7 @@
<field name="state" widget="statusbar" clickable="1"/>
</header>
<sheet>
<div class="oe_title">
<div class="oe_title" name="title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name" placeholder="Contract Reference"/>
@@ -91,41 +91,41 @@
<group>
<group>
<field name="employee_id"/>
<field name="department_id"/>
<field name="job_id"/>
</group>
<group>
<field name="department_id"/>
<field name="type_id"/>
</group>
</group>
<notebook>
<page string="Information" name="information">
<group>
<group name="salary_and_advantages" string="Salary and Advantages">
<field name="wage"/>
<field name="advantages" nolabel="1" placeholder="Advantages..." colspan="2"/>
<page string="Salary Information" name="information">
<group name="main_info">
<group name="salary_and_advantages" string="Monthly Advantages in Cash">
<label for="wage"/>
<div class="o_row" name="wage">
<field name="wage" nolabel="1"/>
<span>/ month</span>
</div>
<field name="advantages" nolabel="1" placeholder="Advantages..." colspan="2" invisible="1"/>
</group>
<group name="duration_group" string="Duration">
<label for="trial_date_start" string="Trial Period Duration"/>
<div>
<field name="trial_date_start" class="oe_inline"/>
<span attrs="{'invisible': ['|', ('trial_date_start', '=', False), ('trial_date_end', '=', False)]}"> - </span><field name="trial_date_end" class="oe_inline"/>
</div>
<label for="date_start" string="Duration"/>
<div>
<field name="date_start" class="oe_inline"/><span attrs="{'invisible': ['|', ('date_start', '=', False), ('date_end', '=', False)]}"> - </span><field name="date_end" class="oe_inline"/>
</group>
</page>
<page string="Other Information" name="other">
<group>
<group name="duration_group" string="Contract Terms">
<field name="date_start"/>
<label for="date_end"/>
<div class="o_row">
<field name="date_end" nolabel="1"/>
<span class="text-muted">(If fixed-term contract)</span>
</div>
<field name="trial_date_end"/>
<field name="resource_calendar_id"/>
</group>
</group>
<separator string="Notes"/>
<field name="notes"/>
</page>
<page string="Work Permit">
<group col="4">
<field name="visa_no"/>
<field name="permit_no"/>
<field name="visa_expire"/>
<group string="Notes">
<field name="notes" nolabel="1"/>
</group>
</page>
</notebook>
+5 -2
View File
@@ -30,12 +30,15 @@ Generic Payroll system.
'security/hr_payroll_security.xml',
'security/ir.model.access.csv',
'wizard/hr_payroll_payslips_by_employees_views.xml',
'views/hr_payroll_views.xml',
'views/hr_contract_views.xml',
'views/hr_salary_rule_views.xml',
'views/hr_payslip_views.xml',
'views/hr_employee_views.xml',
'data/hr_payroll_sequence.xml',
'views/hr_payroll_report.xml',
'data/hr_payroll_data.xml',
'wizard/hr_payroll_contribution_register_report_views.xml',
'views/res_config_views.xml',
'views/hr_payroll_config_settings_views.xml',
'views/report_contributionregister_templates.xml',
'views/report_payslip_templates.xml',
'views/report_payslipdetails_templates.xml',
+2 -2
View File
@@ -37,7 +37,7 @@
</record>
<record id="hr_rule_basic" model="hr.salary.rule">
<field name="name">Basic</field>
<field name="name">Basic Salary</field>
<field name="sequence" eval="1"/>
<field name="code">BASIC</field>
<field name="category_id" ref="hr_payroll.BASIC"/>
@@ -57,7 +57,7 @@
</record>
<record id="hr_rule_net" model="hr.salary.rule">
<field name="name">Net</field>
<field name="name">Net Salary</field>
<field name="sequence" eval="200"/>
<field name="code">NET</field>
<field name="category_id" ref="hr_payroll.NET"/>
+5 -2
View File
@@ -1,5 +1,8 @@
#-*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import hr_payroll
import res_config
import hr_contract
import hr_employee
import hr_payroll_config_settings
import hr_salary_rule
import hr_payslip
+61
View File
@@ -0,0 +1,61 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class HrContract(models.Model):
"""
Employee contract based on the visa, work permits
allows to configure different Salary structure
"""
_inherit = 'hr.contract'
_description = 'Employee Contract'
struct_id = fields.Many2one('hr.payroll.structure', string='Salary Structure')
schedule_pay = fields.Selection([
('monthly', 'Monthly'),
('quarterly', 'Quarterly'),
('semi-annually', 'Semi-annually'),
('annually', 'Annually'),
('weekly', 'Weekly'),
('bi-weekly', 'Bi-weekly'),
('bi-monthly', 'Bi-monthly'),
], string='Scheduled Pay', index=True, default='monthly')
resource_calendar_id = fields.Many2one(required=True)
@api.multi
def get_all_structures(self):
"""
@return: the structures linked to the given contracts, ordered by hierachy (parent=False first,
then first level children and so on) and without duplicata
"""
structures = self.mapped('struct_id')
if not structures:
return []
# YTI TODO return browse records
return list(set(structures._get_parent_structure().ids))
@api.multi
def get_attribute(self, code, attribute):
return getattr(self.env['hr.contract.advantage.template'].search([('code', '=', code)], limit=1), attribute)
@api.multi
def set_attribute_value(self, code, active):
for contract in self:
if active:
value = self.env['hr.contract.advantage.template'].search([('code', '=', code)], limit=1).default_value
setattr(contract, code, value)
else:
setattr(contract, code, 0.0)
class HrContractAdvandageTemplate(models.Model):
_name = 'hr.contract.advantage.template'
_description = "Employee's Advantage on Contract"
name = fields.Char('Name', required=True)
code = fields.Char('Code', required=True)
lower_bound = fields.Float('Lower Bound', help="Lower bound authorized by the employer for this advantage")
upper_bound = fields.Float('Upper Bound', help="Upper bound authorized by the employer for this advantage")
default_value = fields.Float('Default value for this advantage')
+17
View File
@@ -0,0 +1,17 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class HrEmployee(models.Model):
_inherit = 'hr.employee'
_description = 'Employee'
slip_ids = fields.One2many('hr.payslip', 'employee_id', string='Payslips', readonly=True)
payslip_count = fields.Integer(compute='_compute_payslip_count', string='Payslips', groups="hr_payroll.group_hr_payroll_user")
@api.multi
def _compute_payslip_count(self):
for employee in self:
employee.payslip_count = len(employee.slip_ids)
@@ -2,157 +2,15 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import time
from datetime import datetime, timedelta
from datetime import datetime
from datetime import time as datetime_time
from dateutil import relativedelta
import babel
from odoo import api, fields, models, tools, _
from odoo.exceptions import UserError, ValidationError
from odoo.tools.safe_eval import safe_eval
from odoo.addons import decimal_precision as dp
class HrPayrollStructure(models.Model):
"""
Salary structure used to defined
- Basic
- Allowances
- Deductions
"""
_name = 'hr.payroll.structure'
_description = 'Salary Structure'
@api.model
def _get_parent(self):
return self.env.ref('hr_payroll.structure_base', False)
name = fields.Char(required=True)
code = fields.Char(string='Reference', required=True)
company_id = fields.Many2one('res.company', string='Company', required=True,
copy=False, default=lambda self: self.env['res.company']._company_default_get())
note = fields.Text(string='Description')
parent_id = fields.Many2one('hr.payroll.structure', string='Parent', default=_get_parent)
children_ids = fields.One2many('hr.payroll.structure', 'parent_id', string='Children', copy=True)
rule_ids = fields.Many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id', string='Salary Rules')
@api.constrains('parent_id')
def _check_parent_id(self):
if not self._check_recursion():
raise ValidationError(_('Error ! You cannot create a recursive Salary Structure.'))
@api.multi
def copy(self, default=None):
self.ensure_one()
default = dict(default or {}, code=_("%s (copy)") % (self.code))
return super(HrPayrollStructure, self).copy(default)
@api.multi
def get_all_rules(self):
"""
@return: returns a list of tuple (id, sequence) of rules that are maybe to apply
"""
all_rules = []
for struct in self:
all_rules += struct.rule_ids._recursive_search_of_rules()
return all_rules
@api.multi
def _get_parent_structure(self):
parent = self.mapped('parent_id')
if parent:
parent = parent._get_parent_structure()
return parent + self
class HrContract(models.Model):
"""
Employee contract based on the visa, work permits
allows to configure different Salary structure
"""
_inherit = 'hr.contract'
_description = 'Employee Contract'
struct_id = fields.Many2one('hr.payroll.structure', string='Salary Structure')
schedule_pay = fields.Selection([
('monthly', 'Monthly'),
('quarterly', 'Quarterly'),
('semi-annually', 'Semi-annually'),
('annually', 'Annually'),
('weekly', 'Weekly'),
('bi-weekly', 'Bi-weekly'),
('bi-monthly', 'Bi-monthly'),
], string='Scheduled Pay', index=True, default='monthly')
@api.multi
def get_all_structures(self):
"""
@return: the structures linked to the given contracts, ordered by hierachy (parent=False first,
then first level children and so on) and without duplicata
"""
structures = self.mapped('struct_id')
if not structures:
return []
# YTI TODO return browse records
return list(set(structures._get_parent_structure().ids))
class HrContributionRegister(models.Model):
_name = 'hr.contribution.register'
_description = 'Contribution Register'
company_id = fields.Many2one('res.company', string='Company',
default=lambda self: self.env['res.company']._company_default_get())
partner_id = fields.Many2one('res.partner', string='Partner')
name = fields.Char(required=True)
register_line_ids = fields.One2many('hr.payslip.line', 'register_id',
string='Register Line', readonly=True)
note = fields.Text(string='Description')
class HrSalaryRuleCategory(models.Model):
_name = 'hr.salary.rule.category'
_description = 'Salary Rule Category'
name = fields.Char(required=True)
code = fields.Char(required=True)
parent_id = fields.Many2one('hr.salary.rule.category', string='Parent',
help="Linking a salary category to its parent is used only for the reporting purpose.")
children_ids = fields.One2many('hr.salary.rule.category', 'parent_id', string='Children')
note = fields.Text(string='Description')
company_id = fields.Many2one('res.company', string='Company',
default=lambda self: self.env['res.company']._company_default_get())
class HrPayslipRun(models.Model):
_name = 'hr.payslip.run'
_description = 'Payslip Batches'
name = fields.Char(required=True, readonly=True, states={'draft': [('readonly', False)]})
slip_ids = fields.One2many('hr.payslip', 'payslip_run_id', string='Payslips', readonly=True,
states={'draft': [('readonly', False)]})
state = fields.Selection([
('draft', 'Draft'),
('close', 'Close'),
], string='Status', index=True, readonly=True, copy=False, default='draft')
date_start = fields.Date(string='Date From', required=True, readonly=True,
states={'draft': [('readonly', False)]}, default=time.strftime('%Y-%m-01'))
date_end = fields.Date(string='Date To', required=True, readonly=True,
states={'draft': [('readonly', False)]},
default=str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10])
credit_note = fields.Boolean(string='Credit Note', readonly=True,
states={'draft': [('readonly', False)]},
help="If its checked, indicates that all payslips generated from here are refund payslips.")
@api.multi
def draft_payslip_run(self):
return self.write({'state': 'draft'})
@api.multi
def close_payslip_run(self):
return self.write({'state': 'close'})
from odoo.exceptions import UserError, ValidationError
class HrPayslip(models.Model):
@@ -271,7 +129,7 @@ class HrPayslip(models.Model):
raise UserError(_('You cannot delete a payslip which is not draft or cancelled!'))
return super(HrPayslip, self).unlink()
#TODO move this function into hr_contract module, on hr.employee object
# TODO move this function into hr_contract module, on hr.employee object
@api.model
def get_contract(self, employee, date_from, date_to):
"""
@@ -280,20 +138,20 @@ class HrPayslip(models.Model):
@param date_to: date field
@return: returns the ids of all the contracts for the given employee that need to be considered for the given dates
"""
#a contract is valid if it ends between the given dates
# a contract is valid if it ends between the given dates
clause_1 = ['&', ('date_end', '<=', date_to), ('date_end', '>=', date_from)]
#OR if it starts between the given dates
# OR if it starts between the given dates
clause_2 = ['&', ('date_start', '<=', date_to), ('date_start', '>=', date_from)]
#OR if it starts before the date_from and finish after the date_end (or never finish)
# OR if it starts before the date_from and finish after the date_end (or never finish)
clause_3 = ['&', ('date_start', '<=', date_from), '|', ('date_end', '=', False), ('date_end', '>=', date_to)]
clause_final = [('employee_id', '=', employee.id), '|', '|'] + clause_1 + clause_2 + clause_3
clause_final = [('employee_id', '=', employee.id), ('state', '=', 'open'), '|', '|'] + clause_1 + clause_2 + clause_3
return self.env['hr.contract'].search(clause_final).ids
@api.multi
def compute_sheet(self):
for payslip in self:
number = payslip.number or self.env['ir.sequence'].next_by_code('salary.slip')
#delete old payslip lines
# delete old payslip lines
payslip.line_ids.unlink()
# set the list of contract for which the rules have to be applied
# if we don't give the contract, then the rules to apply should be for all current contracts of the employee
@@ -304,14 +162,14 @@ class HrPayslip(models.Model):
return True
@api.model
def get_worked_day_lines(self, contract_ids, date_from, date_to):
def get_worked_day_lines(self, contracts, date_from, date_to):
"""
@param contract_ids: list of contract id
@param contract: Browse record of contracts
@return: returns a list of dict containing the input that should be applied for the given contract between date_from and date_to
"""
res = []
# fill only if the contract as a working schedule linked
for contract in self.env['hr.contract'].browse(contract_ids).filtered(lambda contract: contract.resource_calendar_id):
for contract in contracts.filtered(lambda contract: contract.resource_calendar_id):
day_from = datetime.combine(fields.Date.from_string(date_from), datetime_time.min)
day_to = datetime.combine(fields.Date.from_string(date_to), datetime_time.max)
@@ -329,7 +187,7 @@ class HrPayslip(models.Model):
'number_of_hours': 0.0,
'contract_id': contract.id,
})
leave_time = (interval[1] - interval[0]).seconds/3600
leave_time = (interval[1] - interval[0]).seconds / 3600
current_leave_struct['number_of_hours'] += leave_time
work_hours = contract.employee_id.get_day_work_hours_count(interval[0].date(), calendar=contract.resource_calendar_id)
current_leave_struct['number_of_days'] += leave_time / work_hours
@@ -348,12 +206,10 @@ class HrPayslip(models.Model):
res += [attendances] + leaves.values()
return res
# YTI TODO contract_ids should be a browse record
@api.model
def get_inputs(self, contract_ids, date_from, date_to):
def get_inputs(self, contracts, date_from, date_to):
res = []
contracts = self.env['hr.contract'].browse(contract_ids)
structure_ids = contracts.get_all_structures()
rule_ids = self.env['hr.payroll.structure'].browse(structure_ids).get_all_rules()
sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
@@ -374,9 +230,7 @@ class HrPayslip(models.Model):
def _sum_salary_rule_category(localdict, category, amount):
if category.parent_id:
localdict = _sum_salary_rule_category(localdict, category.parent_id, amount)
if category.code in localdict['categories'].dict:
amount += localdict['categories'].dict[category.code]
localdict['categories'].dict[category.code] = amount
localdict['categories'].dict[category.code] = category.code in localdict['categories'].dict and localdict['categories'].dict[category.code] + amount or amount
return localdict
class BrowsableObject(object):
@@ -567,8 +421,9 @@ class HrPayslip(models.Model):
'struct_id': struct.id,
})
#computation of the salary input
worked_days_line_ids = self.get_worked_day_lines(contract_ids, date_from, date_to)
input_line_ids = self.get_inputs(contract_ids, date_from, date_to)
contracts = self.env['hr.contract'].browse(contract_ids)
worked_days_line_ids = self.get_worked_day_lines(contracts, date_from, date_to)
input_line_ids = self.get_inputs(contracts, date_from, date_to)
res['value'].update({
'worked_days_line_ids': worked_days_line_ids,
'input_line_ids': input_line_ids,
@@ -601,13 +456,14 @@ class HrPayslip(models.Model):
self.struct_id = self.contract_id.struct_id
#computation of the salary input
worked_days_line_ids = self.get_worked_day_lines(contract_ids, date_from, date_to)
contracts = self.env['hr.contract'].browse(contract_ids)
worked_days_line_ids = self.get_worked_day_lines(contracts, date_from, date_to)
worked_days_lines = self.worked_days_line_ids.browse([])
for r in worked_days_line_ids:
worked_days_lines += worked_days_lines.new(r)
self.worked_days_line_ids = worked_days_lines
input_line_ids = self.get_inputs(contract_ids, date_from, date_to)
input_line_ids = self.get_inputs(contracts, date_from, date_to)
input_lines = self.input_line_ids.browse([])
for r in input_line_ids:
input_lines += input_lines.new(r)
@@ -621,6 +477,45 @@ class HrPayslip(models.Model):
self.with_context(contract=True).onchange_employee()
return
def get_salary_line_total(self, code):
self.ensure_one()
line = self.line_ids.filtered(lambda line: line.code == code)
if line:
return line[0].total
else:
return 0.0
class HrPayslipLine(models.Model):
_name = 'hr.payslip.line'
_inherit = 'hr.salary.rule'
_description = 'Payslip Line'
_order = 'contract_id, sequence'
slip_id = fields.Many2one('hr.payslip', string='Pay Slip', required=True, ondelete='cascade')
salary_rule_id = fields.Many2one('hr.salary.rule', string='Rule', required=True)
employee_id = fields.Many2one('hr.employee', string='Employee', required=True)
contract_id = fields.Many2one('hr.contract', string='Contract', required=True, index=True)
rate = fields.Float(string='Rate (%)', digits=dp.get_precision('Payroll Rate'), default=100.0)
amount = fields.Float(digits=dp.get_precision('Payroll'))
quantity = fields.Float(digits=dp.get_precision('Payroll'), default=1.0)
total = fields.Float(compute='_compute_total', string='Total', digits=dp.get_precision('Payroll'), store=True)
@api.depends('quantity', 'amount', 'rate')
def _compute_total(self):
for line in self:
line.total = float(line.quantity) * line.amount * line.rate / 100
@api.model
def create(self, values):
if 'employee_id' not in values or 'contract_id' not in values:
payslip = self.env['hr.payslip'].browse(values.get('slip_id'))
values['employee_id'] = values.get('employee_id') or payslip.employee_id.id
values['contract_id'] = values.get('contract_id') or payslip.contract_id and payslip.contract_id.id
if not values['contract_id']:
raise UserError(_('You must set a contract to create a payslip line.'))
return super(HrPayslipLine, self).create(values)
class HrPayslipWorkedDays(models.Model):
_name = 'hr.payslip.worked_days'
@@ -653,194 +548,30 @@ class HrPayslipInput(models.Model):
help="The contract for which applied this input")
class HrSalaryRule(models.Model):
_name = 'hr.salary.rule'
class HrPayslipRun(models.Model):
_name = 'hr.payslip.run'
_description = 'Payslip Batches'
name = fields.Char(required=True)
code = fields.Char(required=True,
help="The code of salary rules can be used as reference in computation of other rules. "
"In that case, it is case sensitive.")
sequence = fields.Integer(required=True, index=True, default=5,
help='Use to arrange calculation sequence')
quantity = fields.Char(default='1.0',
help="It is used in computation for percentage and fixed amount. "
"For e.g. A rule for Meal Voucher having fixed amount of "
u"1€ per worked day can have its quantity defined in expression "
"like worked_days.WORK100.number_of_days.")
category_id = fields.Many2one('hr.salary.rule.category', string='Category', required=True)
active = fields.Boolean(default=True,
help="If the active field is set to false, it will allow you to hide the salary rule without removing it.")
appears_on_payslip = fields.Boolean(string='Appears on Payslip', default=True,
help="Used to display the salary rule on payslip.")
parent_rule_id = fields.Many2one('hr.salary.rule', string='Parent Salary Rule', index=True)
company_id = fields.Many2one('res.company', string='Company',
default=lambda self: self.env['res.company']._company_default_get())
condition_select = fields.Selection([
('none', 'Always True'),
('range', 'Range'),
('python', 'Python Expression')
], string="Condition Based on", default='none', required=True)
condition_range = fields.Char(string='Range Based on', default='contract.wage',
help='This will be used to compute the % fields values; in general it is on basic, '
'but you can also use categories code fields in lowercase as a variable names '
'(hra, ma, lta, etc.) and the variable basic.')
condition_python = fields.Text(string='Python Condition', required=True,
default='''
# Available variables:
#----------------------
# payslip: object containing the payslips
# employee: hr.employee object
# contract: hr.contract object
# rules: object containing the rules code (previously computed)
# categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category).
# worked_days: object containing the computed worked days
# inputs: object containing the computed inputs
# Note: returned value have to be set in the variable 'result'
result = rules.NET > categories.NET * 0.10''',
help='Applied this rule for calculation if condition is true. You can specify condition like basic > 1000.')
condition_range_min = fields.Float(string='Minimum Range', help="The minimum amount, applied for this rule.")
condition_range_max = fields.Float(string='Maximum Range', help="The maximum amount, applied for this rule.")
amount_select = fields.Selection([
('percentage', 'Percentage (%)'),
('fix', 'Fixed Amount'),
('code', 'Python Code'),
], string='Amount Type', index=True, required=True, default='fix', help="The computation method for the rule amount.")
amount_fix = fields.Float(string='Fixed Amount', digits=dp.get_precision('Payroll'))
amount_percentage = fields.Float(string='Percentage (%)', digits=dp.get_precision('Payroll Rate'),
help='For example, enter 50.0 to apply a percentage of 50%')
amount_python_compute = fields.Text(string='Python Code',
default='''
# Available variables:
#----------------------
# payslip: object containing the payslips
# employee: hr.employee object
# contract: hr.contract object
# rules: object containing the rules code (previously computed)
# categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category).
# worked_days: object containing the computed worked days.
# inputs: object containing the computed inputs.
# Note: returned value have to be set in the variable 'result'
result = contract.wage * 0.10''')
amount_percentage_base = fields.Char(string='Percentage based on', help='result will be affected to a variable')
child_ids = fields.One2many('hr.salary.rule', 'parent_rule_id', string='Child Salary Rule', copy=True)
register_id = fields.Many2one('hr.contribution.register', string='Contribution Register',
help="Eventual third party involved in the salary payment of the employees.")
input_ids = fields.One2many('hr.rule.input', 'input_id', string='Inputs', copy=True)
note = fields.Text(string='Description')
name = fields.Char(required=True, readonly=True, states={'draft': [('readonly', False)]})
slip_ids = fields.One2many('hr.payslip', 'payslip_run_id', string='Payslips', readonly=True,
states={'draft': [('readonly', False)]})
state = fields.Selection([
('draft', 'Draft'),
('close', 'Close'),
], string='Status', index=True, readonly=True, copy=False, default='draft')
date_start = fields.Date(string='Date From', required=True, readonly=True,
states={'draft': [('readonly', False)]}, default=time.strftime('%Y-%m-01'))
date_end = fields.Date(string='Date To', required=True, readonly=True,
states={'draft': [('readonly', False)]},
default=str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10])
credit_note = fields.Boolean(string='Credit Note', readonly=True,
states={'draft': [('readonly', False)]},
help="If its checked, indicates that all payslips generated from here are refund payslips.")
@api.multi
def _recursive_search_of_rules(self):
"""
@return: returns a list of tuple (id, sequence) which are all the children of the passed rule_ids
"""
children_rules = []
for rule in self.filtered(lambda rule: rule.child_ids):
children_rules += rule.child_ids._recursive_search_of_rules()
return [(rule.id, rule.sequence) for rule in self] + children_rules
#TODO should add some checks on the type of result (should be float)
@api.multi
def compute_rule(self, localdict):
"""
:param localdict: dictionary containing the environement in which to compute the rule
:return: returns a tuple build as the base/amount computed, the quantity and the rate
:rtype: (float, float, float)
"""
self.ensure_one()
if self.amount_select == 'fix':
try:
return self.amount_fix, float(safe_eval(self.quantity, localdict)), 100.0
except:
raise UserError(_('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code))
elif self.amount_select == 'percentage':
try:
return (float(safe_eval(self.amount_percentage_base, localdict)),
float(safe_eval(self.quantity, localdict)),
self.amount_percentage)
except:
raise UserError(_('Wrong percentage base or quantity defined for salary rule %s (%s).') % (self.name, self.code))
else:
try:
safe_eval(self.amount_python_compute, localdict, mode='exec', nocopy=True)
return float(localdict['result']), 'result_qty' in localdict and localdict['result_qty'] or 1.0, 'result_rate' in localdict and localdict['result_rate'] or 100.0
except:
raise UserError(_('Wrong python code defined for salary rule %s (%s).') % (self.name, self.code))
def draft_payslip_run(self):
return self.write({'state': 'draft'})
@api.multi
def satisfy_condition(self, localdict):
"""
@param contract_id: id of hr.contract to be tested
@return: returns True if the given rule match the condition for the given contract. Return False otherwise.
"""
self.ensure_one()
if self.condition_select == 'none':
return True
elif self.condition_select == 'range':
try:
result = safe_eval(self.condition_range, localdict)
return self.condition_range_min <= result and result <= self.condition_range_max or False
except:
raise UserError(_('Wrong range condition defined for salary rule %s (%s).') % (self.name, self.code))
else: # python code
try:
safe_eval(self.condition_python, localdict, mode='exec', nocopy=True)
return 'result' in localdict and localdict['result'] or False
except:
raise UserError(_('Wrong python condition defined for salary rule %s (%s).') % (self.name, self.code))
class HrRuleInput(models.Model):
_name = 'hr.rule.input'
_description = 'Salary Rule Input'
name = fields.Char(string='Description', required=True)
code = fields.Char(required=True, help="The code that can be used in the salary rules")
input_id = fields.Many2one('hr.salary.rule', string='Salary Rule Input', required=True)
class HrPayslipLine(models.Model):
_name = 'hr.payslip.line'
_inherit = 'hr.salary.rule'
_description = 'Payslip Line'
_order = 'contract_id, sequence'
slip_id = fields.Many2one('hr.payslip', string='Pay Slip', required=True, ondelete='cascade')
salary_rule_id = fields.Many2one('hr.salary.rule', string='Rule', required=True)
employee_id = fields.Many2one('hr.employee', string='Employee', required=True)
contract_id = fields.Many2one('hr.contract', string='Contract', required=True, index=True)
rate = fields.Float(string='Rate (%)', digits=dp.get_precision('Payroll Rate'), default=100.0)
amount = fields.Float(digits=dp.get_precision('Payroll'))
quantity = fields.Float(digits=dp.get_precision('Payroll'), default=1.0)
total = fields.Float(compute='_compute_total', string='Total', digits=dp.get_precision('Payroll'), store=True)
@api.depends('quantity', 'amount', 'rate')
def _compute_total(self):
for line in self:
line.total = float(line.quantity) * line.amount * line.rate / 100
@api.model
def create(self, values):
if 'employee_id' not in values or 'contract_id' not in values:
payslip = self.env['hr.payslip'].browse(values.get('slip_id'))
values['employee_id'] = values.get('employee_id') or payslip.employee_id.id
values['contract_id'] = values.get('contract_id') or payslip.contract_id and payslip.contract_id.id
if not values['contract_id']:
raise UserError(_('You must set a contract to create a payslip line.'))
return super(HrPayslipLine, self).create(values)
class HrEmployee(models.Model):
_inherit = 'hr.employee'
_description = 'Employee'
slip_ids = fields.One2many('hr.payslip', 'employee_id', string='Payslips', readonly=True)
payslip_count = fields.Integer(compute='_compute_payslip_count', string='Payslips', groups="hr_payroll.group_hr_payroll_user")
@api.multi
def _compute_payslip_count(self):
for employee in self:
employee.payslip_count = len(employee.slip_ids)
def close_payslip_run(self):
return self.write({'state': 'close'})
+236
View File
@@ -0,0 +1,236 @@
# -*- 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 odoo.tools.safe_eval import safe_eval
from odoo.addons import decimal_precision as dp
class HrPayrollStructure(models.Model):
"""
Salary structure used to defined
- Basic
- Allowances
- Deductions
"""
_name = 'hr.payroll.structure'
_description = 'Salary Structure'
@api.model
def _get_parent(self):
return self.env.ref('hr_payroll.structure_base', False)
name = fields.Char(required=True)
code = fields.Char(string='Reference', required=True)
company_id = fields.Many2one('res.company', string='Company', required=True,
copy=False, default=lambda self: self.env['res.company']._company_default_get())
note = fields.Text(string='Description')
parent_id = fields.Many2one('hr.payroll.structure', string='Parent', default=_get_parent)
children_ids = fields.One2many('hr.payroll.structure', 'parent_id', string='Children', copy=True)
rule_ids = fields.Many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id', string='Salary Rules')
@api.constrains('parent_id')
def _check_parent_id(self):
if not self._check_recursion():
raise ValidationError(_('Error ! You cannot create a recursive Salary Structure.'))
@api.multi
def copy(self, default=None):
self.ensure_one()
default = dict(default or {}, code=_("%s (copy)") % (self.code))
return super(HrPayrollStructure, self).copy(default)
@api.multi
def get_all_rules(self):
"""
@return: returns a list of tuple (id, sequence) of rules that are maybe to apply
"""
all_rules = []
for struct in self:
all_rules += struct.rule_ids._recursive_search_of_rules()
return all_rules
@api.multi
def _get_parent_structure(self):
parent = self.mapped('parent_id')
if parent:
parent = parent._get_parent_structure()
return parent + self
class HrContributionRegister(models.Model):
_name = 'hr.contribution.register'
_description = 'Contribution Register'
company_id = fields.Many2one('res.company', string='Company',
default=lambda self: self.env['res.company']._company_default_get())
partner_id = fields.Many2one('res.partner', string='Partner')
name = fields.Char(required=True)
register_line_ids = fields.One2many('hr.payslip.line', 'register_id',
string='Register Line', readonly=True)
note = fields.Text(string='Description')
class HrSalaryRuleCategory(models.Model):
_name = 'hr.salary.rule.category'
_description = 'Salary Rule Category'
name = fields.Char(required=True)
code = fields.Char(required=True)
parent_id = fields.Many2one('hr.salary.rule.category', string='Parent',
help="Linking a salary category to its parent is used only for the reporting purpose.")
children_ids = fields.One2many('hr.salary.rule.category', 'parent_id', string='Children')
note = fields.Text(string='Description')
company_id = fields.Many2one('res.company', string='Company',
default=lambda self: self.env['res.company']._company_default_get())
class HrSalaryRule(models.Model):
_name = 'hr.salary.rule'
name = fields.Char(required=True)
code = fields.Char(required=True,
help="The code of salary rules can be used as reference in computation of other rules. "
"In that case, it is case sensitive.")
sequence = fields.Integer(required=True, index=True, default=5,
help='Use to arrange calculation sequence')
quantity = fields.Char(default='1.0',
help="It is used in computation for percentage and fixed amount. "
"For e.g. A rule for Meal Voucher having fixed amount of "
u"1€ per worked day can have its quantity defined in expression "
"like worked_days.WORK100.number_of_days.")
category_id = fields.Many2one('hr.salary.rule.category', string='Category', required=True)
active = fields.Boolean(default=True,
help="If the active field is set to false, it will allow you to hide the salary rule without removing it.")
appears_on_payslip = fields.Boolean(string='Appears on Payslip', default=True,
help="Used to display the salary rule on payslip.")
parent_rule_id = fields.Many2one('hr.salary.rule', string='Parent Salary Rule', index=True)
company_id = fields.Many2one('res.company', string='Company',
default=lambda self: self.env['res.company']._company_default_get())
condition_select = fields.Selection([
('none', 'Always True'),
('range', 'Range'),
('python', 'Python Expression')
], string="Condition Based on", default='none', required=True)
condition_range = fields.Char(string='Range Based on', default='contract.wage',
help='This will be used to compute the % fields values; in general it is on basic, '
'but you can also use categories code fields in lowercase as a variable names '
'(hra, ma, lta, etc.) and the variable basic.')
condition_python = fields.Text(string='Python Condition', required=True,
default='''
# Available variables:
#----------------------
# payslip: object containing the payslips
# employee: hr.employee object
# contract: hr.contract object
# rules: object containing the rules code (previously computed)
# categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category).
# worked_days: object containing the computed worked days
# inputs: object containing the computed inputs
# Note: returned value have to be set in the variable 'result'
result = rules.NET > categories.NET * 0.10''',
help='Applied this rule for calculation if condition is true. You can specify condition like basic > 1000.')
condition_range_min = fields.Float(string='Minimum Range', help="The minimum amount, applied for this rule.")
condition_range_max = fields.Float(string='Maximum Range', help="The maximum amount, applied for this rule.")
amount_select = fields.Selection([
('percentage', 'Percentage (%)'),
('fix', 'Fixed Amount'),
('code', 'Python Code'),
], string='Amount Type', index=True, required=True, default='fix', help="The computation method for the rule amount.")
amount_fix = fields.Float(string='Fixed Amount', digits=dp.get_precision('Payroll'))
amount_percentage = fields.Float(string='Percentage (%)', digits=dp.get_precision('Payroll Rate'),
help='For example, enter 50.0 to apply a percentage of 50%')
amount_python_compute = fields.Text(string='Python Code',
default='''
# Available variables:
#----------------------
# payslip: object containing the payslips
# employee: hr.employee object
# contract: hr.contract object
# rules: object containing the rules code (previously computed)
# categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category).
# worked_days: object containing the computed worked days.
# inputs: object containing the computed inputs.
# Note: returned value have to be set in the variable 'result'
result = contract.wage * 0.10''')
amount_percentage_base = fields.Char(string='Percentage based on', help='result will be affected to a variable')
child_ids = fields.One2many('hr.salary.rule', 'parent_rule_id', string='Child Salary Rule', copy=True)
register_id = fields.Many2one('hr.contribution.register', string='Contribution Register',
help="Eventual third party involved in the salary payment of the employees.")
input_ids = fields.One2many('hr.rule.input', 'input_id', string='Inputs', copy=True)
note = fields.Text(string='Description')
@api.multi
def _recursive_search_of_rules(self):
"""
@return: returns a list of tuple (id, sequence) which are all the children of the passed rule_ids
"""
children_rules = []
for rule in self.filtered(lambda rule: rule.child_ids):
children_rules += rule.child_ids._recursive_search_of_rules()
return [(rule.id, rule.sequence) for rule in self] + children_rules
#TODO should add some checks on the type of result (should be float)
@api.multi
def compute_rule(self, localdict):
"""
:param localdict: dictionary containing the environement in which to compute the rule
:return: returns a tuple build as the base/amount computed, the quantity and the rate
:rtype: (float, float, float)
"""
self.ensure_one()
if self.amount_select == 'fix':
try:
return self.amount_fix, float(safe_eval(self.quantity, localdict)), 100.0
except:
raise UserError(_('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code))
elif self.amount_select == 'percentage':
try:
return (float(safe_eval(self.amount_percentage_base, localdict)),
float(safe_eval(self.quantity, localdict)),
self.amount_percentage)
except:
raise UserError(_('Wrong percentage base or quantity defined for salary rule %s (%s).') % (self.name, self.code))
else:
try:
safe_eval(self.amount_python_compute, localdict, mode='exec', nocopy=True)
return float(localdict['result']), 'result_qty' in localdict and localdict['result_qty'] or 1.0, 'result_rate' in localdict and localdict['result_rate'] or 100.0
except:
raise UserError(_('Wrong python code defined for salary rule %s (%s).') % (self.name, self.code))
@api.multi
def satisfy_condition(self, localdict):
"""
@param contract_id: id of hr.contract to be tested
@return: returns True if the given rule match the condition for the given contract. Return False otherwise.
"""
self.ensure_one()
if self.condition_select == 'none':
return True
elif self.condition_select == 'range':
try:
result = safe_eval(self.condition_range, localdict)
return self.condition_range_min <= result and result <= self.condition_range_max or False
except:
raise UserError(_('Wrong range condition defined for salary rule %s (%s).') % (self.name, self.code))
else: # python code
try:
safe_eval(self.condition_python, localdict, mode='exec', nocopy=True)
return 'result' in localdict and localdict['result'] or False
except:
raise UserError(_('Wrong python condition defined for salary rule %s (%s).') % (self.name, self.code))
class HrRuleInput(models.Model):
_name = 'hr.rule.input'
_description = 'Salary Rule Input'
name = fields.Char(string='Description', required=True)
code = fields.Char(required=True, help="The code that can be used in the salary rules")
input_id = fields.Many2one('hr.salary.rule', string='Salary Rule Input', required=True)
@@ -9,3 +9,4 @@ access_hr_payslip_worked_days_officer,hr.payslip.worked_days.officer,model_hr_pa
access_hr_payslip_run,hr.payslip.run,model_hr_payslip_run,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_hr_rule_input_officer,hr.rule.input.office,model_hr_rule_input,hr_payroll.group_hr_payroll_user,1,1,1,1
access_hr_salary_rule_user,hr.salary.rule.user,model_hr_salary_rule,hr_payroll.group_hr_payroll_user,1,1,1,1
access_hr_contract_advantage_template,hr.contract.advantage.template.user,model_hr_contract_advantage_template,hr_payroll.group_hr_payroll_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
9 access_hr_payslip_run hr.payslip.run model_hr_payslip_run hr_payroll.group_hr_payroll_manager 1 1 1 1
10 access_hr_rule_input_officer hr.rule.input.office model_hr_rule_input hr_payroll.group_hr_payroll_user 1 1 1 1
11 access_hr_salary_rule_user hr.salary.rule.user model_hr_salary_rule hr_payroll.group_hr_payroll_user 1 1 1 1
12 access_hr_contract_advantage_template hr.contract.advantage.template.user model_hr_contract_advantage_template hr_payroll.group_hr_payroll_user 1 1 1 1
@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Root Menus -->
<menuitem id="menu_hr_payroll_root" name="Payroll" sequence="45" web_icon="hr_payroll,static/description/icon.png"/>
<menuitem id="menu_hr_payroll_configuration" name="Configuration" parent="menu_hr_payroll_root"
sequence="100" groups="hr_payroll.group_hr_payroll_manager"/>
<!-- Contract View -->
<record id="hr_contract_form_inherit" model="ir.ui.view">
<field name="name">hr.contract.view.form.inherit</field>
<field name="model">hr.contract</field>
<field name="inherit_id" ref="hr_contract.hr_contract_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='type_id']" position="after">
<field name="struct_id" required="1"/>
</xpath>
<xpath expr="//field[@name='type_id']" position="before">
<field name="company_id"/>
<field name="currency_id" invisible="1"/>
</xpath>
<xpath expr="//field[@name='resource_calendar_id']" position="after">
<field name="schedule_pay"/>
</xpath>
</field>
</record>
<record id="hr_contract_advantage_template_view_form" model="ir.ui.view">
<field name="name">hr.contract.advantage.template.form</field>
<field name="model">hr.contract.advantage.template</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="oe_title">
<h1>
<field name="name" placeholder="Advantage Name"/>
</h1>
</div>
<group>
<group>
<field name="code"/>
</group>
</group>
<group>
<group>
<field name="default_value"/>
<field name="lower_bound"/>
<field name="upper_bound"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="hr_contract_advantage_template_view_tree" model="ir.ui.view">
<field name="name">hr.contract.advantage.template.tree</field>
<field name="model">hr.contract.advantage.template</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="code"/>
<field name="lower_bound"/>
<field name="upper_bound"/>
<field name="default_value"/>
</tree>
</field>
</record>
<record id="hr_contract_advantage_template_action" model="ir.actions.act_window">
<field name="name">Contract Advantage Templates</field>
<field name="res_model">hr.contract.advantage.template</field>
<field name="view_type">form</field>
</record>
<menuitem
id="hr_contract_advantage_template_menu_action"
action="hr_contract_advantage_template_action"
parent="menu_hr_payroll_configuration"
sequence="50"/>
</odoo>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Employee View -->
<record id="payroll_hr_employee_view_form" model="ir.ui.view">
<field name="name">payroll.hr.employee.view.form</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml">
<button name="toggle_active" position="before">
<button name="%(act_hr_employee_payslip_list)d"
class="oe_stat_button"
icon="fa-money"
type="action"
groups="hr_payroll.group_hr_payroll_user">
<field name="payslip_count" widget="statinfo" string="Payslips"/>
</button>
</button>
</field>
</record>
</odoo>
@@ -1,741 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Root Menus -->
<menuitem id="menu_hr_payroll_root" name="Payroll" sequence="45" web_icon="hr_payroll,static/description/icon.png"/>
<menuitem id="menu_hr_payroll_configuration" name="Configuration" parent="menu_hr_payroll_root"
sequence="100" groups="hr_payroll.group_hr_payroll_manager"/>
<!-- Contract View -->
<record id="hr_contract_form_inherit" model="ir.ui.view">
<field name="name">hr.contract.view.form.inherit</field>
<field name="model">hr.contract</field>
<field name="inherit_id" ref="hr_contract.hr_contract_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='wage']" position="after">
<field name="struct_id" required="1"/>
</xpath>
<xpath expr="//field[@name='resource_calendar_id']" position="after">
<field name="schedule_pay"/>
</xpath>
</field>
</record>
<!-- End Contract View-->
<!-- Salary structure -->
<record id="view_hr_payroll_structure_list_view" model="ir.ui.view">
<field name="name">hr.payroll.structure.tree</field>
<field name="model">hr.payroll.structure</field>
<field name="arch" type="xml">
<tree string="Employee Function">
<field name="name"/>
<field name="code"/>
<field name="rule_ids"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
</tree>
</field>
</record>
<record id="view_hr_payroll_structure_tree" model="ir.ui.view">
<field name="name">hr.payroll.structure.tree</field>
<field name="model">hr.payroll.structure</field>
<field name="field_parent">children_ids</field>
<field name="arch" type="xml">
<tree string="Salary Structure">
<field name="name"/>
<field name="code"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
</tree>
</field>
</record>
<record id="view_hr_payroll_structure_filter" model="ir.ui.view">
<field name="name">hr.payroll.structure.select</field>
<field name="model">hr.payroll.structure</field>
<field name="arch" type="xml">
<search string="Payroll Structures">
<field name="name" string="Payroll Structures" filter_domain="['|',('name','ilike',self),('code','ilike',self)]"/>
</search>
</field>
</record>
<record id="view_hr_employee_grade_form" model="ir.ui.view">
<field name="name">hr.payroll.structure.form</field>
<field name="model">hr.payroll.structure</field>
<field name="arch" type="xml">
<form string="Employee Function">
<group col="4">
<field name="name"/>
<field name="code"/>
<field name="parent_id"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
</group>
<notebook colspan="4">
<page string="Salary Rules">
<field name="rule_ids" domain="[('parent_rule_id','=',False)]">
<tree>
<field name="name"/>
<field name="code"/>
<field name="category_id"/>
<field name="sequence" invisible="1"/>
<field name="register_id"/>
</tree>
</field>
</page>
</notebook>
</form>
</field>
</record>
<record id="action_view_hr_payroll_structure_list_form" model="ir.actions.act_window">
<field name="name">Salary Structures</field>
<field name="res_model">hr.payroll.structure</field>
<field name="view_type">form</field>
<field name="view_id" ref="view_hr_payroll_structure_list_view"/>
</record>
<menuitem
id="menu_hr_payroll_structure_view"
action="action_view_hr_payroll_structure_list_form"
parent="menu_hr_payroll_configuration"
sequence="2"
/>
<record id="action_view_hr_payroll_structure_tree" model="ir.actions.act_window">
<field name="name">Salary Structures Hierarchy</field>
<field name="res_model">hr.payroll.structure</field>
<field name="view_type">tree</field>
<field name="domain">[('parent_id','=',False)]</field>
<field name="view_id" ref="view_hr_payroll_structure_tree"/>
</record>
<menuitem
id="menu_hr_payroll_structure_tree"
action="action_view_hr_payroll_structure_tree"
parent="menu_hr_payroll_configuration"
sequence="2"
/>
<!-- End Salary structure -->
<!-- Payslip Line -->
<record id="view_hr_payslip_line_tree" model="ir.ui.view">
<field name="name">hr.payslip.line.tree</field>
<field name="model">hr.payslip.line</field>
<field name="arch" type="xml">
<tree string="Salary Structure" editable="bottom" decoration-info="total == 0">
<field name="category_id"/>
<field name="employee_id" invisible="1"/>
<field name="sequence"/>
<field name="name"/>
<field name="code"/>
<field name="quantity"/>
<field name="rate"/>
<field name="amount"/>
<field name="total"/>
<field name="amount_select" invisible="1"/>
<field name="register_id" invisible="1"/>
</tree>
</field>
</record>
<record id="view_hr_payslip_line_form" model="ir.ui.view">
<field name="name">hr.payslip.line.form</field>
<field name="model">hr.payslip.line</field>
<field name="arch" type="xml">
<form string="Payslip Line">
<group>
<group>
<field name="name"/>
<field name="code"/>
<field name="slip_id"/>
<field name="employee_id"/>
</group>
<group string="Calculations">
<field name="category_id"/>
<field name="amount_select"/>
<field name="amount_fix" attrs="{'readonly':[('amount_select','!=','fix')]}"/>
<field name="amount_percentage" attrs="{'readonly':[('amount_select','!=','percentage')]}"/>
<field name="sequence"/>
</group>
<field name="note"/>
</group>
</form>
</field>
</record>
<record id="view_hr_payslip_line_filter" model="ir.ui.view">
<field name="name">hr.payslip.line.select</field>
<field name="model">hr.payslip.line</field>
<field name="arch" type="xml">
<search string="Search Payslip Lines">
<field name="name" string="Payslip Lines" filter_domain="['|',('name','ilike',self),('code','ilike',self)]"/>
<field name="amount_select"/>
<field name="slip_id"/>
<group col="8" colspan="4" expand="0" string="Group By">
<filter string="Salary Rule Category" name="category_id" context="{'group_by':'category_id'}"/>
<filter string="Contribution Register" name="register_id" context="{'group_by':'register_id'}"/>
<filter string="Amount Type" name="amount_select" context="{'group_by':'amount_select'}"/>
<filter string="Employees" name="employee_id" context="{'group_by':'employee_id'}"/>
</group>
</search>
</field>
</record>
<!-- payslip -->
<record id="view_hr_payslip_tree" model="ir.ui.view">
<field name="name">hr.payslip.tree</field>
<field name="model">hr.payslip</field>
<field name="arch" type="xml">
<tree decoration-info="state in ('confirm','hr_check','accont_check')" decoration-muted="state == 'cancel'" string="Payslips">
<field name="number"/>
<field name="employee_id"/>
<field name="name"/>
<field name="date_from"/>
<field name="date_to"/>
<field name="state"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
<field name="payslip_run_id" invisible="1"/>
</tree>
</field>
</record>
<act_window name="Payslip Computation Details"
context="{'default_slip_id': active_id,'search_default_slip_id': active_id}"
res_model="hr.payslip.line"
src_model="hr.payslip"
id="act_payslip_lines"/>
<record id="view_hr_payslip_form" model="ir.ui.view">
<field name="name">hr.payslip.form</field>
<field name="model">hr.payslip</field>
<field name="arch" type="xml">
<form string="Payslip">
<header>
<button string="Confirm" name="action_payslip_done" type="object" states="draft" class="oe_highlight"/>
<button string="Refund" name="refund_sheet" states="confirm,done" type='object' />
<button string="Set to Draft" name="action_payslip_draft" type="object" states="cancel"/>
<button string="Compute Sheet" name="compute_sheet" type="object" states="draft" class="oe_highlight"/>
<button string="Cancel Payslip" name="action_payslip_cancel" type="object" states="draft,hr_check,confirm,verify"/>
<field name="state" widget="statusbar" statusbar_visible="draft,confirm"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="%(act_payslip_lines)d"
class="oe_stat_button"
icon="fa-money"
type="action">
<field name="payslip_count" widget="statinfo" string="Payslip" help="Payslip Computation Details"/>
</button>
</div>
<div class="oe_title">
<label for="employee_id" class="oe_edit_only"/>
<h1><field name="employee_id" placeholder="Employee"/></h1>
</div>
<group col="4">
<label for="date_from" string="Period"/>
<div>
<field name="date_from" class="oe_inline"/> - <field name="date_to" class="oe_inline"/>
</div>
<field name="contract_id" domain="[('employee_id','=',employee_id),('date_start','&lt;=',date_to),'|',('date_end','&gt;=',date_from),('date_end','=',False)]" context="{'default_employee_id': employee_id}"/>
<field name="number"/>
<field name="struct_id" attrs="{'required':[('contract_id','!=',False)]}"/>
<field name="name"/>
<field name="credit_note"/>
</group>
<notebook>
<page string="Worked Days &amp; Inputs">
<separator string="Worked Days"/>
<field name="worked_days_line_ids">
<tree string="Worked Days" editable="bottom">
<field name="name"/>
<field name="code"/>
<field name="number_of_days" sum="Total Working Days"/>
<field name="number_of_hours"/>
<field name="contract_id"/>
<field name="sequence" invisible="True"/>
</tree>
<form string="Worked Day">
<group col="4">
<field name="name"/>
<field name="code"/>
<field name="sequence"/>
<field name="number_of_days"/>
<field name="number_of_hours"/>
<field name="contract_id"/>
</group>
</form>
</field>
<separator string="Other Inputs"/>
<field name="input_line_ids" colspan="4" nolabel="1">
<tree string="Input Data" editable="bottom">
<field name="name"/>
<field name="code"/>
<field name="amount"/>
<field name="contract_id"/>
<field name="sequence" invisible="True"/>
</tree>
<form string="Payslip Line">
<group col="4">
<field name="name"/>
<field name="code"/>
<field name="sequence"/>
<field name="amount"/>
<field name="contract_id"/>
</group>
</form>
</field>
</page>
<page string="Salary Computation" >
<field name="line_ids" colspan="4" nolabel="1">
<tree string="Salary Structure" editable="bottom" decoration-info="total == 0">
<field name="name"/>
<field name="code"/>
<field name="category_id"/>
<field name="sequence" invisible="1"/>
<field name="quantity"/>
<field name="rate"/>
<field name="salary_rule_id"/>
<field name="amount"/>
<field name="total"/>
</tree>
<form string="Payslip Line">
<group col="4">
<field name="name"/>
<field name="code"/>
<field name="category_id"/>
<field name="sequence"/>
<field name="quantity"/>
<field name="rate"/>
<field name="amount"/>
<field name="total"/>
<field name="salary_rule_id"/>
</group>
</form>
</field>
</page>
<page string="Details By Salary Rule Category">
<field name="details_by_salary_rule_category" context="{'group_by':'category_id'}" domain="[('appears_on_payslip', '=', True)]">
<tree string="Payslip Lines" decoration-info="total == 0">
<field name="category_id"/>
<field name="name"/>
<field name="code"/>
<field name="total"/>
</tree>
</field>
</page>
<page string="Accounting Information">
<group>
<group string="Miscellaneous">
<field name="company_id" groups="base.group_multi_company"/>
<field name="payslip_run_id" domain="[('state','=','draft')]"/>
</group>
<group name="accounting" string="Accounting">
<field name="paid" readonly="1"/>
</group>
</group>
<div colspan="4">
<field name="note" placeholder="Add an internal note..."/>
</div>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_hr_payslip_filter" model="ir.ui.view">
<field name="name">hr.payslip.select</field>
<field name="model">hr.payslip</field>
<field name="arch" type="xml">
<search string="Search Payslips">
<field name="name" string="Payslips" filter_domain="['|',('name','ilike',self),('number','ilike',self)]"/>
<field name="date_from"/>
<filter string="Draft" domain="[('state','=','draft')]" help="Draft Slip"/>
<filter string="Done" domain="[('state','=','done')]" help="Done Slip"/>
<field name="employee_id"/>
<field name="payslip_run_id"/>
<group expand="0" string="Group By">
<filter string="Employees" name="employee_id" context="{'group_by':'employee_id'}"/>
<filter string="PaySlip Batch" name="payslip_run_id" context="{'group_by':'payslip_run_id'}"/>
<filter string="Companies" name="company_id" groups="base.group_multi_company" context="{'group_by':'company_id'}"/>
<filter string="States" name="state" context="{'group_by':'state'}"/>
</group>
</search>
</field>
</record>
<record id="action_view_hr_payslip_form" model="ir.actions.act_window">
<field name="name">Employee Payslips</field>
<field name="res_model">hr.payslip</field>
<field name="view_type">form</field>
<field name="view_id" ref="view_hr_payslip_tree"/>
<field name="search_view_id" ref="view_hr_payslip_filter"/>
</record>
<menuitem action="action_view_hr_payslip_form" id="menu_department_tree" parent="menu_hr_payroll_root" groups="hr_payroll.group_hr_payroll_user"/>
<record id="act_hr_employee_payslip_list" model="ir.actions.act_window">
<field name="res_model">hr.payslip</field>
<field name="name">Payslips</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="context">{'search_default_employee_id': [active_id], 'default_employee_id': active_id}</field>
</record>
<!-- Employee View -->
<record id="payroll_hr_employee_view_form" model="ir.ui.view">
<field name="name">payroll.hr.employee.view.form</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml">
<button name="toggle_active" position="before">
<button name="%(act_hr_employee_payslip_list)d"
class="oe_stat_button"
icon="fa-money"
type="action"
groups="hr_payroll.group_hr_payroll_user">
<field name="payslip_count" widget="statinfo" string="Payslips"/>
</button>
</button>
</field>
</record>
<!-- Salary Category-->
<record id="hr_salary_rule_category_form" model="ir.ui.view">
<field name="name">hr.salary.rule.category.form</field>
<field name="model">hr.salary.rule.category</field>
<field name="arch" type="xml">
<form string="Salary Categories">
<group col="4">
<field name="name"/>
<field name="code"/>
<field name="parent_id"/>
</group>
<group string="Notes">
<field name="note" nolabel="1"/>
</group>
</form>
</field>
</record>
<record id="hr_salary_rule_category_tree" model="ir.ui.view">
<field name="name">hr.salary.rule.category.tree</field>
<field name="model">hr.salary.rule.category</field>
<field name="arch" type="xml">
<tree string="Salary Rule Categories">
<field name="name"/>
<field name="code"/>
<field name="parent_id" invisible="1"/>
</tree>
</field>
</record>
<record id="hr_salary_rule_category_tree_view" model="ir.ui.view">
<field name="name">hr.salary.rule.category.tree.view</field>
<field name="model">hr.salary.rule.category</field>
<field name="field_parent">children_ids</field>
<field name="arch" type="xml">
<tree string="Salary Rule Categories">
<field name="name"/>
<field name="code"/>
<field name="parent_id" invisible="1"/>
</tree>
</field>
</record>
<record id="view_hr_salary_rule_category_filter" model="ir.ui.view">
<field name="name">hr.salary.rule.category.select</field>
<field name="model">hr.salary.rule.category</field>
<field name="arch" type="xml">
<search string="Salary Rule Categories">
<field name="name" string="Salary Rule Categories" filter_domain="['|',('name','ilike',self),('code','ilike',self)]"/>
</search>
</field>
</record>
<record id="action_hr_salary_rule_category" model="ir.actions.act_window">
<field name="name">Salary Rule Categories</field>
<field name="res_model">hr.salary.rule.category</field>
<field name="view_type">form</field>
<field name="view_id" ref="hr_salary_rule_category_tree"/>
<field name="search_view_id" ref="view_hr_salary_rule_category_filter"/>
</record>
<menuitem
id="menu_hr_salary_rule_category"
action="action_hr_salary_rule_category"
parent="menu_hr_payroll_configuration"
sequence="11"
groups="base.group_no_one"
/>
<record id="action_hr_salary_rule_category_tree_view" model="ir.actions.act_window">
<field name="name">Salary Rule Categories Hierarchy</field>
<field name="res_model">hr.salary.rule.category</field>
<field name="view_type">tree</field>
<field name="domain">[('parent_id','=',False)]</field>
<field name="view_id" ref="hr_salary_rule_category_tree_view"/>
</record>
<menuitem
id="menu_hr_salary_rule_category_tree_view"
action="action_hr_salary_rule_category_tree_view"
parent="menu_hr_payroll_configuration"
sequence="12"
groups="base.group_no_one"
/>
<!--
Contribution Register
-->
<record id="hr_contribution_register_tree" model="ir.ui.view">
<field name="name">hr.contribution.register.tree</field>
<field name="model">hr.contribution.register</field>
<field name="arch" type="xml">
<tree string="Contribution Registers">
<field name="name" />
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
</tree>
</field>
</record>
<record id="hr_contribution_register_filter" model="ir.ui.view">
<field name="name">hr.contribution.register.search</field>
<field name="model">hr.contribution.register</field>
<field name="arch" type="xml">
<search string="Contribution Registers">
<field name="name" string="Contribution Registers"/>
<field name="company_id" groups="base.group_multi_company"/>
</search>
</field>
</record>
<record id="hr_contribution_register_form" model="ir.ui.view">
<field name="name">hr.contribution.register.form</field>
<field name="model">hr.contribution.register</field>
<field name="arch" type="xml">
<form string="Contribution">
<group>
<field name="name"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}" class="oe_inline"/>
</group>
<newline/>
<group>
<separator string="Description"/><newline/>
<field name="note" nolabel="1"/></group>
</form>
</field>
</record>
<record id="action_contribution_register_form" model="ir.actions.act_window">
<field name="name">Contribution Registers</field>
<field name="res_model">hr.contribution.register</field>
<field name="view_type">form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to add a new contribution register.
</p><p>
A contribution register is a third party involved in the salary
payment of the employees. It can be the social security, the
estate or anyone that collect or inject money on payslips.
</p>
</field>
<field name="view_id" ref="hr_contribution_register_tree"/>
</record>
<menuitem
id="menu_action_hr_contribution_register_form"
action="action_contribution_register_form"
parent="menu_hr_payroll_configuration"
sequence="14"
/>
<!-- Salary Rules -->
<record id="hr_salary_rule_list" model="ir.ui.view">
<field name="name">hr.salary.rule.list</field>
<field name="model">hr.salary.rule</field>
<field name="arch" type="xml">
<tree string="Salary Rules">
<field name="name"/>
<field name="code"/>
<field name="category_id"/>
<field name="sequence" invisible="1"/>
<field name="register_id"/>
</tree>
</field>
</record>
<record id="hr_salary_rule_tree" model="ir.ui.view">
<field name="name">hr.salary.rule.tree</field>
<field name="model">hr.salary.rule</field>
<field name="field_parent">child_ids</field>
<field eval="20" name="priority"/>
<field name="arch" type="xml">
<tree string="Salary Rules">
<field name="name"/>
<field name="code"/>
<field name="category_id" invisible="True"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
</tree>
</field>
</record>
<record id="hr_salary_rule_form" model="ir.ui.view">
<field name="name">hr.salary.rule.form</field>
<field name="model">hr.salary.rule</field>
<field name="arch" type="xml">
<form string="Salary Rules">
<label for="name" class="oe_edit_only"/>
<h1><field name="name"/></h1>
<label for="category_id" class="oe_edit_only"/>
<h2><field name="category_id"/></h2>
<group col="4">
<field name="code"/>
<field name="sequence" />
<field name="active"/>
<field name="appears_on_payslip"/>
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
</group>
<notebook colspan="6">
<page string="General">
<group col="4">
<separator colspan="4" string="Conditions"/>
<field name="condition_select"/><newline/>
<field name="condition_python" attrs="{'invisible':[('condition_select','!=','python')], 'required': [('condition_select','=','python')]}" colspan="4"/> <newline/>
<field name="condition_range" attrs="{'invisible':[('condition_select','!=','range')], 'required':[('condition_select','=','range')]}"/><newline/>
<field name="condition_range_min" colspan="2" attrs="{'invisible':[('condition_select','!=','range')], 'required':[('condition_select','=','range')]}"/><newline/>
<field name="condition_range_max" colspan="2" attrs="{'invisible':[('condition_select','!=','range')], 'required':[('condition_select','=','range')]}"/><newline/>
<separator colspan="4" string="Computation"/>
<field name="amount_select"/><newline/>
<field name="amount_percentage_base" attrs="{'invisible':[('amount_select','!=','percentage')], 'required': [('amount_select','=','percentage')]}"/><newline/>
<field name="quantity" attrs="{'invisible':[('amount_select','=','code')], 'required':[('amount_select','!=','code')]}"/><newline/>
<field name="amount_fix" attrs="{'invisible':[('amount_select','!=','fix')], 'required':[('amount_select','=','fix')]}"/><newline/>
<field colspan="4" name="amount_python_compute" attrs="{'invisible':[('amount_select','!=','code')], 'required':[('amount_select','=','code')]}"/>
<field name="amount_percentage" attrs="{'invisible':[('amount_select','!=','percentage')], 'required':[('amount_select','=','percentage')]}"/>
<separator colspan="4" string="Company Contribution"/>
<field name="register_id"/>
</group>
</page>
<page name="rules" string="Child Rules">
<field name="parent_rule_id"/>
<separator string="Children Definition"/>
<field name="child_ids"/>
</page>
<page string="Inputs">
<field name="input_ids" mode="tree">
<tree string="Input Data" editable="bottom">
<field name="name"/>
<field name="code"/>
</tree>
</field>
</page>
<page string="Description">
<field name="note"/>
</page>
</notebook>
</form>
</field>
</record>
<record id="view_hr_rule_filter" model="ir.ui.view">
<field name="name">hr.salary.rule.select</field>
<field name="model">hr.salary.rule</field>
<field name="arch" type="xml">
<search string="Search Salary Rule">
<field name="name" string="Salary Rules" filter_domain="['|',('name','ilike',self),('code','ilike',self)]"/>
<field name="category_id"/>
<field name="condition_range_min"/>
<group col="8" colspan="4" expand="0" string="Group By">
<filter string="Category" name="head" context="{'group_by':'category_id'}"/>
</group>
</search>
</field>
</record>
<record id="action_salary_rule_form" model="ir.actions.act_window">
<field name="name">Salary Rules</field>
<field name="res_model">hr.salary.rule</field>
<field name="view_type">form</field>
<field name="view_id" ref="hr_salary_rule_list"/>
<field name="domain">[('parent_rule_id','=',False)]</field>
<field name="search_view_id" ref="view_hr_rule_filter"/>
</record>
<menuitem id="menu_action_hr_salary_rule_form" action="action_salary_rule_form" parent="menu_hr_payroll_configuration" sequence="12"/>
<act_window name="All Children Rules"
domain="[('parent_rule_id', '=', active_id)]"
res_model="hr.salary.rule"
src_model="hr.salary.rule"
view_id="hr_salary_rule_list"
id="act_children_salary_rules"/>
<!-- payslip runs -->
<record id="hr_payslip_run_filter" model="ir.ui.view">
<field name="name">hr.payslip.run.search</field>
<field name="model">hr.payslip.run</field>
<field name="arch" type="xml">
<search string="Search Payslip Batches">
<field name="name" string="Payslip Batches"/>
<field name="date_start"/>
<field name="date_end"/>
<filter string="Draft" domain="[('state','=','draft')]" help="Draft Payslip Batches"/>
<filter name="done_filter" string="Done" domain="[('state','=','close')]" help="Done Payslip Batches"/>
</search>
</field>
</record>
<record id="hr_payslip_run_tree" model="ir.ui.view">
<field name="name">hr.payslip.run.tree</field>
<field name="model">hr.payslip.run</field>
<field name="arch" type="xml">
<tree string="Payslips Batches">
<field name="name"/>
<field name="date_start"/>
<field name="date_end"/>
<field name="credit_note"/>
<field name="state"/>
</tree>
</field>
</record>
<record id="hr_payslip_run_form" model="ir.ui.view">
<field name="name">hr.payslip.run.form</field>
<field name="model">hr.payslip.run</field>
<field name="arch" type="xml">
<form string="Payslips Batches">
<header>
<button name="close_payslip_run" type="object" string="Close" states="draft" class="oe_highlight"/>
<button name="%(action_hr_payslip_by_employees)d" type="action" states="draft" string="Generate Payslips" class="oe_highlight"/>
<button string="Set to Draft" name="draft_payslip_run" type="object" states="close" />
<field name="state" widget="statusbar"/>
</header>
<sheet>
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name"/>
</h1>
<group col="4">
<label for="date_start" string="Period"/>
<div>
<field name="date_start" class="oe_inline"/> - <field name="date_end" class="oe_inline"/>
</div>
<field name="credit_note"/>
</group>
<separator string="Payslips"/>
<field name="slip_ids"/>
</sheet>
</form>
</field>
</record>
<record id="action_hr_payslip_run_tree" model="ir.actions.act_window">
<field name="name">Payslips Batches</field>
<field name="res_model">hr.payslip.run</field>
<field name="view_type">form</field>
<field name="view_id" ref="hr_payslip_run_tree"/>
<field name="search_view_id" ref="hr_payslip_run_filter"/>
</record>
<menuitem action="action_hr_payslip_run_tree" id="menu_hr_payslip_run" parent="menu_hr_payroll_root"/>
<!-- Shortcuts -->
<act_window name="Payslip Lines"
domain="[('register_id', '=', active_id)]"
context="{'default_register_id': active_id, 'search_default_register_id': 1}"
res_model="hr.payslip.line"
src_model="hr.contribution.register"
id="act_contribution_reg_payslip_lines"/>
</odoo>
@@ -0,0 +1,346 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Payslip Line -->
<record id="view_hr_payslip_line_tree" model="ir.ui.view">
<field name="name">hr.payslip.line.tree</field>
<field name="model">hr.payslip.line</field>
<field name="arch" type="xml">
<tree string="Salary Structure" editable="bottom" decoration-info="total == 0">
<field name="category_id"/>
<field name="employee_id" invisible="1"/>
<field name="sequence"/>
<field name="name"/>
<field name="code"/>
<field name="quantity"/>
<field name="rate"/>
<field name="amount"/>
<field name="total"/>
<field name="amount_select" invisible="1"/>
<field name="register_id" invisible="1"/>
</tree>
</field>
</record>
<record id="view_hr_payslip_line_form" model="ir.ui.view">
<field name="name">hr.payslip.line.form</field>
<field name="model">hr.payslip.line</field>
<field name="arch" type="xml">
<form string="Payslip Line">
<group>
<group>
<field name="name"/>
<field name="code"/>
<field name="slip_id"/>
<field name="employee_id"/>
</group>
<group string="Calculations">
<field name="category_id"/>
<field name="amount_select"/>
<field name="amount_fix" attrs="{'readonly':[('amount_select','!=','fix')]}"/>
<field name="amount_percentage" attrs="{'readonly':[('amount_select','!=','percentage')]}"/>
<field name="sequence"/>
</group>
<field name="note"/>
</group>
</form>
</field>
</record>
<record id="view_hr_payslip_line_filter" model="ir.ui.view">
<field name="name">hr.payslip.line.select</field>
<field name="model">hr.payslip.line</field>
<field name="arch" type="xml">
<search string="Search Payslip Lines">
<field name="name" string="Payslip Lines" filter_domain="['|',('name','ilike',self),('code','ilike',self)]"/>
<field name="amount_select"/>
<field name="slip_id"/>
<group col="8" colspan="4" expand="0" string="Group By">
<filter string="Salary Rule Category" name="category_id" context="{'group_by':'category_id'}"/>
<filter string="Contribution Register" name="register_id" context="{'group_by':'register_id'}"/>
<filter string="Amount Type" name="amount_select" context="{'group_by':'amount_select'}"/>
<filter string="Employees" name="employee_id" context="{'group_by':'employee_id'}"/>
</group>
</search>
</field>
</record>
<!-- payslip -->
<record id="view_hr_payslip_tree" model="ir.ui.view">
<field name="name">hr.payslip.tree</field>
<field name="model">hr.payslip</field>
<field name="arch" type="xml">
<tree decoration-info="state in ('confirm','hr_check','accont_check')" decoration-muted="state == 'cancel'" string="Payslips">
<field name="number"/>
<field name="employee_id"/>
<field name="name"/>
<field name="date_from"/>
<field name="date_to"/>
<field name="state"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
<field name="payslip_run_id" invisible="1"/>
</tree>
</field>
</record>
<act_window name="Payslip Computation Details"
context="{'default_slip_id': active_id,'search_default_slip_id': active_id}"
res_model="hr.payslip.line"
src_model="hr.payslip"
id="act_payslip_lines"/>
<record id="view_hr_payslip_form" model="ir.ui.view">
<field name="name">hr.payslip.form</field>
<field name="model">hr.payslip</field>
<field name="arch" type="xml">
<form string="Payslip">
<header>
<button string="Confirm" name="action_payslip_done" type="object" states="draft" class="oe_highlight"/>
<button string="Refund" name="refund_sheet" states="confirm,done" type='object' />
<button string="Set to Draft" name="action_payslip_draft" type="object" states="cancel"/>
<button string="Compute Sheet" name="compute_sheet" type="object" states="draft" class="oe_highlight"/>
<button string="Cancel Payslip" name="action_payslip_cancel" type="object" states="draft,hr_check,confirm,verify"/>
<field name="state" widget="statusbar" statusbar_visible="draft,confirm"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="%(act_payslip_lines)d"
class="oe_stat_button"
icon="fa-money"
type="action">
<field name="payslip_count" widget="statinfo" string="Payslip" help="Payslip Computation Details"/>
</button>
</div>
<div class="oe_title">
<label for="employee_id" class="oe_edit_only"/>
<h1><field name="employee_id" placeholder="Employee"/></h1>
</div>
<group col="4">
<label for="date_from" string="Period"/>
<div>
<field name="date_from" class="oe_inline"/> - <field name="date_to" class="oe_inline"/>
</div>
<field name="contract_id" domain="[('employee_id','=',employee_id),('date_start','&lt;=',date_to),'|',('date_end','&gt;=',date_from),('date_end','=',False)]" context="{'default_employee_id': employee_id}"/>
<field name="number"/>
<field name="struct_id" attrs="{'required':[('contract_id','!=',False)]}"/>
<field name="name"/>
<field name="credit_note"/>
</group>
<notebook>
<page string="Worked Days &amp; Inputs">
<separator string="Worked Days"/>
<field name="worked_days_line_ids">
<tree string="Worked Days" editable="bottom">
<field name="name"/>
<field name="code"/>
<field name="number_of_days" sum="Total Working Days"/>
<field name="number_of_hours"/>
<field name="contract_id"/>
<field name="sequence" invisible="True"/>
</tree>
<form string="Worked Day">
<group col="4">
<field name="name"/>
<field name="code"/>
<field name="sequence"/>
<field name="number_of_days"/>
<field name="number_of_hours"/>
<field name="contract_id"/>
</group>
</form>
</field>
<separator string="Other Inputs"/>
<field name="input_line_ids" colspan="4" nolabel="1">
<tree string="Input Data" editable="bottom">
<field name="name"/>
<field name="code"/>
<field name="amount"/>
<field name="contract_id"/>
<field name="sequence" invisible="True"/>
</tree>
<form string="Payslip Line">
<group col="4">
<field name="name"/>
<field name="code"/>
<field name="sequence"/>
<field name="amount"/>
<field name="contract_id"/>
</group>
</form>
</field>
</page>
<page string="Salary Computation" >
<field name="line_ids" colspan="4" nolabel="1">
<tree string="Salary Structure" editable="bottom" decoration-info="total == 0">
<field name="name"/>
<field name="code"/>
<field name="category_id"/>
<field name="sequence" invisible="1"/>
<field name="quantity"/>
<field name="rate"/>
<field name="salary_rule_id"/>
<field name="amount"/>
<field name="total"/>
</tree>
<form string="Payslip Line">
<group col="4">
<field name="name"/>
<field name="code"/>
<field name="category_id"/>
<field name="sequence"/>
<field name="quantity"/>
<field name="rate"/>
<field name="amount"/>
<field name="total"/>
<field name="salary_rule_id"/>
</group>
</form>
</field>
</page>
<page string="Details By Salary Rule Category">
<field name="details_by_salary_rule_category" context="{'group_by':'category_id'}" domain="[('appears_on_payslip', '=', True)]">
<tree string="Payslip Lines" decoration-info="total == 0">
<field name="category_id"/>
<field name="name"/>
<field name="code"/>
<field name="total"/>
</tree>
</field>
</page>
<page string="Accounting Information">
<group>
<group string="Miscellaneous">
<field name="company_id" groups="base.group_multi_company"/>
<field name="payslip_run_id" domain="[('state','=','draft')]"/>
</group>
<group name="accounting" string="Accounting">
<field name="paid" readonly="1"/>
</group>
</group>
<div colspan="4">
<field name="note" placeholder="Add an internal note..."/>
</div>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_hr_payslip_filter" model="ir.ui.view">
<field name="name">hr.payslip.select</field>
<field name="model">hr.payslip</field>
<field name="arch" type="xml">
<search string="Search Payslips">
<field name="name" string="Payslips" filter_domain="['|',('name','ilike',self),('number','ilike',self)]"/>
<field name="date_from"/>
<filter string="Draft" domain="[('state','=','draft')]" help="Draft Slip"/>
<filter string="Done" domain="[('state','=','done')]" help="Done Slip"/>
<field name="employee_id"/>
<field name="payslip_run_id"/>
<group expand="0" string="Group By">
<filter string="Employees" name="employee_id" context="{'group_by':'employee_id'}"/>
<filter string="PaySlip Batch" name="payslip_run_id" context="{'group_by':'payslip_run_id'}"/>
<filter string="Companies" name="company_id" groups="base.group_multi_company" context="{'group_by':'company_id'}"/>
<filter string="States" name="state" context="{'group_by':'state'}"/>
</group>
</search>
</field>
</record>
<record id="action_view_hr_payslip_form" model="ir.actions.act_window">
<field name="name">Employee Payslips</field>
<field name="res_model">hr.payslip</field>
<field name="view_type">form</field>
<field name="view_id" ref="view_hr_payslip_tree"/>
<field name="search_view_id" ref="view_hr_payslip_filter"/>
</record>
<menuitem action="action_view_hr_payslip_form" id="menu_department_tree" parent="menu_hr_payroll_root" groups="hr_payroll.group_hr_payroll_user"/>
<record id="act_hr_employee_payslip_list" model="ir.actions.act_window">
<field name="res_model">hr.payslip</field>
<field name="name">Payslips</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="context">{'search_default_employee_id': [active_id], 'default_employee_id': active_id}</field>
</record>
<!-- payslip runs -->
<record id="hr_payslip_run_filter" model="ir.ui.view">
<field name="name">hr.payslip.run.search</field>
<field name="model">hr.payslip.run</field>
<field name="arch" type="xml">
<search string="Search Payslip Batches">
<field name="name" string="Payslip Batches"/>
<field name="date_start"/>
<field name="date_end"/>
<filter string="Draft" domain="[('state','=','draft')]" help="Draft Payslip Batches"/>
<filter name="done_filter" string="Done" domain="[('state','=','close')]" help="Done Payslip Batches"/>
</search>
</field>
</record>
<record id="hr_payslip_run_tree" model="ir.ui.view">
<field name="name">hr.payslip.run.tree</field>
<field name="model">hr.payslip.run</field>
<field name="arch" type="xml">
<tree string="Payslips Batches">
<field name="name"/>
<field name="date_start"/>
<field name="date_end"/>
<field name="credit_note"/>
<field name="state"/>
</tree>
</field>
</record>
<record id="hr_payslip_run_form" model="ir.ui.view">
<field name="name">hr.payslip.run.form</field>
<field name="model">hr.payslip.run</field>
<field name="arch" type="xml">
<form string="Payslips Batches">
<header>
<button name="close_payslip_run" type="object" string="Close" states="draft" class="oe_highlight"/>
<button name="%(action_hr_payslip_by_employees)d" type="action" states="draft" string="Generate Payslips" class="oe_highlight"/>
<button string="Set to Draft" name="draft_payslip_run" type="object" states="close" />
<field name="state" widget="statusbar"/>
</header>
<sheet>
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name"/>
</h1>
<group col="4">
<label for="date_start" string="Period"/>
<div>
<field name="date_start" class="oe_inline"/> - <field name="date_end" class="oe_inline"/>
</div>
<field name="credit_note"/>
</group>
<separator string="Payslips"/>
<field name="slip_ids"/>
</sheet>
</form>
</field>
</record>
<record id="action_hr_payslip_run_tree" model="ir.actions.act_window">
<field name="name">Payslips Batches</field>
<field name="res_model">hr.payslip.run</field>
<field name="view_type">form</field>
<field name="view_id" ref="hr_payslip_run_tree"/>
<field name="search_view_id" ref="hr_payslip_run_filter"/>
</record>
<menuitem action="action_hr_payslip_run_tree" id="menu_hr_payslip_run" parent="menu_hr_payroll_root"/>
<!-- Shortcuts -->
<act_window name="Payslip Lines"
domain="[('register_id', '=', active_id)]"
context="{'default_register_id': active_id, 'search_default_register_id': 1}"
res_model="hr.payslip.line"
src_model="hr.contribution.register"
id="act_contribution_reg_payslip_lines"/>
</odoo>
@@ -0,0 +1,356 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Salary structure -->
<record id="view_hr_payroll_structure_list_view" model="ir.ui.view">
<field name="name">hr.payroll.structure.tree</field>
<field name="model">hr.payroll.structure</field>
<field name="arch" type="xml">
<tree string="Employee Function">
<field name="name"/>
<field name="code"/>
<field name="rule_ids"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
</tree>
</field>
</record>
<record id="view_hr_payroll_structure_tree" model="ir.ui.view">
<field name="name">hr.payroll.structure.tree</field>
<field name="model">hr.payroll.structure</field>
<field name="field_parent">children_ids</field>
<field name="arch" type="xml">
<tree string="Salary Structure">
<field name="name"/>
<field name="code"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
</tree>
</field>
</record>
<record id="view_hr_payroll_structure_filter" model="ir.ui.view">
<field name="name">hr.payroll.structure.select</field>
<field name="model">hr.payroll.structure</field>
<field name="arch" type="xml">
<search string="Payroll Structures">
<field name="name" string="Payroll Structures" filter_domain="['|',('name','ilike',self),('code','ilike',self)]"/>
</search>
</field>
</record>
<record id="view_hr_employee_grade_form" model="ir.ui.view">
<field name="name">hr.payroll.structure.form</field>
<field name="model">hr.payroll.structure</field>
<field name="arch" type="xml">
<form string="Employee Function">
<group col="4">
<field name="name"/>
<field name="code"/>
<field name="parent_id"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
</group>
<notebook colspan="4">
<page string="Salary Rules">
<field name="rule_ids" domain="[('parent_rule_id','=',False)]">
<tree>
<field name="name"/>
<field name="code"/>
<field name="category_id"/>
<field name="sequence" invisible="1"/>
<field name="register_id"/>
</tree>
</field>
</page>
</notebook>
</form>
</field>
</record>
<record id="action_view_hr_payroll_structure_list_form" model="ir.actions.act_window">
<field name="name">Salary Structures</field>
<field name="res_model">hr.payroll.structure</field>
<field name="view_type">form</field>
<field name="view_id" ref="view_hr_payroll_structure_list_view"/>
</record>
<menuitem
id="menu_hr_payroll_structure_view"
action="action_view_hr_payroll_structure_list_form"
parent="menu_hr_payroll_configuration"
sequence="2"
/>
<record id="action_view_hr_payroll_structure_tree" model="ir.actions.act_window">
<field name="name">Salary Structures Hierarchy</field>
<field name="res_model">hr.payroll.structure</field>
<field name="view_type">tree</field>
<field name="domain">[('parent_id','=',False)]</field>
<field name="view_id" ref="view_hr_payroll_structure_tree"/>
</record>
<menuitem
id="menu_hr_payroll_structure_tree"
action="action_view_hr_payroll_structure_tree"
parent="menu_hr_payroll_configuration"
sequence="2"
/>
<!-- Salary Category-->
<record id="hr_salary_rule_category_form" model="ir.ui.view">
<field name="name">hr.salary.rule.category.form</field>
<field name="model">hr.salary.rule.category</field>
<field name="arch" type="xml">
<form string="Salary Categories">
<group col="4">
<field name="name"/>
<field name="code"/>
<field name="parent_id"/>
</group>
<group string="Notes">
<field name="note" nolabel="1"/>
</group>
</form>
</field>
</record>
<record id="hr_salary_rule_category_tree" model="ir.ui.view">
<field name="name">hr.salary.rule.category.tree</field>
<field name="model">hr.salary.rule.category</field>
<field name="arch" type="xml">
<tree string="Salary Rule Categories">
<field name="name"/>
<field name="code"/>
<field name="parent_id" invisible="1"/>
</tree>
</field>
</record>
<record id="hr_salary_rule_category_tree_view" model="ir.ui.view">
<field name="name">hr.salary.rule.category.tree.view</field>
<field name="model">hr.salary.rule.category</field>
<field name="field_parent">children_ids</field>
<field name="arch" type="xml">
<tree string="Salary Rule Categories">
<field name="name"/>
<field name="code"/>
<field name="parent_id" invisible="1"/>
</tree>
</field>
</record>
<record id="view_hr_salary_rule_category_filter" model="ir.ui.view">
<field name="name">hr.salary.rule.category.select</field>
<field name="model">hr.salary.rule.category</field>
<field name="arch" type="xml">
<search string="Salary Rule Categories">
<field name="name" string="Salary Rule Categories" filter_domain="['|',('name','ilike',self),('code','ilike',self)]"/>
</search>
</field>
</record>
<record id="action_hr_salary_rule_category" model="ir.actions.act_window">
<field name="name">Salary Rule Categories</field>
<field name="res_model">hr.salary.rule.category</field>
<field name="view_type">form</field>
<field name="view_id" ref="hr_salary_rule_category_tree"/>
<field name="search_view_id" ref="view_hr_salary_rule_category_filter"/>
</record>
<menuitem
id="menu_hr_salary_rule_category"
action="action_hr_salary_rule_category"
parent="menu_hr_payroll_configuration"
sequence="11"
groups="base.group_no_one"
/>
<record id="action_hr_salary_rule_category_tree_view" model="ir.actions.act_window">
<field name="name">Salary Rule Categories Hierarchy</field>
<field name="res_model">hr.salary.rule.category</field>
<field name="view_type">tree</field>
<field name="domain">[('parent_id','=',False)]</field>
<field name="view_id" ref="hr_salary_rule_category_tree_view"/>
</record>
<menuitem
id="menu_hr_salary_rule_category_tree_view"
action="action_hr_salary_rule_category_tree_view"
parent="menu_hr_payroll_configuration"
sequence="12"
groups="base.group_no_one"
/>
<!--
Contribution Register
-->
<record id="hr_contribution_register_tree" model="ir.ui.view">
<field name="name">hr.contribution.register.tree</field>
<field name="model">hr.contribution.register</field>
<field name="arch" type="xml">
<tree string="Contribution Registers">
<field name="name" />
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
</tree>
</field>
</record>
<record id="hr_contribution_register_filter" model="ir.ui.view">
<field name="name">hr.contribution.register.search</field>
<field name="model">hr.contribution.register</field>
<field name="arch" type="xml">
<search string="Contribution Registers">
<field name="name" string="Contribution Registers"/>
<field name="company_id" groups="base.group_multi_company"/>
</search>
</field>
</record>
<record id="hr_contribution_register_form" model="ir.ui.view">
<field name="name">hr.contribution.register.form</field>
<field name="model">hr.contribution.register</field>
<field name="arch" type="xml">
<form string="Contribution">
<group>
<field name="name"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}" class="oe_inline"/>
</group>
<newline/>
<group>
<separator string="Description"/><newline/>
<field name="note" nolabel="1"/></group>
</form>
</field>
</record>
<record id="action_contribution_register_form" model="ir.actions.act_window">
<field name="name">Contribution Registers</field>
<field name="res_model">hr.contribution.register</field>
<field name="view_type">form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to add a new contribution register.
</p><p>
A contribution register is a third party involved in the salary
payment of the employees. It can be the social security, the
estate or anyone that collect or inject money on payslips.
</p>
</field>
<field name="view_id" ref="hr_contribution_register_tree"/>
</record>
<menuitem
id="menu_action_hr_contribution_register_form"
action="action_contribution_register_form"
parent="menu_hr_payroll_configuration"
sequence="14"
/>
<!-- Salary Rules -->
<record id="hr_salary_rule_list" model="ir.ui.view">
<field name="name">hr.salary.rule.list</field>
<field name="model">hr.salary.rule</field>
<field name="arch" type="xml">
<tree string="Salary Rules">
<field name="name"/>
<field name="code"/>
<field name="category_id"/>
<field name="sequence" invisible="1"/>
<field name="register_id"/>
</tree>
</field>
</record>
<record id="hr_salary_rule_tree" model="ir.ui.view">
<field name="name">hr.salary.rule.tree</field>
<field name="model">hr.salary.rule</field>
<field name="field_parent">child_ids</field>
<field eval="20" name="priority"/>
<field name="arch" type="xml">
<tree string="Salary Rules">
<field name="name"/>
<field name="code"/>
<field name="category_id" invisible="True"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
</tree>
</field>
</record>
<record id="hr_salary_rule_form" model="ir.ui.view">
<field name="name">hr.salary.rule.form</field>
<field name="model">hr.salary.rule</field>
<field name="arch" type="xml">
<form string="Salary Rules">
<label for="name" class="oe_edit_only"/>
<h1><field name="name"/></h1>
<label for="category_id" class="oe_edit_only"/>
<h2><field name="category_id"/></h2>
<group col="4">
<field name="code"/>
<field name="sequence" />
<field name="active"/>
<field name="appears_on_payslip"/>
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
</group>
<notebook colspan="6">
<page string="General">
<group col="4">
<separator colspan="4" string="Conditions"/>
<field name="condition_select"/><newline/>
<field name="condition_python" attrs="{'invisible':[('condition_select','!=','python')], 'required': [('condition_select','=','python')]}" colspan="4"/> <newline/>
<field name="condition_range" attrs="{'invisible':[('condition_select','!=','range')], 'required':[('condition_select','=','range')]}"/><newline/>
<field name="condition_range_min" colspan="2" attrs="{'invisible':[('condition_select','!=','range')], 'required':[('condition_select','=','range')]}"/><newline/>
<field name="condition_range_max" colspan="2" attrs="{'invisible':[('condition_select','!=','range')], 'required':[('condition_select','=','range')]}"/><newline/>
<separator colspan="4" string="Computation"/>
<field name="amount_select"/><newline/>
<field name="amount_percentage_base" attrs="{'invisible':[('amount_select','!=','percentage')], 'required': [('amount_select','=','percentage')]}"/><newline/>
<field name="quantity" attrs="{'invisible':[('amount_select','=','code')], 'required':[('amount_select','!=','code')]}"/><newline/>
<field name="amount_fix" attrs="{'invisible':[('amount_select','!=','fix')], 'required':[('amount_select','=','fix')]}"/><newline/>
<field colspan="4" name="amount_python_compute" attrs="{'invisible':[('amount_select','!=','code')], 'required':[('amount_select','=','code')]}"/>
<field name="amount_percentage" attrs="{'invisible':[('amount_select','!=','percentage')], 'required':[('amount_select','=','percentage')]}"/>
<separator colspan="4" string="Company Contribution"/>
<field name="register_id"/>
</group>
</page>
<page name="rules" string="Child Rules">
<field name="parent_rule_id"/>
<separator string="Children Definition"/>
<field name="child_ids"/>
</page>
<page string="Inputs">
<field name="input_ids" mode="tree">
<tree string="Input Data" editable="bottom">
<field name="name"/>
<field name="code"/>
</tree>
</field>
</page>
<page string="Description">
<field name="note"/>
</page>
</notebook>
</form>
</field>
</record>
<record id="view_hr_rule_filter" model="ir.ui.view">
<field name="name">hr.salary.rule.select</field>
<field name="model">hr.salary.rule</field>
<field name="arch" type="xml">
<search string="Search Salary Rule">
<field name="name" string="Salary Rules" filter_domain="['|',('name','ilike',self),('code','ilike',self)]"/>
<field name="category_id"/>
<field name="condition_range_min"/>
<group col="8" colspan="4" expand="0" string="Group By">
<filter string="Category" name="head" context="{'group_by':'category_id'}"/>
</group>
</search>
</field>
</record>
<record id="action_salary_rule_form" model="ir.actions.act_window">
<field name="name">Salary Rules</field>
<field name="res_model">hr.salary.rule</field>
<field name="view_type">form</field>
<field name="view_id" ref="hr_salary_rule_list"/>
<field name="domain">[('parent_rule_id','=',False)]</field>
<field name="search_view_id" ref="view_hr_rule_filter"/>
</record>
<menuitem id="menu_action_hr_salary_rule_form" action="action_salary_rule_form" parent="menu_hr_payroll_configuration" sequence="12"/>
<act_window name="All Children Rules"
domain="[('parent_rule_id', '=', active_id)]"
res_model="hr.salary.rule"
src_model="hr.salary.rule"
view_id="hr_salary_rule_list"
id="act_children_salary_rules"/>
</odoo>
+2 -1
View File
@@ -20,6 +20,7 @@ class Job(models.Model):
'hr.employee', related='department_id.manager_id', string="Department Manager",
readonly=True, store=True)
user_id = fields.Many2one('res.users', "Recruitment Responsible", track_visibility='onchange')
hr_responsible_id = fields.Many2one('res.users', "HR Responsible", track_visibility='onchange')
document_ids = fields.One2many('ir.attachment', compute='_compute_document_ids', string="Applications")
documents_count = fields.Integer(compute='_compute_document_ids', string="Documents")
alias_id = fields.Many2one(
@@ -47,7 +48,7 @@ class Job(models.Model):
@api.multi
def _compute_application_count(self):
read_group_result = self.env['hr.applicant'].read_group([('job_id', '=', self.id)], ['job_id'], ['job_id'])
read_group_result = self.env['hr.applicant'].read_group([('job_id', 'in', self.ids)], ['job_id'], ['job_id'])
result = dict((data['job_id'][0], data['job_id_count']) for data in read_group_result)
for job in self:
job.application_count = result.get(job.id, 0)
@@ -327,7 +327,7 @@ class Applicant(models.Model):
@api.multi
def _track_subtype(self, init_values):
record = self[0]
if 'emp_id' in init_values and record.emp_id:
if 'emp_id' in init_values and record.emp_id and record.emp_id.active:
return 'hr_recruitment.mt_applicant_hired'
elif 'stage_id' in init_values and record.stage_id and record.stage_id.sequence <= 1:
return 'hr_recruitment.mt_applicant_new'
@@ -26,7 +26,7 @@
</div>
</div>
<h2>Recruitment Process</h2>
<div class="row mt16 o_settings_container">
<div class="row mt16 o_settings_container" name="recruitment_process_div">
<div class="col-xs-12 col-md-6 o_setting_box" title="Use interview forms tailored to each job position during the recruitment process. Select the form to use in the job position detail form. This relies on Survey app.">
<div class="o_setting_left_pane">
<field name="module_hr_recruitment_survey"/>
@@ -448,6 +448,7 @@
</xpath>
<xpath expr="//field[@name='department_id']" position="after">
<field name="user_id"/>
<field name="hr_responsible_id"/>
</xpath>
<div name="button_box" position="inside">
<button class="oe_stat_button"
+1 -2
View File
@@ -20,9 +20,8 @@ Belgian Payroll Rules.
* Salary Maj, ONSS, Withholding Tax, Child Allowance, ...
""",
'demo': ['data/l10n_be_hr_payroll_demo.xml'],
'website': 'https://www.odoo.com/page/accounting',
'data':[
'data': [
'views/l10n_be_hr_payroll_view.xml',
'data/l10n_be_hr_payroll_data.xml',
'data/hr.salary.rule.csv',
File diff suppressed because it is too large Load Diff
@@ -44,14 +44,19 @@
<field name="code">ONSS</field>
<field name="parent_id" ref="hr_payroll.ALW"/>
</record>
<record id="hr_payroll_head_employment_bonus" model="hr.salary.rule.category">
<field name="name">Employment Bonus</field>
<field name="code">EmpBonus</field>
<field name="parent_id" ref="hr_payroll.ALW"/>
</record>
<record id="hr_payroll_head_div_impos" model="hr.salary.rule.category">
<field name="name">Misc. Taxable</field>
<field name="code">DIV. IMPOS</field>
<field name="parent_id" ref="hr_payroll.ALW"/>
</record>
<record id="hr_payroll_head_pp" model="hr.salary.rule.category">
<field name="name">Withholding Tax Grid 2011 (Precompte Professionnel Bareme 2011)</field>
<field name="code">P.P.</field>
<field name="name">Withholding Tax Grid 2017 (Precompte Professionnel Bareme 2017)</field>
<field name="code">PP</field>
<field name="parent_id" ref="hr_payroll.DED"/>
</record>
<record id="hr_payroll_head_div_net" model="hr.salary.rule.category">
@@ -61,10 +66,24 @@
</record>
<record id="hr_payroll_head_child_alw" model="hr.salary.rule.category">
<field name="name">Child Allowance Belgium</field>
<field name="code">Ch.A</field>
<field name="code">ChA</field>
<field name="parent_id" ref="hr_payroll.DED"/>
</record>
<record id="hr_salary_rule_category_family_reductions" model="hr.salary.rule.category">
<field name="name">Reduction for special family charges</field>
<field name="code">FamRed</field>
<field name="parent_id" ref="hr_payroll.DED"/>
</record>
<record id="hr_salary_rule_pp_reductions" model="hr.salary.rule.category">
<field name="name">Withholding Tax Reduction</field>
<field name="code">PPRed</field>
<field name="parent_id" ref="hr_payroll.DED"/>
</record>
<record id="hr_salary_rule_category_spec_soc_contribution" model="hr.salary.rule.category">
<field name="name">Special social contribution</field>
<field name="code">SSC</field>
<field name="parent_id" ref="hr_payroll.DED"/>
</record>
<!-- HR SALARY RULES-->
<record id="hr_payroll_rules_maj" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_salary"/>
@@ -76,11 +95,12 @@
</record>
<record id="hr_payroll_rules_employee" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_salary"/>
<field name="name">Employee Salary</field>
<field name="name">Gross Salary</field>
<field name="code">SALARY</field>
<field name="condition_select">none</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = contract.wage * 1</field>
<field name="amount_python_compute">result = contract.wage * 1 + contract.car_atn + contract.mobile + contract.mobile_plus + contract.internet</field>
<field name="sequence">20</field>
<field name="appears_on_payslip" eval="False"/>
</record>
<record id="hr_payroll_rules_onss_rule" model="hr.salary.rule">
@@ -93,19 +113,87 @@
<field name="amount_percentage_base">SALARY</field>
<field name="amount_percentage">-13.07</field>
<field name="register_id" ref="contrib_register_onss"/>
</record>
<record id="hr_payroll_rules_p_p_b1" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_pp"/>
<field name="name">Withholding Tax Grid 2011</field>
<field name="code">P.P</field>
<field name="sequence">120</field>
<field name="amount_select">fix</field>
<field name="condition_select">none</field>
<field name="appears_on_payslip" eval="False"/>
</record>
<record id="hr_salary_rule_employment_bonus_employees" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_employment_bonus"/>
<field name="name">Employment Bonus</field>
<field name="code">EmpBonus.1</field>
<field name="sequence">42</field>
<field name="condition_select">python</field>
<field name="condition_python">result = categories.BRUT &lt;= 2431.27</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">
salary = categories.BRUT
if salary &lt;= 1577.89:
result = 193.79
elif salary &lt;= 2461.27:
result = 193.79 - (0.2194 * (salary - 1577.89))
</field>
</record>
<record id="hr_salary_rule_employment_bonus_workers" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_employment_bonus"/>
<field name="name">Employment Bonus</field>
<field name="code">EmpBonus.2</field>
<field name="sequence">42</field>
<field name="condition_select">python</field>
<field name="condition_python">result = categories.BRUT + categories.ONSS &lt;= 2431.27</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">
salary = categories.BRUT
if salary &lt;= 1577.89:
result = 209.29
elif salary &lt;= 2461.27:
result = 209.29 - (0.2369 * (salary - 1577.89))
</field>
</record>
<record id="hr_payroll_rules_mis_ex_onss_1" model="hr.salary.rule">
<field name="category_id" ref="hr_salary_rule_category_spec_soc_contribution"/>
<field name="name">Special social cotisation</field>
<field name="code">M.ONSS.1</field>
<field name="amount_select">fix</field>
<field name="sequence">165</field>
<field name="condition_select">python</field>
<field name="appears_on_payslip" eval="False"/>
<field name="condition_python">result = (((employee.marital=='single') or (employee.marital=='married' and employee.spouse_fiscal_status=='without income')) and (employee.resident_bool!=True))</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">
if contract.wage &gt;= 0.01 and contract.wage &lt;= 1095.09:
result = 0.0
elif contract.wage &gt;= 1095.10 and contract.wage &lt;= 1945.38:
result = 0.0
elif contract.wage &gt;= 1945.39 and contract.wage &lt;= 2190.18:
result = -min((contract.wage - 1945.38) * 0.076, 18.60)
elif contract.wage &gt;= 2190.19 and contract.wage &lt;= 6038.82:
result = -min(18.60 + (contract.wage - 2190.18) * 0.011, 60.94)
else:
result = -60.94
</field>
</record>
<record id="hr_payroll_rules_mis_ex_onss_2" model="hr.salary.rule">
<field name="category_id" ref="hr_salary_rule_category_spec_soc_contribution"/>
<field name="name">Special social cotisation</field>
<field name="code">M.ONSS.2</field>
<field name="amount_select">fix</field>
<field name="sequence">165</field>
<field name="condition_select">python</field>
<field name="appears_on_payslip" eval="False"/>
<field name="condition_python">result = (((employee.marital=='married' and employee.spouse_fiscal_status=='with income')) and (employee.resident_bool!=True))</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">
if contract.wage &gt;= 0.01 and contract.wage &lt;= 1095.09:
result = 0.0
elif contract.wage &gt;= 1095.10 and contract.wage &lt;= 1945.38:
result = -9.30
elif contract.wage &gt;= 1945.39 and contract.wage &lt;= 2190.18:
result = -min(max((contract.wage - 1945.38) * 0.076, 9.30), 18.60)
elif contract.wage &gt;= 2190.19 and contract.wage &lt;= 6038.82:
result = -min(18.60 + (contract.wage - 2190.18) * 0.011, 51.64)
else:
result = -51.64
</field>
</record>
<record id="hr_payroll_rules_bareme" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_pp"/>
<field name="parent_rule_id" ref="hr_payroll_rules_p_p_b1"/>
<field name="name">Withholding Tax</field>
<field name="code">B.I.</field>
<field name="amount_select">fix</field>
@@ -116,7 +204,6 @@
</record>
<record id="hr_payroll_rules_baremeII" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_pp"/>
<field name="parent_rule_id" ref="hr_payroll_rules_p_p_b1"/>
<field name="name">Withholding Tax</field>
<field name="code">B.II</field>
<field name="amount_select">fix</field>
@@ -127,7 +214,6 @@
</record>
<record id="hr_payroll_rules_baremeIII" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_pp"/>
<field name="parent_rule_id" ref="hr_payroll_rules_p_p_b1"/>
<field name="name">Withholding Tax</field>
<field name="code">B.III</field>
<field name="amount_select">fix</field>
@@ -136,17 +222,63 @@
<field name="condition_python">result = employee.resident_bool==True</field>
<field name="appears_on_payslip" eval="False"/>
</record>
<record id="hr_salary_rule_pp_reduction_low_salary" model="hr.salary.rule">
<field name="category_id" ref="hr_salary_rule_pp_reductions"/>
<field name="name">Withholding Tax Reduction for Low Salaries</field>
<field name="code">PPRed.0</field>
<field name="sequence">131</field>
<field name="condition_select">python</field>
<field name="condition_python">result = categories.GROSS &lt;= 2027.86 and categories.GROSS &gt; 565.02</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">
result = 5.83
</field>
</record>
<record id="hr_salary_rule_pp_reduction_employees" model="hr.salary.rule">
<field name="category_id" ref="hr_salary_rule_pp_reductions"/>
<field name="name">Withholding Tax Reduction for Low Salaries (ONSS)</field>
<field name="code">PPRed.1</field>
<field name="sequence">132</field>
<field name="condition_select">python</field>
<field name="condition_python">result = categories.BRUT &lt;= 2431.27</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">
salary = categories.BRUT
percentage = 0.2803
if salary &lt;= 1577.89:
result = 193.79 * percentage
elif salary &lt;= 2461.27:
result = (193.79 - (0.2194 * (salary - 1577.89))) * percentage
</field>
</record>
<record id="hr_salary_rule_pp_reduction_workers" model="hr.salary.rule">
<field name="category_id" ref="hr_salary_rule_pp_reductions"/>
<field name="name">Withholding Tax Reduction for Low Salaries (ONSS)</field>
<field name="code">PPRed.2</field>
<field name="sequence">133</field>
<field name="condition_select">python</field>
<field name="condition_python">result = categories.BRUT &lt;= 2431.27</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">
salary = categories.BRUT
percentage = 0.2803
if salary &lt;= 1577.89:
result = 209.29 * percentage
elif salary &lt;= 2461.27:
result = (209.29 - (0.2369 * (salary - 1577.89))) * percentage
</field>
</record>
<record id="hr_payroll_rules_child_alw" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll.DED"/>
<field name="category_id" ref="hr_payroll_head_child_alw"/>
<field name="name">Child Allowance Belgium</field>
<field name="code">Child.Allowance</field>
<field name="sequence">140</field>
<field name="amount_select">fix</field>
<field name="condition_select">python</field>
<field name="condition_python">result = bool(employee.children)</field>
<field name="condition_python">result = bool(employee.dependent_children)</field>
</record>
<record id="hr_payroll_rules_child" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll.DED"/>
<field name="category_id" ref="hr_payroll_head_child_alw"/>
<field name="parent_rule_id" ref="hr_payroll_rules_child_alw"/>
<field name="name">Child Allowance Belgium</field>
<field name="code">Ch.A</field>
@@ -160,114 +292,195 @@
a) Reduction for dependent children</field>
<field name="condition_select">python</field>
<field name="condition_python">result = bool(employee.children)</field>
<field name="condition_python">result = bool(employee.dependent_children)</field>
</record>
<record id="hr_payroll_rules_child_handicap" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll.DED"/>
<field name="parent_rule_id" ref="hr_payroll_rules_child_alw"/>
<field name="name">Child Allowance for the Disabled Belgium</field>
<field name="code">Ch.Handicap</field>
<field name="amount_select">fix</field>
<record id="hr_salary_rule_other_family_charges_reductions" model="hr.salary.rule">
<field name="category_id" ref="hr_salary_rule_category_family_reductions"/>
<field name="name">Other family charges reductions</field>
<field name="code">Oth.Fam.Red</field>
<field name="sequence">143</field>
<field name="note">"Disabled child" means:
the child reaches at least 66% of failure or reduced physical or mental capacity
the head of one or more conditions;
the child that is established, regardless of age, that due to developments and recognized
before age 65</field>
<field name="condition_select">python</field>
<field name="condition_python">result = bool(employee.disabled_children_number)</field>
<field name="amount_select">fix</field>
<field name="condition_select">python</field>
<field name="condition_python">result = True</field>
</record>
<record id="hr_salary_rule_reduction_isolated" model="hr.salary.rule">
<field name="category_id" ref='hr_salary_rule_category_family_reductions'/>
<field name="parent_rule_id" ref="hr_salary_rule_other_family_charges_reductions"/>
<field name="name">Reduction for isolated people</field>
<field name="code">Red.Iso</field>
<field name="sequence">144</field>
<field name="condition_select">python</field>
<field name="condition_python">result = (employee.marital == 'single') and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA &lt; 0.0)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = min(25.00, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
</record>
<record id="hr_salary_rule_reduction_isolated_parent" model="hr.salary.rule">
<field name="category_id" ref='hr_salary_rule_category_family_reductions'/>
<field name="parent_rule_id" ref="hr_salary_rule_other_family_charges_reductions"/>
<field name="name">Reduction for isolated parent</field>
<field name="code">Red.Iso.Par</field>
<field name="sequence">144</field>
<field name="condition_select">python</field>
<field name="condition_python">result = (employee.marital == 'widower' or (employee.marital in ['divorced', 'single', 'widower'] and employee.dependent_children)) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA &lt; 0.0)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = min(35.00, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
</record>
<record id="hr_salary_rule_reduction_disabled" model="hr.salary.rule">
<field name="category_id" ref='hr_salary_rule_category_family_reductions'/>
<field name="parent_rule_id" ref="hr_salary_rule_other_family_charges_reductions"/>
<field name="name">Reduction for disabled employee</field>
<field name="code">Red.Dis</field>
<field name="sequence">145</field>
<field name="condition_select">python</field>
<field name="condition_python">result = (employee.disabled) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA &lt; 0.0)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = min(35.00, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
</record>
<record id="hr_salary_rule_reduction_seniors" model="hr.salary.rule">
<field name="category_id" ref='hr_salary_rule_category_family_reductions'/>
<field name="parent_rule_id" ref="hr_salary_rule_other_family_charges_reductions"/>
<field name="name">Reduction for dependent seniors</field>
<field name="code">Red.Seniors</field>
<field name="sequence">146</field>
<field name="condition_select">python</field>
<field name="condition_python">result = (employee.other_dependent_people and employee.dependent_seniors) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA &lt; 0.0)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = min(71.0 * employee.dependent_seniors, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
</record>
<record id="hr_salary_rule_reduction_juniors" model="hr.salary.rule">
<field name="category_id" ref='hr_salary_rule_category_family_reductions'/>
<field name="parent_rule_id" ref="hr_salary_rule_other_family_charges_reductions"/>
<field name="name">Reduction for dependent juniors</field>
<field name="code">Red.Juniors</field>
<field name="sequence">147</field>
<field name="condition_select">python</field>
<field name="condition_python">result = (employee.other_dependent_people and employee.dependent_juniors) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA &lt; 0.0)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = min(35.0 * employee.dependent_juniors, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
</record>
<record id="hr_salary_rule_reduction_insufficient_net" model="hr.salary.rule">
<field name="category_id" ref='hr_salary_rule_category_family_reductions'/>
<field name="parent_rule_id" ref="hr_salary_rule_other_family_charges_reductions"/>
<field name="name">Reduction for low spouse revenue</field>
<field name="code">Red.Spouse.Net</field>
<field name="sequence">148</field>
<field name="condition_select">python</field>
<field name="condition_python">result = (employee.marital == 'married' and employee.spouse_net_revenue &lt;= 221.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA &lt; 0.0)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = min(110.50, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
</record>
<record id="hr_salary_rule_reduction_insufficient_other_net" model="hr.salary.rule">
<field name="category_id" ref='hr_salary_rule_category_family_reductions'/>
<field name="parent_rule_id" ref="hr_salary_rule_other_family_charges_reductions"/>
<field name="name">Reduction for low spouse other revenue</field>
<field name="code">Red.Spouse.Oth.Net</field>
<field name="sequence">149</field>
<field name="condition_select">python</field>
<field name="condition_python">result = (employee.marital == 'married' and not employee.spouse_net_revenue and employee.spouse_other_net_revenue &lt;= 440.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA &lt; 0.0)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = min(220.0, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
</record>
<record id="hr_payroll_rules_spouse_handicap" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_div_net"/>
<field name="parent_rule_id" ref="hr_salary_rule_other_family_charges_reductions"/>
<field name="name">Disabled Spouse Allowance</field>
<field name="code">Sp.handicap</field>
<field name="amount_select">fix</field>
<field name="sequence">145</field>
<field name="amount_fix">31</field>
<field name="sequence">150</field>
<field name="condition_select">python</field>
<field name="condition_python">result = employee.disabled_spouse_bool==True</field>
</record>
<record id="hr_payroll_rules_company_car_parent" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_div_impos"/>
<field name="name">Company Car</field>
<field name="code">car</field>
<field name="amount_select">fix</field>
<field name="sequence">15</field>
<field name="condition_select">none</field>
<field name="appears_on_payslip" eval="False"/>
<field name="condition_python">result = (employee.disabled_spouse_bool==True) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA &lt; 0.0)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">min(35.0, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
</record>
<record id="hr_payroll_rules_parent_company_car" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_div_impos"/>
<field name="name">Company car</field>
<field name="parent_rule_id" ref="hr_payroll_rules_company_car_parent"/>
<field name="code">CAR 1</field>
<field name="name">ATN Company Car</field>
<field name="code">ATN.CAR</field>
<field name="sequence">15</field>
<field name="condition_select">python</field>
<field name="condition_python">result = bool(contract.car_employee_deduction)</field>
<field name="condition_python">
result = bool(contract.transport_mode == 'company_car' and contract.car_atn)
</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = contract.car_employee_deduction</field>
<field name="amount_python_compute">
result = contract.car_atn
</field>
</record>
<record id="hr_salary_rule_atn_internet" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_div_impos"/>
<field name="name">ATN Internet</field>
<field name="code">ATN.INT</field>
<field name="sequence">16</field>
<field name="condition_select">python</field>
<field name="condition_python">result = bool(contract.internet)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = 5.0</field>
</record>
<record id="hr_salary_rule_atn_mobile" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_div_impos"/>
<field name="name">ATN Mobile</field>
<field name="code">ATN.MOB</field>
<field name="sequence">17</field>
<field name="condition_select">python</field>
<field name="condition_python">result = bool(contract.mobile)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = 12.0</field>
</record>
<record id="hr_payroll_rules_company_car_2" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_div_net"/>
<field name="name">Retenue Company car</field>
<field name="parent_rule_id" ref="hr_payroll_rules_company_car_parent"/>
<field name="code">CAR 2</field>
<field name="name">ATN Company Car</field>
<field name="code">ATN.CAR.2</field>
<field name="sequence">160</field>
<field name="condition_select">python</field>
<field name="condition_python">result = contract.car_employee_deduction</field>
<field name="condition_python">
result = bool(contract.transport_mode == 'company_car' and contract.car_atn)
</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = -contract.car_employee_deduction</field>
<field name="amount_python_compute">
result = -contract.car_atn
</field>
</record>
<record id="hr_payroll_rules_company_car_emp" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll.COMP"/>
<field name="name">Company car, Company part</field>
<field name="parent_rule_id" ref="hr_payroll_rules_company_car_parent"/>
<field name="code">CAR 3</field>
<field name="sequence">160</field>
<field name="condition_select">python</field>
<field name="condition_python">result = bool(contract.car_company_amount)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = -contract.car_company_amount</field>
<field name="appears_on_payslip" eval="False"/>
</record>
<record id="hr_payroll_rules_parent_ch" model="hr.salary.rule">
<record id="hr_salary_rule_atn_internet_2" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_div_net"/>
<field name="name">Meal Voucher</field>
<field name="code">MEAL_V</field>
<field name="amount_select">fix</field>
<field name="sequence">165</field>
<field name="condition_select">none</field>
<field name="appears_on_payslip" eval="False"/>
</record>
<record id="hr_payroll_rules_ch_value" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll.COMP"/>
<field name="parent_rule_id" ref="hr_payroll_rules_parent_ch"/>
<field name="name">Meal Voucher, Company Part</field>
<field name="code">MEAL_V_COMP</field>
<field name="amount_select">fix</field>
<field name="sequence">165</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = -(contract.meal_voucher_amount - contract.meal_voucher_employee_deduction) * worked_days.WORK100.number_of_days</field>
<field name="name">ATN Internet</field>
<field name="code">ATN.INT.2</field>
<field name="sequence">161</field>
<field name="condition_select">python</field>
<field name="condition_python">result = bool(contract.meal_voucher_amount)</field>
<field name="condition_python">result = contract.internet</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = -5.0</field>
</record>
<record id="hr_salary_rule_atn_mobile_2" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_div_net"/>
<field name="name">ATN Mobile</field>
<field name="code">ATN.MOB.2</field>
<field name="sequence">162</field>
<field name="condition_select">python</field>
<field name="condition_python">result = contract.mobile</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = -12.0</field>
</record>
<record id="hr_salary_rule_commission_on_target" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_div_net"/>
<field name="name">Commission on Target</field>
<field name="code">COM</field>
<field name="sequence">170</field>
<field name="condition_select">python</field>
<field name="condition_python">result = bool(contract.commission_on_target)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = contract.warrant_value_employee / 12.0</field>
<field name="appears_on_payslip" eval="False"/>
<field name="register_id" ref="contrib_register_meal_vouchers"/>
</record>
<record id="hr_payroll_rules_ch_worker" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_div_net"/>
<field name="name">Retain on Meal Voucher</field>
<field name="parent_rule_id" ref="hr_payroll_rules_parent_ch"/>
<field name="code">MEAL_V_EMP</field>
<field name="sequence">165</field>
<field name="condition_select">python</field>
<field name="condition_python">result = bool(contract.meal_voucher_employee_deduction)</field>
<field name="condition_python">result = bool(contract.meal_voucher_amount)</field>
<field name="amount_select">percentage</field>
<field name="amount_percentage">-100.0</field>
<field name="amount_percentage_base">contract.meal_voucher_employee_deduction</field>
<field name="quantity">worked_days.WORK100.number_of_days</field>
<field name="amount_percentage_base">contract.meal_voucher_amount - contract.meal_voucher_paid_by_employer</field>
<field name="quantity">worked_days.WORK100 and worked_days.WORK100.number_of_days</field>
<field name="appears_on_payslip" eval="True"/>
<field name="register_id" ref="contrib_register_meal_vouchers"/>
</record>
@@ -278,38 +491,18 @@
<field name="amount_select">code</field>
<field name="sequence">65</field>
<field name="condition_select">python</field>
<field name="condition_python">result = bool(contract.travel_reimbursement_amount)</field>
<field name="amount_python_compute">result = contract.travel_reimbursement_amount</field>
<field name="condition_python">result = bool(contract.others_reimbursed_amount)</field>
<field name="amount_python_compute">result = contract.others_reimbursed_amount</field>
</record>
<record id="hr_payroll_rules_mis_ex_onss" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll.DED"/>
<field name="name">Miscellaneous exempt ONSS</field>
<field name="code">M.ONSS</field>
<record id="hr_salary_rule_representation_fees" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_div_net"/>
<field name="name">Representation Fees</field>
<field name="code">REP.FEES</field>
<field name="amount_select">code</field>
<field name="sequence">165</field>
<field name="sequence">171</field>
<field name="condition_select">python</field>
<field name="condition_python">result = bool(contract.misc_onss_deduction)</field>
<field name="amount_python_compute">result = -contract.misc_onss_deduction</field>
</record>
<record id="hr_payroll_rules_insurance" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll.DED"/>
<field name="name">Insurance</field>
<field name="code">Ins</field>
<field name="amount_select">code</field>
<field name="sequence">165</field>
<field name="condition_select">python</field>
<field name="condition_python">result = bool(contract.insurance_employee_deduction)</field>
<field name="amount_python_compute">result = -contract.insurance_employee_deduction</field>
</record>
<record id="hr_payroll_rules_advantage" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll.ALW"/>
<field name="name">Benefits of various nature</field>
<field name="code">Ben</field>
<field name="amount_select">code</field>
<field name="sequence">10</field>
<field name="condition_select">python</field>
<field name="condition_python">result = bool(contract.misc_advantage_amount)</field>
<field name="amount_python_compute">result = contract.misc_advantage_amount</field>
<field name="condition_python">result = bool(contract.representation_fees)</field>
<field name="amount_python_compute">result = contract.representation_fees</field>
</record>
<record id="hr_payroll_rules_suppl_net" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_div_net"/>
@@ -334,15 +527,119 @@
<record id="hr_payroll_salary_structure_employee" model="hr.payroll.structure">
<field name="code">C.P.218</field>
<field name="name">Belgian Employee</field>
<field eval="[(6, 0, [ref('hr_payroll_rules_company_car_parent'),ref('hr_payroll_rules_onss_rule'),ref('hr_payroll_rules_employee'),ref('hr_payroll_rules_p_p_b1'),ref('hr_payroll_rules_child_alw'),ref('hr_payroll_rules_parent_ch'),ref('hr_payroll_rules_reim_travel'),ref('hr_payroll_rules_mis_ex_onss'),ref('hr_payroll_rules_insurance'),ref('hr_payroll_rules_advantage'),ref('hr_payroll_rules_suppl_net'),ref('hr_payroll_rules_retained_net')])]" name="rule_ids"/>
<field eval="[(6, 0, [
ref('hr_payroll_rules_parent_company_car'),
ref('hr_payroll_rules_company_car_2'),
ref('hr_salary_rule_atn_mobile_2'),
ref('hr_salary_rule_atn_internet_2'),
ref('hr_salary_rule_atn_internet'),
ref('hr_salary_rule_atn_mobile'),
ref('hr_payroll_rules_onss_rule'),
ref('hr_salary_rule_employment_bonus_employees'),
ref('hr_payroll_rules_employee'),
ref('hr_payroll_rules_bareme'),
ref('hr_payroll_rules_baremeII'),
ref('hr_payroll_rules_baremeIII'),
ref('hr_salary_rule_other_family_charges_reductions'),
ref('hr_payroll_rules_child_alw'),
ref('hr_payroll_rules_ch_worker'),
ref('hr_payroll_rules_reim_travel'),
ref('hr_payroll_rules_mis_ex_onss_1'),
ref('hr_payroll_rules_mis_ex_onss_2'),
ref('hr_salary_rule_representation_fees'),
ref('hr_payroll_rules_suppl_net'),
ref('hr_payroll_rules_retained_net'),
ref('hr_salary_rule_commission_on_target'),
ref('hr_salary_rule_pp_reduction_employees'),
ref('hr_salary_rule_pp_reduction_low_salary'),
])]" name="rule_ids"/>
<field name="company_id" ref="base.main_company"/>
<field name="parent_id" ref="hr_payroll.structure_base"/>
</record>
<record id="hr_payroll_salary_structure_worker" model="hr.payroll.structure">
<field name="code">C.P.219</field>
<field name="name">Belgian Worker</field>
<field eval="[(6, 0, [ref('hr_payroll_rules_maj'), ref('hr_payroll_rules_onss_rule'),ref('hr_payroll_rules_p_p_b1'),ref('hr_payroll_rules_child_alw'),ref('hr_payroll_rules_parent_ch'),ref('hr_payroll_rules_reim_travel'),ref('hr_payroll_rules_mis_ex_onss'),ref('hr_payroll_rules_insurance'),ref('hr_payroll_rules_advantage'),ref('hr_payroll_rules_suppl_net'),ref('hr_payroll_rules_retained_net')])]" name="rule_ids"/>
<field eval="[(6, 0, [
ref('hr_payroll_rules_maj'),
ref('hr_payroll_rules_onss_rule'),
ref('hr_salary_rule_employment_bonus_workers'),
ref('hr_payroll_rules_bareme'),
ref('hr_payroll_rules_baremeII'),
ref('hr_payroll_rules_baremeIII'),
ref('hr_payroll_rules_child_alw'),
ref('hr_salary_rule_other_family_charges_reductions'),
ref('hr_payroll_rules_ch_worker'),
ref('hr_payroll_rules_reim_travel'),
ref('hr_payroll_rules_mis_ex_onss_1'),
ref('hr_payroll_rules_mis_ex_onss_2'),
ref('hr_salary_rule_representation_fees'),
ref('hr_payroll_rules_suppl_net'),
ref('hr_payroll_rules_retained_net'),
ref('hr_salary_rule_commission_on_target'),
ref('hr_salary_rule_pp_reduction_workers')
])]" name="rule_ids"/>
<field name="company_id" ref="base.main_company"/>
<field name="parent_id" ref="hr_payroll.structure_base"/>
</record>
<!-- Salary advantages definition -->
<record id="hr_contract_advantage_template_meal_voucher_amount" model="hr.contract.advantage.template">
<field name="name">Check Value Meal</field>
<field name="code">meal_voucher_amount</field>
<field name="default_value">7.45</field>
</record>
<record id="hr_contract_advantage_template_internet" model="hr.contract.advantage.template">
<field name="name">Internet</field>
<field name="code">internet</field>
<field name="default_value">38.0</field>
</record>
<record id="hr_contract_advantage_template_mobile" model="hr.contract.advantage.template">
<field name="name">Mobile</field>
<field name="code">mobile</field>
<field name="default_value">30.0</field>
</record>
<record id="hr_contract_advantage_template_mobile_plus" model="hr.contract.advantage.template">
<field name="name">Mobile +</field>
<field name="code">mobile_plus</field>
<field name="default_value">50.0</field>
</record>
<record id="hr_contract_advantage_template_additional_net_amount" model="hr.contract.advantage.template">
<field name="name">Net supplements</field>
<field name="code">additional_net_amount</field>
</record>
<record id="hr_contract_advantage_template_retained_net_amount" model="hr.contract.advantage.template">
<field name="name">Net retained</field>
<field name="code">retained_net_amount</field>
</record>
<record id="hr_contract_advantage_template_commission_on_target" model="hr.contract.advantage.template">
<field name="name">Commission on target</field>
<field name="code">commission_on_target</field>
<field name="default_value">1000.0</field>
</record>
<record id="hr_contract_advantage_template_fuel_card" model="hr.contract.advantage.template">
<field name="name">Fuel card</field>
<field name="code">fuel_card</field>
<field name="default_value">150.0</field>
<field name="lower_bound">0.0</field>
<field name="upper_bound">500.0</field>
</record>
<record id="hr_contract_advantage_template_holidays" model="hr.contract.advantage.template">
<field name="name">Holidays duration</field>
<field name="code">holidays</field>
<field name="default_value">20.0</field>
<field name="lower_bound">0.0</field>
<field name="upper_bound">30.0</field>
</record>
<record id="hr_contract_advantage_template_representation_fees" model="hr.contract.advantage.template">
<field name="name">Representation fees</field>
<field name="code">representation_fees</field>
<field name="default_value">150.0</field>
</record>
<record id="hr_contract_advantage_template_eco_checks" model="hr.contract.advantage.template">
<field name="name">Eco-checks</field>
<field name="code">eco_checks</field>
<field name="default_value">250.0</field>
</record>
</odoo>
@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Employee Contract -->
<record id="hr_payroll.hr_contract_firstcontract1" model="hr.contract">
<field name="name">Developer's Contract (Belgium payroll C.P. 218)</field>
<field name="struct_id" ref="hr_payroll_salary_structure_employee"/>
<field eval="2500.0" name="wage"/>
<field eval="6.0" name="meal_voucher_amount"/>
<field eval="1.09" name="meal_voucher_employee_deduction"/>
<field eval="250.0" name="car_company_amount"/>
<field eval="110" name="car_employee_deduction"/>
<field eval="50.0" name="additional_net_amount"/>
</record>
<record id="hr_payroll.hr_employee_payroll" model="hr.employee">
<field name="marital">single</field>
</record>
<record id="res_partner_onss" model="res.partner">
<field name="street">54 Rue Jean Baptiste Colette BP </field>
<field name="city">Attiches</field>
<field name="zip">59551</field>
<field name="country_id" ref="base.fr"/>
<field name="email">onss@yourcompany.example.com</field>
</record>
<record id="res_partner_pp" model="res.partner">
<field name="street">Shanghai Branch</field>
<field name="street2">Anshun Road 83, Room 1603 - </field>
<field name="city">Shanghai</field>
<field name="zip">200052</field>
<field name="country_id" ref="base.cn"/>
<field name="email">fiscal@yourcompany.example.com</field>
</record>
</odoo>
@@ -1,30 +1,246 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo.addons import decimal_precision as dp
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class HrContract(models.Model):
_inherit = 'hr.contract'
travel_reimbursement_amount = fields.Float(string='Reimbursement of travel expenses', digits=dp.get_precision('Payroll'))
car_company_amount = fields.Float(string='Company car employer', digits=dp.get_precision('Payroll'))
car_employee_deduction = fields.Float(string='Company Car Deduction for Worker', digits=dp.get_precision('Payroll'))
misc_onss_deduction = fields.Float(string='Miscellaneous exempt ONSS', digits=dp.get_precision('Payroll'))
meal_voucher_amount = fields.Float(string='Check Value Meal', digits=dp.get_precision('Payroll'))
meal_voucher_employee_deduction = fields.Float(string='Check Value Meal - by worker', digits=dp.get_precision('Payroll'))
insurance_employee_deduction = fields.Float(string='Insurance Group - by worker', digits=dp.get_precision('Payroll'))
misc_advantage_amount = fields.Float(string='Benefits of various nature', digits=dp.get_precision('Payroll'))
additional_net_amount = fields.Float(string='Net supplements', digits=dp.get_precision('Payroll'))
retained_net_amount = fields.Float('Net retained ', digits=dp.get_precision('Payroll'))
transport_mode = fields.Selection([
('company_car', 'Company car'),
('public_transport', 'Public Transport'),
('others', 'Other'),
], string="Transport", default='company_car')
car_atn = fields.Monetary(string='ATN Company Car')
public_transport_employee_amount = fields.Monetary('Paid by the employee (Monthly)')
thirteen_month = fields.Monetary(compute='_compute_holidays_advantages', string='13th Month')
double_holidays = fields.Monetary(compute='_compute_holidays_advantages', string='Double holidays')
warrant_value_employee = fields.Monetary(compute='_compute_warrants_cost', string="Warrant value for the employee")
# Employer costs fields
final_yearly_costs = fields.Monetary(compute='_compute_final_yearly_costs', readonly=False,
string='Total Employee Cost', groups="hr.group_hr_manager")
monthly_yearly_costs = fields.Monetary(compute='_compute_monthly_yearly_costs', string='Monthly Equivalent Cost', readonly=True)
ucm_insurance = fields.Monetary(compute='_compute_ucm_insurance', string="Social Secretary Costs")
social_security_contributions = fields.Monetary(compute='_compute_social_security_contributions', string="Social Security Contributions")
yearly_cost_before_charges = fields.Monetary(compute='_compute_yearly_cost_before_charges', string="Yearly Costs Before Charges")
meal_voucher_paid_by_employer = fields.Monetary(compute='_compute_meal_voucher_paid_by_employer', string="Meal Voucher Paid by Employer")
company_car_total_depreciated_cost = fields.Monetary()
public_transport_reimbursed_amount = fields.Monetary(string='Reimbursed amount',
compute='_compute_public_transport_reimbursed_amount', readonly=False, store=True)
others_reimbursed_amount = fields.Monetary(string='Reimbursed amount')
transport_employer_cost = fields.Monetary(compute='_compute_transport_employer_cost', string="Employer cost from employee transports")
warrants_cost = fields.Monetary(compute='_compute_warrants_cost')
# Advantages
commission_on_target = fields.Monetary(string="Commission on Target", default=1000)
fuel_card = fields.Monetary(string="Fuel Card", default=150)
internet = fields.Monetary(string="Internet", default=38, help="Your internet subscription will be paid up to this amount.")
representation_fees = fields.Monetary(string="Representation Fees", default=150.0)
mobile = fields.Monetary(string="Mobile", default=30, help="Your mobile subscription will be paid up to this amount.")
mobile_plus = fields.Monetary(string="International Communication", default=50, help="Your international mobile subscription will be paid up to this amount.")
meal_voucher_amount = fields.Monetary(string="Meal Vouchers", default=7.45)
holidays = fields.Float(string="Holidays", default=20)
holidays_compensation = fields.Monetary(compute='_compute_holidays_compensation', string="Holidays Compensation")
wage_with_holidays = fields.Monetary(compute='_compute_wage_with_holidays', sting="Wage update with holidays retenues")
additional_net_amount = fields.Monetary(string="Net Supplements")
retained_net_amount = fields.Monetary(sting="Net Retained")
eco_checks = fields.Monetary("Eco Vouchers", default=250)
@api.depends('holidays', 'wage')
def _compute_wage_with_holidays(self):
for contract in self:
if contract.holidays > 20.0:
yearly_cost = contract.final_yearly_costs * (1.0 - (contract.holidays - 20.0) / 220.0)
contract.wage_with_holidays = contract._get_gross_from_employer_costs(yearly_cost)
else:
contract.wage_with_holidays = contract.wage
@api.depends('transport_mode', 'company_car_total_depreciated_cost',
'public_transport_reimbursed_amount', 'others_reimbursed_amount')
def _compute_transport_employer_cost(self):
for contract in self:
if contract.transport_mode == 'company_car':
contract.transport_employer_cost = contract.company_car_total_depreciated_cost
elif contract.transport_mode == 'public_transport':
contract.transport_employer_cost = contract.public_transport_reimbursed_amount
elif contract.transport_mode == 'others':
contract.transport_employer_cost == contract.others_reimbursed_amount
@api.depends('commission_on_target')
def _compute_warrants_cost(self):
for contract in self:
contract.warrants_cost = contract.commission_on_target * 1.326 / 1.05 * 12.0
contract.warrant_value_employee = contract.warrants_cost * 0.54
@api.depends('wage', 'fuel_card', 'representation_fees', 'transport_employer_cost',
'internet', 'mobile', 'mobile_plus')
def _compute_yearly_cost_before_charges(self):
for contract in self:
contract.yearly_cost_before_charges = 12.0 * (
contract.wage * (1.0 + 1.0 / 12.0) +
contract.fuel_card +
contract.representation_fees +
contract.internet +
contract.mobile +
contract.mobile_plus +
contract.transport_employer_cost
)
@api.depends('yearly_cost_before_charges', 'ucm_insurance', 'social_security_contributions',
'social_security_contributions', 'double_holidays', 'warrants_cost', 'meal_voucher_paid_by_employer')
def _compute_final_yearly_costs(self):
for contract in self:
contract.final_yearly_costs = (
contract.yearly_cost_before_charges +
contract.ucm_insurance +
contract.social_security_contributions +
contract.double_holidays +
contract.warrants_cost +
(220.0 * contract.meal_voucher_paid_by_employer)
)
@api.depends('holidays', 'final_yearly_costs')
def _compute_holidays_compensation(self):
for contract in self:
if contract.holidays < 20:
decrease_amount = contract.final_yearly_costs * (20.0 - contract.holidays) / 220.0
contract.holidays_compensation = decrease_amount
else:
contract.holidays_compensation = 0.0
@api.onchange('final_yearly_costs')
def _onchange_final_yearly_costs(self):
self.wage = self._get_gross_from_employer_costs(self.final_yearly_costs)
@api.depends('meal_voucher_amount')
def _compute_meal_voucher_paid_by_employer(self):
for contract in self:
contract.meal_voucher_paid_by_employer = contract.meal_voucher_amount * (1 - 0.1463)
@api.depends('wage')
def _compute_social_security_contributions(self):
for contract in self:
total_wage = contract.wage * 13.0
contract.social_security_contributions = (total_wage) * 0.3507
@api.depends('wage')
def _compute_ucm_insurance(self):
for contract in self:
contract.ucm_insurance = (contract.wage * 12.0) * 0.05
@api.depends('public_transport_employee_amount')
def _compute_public_transport_reimbursed_amount(self):
for contract in self:
contract.public_transport_reimbursed_amount = contract._get_public_transport_reimbursed_amount(contract.public_transport_employee_amount)
def _get_public_transport_reimbursed_amount(self, amount):
return amount * 0.68
@api.depends('final_yearly_costs')
def _compute_monthly_yearly_costs(self):
for contract in self:
contract.monthly_yearly_costs = contract.final_yearly_costs / 12.0
@api.depends('wage')
def _compute_holidays_advantages(self):
for contract in self:
contract.double_holidays = contract.wage * 0.92
contract.thirteen_month = contract.wage
@api.onchange('mobile', 'mobile_plus')
def _onchange_mobile(self):
if self.mobile_plus and not self.mobile:
raise ValidationError(_('You should have a mobile subscription to select an international communication amount!'))
def _get_internet_amount(self, has_internet):
if has_internet:
return self.get_attribute('internet', 'default_value')
else:
return 0.0
def _get_mobile_amount(self, has_mobile, international_communication):
if has_mobile and international_communication:
return self.get_attribute('mobile', 'default_value') + self.get_attribute('mobile_plus', 'default_value')
elif has_mobile:
return self.get_attribute('mobile', 'default_value')
else:
return 0.0
def _get_gross_from_employer_costs(self, yearly_cost):
contract = self
remaining_for_gross = yearly_cost \
- 12.0 * contract.representation_fees \
- 12.0 * contract.fuel_card \
- 12.0 * contract.internet \
- 12.0 * (contract.mobile + contract.mobile_plus) \
- 12.0 * contract.transport_employer_cost \
- (1.326 / 1.05 * 12.0) * contract.commission_on_target \
- 220.0 * contract.meal_voucher_paid_by_employer
gross = remaining_for_gross / (12.0 * 0.05 + 13.0 + 13.0 * 0.3507 + 0.92)
return gross
class HrEmployee(models.Model):
_inherit = 'hr.employee'
spouse_fiscal_status = fields.Selection([('without income','Without Income'),('with income','With Income')], string='Tax status for spouse')
spouse_fiscal_status = fields.Selection([
('without income', 'Without Income'),
('with income', 'With Income')
], string='Tax status for spouse')
disabled = fields.Boolean(string="Disabled", help="If the employee is declared disabled by law")
disabled_spouse_bool = fields.Boolean(string='Disabled Spouse', help='if recipient spouse is declared disabled by law')
disabled_children_bool = fields.Boolean(string='Disabled Children', help='if recipient children is/are declared disabled by law')
resident_bool = fields.Boolean(string='Nonresident', help='if recipient lives in a foreign country')
disabled_children_number = fields.Integer('Number of disabled children')
dependent_children = fields.Integer(compute='_compute_dependent_children', string='Considered number of dependent children')
other_dependent_people = fields.Boolean(string="Other Dependent People", help="If other people are dependent on the employee")
other_senior_dependent = fields.Integer('# seniors (>=65)', help="Number of seniors dependent on the employee, including the disabled ones")
other_disabled_senior_dependent = fields.Integer('# disabled seniors (>=65)')
other_juniors_dependent = fields.Integer('# people (<65)', help="Number of juniors dependent on the employee, including the disabled ones")
other_disabled_juniors_dependent = fields.Integer('# disabled people (<65)')
dependent_seniors = fields.Integer(compute='_compute_dependent_people', string="Considered number of dependent seniors")
dependent_juniors = fields.Integer(compute='_compute_dependent_people', string="Considered number of dependent juniors")
spouse_net_revenue = fields.Float(string="Spouse Net Revenue", help="Own professional income, other than pensions, annuities or similar income")
spouse_other_net_revenue = fields.Float(string="Spouse Other Net Revenue",
help='Own professional income which is exclusively composed of pensions, annuities or similar income')
@api.constrains('spouse_fiscal_status', 'spouse_net_revenue', 'spouse_other_net_revenue')
def _check_spouse_revenue(self):
for employee in self:
if employee.spouse_fiscal_status == 'with income' and not employee.spouse_net_revenue and not employee.spouse_other_net_revenue:
raise ValidationError(_("The revenue for the spouse can't be equal to zero is the fiscal status is 'With Income'."))
@api.onchange('spouse_fiscal_status')
def _onchange_spouse_fiscal_status(self):
self.spouse_net_revenue = 0.0
self.spouse_other_net_revenue = 0.0
@api.onchange('disabled_children_bool')
def _onchange_disabled_children_bool(self):
self.disabled_children_number = 0
@api.onchange('other_dependent_people')
def _onchange_other_dependent_people(self):
self.other_senior_dependent = 0.0
self.other_disabled_senior_dependent = 0.0
self.other_juniors_dependent = 0.0
self.other_disabled_juniors_dependent = 0.0
@api.depends('disabled_children_bool', 'disabled_children_number', 'children')
def _compute_dependent_children(self):
for employee in self:
if employee.disabled_children_bool:
employee.dependent_children = employee.children + employee.disabled_children_number
else:
employee.dependent_children = employee.children
@api.depends('other_dependent_people', 'other_senior_dependent',
'other_disabled_senior_dependent', 'other_juniors_dependent', 'other_disabled_juniors_dependent')
def _compute_dependent_people(self):
for employee in self:
employee.dependent_seniors = employee.other_senior_dependent + employee.other_disabled_senior_dependent
employee.dependent_juniors = employee.other_juniors_dependent + employee.other_disabled_juniors_dependent
@@ -1,27 +1,124 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="hr_contract_form_l10n_be_inherit" model="ir.ui.view">
<field name="name">hr.contract.form.inherit</field>
<field name="model">hr.contract</field>
<field name="priority">20</field>
<field name="inherit_id" ref="hr_contract.hr_contract_view_form"/>
<field name="arch" type="xml">
<data>
<xpath expr="//field[@name='struct_id']" position="after">
<field name="travel_reimbursement_amount"/>
<field name="car_company_amount"/>
<field name="car_employee_deduction"/>
<field name="misc_onss_deduction"/>
<field name="meal_voucher_amount"/>
<field name="meal_voucher_employee_deduction"/>
<field name="misc_advantage_amount"/>
<field name="additional_net_amount"/>
<field name="retained_net_amount"/>
<field name="insurance_employee_deduction"/>
</xpath>
</data>
</field>
</record>
<record id="hr_contract_view_form" model="ir.ui.view">
<field name="name">hr.contract.form</field>
<field name="model">hr.contract</field>
<field name="inherit_id" ref="hr_payroll.hr_contract_form_inherit"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='wage']" position="after">
<label for="wage_with_holidays" attrs="{'invisible': [('holidays', '&lt;=', 20.0)]}"/>
<div class="o_row" attrs="{'invisible': [('holidays', '&lt;=', 20.0)]}">
<field name="wage_with_holidays" nolabel="1"/>
<span>/ month</span>
</div>
<label for="fuel_card"/>
<div class="o_row">
<field name="fuel_card" nolabel="1"/>
<span>/ month</span>
</div>
<label for="meal_voucher_amount"/>
<div class="o_row">
<field name="meal_voucher_amount" nolabel="1"/>
<span>/ worked day</span>
</div>
<label for="representation_fees"/>
<div class="o_row">
<field name="representation_fees" nolabel="1"/>
<span>/ month</span>
</div>
<label for="additional_net_amount"/>
<div class="o_row">
<field name="additional_net_amount" nolabel="1"/>
<span>/ month</span>
</div>
<label for="retained_net_amount"/>
<div class="o_row">
<field name="retained_net_amount" nolabel="1"/>
<span>/ month</span>
</div>
<label for="commission_on_target"/>
<div class="o_row">
<field name="commission_on_target" nolabel="1"/>
<span>/ month</span>
</div>
</xpath>
<xpath expr="//group[@name='salary_and_advantages']" position="after">
<group string="Employee Costs" groups="hr.group_hr_manager">
<field name="yearly_cost_before_charges" invisible="1"/>
<field name="social_security_contributions" invisible="1"/>
<field name="warrants_cost" invisible="1"/>
<field name="warrant_value_employee" invisible="1"/>
<field name="ucm_insurance" invisible="1"/>
<field name="meal_voucher_paid_by_employer" invisible="1"/>
<label for="final_yearly_costs"/>
<div class="o_row">
<field name="final_yearly_costs" nolabel="1"/>
<span>/ year</span>
</div>
<field name="monthly_yearly_costs"/>
</group>
<group string="Monthly Advantages in Nature">
<field name="transport_mode" widget="radio"/>
<field name="car_atn" attrs="{'invisible': [('transport_mode', '!=', 'company_car')]}" invisible="1"/>
<field name="transport_employer_cost" invisible="1"/>
<label string="Public Transport" attrs="{'invisible': [('transport_mode', '!=', 'public_transport')]}"/>
<div attrs="{'invisible': [('transport_mode', '!=', 'public_transport')]}">
<div>
<label for="public_transport_employee_amount" class="oe_inline"/>
<field name="public_transport_employee_amount" class="oe_inline"/>
</div>
<div>
<label for="public_transport_reimbursed_amount" class="oe_inline"/>
<field name="public_transport_reimbursed_amount" class="oe_inline"/>
</div>
</div>
<label string="Other" attrs="{'invisible': [('transport_mode', '!=', 'others')]}"/>
<div attrs="{'invisible': [('transport_mode', '!=', 'others')]}" class="o_row">
<field name="others_reimbursed_amount" class="oe_inline" nolabel="1"/>
<span>/ month</span>
</div>
<label for="internet"/>
<div class="o_row">
<field name="internet" nolabel="1"/>
<span>/ month</span>
</div>
<label for="mobile"/>
<div class="o_row">
<field name="mobile" nolabel="1"/>
<span>/ month</span>
</div>
<label for="mobile_plus"/>
<div class="o_row">
<field name="mobile_plus" nolabel="1"/>
<span>/ month</span>
</div>
</group>
<group string="Yearly Advantages">
<label for="eco_checks"/>
<div class="o_row">
<field name="eco_checks" nolabel="1"/>
<span>/ year</span>
</div>
<label for="thirteen_month"/>
<div class="o_row">
<field name="thirteen_month" nolabel="1"/>
<span>/ year</span>
</div>
<label for="double_holidays"/>
<div class="o_row">
<field name="double_holidays" nolabel="1"/>
<span>/ year</span>
</div>
<label for="holidays"/>
<div class="o_row">
<field name="holidays" nolabel="1"/>
<span>/ year</span>
</div>
<field name="holidays_compensation" invisible="1"/>
</group>
</xpath>
</field>
</record>
<record id="hr_employee_form__l10n_be_view_for" model="ir.ui.view">
<field name="name">hr.employee.inherit.form</field>
@@ -29,7 +126,6 @@
<field name="inherit_id" ref="hr_contract.hr_hr_employee_view_form2"/>
<field name="priority">30</field>
<field name="arch" type="xml">
<data>
<xpath expr="//field[@name='vehicle_distance']" position="after">
<field name="resident_bool" eval="False"/>
</xpath>
@@ -37,11 +133,23 @@
<field name="spouse_fiscal_status" attrs="{'invisible':[('marital','!=','married')],'required':[('marital','=','married')]}" colspan="1" help="if spouse has professionnel income or not"/>
<field name="disabled_spouse_bool" attrs="{'invisible':[('marital','!=','married')]}" colspan="1"/>
</xpath>
<xpath expr="//field[@name='gender']" position="after">
<field name="disabled"/>
</xpath>
<xpath expr="//field[@name='children']" position="after">
<field name="disabled_children_bool"/>
<field name="disabled_children_number" attrs="{'invisible':[('disabled_children_bool','!=',True)]}" colspan="1" help="number of dependent children declared as disabled"/>
<field name="dependent_children" invisible="1"/>
<field name="other_dependent_people"/>
<field name="other_senior_dependent" attrs="{'invisible':[('other_dependent_people','!=',True)]}"/>
<field name="other_disabled_senior_dependent" attrs="{'invisible':[('other_dependent_people','!=',True)]}"/>
<field name="other_juniors_dependent" attrs="{'invisible':[('other_dependent_people','!=',True)]}"/>
<field name="other_disabled_juniors_dependent" attrs="{'invisible':[('other_dependent_people','!=',True)]}"/>
</xpath>
<xpath expr="//field[@name='spouse_fiscal_status']" position="after">
<field name="spouse_net_revenue" attrs="{'invisible': ['|', ('marital', '!=', 'married'), ('spouse_fiscal_status', '!=', 'with income')]}"/>
<field name="spouse_other_net_revenue" attrs="{'invisible': ['|', ('marital', '!=', 'married'), ('spouse_fiscal_status', '!=', 'with income')]}"/>
</xpath>
</data>
</field>
</record>
</odoo>
@@ -0,0 +1,4 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import models
@@ -0,0 +1,16 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Belgium - Payroll - Fleet',
'category': 'Localization',
'depends': ['l10n_be_hr_payroll', 'fleet'],
'description': """
""",
'data': [
'views/fleet_views.xml',
'views/fleet_config_settings_views.xml',
'views/hr_contract_views.xml',
],
'auto_install': True,
}
@@ -0,0 +1,6 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import fleet
import fleet_config_settings
import hr_contract
@@ -0,0 +1,150 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import babel.dates
from odoo import api, fields, models
from odoo.fields import Datetime
class FleetVehicle(models.Model):
_inherit = 'fleet.vehicle'
co2_fee = fields.Float(compute='_compute_co2_fee', string="CO2 fee (monthly)")
total_depreciated_cost = fields.Float(compute='_compute_total_depreciated_cost',
string="Total Depreciated Cost", help="This includes all the depreciated costs and the CO2 fee")
total_cost = fields.Float(compute='_compute_total_cost', string="Total Cost", help="This include all the costs and the CO2 fee")
fuel_type = fields.Selection(required=True, default='diesel')
atn = fields.Float(compute='_compute_car_atn')
acquisition_date = fields.Date(required=True)
@api.depends('co2_fee', 'log_contracts', 'log_contracts.state', 'log_contracts.recurring_cost_amount_depreciated')
def _compute_total_depreciated_cost(self):
for car in self:
car.total_depreciated_cost = car.co2_fee + \
sum(car.log_contracts.filtered(
lambda contract: contract.state == 'open'
).mapped('recurring_cost_amount_depreciated'))
@api.depends('co2_fee', 'log_contracts', 'log_contracts.state', 'log_contracts.cost_generated')
def _compute_total_cost(self):
for car in self:
car.total_cost = car.co2_fee
contracts = car.log_contracts.filtered(
lambda contract: contract.state == 'open' and contract.cost_frequency != 'no'
)
for contract in contracts:
if contract.cost_frequency == "daily":
car.total_cost += contract.cost_generated * 30.0
elif contract.cost_frequency == "weekly":
car.total_cost += contract.cost_generated * 4.0
elif contract.cost_frequency == "monthly":
car.total_cost += contract.cost_generated
elif contract.cost_frequency == "yearly":
car.total_cost += contract.cost_generated / 12.0
def _get_co2_fee(self, co2):
return max((((co2 * 9.0) - 600.0) * 1.2488) / 12.0, 0.0)
@api.depends('co2')
def _compute_co2_fee(self):
for car in self:
car.co2_fee = self._get_co2_fee(car.co2)
@api.depends('fuel_type', 'car_value', 'acquisition_date')
def _compute_car_atn(self):
for car in self:
car.atn = car._get_car_atn(car.acquisition_date, car.car_value, car.fuel_type, car.co2)
@api.depends('model_id', 'license_plate', 'total_depreciated_cost', 'acquisition_date')
def _compute_vehicle_name(self):
super(FleetVehicle, self)._compute_vehicle_name()
for vehicle in self:
acquisition_date = self._get_acquisition_date()
vehicle.name += u" \u2022 " + str(round(vehicle.total_depreciated_cost, 2)) + u" \u2022 " + acquisition_date
def _get_acquisition_date(self):
self.ensure_one()
return babel.dates.format_date(
date=Datetime.from_string(self.acquisition_date),
format='MMMM y',
locale=self._context.get('lang', 'en_US')
)
def _get_car_atn(self, acquisition_date, car_value, fuel_type, co2):
# Compute the correction coefficient from the age of the car
now = Datetime.from_string(Datetime.now())
start = Datetime.from_string(acquisition_date)
if start:
number_of_month = (now.year - start.year) * 12.0 + now.month - start.month + int(bool(now.day - start.day + 1))
if number_of_month <= 12:
age_coefficient = 1.00
elif number_of_month <= 24:
age_coefficient = 0.94
elif number_of_month <= 36:
age_coefficient = 0.88
elif number_of_month <= 48:
age_coefficient = 0.82
elif number_of_month <= 60:
age_coefficient = 0.76
else:
age_coefficient = 0.70
car_value = car_value * age_coefficient
# Compute atn value from corrected car_value
magic_coeff = 6.0 / 7.0 # Don't ask me why
if fuel_type == 'electric':
atn = 0.0
else:
if fuel_type in ['diesel', 'hybrid']:
reference = 87.0
else:
reference = 105.0
if co2 <= reference:
atn = car_value * max(0.04, (0.055 - 0.001 * (reference - co2))) * magic_coeff
else:
atn = car_value * min(0.18, (0.055 + 0.001 * (co2 - reference))) * magic_coeff
return max(1280, atn) / 12.0
class FleetVehicleLogContract(models.Model):
_inherit = 'fleet.vehicle.log.contract'
recurring_cost_amount_depreciated = fields.Float("Recurring Cost Amount (depreciated)")
class FleetVehicleModel(models.Model):
_inherit = 'fleet.vehicle.model'
default_recurring_cost_amount_depreciated = fields.Float(string="Recurring Cost (Depreciated)",
help="Default recurring cost amount that should be applied to a new car from this model")
default_co2 = fields.Float(string="CO2 emissions")
default_fuel_type = fields.Selection([('gasoline', 'Gasoline'), ('diesel', 'Diesel'), ('electric', 'Electric'), ('hybrid', 'Hybrid')], 'Fuel Type', help='Fuel Used by the vehicle')
default_car_value = fields.Float(string="Catalog Value (VAT Incl.)")
can_be_requested = fields.Boolean(string="Can be requested", help="Can be requested on a contract as a new car")
default_atn = fields.Float(compute='_compute_atn', string="ATN")
default_total_depreciated_cost = fields.Float(compute='_compute_default_total_depreciated_cost')
@api.depends('default_car_value', 'default_co2', 'default_fuel_type')
def _compute_atn(self):
now = Datetime.now()
for model in self:
model.default_atn = self.env['fleet.vehicle']._get_car_atn(now, model.default_car_value, model.default_fuel_type, model.default_co2)
@api.depends('default_co2', 'default_recurring_cost_amount_depreciated')
def _compute_default_total_depreciated_cost(self):
for model in self:
model.default_total_depreciated_cost = model.default_co2 + model.default_recurring_cost_amount_depreciated
@api.multi
@api.depends('name', 'brand_id')
def name_get(self):
res = super(FleetVehicleModel, self).name_get()
new_res = []
for res_item in res:
model = self.browse(res_item[0])
if model.default_total_depreciated_cost != 0.0:
new_res.append((res_item[0], res_item[1] + u" \u2022 " + str(model.default_total_depreciated_cost)))
else:
new_res.append(res_item)
return new_res
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class FleetConfigSettings(models.TransientModel):
_name = 'fleet.config.settings'
_inherit = ['res.config.settings']
max_unused_cars = fields.Integer(string='Maximum unused cars')
@api.multi
def set_default_max_unused_cars(self):
Param = self.env['ir.config_parameter'].sudo()
Param.set_param("l10n_be_hr_payroll_fleet.max_unused_cars", self.max_unused_cars)
@api.model
def get_default_max_unused_cars(self, fields):
params = self.env['ir.config_parameter'].sudo()
max_unused_cars = params.get_param('l10n_be_hr_payroll_fleet.max_unused_cars', default=3)
return dict(max_unused_cars=int(max_unused_cars))
@@ -0,0 +1,52 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class HrContract(models.Model):
_inherit = 'hr.contract'
car_id = fields.Many2one('fleet.vehicle', string='Car',
domain=lambda self: self._get_available_cars_domain(),
default=lambda self: self.env['fleet.vehicle'].search([('driver_id', '=', self.employee_id.address_home_id.id)], limit=1))
car_atn = fields.Float(compute='_compute_car_atn_and_costs', string='ATN Company Car')
company_car_total_depreciated_cost = fields.Float(compute='_compute_car_atn_and_costs')
available_cars_amount = fields.Integer(compute='_compute_available_cars_amount', string='Number of available cars')
new_car = fields.Boolean('Request a new car')
new_car_model_id = fields.Many2one('fleet.vehicle.model', string="Model", domain=lambda self: self._get_possible_model_domain())
max_unused_cars = fields.Integer(compute='_compute_max_unused_cars')
@api.depends('car_id', 'new_car', 'new_car_model_id')
def _compute_car_atn_and_costs(self):
for contract in self:
if contract.car_id:
contract.car_atn = contract.car_id.atn
contract.company_car_total_depreciated_cost = contract.car_id.total_depreciated_cost
elif contract.new_car and contract.new_car_model_id:
contract.car_atn = contract.new_car_model_id.default_atn
contract.company_car_total_depreciated_cost = contract.new_car_model_id.default_total_depreciated_cost
@api.depends('name')
def _compute_available_cars_amount(self):
for contract in self:
contract.available_cars_amount = self.env['fleet.vehicle'].search_count([('driver_id', '=', False)])
@api.depends('name')
def _compute_max_unused_cars(self):
params = self.env['ir.config_parameter'].sudo()
max_unused_cars = params.get_param('l10n_be_hr_payroll_fleet.max_unused_cars', default=3)
for contract in self:
contract.max_unused_cars = int(max_unused_cars)
@api.onchange('employee_id')
def _onchange_employee_id(self):
super(HrContract, self)._onchange_employee_id()
self.car_id = self.env['fleet.vehicle'].search([('driver_id', '=', self.employee_id.address_home_id.id)], limit=1)
return {'domain': {'car_id': self._get_available_cars_domain()}}
def _get_available_cars_domain(self):
return ['|', ('driver_id', '=', False), ('driver_id', '=', self.employee_id.address_home_id.id)]
def _get_possible_model_domain(self):
return [('can_be_requested', '=', True)]
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="fleet_config_settings_view_form" model="ir.ui.view">
<field name="name">fleet.config.settings.form</field>
<field name="model">fleet.config.settings</field>
<field name="arch" type="xml">
<form string="Configure Fleet" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
</header>
<sheet>
<h2>Fleet Management</h2>
<div class="row mt16 o_settings_container">
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_left_pane"/>
<div class="o_setting_right_pane">
<label string="New Car Request"/>
<div class="text-muted o_row">
<span>An employee cannot request a new car there are more than </span>
<field name="max_unused_cars"/>
<span> existing vehicles availables.</span>
</div>
</div>
</div>
</div>
</sheet>
</form>
</field>
</record>
<record id="fleet_config_settings_action" model="ir.actions.act_window">
<field name="name">Configure Fleet</field>
<field name="res_model">fleet.config.settings</field>
<field name="view_mode">form</field>
<field name="target">inline</field>
</record>
<menuitem id="fleet_config_settings_menu" name="Settings"
parent="fleet.fleet_configuration" sequence="0" action="fleet_config_settings_action"
groups="base.group_system"/>
</data>
</odoo>
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="fleet_vehicle_view_form" model="ir.ui.view">
<field name="name">fleet.vehicle.form</field>
<field name="model">fleet.vehicle</field>
<field name="inherit_id" ref="fleet.fleet_vehicle_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='co2']" position="after">
<field name="co2_fee"/>
</xpath>
<xpath expr="//field[@name='car_value']" position="after">
<label for="total_depreciated_cost"/>
<div class="o_row">
<field name="total_depreciated_cost" nolabel="1"/>
<span>/ month</span>
</div>
<label for="total_cost"/>
<div class="o_row">
<field name="total_cost" nolabel="1"/>
<span>/ month</span>
</div>
<field name="atn"/>
</xpath>
</field>
</record>
<record id="fleet_vehicle_log_contract_view_form" model="ir.ui.view">
<field name="name">fleet.vehicle.log.contract.form</field>
<field name="model">fleet.vehicle.log.contract</field>
<field name="inherit_id" ref="fleet.fleet_vehicle_log_contract_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='amount']" position="after">
<field name="recurring_cost_amount_depreciated"/>
</xpath>
</field>
</record>
<record id="fleet_vehicle_model_view_form" model="ir.ui.view">
<field name="name">fleet.vehicle.model.form</field>
<field name="model">fleet.vehicle.model</field>
<field name="inherit_id" ref="fleet.fleet_vehicle_model_view_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="before">
<group>
<group>
<field name="can_be_requested"/>
</group>
</group>
</xpath>
<xpath expr="//page[@name='vendors']" position="after">
<page string="Information">
<group>
<group>
<field name="default_car_value"/>
<field name="default_co2"/>
<field name="default_fuel_type" widget="selection"/>
<field name="default_recurring_cost_amount_depreciated"/>
</group>
<group>
<field name="default_atn"/>
<field name="default_total_depreciated_cost" invisible="1"/>
</group>
</group>
</page>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="hr_contract_view_form" model="ir.ui.view">
<field name="name">hr.contract.form"</field>
<field name="model">hr.contract</field>
<field name="inherit_id" ref="l10n_be_hr_payroll.hr_contract_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='transport_mode']" position="after">
<label for="car_id" attrs="{'invisible': [('transport_mode', '!=', 'company_car')]}"/>
<div attrs="{'invisible': [('transport_mode', '!=', 'company_car')]}">
<field name="car_id" nolabel="1" attrs="{'invisible': [('new_car', '=', True)]}"/>
<field name="available_cars_amount" invisible="1"/>
<div>
<field name="new_car" attrs="{'invisible': [('available_cars_amount', '&gt;', 3)]}" nolabel="1"/>
<label for="new_car"/>
</div>
<div attrs="{'invisible': [('new_car', '=', False)]}" class="oe_inline">
<label for="new_car_model_id" class="oe_inline"/>
<field name="new_car_model_id" class="oe_inline"/>
</div>
</div>
<field name="max_unused_cars" invisible="1"/>
</xpath>
<xpath expr="//field[@name='car_atn']" position="after">
<field name="company_car_total_depreciated_cost" invisible="1"/>
</xpath>
</field>
</record>
</odoo>