# 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 import time from decimal import Decimal from operator import attrgetter from functools import lru_cache 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 = 7.8336 _ZERO = Decimal('0.0') def get_dom_contract_period(start, end): dom = [ ("state", "!=", "canceled"), [ "OR", [ ("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), ], ], ] return dom 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')), ], ondelete='RESTRICT') 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').in_(['draft', 'posted']), }, 'post': { 'invisible': Eval('state') != 'processed', }, 'cancel': { 'invisible': Eval('state') != 'draft', }, 'process': { 'invisible': Eval('state') != 'draft', },'force_draft': { 'invisible': Eval('state') != 'posted', }, }) @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): for payroll in records: payrolls = cls.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): wage_dict = cls.create_cache_wage_types() for payroll in records: payroll.create_move(wage_dict) @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): 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 return salary_full def create_move(self, wage_dict): 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(wage_dict) 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, wage_dict): 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) tax = getattr(configuration, 'tax_withholding') entity_in_line = configuration.expense_contribution_entity debit_acc2 = None attr_getter = attrgetter( 'amount', 'party', 'amount_60_40', 'wage_type.id', 'tax_base' ) for line in self.lines: amount, party, amount_60_40, wage_type, tax_base = 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'] if amount <= 0 or not wage_type: continue try: party_id = party.id except Exception: party_id = None if not party_id: if mandatory_wages.get(wage_type): party_id = mandatory_wages[wage_type].id else: party_id = employee_id expense = Decimal(0) # if not line.wage_type: # continue if expense_for: expense = line.get_expense_amount(wage_type_) 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 else: if expense: amount_debit = expense elif debit_acc: amount_debit = amount amount_credit = amount + expense # debit_acc = line.wage_type.debit_account if True: if debit_acc and amount_debit > _ZERO: if 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} ) if debit_acc2: if debit_acc2['id'] not in lines_moves.keys(): lines_moves[debit_acc2['id']] = { employee_id: line.get_move_line( debit_acc2, party_id, ('debit', amount_debit2))} else: line.update_move_line( lines_moves[debit_acc2['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 tax_base and tax: tax_line = { 'amount': tax_base, 'tax': tax.id, 'type': 'base', } lines_moves[credit_acc['id']][party_id]['tax_lines'] = [('create', [tax_line])] if definition != 'payment': deduction_acc = 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', -amount), )} line_credit_ready = True else: lines_moves[deduction_acc['id']][employee_id]['credit'] -= amount if credit_acc and not line_credit_ready: 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'])) result = [] for r in lines_moves.values(): _line = list(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, config, wages, extras, discounts=None, cache_wage_dict=None): PayrollLine = Pool().get('staff.payroll.line') Wage = Pool().get('staff.wage_type') 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 compute_unit_price = Wage.compute_unit_price for wage, party, fix_amount in wages: if not fix_amount: time_salary = time.time() salary_args = get_salary_full(wage) time_salary2 = time.time() # print(time_salary2-time_salary, 'time salary') # Este metodo instancia podria pasarse a un metodo de clase unit_value = compute_unit_price(wage['unit_price_formula'], salary_args) else: unit_value = fix_amount discount = None if discounts and discounts.get(wage['id']): discount = Decimal(discounts.get(wage['id'])) if wage['type_concept'] == 'especial': qty = get_line_quantity_special(wage) else: time_qty = time.time() qty = get_line_quantity( config, wage, self.start, self.end, extras, discount ) time_qty2 = time.time() # print(time_qty2 - time_qty, 'time qty') time_line = time.time() # print(wage, self.start, self.end, party, qty) line_ = get_line(wage, qty, unit_value, party) time_line2 = time.time() # print(time_line2-time_line, 'time line') values_append(line_) PayrollLine.create(values) @classmethod def get_fields_names_wage_types(cls): 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', 'debit_account.name', 'credit_account.name', 'deduction_account.name', 'account_60_40.name' ] return fields_names @classmethod def create_cache_wage_types(cls): Wage = Pool().get('staff.wage_type') fields_names = cls.get_fields_names_wage_types() 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_no_salary = [] wage_salary_append = wage_salary.append wage_no_salary_append = wage_no_salary.append attr_mandatory = attrgetter('wage_type', 'party', 'fix_amount') for concept in self.employee.mandatory_wages: wage_type, party, fix_amount = attr_mandatory(concept) if wage_type.salary_constitute: wage_salary_append( (cache_wage_dict[wage_type.id], party, fix_amount)) else: wage_no_salary_append( (cache_wage_dict[wage_type.id], party, fix_amount)) 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) 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: wage_id = line.wage_type.id wage = cache_wage_dict[wage_id] if wage['salary_constitute']: continue salary_args = get_salary_full(wage) unit_value = compute_unit_price(wage['unit_price_formula'], salary_args) 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, config, quantity_days, wage, extras, discount): default_hour_workday = config.default_hour_workday or _DEFAULT_WORK_DAY quantity = wage['default_quantity'] or 0 uom_id = wage['uom'] wage_name = wage['name'].lower() if quantity_days < 0: quantity_days = 0 if uom_id == Id('product', 'uom_day').pyson(): quantity = quantity_days if discount: quantity -= discount elif uom_id == Id('product', 'uom_hour').pyson(): if wage['type_concept'] != 'extras': quantity = quantity_days * default_hour_workday if discount: quantity -= discount else: extra_key = wage_name.split(' ')[0] quantity = extras.get(extra_key, 0) return quantity def get_line_quantity(self, config, wage, start=None, end=None, extras=None, discount=None): quantity_days = self.get_days(start, end) quantity = self._get_line_quantity( config, quantity_days, wage, extras, discount) return quantity def get_line_quantity_special(self, wage): quantity_days = 0 if self.contract and self.date_effective: 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' and line.wage_type.type_concept != 'unpaid_leave': 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.id, self.start, self.end ) if not self.description: self.description = self.period.description @classmethod def search_contract_on_period(cls, employee_id, period_start, period_end): Contract = Pool().get('staff.contract') dom_contract = get_dom_contract_period(period_start, period_end) contracts = Contract.search([ ('employee', '=', employee_id), dom_contract ]) 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, 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: wage = cache_wage_dict[line.wage_type.id] if not wage['concepts_salary']: continue salary_args = get_salary_full(wage) unit_value = compute_unit_price(wage['unit_price_formula'], salary_args) 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', '_parent_payroll.last_payroll') def on_change_wage_type(self): if not self.wage_type: return 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'] parties = [] for wage in self.payroll.employee.mandatory_wages: if wage.wage_type.id == wage_id and wage.party: parties.append(wage.party.id) if parties: self.party = parties[0] if wage_type['unit_price_formula']: salary_args = self.payroll.get_salary_full(wage_type) self.unit_value = self.wage_type.compute_unit_price( wage_type['unit_price_formula'], 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, wage_type): expense = 0 wage_type_ = self.wage_type if wage_type['expense_formula']: salary_args = self.payroll.get_salary_full(wage_type) expense = wage_type_.compute_expense(wage_type['expense_formula'], 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): time_initial = time.time() pool = Pool() Employee = pool.get('company.employee') Payroll = pool.get('staff.payroll') Configuration = Pool().get('staff.configuration') config = Configuration(1) #Remove employees with payroll this period payrolls_period = Payroll.search([ ('period', '=', self.start.period.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] 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.id, start, end) if not contract: continue values = get_values(contract, start, end) payroll_to_create.append(values) wages = [ (cache_wage_dict[wage_type.id], 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 time_pre = time.time() payroll.set_preliquidation(config, {}, None, cache_wage_dict) time_pre2 = time.time() # print(time_pre2-time_pre, 'time preliquidation') 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() # print(time_final - time_initial, 'final create payroll') 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') Configuration = Pool().get('staff.configuration') config = Configuration(1) ids = Transaction().context['active_ids'] cache_wage_dict = Payroll.create_cache_wage_types() for payroll in Payroll.browse(ids): if payroll.state != 'draft': return if not payroll.lines: time_1 = time.time() payroll.set_preliquidation(config, {}, None, cache_wage_dict) time_2 = time.time() # print(time_2-time_1, 'time preliquidation') else: payroll.update_preliquidation({}, cache_wage_dict) 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') wages_dict = Payroll.create_cache_wage_types() ids = Transaction().context['active_ids'] for payroll in Payroll.browse(ids): if payroll.state != 'draft' or not payroll.lines: continue payroll.recompute_lines(wages_dict) return 'end' class Move(metaclass=PoolMeta): __name__ = 'account.move' @classmethod def _get_origin(cls): return super(Move, cls)._get_origin() + ['staff.payroll']