payroll optimization test
This commit is contained in:
parent
60f93248fb
commit
c6b3e7b3ef
192
payroll.py
192
payroll.py
|
@ -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'
|
||||||
|
|
||||||
|
|
||||||
|
|
33
wage_type.py
33
wage_type.py
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue