trytonpsk-staff_payroll/payroll.py

1022 lines
39 KiB
Python
Raw Normal View History

2020-04-16 00:37:53 +02:00
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import datetime
2023-04-29 18:14:49 +02:00
import time
2020-04-16 00:37:53 +02:00
from decimal import Decimal
2023-02-01 23:24:32 +01:00
from operator import attrgetter
2023-04-29 18:14:49 +02:00
from functools import lru_cache
2020-04-16 00:37:53 +02:00
from trytond.model import Workflow, ModelView, ModelSQL, fields
from trytond.pyson import Bool, Eval, If, Id
from trytond.pool import Pool, PoolMeta
from trytond.transaction import Transaction
from trytond.modules.company import CompanyReport
from trytond.wizard import Wizard, StateView, StateTransition, Button
2021-06-04 23:04:40 +02:00
from trytond.i18n import gettext
from .exceptions import (PayrollDeleteError, PayrollPeriodCloseError,
2023-05-01 19:19:03 +02:00
PayrollExistPeriodError, PayrollMissingSequence, WageTypeValidationError,
PayrollValidationError)
2020-04-16 00:37:53 +02:00
STATES = {'readonly': (Eval('state') != 'draft')}
_DEFAULT_WORK_DAY = 8
2020-04-16 00:37:53 +02:00
_ZERO = Decimal('0.0')
2023-02-01 23:24:32 +01:00
2022-04-01 00:08:24 +02:00
def get_dom_contract_period(start, end):
2022-04-01 01:39:31 +02:00
dom = ['OR', [
2022-04-01 00:08:24 +02:00
('start_date', '>=', start),
('finished_date', '<=', end),
('finished_date', '!=', None),
], [
('start_date', '<=', start),
('finished_date', '>=', start),
('finished_date', '!=', None),
], [
('start_date', '<=', end),
('finished_date', '>=', end),
('finished_date', '!=', None),
], [
('start_date', '<=', start),
('finished_date', '>=', end),
('finished_date', '!=', None),
], [
('start_date', '<=', start),
('finished_date', '=', None),
],
[
('start_date', '>=', start),
('start_date', '<=', end),
('finished_date', '=', None),
],
2022-04-01 01:39:31 +02:00
]
2022-04-01 00:08:24 +02:00
return dom
2020-04-16 00:37:53 +02:00
2022-11-15 15:46:26 +01:00
2020-04-16 00:37:53 +02:00
class Payroll(Workflow, ModelSQL, ModelView):
"Staff Payroll"
__name__ = "staff.payroll"
_rec_name = 'number'
number = fields.Char('Number', readonly=True, help="Secuence",
2023-02-01 23:24:32 +01:00
select=True)
2020-04-16 00:37:53 +02:00
period = fields.Many2One('staff.payroll.period', 'Period',
2023-02-01 23:24:32 +01:00
required=True, states={
'readonly': Eval('state') != 'draft',
})
2020-04-16 00:37:53 +02:00
employee = fields.Many2One('company.employee', 'Employee',
2023-02-01 23:24:32 +01:00
states=STATES, required=True, depends=['state'], select=True)
2020-04-16 00:37:53 +02:00
kind = fields.Selection([
('normal', 'Normal'),
('special', 'Special'),
], 'Kind', required=True, select=True, states=STATES,
help="Special allow overlap dates with another payroll")
contract = fields.Many2One('staff.contract', 'Contract',
2023-02-01 23:24:32 +01:00
select=True, domain=[
('employee', '=', Eval('employee')),
], ondelete='RESTRICT')
2020-04-16 00:37:53 +02:00
start = fields.Date('Start', states=STATES, required=True)
end = fields.Date('End', states=STATES, required=True)
date_effective = fields.Date('Date Effective', states=STATES,
2023-02-01 23:24:32 +01:00
required=True)
2020-04-16 00:37:53 +02:00
description = fields.Char('Description', states=STATES, select=True)
lines = fields.One2Many('staff.payroll.line', 'payroll', 'Wage Line',
2023-02-01 23:24:32 +01:00
states=STATES, depends=['employee', 'state'])
2020-04-16 00:37:53 +02:00
gross_payments = fields.Function(fields.Numeric('Gross Payments',
2023-02-01 23:24:32 +01:00
digits=(16, 2), depends=['lines', 'states']), 'on_change_with_amount')
2020-04-16 00:37:53 +02:00
total_deductions = fields.Function(fields.Numeric(
2023-02-01 23:24:32 +01:00
'Total Deductions', digits=(16, 2), depends=['lines', 'states']),
'on_change_with_amount')
2020-04-16 00:37:53 +02:00
net_payment = fields.Function(fields.Numeric('Net Payment',
2023-02-01 23:24:32 +01:00
digits=(16, 2), depends=['lines', 'states']),
'get_net_payment')
2020-04-16 00:37:53 +02:00
total_cost = fields.Function(fields.Numeric('Total Cost',
2023-02-01 23:24:32 +01:00
digits=(16, 2), depends=['lines', 'states']),
'get_total_cost')
2020-04-16 00:37:53 +02:00
currency = fields.Many2One('currency.currency', 'Currency',
2023-02-01 23:24:32 +01:00
required=False,
states={
'readonly': ((Eval('state') != 'draft')
| (Eval('lines', [0]) & Eval('currency'))),
}, depends=['state'])
2020-04-16 00:37:53 +02:00
worked_days = fields.Function(fields.Integer('Worked Days',
2023-02-01 23:24:32 +01:00
depends=['start', 'end', 'state']),
'on_change_with_worked_days')
2020-04-16 00:37:53 +02:00
state = fields.Selection([
('draft', 'Draft'),
('processed', 'Processed'),
('cancel', 'Cancel'),
('posted', 'Posted'),
], 'State', readonly=True)
journal = fields.Many2One('account.journal', 'Journal', required=True,
2023-02-01 23:24:32 +01:00
states=STATES)
2020-04-16 00:37:53 +02:00
company = fields.Many2One('company.company', 'Company', required=True,
2023-02-01 23:24:32 +01:00
states={
'readonly': (Eval('state') != 'draft') | Eval('lines', [0]),
},
domain=[
('id', If(Eval('context', {}).contains('company'), '=', '!='),
Eval('context', {}).get('company', 0)),
],
depends=['state'])
2020-04-16 00:37:53 +02:00
move = fields.Many2One('account.move', 'Move', readonly=True)
origin = fields.Reference('Origin', selection='get_origin',
2023-02-01 23:24:32 +01:00
select=True, depends=['state'],
states={
'readonly': Eval('state') != 'draft',
})
2020-04-16 00:37:53 +02:00
notes = fields.Text("Notes", states=STATES)
@classmethod
def __setup__(cls):
super(Payroll, cls).__setup__()
cls._order = [
('period', 'DESC'),
('start', 'DESC'),
]
cls._transitions |= set((
('draft', 'cancel'),
('cancel', 'draft'),
('draft', 'processed'),
('processed', 'posted'),
('posted', 'draft'),
('processed', 'draft'),
))
2020-04-16 00:37:53 +02:00
cls._buttons.update({
'draft': {
2023-04-22 19:01:38 +02:00
'invisible': Eval('state').in_(['draft', 'posted']),
},
'post': {
'invisible': Eval('state') != 'processed',
},
'cancel': {
'invisible': Eval('state') != 'draft',
},
'process': {
'invisible': Eval('state') != 'draft',
2023-04-22 19:01:38 +02:00
},'force_draft': {
'invisible': Eval('state') != 'posted',
},
})
2020-04-16 00:37:53 +02:00
@staticmethod
def default_company():
return Transaction().context.get('company')
@staticmethod
def default_kind():
return 'normal'
@staticmethod
def default_state():
return 'draft'
@staticmethod
def _get_origin():
'Return list of Model names for origin Reference'
return []
2020-04-16 00:37:53 +02:00
@staticmethod
def default_journal():
Configuration = Pool().get('staff.configuration')
configuration = Configuration(1)
if configuration.default_journal:
return configuration.default_journal.id
@staticmethod
def default_currency():
Company = Pool().get('company.company')
if Transaction().context.get('company'):
company = Company(Transaction().context['company'])
return company.currency.id
@classmethod
def delete(cls, records):
# Cancel before delete
cls.cancel(records)
for payroll in records:
if payroll.state != 'cancel':
2021-06-04 23:04:40 +02:00
raise PayrollDeleteError(
gettext('staff_payroll.msg_delete_cancel', payroll=payroll.rec_name))
2020-04-16 00:37:53 +02:00
if payroll.move:
2021-06-04 23:04:40 +02:00
raise PayrollDeleteError(
gettext('staff_payroll.msg_existing_move', payroll=payroll.rec_name))
2020-04-16 00:37:53 +02:00
super(Payroll, cls).delete(records)
@classmethod
def validate(cls, payrolls):
super(Payroll, cls).validate(payrolls)
for payroll in payrolls:
payroll.check_start_end()
@classmethod
def search_rec_name(cls, name, clause):
if clause[1].startswith('!') or clause[1].startswith('not '):
bool_op = 'AND'
else:
bool_op = 'OR'
return [
bool_op,
2020-04-16 00:37:53 +02:00
('employee',) + tuple(clause[1:]),
('number',) + tuple(clause[1:]),
]
2020-04-16 00:37:53 +02:00
@classmethod
@ModelView.button
@Workflow.transition('draft')
def draft(cls, records):
pass
@classmethod
@ModelView.button
@Workflow.transition('cancel')
def cancel(cls, records):
pass
@classmethod
@ModelView.button
@Workflow.transition('processed')
def process(cls, records):
Payroll = Pool().get('staff.payroll')
for payroll in records:
payrolls = Payroll.search([
('period', '=', payroll.period.id),
('contract', '=', payroll.contract.id),
('kind', '=', 'normal'),
2020-04-16 00:37:53 +02:00
])
if len(payrolls) > 1:
2021-11-10 18:33:04 +01:00
raise PayrollExistPeriodError(
gettext('staff_payroll.msg_payroll_exist_period'))
2020-04-16 00:37:53 +02:00
if payroll.period.state == 'closed':
2021-11-10 18:33:04 +01:00
raise PayrollPeriodCloseError(
gettext('staff_payroll.msg_period_closed'))
2020-04-16 00:37:53 +02:00
payroll.set_number()
@classmethod
@ModelView.button
@Workflow.transition('posted')
def post(cls, records):
2023-05-10 18:45:38 +02:00
wage_dict = cls.create_cache_wage_types()
2020-04-16 00:37:53 +02:00
for payroll in records:
2023-05-10 18:45:38 +02:00
payroll.create_move(wage_dict)
2020-04-16 00:37:53 +02:00
@fields.depends('start', 'end')
def on_change_start(self):
if not self.start:
return
Configuration = Pool().get('staff.configuration')
configuration = Configuration(1)
period_days = configuration.default_liquidation_period
if period_days and not self.end:
self.end = self.start + datetime.timedelta(period_days - 1)
@classmethod
def get_origin(cls):
Model = Pool().get('ir.model')
models = cls._get_origin()
models = Model.search([
('model', 'in', models),
])
return [(None, '')] + [(m.model, m.name) for m in models]
def set_number(self):
if self.number:
return
pool = Pool()
Configuration = pool.get('staff.configuration')
configuration = Configuration(1)
if not configuration.staff_payroll_sequence:
2021-06-04 23:04:40 +02:00
raise PayrollMissingSequence(gettext('msg_sequence_missing'))
seq = configuration.staff_payroll_sequence.get()
self.write([self], {'number': seq})
2020-04-16 00:37:53 +02:00
@fields.depends('start', 'end')
def on_change_with_worked_days(self, name=None):
if self.start and self.end and self.employee:
return self.get_days(self.start, self.end)
def get_salary_full(self, wage):
"""
Return a dict with sum of total amount of all wages defined
as salary on context of wage
"""
salary_full = self.compute_salary_full(wage)
return {'salary': salary_full}
def compute_salary_full(self, wage):
2023-05-13 00:36:13 +02:00
if wage['concepts_salary']:
salary_full = sum(
line.amount for line in self.lines if line.wage_type.id in wage['concepts_salary'])
else:
salary_full = self.contract.get_salary_in_date(self.end) or 0
2020-04-16 00:37:53 +02:00
return salary_full
2023-05-10 18:45:38 +02:00
def create_move(self, wage_dict):
2020-04-16 00:37:53 +02:00
pool = Pool()
Move = pool.get('account.move')
Period = pool.get('account.period')
if self.move:
return
period_id = Period.find(self.company.id, date=self.date_effective)
2023-05-10 18:45:38 +02:00
move_lines = self.get_moves_lines(wage_dict)
2020-04-16 00:37:53 +02:00
move, = Move.create([{
'journal': self.journal.id,
'origin': str(self),
'period': period_id,
'date': self.date_effective,
'state': 'draft',
'description': self.description,
'lines': [('create', move_lines)],
2020-04-16 00:37:53 +02:00
}])
self.write([self], {'move': move.id})
Move.post([self.move])
2023-05-10 18:45:38 +02:00
def get_moves_lines(self, wage_dict):
2020-04-16 00:37:53 +02:00
lines_moves = {}
mandatory_wages = dict([(m.wage_type.id, m.party)
for m in self.employee.mandatory_wages])
2021-09-22 21:47:22 +02:00
employee_id = self.employee.party.id
Configuration = Pool().get('staff.configuration')
configuration = Configuration(1)
entity_in_line = configuration.expense_contribution_entity
2023-02-01 23:24:32 +01:00
debit_acc2 = None
attr_getter = attrgetter(
2023-05-10 18:45:38 +02:00
'amount', 'party', 'amount_60_40', 'wage_type.id'
2023-02-01 23:24:32 +01:00
)
2020-04-16 00:37:53 +02:00
for line in self.lines:
2023-05-10 18:45:38 +02:00
amount, party, amount_60_40, wage_type = attr_getter(line)
wage_type_= wage_dict[wage_type]
definition = wage_type_['definition']
account_60_40 = wage_type_['account_60_40.']
debit_acc = wage_type_['debit_account.']
credit_acc = wage_type_['credit_account.']
expense_for = wage_type_['expense_formula']
2023-02-01 23:24:32 +01:00
if amount <= 0 or not wage_type:
2020-04-16 00:37:53 +02:00
continue
2023-02-01 23:24:32 +01:00
try:
party_id = party.id
except:
party_id = None
if not party_id:
2023-05-10 18:45:38 +02:00
if mandatory_wages.get(wage_type):
party_id = mandatory_wages[wage_type].id
2020-04-16 00:37:53 +02:00
else:
2021-09-22 21:47:22 +02:00
party_id = employee_id
2020-04-16 00:37:53 +02:00
expense = Decimal(0)
2023-02-01 23:24:32 +01:00
# if not line.wage_type:
# continue
if expense_for:
2023-05-10 18:45:38 +02:00
expense = line.get_expense_amount(wage_type_)
2020-04-16 00:37:53 +02:00
2023-02-01 23:24:32 +01:00
if definition == 'payment':
amount_debit = amount + expense
if amount_60_40:
amount_debit = amount - amount_60_40
amount_debit2 = amount_60_40
debit_acc2 = account_60_40
2020-04-16 00:37:53 +02:00
else:
if expense:
amount_debit = expense
2023-02-01 23:24:32 +01:00
elif debit_acc:
amount_debit = amount
2020-04-16 00:37:53 +02:00
2023-02-01 23:24:32 +01:00
amount_credit = amount + expense
# debit_acc = line.wage_type.debit_account
2020-04-16 00:37:53 +02:00
2023-05-10 18:45:38 +02:00
if True:
2020-04-16 00:37:53 +02:00
if debit_acc and amount_debit > _ZERO:
2023-02-01 23:24:32 +01:00
if definition == 'discount':
2020-04-16 00:37:53 +02:00
amount_debit = amount_debit * (-1)
2023-05-10 18:45:38 +02:00
if debit_acc['id'] not in lines_moves.keys():
if entity_in_line:
p = party_id
else:
p = employee_id
2023-05-10 18:45:38 +02:00
lines_moves[debit_acc['id']] = {
2021-09-22 21:47:22 +02:00
employee_id: line.get_move_line(
debit_acc, p,
('debit', amount_debit)
)}
2020-04-16 00:37:53 +02:00
else:
line.update_move_line(
2023-05-10 18:45:38 +02:00
lines_moves[debit_acc['id']][employee_id],
2020-04-16 00:37:53 +02:00
{'debit': amount_debit, 'credit': _ZERO}
)
2023-02-01 23:24:32 +01:00
if debit_acc2:
2023-05-10 18:45:38 +02:00
if debit_acc2['id'] not in lines_moves.keys():
lines_moves[debit_acc2['id']] = {
2023-02-01 23:24:32 +01:00
employee_id: line.get_move_line(
debit_acc2, party_id,
('debit', amount_debit2)
)}
else:
line.update_move_line(
2023-05-10 18:45:38 +02:00
lines_moves[debit_acc2['id']][employee_id],
2023-02-01 23:24:32 +01:00
{'debit': amount_debit, 'credit': _ZERO}
)
# credit_acc = line.wage_type.credit_account
2020-04-16 00:37:53 +02:00
if amount_credit > _ZERO:
line_credit_ready = False
if credit_acc:
2023-05-10 18:45:38 +02:00
if credit_acc['id'] not in lines_moves.keys():
lines_moves[credit_acc['id']] = {
2021-09-22 21:47:22 +02:00
party_id: line.get_move_line(
2021-11-10 18:33:04 +01:00
credit_acc, party_id, ('credit',
amount_credit)
)}
2020-04-16 00:37:53 +02:00
line_credit_ready = True
else:
2023-05-10 18:45:38 +02:00
if party_id not in lines_moves[credit_acc['id']].keys():
lines_moves[credit_acc['id']].update({
2021-09-22 21:47:22 +02:00
party_id: line.get_move_line(
2021-11-10 18:33:04 +01:00
credit_acc, party_id, (
'credit', amount_credit)
)
})
2020-04-16 00:37:53 +02:00
line_credit_ready = True
2023-02-01 23:24:32 +01:00
if definition != 'payment':
2023-05-10 18:45:38 +02:00
deduction_acc = wage_type_['deduction_account.']
2020-04-16 00:37:53 +02:00
if deduction_acc:
2023-05-10 18:45:38 +02:00
if deduction_acc['id'] not in lines_moves.keys():
lines_moves[deduction_acc['id']] = {
2021-09-22 21:47:22 +02:00
employee_id: line.get_move_line(
2021-11-10 18:33:04 +01:00
deduction_acc, employee_id, (
2023-02-01 23:24:32 +01:00
'credit', -amount),
2021-11-10 18:33:04 +01:00
)}
2020-04-16 00:37:53 +02:00
line_credit_ready = True
else:
2023-05-10 18:45:38 +02:00
lines_moves[deduction_acc['id']][employee_id]['credit'] -= amount
2020-04-16 00:37:53 +02:00
if credit_acc and not line_credit_ready:
2023-05-10 18:45:38 +02:00
lines_moves[credit_acc['id']][party_id]['credit'] += amount_credit
# except Exception as e:
# raise WageTypeValidationError(
# gettext('staff_payroll.bad_configuration_of_wage_type', wage=wage_type_['name']))
2020-04-16 00:37:53 +02:00
result = []
for r in lines_moves.values():
2022-02-28 23:34:09 +01:00
_line = list(r.values())
2020-04-16 00:37:53 +02:00
if _line[0]['debit'] > 0 and _line[0]['credit'] > 0:
new_value = _line[0]['debit'] - _line[0]['credit']
if new_value >= 0:
2022-02-11 16:29:02 +01:00
_line[0]['debit'] = abs(new_value)
2020-04-16 00:37:53 +02:00
_line[0]['credit'] = 0
else:
2022-02-11 16:29:02 +01:00
_line[0]['credit'] = abs(new_value)
2020-04-16 00:37:53 +02:00
_line[0]['debit'] = 0
result.extend(_line)
return result
2023-04-29 18:14:49 +02:00
def _create_payroll_lines(self, config, wages, extras, discounts=None, cache_wage_dict=None):
2020-04-16 00:37:53 +02:00
PayrollLine = Pool().get('staff.payroll.line')
2023-04-29 18:14:49 +02:00
Wage = Pool().get('staff.wage_type')
2020-04-16 00:37:53 +02:00
values = []
salary_args = {}
2023-04-29 18:14:49 +02:00
# salary_in_date = self.contract.get_salary_in_date(self.end)
2021-11-11 04:16:17 +01:00
get_line = self.get_line
get_line_quantity = self.get_line_quantity
get_line_quantity_special = self.get_line_quantity_special
2021-11-11 05:53:40 +01:00
get_salary_full = self.get_salary_full
2021-11-11 18:36:25 +01:00
values_append = values.append
2023-04-29 18:14:49 +02:00
compute_unit_price = Wage.compute_unit_price
2020-05-28 02:39:25 +02:00
for wage, party, fix_amount in wages:
if not fix_amount:
2023-04-29 18:14:49 +02:00
time_salary = time.time()
2021-11-11 05:53:40 +01:00
salary_args = get_salary_full(wage)
2023-04-29 18:14:49 +02:00
time_salary2 = time.time()
2023-05-01 19:19:03 +02:00
# print(time_salary2-time_salary, 'time salary')
2021-11-11 18:36:25 +01:00
# Este metodo instancia podria pasarse a un metodo de clase
2023-04-29 18:14:49 +02:00
unit_value = compute_unit_price(wage['unit_price_formula'], salary_args)
2020-05-28 02:39:25 +02:00
else:
unit_value = fix_amount
2020-04-16 00:37:53 +02:00
discount = None
2023-04-29 18:14:49 +02:00
if discounts and discounts.get(wage['id']):
discount = discounts.get(wage['id'])
if wage['type_concept'] == 'especial':
qty = get_line_quantity_special(wage)
else:
time_qty = time.time()
2021-11-11 04:16:17 +01:00
qty = get_line_quantity(
2023-04-29 18:14:49 +02:00
config, wage, self.start, self.end, extras, discount
)
2023-04-29 18:14:49 +02:00
time_qty2 = time.time()
2023-05-01 19:19:03 +02:00
# print(time_qty2 - time_qty, 'time qty')
2023-04-29 18:14:49 +02:00
time_line = time.time()
2023-05-01 19:19:03 +02:00
# print(wage, self.start, self.end, party, qty)
2021-11-11 04:16:17 +01:00
line_ = get_line(wage, qty, unit_value, party)
2023-04-29 18:14:49 +02:00
time_line2 = time.time()
2023-05-01 19:19:03 +02:00
# print(time_line2-time_line, 'time line')
2021-11-11 18:36:25 +01:00
values_append(line_)
2020-04-16 00:37:53 +02:00
PayrollLine.create(values)
2023-04-29 18:14:49 +02:00
@classmethod
2023-05-13 00:36:13 +02:00
def get_fields_names_wage_types(cls):
2023-04-29 18:14:49 +02:00
fields_names = [
'unit_price_formula', 'concepts_salary', 'salary_constitute',
'name', 'sequence', 'definition', 'unit_price_formula',
'expense_formula', 'uom', 'default_quantity', 'type_concept',
'salary_constitute', 'receipt', 'concepts_salary',
'contract_finish', 'limit_days', 'month_application',
2023-05-10 18:45:38 +02:00
'minimal_amount', 'adjust_days_worked', 'round_amounts',
'debit_account.name', 'credit_account.name',
'deduction_account.name', 'account_60_40.name'
2023-04-29 18:14:49 +02:00
]
2023-05-13 00:36:13 +02:00
return fields_names
@classmethod
def create_cache_wage_types(cls):
Wage = Pool().get('staff.wage_type')
fields_names = cls.get_fields_names_wage_types()
2023-04-29 18:14:49 +02:00
wages = Wage.search_read([], fields_names=fields_names)
return {w['id']: w for w in wages}
def set_preliquidation(self, config, extras, discounts=None, cache_wage_dict=None):
2020-04-16 00:37:53 +02:00
wage_salary = []
wage_no_salary = []
2023-05-01 12:25:06 +02:00
wage_salary_append = wage_salary.append
2023-04-29 18:14:49 +02:00
wage_no_salary_append = wage_no_salary.append
attr_mandatory = attrgetter('wage_type', 'party', 'fix_amount')
2020-04-16 00:37:53 +02:00
for concept in self.employee.mandatory_wages:
2023-04-29 18:14:49 +02:00
wage_type, party, fix_amount = attr_mandatory(concept)
if wage_type.salary_constitute:
2021-11-10 18:33:04 +01:00
wage_salary.append(
2023-04-29 18:14:49 +02:00
(cache_wage_dict[wage_type.id], party, fix_amount))
2020-04-16 00:37:53 +02:00
else:
2021-11-10 18:33:04 +01:00
wage_no_salary.append(
2023-04-29 18:14:49 +02:00
(cache_wage_dict[wage_type.id], party, fix_amount))
2020-04-16 00:37:53 +02:00
2023-04-29 18:14:49 +02:00
self._create_payroll_lines(config, wage_salary, extras, discounts, cache_wage_dict)
self._create_payroll_lines(config, wage_no_salary, extras, discounts, cache_wage_dict)
2020-04-16 00:37:53 +02:00
2023-04-29 18:14:49 +02:00
def update_preliquidation(self, extras, cache_wage_dict):
Wage = Pool().get('staff.wage_type')
get_salary_full = self.get_salary_full
compute_unit_price = Wage.compute_unit_price
2020-04-16 00:37:53 +02:00
for line in self.lines:
2023-04-29 18:14:49 +02:00
wage_id = line.wage_type.id
wage = cache_wage_dict[wage_id]
if wage['salary_constitute']:
continue
2023-04-29 18:14:49 +02:00
salary_args = get_salary_full(wage)
unit_value = compute_unit_price(wage['unit_price_formula'], salary_args)
line.write([line], {
'unit_value': unit_value,
})
2020-04-16 00:37:53 +02:00
def get_line(self, wage, qty, unit_value, party=None):
res = {
2023-04-29 18:14:49 +02:00
'sequence': wage['sequence'],
'payroll': self.id,
2023-04-29 18:14:49 +02:00
'wage_type': wage['id'],
'description': wage['name'],
2021-11-10 20:41:58 +01:00
'quantity': Decimal(str(round(qty, 2))),
2021-11-10 23:15:04 +01:00
'unit_value': Decimal(str(round(unit_value, 2))),
2023-04-29 18:14:49 +02:00
'uom': wage['uom'],
'receipt': wage['receipt'],
2020-04-16 00:37:53 +02:00
}
if party:
res['party'] = party.id
return res
2023-04-29 18:14:49 +02:00
def _get_line_quantity(self, config, quantity_days, wage, extras, discount):
default_hour_workday = config.default_hour_workday or _DEFAULT_WORK_DAY
2023-04-29 18:14:49 +02:00
quantity = wage['default_quantity'] or 0
uom_id = wage['uom']
wage_name = wage['name'].lower()
2020-04-16 00:37:53 +02:00
if quantity_days < 0:
quantity_days = 0
2023-04-29 18:14:49 +02:00
if uom_id == Id('product', 'uom_day').pyson():
2020-04-16 00:37:53 +02:00
quantity = quantity_days
if discount:
quantity -= discount
2023-04-29 18:14:49 +02:00
elif uom_id == Id('product', 'uom_hour').pyson():
if wage['type_concept'] != 'extras':
2020-04-16 00:37:53 +02:00
quantity = quantity_days * default_hour_workday
if discount:
quantity -= discount
else:
2023-05-10 18:45:38 +02:00
extra_key = wage_name.split(' ')[0]
quantity = extras.get(extra_key, 0)
2023-05-01 12:25:06 +02:00
2020-04-16 00:37:53 +02:00
return quantity
2023-04-29 18:14:49 +02:00
def get_line_quantity(self, config, wage, start=None, end=None, extras=None, discount=None):
2020-04-16 00:37:53 +02:00
quantity_days = self.get_days(start, end)
2021-11-10 18:33:04 +01:00
quantity = self._get_line_quantity(
2023-04-29 18:14:49 +02:00
config, quantity_days, wage, extras, discount)
2020-04-16 00:37:53 +02:00
return quantity
def get_line_quantity_special(self, wage):
quantity_days = 0
2023-04-29 18:14:49 +02:00
if self.contract and self.date_effective:
2021-11-10 18:33:04 +01:00
quantity_days = (self.date_effective
- self.contract.start_date).days
2020-04-16 00:37:53 +02:00
if quantity_days > wage.limit_days:
quantity_days = wage.limit_days
return quantity_days
@fields.depends('lines')
def on_change_with_amount(self, name=None):
res = []
for line in self.lines:
if not line.amount:
continue
if name == 'gross_payments':
if line.wage_type.definition == 'payment' and line.receipt:
res.append(line.amount)
else:
2022-05-03 16:34:42 +02:00
if line.wage_type.definition != 'payment' and line.wage_type.type_concept != 'unpaid_leave':
2020-04-16 00:37:53 +02:00
res.append(line.amount)
res = self.currency.round(sum(res))
return res
def get_net_payment(self, name=None):
return (self.gross_payments - self.total_deductions)
def get_total_cost(self, name):
res = sum([
line.amount for line in self.lines
2021-11-10 18:33:04 +01:00
if line.wage_type.definition == 'payment'])
2020-04-16 00:37:53 +02:00
return res
def check_start_end(self):
if self.start <= self.end:
if self.kind != 'normal':
return
if self.start >= self.period.start and \
self.end <= self.period.end:
return
if self.start >= self.period.start and \
self.end is None:
2020-04-16 00:37:53 +02:00
return
2021-06-04 23:04:40 +02:00
raise PayrollValidationError(
gettext('staff_payroll.msg_wrong_start_end', employee=self.employee.party.name))
2020-04-16 00:37:53 +02:00
@fields.depends('period', 'start', 'end', 'employee')
def on_change_period(self):
self.start = None
self.end = None
self.contract = None
self.description = None
if self.period:
self.start = self.period.start
self.end = self.period.end
self.contract = self.search_contract_on_period(
self.employee.id, self.start, self.end
2020-04-16 00:37:53 +02:00
)
if not self.description:
self.description = self.period.description
@classmethod
def search_contract_on_period(cls, employee_id, period_start, period_end):
2020-04-16 00:37:53 +02:00
Contract = Pool().get('staff.contract')
dom_contract = get_dom_contract_period(period_start, period_end)
2020-04-16 00:37:53 +02:00
contracts = Contract.search([
('employee', '=', employee_id),
2022-04-01 00:08:24 +02:00
dom_contract
2020-04-16 00:37:53 +02:00
])
if not contracts:
2021-11-10 18:42:47 +01:00
# last_date_futhermore = employee.get_last_date_futhermore()
# if last_date_futhermore and last_date_futhermore > period.start:
# return employee.contract
2020-04-16 00:37:53 +02:00
return
2021-11-10 18:42:47 +01:00
values = dict([(c.finished_date or c.start_date, c)
for c in contracts])
2020-04-16 00:37:53 +02:00
last_contract = values[max(values.keys())]
2021-11-11 04:09:58 +01:00
finished_date = last_contract.finished_date
start = period_start
end = period_end
2021-11-11 04:09:58 +01:00
if finished_date:
if (finished_date >= start
and finished_date <= end) or \
(finished_date >= end
and finished_date >= start):
2020-04-16 00:37:53 +02:00
return last_contract
else:
return last_contract
def get_days(self, start, end):
adjust = 1
quantity_days = (end - start).days + adjust
if quantity_days < 0:
quantity_days = 0
return quantity_days
2023-04-29 18:14:49 +02:00
def recompute_lines(self, cache_wage_dict):
Wage = Pool().get('staff.wage_type')
compute_unit_price = Wage.compute_unit_price
get_salary_full = self.get_salary_full
2020-04-16 00:37:53 +02:00
for line in self.lines:
2023-04-29 18:14:49 +02:00
wage = cache_wage_dict[line.wage_type.id]
if not wage['concepts_salary']:
2020-04-16 00:37:53 +02:00
continue
2023-04-29 18:14:49 +02:00
salary_args = get_salary_full(wage)
unit_value = compute_unit_price(wage['unit_price_formula'], salary_args)
2020-04-16 00:37:53 +02:00
line.write([line], {'unit_value': unit_value})
def _validate_amount_wage(self, wage, amount):
return amount
2020-04-16 00:37:53 +02:00
class PayrollLine(ModelSQL, ModelView):
"Payroll Line"
__name__ = "staff.payroll.line"
sequence = fields.Integer('Sequence')
payroll = fields.Many2One('staff.payroll', 'Payroll',
2021-11-10 18:33:04 +01:00
ondelete='CASCADE', select=True, required=True)
2020-04-16 00:37:53 +02:00
description = fields.Char('Description', required=True)
wage_type = fields.Many2One('staff.wage_type', 'Wage Type',
2021-11-10 18:33:04 +01:00
required=True, depends=['payroll', '_parent_payroll'])
2020-04-16 00:37:53 +02:00
uom = fields.Many2One('product.uom', 'Unit', depends=['wage_type'],
2021-11-10 18:33:04 +01:00
states={'readonly': Bool(Eval('wage_type'))})
2020-04-16 00:37:53 +02:00
quantity = fields.Numeric('Quantity', digits=(16, 2))
unit_value = fields.Numeric('Unit Value', digits=(16, 2),
2021-11-10 18:33:04 +01:00
depends=['wage_type'])
2020-04-16 00:37:53 +02:00
amount = fields.Function(fields.Numeric('Amount',
2021-11-10 18:33:04 +01:00
digits=(16, 2), depends=['unit_value', 'quantity'],
states={
'readonly': ~Eval('_parent_payroll'),
}), 'get_amount')
2020-04-16 00:37:53 +02:00
receipt = fields.Boolean('Print Receipt')
reconciled = fields.Function(fields.Boolean('Reconciled'),
2021-11-10 18:33:04 +01:00
'get_reconciled')
2020-04-16 00:37:53 +02:00
party = fields.Many2One('party.party', 'Party', depends=['wage_type'])
@classmethod
def __setup__(cls):
super(PayrollLine, cls).__setup__()
cls._order.insert(0, ('sequence', 'ASC'))
@staticmethod
def default_quantity():
return Decimal(str(1))
@fields.depends('wage_type', 'uom', 'quantity', 'party',
2021-11-10 18:33:04 +01:00
'description', 'unit_value', 'payroll', 'receipt', 'sequence',
'_parent_payroll.employee', '_parent_payroll.contract',
'_parent_payroll.end', '_parent_payroll.lines', '_parent_payroll.start',
2022-02-16 00:02:57 +01:00
'_parent_payroll.period', '_parent_payroll.kind', '_parent_payroll.last_payroll')
2020-04-16 00:37:53 +02:00
def on_change_wage_type(self):
if not self.wage_type:
return
2023-05-13 00:36:13 +02:00
fields_names = self.payroll.get_fields_names_wage_types()
wage_id = self.wage_type.id
wage_type, = self.wage_type.search_read(
[('id', '=', wage_id)],
fields_names=fields_names)
self.uom = wage_type['uom']
self.description = wage_type['name']
self.quantity = wage_type['default_quantity']
self.receipt = wage_type['receipt']
self.sequence = wage_type['sequence']
2020-04-16 00:37:53 +02:00
parties = []
for wage in self.payroll.employee.mandatory_wages:
2023-05-13 00:36:13 +02:00
if wage.wage_type.id == wage_id and wage.party:
2020-04-16 00:37:53 +02:00
parties.append(wage.party.id)
if parties:
self.party = parties[0]
2023-05-13 00:36:13 +02:00
if wage_type['unit_price_formula']:
salary_args = self.payroll.get_salary_full(wage_type)
2020-04-16 00:37:53 +02:00
self.unit_value = self.wage_type.compute_unit_price(
2023-05-13 00:36:13 +02:00
wage_type['unit_price_formula'], salary_args)
2020-04-16 00:37:53 +02:00
def get_amount(self, name):
return self.on_change_with_amount()
@fields.depends('quantity', 'unit_value', '_parent_payroll.currency')
def on_change_with_amount(self):
quantity = 0
unit_value = 0
res = _ZERO
if self.quantity and self.unit_value:
quantity = float(self.quantity)
unit_value = float(self.unit_value)
res = Decimal(str(round((quantity * unit_value), 2)))
return res
def get_reconciled(self, name=None):
#TODO: Reconciled must be computed from move line, similar way
# to account invoice
pass
2021-09-22 21:47:22 +02:00
def get_move_line(self, account, party_id, amount):
2020-04-16 00:37:53 +02:00
debit = credit = _ZERO
if amount[0] == 'debit':
debit = amount[1]
else:
credit = amount[1]
res = {
2023-05-10 18:45:38 +02:00
'description': account['name'],
2020-04-16 00:37:53 +02:00
'debit': debit,
'credit': credit,
2023-05-10 18:45:38 +02:00
'account': account['id'],
2021-09-22 21:47:22 +02:00
'party': party_id,
2020-04-16 00:37:53 +02:00
}
return res
def update_move_line(self, move_line, values):
if values['debit']:
move_line['debit'] += values['debit']
if values['credit']:
move_line['credit'] += values['credit']
return move_line
2023-05-10 18:45:38 +02:00
def get_expense_amount(self, wage_type):
2020-05-24 01:27:13 +02:00
expense = 0
2023-05-10 18:45:38 +02:00
wage_type_ = self.wage_type
if wage_type['expense_formula']:
2023-04-29 18:14:49 +02:00
salary_args = self.payroll.get_salary_full(wage_type)
2023-05-10 18:45:38 +02:00
expense = wage_type_.compute_expense(wage_type['expense_formula'], salary_args)
2020-05-24 01:27:13 +02:00
return expense
2020-04-16 00:37:53 +02:00
class PayrollReport(CompanyReport):
__name__ = 'staff.payroll'
@classmethod
2021-06-04 23:04:40 +02:00
def get_context(cls, records, header, data):
report_context = super().get_context(records, header, data)
2020-04-16 00:37:53 +02:00
return report_context
class PayrollGroupStart(ModelView):
'Payroll Group Start'
__name__ = 'staff.payroll_group.start'
period = fields.Many2One('staff.payroll.period', 'Period',
2023-01-24 01:42:43 +01:00
required=True, domain=[('state', '=', 'open')])
2020-04-16 00:37:53 +02:00
description = fields.Char('Description', required=True)
company = fields.Many2One('company.company', 'Company',
2023-01-24 01:42:43 +01:00
required=True)
2020-04-16 00:37:53 +02:00
wage_types = fields.Many2Many('staff.wage_type', None, None,
2023-01-24 01:42:43 +01:00
'Wage Types')
2020-04-16 00:37:53 +02:00
@staticmethod
def default_company():
return Transaction().context.get('company')
@fields.depends('period', 'description')
def on_change_period(self):
if not self.period:
return
if not self.description and self.period.description:
self.description = self.period.description
class PayrollGroup(Wizard):
'Payroll Group'
__name__ = 'staff.payroll_group'
2022-12-16 20:34:51 +01:00
start = StateView(
'staff.payroll_group.start',
'staff_payroll.payroll_group_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Accept', 'open_', 'tryton-ok', default=True),
])
2020-04-16 00:37:53 +02:00
open_ = StateTransition()
def transition_open_(self):
2023-04-29 18:14:49 +02:00
time_initial = time.time()
2020-04-16 00:37:53 +02:00
pool = Pool()
Employee = pool.get('company.employee')
Payroll = pool.get('staff.payroll')
2023-04-29 18:14:49 +02:00
Configuration = Pool().get('staff.configuration')
config = Configuration(1)
2020-04-16 00:37:53 +02:00
#Remove employees with payroll this period
payrolls_period = Payroll.search([
('period', '=', self.start.period.id),
2021-11-11 18:36:25 +01:00
# ('department', '=', self.start.department.id),
2020-04-16 00:37:53 +02:00
])
2023-04-29 18:14:49 +02:00
cache_wage_dict = Payroll.create_cache_wage_types()
2020-04-16 00:37:53 +02:00
2021-11-11 04:39:54 +01:00
# contracts_w_payroll = [p.contract.id for p in payrolls_period]
employee_w_payroll = [p.employee.id for p in payrolls_period]
2021-11-11 05:03:47 +01:00
dom_employees = self.get_employees_dom(employee_w_payroll)
2020-04-16 00:37:53 +02:00
payroll_to_create = []
2022-04-01 02:06:22 +02:00
employees = Employee.search(dom_employees, limit=400)
2021-11-11 04:39:54 +01:00
get_values = self.get_values
search_contract_on_period = Payroll.search_contract_on_period
2021-11-11 04:40:51 +01:00
period = self.start.period
2021-11-11 18:36:25 +01:00
start = period.start
end = period.end
2021-09-14 16:02:55 +02:00
for employee in employees:
2021-11-11 04:39:54 +01:00
if employee.id in employee_w_payroll:
continue
contract = search_contract_on_period(employee.id, start, end)
2021-11-11 04:39:54 +01:00
if not contract:
2020-04-16 00:37:53 +02:00
continue
2021-11-11 04:39:54 +01:00
values = get_values(contract, start, end)
2020-04-16 00:37:53 +02:00
payroll_to_create.append(values)
2021-11-11 05:00:02 +01:00
wages = [
(cache_wage_dict[wage_type.id], None, None) for wage_type in self.start.wage_types
2021-11-11 05:00:02 +01:00
]
2021-11-11 04:43:11 +01:00
PayrollCreate = Payroll.create
2020-04-16 00:37:53 +02:00
if payroll_to_create:
2021-11-11 04:43:11 +01:00
payrolls = PayrollCreate(payroll_to_create)
2021-11-11 04:19:08 +01:00
len_payrolls = len(payrolls)
2020-05-23 16:02:35 +02:00
cont = 0
2020-04-16 00:37:53 +02:00
for payroll in payrolls:
2023-05-01 19:19:03 +02:00
# print('contador > ', cont, ' / ', len_payrolls)
2023-04-29 18:14:49 +02:00
# try:
cont += 1
time_pre = time.time()
payroll.set_preliquidation(config, {}, None, cache_wage_dict)
time_pre2 = time.time()
2023-05-13 00:36:13 +02:00
# print(time_pre2-time_pre, 'time preliquidation')
2023-04-29 18:14:49 +02:00
if wages:
payroll._create_payroll_lines(config, wages, None, {}, cache_wage_dict)
# except Exception as e:
# print('Fallo al crear nomina : ', payroll.employee.party.name, e)
time_final = time.time()
2023-05-13 00:36:13 +02:00
# print(time_final - time_initial, 'final create payroll')
2020-04-16 00:37:53 +02:00
return 'end'
2021-11-11 18:36:25 +01:00
def get_employees_dom(self, employees_w_payroll):
2020-04-16 00:37:53 +02:00
dom_employees = [
2021-11-11 05:45:29 +01:00
('active', '=', True),
2021-11-11 18:36:25 +01:00
('id', 'not in', employees_w_payroll),
]
2020-04-16 00:37:53 +02:00
return dom_employees
2020-08-06 19:33:45 +02:00
def get_values(self, contract, start_date, end_date):
employee = contract.employee
2021-11-11 05:39:45 +01:00
ct_start_date = contract.start_date
2021-12-04 18:28:28 +01:00
ct_end_date = contract.finished_date
2021-11-11 05:39:45 +01:00
if ct_start_date and ct_start_date >= start_date and \
2021-12-23 21:02:58 +01:00
ct_start_date <= end_date:
2021-11-11 05:39:45 +01:00
start_date = ct_start_date
if ct_end_date and ct_end_date >= start_date and \
ct_end_date <= end_date:
end_date = ct_end_date
2020-04-16 00:37:53 +02:00
values = {
'employee': employee.id,
'period': self.start.period.id,
'start': start_date,
'end': end_date,
'description': self.start.description,
'date_effective': end_date,
2021-11-11 05:45:29 +01:00
'contract': contract.id,
2022-01-14 22:08:26 +01:00
'department': employee.department.id if employee.department else None,
2020-04-16 00:37:53 +02:00
}
return values
class PayrollPreliquidation(Wizard):
'Payroll Preliquidation'
__name__ = 'staff.payroll.preliquidation'
start_state = 'create_preliquidation'
create_preliquidation = StateTransition()
def transition_create_preliquidation(self):
Payroll = Pool().get('staff.payroll')
2023-04-29 18:14:49 +02:00
Configuration = Pool().get('staff.configuration')
config = Configuration(1)
2020-04-16 00:37:53 +02:00
ids = Transaction().context['active_ids']
2023-04-29 18:14:49 +02:00
cache_wage_dict = Payroll.create_cache_wage_types()
2020-04-16 00:37:53 +02:00
for payroll in Payroll.browse(ids):
if payroll.state != 'draft':
return
if not payroll.lines:
2023-04-29 18:14:49 +02:00
time_1 = time.time()
payroll.set_preliquidation(config, {}, None, cache_wage_dict)
time_2 = time.time()
2023-05-13 00:36:13 +02:00
# print(time_2-time_1, 'time preliquidation')
2020-04-16 00:37:53 +02:00
else:
2023-04-29 18:14:49 +02:00
payroll.update_preliquidation({}, cache_wage_dict)
2020-04-16 00:37:53 +02:00
return 'end'
class PayrollRecompute(Wizard):
'Payroll Recompute'
__name__ = 'staff.payroll.recompute'
start_state = 'do_recompute'
do_recompute = StateTransition()
def transition_do_recompute(self):
Payroll = Pool().get('staff.payroll')
2023-05-10 21:45:29 +02:00
wages_dict = Payroll.create_cache_wage_types()
2020-04-16 00:37:53 +02:00
ids = Transaction().context['active_ids']
for payroll in Payroll.browse(ids):
if payroll.state != 'draft' or not payroll.lines:
continue
2023-05-10 21:45:29 +02:00
payroll.recompute_lines(wages_dict)
2020-04-16 00:37:53 +02:00
return 'end'
class Move(metaclass=PoolMeta):
2020-04-16 00:37:53 +02:00
__name__ = 'account.move'
@classmethod
def _get_origin(cls):
return super(Move, cls)._get_origin() + ['staff.payroll']