trytonpsk-staff_payroll_co/liquidation.py

738 lines
28 KiB
Python
Raw Normal View History

2020-04-16 00:38:42 +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.
from decimal import Decimal
from trytond import backend
from trytond.model import Workflow, ModelSQL, ModelView, fields
from trytond.pool import Pool
from trytond.report import Report
from trytond.pyson import Eval, If, Bool
2020-04-18 20:23:40 +02:00
from trytond.wizard import Wizard, StateView, Button, StateReport, StateTransition
2020-04-16 00:38:42 +02:00
from trytond.transaction import Transaction
__all__ = ['Liquidation', '|Report', 'LiquidationLine',
'LiquidationLineMoveLine', 'LiquidationLineAdjustment',
2020-04-18 20:23:40 +02:00
'LiquidationAdjustmentStart', 'LiquidationAdjustment']
2020-04-16 00:38:42 +02:00
STATES = {'readonly': (Eval('state') != 'draft'),}
_ZERO = Decimal('0.0')
BONUS_SERVICE = ['bonus_service']
CONTRACT = ['bonus_service', 'health', 'retirement', 'unemployment',
'interest', 'holidays']
class Liquidation(Workflow, ModelSQL, ModelView):
'Staff Liquidation'
__name__ = 'staff.liquidation'
number = fields.Char('Number', readonly=True, help="Secuence",
select=True)
employee = fields.Many2One('company.employee', 'Employee',
states=STATES, required=True, depends=['state'])
start_period = fields.Many2One('staff.payroll.period', 'Start Period',
required=True, states=STATES)
end_period = fields.Many2One('staff.payroll.period', 'End Period',
required=True, states=STATES, depends=['start_period'])
kind = fields.Selection([
('contract', 'Contract'),
('bonus_service', 'Bonus Service'),
('interest', 'Interest'),
('vacation', 'Vacation'),
], 'Kind', required=True, states=STATES)
liquidation_date = fields.Date('Liquidation Date', states=STATES,
required=True)
lines = fields.One2Many('staff.liquidation.line', 'liquidation',
'Lines', states=STATES, depends=['employee', 'state'])
gross_payments = fields.Function(fields.Numeric(
'Gross Payments', states=STATES, digits=(16, 2)),
'get_sum_operation')
total_deductions = fields.Function(fields.Numeric(
'Total Deductions', states=STATES, digits=(16, 2)),
'get_sum_operation')
net_payment = fields.Function(fields.Numeric(
'Net Payment', states=STATES, digits=(16, 2)),
'get_net_payment')
time_contracting = fields.Integer('Time Contracting', states=STATES,
depends=['start_period', 'end_period', 'employee'])
state = fields.Selection([
('draft', 'Draft'),
('confirmed', 'Confirmed'),
('posted', 'Posted'),
('cancel', 'Cancel'),
], 'State', readonly=True)
company = fields.Many2One('company.company', 'Company', required=True,
states={
'readonly': (Eval('state') != 'draft') | Eval('lines', [0]),
},
domain=[
('id', If(Eval('context', {}).contains('company'), '=', '!='),
Eval('context', {}).get('company', 0)),
],
depends=['state'], select=True)
description = fields.Char('Description', states=STATES, select=True)
cause = fields.Char('Cause', states=STATES)
permissons = fields.Char('Permissons', states=STATES)
journal = fields.Many2One('account.journal', 'Journal', required=True,
states=STATES)
currency = fields.Many2One('currency.currency', 'Currency',
required=True, states={
'readonly': ((Eval('state') != 'draft')
| (Eval('lines', [0]) & Eval('currency'))),
},
depends=['state'])
move = fields.Many2One('account.move', 'Move', readonly=True)
contract = fields.Many2One('staff.contract', 'Contract', required=True, states=STATES,
domain=[('employee', '=', Eval('employee'))])
account = fields.Many2One('account.account', 'Account',
required=True, domain=[
('kind', '!=', 'view'),
])
payrolls = fields.Function(fields.Many2Many('staff.payroll',
None, None, 'Payroll', depends=['start_period', 'end_period'],
domain=[
('employee', '=', Eval('employee')),
('kind', '=', 'normal'),
],), 'get_payrolls')
start = fields.Function(fields.Date('Start Date'), 'get_dates')
end = fields.Function(fields.Date('End Date'), 'get_dates')
@classmethod
def __setup__(cls):
super(Liquidation, cls).__setup__()
cls._error_messages.update({
'employee_without_salary': ('The employee does not have salary!'),
'wrong_start_end': ('The date end can not smaller than date start'),
'sequence_missing': ('Sequence liquidation is missing!'),
'payroll_not_posted': ('The employee has payrolls does not posted!'),
})
cls._transitions |= set((
('draft', 'cancel'),
('cancel', 'draft'),
('confirmed', 'draft'),
('confirmed', 'posted'),
('draft', 'confirmed'),
('posted', 'draft'),
))
cls._buttons.update({
'draft': {
'invisible': Eval('state') == 'draft',
},
'confirm': {
'invisible': Eval('state') != 'draft',
},
'cancel': {
'invisible': Eval('state') != 'draft',
},
'post': {
'invisible': Eval('state') != 'confirmed',
},
'compute_liquidation': {
'invisible': Bool(Eval('lines')),
},
})
@staticmethod
def default_company():
return Transaction().context.get('company')
@staticmethod
def default_kind():
return 'contract'
@staticmethod
def default_state():
return 'draft'
@staticmethod
def default_currency():
Company = Pool().get('company.company')
company = Transaction().context.get('company')
if company:
company = Company(company)
return company.currency.id
@staticmethod
def default_journal():
Configuration = Pool().get('staff.configuration')
configuration = Configuration(1)
if configuration.default_journal:
return configuration.default_journal.id
@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,
('employee',) + tuple(clause[1:]),
('number',) + tuple(clause[1:]),
]
@classmethod
@ModelView.button
@Workflow.transition('confirmed')
def confirm(cls, records):
for rec in records:
rec.set_number()
@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('posted')
def post(cls, records):
for rec in records:
rec.create_move()
@classmethod
@ModelView.button
def compute_liquidation(cls, records):
for rec in records:
rec.set_liquidation_lines()
def get_dates(self, name):
if name == 'start':
values = [self.start_period.start]
if self.employee.contract.start_date:
values.append(self.employee.contract.start_date)
res = max(values)
if name == 'end':
values = [self.end_period.end]
if self.employee.contract.end_date:
values.append(self.employee.contract.end_date)
res = min(values)
return res
def get_payrolls(self, name):
if not self.employee or not self.contract:
return
Payroll = Pool().get('staff.payroll')
date_start, date_end = self._compute_time()
payrolls = Payroll.search([
('employee', '=', self.employee.id),
('start', '>=', date_start),
('end', '<=', date_end),
('contract', '=', self.contract.id),
])
payrolls_ids = [payroll.id for payroll in payrolls]
return payrolls_ids
def create_move(self):
pool = Pool()
Move = pool.get('account.move')
MoveLine = pool.get('account.move.line')
Period = pool.get('account.period')
if self.move:
return
move_lines, grouped = self.get_moves_lines()
if move_lines:
period_id = Period.find(self.company.id, date=self.liquidation_date)
move, = Move.create([{
'journal': self.journal.id,
#'origin': str(self),
'period': period_id,
'date': self.liquidation_date,
'description': self.description,
'lines': [('create', move_lines)],
}])
self.write([self], {'move': move.id})
for ml in move.lines:
if ml.account.id not in grouped.keys() or ml.account.kind not in ('payable', 'receivable'):
continue
to_reconcile = [ml]
to_reconcile.extend(grouped[ml.account.id]['lines'])
MoveLine.reconcile(set(to_reconcile))
Move.post([move])
def get_moves_lines(self):
lines_moves = []
to_reconcile = []
grouped = {}
amount = []
for line in self.lines:
if line.move_lines:
for moveline in line.move_lines:
to_reconcile.append(moveline)
if moveline.account.id not in grouped.keys():
grouped[moveline.account.id] = {
'amount': [],
'description': line.description,
'lines': [],
}
grouped[moveline.account.id]['amount'].append(moveline.credit)
grouped[moveline.account.id]['lines'].append(moveline)
amount.append(moveline.credit)
# else:
# if line.wage.credit_account and line.amount > 0:
# grouped[line.wage.credit_account.id] = {
# 'amount': [line.amount],
# 'description': line.description,
# 'lines': [],
# }
# amount.append(line.amount)
# else:
# lines_moves.append(self._prepare_line(self.description,
# line.wage.credit_account, credit=(abs(line.amount))))
# amount.append(line.amount)
for adjust in line.adjustments:
if adjust.account.id not in grouped.keys():
grouped[adjust.account.id] = {
'amount': [],
'description': adjust.description,
2020-04-16 00:38:42 +02:00
'lines': [],
}
grouped[adjust.account.id]['amount'].append(adjust.amount)
amount.append(adjust.amount)
2020-04-16 00:38:42 +02:00
for account_id, values in grouped.items():
lines_moves.append(self._prepare_line(values['description'],
account_id, debit=sum(values['amount'])))
if lines_moves:
lines_moves.append(self._prepare_line(self.description,
self.account, credit=sum(amount)))
return lines_moves, grouped
def _prepare_line(self, description, account_id, debit=_ZERO, credit=_ZERO):
if debit < _ZERO:
credit = debit
debit = _ZERO
if credit < _ZERO:
debit = credit
credit = _ZERO
credit = abs(credit)
debit = abs(debit)
res = {
'description': description,
'debit': debit,
'credit': credit,
'account': account_id,
'party': self.employee.party.id,
}
return res
def _compute_time(self):
date_end_contract = None
if self.contract.start_date > self.start_period.start:
date_start = self.contract.start_date
else:
date_start = self.start_period.start
if self.contract.futhermores:
date_end_contract = self.contract.finished_date
if date_end_contract and date_end_contract <= self.end_period.end:
date_end = date_end_contract
elif self.contract.end_date and self.contract.end_date < self.end_period.end:
date_end = self.contract.end_date
else:
date_end = self.end_period.end
return date_start, date_end
def set_liquidation_lines(self):
pool = Pool()
Payroll = pool.get('staff.payroll')
Liquidation = pool.get('staff.liquidation')
MoveLine = pool.get('account.move.line')
date_start, date_end = self._compute_time()
payrolls = Payroll.search([
('employee', '=', self.employee.id),
('start', '>=', date_start),
('end', '<=', date_end),
('contract', '=', self.contract.id),
])
wages = {}
days = 0
moves_ids = []
for payroll in payrolls:
#if not payroll.move:
# self.raise_user_error('payroll_not_posted')
# return
if not payroll.move:
continue
moves_ids.append(payroll.move.id)
#moves_ids = [payroll.move.id for payroll in payrolls]
for payroll in payrolls:
days += payroll.worked_days
for l in payroll.lines:
if not l.wage_type.contract_finish:
continue
if self.kind == 'contract':
if l.wage_type.type_concept not in CONTRACT:
continue
elif self.kind == 'interest':
if l.wage_type.type_concept != 'interest':
continue
elif self.kind == 'vacation':
if l.wage_type.type_concept != 'holidays':
continue
else:
if l.wage_type.type_concept not in BONUS_SERVICE:
continue
if not l.wage_type.contract_finish:
continue
if l.wage_type.id not in wages:
account_id = None
if l.wage_type.credit_account:
account_id = l.wage_type.credit_account.id
lines = MoveLine.search([
('move', 'in', moves_ids),
('account', '=', account_id),
('party', '=', self.employee.party.id),
('reconciliation', '=', None),
])
values = []
lines_to_reconcile = []
for line in lines:
values.append(abs(line.debit - line.credit))
lines_to_reconcile.append(line.id)
wages[l.wage_type.id] = {
'sequence': l.wage_type.sequence,
'wage': l.wage_type.id,
'description': l.wage_type.name,
'amount': sum(values),
'days': self.time_contracting,
'account': account_id,
'move_lines': [('add', lines_to_reconcile)],
}
lines_to_create = wages.values()
Liquidation.write([self], {
'lines': [('create', lines_to_create)]}
)
@fields.depends('start_period', 'end_period', 'contract')
def on_change_with_time_contracting(self):
delta = None
if self.start_period and self.end_period and self.contract:
date_start, date_end = self._compute_time()
delta = (date_end - date_start).days + 1
return delta
def _get_time_contracting(self):
if not self.start_period or not self.end_period or not self.employee:
return
Payroll = Pool().get('staff.payroll')
payrolls = Payroll.search([
('employee', '=', self.employee.id),
('period.start', '>=', self.contract.start_date),
('period.end', '<=', self.contract.end_date),
('contract', '=', self.contract.id),
])
return sum([p.worked_days for p in payrolls])
def set_number(self):
if self.number:
return
pool = Pool()
Sequence = pool.get('ir.sequence')
Configuration = pool.get('staff.configuration')
configuration = Configuration(1)
if not configuration.staff_liquidation_sequence:
self.raise_user_error('sequence_missing',)
seq = configuration.staff_liquidation_sequence.id
self.write([self], {'number': Sequence.get_id(seq)})
def get_sum_operation(self, name):
res = []
for line in self.lines:
if not line.amount:
continue
if name == 'gross_payments' and line.wage.definition == 'payment':
res.append(line.amount)
elif name == 'total_deductions' and line.wage.definition != 'payment':
res.append(line.amount)
return sum(res)
def get_net_payment(self, name):
return self.currency.round(self.gross_payments + self.total_deductions)
def get_currency(self, name):
return self.company.currency.id
class LiquidationLine(ModelSQL, ModelView):
'Staff Liquidation Line'
__name__ = 'staff.liquidation.line'
sequence = fields.Integer('Sequence', required=True)
liquidation = fields.Many2One('staff.liquidation', 'Liquidation',
required=True)
wage = fields.Many2One('staff.wage_type', 'Wage Type', required=True)
description = fields.Char('Description', required=True)
amount = fields.Numeric('Amount', digits=(16, 2), required=True, depends=['adjustments', 'move_lines'])
2020-04-16 00:38:42 +02:00
days = fields.Integer('Days')
notes = fields.Char('Notes')
account = fields.Many2One('account.account', 'Account')
move_lines = fields.Many2Many('staff.liquidation.line-move.line',
'line', 'move_line', 'Liquidation Line - Move Line',
domain=[
('party', '=', Eval('party')),
('account', '=', Eval('account')),
], depends=['party', 'account'])
party = fields.Function(fields.Many2One('party.party', 'Party'),
'get_party')
adjustments = fields.One2Many('staff.liquidation.line_adjustment', 'staff_liquidation_line', 'Adjustments')
2020-04-18 20:23:40 +02:00
2020-04-16 00:38:42 +02:00
@classmethod
def __setup__(cls):
super(LiquidationLine, cls).__setup__()
cls._order.insert(0, ('sequence', 'ASC'))
@classmethod
def __register__(cls, module_name):
TableHandler = backend.get('TableHandler')
table = TableHandler(cls, module_name)
# Migration from 4.0: remove hoard_amount
if table.column_exist('hoard_amount'):
table.drop_column('hoard_amount')
super(LiquidationLine, cls).__register__(module_name)
def get_party(self, name=None):
if self.liquidation.employee:
return self.liquidation.employee.party.id
@fields.depends('wage', 'description', 'amount', 'liquidation',
'_parent_liquidation.employee', '_parent_liquidation.time_contracting',
'_parent_liquidation.start_period', '_parent_liquidation.end_period',
'_parent_liquidation.currency', 'sequence')
def on_change_wage(self):
if not self.wage:
return
self.sequence = self.wage.sequence
self.description = self.wage.name
self.days = self.liquidation.time_contracting
#self.amount = self.liquidation.compute_amount(self.wage.id)
# @fields.depends('amount', 'move_lines')
# def on_change_move_lines(self):
# self.amount = sum([(ml.credit - ml.debit) for ml in self.move_lines])
@fields.depends('amount', 'adjustments', 'move_lines')
def on_change_with_amount(self, name=None):
amount_ = 0
if self.adjustments:
amount_ += sum([ad.amount or 0 for ad in self.adjustments])
if self.move_lines:
amount_ += sum([(ml.credit - ml.debit) for ml in self.move_lines])
return amount_
2020-04-16 00:38:42 +02:00
class LiquidationLineAdjustment(ModelSQL, ModelView):
'Liquidation Adjustment'
__name__ = 'staff.liquidation.line_adjustment'
2020-04-18 20:23:40 +02:00
staff_liquidation_line = fields.Many2One('staff.liquidation.line', 'Line',
required=True, select=True)
account = fields.Many2One('account.account', 'Acount',
required=True, domain=[
('company', '=', Eval('context', {}).get('company', -1)),
('kind', '!=', 'view'),
])
description = fields.Char('Description', required=True)
amount = fields.Numeric('Amount', digits=(10,2), required=True)
2020-04-16 00:38:42 +02:00
class LiquidationReport(Report):
__name__ = 'staff.liquidation.report'
class LiquidationLineMoveLine(ModelSQL):
"Liquidation Line - MoveLine"
__name__ = "staff.liquidation.line-move.line"
_table = 'staff_liquidation_line_move_line_rel'
line = fields.Many2One('staff.liquidation.line', 'Line',
ondelete='CASCADE', select=True, required=True)
move_line = fields.Many2One('account.move.line', 'Move Line',
ondelete='RESTRICT', select=True, required=True)
2020-04-18 20:23:40 +02:00
class LiquidationAdjustmentStart(ModelView):
'Create Liquidation Adjustment Start'
__name__ = 'staff.liquidation_adjustment.start'
wage_type = fields.Many2One('staff.wage_type', 'Wage Type', required=True, domain=[
('contract_finish', '=', True)
])
amount = fields.Numeric('Amount', required=True)
account = fields.Many2One('account.account', 'Acount',
required=True, domain=[
('company', '=', Eval('context', {}).get('company', -1)),
('kind', '!=', 'view'),
])
description = fields.Char('Description', required=True)
@fields.depends('wage_type', 'account')
def on_change_wage_type(self):
if self.wage_type and self.wage_type.debit_account:
self.account = self.wage_type.debit_account.id
2020-04-18 20:23:40 +02:00
class LiquidationAdjustment(Wizard):
'Create Liquidation Adjustment'
__name__ = 'staff.liquidation_adjustment'
start = StateView('staff.liquidation_adjustment.start',
'staff_payroll_co.liquidation_adjustment_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Create', 'accept', 'tryton-ok', default=True),
])
accept = StateTransition()
@classmethod
def __setup__(cls):
super(LiquidationAdjustment, cls).__setup__()
cls._error_messages.update({
'liquidation_with_move': ('The liquidation Nro "%s" have move!'),
})
def create_adjustment(self, line):
LineAdjustment = Pool().get('staff.liquidation.line_adjustment')
adjust, = LineAdjustment.create([{
'staff_liquidation_line': line.id,
'account': self.start.account.id,
'description': self.start.description,
'amount': self.start.amount,
}])
return adjust
2020-04-18 20:23:40 +02:00
def transition_accept(self):
pool = Pool()
Liquidation = pool.get('staff.liquidation')
LiquidationLine = pool.get('staff.liquidation.line')
Date = pool.get('ir.date')
id_ = Transaction().context['active_id']
liquidation, = Liquidation.search([('id', '=', id_)])
line_created = None
if liquidation:
if liquidation.move:
self.raise_user_error('liquidation_with_move', (liquidation.number))
for line in liquidation.lines:
if line.wage.id == self.start.wage_type.id:
if line.amount:
line.amount += self.start.amount
line.save()
line_created = self.create_adjustment(line)
if not line_created:
line, = LiquidationLine.create([{
'sequence':len(liquidation.lines) + 1,
'liquidation': liquidation.id,
'wage': self.start.wage_type.id,
'description': self.start.wage_type.name,
'amount': self.start.amount,
}])
self.create_adjustment(line)
2020-04-18 20:23:40 +02:00
return 'end'
2020-06-10 22:59:25 +02:00
class LiquidationGroupStart(ModelView):
'Liquidation Group Start'
__name__ = 'staff.liquidation_group.start'
employees = fields.Many2Many('company.employee', None, None, 'Employee',
required=True)
start_period = fields.Many2One('staff.payroll.period', 'Start Period',
required=True)
end_period = fields.Many2One('staff.payroll.period', 'End Period',
required=True, depends=['start_period'])
kind = fields.Selection([
('contract', 'Contract'),
('bonus_service', 'Bonus Service'),
('interest', 'Interest'),
('vacation', 'Vacation'),
], 'Kind', required=True)
liquidation_date = fields.Date('Liquidation Date', required=True)
company = fields.Many2One('company.company', 'Company', required=True)
description = fields.Char('Description', required=True)
account = fields.Many2One('account.account', 'Account',
required=True, domain=[
('kind', '!=', 'view'),
('company', '=', Eval('company'))
])
@staticmethod
def default_company():
return Transaction().context.get('company')
class LiquidationGroup(Wizard):
'Liquidation Group'
__name__ = 'staff.liquidation_group'
start = StateView('staff.liquidation_group.start',
'staff_payroll_co.liquidation_group_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Accept', 'open_', 'tryton-ok', default=True),
])
open_ = StateTransition()
def transition_open_(self):
pool = Pool()
Liquidation = pool.get('staff.liquidation')
compute_liquidations = []
start_period = self.start.start_period.id
end_period = self.start.end_period.id
kind = self.start.kind
liquidation_date = self.start.liquidation_date
company_id = self.start.company.id
description = self.start.description
currency_id = self.start.company.currency.id
account_id = self.start.account.id
employees = self.start.employees
employee_ids = [e.id for e in employees]
contracts = Liquidation.search_read([
('employee', 'in', employee_ids)
2020-06-12 02:00:14 +02:00
('kind', '=', kind)
2020-06-10 22:59:25 +02:00
], fields_names=['contract'])
contract_ids = [i['contract'] for i in contracts]
for employee in employees:
if employee.contract in contract_ids or not employee.contract:
continue
lqt_create = {
'start_period': start_period,
'end_period': end_period,
'employee': employee.id,
'contract': employee.contract.id,
'kind': kind,
'liquidation_date': liquidation_date,
'time_contracting': None,
'state': 'draft',
'company': company_id,
'description': description,
'currency': currency_id,
'account': account_id
}
liquidation, = Liquidation.create([lqt_create])
liquidation.on_change_with_time_contracting()
liquidation.save()
compute_liquidations.append(liquidation)
Liquidation.compute_liquidation(compute_liquidations)
return 'end'