mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
[REM] hr_payroll_hibou: available in professional
H14528
This commit is contained in:
@@ -1 +0,0 @@
|
||||
from . import models
|
||||
@@ -1,32 +0,0 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Hibou Payroll',
|
||||
'author': 'Hibou Corp. <hello@hibou.io>',
|
||||
'version': '15.0.2.1.0',
|
||||
'category': 'Payroll Localization',
|
||||
'depends': [
|
||||
'hr_payroll',
|
||||
'hr_contract_reports',
|
||||
'hibou_professional',
|
||||
],
|
||||
'description': """
|
||||
Hibou Payroll
|
||||
=============
|
||||
|
||||
Base module for fixing specific qwerks or assumptions in the way Payroll Odoo Enterprise Edition behaves.
|
||||
|
||||
2.1.0 : fixes precision error in upstream by changing `normal_wage` field from Integer to Float
|
||||
""",
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/cron_data.xml',
|
||||
'views/payroll_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/update_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
],
|
||||
'auto_install': True,
|
||||
'license': 'OPL-1',
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<data noupdate="1">
|
||||
<!-- Cron jobs -->
|
||||
<record model="ir.cron" id="payroll_publisher_update_cron">
|
||||
<field name="name">Payroll Update: download latest parameter values/rates.</field>
|
||||
<field name="model_id" ref="hr_payroll_hibou.model_hr_payroll_publisher_update"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model.cron_payroll_update()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">weeks</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="nextcall" eval="(datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="doall" eval="False"/>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
</odoo>
|
||||
@@ -1,212 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * hr_payroll_hibou
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 15.0+e\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-10-12 01:26+0000\n"
|
||||
"PO-Revision-Date: 2021-10-12 01:26+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model:ir.model.fields.selection,name:hr_payroll_hibou.selection__res_config_settings__payslip_sum_type__date
|
||||
msgid "Accounting Date"
|
||||
msgstr "Fecha de Contabilidad"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model:ir.model.fields,field_description:hr_payroll_hibou.field_res_config_settings__module_hr_payroll_attendance
|
||||
msgid "Attendance Entries & Overtime"
|
||||
msgstr "Entradas de Asistencias y Horas Extras"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model:ir.model.fields,help:hr_payroll_hibou.field_res_config_settings__payslip_sum_type
|
||||
msgid ""
|
||||
"Behavior for what payslips are considered during rule execution. Stock Odoo behavior would not consider a payslip starting on 2019-12-30 ending on 2020-01-07 when summing a 2020 payslip category.\n"
|
||||
"\n"
|
||||
"Accounting Date requires Payroll Accounting and will fall back to date_to as the 'closest behavior'."
|
||||
msgstr ""
|
||||
"El comportamiento que se considera en los recibos de nómina durante la ejecución de una regla. El comportamiento de Stock Odoo no considerará a un recibo de nomina a partir de 2019-12-30 y que termina en 2020-01-07 al sumar una categoría de nómina de 2020.\n"
|
||||
"\n"
|
||||
"La fecha de contabilidad requiere contabilidad de nomina y usara date_to de respaldo como el 'comportamiento más cercana'."
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model:ir.model.fields,field_description:hr_payroll_hibou.field_res_config_settings__module_hr_payroll_commission
|
||||
msgid "Commission"
|
||||
msgstr "Comisión"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model:ir.model,name:hr_payroll_hibou.model_res_config_settings
|
||||
msgid "Config Settings"
|
||||
msgstr "Opciones de configuración"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model_terms:ir.ui.view,arch_db:hr_payroll_hibou.res_config_settings_view_form_hibou
|
||||
msgid "Configure Matching & Limits"
|
||||
msgstr "Configurar Contribuciones & Limites"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model_terms:ir.ui.view,arch_db:hr_payroll_hibou.res_config_settings_view_form_hibou
|
||||
msgid ""
|
||||
"Customize the behavior of what payslips are eligible when summing over date ranges in rules.\n"
|
||||
" Generally, \"Date To\" or \"Accounting Date\" would be preferred in the United States and anywhere\n"
|
||||
" else where the ending date on the payslip is used to calculate wage bases."
|
||||
msgstr ""
|
||||
"Personalizar el comportamiento de las nóminas elegibles al sumar rangos de fechas en las reglas.\n"
|
||||
" Generalmente, \"Hasta La Fecha\" o \"Fecha Contable\" sería preferible en los Estados Unidos y en cualquier lugar\n"
|
||||
" donde la fecha de finalización en la nómina se utiliza para calcular las bases salariales.."
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model:ir.model.fields.selection,name:hr_payroll_hibou.selection__res_config_settings__payslip_sum_type__date_from
|
||||
msgid "Date From"
|
||||
msgstr "Desde la Fecha"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model:ir.model.fields.selection,name:hr_payroll_hibou.selection__res_config_settings__payslip_sum_type__date_to
|
||||
msgid "Date To"
|
||||
msgstr "Hasta la Fecha"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model:ir.model.fields,field_description:hr_payroll_hibou.field_hr_payroll_structure_type__default_schedule_pay
|
||||
msgid "Default Scheduled Pay"
|
||||
msgstr "Pago Programado Predeterminado"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model:ir.model.fields,help:hr_payroll_hibou.field_hr_payroll_structure__schedule_pay
|
||||
#: model:ir.model.fields,help:hr_payroll_hibou.field_hr_payroll_structure_type__default_schedule_pay
|
||||
msgid "Defines the frequency of the wage payment."
|
||||
msgstr "Define la frecuencia del pago de salario."
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model:ir.model,name:hr_payroll_hibou.model_hr_contract
|
||||
msgid "Employee Contract"
|
||||
msgstr "Contrato del empleado"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model_terms:ir.ui.view,arch_db:hr_payroll_hibou.res_config_settings_view_form_hibou
|
||||
msgid ""
|
||||
"Extend Attendance into Payroll with Work Types, Overtime!\n"
|
||||
" <strong>Hibou Professional</strong>"
|
||||
msgstr ""
|
||||
"¡Extender asistencias en nóminas con tipos de trabajos, Horas Extra!\n"
|
||||
" <strong>Hibou Profesional</strong>"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model_terms:ir.ui.view,arch_db:hr_payroll_hibou.res_config_settings_view_form_hibou
|
||||
msgid ""
|
||||
"Extend Timesheets into Payroll with Work Types, Overtime!\n"
|
||||
" <strong>Hibou Professional</strong>"
|
||||
msgstr ""
|
||||
"¡Extender hojas de asistencia en nominas con tipos de trabajos, Horas Extra!\n"
|
||||
" <strong>Hibou Profesional</strong>"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model_terms:ir.ui.view,arch_db:hr_payroll_hibou.res_config_settings_view_form_hibou
|
||||
msgid "Hibou Payroll"
|
||||
msgstr "Hibou Nómina"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model:ir.model.fields.selection,name:hr_payroll_hibou.selection__hr_contract__wage_type__hourly
|
||||
msgid "Hourly Wage"
|
||||
msgstr "Salario por Hora"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model_terms:ir.ui.view,arch_db:hr_payroll_hibou.res_config_settings_view_form_hibou
|
||||
msgid ""
|
||||
"Pay Commissions in Payroll!\n"
|
||||
" <strong>Hibou Professional</strong>"
|
||||
msgstr ""
|
||||
"¡Paga comisiones en Nómina!\n"
|
||||
" <strong>Hibou Profesional</strong>"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model:ir.model,name:hr_payroll_hibou.model_hr_payslip
|
||||
msgid "Pay Slip"
|
||||
msgstr "Recibo de nómina"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model:ir.model.fields,field_description:hr_payroll_hibou.field_res_config_settings__module_hr_payroll_payment
|
||||
msgid "Payments & Advanced Accounting"
|
||||
msgstr "Pagos & Contabilidad Avanzada"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model:ir.model.fields,field_description:hr_payroll_hibou.field_res_config_settings__payslip_sum_type
|
||||
#: model_terms:ir.ui.view,arch_db:hr_payroll_hibou.res_config_settings_view_form_hibou
|
||||
msgid "Payslip Sum Behavior"
|
||||
msgstr "Comportamiento de la suma de recibos de nómina"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model:ir.model.fields.selection,name:hr_payroll_hibou.selection__hr_contract__wage_type__monthly
|
||||
msgid "Period Fixed Wage"
|
||||
msgstr "Periodo Salario Fijo"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model_terms:ir.ui.view,arch_db:hr_payroll_hibou.res_config_settings_view_form_hibou
|
||||
msgid ""
|
||||
"Provide retirement plans with optional company matching.\n"
|
||||
" <strong>Hibou Professional</strong>"
|
||||
msgstr ""
|
||||
"Proporcionar planes de jubilación con contribucion opcional de la compañía.\n"
|
||||
" <strong>Hibou Profesional</strong>"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model_terms:ir.ui.view,arch_db:hr_payroll_hibou.res_config_settings_view_form_hibou
|
||||
msgid ""
|
||||
"Register payments on payslips! Have control over journal entries created from\n"
|
||||
" payroll to include partner details, set grouping options, and apply fiscal position\n"
|
||||
" account mappings.\n"
|
||||
" <strong>Hibou Professional</strong>"
|
||||
msgstr ""
|
||||
"Registrar pagos en recibos de nomina! Ten control sobre sus asientos contables creado desde\n"
|
||||
" la nomina para incluir detalles del socio, establecer opciones de agrupamiento, y aplicar la asignación de cuentas\n"
|
||||
" de posición fiscal.\n"
|
||||
" <strong>Hibou Profesional</strong>"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model:ir.model,name:hr_payroll_hibou.model_hr_payroll_structure
|
||||
msgid "Salary Structure"
|
||||
msgstr "Estructura salarial"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model:ir.model,name:hr_payroll_hibou.model_hr_payroll_structure_type
|
||||
msgid "Salary Structure Type"
|
||||
msgstr "Tipo de estructura salarial"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model:ir.model.fields,field_description:hr_payroll_hibou.field_hr_payroll_structure__schedule_pay
|
||||
msgid "Scheduled Pay"
|
||||
msgstr "Pago Programado"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model:ir.model.fields.selection,name:hr_payroll_hibou.selection__hr_payroll_structure__schedule_pay__semi-monthly
|
||||
#: model:ir.model.fields.selection,name:hr_payroll_hibou.selection__hr_payroll_structure_type__default_schedule_pay__semi-monthly
|
||||
msgid "Semi-monthly"
|
||||
msgstr "Semestral"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model:ir.model.fields,field_description:hr_payroll_hibou.field_res_config_settings__module_hr_payroll_timesheet
|
||||
msgid "Timesheet Entries & Overtime"
|
||||
msgstr "Entradas de hojas de asistencias y horas extra"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model:ir.model.fields,field_description:hr_payroll_hibou.field_res_config_settings__module_l10n_us_hr_payroll
|
||||
msgid "USA Payroll"
|
||||
msgstr "Nómina de EE.UU"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model:ir.model.fields,field_description:hr_payroll_hibou.field_res_config_settings__module_l10n_us_hr_payroll_401k
|
||||
#: model_terms:ir.ui.view,arch_db:hr_payroll_hibou.res_config_settings_view_form_hibou
|
||||
msgid "USA Payroll 401k"
|
||||
msgstr "Nómina de EE.UUU 401K"
|
||||
|
||||
#. module: hr_payroll_hibou
|
||||
#: model:ir.model.fields,field_description:hr_payroll_hibou.field_hr_contract__wage_type
|
||||
#: model:ir.model.fields,field_description:hr_payroll_hibou.field_hr_payslip__wage_type
|
||||
msgid "Wage Type"
|
||||
msgstr "Tipo de salario"
|
||||
@@ -1,6 +0,0 @@
|
||||
from . import browsable_object
|
||||
from . import hr_contract
|
||||
from . import hr_payslip
|
||||
from . import hr_salary_rule
|
||||
from . import res_config_settings
|
||||
from . import update
|
||||
@@ -1,156 +0,0 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields
|
||||
from odoo.addons.hr_payroll.models import browsable_object
|
||||
|
||||
|
||||
class BrowsableObject(object):
|
||||
def __init__(self, employee_id, dict, env):
|
||||
self.employee_id = employee_id
|
||||
self.dict = dict
|
||||
self.env = env
|
||||
# Customization to allow changing the behavior of the discrete browsable objects.
|
||||
# you can think of this as 'compiling' the query based on the configuration.
|
||||
sum_field = env['ir.config_parameter'].sudo().get_param('hr_payroll.payslip.sum_behavior', 'date_from')
|
||||
if sum_field == 'date' and 'date' not in env['hr.payslip']:
|
||||
# missing attribute, closest by definition
|
||||
sum_field = 'date_to'
|
||||
if not sum_field:
|
||||
sum_field = 'date_from'
|
||||
self._compile_browsable_query(sum_field)
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return attr in self.dict and self.dict.__getitem__(attr) or 0.0
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.dict[key] or 0.0
|
||||
|
||||
def _compile_browsable_query(self, sum_field):
|
||||
pass
|
||||
|
||||
|
||||
class InputLine(BrowsableObject):
|
||||
"""a class that will be used into the python code, mainly for usability purposes"""
|
||||
def _compile_browsable_query(self, sum_field):
|
||||
self.__browsable_query = """
|
||||
SELECT sum(amount) as sum
|
||||
FROM hr_payslip as hp, hr_payslip_input as pi
|
||||
WHERE hp.employee_id = %s AND hp.state in ('done', 'paid')
|
||||
AND hp.{sum_field} >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s""".format(sum_field=sum_field)
|
||||
|
||||
def sum(self, code, from_date, to_date=None):
|
||||
if to_date is None:
|
||||
to_date = fields.Date.today()
|
||||
self.env.cr.execute(self.__browsable_query, (self.employee_id, from_date, to_date, code))
|
||||
return self.env.cr.fetchone()[0] or 0.0
|
||||
|
||||
|
||||
class WorkedDays(BrowsableObject):
|
||||
"""a class that will be used into the python code, mainly for usability purposes"""
|
||||
def _compile_browsable_query(self, sum_field):
|
||||
self.__browsable_query = """
|
||||
SELECT sum(number_of_days) as number_of_days, sum(number_of_hours) as number_of_hours
|
||||
FROM hr_payslip as hp, hr_payslip_worked_days as pi
|
||||
WHERE hp.employee_id = %s AND hp.state in ('done', 'paid')
|
||||
AND hp.{sum_field} >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s""".format(sum_field=sum_field)
|
||||
|
||||
def _sum(self, code, from_date, to_date=None):
|
||||
if to_date is None:
|
||||
to_date = fields.Date.today()
|
||||
self.env.cr.execute(self.__browsable_query, (self.employee_id, from_date, to_date, code))
|
||||
return self.env.cr.fetchone()
|
||||
|
||||
def sum(self, code, from_date, to_date=None):
|
||||
res = self._sum(code, from_date, to_date)
|
||||
return res and res[0] or 0.0
|
||||
|
||||
def sum_hours(self, code, from_date, to_date=None):
|
||||
res = self._sum(code, from_date, to_date)
|
||||
return res and res[1] or 0.0
|
||||
|
||||
|
||||
class Payslips(BrowsableObject):
|
||||
"""a class that will be used into the python code, mainly for usability purposes"""
|
||||
def _compile_browsable_query(self, sum_field):
|
||||
# Note that the core odoo has this as `hp.credit_note = False` but what if it is NULL?
|
||||
# reverse of the desired behavior.
|
||||
self.__browsable_query_rule = """
|
||||
SELECT sum(case when hp.credit_note is not True then (pl.total) else (-pl.total) end)
|
||||
FROM hr_payslip as hp, hr_payslip_line as pl
|
||||
WHERE hp.employee_id = %s AND hp.state in ('done', 'paid')
|
||||
AND hp.{sum_field} >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id AND pl.code = %s""".format(sum_field=sum_field)
|
||||
# Original (non-recursive)
|
||||
# self.__browsable_query_category = """
|
||||
# SELECT sum(case when hp.credit_note is not True then (pl.total) else (-pl.total) end)
|
||||
# FROM hr_payslip as hp, hr_payslip_line as pl, hr_salary_rule_category as rc
|
||||
# WHERE hp.employee_id = %s AND hp.state = 'done'
|
||||
# AND hp.{sum_field} >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id
|
||||
# AND rc.id = pl.category_id AND rc.code = %s""".format(sum_field=sum_field)
|
||||
|
||||
# Hibou Recursive version
|
||||
self.__browsable_query_category = """
|
||||
WITH RECURSIVE
|
||||
category_by_code as (
|
||||
SELECT id
|
||||
FROM hr_salary_rule_category
|
||||
WHERE code = %s
|
||||
),
|
||||
category_ids as (
|
||||
SELECT COALESCE((SELECT id FROM category_by_code), -1) AS id
|
||||
UNION ALL
|
||||
SELECT rc.id
|
||||
FROM hr_salary_rule_category AS rc
|
||||
JOIN category_ids AS rcs ON rcs.id = rc.parent_id
|
||||
)
|
||||
|
||||
SELECT sum(case when hp.credit_note is not True then (pl.total) else (-pl.total) end)
|
||||
FROM hr_payslip as hp, hr_payslip_line as pl
|
||||
WHERE hp.employee_id = %s AND hp.state in ('done', 'paid')
|
||||
AND hp.{sum_field} >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id
|
||||
AND pl.category_id in (SELECT id from category_ids)""".format(sum_field=sum_field)
|
||||
|
||||
def sum(self, code, from_date, to_date=None):
|
||||
if to_date is None:
|
||||
to_date = fields.Date.today()
|
||||
self.env.cr.execute(self.__browsable_query_rule, (self.employee_id, from_date, to_date, code))
|
||||
res = self.env.cr.fetchone()
|
||||
return res and res[0] or 0.0
|
||||
|
||||
def rule_parameter(self, code):
|
||||
return self.env['hr.rule.parameter']._get_parameter_from_code(code, self.dict.date_to)
|
||||
|
||||
def sum_category(self, code, from_date, to_date=None):
|
||||
if to_date is None:
|
||||
to_date = fields.Date.today()
|
||||
|
||||
self.env['hr.payslip'].flush(['credit_note', 'employee_id', 'state', 'date_from', 'date_to'])
|
||||
self.env['hr.payslip.line'].flush(['total', 'slip_id', 'category_id'])
|
||||
self.env['hr.salary.rule.category'].flush(['code'])
|
||||
|
||||
# standard version
|
||||
# self.env.cr.execute(self.__browsable_query_category, (self.employee_id, from_date, to_date, code))
|
||||
# recursive category version
|
||||
self.env.cr.execute(self.__browsable_query_category, (code, self.employee_id, from_date, to_date))
|
||||
res = self.env.cr.fetchone()
|
||||
return res and res[0] or 0.0
|
||||
|
||||
@property
|
||||
def paid_amount(self):
|
||||
return self.dict._get_paid_amount()
|
||||
|
||||
# Hibou helper
|
||||
@property
|
||||
def pay_periods_in_year(self):
|
||||
return self.dict.get_pay_periods_in_year()
|
||||
|
||||
# Patch over Core
|
||||
browsable_object.BrowsableObject.__init__ = BrowsableObject.__init__
|
||||
browsable_object.BrowsableObject._compile_browsable_query = BrowsableObject._compile_browsable_query
|
||||
browsable_object.InputLine._compile_browsable_query = InputLine._compile_browsable_query
|
||||
browsable_object.InputLine.sum = InputLine.sum
|
||||
browsable_object.WorkedDays._compile_browsable_query = WorkedDays._compile_browsable_query
|
||||
browsable_object.WorkedDays.sum = WorkedDays.sum
|
||||
browsable_object.Payslips._compile_browsable_query = Payslips._compile_browsable_query
|
||||
browsable_object.Payslips.sum = Payslips.sum
|
||||
browsable_object.Payslips.sum_category = Payslips.sum_category
|
||||
browsable_object.Payslips.pay_periods_in_year = Payslips.pay_periods_in_year
|
||||
@@ -1,20 +0,0 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrContract(models.Model):
|
||||
_inherit = 'hr.contract'
|
||||
|
||||
wage_type = fields.Selection([('monthly', 'Period Fixed Wage'), ('hourly', 'Hourly Wage')],
|
||||
default='monthly', required=True, related=False)
|
||||
|
||||
def _get_contract_wage(self, work_type=None):
|
||||
# Override if you pay differently for different work types
|
||||
# In 14.0, this utilizes new computed field mechanism,
|
||||
# but will still get the 'wage' field by default.
|
||||
self.ensure_one()
|
||||
return self[self._get_contract_wage_field(work_type=work_type)]
|
||||
|
||||
def _get_contract_wage_field(self, work_type=None):
|
||||
if self.wage_type == 'hourly':
|
||||
return 'hourly_wage'
|
||||
return super()._get_contract_wage_field()
|
||||
@@ -1,53 +0,0 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class HrPayslip(models.Model):
|
||||
_inherit = 'hr.payslip'
|
||||
|
||||
# From IRS Publication 15-T or logically (annually, bi-monthly)
|
||||
PAY_PERIODS_IN_YEAR = {
|
||||
'annually': 1,
|
||||
'semi-annually': 2,
|
||||
'quarterly': 4,
|
||||
'bi-monthly': 6,
|
||||
'monthly': 12,
|
||||
'semi-monthly': 24,
|
||||
'bi-weekly': 26,
|
||||
'weekly': 52,
|
||||
'daily': 260,
|
||||
}
|
||||
|
||||
# normal_wage is an integer field, but that lacks precision.
|
||||
normal_wage = fields.Float(compute='_compute_normal_wage', store=True)
|
||||
# We need to be able to support more complexity,
|
||||
# namely, that different employees will be paid by different wage types as 'salary' vs 'hourly'
|
||||
wage_type = fields.Selection(related='contract_id.wage_type')
|
||||
|
||||
@api.depends('contract_id')
|
||||
def _compute_normal_wage(self):
|
||||
with_contract = self.filtered('contract_id')
|
||||
# fixes bug in original computation if the size of the recordset is >1
|
||||
(self - with_contract).update({'normal_wage': 0.0})
|
||||
for payslip in with_contract:
|
||||
payslip.normal_wage = payslip._get_contract_wage()
|
||||
|
||||
def get_year(self):
|
||||
"""
|
||||
# Helper method to get the year (normalized between Odoo Versions)
|
||||
:return: int year of payslip
|
||||
"""
|
||||
return self.date_to.year
|
||||
|
||||
def _get_contract_wage(self, work_type=None):
|
||||
# Override if you pay differently for different work types
|
||||
# In 14.0, this utilizes new computed field mechanism,
|
||||
# but will still get the 'wage' field by default.
|
||||
|
||||
# This would be a good place to override though with a 'work type'
|
||||
# based mechanism, like a minimum rate or 'rate card' implementation
|
||||
return self.contract_id._get_contract_wage(work_type=work_type)
|
||||
|
||||
def get_pay_periods_in_year(self):
|
||||
return self.PAY_PERIODS_IN_YEAR.get(self.contract_id.schedule_pay, 0)
|
||||
@@ -1,26 +0,0 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrPayrollStructure(models.Model):
|
||||
_inherit = 'hr.payroll.structure'
|
||||
|
||||
schedule_pay = fields.Selection(selection_add=[
|
||||
('semi-monthly', 'Semi-monthly'),
|
||||
], ondelete={'semi-monthly': 'set null'})
|
||||
|
||||
|
||||
class HrPayrollStructureType(models.Model):
|
||||
_inherit = 'hr.payroll.structure.type'
|
||||
|
||||
default_schedule_pay = fields.Selection(selection_add=[
|
||||
('semi-monthly', 'Semi-monthly'),
|
||||
])
|
||||
|
||||
|
||||
class HrRuleParameter(models.Model):
|
||||
_inherit = 'hr.rule.parameter'
|
||||
|
||||
update_locked = fields.Boolean(string='Update Lock',
|
||||
help='Lock parameter to prevent updating rates from publisher.')
|
||||
@@ -1,32 +0,0 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
# TODO We need MORE here...
|
||||
module_hr_payroll_payment = fields.Boolean(string='Payments & Advanced Accounting')
|
||||
module_hr_payroll_attendance = fields.Boolean(string='Attendance Entries & Overtime')
|
||||
module_hr_payroll_timesheet = fields.Boolean(string='Timesheet Entries & Overtime')
|
||||
module_hr_payroll_commission = fields.Boolean(string='Commission')
|
||||
module_l10n_us_hr_payroll = fields.Boolean(string='USA Payroll')
|
||||
module_l10n_us_hr_payroll_401k = fields.Boolean(string='USA Payroll 401k')
|
||||
|
||||
payslip_sum_type = fields.Selection([
|
||||
('date_from', 'Date From'),
|
||||
('date_to', 'Date To'),
|
||||
('date', 'Accounting Date'),
|
||||
], 'Payslip Sum Behavior', help="Behavior for what payslips are considered "
|
||||
"during rule execution. Stock Odoo behavior "
|
||||
"would not consider a payslip starting on 2019-12-30 "
|
||||
"ending on 2020-01-07 when summing a 2020 payslip category.\n\n"
|
||||
"Accounting Date requires Payroll Accounting and will "
|
||||
"fall back to date_to as the 'closest behavior'.",
|
||||
config_parameter='hr_payroll.payslip.sum_behavior')
|
||||
|
||||
def set_values(self):
|
||||
super(ResConfigSettings, self).set_values()
|
||||
self.env['ir.config_parameter'].set_param('hr_payroll.payslip.sum_behavior',
|
||||
self.payslip_sum_type or 'date_from')
|
||||
@@ -1,161 +0,0 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
import requests
|
||||
import json
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class HRPayrollPublisherUpdate(models.Model):
|
||||
_name = 'hr.payroll.publisher.update'
|
||||
_description = 'Payroll Update'
|
||||
_order = 'id DESC'
|
||||
|
||||
def _default_request_modules(self):
|
||||
request_modules = self.env.context.get('default_request_modules')
|
||||
if not request_modules:
|
||||
request_modules = '\n'.join(self.env['publisher_warranty.contract'].hibou_payroll_modules_to_update())
|
||||
return request_modules
|
||||
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('result', 'Result'),
|
||||
('done', 'Done'),
|
||||
('error', 'Error'),
|
||||
], default='draft')
|
||||
request_modules = fields.Char(default=_default_request_modules,
|
||||
states={'done': [('readonly', True)],
|
||||
'error': [('readonly', True)]})
|
||||
result = fields.Text(readonly=True)
|
||||
parameter_codes_retrieved = fields.Text(readonly=True)
|
||||
error = fields.Text(readonly=True)
|
||||
|
||||
@api.model
|
||||
def cron_payroll_update(self):
|
||||
update = self.create({})
|
||||
if update.request_modules:
|
||||
update.button_send()
|
||||
else:
|
||||
update.unlink()
|
||||
|
||||
def button_send(self):
|
||||
self.ensure_one()
|
||||
if not self.request_modules:
|
||||
raise UserError('One or more modules needed.')
|
||||
if self.result:
|
||||
raise UserError('Already retrieved')
|
||||
self._send()
|
||||
if self.result and not self.state == 'error':
|
||||
self._process_result()
|
||||
|
||||
def _send(self):
|
||||
try:
|
||||
self.env['publisher_warranty.contract'].hibou_payroll_update(self)
|
||||
except UserError as e:
|
||||
self.set_error_state(e.name)
|
||||
|
||||
def set_error_state(self, message=''):
|
||||
self.write({
|
||||
'state': 'error',
|
||||
'error': message,
|
||||
})
|
||||
|
||||
def set_result(self, result):
|
||||
self.write({
|
||||
'state': 'result',
|
||||
'result': result,
|
||||
'error': False,
|
||||
})
|
||||
|
||||
def button_process_result(self):
|
||||
self.ensure_one()
|
||||
if not self.result:
|
||||
raise UserError('No Result to process.')
|
||||
self._process_result()
|
||||
|
||||
def _process_result(self):
|
||||
try:
|
||||
result_dict = json.loads(self.result)
|
||||
parameter_values = result_dict.get('payroll_parameter_values')
|
||||
if not parameter_values or not isinstance(parameter_values, list):
|
||||
self.set_error_state('Result is missing expected parameter values.')
|
||||
parameter_map = {}
|
||||
parameter_model = self.env['hr.rule.parameter'].sudo()
|
||||
for code, date_from, pv in parameter_values:
|
||||
date_from = fields.Date.from_string(date_from)
|
||||
if code not in parameter_map:
|
||||
parameter_map[code] = parameter_model.search([('code', '=', code)], limit=1)
|
||||
parameter = parameter_map[code]
|
||||
if not parameter or parameter.update_locked:
|
||||
continue
|
||||
# watch out for versions of Odoo where this is not datetime.date
|
||||
parameter_version = parameter.parameter_version_ids.filtered(lambda p: p.date_from == date_from)
|
||||
if not parameter_version:
|
||||
parameter.write({
|
||||
'parameter_version_ids': [(0, 0, {
|
||||
'date_from': date_from,
|
||||
'parameter_value': pv,
|
||||
})]
|
||||
})
|
||||
elif parameter_version.parameter_value != pv:
|
||||
parameter_version.write({
|
||||
'parameter_value': pv,
|
||||
})
|
||||
# We have applied all of the updates. Set statistics.
|
||||
self.write({
|
||||
'state': 'done',
|
||||
'error': '',
|
||||
'parameter_codes_retrieved': '\n'.join('%s%s' % (c, '' if p and not p.update_locked else ' (LOCKED)' if p.update_locked else ' (MISSING)') for c, p in parameter_map.items()),
|
||||
})
|
||||
except Exception as e:
|
||||
self.set_error_state(str(e))
|
||||
|
||||
|
||||
class PublisherWarrantyContract(models.AbstractModel):
|
||||
_inherit = 'publisher_warranty.contract'
|
||||
|
||||
CONFIG_HIBOU_URL_PAYROLL = 'https://api.hibou.io/hibouapi/v1/professional/payroll'
|
||||
|
||||
@api.model
|
||||
def hibou_payroll_modules_to_update(self):
|
||||
# Filled downstream
|
||||
return []
|
||||
|
||||
@api.model
|
||||
def hibou_payroll_update(self, update_request):
|
||||
# Check status locally
|
||||
status = self.hibou_professional_status()
|
||||
if status['expired']:
|
||||
raise UserError('Hibou Professional Subscription Expired, you cannot retrieve updates.')
|
||||
if status['expiration_reason'] == 'trial':
|
||||
raise UserError('Hibou Professional Subscription Trial, not eligible for updates.')
|
||||
if not status['professional_code']:
|
||||
raise UserError('Hibou Professional Subscription Missing, please setup your subscription.')
|
||||
|
||||
if self.env.context.get('test_payroll_update_result'):
|
||||
update_request.set_result(self.env.context.get('test_payroll_update_result'))
|
||||
return
|
||||
|
||||
try:
|
||||
res = self._hibou_payroll_update(update_request.request_modules)
|
||||
if res.get('error'):
|
||||
update_request.set_error_state(str(res.get('error')))
|
||||
else:
|
||||
update_request.set_result(json.dumps(res))
|
||||
except Exception as e:
|
||||
update_request.set_error_state(str(e))
|
||||
|
||||
|
||||
def _hibou_payroll_update(self, payroll_modules):
|
||||
data = self._get_hibou_message()
|
||||
data['payroll_modules'] = payroll_modules
|
||||
data = {
|
||||
'jsonrpc': '2.0',
|
||||
'method': 'call',
|
||||
'params': data,
|
||||
}
|
||||
r = requests.post(self.CONFIG_HIBOU_URL_PAYROLL, json=data, timeout=30)
|
||||
r.raise_for_status()
|
||||
wrapper = r.json()
|
||||
return wrapper.get('result', {})
|
||||
@@ -1,2 +0,0 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
hr_payroll_hibou.access_hr_payroll_publisher_update,access_hr_payroll_publisher_update,hr_payroll_hibou.model_hr_payroll_publisher_update,base.group_user,1,1,1,1
|
||||
|
@@ -1,7 +0,0 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import common
|
||||
|
||||
from . import test_contract_wage_type
|
||||
from . import test_special
|
||||
from . import test_update
|
||||
@@ -1,166 +0,0 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from logging import getLogger
|
||||
from sys import float_info as sys_float_info
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo.tests import common
|
||||
from odoo.tools.float_utils import float_round as odoo_float_round
|
||||
|
||||
|
||||
def process_payslip(payslip):
|
||||
try:
|
||||
return payslip.action_payslip_done()
|
||||
except AttributeError:
|
||||
# v9
|
||||
return payslip.process_sheet()
|
||||
|
||||
|
||||
class TestPayslip(common.TransactionCase):
|
||||
debug = False
|
||||
_logger = getLogger(__name__)
|
||||
|
||||
def process_payslip(self, payslip=None):
|
||||
if not payslip:
|
||||
return process_payslip(self.payslip)
|
||||
return process_payslip(payslip)
|
||||
|
||||
def setUp(self):
|
||||
super(TestPayslip, self).setUp()
|
||||
self.contract_model = self.env['hr.contract']
|
||||
self.env.user.tz = 'PST8PDT'
|
||||
self.env.ref('resource.resource_calendar_std').tz = 'PST8PDT'
|
||||
self.env['ir.config_parameter'].set_param('hr_payroll.payslip.sum_behavior', 'date_to')
|
||||
self.structure_type = self.env['hr.payroll.structure.type'].create({
|
||||
'name': 'Test Structure Type',
|
||||
})
|
||||
self.structure = self.env['hr.payroll.structure'].create({
|
||||
'name': 'Test Structure',
|
||||
'type_id': self.structure_type.id,
|
||||
})
|
||||
self._log('structue_type %s and structure %s' % (self.structure_type, self.structure))
|
||||
self.structure_type.default_struct_id = self.structure
|
||||
self.resource_calendar = self.ref('resource.resource_calendar_std')
|
||||
|
||||
float_info = sys_float_info
|
||||
|
||||
def float_round(self, value, digits):
|
||||
return odoo_float_round(value, digits)
|
||||
|
||||
_payroll_digits = -1
|
||||
|
||||
@property
|
||||
def payroll_digits(self):
|
||||
if self._payroll_digits == -1:
|
||||
self._payroll_digits = self.env['decimal.precision'].precision_get('Payroll')
|
||||
return self._payroll_digits
|
||||
|
||||
def _log(self, message):
|
||||
if self.debug:
|
||||
self._logger.warning(message)
|
||||
|
||||
def _createEmployee(self):
|
||||
employee = self.env['hr.employee'].create({
|
||||
'birthday': '1985-03-14',
|
||||
'country_id': self.ref('base.us'),
|
||||
'department_id': self.ref('hr.dep_rd'),
|
||||
'gender': 'male',
|
||||
'name': 'Jared'
|
||||
})
|
||||
employee.address_home_id = self.env['res.partner'].create({
|
||||
'name': 'Jared (private)',
|
||||
})
|
||||
return employee
|
||||
|
||||
def _get_contract_defaults(self, contract_values):
|
||||
if not contract_values.get('state'):
|
||||
contract_values['state'] = 'open' # Running
|
||||
if not contract_values.get('structure_type_id'):
|
||||
contract_values['structure_type_id'] = self.structure_type.id
|
||||
if not contract_values.get('date_start'):
|
||||
contract_values['date_start'] = '2016-01-01'
|
||||
if not contract_values.get('date_end'):
|
||||
contract_values['date_end'] = '2030-12-31'
|
||||
if not contract_values.get('resource_calendar_id'):
|
||||
contract_values['resource_calendar_id'] = self.resource_calendar
|
||||
|
||||
# Compatibility with earlier Odoo versions
|
||||
if not contract_values.get('journal_id') and hasattr(self.contract_model, 'journal_id'):
|
||||
try:
|
||||
contract_values['journal_id'] = self.env['account.journal'].search([('type', '=', 'general')], limit=1).id
|
||||
except KeyError:
|
||||
# Accounting not installed
|
||||
pass
|
||||
|
||||
def _createContract(self, employee, **kwargs):
|
||||
if not 'schedule_pay' in kwargs:
|
||||
kwargs['schedule_pay'] = 'monthly'
|
||||
schedule_pay = kwargs['schedule_pay']
|
||||
contract_values = {
|
||||
'name': 'Test Contract',
|
||||
'employee_id': employee.id,
|
||||
}
|
||||
|
||||
for key, val in kwargs.items():
|
||||
# Assume any Odoo object is in a Many2one
|
||||
if hasattr(val, 'id'):
|
||||
val = val.id
|
||||
found = False
|
||||
if hasattr(self.contract_model, key):
|
||||
contract_values[key] = val
|
||||
found = True
|
||||
if not found:
|
||||
self._logger.warning('cannot locate attribute names "%s" on hr.contract().' % (key, ))
|
||||
|
||||
self._get_contract_defaults(contract_values)
|
||||
contract = self.contract_model.create(contract_values)
|
||||
|
||||
# Compatibility with Odoo 14
|
||||
contract.structure_type_id.default_struct_id.schedule_pay = schedule_pay
|
||||
return contract
|
||||
|
||||
def _createPayslip(self, employee, date_from, date_to, skip_compute=False, other_values=False):
|
||||
if not other_values:
|
||||
other_values = {}
|
||||
create_values = {
|
||||
'name': 'Test %s From: %s To: %s' % (employee.name, date_from, date_to),
|
||||
'employee_id': employee.id,
|
||||
'date_from': date_from,
|
||||
'date_to': date_to
|
||||
}
|
||||
create_values.update(other_values)
|
||||
slip = self.env['hr.payslip'].create(create_values)
|
||||
# Included in hr.payslip.action_refresh_from_work_entries() as ov 14.0 EE
|
||||
# slip._onchange_employee()
|
||||
# as is the 'compute' that is almost always called immediaately after
|
||||
if not skip_compute:
|
||||
slip.action_refresh_from_work_entries()
|
||||
return slip
|
||||
|
||||
def _getCategories(self, payslip):
|
||||
categories = defaultdict(float)
|
||||
for line in payslip.line_ids:
|
||||
self._log(' line code: ' + str(line.code) +
|
||||
' category code: ' + line.category_id.code +
|
||||
' total: ' + str(line.total) +
|
||||
' rate: ' + str(line.rate) +
|
||||
' amount: ' + str(line.amount))
|
||||
category_id = line.category_id
|
||||
category_code = line.category_id.code
|
||||
while category_code:
|
||||
categories[category_code] += line.total
|
||||
category_id = category_id.parent_id
|
||||
category_code = category_id.code
|
||||
return categories
|
||||
|
||||
def _getRules(self, payslip):
|
||||
rules = defaultdict(float)
|
||||
for line in payslip.line_ids:
|
||||
rules[line.code] += line.total
|
||||
return rules
|
||||
|
||||
def assertPayrollEqual(self, first, second):
|
||||
self.assertAlmostEqual(first, second, self.payroll_digits)
|
||||
|
||||
def assertPayrollAlmostEqual(self, first, second):
|
||||
self.assertAlmostEqual(first, second, self.payroll_digits-1)
|
||||
@@ -1,26 +0,0 @@
|
||||
from .common import TestPayslip, process_payslip
|
||||
|
||||
|
||||
class TestContractWageType(TestPayslip):
|
||||
|
||||
def test_per_contract_wage_type_salary(self):
|
||||
self.debug = True
|
||||
salary = 80000.0
|
||||
employee = self._createEmployee()
|
||||
contract = self._createContract(employee, wage=salary, hourly_wage=salary/100.0, wage_type='monthly', schedule_pay='bi-weekly')
|
||||
payslip = self._createPayslip(employee, '2019-12-30', '2020-01-12')
|
||||
self.assertEqual(contract.wage_type, 'monthly')
|
||||
self.assertEqual(payslip.wage_type, 'monthly')
|
||||
cats = self._getCategories(payslip)
|
||||
self.assertEqual(cats['BASIC'], salary)
|
||||
|
||||
def test_per_contract_wage_type_hourly(self):
|
||||
self.debug = True
|
||||
hourly_wage = 21.50
|
||||
employee = self._createEmployee()
|
||||
contract = self._createContract(employee, wage=hourly_wage*100.0, hourly_wage=hourly_wage, wage_type='hourly', schedule_pay='bi-weekly')
|
||||
payslip = self._createPayslip(employee, '2019-12-30', '2020-01-12')
|
||||
self.assertEqual(contract.wage_type, 'hourly')
|
||||
self.assertEqual(payslip.wage_type, 'hourly')
|
||||
cats = self._getCategories(payslip)
|
||||
self.assertEqual(cats['BASIC'], hourly_wage * 80.0)
|
||||
@@ -1,127 +0,0 @@
|
||||
from .common import TestPayslip, process_payslip
|
||||
|
||||
|
||||
class TestSpecial(TestPayslip):
|
||||
|
||||
def test_get_year(self):
|
||||
salary = 80000.0
|
||||
employee = self._createEmployee()
|
||||
# so the schedule_pay is now on the Structure...
|
||||
contract = self._createContract(employee, wage=salary)
|
||||
payslip = self._createPayslip(employee, '2020-01-01', '2020-01-14')
|
||||
self.assertEqual(payslip.get_year(), 2020)
|
||||
|
||||
def test_semi_monthly(self):
|
||||
salary = 80000.0
|
||||
employee = self._createEmployee()
|
||||
# so the schedule_pay is now on the Structure...
|
||||
contract = self._createContract(employee, wage=salary, schedule_pay='semi-monthly')
|
||||
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-14')
|
||||
|
||||
def test_payslip_sum_behavior(self):
|
||||
self.env['ir.config_parameter'].set_param('hr_payroll.payslip.sum_behavior', 'date')
|
||||
rule_category_comp = self.env.ref('hr_payroll.COMP')
|
||||
test_rule_category = self.env['hr.salary.rule.category'].create({
|
||||
'name': 'Test Sum Behavior',
|
||||
'code': 'test_sum_behavior',
|
||||
'parent_id': rule_category_comp.id,
|
||||
})
|
||||
test_rule = self.env['hr.salary.rule'].create({
|
||||
'sequence': 450,
|
||||
'struct_id': self.structure.id,
|
||||
'category_id': test_rule_category.id,
|
||||
'name': 'Test Sum Behavior',
|
||||
'code': 'test_sum_behavior',
|
||||
'condition_select': 'python',
|
||||
'condition_python': 'result = 1',
|
||||
'amount_select': 'code',
|
||||
'amount_python_compute': '''
|
||||
ytd_category = payslip.sum_category('test_sum_behavior', '2020-01-01', '2021-01-01')
|
||||
ytd_rule = payslip.sum('test_sum_behavior', '2020-01-01', '2021-01-01')
|
||||
result = 0.0
|
||||
if ytd_category != ytd_rule:
|
||||
# error
|
||||
result = -1.0
|
||||
elif ytd_rule == 0.0:
|
||||
# first payslip in period
|
||||
result = 1.0
|
||||
'''
|
||||
})
|
||||
salary = 80000.0
|
||||
employee = self._createEmployee()
|
||||
contract = self._createContract(employee, wage=salary, schedule_pay='bi-weekly')
|
||||
payslip = self._createPayslip(employee, '2019-12-30', '2020-01-12')
|
||||
cats = self._getCategories(payslip)
|
||||
self.assertEqual(cats['BASIC'], salary)
|
||||
self.assertEqual(cats['test_sum_behavior'], 1.0)
|
||||
process_payslip(payslip)
|
||||
|
||||
# Basic date_from behavior.
|
||||
self.env['ir.config_parameter'].set_param('hr_payroll.payslip.sum_behavior', 'date_from')
|
||||
# The the date_from on the last payslip will not be found
|
||||
payslip = self._createPayslip(employee, '2020-01-13', '2020-01-27')
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
self.assertEqual(cats['test_sum_behavior'], 1.0)
|
||||
|
||||
# date_to behavior.
|
||||
self.env['ir.config_parameter'].set_param('hr_payroll.payslip.sum_behavior', 'date_to')
|
||||
# The date_to on the last payslip is found
|
||||
payslip = self._createPayslip(employee, '2020-01-13', '2020-01-27')
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
self.assertEqual(cats['test_sum_behavior'], 0.0)
|
||||
|
||||
def test_recursive_salary_rule_category(self):
|
||||
self.debug = True
|
||||
# In this scenario, you are in rule code that will check for the category
|
||||
# and a subcategory will also
|
||||
alw_category = self.env.ref('hr_payroll.ALW')
|
||||
ded_category = self.env.ref('hr_payroll.DED')
|
||||
test_category = self.env['hr.salary.rule.category'].create({
|
||||
'name': 'Special ALW',
|
||||
'code': 'ALW_SPECIAL_RECURSIVE',
|
||||
'parent_id': alw_category.id,
|
||||
})
|
||||
test_special_alw = self.env['hr.salary.rule'].create({
|
||||
'name': 'Flat amount 200',
|
||||
'code': 'ALW_SPECIAL_RECURSIVE',
|
||||
'category_id': test_category.id,
|
||||
'condition_select': 'none',
|
||||
'amount_select': 'fix',
|
||||
'amount_fix': 200.0,
|
||||
'struct_id': self.structure.id,
|
||||
})
|
||||
test_recursion = self.env['hr.salary.rule'].create({
|
||||
'name': 'Actual Test Behavior',
|
||||
'code': 'RECURSION_TEST',
|
||||
'category_id': ded_category.id,
|
||||
'condition_select': 'none',
|
||||
'amount_select': 'code',
|
||||
'amount_python_compute': """
|
||||
# this rule will always be the total of the ALW category and YTD ALW category
|
||||
result = categories.ALW
|
||||
# Note, this tests the hr.payslip.get_year() to return an integer rep of year
|
||||
year = payslip.dict.get_year()
|
||||
result += payslip.sum_category('ALW', str(year) + '-01-01', str(year+1) + '-01-01')
|
||||
""",
|
||||
'sequence': 101,
|
||||
'struct_id': self.structure.id,
|
||||
})
|
||||
|
||||
salary = 80000.0
|
||||
employee = self._createEmployee()
|
||||
contract = self._createContract(employee, wage=salary, schedule_pay='bi-weekly')
|
||||
payslip = self._createPayslip(employee, '2020-01-01', '2020-01-14')
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
rules = self._getRules(payslip)
|
||||
self.assertEqual(rules['RECURSION_TEST'], 200.0)
|
||||
process_payslip(payslip)
|
||||
|
||||
payslip = self._createPayslip(employee, '2020-01-15', '2020-01-27')
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
rules = self._getRules(payslip)
|
||||
# two hundred is in the YTD ALW
|
||||
self.assertEqual(rules['RECURSION_TEST'], 200.0 + 200.0)
|
||||
@@ -1,93 +0,0 @@
|
||||
import datetime
|
||||
|
||||
from odoo import fields
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests import common
|
||||
|
||||
|
||||
class TestUpdate(common.TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# setup the database to run in general
|
||||
today = datetime.date.today()
|
||||
tomorrow = today + datetime.timedelta(days=1)
|
||||
self.param_model = self.env['ir.config_parameter'].sudo()
|
||||
self.param_model.set_param('database.hibou_professional_expiration_date', fields.Date.to_string(tomorrow))
|
||||
self.param_model.set_param('database.hibou_professional_code', 'TESTCODE')
|
||||
|
||||
def test_01_database_state(self):
|
||||
today = datetime.date.today()
|
||||
tomorrow = today + datetime.timedelta(days=1)
|
||||
yesterday = today - datetime.timedelta(days=1)
|
||||
self.param_model.set_param('database.hibou_professional_expiration_date', fields.Date.to_string(yesterday))
|
||||
|
||||
update = self.env['hr.payroll.publisher.update']\
|
||||
.with_context(test_payroll_update_result='{"payroll_parameter_values":[]}')\
|
||||
.create({
|
||||
'request_modules': 'test',
|
||||
})
|
||||
self.assertEqual(update.state, 'draft')
|
||||
update.button_send()
|
||||
self.assertEqual(update.state, 'error')
|
||||
|
||||
self.param_model.set_param('database.hibou_professional_expiration_date', fields.Date.to_string(tomorrow))
|
||||
update.button_send()
|
||||
self.assertEqual(update.state, 'done')
|
||||
self.assertEqual(update.parameter_codes_retrieved, '')
|
||||
|
||||
# Reset to a degree.
|
||||
update = update.with_context(test_payroll_update_result='{"payroll_parameter_values":[["missing_code", "2021-01-01", "5.0"]]}')
|
||||
update.write({
|
||||
'state': 'draft',
|
||||
'result': '',
|
||||
})
|
||||
update.button_send()
|
||||
self.assertEqual(update.state, 'done')
|
||||
self.assertEqual(update.parameter_codes_retrieved, 'missing_code (MISSING)')
|
||||
|
||||
# Actually add to a rule.
|
||||
test_parameter = self.env['hr.rule.parameter'].create({
|
||||
'code': 'test_parameter_1',
|
||||
'name': 'Test Parameter 1',
|
||||
})
|
||||
update = update.with_context(test_payroll_update_result='{"payroll_parameter_values":[["test_parameter_1", "2021-01-01", "5.0"]]}')
|
||||
update.write({
|
||||
'state': 'draft',
|
||||
'result': '',
|
||||
})
|
||||
update.button_send()
|
||||
self.assertEqual(update.state, 'done')
|
||||
self.assertEqual(update.parameter_codes_retrieved, 'test_parameter_1')
|
||||
self.assertTrue(test_parameter.parameter_version_ids)
|
||||
self.assertEqual(test_parameter.parameter_version_ids.parameter_value, '5.0')
|
||||
self.assertEqual(str(test_parameter.parameter_version_ids.date_from), '2021-01-01')
|
||||
|
||||
test_parameter.parameter_version_ids.write({
|
||||
'parameter_value': '',
|
||||
})
|
||||
self.assertEqual(test_parameter.parameter_version_ids.parameter_value, '')
|
||||
update.write({
|
||||
'state': 'draft',
|
||||
'result': '',
|
||||
})
|
||||
update.button_send()
|
||||
# doesn't make a new one, updates existing...
|
||||
self.assertEqual(test_parameter.parameter_version_ids.parameter_value, '5.0')
|
||||
|
||||
# Test that we can lock the parameter
|
||||
test_parameter.parameter_version_ids.write({
|
||||
'parameter_value': 'locked',
|
||||
})
|
||||
test_parameter.write({
|
||||
'update_locked': True,
|
||||
})
|
||||
self.assertEqual(test_parameter.parameter_version_ids.parameter_value, 'locked')
|
||||
update.write({
|
||||
'state': 'draft',
|
||||
'result': '',
|
||||
})
|
||||
update.button_send()
|
||||
# doesn't make a new one, updates existing...
|
||||
self.assertEqual(test_parameter.parameter_version_ids.parameter_value, 'locked')
|
||||
self.assertEqual(update.parameter_codes_retrieved, 'test_parameter_1 (LOCKED)')
|
||||
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="hr_rule_parameter_view_form_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.rule.parameter.form.inherit</field>
|
||||
<field name="model">hr.rule.parameter</field>
|
||||
<field name="inherit_id" ref="hr_payroll.hr_rule_parameter_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='code']" position="after">
|
||||
<field name="update_locked" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,100 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="res_config_settings_view_form_hibou" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.hr.payroll.hibou</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="priority" eval="50"/>
|
||||
<field name="inherit_id" ref="hr_payroll.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@data-key='hr_payroll']" position="inside">
|
||||
<field name="module_l10n_us_hr_payroll" invisible="1"/>
|
||||
<!-- TODO payment, attendance, timesheet, ovetrtime... -->
|
||||
<h2>Hibou Payroll</h2>
|
||||
<div class="row mt16 o_settings_container" id="hr_payroll_hibou">
|
||||
<div class="col-lg-6 col-12 o_setting_box">
|
||||
<div class="o_setting_left_pane"/>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="payslip_sum_type" string="Payslip Sum Behavior"/>
|
||||
<div class="text-muted">
|
||||
Customize the behavior of what payslips are eligible when summing over date ranges in rules.
|
||||
Generally, "Date To" or "Accounting Date" would be preferred in the United States and anywhere
|
||||
else where the ending date on the payslip is used to calculate wage bases.
|
||||
</div>
|
||||
<field name="payslip_sum_type"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 col-12 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="module_l10n_us_hr_payroll_401k"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="module_l10n_us_hr_payroll_401k" string="USA Payroll 401k"/>
|
||||
<div class="text-muted">
|
||||
Provide retirement plans with optional company matching.
|
||||
<strong>Hibou Professional</strong>
|
||||
</div>
|
||||
<div class="mt8" id="l10n_us_hr_payroll_401k_match">
|
||||
<button name="%(hr_payroll.hr_rule_parameter_action)d" icon="fa-arrow-right" type="action"
|
||||
string="Configure Matching & Limits" class="btn-link"
|
||||
context="{'search_default_us_payroll_401k': True}"
|
||||
attrs="{'invisible': [('module_l10n_us_hr_payroll_401k', '=', False)]}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 col-12 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="module_hr_payroll_payment"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="module_hr_payroll_payment"/>
|
||||
<div class="text-muted">
|
||||
Register payments on payslips! Have control over journal entries created from
|
||||
payroll to include partner details, set grouping options, and apply fiscal position
|
||||
account mappings.
|
||||
<strong>Hibou Professional</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 col-12 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="module_hr_payroll_attendance"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="module_hr_payroll_attendance"/>
|
||||
<div class="text-muted">
|
||||
Extend Attendance into Payroll with Work Types, Overtime!
|
||||
<strong>Hibou Professional</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 col-12 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="module_hr_payroll_timesheet"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="module_hr_payroll_timesheet"/>
|
||||
<div class="text-muted">
|
||||
Extend Timesheets into Payroll with Work Types, Overtime!
|
||||
<strong>Hibou Professional</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 col-12 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="module_hr_payroll_commission"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="module_hr_payroll_commission"/>
|
||||
<div class="text-muted">
|
||||
Pay Commissions in Payroll!
|
||||
<strong>Hibou Professional</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,63 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="hr_payroll_publisher_update_tree" model="ir.ui.view">
|
||||
<field name="name">hr.payroll.publisher.update.tree</field>
|
||||
<field name="model">hr.payroll.publisher.update</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Payroll Updates">
|
||||
<field name="create_date"/>
|
||||
<field name="write_date"/>
|
||||
<field name="request_modules"/>
|
||||
<field name="state"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_publisher_update_form" model="ir.ui.view">
|
||||
<field name="name">hr.payroll.publisher.update.form</field>
|
||||
<field name="model">hr.payroll.publisher.update</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Payroll Udate">
|
||||
<header>
|
||||
<button name="button_send" type="object" string="Send" attrs="{'invisible': [('state', 'not in', ('draft', 'error'))]}" />
|
||||
<button name="button_process_result" type="object" string="Process Results" attrs="{'invisible': [('state', 'not in', ('result', 'error'))]}" />
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,result,done" />
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<group name="general" string="General">
|
||||
<field name="create_date" />
|
||||
<field name="write_date" />
|
||||
<field name="request_modules" widget="text" />
|
||||
<field name="error" attrs="{'invisible': [('error', '=', False)]}" />
|
||||
</group>
|
||||
<group name="parameter_codes" string="Parameter Codes">
|
||||
<field name="parameter_codes_retrieved" />
|
||||
<p colspan="2">Note missing codes probably means code upgrades are required. Locked codes are only locked in this database.</p>
|
||||
</group>
|
||||
<group name="result" string="Result" groups="base.group_no_one" colspan="4">
|
||||
<field name="result" nolabel="1" colspan="4" />
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_payroll_updates" model="ir.actions.act_window">
|
||||
<field name="name">Payroll Updates</field>
|
||||
<field name="res_model">hr.payroll.publisher.update</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="help" type="html">
|
||||
<p>
|
||||
No Updates
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_hr_payroll_configuration_updates" name="Payroll Updates"
|
||||
action="action_payroll_updates"
|
||||
sequence="20" parent="hr_work_entry_contract_enterprise.menu_hr_payroll_configuration"/>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user