# This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. from decimal import Decimal from datetime import timedelta, date from trytond import backend from trytond.model import Workflow, ModelSQL, ModelView, fields from trytond.pool import Pool from trytond.report import Report from trytond.pyson import Eval, If, Bool from trytond.wizard import Wizard, StateView, Button, StateTransition, StateReport from trytond.transaction import Transaction from trytond.i18n import gettext from .exceptions import (LiquidationEmployeeError, MissingSecuenceLiquidation, LiquidationMoveError, WageTypeConceptError) STATES = {'readonly': (Eval('state') != 'draft')} _ZERO = Decimal('0.0') BONUS_SERVICE = ['bonus_service'] CONTRACT = [ 'bonus_service', 'health', 'retirement', 'unemployment', 'interest', 'holidays' ] class Liquidation(Workflow, ModelSQL, ModelView): 'Staff Liquidation' __name__ = 'staff.liquidation' number = fields.Char('Number', readonly=True, help="Secuence", select=True) employee = fields.Many2One('company.employee', 'Employee', states=STATES, required=True, depends=['state']) start_period = fields.Many2One('staff.payroll.period', 'Start Period', required=True, states=STATES) end_period = fields.Many2One('staff.payroll.period', 'End Period', required=True, states=STATES, depends=['start_period']) kind = fields.Selection([ ('contract', 'Contract'), ('bonus_service', 'Bonus Service'), ('interest', 'Interest'), ('unemployment', 'Unemployment'), ('vacation', 'Vacation'), ], 'Kind', required=True, states=STATES) liquidation_date = fields.Date('Liquidation Date', states=STATES, required=True) lines = fields.One2Many('staff.liquidation.line', 'liquidation', 'Lines', states=STATES, depends=['employee', 'state']) gross_payments = fields.Function(fields.Numeric( 'Gross Payments', states=STATES, digits=(16, 2)), 'get_sum_operation') total_deductions = fields.Function(fields.Numeric( 'Total Deductions', states=STATES, digits=(16, 2)), 'get_sum_operation') net_payment = fields.Function(fields.Numeric( 'Net Payment', states=STATES, digits=(16, 2)), 'get_net_payment') time_contracting = fields.Integer('Time Contracting', states=STATES, depends=['start_period', 'end_period', 'employee']) state = fields.Selection([ ('draft', 'Draft'), ('confirmed', 'Confirmed'), ('posted', 'Posted'), ('cancel', 'Cancel'), ], 'State', readonly=True) company = fields.Many2One('company.company', 'Company', required=True, states={ 'readonly': (Eval('state') != 'draft') | Eval('lines', [0]), }, domain=[ ('id', If(Eval('context', {}).contains('company'), '=', '!='), Eval('context', {}).get('company', 0)), ], depends=['state'], select=True) description = fields.Char('Description', states=STATES, select=True) cause = fields.Char('Cause', states=STATES) permissons = fields.Char('Permissons', states=STATES) journal = fields.Many2One('account.journal', 'Journal', required=True, states=STATES) currency = fields.Many2One('currency.currency', 'Currency', required=True, states={ 'readonly': ((Eval('state') != 'draft') | (Eval('lines', [0]) & Eval('currency'))), }, depends=['state']) move = fields.Many2One('account.move', 'Move', readonly=True) contract = fields.Many2One('staff.contract', 'Contract', states=STATES, domain=[('employee', '=', Eval('employee'))]) account = fields.Many2One('account.account', 'Account', required=True, domain=[ ('type', '!=', None), ]) payrolls = fields.Function(fields.Many2Many('staff.payroll', None, None, 'Payroll', depends=['start_period', 'end_period'], domain=[ ('employee', '=', Eval('employee')), ('kind', '=', 'normal'), ],), 'get_payrolls') start = fields.Function(fields.Date('Start Date'), 'get_dates') end = fields.Function(fields.Date('End Date'), 'get_dates') party_to_pay = fields.Many2One('party.party', 'Party to Pay', states=STATES) last_salary = fields.Numeric('Last Salary', states=STATES, digits=(16, 2)) @classmethod def __setup__(cls): super(Liquidation, cls).__setup__() cls._transitions |= set(( ('draft', 'cancel'), ('cancel', 'draft'), ('confirmed', 'draft'), ('confirmed', 'posted'), ('draft', 'confirmed'), ('posted', 'draft'), )) cls._buttons.update({ 'draft': { 'invisible': Eval('state') == 'draft', }, 'confirm': { 'invisible': Eval('state') != 'draft', }, 'cancel': { 'invisible': Eval('state') != 'draft', }, 'post': { 'invisible': Eval('state') != 'confirmed', }, 'compute_liquidation': { 'invisible': Bool(Eval('lines')), }, }) @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_kind(): return 'contract' @staticmethod def default_state(): return 'draft' @staticmethod def default_currency(): Company = Pool().get('company.company') company = Transaction().context.get('company') if company: company = Company(company) return company.currency.id @staticmethod def default_journal(): Configuration = Pool().get('staff.configuration') configuration = Configuration(1) if configuration.default_journal: return configuration.default_journal.id @classmethod def search_rec_name(cls, name, clause): if clause[1].startswith('!') or clause[1].startswith('not '): bool_op = 'AND' else: bool_op = 'OR' return [ bool_op, ('employee',) + tuple(clause[1:]), ('number',) + tuple(clause[1:]), ] @classmethod @ModelView.button @Workflow.transition('confirmed') def confirm(cls, records): for rec in records: if not rec.contract: raise LiquidationEmployeeError(gettext('staff_payroll_co.msg_dont_contract')) rec.set_number() @classmethod @ModelView.button @Workflow.transition('draft') def draft(cls, records): pass @classmethod @ModelView.button @Workflow.transition('cancel') def cancel(cls, records): pass @classmethod @ModelView.button @Workflow.transition('posted') def post(cls, records): for rec in records: rec.create_move() @classmethod def create_withholding(cls, liquidation): rec = liquidation pool = Pool() UvtWithholding = pool.get('staff.payroll.uvt_withholding') WageType = pool.get('staff.wage_type') wage_tax = WageType.search([('type_concept', '=', 'tax')]) if not wage_tax: return wage_tax = wage_tax[0] deductions_month = sum([ l.amount for l in rec.lines if l.wage.definition != 'payment' ]) salary_full = rec.net_payment payrolls = {p.end: p for p in rec.payrolls} if not payrolls: return max_date = max(payrolls.keys()) if rec.liquidation_date.month == max_date.month: payroll = payrolls[max_date] line_tax = None for line in payroll.lines: if line.wage_type.type_concept == 'tax' and line.amount: line_tax = line if not line_tax: deductions_month += payroll.get_deductions_month() salary_args = payroll.get_salary_full(wage_tax) salary_full += salary_args['salary'] base_salary_withholding = salary_full - deductions_month amount_tax = UvtWithholding.compute_withholding( base_salary_withholding) amount_tax = rec.currency.round(Decimal(amount_tax)) if amount_tax: create_tax = { 'sequence': wage_tax.sequence, 'wage': wage_tax.id, 'description': wage_tax.name, 'amount': amount_tax * -1, 'days': rec.time_contracting, 'account': wage_tax.credit_account, } cls.write([rec], { 'lines': [('create', [create_tax])] }) @classmethod @ModelView.button def compute_liquidation(cls, records): for rec in records: rec.set_liquidation_lines() cls.create_withholding(rec) def get_dates(self, name): if name == 'start': values = [self.start_period.start] if self.contract.start_date: values.append(self.contract.start_date) res = max(values) elif name == 'end': values = [self.end_period.end] if self.contract.end_date: values.append(self.contract.end_date) res = min(values) return res def get_payrolls(self, name): if not self.employee or not self.contract: return Payroll = Pool().get('staff.payroll') date_start, date_end = self._get_dates() payrolls = Payroll.search([ ('employee', '=', self.employee.id), ('start', '>=', date_start), ('end', '<=', date_end), ('contract', '=', self.contract.id), ]) payrolls_ids = [payroll.id for payroll in payrolls] return payrolls_ids def get_salary(self, name): res = 0 if self.contract: if name == 'last_salary': res = self.contract.salary elif name == 'salary_average': Payroll = Pool().get('staff.payroll') Wage = Pool().get('staff.wage_type') wage, = Wage.search([('type_concept', '=', 'unemployment')]) res = Payroll.get_salary_average(self.end, self.employee, self.contract, wage) values = [self.end_period.end] if self.contract.end_date: values.append(self.contract.end_date) res = min(values) return res def create_move(self): pool = Pool() Move = pool.get('account.move') MoveLine = pool.get('account.move.line') Period = pool.get('account.period') if self.move: return move_lines, grouped = self.get_moves_lines() if move_lines: period_id = Period.find(self.company.id, date=self.liquidation_date) move, = Move.create([{ 'journal': self.journal.id, #'origin': str(self), 'period': period_id, 'date': self.liquidation_date, 'description': self.description, 'lines': [('create', move_lines)], }]) self.write([self], {'move': move.id}) for ml in move.lines: if ml.account.id not in grouped.keys() or ( ml.account.type.statement not in ('balance')): continue to_reconcile = [ml] to_reconcile.extend(grouped[ml.account.id]['lines']) if len(to_reconcile) > 1: MoveLine.reconcile(set(to_reconcile)) Move.post([move]) def get_moves_lines(self): lines_moves = [] to_reconcile = [] grouped = {} amount = [] for line in self.lines: if line.move_lines: for moveline in line.move_lines: to_reconcile.append(moveline) if moveline.account.id not in grouped.keys(): grouped[moveline.account.id] = { 'amount': [], 'description': line.description, 'lines': [], } grouped[moveline.account.id]['amount'].append(moveline.credit) grouped[moveline.account.id]['lines'].append(moveline) amount.append(moveline.credit) for adjust in line.adjustments: if adjust.account.id not in grouped.keys(): grouped[adjust.account.id] = { 'amount': [], 'description': adjust.description, 'lines': [], } grouped[adjust.account.id]['amount'].append(adjust.amount) amount.append(adjust.amount) for account_id, values in grouped.items(): _amount = sum(values['amount']) debit = _amount credit = _ZERO lines_moves.append(self._prepare_line(values['description'], account_id, debit=debit, credit=credit)) if lines_moves: lines_moves.append(self._prepare_line( self.description, self.account, credit=sum(amount), party_to_pay=self.party_to_pay, )) return lines_moves, grouped def _prepare_line(self, description, account_id, debit=_ZERO, credit=_ZERO, party_to_pay=None): if debit < _ZERO: credit = debit debit = _ZERO elif credit < _ZERO: debit = credit credit = _ZERO credit = abs(credit) debit = abs(debit) party_id = self.employee.party.id if party_to_pay: party_id = self.party_to_pay.id res = { 'description': description, 'debit': debit, 'credit': credit, 'account': account_id, 'party': party_id, } return res def _get_dates(self): date_end_contract = None date_start = self.start_period.start if self.contract.start_date > self.start_period.start: date_start = self.contract.start_date if self.contract.futhermores: date_end_contract = self.contract.finished_date date_end = self.end_period.end if date_end_contract and date_end_contract <= self.end_period.end: date_end = date_end_contract elif self.contract.end_date and self.contract.end_date < self.end_period.end: date_end = self.contract.end_date return date_start, date_end def _get_dates_liquidation(self): # date_end_contract = None date_start = self.start_period.start if self.contract.start_date > self.start_period.start: date_start = self.contract.start_date # if self.contract.futhermores: # date_end_contract = self.contract.finished_date date_end = self.end_period.end # if date_end_contract and date_end_contract <= self.end_period.end: # date_end = date_end_contract # elif self.contract.end_date and self.contract.end_date < self.end_period.end: # date_end = self.contract.end_date return date_start, date_end @classmethod def get_moves_lines_pending(cls, employee, wage_type, effective_date): MoveLine = Pool().get('account.move.line') lines = [] if not wage_type.credit_account: return account_id = wage_type.credit_account.id lines = MoveLine.search([ ('move.date', '<=', effective_date), ('credit', '>', 0), ('account', '=', account_id), ('party', '=', employee.party.id), ('reconciliation', '=', None), ]) return lines def set_liquidation_lines(self): pool = Pool() Payroll = pool.get('staff.payroll') date_start, date_end = self._get_dates_liquidation() payrolls = Payroll.search([ ('employee', '=', self.employee.id), ('start', '>=', date_start), ('end', '<=', date_end), ('contract', '=', self.contract.id), ('state', '=', 'posted') ]) wages = {} wages_target = {} for payroll in payrolls: for l in payroll.lines: if not l.wage_type.contract_finish: continue if self.kind == 'contract': if l.wage_type.type_concept not in CONTRACT: continue elif self.kind == 'interest': if l.wage_type.type_concept != 'interest': continue elif self.kind == 'vacation': if l.wage_type.type_concept != 'holidays': continue elif self.kind == 'unemployment': if l.wage_type.type_concept != 'unemployment': continue else: if l.wage_type.type_concept not in BONUS_SERVICE: continue if not l.wage_type.contract_finish: continue if l.wage_type.id not in wages_target.keys(): mlines = self.get_moves_lines_pending( payroll.employee, l.wage_type, date_end ) if not mlines: continue wages_target[l.wage_type.id] = [ l.wage_type.credit_account.id, mlines, l.wage_type, ] # wages.append(l.wage_type.id) # This looks for lines provisioned before start period # old_lines_provisioned = MoveLine.search([ # ('party', '=', self.employee.party.id), # ('move.date', '<', date_start), # ('reconciliation', '=', None), # ('account', '=', account_id), # ]) # lines.extend(old_lines_provisioned) for (account_id, lines, wage_type) in wages_target.values(): values = [] lines_to_reconcile = [] for line in lines: values.append(abs(line.debit - line.credit)) lines_to_reconcile.append(line.id) wages[wage_type.id] = { 'sequence': wage_type.sequence, 'wage': wage_type.id, 'description': wage_type.name, 'amount': sum(values), 'days': self.time_contracting, 'account': account_id, 'move_lines': [('add', lines_to_reconcile)], 'party_to_pay': self.party_to_pay, } Liquidation.write([self], {'lines': [('create', wages.values())]}) @fields.depends('start_period', 'end_period', 'contract') def on_change_with_time_contracting(self): delta = None if self.start_period and self.end_period and self.contract: try: date_start, date_end = self._get_dates() delta = self.getDifference(date_start, date_end) except: raise LiquidationEmployeeError( gettext('staff_payroll_co.msg_error_dates', s=self.employee.party.name)) delta = 0 return delta def on_change_contract(self): if self.contract: self.last_salary = self.contract.salary def countLeapYears(self, d): years = d.year # Check if the current year needs to be considered # for the count of leap years or not if (d.month <= 2): years -= 1 # An year is a leap year if it is a multiple of 4, # multiple of 400 and not a multiple of 100. return int(years / 4) - int(years / 100) + int(years / 400) def getDifference(self, start_date, end_date): # COUNT TOTAL NUMBER OF DAYS BEFORE FIRST DATE 'dt1' # initialize count using years and day n1 = start_date.year * 360 + start_date.day # Add days for months in given date for i in range(0, start_date.month - 1): n1 += 30 # Since every leap year is of 360 days, # Add a day for every leap year n1 += self.countLeapYears(start_date) # SIMILARLY, COUNT TOTAL NUMBER OF DAYS BEFORE 'dt2' n2 = end_date.year * 360 + end_date.day for i in range(0, end_date.month - 1): n2 += 30 n2 += self.countLeapYears(end_date) # return difference between two counts return (n2 - n1+1) def set_number(self): if self.number: return pool = Pool() Configuration = pool.get('staff.configuration') configuration = Configuration(1) if not configuration.staff_liquidation_sequence: raise MissingSecuenceLiquidation( gettext('staff_payroll_co.msg_sequence_missing')) seq = configuration.staff_liquidation_sequence.get() self.write([self], {'number': seq}) def get_sum_operation(self, name): res = [] for line in self.lines: if not line.amount: continue if name == 'gross_payments' and line.wage.definition == 'payment': res.append(line.amount) elif name == 'total_deductions' and line.wage.definition != 'payment': res.append(line.amount) return sum(res) def get_net_payment(self, name): res = (self.gross_payments or 0) - abs((self.total_deductions or 0)) if res: return self.currency.round(res) return 0 def get_currency(self, name): return self.company.currency.id class LiquidationLine(ModelSQL, ModelView): 'Staff Liquidation Line' __name__ = 'staff.liquidation.line' sequence = fields.Integer('Sequence', required=True) liquidation = fields.Many2One('staff.liquidation', 'Liquidation', required=True) wage = fields.Many2One('staff.wage_type', 'Wage Type', required=True) description = fields.Char('Description', required=True) amount = fields.Numeric('Amount', digits=(16, 2), required=True, depends=['adjustments', 'move_lines']) days = fields.Integer('Days') notes = fields.Char('Notes') account = fields.Many2One('account.account', 'Account') move_lines = fields.Many2Many('staff.liquidation.line-move.line', 'line', 'move_line', 'Liquidation Line - Move Line', domain=[ ('party', '=', Eval('party')), ('account', '=', Eval('account')), ], depends=['party', 'account']) party = fields.Function(fields.Many2One('party.party', 'Party'), 'get_party') adjustments = fields.One2Many('staff.liquidation.line_adjustment', 'staff_liquidation_line', 'Adjustments') party_to_pay = fields.Many2One('party.party', 'Party to Pay') salary_average = fields.Function(fields.Numeric('Salary Average', digits=(16, 2)), 'get_average_payroll') @classmethod def __setup__(cls): super(LiquidationLine, cls).__setup__() cls._order.insert(0, ('sequence', 'ASC')) @classmethod def __register__(cls, module_name): table_h = cls.__table_handler__(module_name) # Migration from 4.0: remove hoard_amount if table_h.column_exist('hoard_amount'): table_h.drop_column('hoard_amount') super(LiquidationLine, cls).__register__(module_name) def get_party(self, name=None): if self.liquidation.employee: return self.liquidation.employee.party.id @fields.depends('wage', 'description', 'amount', 'liquidation', '_parent_liquidation.employee', '_parent_liquidation.time_contracting', '_parent_liquidation.start_period', '_parent_liquidation.end_period', '_parent_liquidation.currency', 'sequence') def on_change_wage(self): if not self.wage: return self.sequence = self.wage.sequence self.description = self.wage.name self.days = self.liquidation.time_contracting @fields.depends('amount', 'adjustments', 'move_lines') def on_change_with_amount(self, name=None): amount_ = 0 if self.adjustments: amount_ += sum([ad.amount or 0 for ad in self.adjustments]) if self.move_lines: amount_ += sum([(ml.credit - ml.debit) for ml in self.move_lines]) return amount_ def get_average_payroll(self, name): contract = self.liquidation.contract employee = self.liquidation.employee end = self.liquidation.end Payroll = Pool().get('staff.payroll') res = Payroll.get_salary_average(end, employee, contract, self.wage) * 30 return res class LiquidationLineAdjustment(ModelSQL, ModelView): 'Liquidation Adjustment' __name__ = 'staff.liquidation.line_adjustment' staff_liquidation_line = fields.Many2One('staff.liquidation.line', 'Line', required=True, select=True) account = fields.Many2One('account.account', 'Acount', required=True, domain=[ ('company', '=', Eval('context', {}).get('company', -1)), ('type', '!=', None), ]) description = fields.Char('Description', required=True) amount = fields.Numeric('Amount', digits=(10,2), required=True) class LiquidationReport(Report): __name__ = 'staff.liquidation.report' class LiquidationLineMoveLine(ModelSQL): "Liquidation Line - MoveLine" __name__ = "staff.liquidation.line-move.line" _table = 'staff_liquidation_line_move_line_rel' line = fields.Many2One('staff.liquidation.line', 'Line', ondelete='CASCADE', select=True, required=True) move_line = fields.Many2One('account.move.line', 'Move Line', ondelete='RESTRICT', select=True, required=True) class LiquidationAdjustmentStart(ModelView): 'Create Liquidation Adjustment Start' __name__ = 'staff.liquidation_adjustment.start' wage_type = fields.Many2One('staff.wage_type', 'Wage Type', required=True, domain=[ ('contract_finish', '=', True) ]) amount = fields.Numeric('Amount', required=True) account = fields.Many2One('account.account', 'Acount', required=True, domain=[ ('company', '=', Eval('context', {}).get('company', -1)), ('type', '!=', None), ]) description = fields.Char('Description', required=True) @fields.depends('wage_type', 'account') def on_change_wage_type(self): if self.wage_type and self.wage_type.debit_account: self.account = self.wage_type.debit_account.id class LiquidationAdjustment(Wizard): 'Create Liquidation Adjustment' __name__ = 'staff.liquidation_adjustment' start = StateView('staff.liquidation_adjustment.start', 'staff_payroll_co.liquidation_adjustment_start_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Create', 'accept', 'tryton-ok', default=True), ]) accept = StateTransition() @classmethod def __setup__(cls): super(LiquidationAdjustment, cls).__setup__() def create_adjustment(self, line): LineAdjustment = Pool().get('staff.liquidation.line_adjustment') adjust, = LineAdjustment.create([{ 'staff_liquidation_line': line.id, 'account': self.start.account.id, 'description': self.start.description, 'amount': self.start.amount, }]) return adjust def transition_accept(self): pool = Pool() Liquidation = pool.get('staff.liquidation') LiquidationLine = pool.get('staff.liquidation.line') id_ = Transaction().context['active_id'] liquidation, = Liquidation.search([('id', '=', id_)]) line_created = None if liquidation: if liquidation.move: raise LiquidationMoveError( gettext('staff_payroll_co.msg_liquidation_with_move', s=liquidation.number)) for line in liquidation.lines: if line.wage.id == self.start.wage_type.id: if line.amount: line.amount += self.start.amount line.save() line_created = self.create_adjustment(line) if not line_created: line, = LiquidationLine.create([{ 'sequence': len(liquidation.lines) + 1, 'liquidation': liquidation.id, 'wage': self.start.wage_type.id, 'description': self.start.wage_type.name, 'amount': self.start.amount, }]) self.create_adjustment(line) return 'end' class LiquidationGroupStart(ModelView): 'Liquidation Group Start' __name__ = 'staff.liquidation_group.start' start_period = fields.Many2One('staff.payroll.period', 'Start Period', required=True) end_period = fields.Many2One('staff.payroll.period', 'End Period', required=True, depends=['start_period']) kind = fields.Selection([ ('contract', 'Contract'), ('bonus_service', 'Bonus Service'), ('interest', 'Interest'), ('vacation', 'Vacation'), ('unemployment', 'Unemployment'), ], 'Kind', required=True) liquidation_date = fields.Date('Liquidation Date', required=True) company = fields.Many2One('company.company', 'Company', required=True) description = fields.Char('Description', required=True) account = fields.Many2One('account.account', 'Account', required=True, domain=[ ('type.payable', '=', True), ('company', '=', Eval('company')) ]) employees = fields.Many2Many('company.employee', None, None, 'Employee') party_to_pay = fields.Many2One('party.party', 'Party to Pay', states={ 'invisible': Eval('kind') != 'unemployment' }) @staticmethod def default_company(): return Transaction().context.get('company') class LiquidationGroup(Wizard): 'Liquidation Group' __name__ = 'staff.liquidation_group' start = StateView('staff.liquidation_group.start', 'staff_payroll_co.liquidation_group_start_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Accept', 'open_', 'tryton-ok', default=True), ]) open_ = StateTransition() def transition_open_(self): pool = Pool() Liquidation = pool.get('staff.liquidation') Contract = pool.get('staff.contract') Employee = pool.get('company.employee') Period = pool.get('staff.payroll.period') to_liquidation = [] start_period = self.start.start_period.id end_period = self.start.end_period.id kind = self.start.kind liquidation_date = self.start.liquidation_date company_id = self.start.company.id description = self.start.description currency_id = self.start.company.currency.id account_id = self.start.account.id start_date = self.start.start_period.start end_date = self.start.end_period.end periods = Period.search([ ('start', '>=', start_date), ('start', '<=', end_date) ]) dom_contracts = [('kind', '=', kind)] if self.start.employees: employees = self.start.employees employee_ids = [e.id for e in employees] dom_contracts.append(('employee', 'in', employee_ids)) else: employees = Employee.search([('contract', '!=', None)]) if periods: periods_ids = [p.id for p in periods] dom_contracts.append(('start_period', 'in', periods_ids)) dom_contracts.append(('end_period', 'in', periods_ids)) contracts = Liquidation.search_read( dom_contracts, fields_names=['contract'] ) contract_ids = [i['contract'] for i in contracts] for employee in employees: contracts = Contract.search([ ('employee', '=', employee.id), ('id', 'not in', contract_ids), ('id', 'not in', contract_ids), ]) if not contracts: continue contract = contracts[0] if kind == 'unemployment': wages = [ mw for mw in employee.mandatory_wages if mw.wage_type.type_concept == 'unemployment' ] if not wages: continue wage = wages[0] lines = Liquidation.get_moves_lines_pending( employee, wage.wage_type, self.start.end_period.end ) if not lines: continue lqt_create = { 'start_period': start_period, 'end_period': end_period, 'employee': employee.id, 'contract': contract.id, 'kind': kind, 'liquidation_date': liquidation_date, 'time_contracting': None, 'state': 'draft', 'company': company_id, 'description': description, 'currency': currency_id, 'account': account_id, 'party_to_pay': self.start.party_to_pay.id if self.start.party_to_pay else None } liq, = Liquidation.create([lqt_create]) liq.time_contracting = liq.on_change_with_time_contracting() liq.save() to_liquidation.append(liq) Liquidation.compute_liquidation(to_liquidation) return 'end' class MoveProvisionBonusServiceStart(ModelView): 'Move Privision Bonus Service Start' __name__ = 'staff.move_provision_bonus_service.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_type = fields.Many2One('staff.wage_type', 'Wage Types', domain=[ ('type_concept', '=', 'bonus_service') ], required=True) category = fields.Many2One('staff.employee_category', 'Category') @staticmethod def default_company(): return Transaction().context.get('company') class MoveProvisionBonusService(Wizard): 'Move Provision Bonus Service' __name__ = 'staff.move_provision_bonus_service' start = StateView('staff.move_provision_bonus_service.start', 'staff_payroll_co.move_provision_bonus_service_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Accept', 'open_', 'tryton-ok', default=True), ]) open_ = StateTransition() def transition_open_(self): pool = Pool() Contract = pool.get('staff.contract') Move = pool.get('account.move') AccountPeriod = pool.get('account.period') configuration = Pool().get('staff.configuration')(1) journal_id = None if configuration and configuration.default_journal: journal_id = configuration.default_journal.id _end_date = self.start.period.end _company = self.start.company provision_wage = self.start.wage_type period_days = (self.start.period.end - self.start.period.start).days + 1 dom_contract = [ ['AND', ['OR', [ ('end_date', '>', self.start.period.start), ], [ ('end_date', '=', None), ], ]], ] if self.start.category: dom_contract.append( ('employee.category', '=', self.start.category.id) ) for contract in Contract.search(dom_contract): period_in_month = 1 if period_days > 15 else 2 salary_amount = contract.get_salary_in_date(_end_date) base_ = salary_amount move_lines = [] employee = contract.employee for concept in contract.employee.mandatory_wages: if concept.wage_type and concept.wage_type.salary_constitute: if concept.wage_type.type_concept == 'transport': base_ += concept.wage_type.compute_unit_price({'salary': 0}) * concept.wage_type.default_quantity if concept.fix_amount: base_ += concept.fix_amount period_id = AccountPeriod.find(_company.id, date=_end_date) provision_amount = provision_wage.compute_unit_price( {'salary': (round((base_ / period_in_month), 2))} ) move_lines.extend([ { 'debit': provision_amount, 'credit': 0, 'party': employee.party.id, 'account': provision_wage.debit_account.id }, { 'debit': 0, 'credit': provision_amount, 'party': employee.party.id, 'account': provision_wage.credit_account.id } ]) move, = Move.create([{ 'journal': journal_id, # 'origin': str(contract), 'period': period_id, 'company': _company.id, 'date': _end_date, 'state': 'draft', 'description': self.start.description, 'lines': [('create', move_lines)], }]) return 'end' class LiquidationExportStart(ModelView): 'Liquidation Export Start' __name__ = 'staff.liquidation_export.start' start_date = fields.Date('Start Date', required=True) end_date = fields.Date('End Date', required=True) company = fields.Many2One('company.company', 'Company', required=True) department = fields.Many2One('company.department', 'Department') kind = fields.Selection([ ('contract', 'Contract'), ('bonus_service', 'Bonus Service'), ('interest', 'Interest'), ('unemployment', 'Unemployment'), ('vacation', 'Vacation'), ], 'Kind') @staticmethod def default_company(): return Transaction().context.get('company') @fields.depends('start_date') def on_change_with_end_date(self, name=None): if self.start_date: return self.start_date class LiquidationExport(Wizard): 'Liquidation Export' __name__ = 'staff.liquidation_export' start = StateView('staff.liquidation_export.start', 'staff_payroll_co.liquidation_start_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Print', 'print_', 'tryton-ok', default=True), ]) print_ = StateReport('staff.liquidation_export.report') def do_print_(self, action): department_id = None if self.start.department: department_id = self.start.department.id data = { 'ids': [], 'company': self.start.company.id, 'start_date': self.start.start_date, 'end_date': self.start.end_date, 'department': department_id, 'kind': self.start.kind, } return action, data def transition_print_(self): return 'end' class LiquidationExportReport(Report): __name__ = 'staff.liquidation_export.report' @classmethod def get_domain_liquidation(cls, data): dom_liquidation = [] return dom_liquidation @classmethod def get_context(cls, records, header, data): report_context = super().get_context(records, header, data) pool = Pool() user = pool.get('res.user')(Transaction().user) Liquidation = pool.get('staff.liquidation') Department = pool.get('company.department') dom_liq = cls.get_domain_liquidation(data) dom_liq.append(('state', 'in', ['processed', 'posted', 'draft']),) dom_liq.append(('liquidation_date', '>=', data['start_date']),) dom_liq.append(('liquidation_date', '<=', data['end_date']),) if data['department']: dom_liq.append(('employee.department', '=', data['department']),) department = Department(data['department']).name else: department = None if data['kind']: dom_liq.append(('kind', '=', data['kind'])) liquidations = Liquidation.search(dom_liq) # default_vals = cls.default_values() sum_gross_payments = [] sum_total_deductions = [] sum_net_payment = [] parties = {} payments = ['interest', 'holidays', 'bonus_service', 'unemployment'] for liquidation in liquidations: employee_id = liquidation.employee.id if employee_id not in parties.keys(): position_employee = liquidation.employee.position.name if liquidation.employee.position else '' position_contract = liquidation.contract.position.name if liquidation.contract and liquidation.contract.position else '' parties[employee_id] = {} parties[employee_id]['employee_code'] = liquidation.employee.code parties[employee_id]['employee_contract'] = liquidation.contract.id parties[employee_id]['employee'] = liquidation.employee.party.name parties[employee_id]['employee_id_number'] = liquidation.employee.party.id_number parties[employee_id]['employee_position'] = position_contract or position_employee or '' for line in liquidation.lines: if line.wage.type_concept in (payments): concept = line.wage.type_concept else: if line.wage.definition == 'payment' and line.wage.receipt: concept = 'others_payments' elif line.wage.definition == 'deduction' or \ line.wage.definition == 'discount' and \ line.wage.receipt: concept = 'others_deductions' if not concept: raise WageTypeConceptError( gettext('staff_payroll_co.msg_type_concept_not_exists', s=line.wage.name)) if concept not in parties[employee_id].keys(): parties[employee_id][concept] = 0 parties[employee_id][concept] += line.amount parties[employee_id]['time_contracting'] = liquidation.time_contracting parties[employee_id]['net_payment'] = liquidation.net_payment parties[employee_id]['gross_payments'] = liquidation.gross_payments parties[employee_id]['total_deductions'] = liquidation.total_deductions sum_gross_payments.append(liquidation.gross_payments) sum_total_deductions.append(liquidation.total_deductions) sum_net_payment.append(liquidation.net_payment) employee_dict = {e['employee']: e for e in parties.values()} report_context['records'] = sorted(employee_dict.items(), key=lambda t: t[0]) report_context['department'] = department report_context['start_date'] = data['start_date'] report_context['end_date'] = data['end_date'] report_context['company'] = user.company report_context['user'] = user report_context['sum_gross_payments'] = sum(sum_gross_payments) report_context['sum_net_payment'] = sum(sum_net_payment) report_context['sum_total_deductions'] = sum(sum_total_deductions) return report_context