[IMP] hr_payroll: improve general usability

This commit improves general usability.

Specification
=============

Screens usability:

1/ Benefits
 1.1/ Calendar
    - Add a sidebar filter to filter employees. The filters are saved per user.
      If no filter exists for the user the 'Everybody' filter should be true by default.
    - Generate benefits when clicking on the menu -> remove the wizard (popup): generate benefits for all running contracts
    - Once generated, don't show again the generate benefits button: only show the Validate button
    - Generate Payslips button: remove the wizard. It generates payslip of validated benefits
      and jumps to the payslip batch form view.
 1.2/ Form:
    - rename: Start into "From"
    - rename: End into "To"
    - rename: Hours into "Period" (add 'hours' as unit on the right of the field)

2/ Payslip
 2.1/ Form:
    - Remove Payslip stat button.
    - a computed payslip should be editable

 2.2/ List:
    - show: Reference | Employee | Batch Name | From | To | Gross Salary | Net Salary | Status
    - In action buttons add a "Confirm" to confirm multiple payslips
    - add a quick search on contract
    - filters: rename 'draft' into "To Compute"
    - filter: add "To Confirm"

Menus:

1/ Salary Computation
    - Benefits (object: hr.benefit ;  view: calendar, list, form, pivot. Group: hr officer ;  filters: group by employee in the view)
    - Payslips (objetc: hr.payslip ; Views: list, form, pivot. Group: hr officer ; filters: last batch ; Sort: employee name alphabetically)
2/ Employee Payslips
    - Batches  (objetc: hr.payslip.run ;  Views:  list, form. Group: hr officer ; Sort: creation date (last one first)
    - All Payslips (objetc: hr.payslip ; Views:  list, form, pivot. Group: hr officer ; filters: no filters; Sort: employee name alphabetically)
3/ Configuration
    - Settings
    ---Salary---
        - Rules
        - Structure
        - Contributions Registers
    ---Benefits---
        - Benefits Type
    ---Repository---
        - Employees (list)
        - Contracts

When a benefit linked to a leave is cancelled, it should refuse the leave.
When a payslip batch is deleted, also delete associated payslips.
+ Refactoring of benefit js to use "Odoo style" event binding.

TaskID: 1916136

closes odoo/odoo#30110
This commit is contained in:
Robot Odoo
2019-01-18 16:17:07 +01:00
committed by GitHub
25 changed files with 420 additions and 245 deletions
+5 -2
View File
@@ -36,15 +36,18 @@ class Employee(models.Model):
"""
Returns the contracts of the employee between date_from and date_to
"""
self.ensure_one()
# a contract is valid if it ends between the given dates
if not self:
employees = self.env['hr.employee'].search([])
else:
employees = self
clause_1 = ['&', ('date_end', '<=', date_to), ('date_end', '>=', date_from)]
# 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)
clause_3 = ['&', ('date_start', '<=', date_from), '|', ('date_end', '=', False), ('date_end', '>=', date_to)]
clause_final = expression.AND([
[('employee_id', '=', self.id), ('state', '=', 'open')],
[('employee_id', 'in', employees.ids), ('state', '=', 'open')],
expression.OR([clause_1, clause_2, clause_3])])
return self.env['hr.contract'].search(clause_final)
@@ -56,6 +56,7 @@
<field name="employee_id"/>
<field name="department_id" operator="child_of"/>
<field name="state"/>
<filter string="Running" name="running" domain="[('state', '=', 'open')]"/>
<filter string="To Renew" name="to_renew" domain="[('state', '=', 'pending')]"/>
<separator />
<filter string="Current Employee" name="current_employee" domain="[('employee_id.active','=',True)]"/>
@@ -151,7 +152,11 @@
<field name="name">hr.contract.tree</field>
<field name="model">hr.contract</field>
<field name="arch" type="xml">
<tree string="Contracts" decoration-bf="message_needaction == True">
<tree string="Contracts"
decoration-bf="message_needaction == True"
decoration-info="state == 'draft'"
decoration-muted="state in ('close', 'cancel')"
decoration-danger="state == 'pending'">
<field name="name"/>
<field name="employee_id"/>
<field name="job_id"/>
@@ -159,6 +164,7 @@
<field name="resource_calendar_id"/>
<field name="date_start"/>
<field name="date_end"/>
<field name="state"/>
<field name="wage" invisible="1"/>
<field name="message_needaction" invisible="1"/>
</tree>
+3 -1
View File
@@ -7,6 +7,8 @@
'sequence': 38,
'summary': 'Manage your employee payroll records',
'description': "",
'installable': True,
'application': True,
'depends': [
'hr_contract',
'hr_holidays',
@@ -32,7 +34,7 @@
'views/hr_leave_views.xml',
'views/resource_views.xml',
'views/hr_benefit_template.xml',
'wizard/hr_benefit_employee_views.xml',
'views/hr_payroll_menu.xml',
],
'demo': ['data/hr_payroll_demo.xml'],
'qweb': [
+17 -3
View File
@@ -18,9 +18,9 @@ class HrBenefit(models.Model):
active = fields.Boolean(default=True)
employee_id = fields.Many2one('hr.employee', required=True,
domain=lambda self: [('contract_ids.state', 'in', ('open', 'pending')), ('company_id', '=', self.env.user.company_id.id)])
date_start = fields.Datetime(required=True, string='Start')
date_stop = fields.Datetime(string='End')
duration = fields.Float(compute='_compute_duration', inverse='_inverse_duration', store=True, string="Hours")
date_start = fields.Datetime(required=True, string='From')
date_stop = fields.Datetime(string='To')
duration = fields.Float(compute='_compute_duration', inverse='_inverse_duration', store=True, string="Period")
benefit_type_id = fields.Many2one('hr.benefit.type')
color = fields.Integer(related='benefit_type_id.color', readonly=True)
state = fields.Selection([
@@ -73,6 +73,7 @@ class HrBenefit(models.Model):
vals['active'] = True
if vals['state'] == 'cancelled':
vals['active'] = False
self.mapped('leave_id').action_refuse()
return super(HrBenefit, self).write(vals)
@api.multi
@@ -283,3 +284,16 @@ class HrBenefitType(models.Model):
help="If the active field is set to false, it will allow you to hide the benefit type without removing it.")
is_leave = fields.Boolean(default=False, string="Leave")
class Contacts(models.Model):
""" Personnal calendar filter """
_name = 'hr.user.benefit.employee'
_description = 'Benefits Employees'
user_id = fields.Many2one('res.users', 'Me', required=True, default=lambda self: self.env.user)
employee_id = fields.Many2one('hr.employee', 'Employee', required=True)
active = fields.Boolean('Active', default=True)
_sql_constraints = [
('user_id_employee_id_unique', 'UNIQUE(user_id,employee_id)', 'You cannot have twice the same employee.')
]
+26 -5
View File
@@ -25,15 +25,36 @@ class HrEmployee(models.Model):
for employee in self:
employee.payslip_count = len(employee.slip_ids)
def has_non_validated_benefits(self, date_from, date_to):
return bool(self.env['hr.benefit'].search_count([
('employee_id', 'in', self.ids),
('date_start', '<=', date_to),
('date_stop', '>=', date_from),
('state', 'in', ['draft', 'confirmed'])
]))
@api.multi
@api.model
def generate_benefit(self, date_start, date_stop):
date_start = date_start.replace(tzinfo=pytz.utc)
date_stop = date_stop.replace(tzinfo=pytz.utc)
def _format_datetime(date):
fmt = '%Y-%m-%d %H:%M:%S'
date = datetime.strptime(date, fmt) if isinstance(date, str) else date
return date.replace(tzinfo=pytz.utc) if not date.tzinfo else date
for employee in self:
date_start = _format_datetime(date_start)
date_stop = _format_datetime(date_stop)
current_contracts = self.env['hr.employee']._get_contracts(date_start, date_stop)
current_employees = current_contracts.mapped('employee_id')
mapped_data = dict.fromkeys(current_employees, self.env['hr.contract'])
for contract in current_contracts:
mapped_data[contract.employee_id] |= contract
for employee, contracts in mapped_data.items():
# Approved leaves
emp_leaves = employee.resource_calendar_id.leave_ids.filtered(
lambda r:
@@ -47,7 +68,7 @@ class HrEmployee(models.Model):
hr_leave.copy_to_benefits()
new_benefits = self.env['hr.benefit']
for contract in employee._get_contracts(date_start, date_stop):
for contract in contracts:
date_start_benefits = max(date_start, datetime.combine(contract.date_start, datetime.min.time()).replace(tzinfo=pytz.utc))
date_stop_benefits = min(date_stop, datetime.combine(contract.date_end or datetime.max.date(), datetime.max.time()).replace(tzinfo=pytz.utc))
+1 -1
View File
@@ -69,5 +69,5 @@ class HrLeave(models.Model):
def action_refuse(self):
super(HrLeave, self).action_refuse()
benefits = self.env['hr.benefit'].search([('leave_id', 'in', self.ids)])
benefits.write({'display_warning': False, 'leave_id': None})
benefits.write({'display_warning': False, 'active': False})
return True
+25 -21
View File
@@ -18,22 +18,22 @@ class HrPayslip(models.Model):
_description = 'Pay Slip'
struct_id = fields.Many2one('hr.payroll.structure', string='Structure',
readonly=True, states={'draft': [('readonly', False)]},
readonly=True, states={'draft': [('readonly', False)], 'verify': [('readonly', False)]},
help='Defines the rules that have to be applied to this payslip, accordingly '
'to the contract chosen. If you let empty the field contract, this field isn\'t '
'mandatory anymore and thus the rules applied will be all the rules set on the '
'structure of all contracts of the employee valid for the chosen period')
name = fields.Char(string='Payslip Name', readonly=True,
states={'draft': [('readonly', False)]})
states={'draft': [('readonly', False)], 'verify': [('readonly', False)]})
number = fields.Char(string='Reference', readonly=True, copy=False,
states={'draft': [('readonly', False)]})
states={'draft': [('readonly', False)], 'verify': [('readonly', False)]})
employee_id = fields.Many2one('hr.employee', string='Employee', required=True, readonly=True,
states={'draft': [('readonly', False)]})
date_from = fields.Date(string='Date From', readonly=True, required=True,
default=lambda self: fields.Date.to_string(date.today().replace(day=1)), states={'draft': [('readonly', False)]})
date_to = fields.Date(string='Date To', readonly=True, required=True,
states={'draft': [('readonly', False)], 'verify': [('readonly', False)]})
date_from = fields.Date(string='From', readonly=True, required=True,
default=lambda self: fields.Date.to_string(date.today().replace(day=1)), states={'draft': [('readonly', False)], 'verify': [('readonly', False)]})
date_to = fields.Date(string='To', readonly=True, required=True,
default=lambda self: fields.Date.to_string((datetime.now() + relativedelta(months=+1, day=1, days=-1)).date()),
states={'draft': [('readonly', False)]})
states={'draft': [('readonly', False)], 'verify': [('readonly', False)]})
# this is chaos: 4 states are defined, 3 are used ('verify' isn't) and 5 exist ('confirm' seems to have existed)
state = fields.Selection([
('draft', 'Draft'),
@@ -46,32 +46,36 @@ class HrPayslip(models.Model):
\n* If the payslip is confirmed then status is set to \'Done\'.
\n* When user cancel payslip the status is \'Rejected\'.""")
line_ids = fields.One2many('hr.payslip.line', 'slip_id', string='Payslip Lines', readonly=True,
states={'draft': [('readonly', False)]})
states={'draft': [('readonly', False)], 'verify': [('readonly', False)]})
company_id = fields.Many2one('res.company', string='Company', readonly=True, copy=False,
default=lambda self: self.env['res.company']._company_default_get(),
states={'draft': [('readonly', False)]})
states={'draft': [('readonly', False)], 'verify': [('readonly', False)]})
worked_days_line_ids = fields.One2many('hr.payslip.worked_days', 'payslip_id',
string='Payslip Worked Days', copy=True, readonly=True,
states={'draft': [('readonly', False)]})
states={'draft': [('readonly', False)], 'verify': [('readonly', False)]})
input_line_ids = fields.One2many('hr.payslip.input', 'payslip_id', string='Payslip Inputs',
readonly=True, states={'draft': [('readonly', False)]})
readonly=True, states={'draft': [('readonly', False)], 'verify': [('readonly', False)]})
paid = fields.Boolean(string='Made Payment Order ? ', readonly=True, copy=False,
states={'draft': [('readonly', False)]})
note = fields.Text(string='Internal Note', readonly=True, states={'draft': [('readonly', False)]})
states={'draft': [('readonly', False)], 'verify': [('readonly', False)]})
note = fields.Text(string='Internal Note', readonly=True, states={'draft': [('readonly', False)], 'verify': [('readonly', False)]})
contract_id = fields.Many2one('hr.contract', string='Contract', readonly=True,
states={'draft': [('readonly', False)]})
states={'draft': [('readonly', False)], 'verify': [('readonly', False)]})
credit_note = fields.Boolean(string='Credit Note', readonly=True,
states={'draft': [('readonly', False)]},
states={'draft': [('readonly', False)], 'verify': [('readonly', False)]},
help="Indicates this payslip has a refund of another")
payslip_run_id = fields.Many2one('hr.payslip.run', string='Payslip Batches', readonly=True,
copy=False, states={'draft': [('readonly', False)]})
payslip_count = fields.Integer(compute='_compute_payslip_count', string="Payslip Computation Details")
payslip_run_id = fields.Many2one('hr.payslip.run', string='Batche Name', readonly=True,
copy=False, states={'draft': [('readonly', False)], 'verify': [('readonly', False)]}, ondelete='cascade')
compute_date = fields.Date('Computed On')
basic_wage = fields.Monetary(compute='_compute_basic_net')
net_wage = fields.Monetary(compute='_compute_basic_net')
currency_id = fields.Many2one(related='contract_id.currency_id')
@api.multi
def _compute_payslip_count(self):
def _compute_basic_net(self):
for payslip in self:
payslip.payslip_count = len(payslip.line_ids)
payslip.basic_wage = payslip.get_salary_line_total('BASIC')
payslip.net_wage = payslip.get_salary_line_total('NET')
@api.constrains('date_from', 'date_to')
def _check_dates(self):
@@ -44,5 +44,16 @@
<field name="perm_read" eval="0"/>
</record>
<record id="hr_user_benefit_employee" model="ir.rule">
<field name="name">Benefits/Employee calendar filter: only self</field>
<field name="model_id" ref="model_hr_user_benefit_employee"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="perm_create" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_unlink" eval="1"/>
<field name="perm_read" eval="0"/>
</record>
</data>
</odoo>
@@ -11,3 +11,4 @@ access_hr_payslip_run,hr.payslip.run,model_hr_payslip_run,hr_payroll.group_hr_pa
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_benefit,access_hr_benefit,model_hr_benefit,group_hr_payroll_user,1,1,1,1
access_hr_benefit_type,access_hr_benefit_type,model_hr_benefit_type,group_hr_payroll_user,1,1,1,1
access_hr_benefit_employee,hr.benefit.employee,model_hr_user_benefit_employee,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
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_benefit access_hr_benefit model_hr_benefit group_hr_payroll_user 1 1 1 1
13 access_hr_benefit_type access_hr_benefit_type model_hr_benefit_type group_hr_payroll_user 1 1 1 1
14 access_hr_benefit_employee hr.benefit.employee model_hr_user_benefit_employee group_hr_payroll_user 1 1 1 1
+101 -79
View File
@@ -3,9 +3,19 @@ odoo.define('hr_payroll.benefit.view_custo', function(require) {
var core = require('web.core');
var CalendarController = require("web.CalendarController");
var CalendarModel = require('web.CalendarModel');
var CalendarRenderer = require('web.CalendarRenderer');
var CalendarView = require('web.CalendarView');
var viewRegistry = require('web.view_registry');
var _t = core._t;
CalendarController.include({
var BenefitCalendarController = CalendarController.extend({
events: {
'click .btn-benefit-generate': '_onGenerateBenefits',
'click .btn-benefit-validate': '_onValidateBenefits',
'click .btn-payslip-generate': '_onGeneratePayslips',
},
update: function () {
var self = this;
@@ -14,35 +24,9 @@ odoo.define('hr_payroll.benefit.view_custo', function(require) {
});
},
_renderGenerateButton: function(date_from, date_to, employee_ids, secondary) {
var self = this;
var primary = !secondary ? 'btn-primary' : 'btn-secondary';
var txt = _t("Generate Benefits");
_renderBenefitButton: function (text, event_class) {
this.$buttons.find('.o_calendar_button_month').after(
$('<button class="btn ' + primary + ' btn-benefit" type="button">'+ txt +'</button>')
.off('click')
.on('click', function (e) {
e.preventDefault();
e.stopImmediatePropagation();
var date_fmt = 'YYYY-MM-DD HH:mm:ss';
var options = {
on_close: function () {
self.reload();
},
};
self.do_action({
type: 'ir.actions.act_window',
name: txt,
res_model: 'hr.benefit.employees',
view_type: 'form',
views: [[false,'form']],
target: 'new',
context: {
'start_benefits': date_from.format(date_fmt),
'stop_benefits':date_to.format(date_fmt),
},
}, options);
})
$('<button class="btn btn-primary btn-benefit ' + event_class + '" type="button">'+ _t(text) +'</button>')
);
},
@@ -51,58 +35,59 @@ odoo.define('hr_payroll.benefit.view_custo', function(require) {
return;
}
var firstDay = this.model.data.target_date.clone().startOf('month');
var lastDay = this.model.data.target_date.clone().endOf('month');
var events = this._checkDataInRange(firstDay, lastDay, this.model.data.data);
var is_validated = this._checkValidation(events);
this.firstDay = this.model.data.target_date.clone().startOf('month');
this.lastDay = this.model.data.target_date.clone().endOf('month');
this.events = this._checkDataInRange(this.firstDay, this.lastDay, this.model.data.data);
var is_validated = this._checkValidation(this.events);
this.$buttons.find('.btn-benefit').remove();
var employee_ids = _.map(events, function (event) { return event.record.employee_id[0]; });
employee_ids = _.uniq(employee_ids);
if (this.events.length === 0) {
this._renderBenefitButton("Generate Benefits", 'btn-benefit-generate');
}
if (is_validated && this.events.length !== 0) {
this._renderBenefitButton("Generate Payslips", 'btn-payslip-generate');
}
else if (!is_validated) {
this._renderBenefitButton("Validate Benefits", 'btn-benefit-validate');
}
},
_onGeneratePayslips: function (e) {
e.preventDefault();
e.stopImmediatePropagation();
var date_fmt = 'YYYY-MM-DD';
this.do_action('hr_payroll.action_generate_payslips_from_benefits', {
additional_context: {
default_date_start: this.firstDay.format(date_fmt),
default_date_end: this.lastDay.format(date_fmt),
},
});
},
_onValidateBenefits: function (e) {
e.preventDefault();
e.stopImmediatePropagation();
var self = this;
if (this.model.data.domain.length !== 0) { // select by default the employee in the domain
var employee_search_id = (this.model.data.domain[0][0] === 'employee_id' && this.model.data.domain[0][1] === '=')? [this.model.data.domain[0][2]]: null;
}
if (events.length === 0) { // Generate button
this._renderGenerateButton(firstDay, lastDay, employee_search_id);
} else {
this._renderGenerateButton(firstDay, lastDay, employee_ids, true);
}
if (is_validated && events.length !== 0) { // Generate Payslip button
this.$buttons.find('.o_calendar_button_month').after(
$('<button class="btn btn-primary btn-benefit" type="button">'+ _t('Generate Payslips') +'</button>')
.off('click')
// action_hr_payslip_by_employees
.on('click', function (e) {
e.preventDefault();
e.stopImmediatePropagation();
var date_fmt = 'YYYY-MM-DD';
self.do_action('hr_payroll.action_hr_payslip_by_employees', {
additional_context: {
default_employee_ids: employee_ids || [],
default_date_start: firstDay.format(date_fmt),
default_date_end: lastDay.format(date_fmt),
},
});
})
);
}
else if (!is_validated) { // Validate button
this.$buttons.find('.o_calendar_button_month').after(
$('<button class="btn btn-primary btn-benefit" type="button">'+ _t('Validate Benefits') +'</button>')
.off('click')
.on('click', function (e) {
e.preventDefault();
e.stopImmediatePropagation();
self._rpc({
model: 'hr.benefit',
method: 'action_validate',
args: [_.map(events, function (event) { return event.record.id; })],
}).then(function () {
return self.reload();
});
})
);
}
this._rpc({
model: 'hr.benefit',
method: 'action_validate',
args: [_.map(this.events, function (event) { return event.record.id; })],
}).then(function () {
return self.reload();
});
},
_onGenerateBenefits: function (e) {
e.preventDefault();
e.stopImmediatePropagation();
var date_fmt = 'YYYY-MM-DD HH:mm:ss';
var self = this;
this._rpc({
model: 'hr.employee',
method: 'generate_benefit',
args: [this.firstDay.format(date_fmt), this.lastDay.format(date_fmt)],
}).then(function () {
self.reload();
});
},
_checkDataInRange: function (firstDay, lastDay, events) {
@@ -125,4 +110,41 @@ odoo.define('hr_payroll.benefit.view_custo', function(require) {
this._renderBenefitButtons();
},
});
var BenefitCalendarModel = CalendarModel.extend({
/**
* Display everybody's benefits if no employee filter exists
* @private
* @override
* @param {any} filter
* @returns {Deferred}
*/
_loadFilter: function (filter) {
return this._super.apply(this, arguments).then(function () {
var filters = filter.filters;
var all_filter = filters[filters.length - 1];
if (all_filter) {
all_filter.label = _t("Everybody's benefits");
if (filter.write_model && filter.filters.length <= 1 && all_filter.active === undefined) {
filter.all = true;
all_filter.active = true;
}
}
});
}
});
var BenefitCalendarView = CalendarView.extend({
config: _.extend({}, CalendarView.prototype.config, {
Controller: BenefitCalendarController,
Model: BenefitCalendarModel,
Renderer: CalendarRenderer,
}),
});
viewRegistry.add('benefits_calendar', BenefitCalendarView);
});
@@ -27,8 +27,6 @@ class TestPayslipFlow(TestPayslipBase):
context = {
"lang": "en_US", "tz": False, "active_model": "ir.ui.menu",
"department_id": False, "section_id": False,
"active_ids": [self.ref("hr_payroll.menu_department_tree")],
"active_id": self.ref("hr_payroll.menu_department_tree")
}
# I click on 'Compute Sheet' button on payslip
richard_payslip.with_context(context).compute_sheet()
+20 -25
View File
@@ -1,22 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record model="ir.actions.server" id="generate_benefit">
<field name="name">Generate Benefit</field>
<field name="model_id" ref="hr.model_hr_employee"/>
<field name="binding_model_id" ref="hr.model_hr_employee"/>
<field name="state">code</field>
<field name="code">
if records:
records.generate_benefit()
</field>
</record>
<record id="hr_benefit_action" model="ir.actions.act_window">
<field name="name">Benefit</field>
<field name="res_model">hr.benefit</field>
<field name="view_type">form</field>
<field name="view_mode">calendar,tree,form</field>
<field name="view_mode">calendar,tree,form,pivot</field>
</record>
<record id="hr_benefit_calendar" model="ir.ui.view">
@@ -24,6 +13,7 @@ if records:
<field name="model">hr.benefit</field>
<field name="arch" type="xml">
<calendar string="Benefit"
js_class="benefits_calendar"
date_start="date_start"
date_stop="date_stop"
mode="month"
@@ -31,14 +21,13 @@ if records:
color="benefit_type_id"
event_limit="5">
<field name="state" invisible="1"/>
<field name="employee_id" invisible="1"/>
<field name="display_warning" invisible="1"/>
<field name="name"/>
<!-- Sidebar favorites filters -->
<field name="employee_id" write_model="hr.user.benefit.employee" write_field="employee_id" avatar_field="image_small"/>
<templates>
<t t-name="calendar-box">
<t t-set="color" t-value="widget.getColor(event.color_index)"/>
<div t-att-style="typeof color === 'string' ? ('background-color:'+color)+';' : ''" t-attf-class="#{record.is_highlighted &amp;&amp; record.is_highlighted.value ? 'o_event_hightlight' : ''} #{typeof color === 'number' ? 'o_calendar_color_'+color : ''}">
<field name="name"/>
<div class="fc-time"/>
<div class="o_fields">
<t t-if="record.display_warning">
@@ -99,7 +88,10 @@ if records:
<group>
<field name="date_start" attrs="{'readonly': [('state', '!=', 'draft')]}" />
<field name="date_stop" attrs="{'readonly': [('state', '!=', 'draft')]}" />
<field name="duration" attrs="{'readonly': [('state', '!=', 'draft')]}" />
<label for="duration" string="Period"/>
<div>
<field name="duration" nolabel="1" attrs="{'readonly': [('state', '!=', 'draft')]}" /><span class="ml8">Hours</span>
</div>
</group>
</group>
</sheet>
@@ -115,6 +107,8 @@ if records:
<field name="employee_id"/>
<field name="name"/>
<filter string="My Benefits" domain="[('employee_id.user_id','=',uid)]" name="my_benefits"/>
<filter string="With error" domain="[('display_warning','=', True)]" name="benefits_error"/>
<filter name="date_filter" string="Date" date="date_start"/>
<filter name="group_employee" string="Employee" context="{'group_by':'employee_id'}"/>
</search>
</field>
@@ -145,19 +139,20 @@ if records:
</field>
</record>
<menuitem
id="hr_menu_benefit_menu" name="Benefits" action="hr_benefit_action" parent="menu_hr_payroll_root" sequence="25"/>
<record model="ir.actions.server" id="action_generate_payslips_from_benefits">
<field name="name">Generate payslips</field>
<field name="model_id" ref="model_hr_payslip_employees"/>
<field name="binding_model_id" ref="model_hr_payslip_employees" />
<field name="state">code</field>
<field name="code">
action = env['hr.payslip.employees'].create({}).compute_sheet()
</field>
</record>
<record id="action_view_hr_benefit_type" model="ir.actions.act_window">
<field name="name">Benefit Types</field>
<field name="res_model">hr.benefit.type</field>
<field name="view_mode">tree,kanban,form</field>
</record>
<menuitem
id="menu_hr_benefit_type_view"
action="action_view_hr_benefit_type"
parent="menu_hr_payroll_configuration"
sequence="25"
/>
</odoo>
</odoo>
+15 -6
View File
@@ -1,11 +1,5 @@
<?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>
@@ -24,4 +18,19 @@
</xpath>
</field>
</record>
<record id="action_hr_contract_repository" model="ir.actions.act_window">
<field name="name">Contracts</field>
<field name="res_model">hr.contract</field>
<field name="view_type">form</field>
<field name="view_mode">tree,kanban,form,activity</field>
<field name="domain">[('employee_id', '!=', False)]</field>
<field name="context">{'search_default_running': 1, 'search_default_to_renew': 1}</field>
<field name="search_view_id" ref="hr_contract.hr_contract_view_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new contract
</p>
</field>
</record>
</odoo>
+147
View File
@@ -0,0 +1,147 @@
<?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_salary_computation"
name="Salary Computation"
parent="menu_hr_payroll_root"
sequence="50"
groups="hr_payroll.group_hr_payroll_manager"/>
<menuitem
id="menu_hr_payroll_payslips"
name="Employee Payslips"
parent="menu_hr_payroll_root"
sequence="60"
groups="hr_payroll.group_hr_payroll_manager"/>
<menuitem
id="menu_hr_payroll_configuration"
name="Configuration"
parent="menu_hr_payroll_root"
sequence="100"
groups="hr_payroll.group_hr_payroll_manager"/>
<!-- **** Salary Computation **** -->
<menuitem
id="menu_hr_payroll_employee_payslips"
name="Payslips"
parent="menu_hr_payroll_salary_computation"
sequence="60"
action="action_view_hr_payslip_month_form"
groups="hr_payroll.group_hr_payroll_manager"/>
<!-- TODO add filter-->
<menuitem
id="hr_menu_benefit_menu"
name="Benefits"
action="hr_benefit_action"
parent="menu_hr_payroll_salary_computation"
sequence="25"/>
<!-- **** Employee Payslips **** -->
<menuitem
id="menu_hr_payslip_run"
action="action_hr_payslip_run_tree"
name="Batches"
parent="menu_hr_payroll_payslips"/>
<menuitem
id="menu_hr_payroll_employee_all_payslips"
name="All Payslips"
parent="menu_hr_payroll_payslips"
sequence="60"
action="action_view_hr_payslip_form"
groups="hr_payroll.group_hr_payroll_manager"/>
<!-- **** Configuration **** -->
<menuitem
id="menu_hr_payroll_global_settings"
name="Settings"
parent="menu_hr_payroll_configuration"
sequence="0"
action="action_hr_payroll_configuration"
groups="base.group_system"/>
<!-- Benefits Configuration -->
<menuitem
id="menu_hr_benefit_confirguration"
name="Benefits"
parent="menu_hr_payroll_configuration"
sequence="50"
/>
<menuitem
id="menu_hr_benefit_type_view"
action="action_view_hr_benefit_type"
parent="menu_hr_benefit_confirguration"
/>
<!-- Salary Configuration -->
<menuitem
id="menu_hr_salary_configuration"
name="Salary"
parent="menu_hr_payroll_configuration"
sequence="30"
/>
<menuitem
id="menu_action_hr_salary_rule_form"
action="action_salary_rule_form"
name="Rules"
parent="menu_hr_salary_configuration"
sequence="10"/>
<menuitem
id="menu_hr_salary_rule_category"
action="action_hr_salary_rule_category"
name="Rule Categories"
parent="menu_hr_salary_configuration"
sequence="20"
groups="base.group_no_one"
/>
<menuitem
id="menu_hr_payroll_structure_view"
action="action_view_hr_payroll_structure_list_form"
name="Structures"
parent="menu_hr_salary_configuration"
sequence="30"
/>
<menuitem
id="menu_action_hr_contribution_register_form"
action="action_contribution_register_form"
parent="menu_hr_salary_configuration"
sequence="50"
/>
<!-- Repository Configuration -->
<menuitem
id="menu_hr_repository_confirguration"
name="Repository"
parent="menu_hr_payroll_configuration"
sequence="50"
/>
<menuitem
id="menu_hr_payroll_employees_configuration"
action="hr.open_view_employee_list_my"
parent="menu_hr_repository_confirguration"
sequence="10"
/>
<menuitem
id="menu_hr_payroll_contracts_configuration"
action="hr_payroll.action_hr_contract_repository"
parent="menu_hr_repository_confirguration"
sequence="20"
/>
</odoo>
+30 -17
View File
@@ -71,14 +71,16 @@
<field name="model">hr.payslip</field>
<field name="arch" type="xml">
<tree decoration-info="state in ('confirm','verify')" decoration-muted="state == 'cancel'" js_class="hr_payroll_payslip_tree" string="Payslips">
<field name="currency_id" invisible="1"/>
<field name="number"/>
<field name="employee_id"/>
<field name="name"/>
<field name="payslip_run_id"/>
<field name="date_from"/>
<field name="date_to"/>
<field name="basic_wage" widget="monetary" options="{'currency_field': 'currency_id'}"/>
<field name="net_wage" widget="monetary" options="{'currency_field': 'currency_id'}"/>
<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>
@@ -137,14 +139,6 @@
<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>
@@ -154,9 +148,9 @@
<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="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}" required="1"/>
<field name="number"/>
<field name="struct_id" attrs="{'required':[('contract_id','!=',False)]}"/>
<field name="struct_id" required="1"/>
<field name="name"/>
<field name="credit_note" invisible="1"/>
</group>
@@ -259,9 +253,12 @@
<field name="employee_id"/>
<field name="name" string="Payslips" filter_domain="['|',('name','ilike',self),('number','ilike',self)]"/>
<field name="date_from"/>
<field name="contract_id"/>
<field name="payslip_run_id"/>
<filter string="Draft" name="draft" domain="[('state','=','draft')]" help="Draft Slip"/>
<filter string="To Compute" name="draft" domain="[('state','=','draft')]" help="Draft Slip"/>
<filter string="To Confirm" name="draft" domain="[('state','=','verify')]" help="Slips to Confirm"/>
<filter string="Done" name="done" domain="[('state','=','done')]" help="Done Slip"/>
<filter name="date_filter" string="Date" date="date_to" default_period="last_month"/>
<separator/>
<filter string="Credit Notes" name="credit_note" domain="[('credit_note', '=', True)]"/>
<group expand="0" string="Group By">
@@ -277,12 +274,18 @@
<field name="name">Employee Payslips</field>
<field name="res_model">hr.payslip</field>
<field name="view_type">form</field>
<field name="view_mode">tree,kanban,form</field>
<field name="view_mode">tree,kanban,form,pivot</field>
<field name="search_view_id" ref="view_hr_payslip_filter"/>
<field name="context">{'search_default_group_by_batch': 1}</field>
</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="action_view_hr_payslip_month_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_mode">tree,kanban,form</field>
<field name="search_view_id" ref="view_hr_payslip_filter"/>
<field name="context">{"search_default_group_by_batch":1}</field>
</record>
<record id="act_hr_employee_payslip_list" model="ir.actions.act_window">
<field name="res_model">hr.payslip</field>
@@ -398,7 +401,17 @@
<field name="view_mode">tree,kanban,form</field>
<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"/>
<record model="ir.actions.server" id="action_hr_payroll_confirm_payroll">
<field name="name">Confirm</field>
<field name="model_id" ref="hr_payroll.model_hr_payslip"/>
<field name="binding_model_id" ref="hr_payroll.model_hr_payslip"/>
<field name="state">code</field>
<field name="code">
if records:
records.action_payslip_done()
</field>
</record>
<!-- Shortcuts -->
@@ -100,12 +100,7 @@
<field name="view_type">form</field>
<field name="view_mode">tree,kanban,form</field>
</record>
<menuitem
id="menu_hr_payroll_structure_view"
action="action_view_hr_payroll_structure_list_form"
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>
@@ -155,13 +150,7 @@
<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"
/>
<!--
Contribution Register
@@ -239,12 +228,6 @@
</p>
</field>
</record>
<menuitem
id="menu_action_hr_contribution_register_form"
action="action_contribution_register_form"
parent="menu_hr_payroll_configuration"
sequence="14"
/>
<!-- Salary Rules -->
@@ -380,8 +363,6 @@
<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"
@@ -61,6 +61,4 @@
<field name="context">{'module' : 'hr_payroll'}</field>
</record>
<menuitem id="menu_hr_payroll_global_settings" name="Settings"
parent="menu_hr_payroll_configuration" sequence="0" action="action_hr_payroll_configuration" groups="base.group_system"/>
</odoo>
-1
View File
@@ -3,4 +3,3 @@
from . import hr_payroll_payslips_by_employees
from . import hr_payroll_contribution_register_report
from . import hr_benefit_employee
@@ -1,26 +0,0 @@
# -*- 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
class HrPayslipEmployees(models.TransientModel):
_name = 'hr.benefit.employees'
_description = 'Generate benefits for all selected employees'
def _get_domain(self):
return [('contract_ids.state', 'in', ('open', 'pending')), ('company_id', '=', self.env.user.company_id.id)]
employee_ids = fields.Many2many('hr.employee', string='Employees', domain=lambda self: self._get_domain())
def generate_benefit(self):
if not self.employee_ids:
raise UserError(_("You must select employee(s) to generate benefits."))
date_start = self.env.context.get('start_benefits')
date_stop = self.env.context.get('stop_benefits')
date_start = fields.Datetime.from_string(date_start)
date_stop = fields.Datetime.from_string(date_stop)
self.employee_ids.generate_benefit(date_start, date_stop)
@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_hr_benefit_by_employees" model="ir.ui.view">
<field name="name">hr_benefit_employees</field>
<field name="model">hr.benefit.employees</field>
<field name="arch" type="xml">
<form string="Benefits by Employees">
<header>
<button icon="fa-cogs" string="Generate" name="generate_benefit" type="object" class="oe_highlight" />
</header>
<group>
<span colspan="4" nolabel="1">This wizard will generate benefits for all selected employee(s) based on attendances.</span>
</group>
<group colspan="4" >
<separator string="Employees" colspan="4"/>
<newline/>
<field name="employee_ids" nolabel="1"/>
</group>
<footer></footer>
</form>
</field>
</record>
</odoo>
@@ -11,7 +11,7 @@ class HrPayslipEmployees(models.TransientModel):
_description = 'Generate payslips for all selected employees'
def _get_available_contracts_domain(self):
return [('contract_ids.state', 'in', ('open', 'pending')), ('company_id', '=', self.env.user.company_id.id)]
return [('contract_ids.state', 'in', ('open', 'pending', 'close')), ('company_id', '=', self.env.user.company_id.id)]
def _get_employees(self):
return self.env['hr.employee'].search(self._get_available_contracts_domain())
@@ -39,7 +39,7 @@ class HrPayslipEmployees(models.TransientModel):
payslips = self.env['hr.payslip']
Payslip = self.env['hr.payslip']
for employee in self.employee_ids:
for employee in self.employee_ids.filtered(lambda e: not e.has_non_validated_benefits(payslip_run.date_start, payslip_run.date_end)):
values = Payslip.default_get(Payslip.fields_get())
values.update({
'employee_id': employee.id,
@@ -27,10 +27,10 @@ class HrPayslipLine(models.Model):
class HrPayslip(models.Model):
_inherit = 'hr.payslip'
date = fields.Date('Date Account', states={'draft': [('readonly', False)]}, readonly=True,
date = fields.Date('Date Account', states={'draft': [('readonly', False)], 'verify': [('readonly', False)]}, readonly=True,
help="Keep empty to use the period of the validation(Payslip) date.")
journal_id = fields.Many2one('account.journal', 'Salary Journal', readonly=True, required=True,
states={'draft': [('readonly', False)]}, default=lambda self: self.env['account.journal'].search([('type', '=', 'general')], limit=1))
states={'draft': [('readonly', False)], 'verify': [('readonly', False)]}, default=lambda self: self.env['account.journal'].search([('type', '=', 'general')], limit=1))
move_id = fields.Many2one('account.move', 'Accounting Entry', readonly=True, copy=False)
@api.model
@@ -23,8 +23,6 @@ class TestHrPayrollAccount(common.TransactionCase):
self._load('account', 'test', 'account_minimal_test.xml')
self.payslip_action_id = self.ref('hr_payroll.menu_department_tree')
self.res_partner_bank = self.env['res.partner.bank'].create({
'acc_number': '001-9876543-21',
'partner_id': self.ref('base.res_partner_12'),
@@ -93,7 +91,7 @@ class TestHrPayrollAccount(common.TransactionCase):
self.assertEqual(self.hr_payslip.state, 'draft', 'State not changed!')
# I click on "Compute Sheet" button.
context = {"lang": "en_US", "tz": False, "active_model": 'hr.payslip', "department_id": False, "active_ids": [self.payslip_action_id], "section_id": False, "active_id": self.payslip_action_id}
context = {"lang": "en_US", "tz": False, "active_model": 'hr.payslip', "department_id": False, "section_id": False}
self.hr_payslip.with_context(context).compute_sheet()
# I want to check cancel button. So I first cancel the sheet then make it set to draft.
@@ -481,7 +481,7 @@ return AbstractModel.extend({
*/
_loadFilter: function (filter) {
if (!filter.write_model) {
return;
return $.when();
}
var field = this.fields[filter.fieldName];
@@ -112,11 +112,13 @@ var SidebarFilter = Widget.extend(FieldManagerMixin, {
_onFieldChanged: function (event) {
var self = this;
event.stopPropagation();
var createValues = {'user_id': session.uid};
var value = event.data.changes[this.write_field].id;
createValues[this.write_field] = value;
this._rpc({
model: this.write_model,
method: 'create',
args: [{'user_id': session.uid,'partner_id': value,}],
args: [createValues],
})
.then(function () {
self.trigger_up('changeFilter', {