mirror of
https://bitbucket.org/presik/trytonpsk-staff_payroll.git
synced 2023-12-14 05:33:13 +01:00
933 lines
36 KiB
Python
933 lines
36 KiB
Python
# 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
|
|
from decimal import Decimal
|
|
|
|
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
|
|
from trytond.i18n import gettext
|
|
from .exceptions import (PayrollDeleteError, PayrollPeriodCloseError,
|
|
PayrollExistPeriodError, PayrollMissingSequence, WageTypeValidationError,
|
|
PayrollValidationError)
|
|
|
|
STATES = {'readonly': (Eval('state') != 'draft')}
|
|
|
|
_DEFAULT_WORK_DAY = 8
|
|
|
|
_ZERO = Decimal('0.0')
|
|
|
|
|
|
class Payroll(Workflow, ModelSQL, ModelView):
|
|
"Staff Payroll"
|
|
__name__ = "staff.payroll"
|
|
_rec_name = 'number'
|
|
number = fields.Char('Number', readonly=True, help="Secuence",
|
|
select=True)
|
|
period = fields.Many2One('staff.payroll.period', 'Period',
|
|
required=True, states={
|
|
'readonly': Eval('state') != 'draft',
|
|
})
|
|
employee = fields.Many2One('company.employee', 'Employee',
|
|
states=STATES, required=True, depends=['state'], select=True)
|
|
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',
|
|
select=True, domain=[
|
|
('employee', '=', Eval('employee')),
|
|
])
|
|
start = fields.Date('Start', states=STATES, required=True)
|
|
end = fields.Date('End', states=STATES, required=True)
|
|
date_effective = fields.Date('Date Effective', states=STATES,
|
|
required=True)
|
|
description = fields.Char('Description', states=STATES, select=True)
|
|
lines = fields.One2Many('staff.payroll.line', 'payroll', 'Wage Line',
|
|
states=STATES, depends=['employee', 'state'])
|
|
gross_payments = fields.Function(fields.Numeric('Gross Payments',
|
|
digits=(16, 2), depends=['lines', 'states']), 'on_change_with_amount')
|
|
total_deductions = fields.Function(fields.Numeric(
|
|
'Total Deductions', digits=(16, 2), depends=['lines', 'states']),
|
|
'on_change_with_amount')
|
|
net_payment = fields.Function(fields.Numeric('Net Payment',
|
|
digits=(16, 2), depends=['lines', 'states']),
|
|
'get_net_payment')
|
|
total_cost = fields.Function(fields.Numeric('Total Cost',
|
|
digits=(16, 2), depends=['lines', 'states']),
|
|
'get_total_cost')
|
|
currency = fields.Many2One('currency.currency', 'Currency',
|
|
required=False,
|
|
states={
|
|
'readonly': ((Eval('state') != 'draft')
|
|
| (Eval('lines', [0]) & Eval('currency'))),
|
|
}, depends=['state'])
|
|
worked_days = fields.Function(fields.Integer('Worked Days',
|
|
depends=['start', 'end', 'state']),
|
|
'on_change_with_worked_days')
|
|
state = fields.Selection([
|
|
('draft', 'Draft'),
|
|
('processed', 'Processed'),
|
|
('cancel', 'Cancel'),
|
|
('posted', 'Posted'),
|
|
], 'State', readonly=True)
|
|
journal = fields.Many2One('account.journal', 'Journal', required=True,
|
|
states=STATES)
|
|
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'])
|
|
move = fields.Many2One('account.move', 'Move', readonly=True)
|
|
origin = fields.Reference('Origin', selection='get_origin',
|
|
select=True, depends=['state'],
|
|
states={
|
|
'readonly': Eval('state') != 'draft',
|
|
})
|
|
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'),
|
|
))
|
|
cls._buttons.update({
|
|
'draft': {
|
|
'invisible': Eval('state') == 'draft',
|
|
},
|
|
'post': {
|
|
'invisible': Eval('state') != 'processed',
|
|
},
|
|
'cancel': {
|
|
'invisible': Eval('state') != 'draft',
|
|
},
|
|
'process': {
|
|
'invisible': Eval('state') != 'draft',
|
|
},
|
|
})
|
|
|
|
@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 []
|
|
|
|
@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':
|
|
raise PayrollDeleteError(
|
|
gettext('staff_payroll.msg_delete_cancel', payroll=payroll.rec_name))
|
|
if payroll.move:
|
|
raise PayrollDeleteError(
|
|
gettext('staff_payroll.msg_existing_move', payroll=payroll.rec_name))
|
|
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,
|
|
('employee',) + tuple(clause[1:]),
|
|
('number',) + tuple(clause[1:]),
|
|
]
|
|
|
|
@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'),
|
|
])
|
|
if len(payrolls) > 1:
|
|
raise PayrollExistPeriodError(
|
|
gettext('staff_payroll.msg_payroll_exist_period'))
|
|
if payroll.period.state == 'closed':
|
|
raise PayrollPeriodCloseError(
|
|
gettext('staff_payroll.msg_period_closed'))
|
|
payroll.set_number()
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('posted')
|
|
def post(cls, records):
|
|
for payroll in records:
|
|
payroll.create_move()
|
|
|
|
@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:
|
|
raise PayrollMissingSequence(gettext('msg_sequence_missing'))
|
|
seq = configuration.staff_payroll_sequence.get()
|
|
self.write([self], {'number': seq})
|
|
|
|
@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):
|
|
wages_ids = [s.id for s in wage.concepts_salary]
|
|
if wages_ids:
|
|
salary_full = sum([
|
|
line.amount for line in self.lines if line.wage_type.id in wages_ids
|
|
])
|
|
else:
|
|
salary_full = self.contract.get_salary_in_date(self.end) or 0
|
|
return salary_full
|
|
|
|
def create_move(self):
|
|
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)
|
|
move_lines = self.get_moves_lines()
|
|
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)],
|
|
}])
|
|
self.write([self], {'move': move.id})
|
|
Move.post([self.move])
|
|
|
|
def get_moves_lines(self):
|
|
lines_moves = {}
|
|
mandatory_wages = dict([(m.wage_type.id, m.party)
|
|
for m in self.employee.mandatory_wages])
|
|
employee_id = self.employee.party.id
|
|
|
|
Configuration = Pool().get('staff.configuration')
|
|
configuration = Configuration(1)
|
|
entity_in_line = configuration.expense_contribution_entity
|
|
for line in self.lines:
|
|
if line.amount <= 0:
|
|
continue
|
|
if line.party:
|
|
party_id = line.party.id
|
|
else:
|
|
if mandatory_wages.get(line.wage_type.id):
|
|
party_id = mandatory_wages[line.wage_type.id].id
|
|
else:
|
|
party_id = employee_id
|
|
|
|
expense = Decimal(0)
|
|
if not line.wage_type:
|
|
continue
|
|
if line.wage_type.expense_formula:
|
|
expense = line.get_expense_amount()
|
|
|
|
if line.wage_type.definition == 'payment':
|
|
amount_debit = line.amount + expense
|
|
else:
|
|
if expense:
|
|
amount_debit = expense
|
|
elif line.wage_type.debit_account:
|
|
amount_debit = line.amount
|
|
|
|
amount_credit = line.amount + expense
|
|
debit_acc = line.wage_type.debit_account
|
|
|
|
try:
|
|
if debit_acc and amount_debit > _ZERO:
|
|
if line.wage_type.definition == 'discount':
|
|
amount_debit = amount_debit * (-1)
|
|
if debit_acc.id not in lines_moves.keys():
|
|
if entity_in_line:
|
|
p = party_id
|
|
else:
|
|
p = employee_id
|
|
|
|
lines_moves[debit_acc.id] = {
|
|
employee_id: line.get_move_line(
|
|
debit_acc, p,
|
|
('debit', amount_debit)
|
|
)}
|
|
else:
|
|
line.update_move_line(
|
|
lines_moves[debit_acc.id][employee_id],
|
|
{'debit': amount_debit, 'credit': _ZERO}
|
|
)
|
|
|
|
credit_acc = line.wage_type.credit_account
|
|
if amount_credit > _ZERO:
|
|
line_credit_ready = False
|
|
if credit_acc:
|
|
if credit_acc.id not in lines_moves.keys():
|
|
lines_moves[credit_acc.id] = {
|
|
party_id: line.get_move_line(
|
|
credit_acc, party_id, ('credit',
|
|
amount_credit)
|
|
)}
|
|
line_credit_ready = True
|
|
else:
|
|
if party_id not in lines_moves[credit_acc.id].keys():
|
|
lines_moves[credit_acc.id].update({
|
|
party_id: line.get_move_line(
|
|
credit_acc, party_id, (
|
|
'credit', amount_credit)
|
|
)
|
|
})
|
|
line_credit_ready = True
|
|
if line.wage_type.definition != 'payment':
|
|
deduction_acc = line.wage_type.deduction_account
|
|
if deduction_acc:
|
|
if deduction_acc.id not in lines_moves.keys():
|
|
lines_moves[deduction_acc.id] = {
|
|
employee_id: line.get_move_line(
|
|
deduction_acc, employee_id, (
|
|
'credit', -line.amount),
|
|
)}
|
|
line_credit_ready = True
|
|
else:
|
|
lines_moves[deduction_acc.id][employee_id]['credit'] -= line.amount
|
|
|
|
if credit_acc and not line_credit_ready:
|
|
lines_moves[credit_acc.id][party_id]['credit'] += amount_credit
|
|
except:
|
|
raise WageTypeValidationError(
|
|
gettext('staff_payroll.bad_configuration_wage_type', wage=line.wage_type.name))
|
|
|
|
result = []
|
|
for r in lines_moves.values():
|
|
_line = r.values()
|
|
if _line[0]['debit'] > 0 and _line[0]['credit'] > 0:
|
|
new_value = _line[0]['debit'] - _line[0]['credit']
|
|
if new_value >= 0:
|
|
_line[0]['debit'] = abs(new_value)
|
|
_line[0]['credit'] = 0
|
|
else:
|
|
_line[0]['credit'] = abs(new_value)
|
|
_line[0]['debit'] = 0
|
|
result.extend(_line)
|
|
return result
|
|
|
|
def _create_payroll_lines(self, wages, extras, discounts=None):
|
|
PayrollLine = Pool().get('staff.payroll.line')
|
|
values = []
|
|
salary_args = {}
|
|
salary_in_date = self.contract.get_salary_in_date(self.end)
|
|
get_line = self.get_line
|
|
get_line_quantity = self.get_line_quantity
|
|
get_line_quantity_special = self.get_line_quantity_special
|
|
get_salary_full = self.get_salary_full
|
|
values_append = values.append
|
|
for wage, party, fix_amount in wages:
|
|
if not fix_amount:
|
|
salary_args = get_salary_full(wage)
|
|
if wage.salary_constitute:
|
|
salary_args['salary'] = salary_in_date
|
|
|
|
# Este metodo instancia podria pasarse a un metodo de clase
|
|
unit_value = wage.compute_unit_price(salary_args)
|
|
else:
|
|
unit_value = fix_amount
|
|
|
|
discount = None
|
|
if discounts and discounts.get(wage.id):
|
|
discount = discounts.get(wage.id)
|
|
qty = get_line_quantity_special(wage)
|
|
if qty == 0:
|
|
qty = get_line_quantity(
|
|
wage, self.start, self.end, extras, discount
|
|
)
|
|
line_ = get_line(wage, qty, unit_value, party)
|
|
values_append(line_)
|
|
PayrollLine.create(values)
|
|
|
|
def set_preliquidation(self, extras, discounts=None):
|
|
wage_salary = []
|
|
wage_no_salary = []
|
|
|
|
for concept in self.employee.mandatory_wages:
|
|
wage_type = concept.wage_type
|
|
if concept.wage_type.salary_constitute:
|
|
wage_salary.append(
|
|
(wage_type, concept.party, concept.fix_amount))
|
|
else:
|
|
wage_no_salary.append(
|
|
(wage_type, concept.party, concept.fix_amount))
|
|
|
|
self._create_payroll_lines(wage_salary, extras, discounts)
|
|
self._create_payroll_lines(wage_no_salary, extras, discounts)
|
|
|
|
def update_preliquidation(self, extras):
|
|
for line in self.lines:
|
|
if line.wage_type.salary_constitute:
|
|
continue
|
|
salary_args = self.get_salary_full(line.wage_type)
|
|
unit_value = line.wage_type.compute_unit_price(salary_args)
|
|
unit_value = self._validate_amount_wage(line.wage_type, unit_value)
|
|
line.write([line], {
|
|
'unit_value': unit_value,
|
|
})
|
|
|
|
def get_line(self, wage, qty, unit_value, party=None):
|
|
res = {
|
|
'sequence': wage.sequence,
|
|
'payroll': self.id,
|
|
'wage_type': wage.id,
|
|
'description': wage.name,
|
|
'quantity': Decimal(str(round(qty, 2))),
|
|
'unit_value': Decimal(str(round(unit_value, 2))),
|
|
'uom': wage.uom,
|
|
'receipt': wage.receipt,
|
|
}
|
|
if party:
|
|
res['party'] = party.id
|
|
return res
|
|
|
|
def _get_line_quantity(self, quantity_days, wage, extras, discount):
|
|
Configuration = Pool().get('staff.configuration')
|
|
config = Configuration(1)
|
|
default_hour_workday = config.default_hour_workday or _DEFAULT_WORK_DAY
|
|
quantity = wage.default_quantity or 0
|
|
if quantity_days < 0:
|
|
quantity_days = 0
|
|
if wage.uom.id == Id('product', 'uom_day').pyson():
|
|
quantity = quantity_days
|
|
if discount:
|
|
quantity -= discount
|
|
elif wage.uom.id == Id('product', 'uom_hour').pyson():
|
|
if wage.type_concept != 'extras':
|
|
quantity = quantity_days * default_hour_workday
|
|
if discount:
|
|
quantity -= discount
|
|
else:
|
|
key_ = [key for key in extras.keys(
|
|
) if wage.name.lower().count(key) > 0]
|
|
if key_:
|
|
key_ext = key_[0]
|
|
extras_ = extras.get(key_ext)
|
|
else:
|
|
extras_ = extras.get((wage.name.lower()))
|
|
if extras_ and self.employee.position and self.employee.position.extras:
|
|
quantity = extras_
|
|
return quantity
|
|
|
|
def get_line_quantity(self, wage, start=None, end=None, extras=None, discount=None):
|
|
quantity = wage.default_quantity or 0
|
|
quantity_days = self.get_days(start, end)
|
|
quantity = self._get_line_quantity(
|
|
quantity_days, wage, extras, discount)
|
|
return quantity
|
|
|
|
def get_line_quantity_special(self, wage):
|
|
quantity_days = 0
|
|
if self.contract and self.date_effective and wage.type_concept == 'special':
|
|
quantity_days = (self.date_effective
|
|
- self.contract.start_date).days
|
|
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:
|
|
if line.wage_type.definition != 'payment':
|
|
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
|
|
if line.wage_type.definition == 'payment'])
|
|
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:
|
|
return
|
|
raise PayrollValidationError(
|
|
gettext('staff_payroll.msg_wrong_start_end', employee=self.employee.party.name))
|
|
|
|
@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, self.period
|
|
)
|
|
if not self.description:
|
|
self.description = self.period.description
|
|
|
|
@classmethod
|
|
def search_contract_on_period(cls, employee, period):
|
|
Contract = Pool().get('staff.contract')
|
|
contracts = Contract.search([
|
|
('employee', '=', employee.id), ['AND', ['OR', [
|
|
('start_date', '>=', period.start),
|
|
('finished_date', '<=', period.end),
|
|
('finished_date', '!=', None),
|
|
], [
|
|
('start_date', '<=', period.start),
|
|
('finished_date', '>=', period.start),
|
|
('finished_date', '!=', None),
|
|
], [
|
|
('start_date', '<=', period.end),
|
|
('finished_date', '>=', period.end),
|
|
('finished_date', '!=', None),
|
|
], [
|
|
('start_date', '<=', period.start),
|
|
('finished_date', '>=', period.end),
|
|
('finished_date', '!=', None),
|
|
], [
|
|
('start_date', '<=', period.start),
|
|
('finished_date', '=', None),
|
|
],
|
|
[
|
|
('start_date', '>=', period.start),
|
|
('start_date', '<=', period.end),
|
|
('finished_date', '=', None),
|
|
],
|
|
]]
|
|
])
|
|
print('INGRESA A ESTA FUNCION', contracts)
|
|
if not contracts:
|
|
# last_date_futhermore = employee.get_last_date_futhermore()
|
|
# if last_date_futhermore and last_date_futhermore > period.start:
|
|
# return employee.contract
|
|
return
|
|
values = dict([(c.finished_date or c.start_date, c)
|
|
for c in contracts])
|
|
|
|
last_contract = values[max(values.keys())]
|
|
finished_date = last_contract.finished_date
|
|
start = period.start
|
|
end = period.end
|
|
if finished_date:
|
|
if (finished_date >= start
|
|
and finished_date <= end) or \
|
|
(finished_date >= end
|
|
and finished_date >= start):
|
|
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
|
|
|
|
def recompute_lines(self):
|
|
for line in self.lines:
|
|
if not line.wage_type.concepts_salary:
|
|
continue
|
|
salary_args = self.get_salary_full(line.wage_type)
|
|
unit_value = line.wage_type.compute_unit_price(salary_args)
|
|
unit_value = self._validate_amount_wage(line.wage_type, unit_value)
|
|
line.write([line], {'unit_value': unit_value})
|
|
|
|
def _validate_amount_wage(self, wage, amount):
|
|
return amount
|
|
|
|
|
|
class PayrollLine(ModelSQL, ModelView):
|
|
"Payroll Line"
|
|
__name__ = "staff.payroll.line"
|
|
sequence = fields.Integer('Sequence')
|
|
payroll = fields.Many2One('staff.payroll', 'Payroll',
|
|
ondelete='CASCADE', select=True, required=True)
|
|
description = fields.Char('Description', required=True)
|
|
wage_type = fields.Many2One('staff.wage_type', 'Wage Type',
|
|
required=True, depends=['payroll', '_parent_payroll'])
|
|
uom = fields.Many2One('product.uom', 'Unit', depends=['wage_type'],
|
|
states={'readonly': Bool(Eval('wage_type'))})
|
|
quantity = fields.Numeric('Quantity', digits=(16, 2))
|
|
unit_value = fields.Numeric('Unit Value', digits=(16, 2),
|
|
depends=['wage_type'])
|
|
amount = fields.Function(fields.Numeric('Amount',
|
|
digits=(16, 2), depends=['unit_value', 'quantity'],
|
|
states={
|
|
'readonly': ~Eval('_parent_payroll'),
|
|
}), 'get_amount')
|
|
receipt = fields.Boolean('Print Receipt')
|
|
reconciled = fields.Function(fields.Boolean('Reconciled'),
|
|
'get_reconciled')
|
|
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',
|
|
'description', 'unit_value', 'payroll', 'receipt', 'sequence',
|
|
'_parent_payroll.employee', '_parent_payroll.contract',
|
|
'_parent_payroll.end', '_parent_payroll.lines', '_parent_payroll.start',
|
|
'_parent_payroll.period', '_parent_payroll.kind')
|
|
def on_change_wage_type(self):
|
|
if not self.wage_type:
|
|
return
|
|
self.uom = self.wage_type.uom.id
|
|
self.description = self.wage_type.name
|
|
self.quantity = self.wage_type.default_quantity
|
|
self.receipt = self.wage_type.receipt
|
|
self.sequence = self.wage_type.sequence
|
|
parties = []
|
|
for wage in self.payroll.employee.mandatory_wages:
|
|
if wage.wage_type.id == self.wage_type.id and wage.party:
|
|
parties.append(wage.party.id)
|
|
if parties:
|
|
self.party = parties[0]
|
|
|
|
if self.wage_type.unit_price_formula:
|
|
salary_args = self.payroll.get_salary_full(self.wage_type)
|
|
self.unit_value = self.wage_type.compute_unit_price(
|
|
salary_args)
|
|
|
|
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
|
|
|
|
def get_move_line(self, account, party_id, amount):
|
|
debit = credit = _ZERO
|
|
if amount[0] == 'debit':
|
|
debit = amount[1]
|
|
else:
|
|
credit = amount[1]
|
|
|
|
res = {
|
|
'description': account.name,
|
|
'debit': debit,
|
|
'credit': credit,
|
|
'account': account.id,
|
|
'party': party_id,
|
|
}
|
|
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
|
|
|
|
def get_expense_amount(self):
|
|
expense = 0
|
|
if self.wage_type.expense_formula:
|
|
salary_args = self.payroll.get_salary_full(self.wage_type)
|
|
expense = self.wage_type.compute_expense(salary_args)
|
|
return expense
|
|
|
|
|
|
class PayrollReport(CompanyReport):
|
|
__name__ = 'staff.payroll'
|
|
|
|
@classmethod
|
|
def get_context(cls, records, header, data):
|
|
report_context = super().get_context(records, header, data)
|
|
return report_context
|
|
|
|
|
|
class PayrollGroupStart(ModelView):
|
|
'Payroll Group Start'
|
|
__name__ = 'staff.payroll_group.start'
|
|
period = fields.Many2One('staff.payroll.period', 'Period',
|
|
required=True, domain=[('state', '=', 'open')])
|
|
description = fields.Char('Description', required=True)
|
|
company = fields.Many2One('company.company', 'Company',
|
|
required=True)
|
|
wage_types = fields.Many2Many('staff.wage_type', None, None,
|
|
'Wage Types')
|
|
|
|
@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'
|
|
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),
|
|
])
|
|
open_ = StateTransition()
|
|
|
|
def transition_open_(self):
|
|
pool = Pool()
|
|
Employee = pool.get('company.employee')
|
|
Payroll = pool.get('staff.payroll')
|
|
|
|
#Remove employees with payroll this period
|
|
payrolls_period = Payroll.search([
|
|
('period', '=', self.start.period.id),
|
|
# ('department', '=', self.start.department.id),
|
|
])
|
|
|
|
# contracts_w_payroll = [p.contract.id for p in payrolls_period]
|
|
employee_w_payroll = [p.employee.id for p in payrolls_period]
|
|
dom_employees = self.get_employees_dom(employee_w_payroll)
|
|
payroll_to_create = []
|
|
employees = Employee.search(dom_employees, limit=400)
|
|
get_values = self.get_values
|
|
search_contract_on_period = Payroll.search_contract_on_period
|
|
period = self.start.period
|
|
start = period.start
|
|
end = period.end
|
|
for employee in employees:
|
|
if employee.id in employee_w_payroll:
|
|
continue
|
|
|
|
contract = search_contract_on_period(employee, period)
|
|
if not contract:
|
|
continue
|
|
|
|
values = get_values(contract, start, end)
|
|
payroll_to_create.append(values)
|
|
|
|
wages = [
|
|
(wage_type, None, None) for wage_type in self.start.wage_types
|
|
]
|
|
PayrollCreate = Payroll.create
|
|
if payroll_to_create:
|
|
payrolls = PayrollCreate(payroll_to_create)
|
|
len_payrolls = len(payrolls)
|
|
cont = 0
|
|
for payroll in payrolls:
|
|
print('contador > ', cont, ' / ', len_payrolls)
|
|
try:
|
|
cont += 1
|
|
# REVISAR: Esta linea es basicamente innecesaria y costosa en recursos
|
|
# payroll.on_change_period()
|
|
|
|
payroll.set_preliquidation({}, None)
|
|
if wages:
|
|
payroll._create_payroll_lines(wages, None, {})
|
|
except:
|
|
print('Fallo al crear nomina : ', payroll.employee.party.name)
|
|
print('-' * 100)
|
|
print('-------------- FINALIZADO -------------')
|
|
print('-' * 100)
|
|
return 'end'
|
|
|
|
def get_employees_dom(self, employees_w_payroll):
|
|
dom_employees = [
|
|
('active', '=', True),
|
|
('id', 'not in', employees_w_payroll),
|
|
]
|
|
return dom_employees
|
|
|
|
def get_values(self, contract, start_date, end_date):
|
|
employee = contract.employee
|
|
ct_start_date = contract.start_date
|
|
ct_end_date = contract.finished_date
|
|
if ct_start_date and ct_start_date >= start_date and \
|
|
ct_start_date <= end_date:
|
|
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
|
|
values = {
|
|
'employee': employee.id,
|
|
'period': self.start.period.id,
|
|
'start': start_date,
|
|
'end': end_date,
|
|
'description': self.start.description,
|
|
'date_effective': end_date,
|
|
'contract': contract.id,
|
|
'department': employee.department.id if employee.department else None,
|
|
}
|
|
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')
|
|
ids = Transaction().context['active_ids']
|
|
for payroll in Payroll.browse(ids):
|
|
if payroll.state != 'draft':
|
|
return
|
|
if not payroll.lines:
|
|
payroll.set_preliquidation({}, None)
|
|
else:
|
|
payroll.update_preliquidation({})
|
|
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')
|
|
ids = Transaction().context['active_ids']
|
|
for payroll in Payroll.browse(ids):
|
|
if payroll.state != 'draft' or not payroll.lines:
|
|
continue
|
|
payroll.recompute_lines()
|
|
return 'end'
|
|
|
|
|
|
class Move(metaclass=PoolMeta):
|
|
__name__ = 'account.move'
|
|
|
|
@classmethod
|
|
def _get_origin(cls):
|
|
return super(Move, cls)._get_origin() + ['staff.payroll']
|