payroll optimization test

This commit is contained in:
Wilson Gomez 2023-04-29 11:14:49 -05:00
parent 60f93248fb
commit c6b3e7b3ef
2 changed files with 148 additions and 77 deletions

View File

@ -1,8 +1,10 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of # This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms. # this repository contains the full copyright notices and license terms.
import datetime import datetime
import time
from decimal import Decimal from decimal import Decimal
from operator import attrgetter from operator import attrgetter
from functools import lru_cache
from trytond.model import Workflow, ModelView, ModelSQL, fields from trytond.model import Workflow, ModelView, ModelSQL, fields
from trytond.pyson import Bool, Eval, If, Id from trytond.pyson import Bool, Eval, If, Id
from trytond.pool import Pool, PoolMeta from trytond.pool import Pool, PoolMeta
@ -300,13 +302,13 @@ class Payroll(Workflow, ModelSQL, ModelView):
return {'salary': salary_full} return {'salary': salary_full}
def compute_salary_full(self, wage): def compute_salary_full(self, wage):
wages_ids = [s.id for s in wage.concepts_salary] if wage['salary_constitute'] or not wage['concepts_salary']:
if wages_ids: return self.contract.get_salary_in_date(self.end) or 0
salary_full = sum(
line.amount for line in self.lines if line.wage_type.id in wages_ids) # wages_ids = [s.id for s in 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
return salary_full return salary_full
def create_move(self): def create_move(self):
@ -451,7 +453,6 @@ class Payroll(Workflow, ModelSQL, ModelView):
if credit_acc and not line_credit_ready: if credit_acc and not line_credit_ready:
lines_moves[credit_acc.id][party_id]['credit'] += amount_credit lines_moves[credit_acc.id][party_id]['credit'] += amount_credit
except Exception as e: except Exception as e:
print(wage_type.name, 'this is wage_type', e)
raise WageTypeValidationError( raise WageTypeValidationError(
gettext('staff_payroll.bad_configuration_of_wage_type', wage=wage_type.name)) gettext('staff_payroll.bad_configuration_of_wage_type', wage=wage_type.name))
result = [] result = []
@ -468,119 +469,150 @@ class Payroll(Workflow, ModelSQL, ModelView):
result.extend(_line) result.extend(_line)
return result return result
def _create_payroll_lines(self, wages, extras, discounts=None): def _create_payroll_lines(self, config, wages, extras, discounts=None, cache_wage_dict=None):
PayrollLine = Pool().get('staff.payroll.line') PayrollLine = Pool().get('staff.payroll.line')
Wage = Pool().get('staff.wage_type')
values = [] values = []
salary_args = {} salary_args = {}
salary_in_date = self.contract.get_salary_in_date(self.end) # salary_in_date = self.contract.get_salary_in_date(self.end)
get_line = self.get_line get_line = self.get_line
get_line_quantity = self.get_line_quantity get_line_quantity = self.get_line_quantity
get_line_quantity_special = self.get_line_quantity_special get_line_quantity_special = self.get_line_quantity_special
get_salary_full = self.get_salary_full get_salary_full = self.get_salary_full
values_append = values.append values_append = values.append
compute_unit_price = Wage.compute_unit_price
for wage, party, fix_amount in wages: for wage, party, fix_amount in wages:
if not fix_amount: if not fix_amount:
time_salary = time.time()
salary_args = get_salary_full(wage) salary_args = get_salary_full(wage)
if wage.salary_constitute: time_salary2 = time.time()
salary_args['salary'] = salary_in_date print(time_salary2-time_salary, 'time salary')
# Este metodo instancia podria pasarse a un metodo de clase # Este metodo instancia podria pasarse a un metodo de clase
unit_value = wage.compute_unit_price(salary_args) unit_value = compute_unit_price(wage['unit_price_formula'], salary_args)
else: else:
unit_value = fix_amount unit_value = fix_amount
discount = None discount = None
if discounts and discounts.get(wage.id): if discounts and discounts.get(wage['id']):
discount = discounts.get(wage.id) discount = discounts.get(wage['id'])
qty = get_line_quantity_special(wage) if wage['type_concept'] == 'especial':
if qty == 0: qty = get_line_quantity_special(wage)
else:
time_qty = time.time()
qty = get_line_quantity( qty = get_line_quantity(
wage, self.start, self.end, extras, discount config, wage, self.start, self.end, extras, discount
) )
time_qty2 = time.time()
print(time_qty2 - time_qty, 'time qty')
time_line = time.time()
line_ = get_line(wage, qty, unit_value, party) line_ = get_line(wage, qty, unit_value, party)
time_line2 = time.time()
print(time_line2-time_line, 'time line')
values_append(line_) values_append(line_)
PayrollLine.create(values) PayrollLine.create(values)
def set_preliquidation(self, extras, discounts=None): @classmethod
def create_cache_wage_types(cls):
Wage = Pool().get('staff.wage_type')
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',
'minimal_amount', 'adjust_days_worked', 'round_amounts'
]
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):
wage_salary = [] wage_salary = []
wage_no_salary = [] wage_no_salary = []
wage_salary_append = wage_salary.append
wage_no_salary_append = wage_no_salary.append
# if not cache_wage_dict:
# cache_wage_dict = self.create_cache_wage_types()
attr_mandatory = attrgetter('wage_type', 'party', 'fix_amount')
for concept in self.employee.mandatory_wages: for concept in self.employee.mandatory_wages:
wage_type = concept.wage_type wage_type, party, fix_amount = attr_mandatory(concept)
if concept.wage_type.salary_constitute: if wage_type.salary_constitute:
wage_salary.append( wage_salary.append(
(wage_type, concept.party, concept.fix_amount)) (cache_wage_dict[wage_type.id], party, fix_amount))
else: else:
wage_no_salary.append( wage_no_salary.append(
(wage_type, concept.party, concept.fix_amount)) (cache_wage_dict[wage_type.id], party, fix_amount))
self._create_payroll_lines(wage_salary, extras, discounts) self._create_payroll_lines(config, wage_salary, extras, discounts, cache_wage_dict)
self._create_payroll_lines(wage_no_salary, extras, discounts) self._create_payroll_lines(config, wage_no_salary, extras, discounts, cache_wage_dict)
def update_preliquidation(self, extras): 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
for line in self.lines: for line in self.lines:
if line.wage_type.salary_constitute: wage_id = line.wage_type.id
wage = cache_wage_dict[wage_id]
if wage['salary_constitute']:
continue continue
salary_args = self.get_salary_full(line.wage_type) salary_args = get_salary_full(wage)
unit_value = line.wage_type.compute_unit_price(salary_args) unit_value = compute_unit_price(wage['unit_price_formula'], salary_args)
unit_value = self._validate_amount_wage(line.wage_type, unit_value)
line.write([line], { line.write([line], {
'unit_value': unit_value, 'unit_value': unit_value,
}) })
def get_line(self, wage, qty, unit_value, party=None): def get_line(self, wage, qty, unit_value, party=None):
res = { res = {
'sequence': wage.sequence, 'sequence': wage['sequence'],
'payroll': self.id, 'payroll': self.id,
'wage_type': wage.id, 'wage_type': wage['id'],
'description': wage.name, 'description': wage['name'],
'quantity': Decimal(str(round(qty, 2))), 'quantity': Decimal(str(round(qty, 2))),
'unit_value': Decimal(str(round(unit_value, 2))), 'unit_value': Decimal(str(round(unit_value, 2))),
'uom': wage.uom, 'uom': wage['uom'],
'receipt': wage.receipt, 'receipt': wage['receipt'],
} }
if party: if party:
res['party'] = party.id res['party'] = party.id
return res return res
def _get_line_quantity(self, quantity_days, wage, extras, discount): def _get_line_quantity(self, config, quantity_days, wage, extras, discount):
Configuration = Pool().get('staff.configuration') # Configuration = Pool().get('staff.configuration')
config = Configuration(1) # config = Configuration(1)
default_hour_workday = config.default_hour_workday or _DEFAULT_WORK_DAY default_hour_workday = config.default_hour_workday or _DEFAULT_WORK_DAY
quantity = wage.default_quantity or 0 quantity = wage['default_quantity'] or 0
uom_id = wage['uom']
wage_name = wage['name'].lower()
if quantity_days < 0: if quantity_days < 0:
quantity_days = 0 quantity_days = 0
if wage.uom.id == Id('product', 'uom_day').pyson(): if uom_id == Id('product', 'uom_day').pyson():
quantity = quantity_days quantity = quantity_days
if discount: if discount:
quantity -= discount quantity -= discount
elif wage.uom.id == Id('product', 'uom_hour').pyson(): elif uom_id == Id('product', 'uom_hour').pyson():
if wage.type_concept != 'extras': if wage['type_concept'] != 'extras':
quantity = quantity_days * default_hour_workday quantity = quantity_days * default_hour_workday
if discount: if discount:
quantity -= discount quantity -= discount
else: else:
key_ = [key for key in extras.keys( key_ = [key for key in extras.keys(
) if wage.name.lower().count(key) > 0] ) if wage_name.count(key) > 0]
if key_: if key_:
key_ext = key_[0] key_ext = key_[0]
extras_ = extras.get(key_ext) extras_ = extras.get(key_ext)
else: else:
extras_ = extras.get((wage.name.lower())) extras_ = extras.get((wage_name))
if extras_ and self.employee.position and self.employee.position.extras: quantity = extras_
quantity = extras_
return quantity return quantity
def get_line_quantity(self, wage, start=None, end=None, extras=None, discount=None): def get_line_quantity(self, config, wage, start=None, end=None, extras=None, discount=None):
quantity = wage.default_quantity or 0
quantity_days = self.get_days(start, end) quantity_days = self.get_days(start, end)
quantity = self._get_line_quantity( quantity = self._get_line_quantity(
quantity_days, wage, extras, discount) config, quantity_days, wage, extras, discount)
return quantity return quantity
def get_line_quantity_special(self, wage): def get_line_quantity_special(self, wage):
quantity_days = 0 quantity_days = 0
if self.contract and self.date_effective and wage.type_concept == 'special': if self.contract and self.date_effective:
quantity_days = (self.date_effective quantity_days = (self.date_effective
- self.contract.start_date).days - self.contract.start_date).days
if quantity_days > wage.limit_days: if quantity_days > wage.limit_days:
@ -675,13 +707,16 @@ class Payroll(Workflow, ModelSQL, ModelView):
quantity_days = 0 quantity_days = 0
return quantity_days return quantity_days
def recompute_lines(self): 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
for line in self.lines: for line in self.lines:
if not line.wage_type.concepts_salary: wage = cache_wage_dict[line.wage_type.id]
if not wage['concepts_salary']:
continue continue
salary_args = self.get_salary_full(line.wage_type) salary_args = get_salary_full(wage)
unit_value = line.wage_type.compute_unit_price(salary_args) unit_value = compute_unit_price(wage['unit_price_formula'], salary_args)
unit_value = self._validate_amount_wage(line.wage_type, unit_value)
line.write([line], {'unit_value': unit_value}) line.write([line], {'unit_value': unit_value})
def _validate_amount_wage(self, wage, amount): def _validate_amount_wage(self, wage, amount):
@ -744,7 +779,7 @@ class PayrollLine(ModelSQL, ModelView):
if self.wage_type.unit_price_formula: if self.wage_type.unit_price_formula:
salary_args = self.payroll.get_salary_full(self.wage_type) salary_args = self.payroll.get_salary_full(self.wage_type)
self.unit_value = self.wage_type.compute_unit_price( self.unit_value = self.wage_type.compute_unit_price(
salary_args) self.wage_type.unit_price_formula, salary_args)
def get_amount(self, name): def get_amount(self, name):
return self.on_change_with_amount() return self.on_change_with_amount()
@ -791,9 +826,10 @@ class PayrollLine(ModelSQL, ModelView):
def get_expense_amount(self): def get_expense_amount(self):
expense = 0 expense = 0
if self.wage_type.expense_formula: wage_type = self.wage_type
salary_args = self.payroll.get_salary_full(self.wage_type) if wage_type.expense_formula:
expense = self.wage_type.compute_expense(salary_args) salary_args = self.payroll.get_salary_full(wage_type)
expense = wage_type.compute_expense(salary_args, wage_type.expense_formula)
return expense return expense
@ -841,14 +877,18 @@ class PayrollGroup(Wizard):
open_ = StateTransition() open_ = StateTransition()
def transition_open_(self): def transition_open_(self):
time_initial = time.time()
pool = Pool() pool = Pool()
Employee = pool.get('company.employee') Employee = pool.get('company.employee')
Payroll = pool.get('staff.payroll') Payroll = pool.get('staff.payroll')
Configuration = Pool().get('staff.configuration')
config = Configuration(1)
#Remove employees with payroll this period #Remove employees with payroll this period
payrolls_period = Payroll.search([ payrolls_period = Payroll.search([
('period', '=', self.start.period.id), ('period', '=', self.start.period.id),
# ('department', '=', self.start.department.id), # ('department', '=', self.start.department.id),
]) ])
cache_wage_dict = Payroll.create_cache_wage_types()
# contracts_w_payroll = [p.contract.id for p in payrolls_period] # contracts_w_payroll = [p.contract.id for p in payrolls_period]
employee_w_payroll = [p.employee.id for p in payrolls_period] employee_w_payroll = [p.employee.id for p in payrolls_period]
@ -881,16 +921,18 @@ class PayrollGroup(Wizard):
cont = 0 cont = 0
for payroll in payrolls: for payroll in payrolls:
print('contador > ', cont, ' / ', len_payrolls) print('contador > ', cont, ' / ', len_payrolls)
try: # try:
cont += 1 cont += 1
# REVISAR: Esta linea es basicamente innecesaria y costosa en recursos time_pre = time.time()
# payroll.on_change_period() payroll.set_preliquidation(config, {}, None, cache_wage_dict)
time_pre2 = time.time()
payroll.set_preliquidation({}, None) print(time_pre2-time_pre, 'time preliquidation')
if wages: if wages:
payroll._create_payroll_lines(wages, None, {}) payroll._create_payroll_lines(config, wages, None, {}, cache_wage_dict)
except: # except Exception as e:
print('Fallo al crear nomina : ', payroll.employee.party.name) # print('Fallo al crear nomina : ', payroll.employee.party.name, e)
time_final = time.time()
print(time_final - time_initial, 'final create payroll')
return 'end' return 'end'
def get_employees_dom(self, employees_w_payroll): def get_employees_dom(self, employees_w_payroll):
@ -931,14 +973,20 @@ class PayrollPreliquidation(Wizard):
def transition_create_preliquidation(self): def transition_create_preliquidation(self):
Payroll = Pool().get('staff.payroll') Payroll = Pool().get('staff.payroll')
Configuration = Pool().get('staff.configuration')
config = Configuration(1)
ids = Transaction().context['active_ids'] ids = Transaction().context['active_ids']
cache_wage_dict = Payroll.create_cache_wage_types()
for payroll in Payroll.browse(ids): for payroll in Payroll.browse(ids):
if payroll.state != 'draft': if payroll.state != 'draft':
return return
if not payroll.lines: if not payroll.lines:
payroll.set_preliquidation({}, None) time_1 = time.time()
payroll.set_preliquidation(config, {}, None, cache_wage_dict)
time_2 = time.time()
print(time_2-time_1, 'time preliquidation')
else: else:
payroll.update_preliquidation({}) payroll.update_preliquidation({}, cache_wage_dict)
return 'end' return 'end'

View File

@ -129,13 +129,15 @@ class WageType(ModelSQL, ModelView):
def default_active(): def default_active():
return True return True
def compute_unit_price(self, args): @classmethod
return self.compute_formula('unit_price_formula', args) def compute_unit_price(cls, formula, args):
return cls.compute_formula(formula, args)
def compute_expense(self, args): @classmethod
return self.compute_formula('expense_formula', args) def compute_expense(cls, formula, args):
return cls.compute_formula(formula, args)
def compute_formula(self, formula, args=None): def compute_formula2(self, formula, args=None):
''' '''
Compute a formula field with a salary value as float Compute a formula field with a salary value as float
:return: A decimal :return: A decimal
@ -155,6 +157,27 @@ class WageType(ModelSQL, ModelView):
raise WageTypeValidationError( raise WageTypeValidationError(
gettext('staff_payroll.msg_invalid_formula', formula=formula)) gettext('staff_payroll.msg_invalid_formula', formula=formula))
@classmethod
def compute_formula(cls, formula, args=None):
'''
Compute a formula field with a salary value as float
:return: A decimal
'''
# Configuration = Pool().get('staff.configuration')
# configuration = Configuration(1)
# minimum_salary = float(configuration.minimum_salary or 0)
# formula = getattr(self, formula)
if not formula:
return Decimal('0.0')
if args.get('salary') != None:
salary = float(args['salary'])
try:
value = Decimal(str(round(eval(formula), 2)))
return value
except Exception:
raise WageTypeValidationError(
gettext('staff_payroll.msg_invalid_formula', formula=formula))
class WageTypeSalary(ModelSQL): class WageTypeSalary(ModelSQL):
"Wage Type Salary" "Wage Type Salary"