# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. from odoo import api, fields, models, _ from odoo.exceptions import UserError, ValidationError from odoo.tools import float_compare, float_is_zero class HrContract(models.Model): _inherit = 'hr.contract' payroll_fiscal_position_id = fields.Many2one('account.fiscal.position', 'Payroll Fiscal Position', domain="[('company_id', '=', company_id)]", help="Used for mapping accounts when processing payslip journal entries.") class HrPayslipRun(models.Model): _inherit = 'hr.payslip.run' @api.depends('slip_ids.is_paid') def _is_paid(self): for run in self: run.is_paid = all(run.slip_ids.mapped('is_paid')) is_paid = fields.Boolean(string="Payslips Paid", compute='_is_paid', store=True) date = fields.Date('Date Account', states={'draft': [('readonly', False)], 'verify': [('readonly', False)]}, readonly=True, help="Keep empty to use the period of the validation(Payslip) date.") batch_payment_id = fields.Many2one('account.batch.payment', string='Payment Batch') def action_register_payment(self): action = self.mapped('slip_ids').action_register_payment() payments = self.env['account.payment'].browse(action['res_ids']) batch_action = payments.create_batch_payment() self.write({'batch_payment_id': batch_action['res_id']}) return batch_action def write(self, values): if 'date' in values: slips = self.mapped('slip_ids').filtered(lambda s: s.state in ('draft', 'verify')) slips.write({'date': values['date']}) return super().write(values) class HrPayslip(models.Model): _inherit = 'hr.payslip' @api.depends('move_id', 'move_id.line_ids.full_reconcile_id') def _is_paid(self): for payslip in self: payslip.is_paid = ( payslip.move_id and len(payslip.move_id.line_ids.filtered(lambda l: ( l.partner_id == payslip.employee_id.address_home_id and l.account_id.internal_type == 'payable' and not l.reconciled ))) == 0 ) is_paid = fields.Boolean(string="Has been Paid", compute='_is_paid', store=True) @api.model def create(self, vals): if 'date' in self.env.context: vals['date'] = self.env.context.get('date') return super(HrPayslip, self).create(vals) def _payment_values(self, amount): method = self.move_id.journal_id.payroll_payment_method_id if amount > 0.0: method = self.move_id.journal_id.payroll_payment_method_refund_id method_line = self.move_id.journal_id.payroll_payment_journal_id.outbound_payment_method_line_ids.filtered( lambda l: l.payment_method_id == method ) values = { 'payment_reference': self.number, 'ref': self.number + ' - ' + self.name, 'journal_id': self.move_id.journal_id.payroll_payment_journal_id.id, 'payment_method_line_id': method_line.id, 'partner_type': 'supplier', 'partner_id': self.employee_id.address_home_id.id, 'payment_type': 'outbound', 'amount': -amount, } if amount > 0.0: values.update({ 'payment_type': 'inbound', 'amount': amount, }) return values def action_register_payment(self): slips = self.filtered(lambda s: s.move_id.state in ('draft', 'posted') and not s.is_paid) if not all(slip.move_id.journal_id.payroll_payment_journal_id for slip in slips): raise UserError(_('Payroll Payment journal not configured on the existing entry\'s journal.')) if not all(slip.move_id.journal_id.payroll_payment_method_id for slip in slips): raise UserError(_('Payroll Payment method not configured on the existing entry\'s journal.')) # as of 14, you cannot reconcile to un-posted moves # so if you are paying it, we must assume you want to post any draft entries slip_moves = slips.mapped('move_id') unposted_moves = slip_moves.filtered(lambda m: m.state == 'draft') unposted_moves._post(soft=False) payments = self.env['account.payment'] for slip in slips: lines_to_pay = slip.move_id.line_ids.filtered(lambda l: l.partner_id == slip.employee_id.address_home_id and l.account_id == slip.employee_id.address_home_id.property_account_payable_id) amount = sum(lines_to_pay.mapped('amount_residual')) if not amount: continue payment_values = slip._payment_values(amount) payment = payments.create(payment_values) payment.action_post() lines_paid = payment.line_ids.filtered(lambda l: l.account_id == slip.employee_id.address_home_id.property_account_payable_id) lines_to_reconcile = lines_to_pay + lines_paid lines_to_reconcile.reconcile() payments += payment action = self.env.ref('account.action_account_payments_payable').read()[0] action.update({ 'res_ids': payments.ids, 'domain': [('id', 'in', payments.ids)], }) return action def action_payslip_done(self): res = super(HrPayslip, self).action_payslip_done() self._generate_move() return res def _generate_move(self): """ Generate the accounting entries related to the selected payslips A move is created for each journal and for each month. """ # Not needed after abstraction #res = super(HrPayslip, self).action_payslip_done() #precision = self.env['decimal.precision'].precision_get('Payroll') # Add payslip without run payslips_to_post = self.filtered(lambda slip: not slip.payslip_run_id) # Adding pay slips from a batch and deleting pay slips with a batch that is not ready for validation. payslip_runs = (self - payslips_to_post).mapped('payslip_run_id') for run in payslip_runs: if run._are_payslips_ready(): payslips_to_post |= run.slip_ids # A payslip need to have a done state and not an accounting move. payslips_to_post = payslips_to_post.filtered(lambda slip: slip.state == 'done' and not slip.move_id) # Check that a journal exists on all the structures if any(not payslip.struct_id for payslip in payslips_to_post): raise ValidationError(_('One of the contract for these payslips has no structure type.')) if any(not structure.journal_id for structure in payslips_to_post.mapped('struct_id')): raise ValidationError(_('One of the payroll structures has no account journal defined on it.')) # Map all payslips by structure journal and pay slips month. # {'journal_id': {'month': [slip_ids]}} # slip_mapped_data = { # slip.struct_id.journal_id.id: {fields.Date().end_of(slip.date_to, 'month'): self.env['hr.payslip']} for slip # in # payslips_to_post} # Hibou Customization: group with journal itself so that journal behavior can be derived. # Hibou Customization: prefer slip's `date` over end of month # Hibou Customization: add an account mapping based on fiscal position slip_mapped_data = { slip.struct_id.journal_id: {slip.date or fields.Date().end_of(slip.date_to, 'month'): self.env['hr.payslip']} for slip in payslips_to_post} for slip in payslips_to_post: slip_mapped_data[slip.struct_id.journal_id][slip.date or fields.Date().end_of(slip.date_to, 'month')] |= slip account_map = payslips_to_post._generate_account_map() for journal in slip_mapped_data: # For each journal_id. """ All methods to generate journal entry should generate one or more journal entries given this format. """ for slip_date in slip_mapped_data[journal]: # For each month. if hasattr(self, '_generate_move_' + str(journal.payroll_entry_type)): getattr(self, '_generate_move_' + str(journal.payroll_entry_type))(slip_mapped_data, journal, slip_date, account_map) else: self._generate_move_original(slip_mapped_data, journal, slip_date, account_map) def _check_slips_employee_home_address(self): employees_missing_partner = self.mapped('employee_id').filtered(lambda e: not e.address_home_id) if employees_missing_partner: raise UserError(_('The following employees are missing private addresses. %s') % \ (', '.join(employees_missing_partner.mapped('name')))) address_ap = self.mapped('employee_id.address_home_id.property_account_payable_id') if len(address_ap) > 1: raise UserError(_('Employee\'s private address account payable not the same for all addresses.')) def _generate_account_map(self): account_map = {} rules = self.mapped('line_ids.salary_rule_id') base_account_map = {a: a for a in (rules.mapped('account_debit') | rules.mapped('account_credit'))} account_map[self.env['account.fiscal.position']] = base_account_map fiscal_positions = self.mapped('contract_id.payroll_fiscal_position_id') for fp in fiscal_positions: account_map[fp] = fp.map_accounts(base_account_map.copy()) return account_map def _process_journal_lines_grouped(self, line_ids, date, precision, account_map): slip = self employee_partner_id = slip.employee_id.address_home_id.id for line in slip.line_ids.filtered(lambda l: l.category_id): amount = -line.total if slip.credit_note else line.total if line.code == 'NET': # Check if the line is the 'Net Salary'. for tmp_line in slip.line_ids.filtered(lambda l: l.category_id): if tmp_line.salary_rule_id.not_computed_in_net: # Check if the rule must be computed in the 'Net Salary' or not. if amount > 0: amount -= abs(tmp_line.total) elif amount < 0: amount += abs(tmp_line.total) if float_is_zero(amount, precision_digits=precision): continue debit_account = line.salary_rule_id.account_debit debit_account_id = account_map[debit_account].id if debit_account else False credit_account = line.salary_rule_id.account_credit credit_account_id = account_map[credit_account].id if credit_account else False partner_id = line.salary_rule_id.partner_id.id or employee_partner_id if debit_account_id: # If the rule has a debit account. debit = amount if amount > 0.0 else 0.0 credit = -amount if amount < 0.0 else 0.0 existing_debit_lines = ( line_id for line_id in line_ids if # line_id['name'] == line.name line_id['partner_id'] == partner_id and line_id['account_id'] == debit_account_id and ((line_id['debit'] > 0 and credit <= 0) or (line_id['credit'] > 0 and debit <= 0))) debit_line = next(existing_debit_lines, False) if not debit_line: debit_line = { 'name': line.name, 'partner_id': partner_id, 'account_id': debit_account_id, 'journal_id': slip.struct_id.journal_id.id, 'date': date, 'debit': debit, 'credit': credit, 'analytic_account_id': line.salary_rule_id.analytic_account_id.id or slip.contract_id.analytic_account_id.id, } line_ids.append(debit_line) else: line_name_pieces = set(debit_line['name'].split(', ')) line_name_pieces.add(line.name) debit_line['name'] = ', '.join(line_name_pieces) debit_line['debit'] += debit debit_line['credit'] += credit if credit_account_id: # If the rule has a credit account. debit = -amount if amount < 0.0 else 0.0 credit = amount if amount > 0.0 else 0.0 existing_credit_line = ( line_id for line_id in line_ids if # line_id['name'] == line.name line_id['partner_id'] == partner_id and line_id['account_id'] == credit_account_id and ((line_id['debit'] > 0 and credit <= 0) or (line_id['credit'] > 0 and debit <= 0)) ) credit_line = next(existing_credit_line, False) if not credit_line: credit_line = { 'name': line.name, 'partner_id': partner_id, 'account_id': credit_account_id, 'journal_id': slip.struct_id.journal_id.id, 'date': date, 'debit': debit, 'credit': credit, 'analytic_account_id': line.salary_rule_id.analytic_account_id.id or slip.contract_id.analytic_account_id.id, } line_ids.append(credit_line) else: line_name_pieces = set(credit_line['name'].split(', ')) line_name_pieces.add(line.name) credit_line['name'] = ', '.join(line_name_pieces) credit_line['debit'] += debit credit_line['credit'] += credit def _generate_move_grouped(self, slip_mapped_data, journal, slip_date, account_map): slip_mapped_data[journal][slip_date]._check_slips_employee_home_address() precision = self.env['decimal.precision'].precision_get('Payroll') line_ids = [] debit_sum = 0.0 credit_sum = 0.0 date = slip_date move_dict = { 'narration': '', 'ref': date.strftime('%B %Y'), 'journal_id': journal.id, 'date': date, } for slip in slip_mapped_data[journal][slip_date]: slip_accounts = account_map[slip.contract_id.payroll_fiscal_position_id] move_dict['narration'] += slip.number or '' + ' - ' + slip.employee_id.name or '' move_dict['narration'] += '\n' slip._process_journal_lines_grouped(line_ids, date, precision, slip_accounts) for line_id in line_ids: # Get the debit and credit sum. debit_sum += line_id['debit'] credit_sum += line_id['credit'] # The code below is called if there is an error in the balance between credit and debit sum. if float_compare(credit_sum, debit_sum, precision_digits=precision) == -1: acc_id = slip.journal_id.default_account_id.id if not acc_id: raise UserError( _('The Expense Journal "%s" has not properly configured the Default Account!') % ( slip.journal_id.name)) existing_adjustment_line = ( line_id for line_id in line_ids if line_id['name'] == _('Adjustment Entry') ) adjust_credit = next(existing_adjustment_line, False) if not adjust_credit: adjust_credit = { 'name': _('Adjustment Entry'), 'partner_id': False, 'account_id': acc_id, 'journal_id': slip.journal_id.id, 'date': date, 'debit': 0.0, 'credit': debit_sum - credit_sum, } line_ids.append(adjust_credit) else: adjust_credit['credit'] = debit_sum - credit_sum elif float_compare(debit_sum, credit_sum, precision_digits=precision) == -1: acc_id = slip.journal_id.default_account_id.id if not acc_id: raise UserError(_('The Expense Journal "%s" has not properly configured the Default Account!') % ( slip.journal_id.name)) existing_adjustment_line = ( line_id for line_id in line_ids if line_id['name'] == _('Adjustment Entry') ) adjust_debit = next(existing_adjustment_line, False) if not adjust_debit: adjust_debit = { 'name': _('Adjustment Entry'), 'partner_id': False, 'account_id': acc_id, 'journal_id': slip.journal_id.id, 'date': date, 'debit': credit_sum - debit_sum, 'credit': 0.0, } line_ids.append(adjust_debit) else: adjust_debit['debit'] = credit_sum - debit_sum # Add accounting lines in the move move_dict['line_ids'] = [(0, 0, line_vals) for line_vals in line_ids] move = self.env['account.move'].create(move_dict) for slip in slip_mapped_data[journal][slip_date]: slip.write({'move_id': move.id, 'date': date}) def _generate_move_slip(self, slip_mapped_data, journal, slip_date, account_map): slip_mapped_data[journal][slip_date]._check_slips_employee_home_address() precision = self.env['decimal.precision'].precision_get('Payroll') for slip in slip_mapped_data[journal][slip_date]: slip_accounts = account_map[slip.contract_id.payroll_fiscal_position_id] line_ids = [] debit_sum = 0.0 credit_sum = 0.0 date = slip_date move_dict = { 'narration': '', 'ref': date.strftime('%B %Y'), 'journal_id': journal.id, 'date': date, } move_dict['narration'] += slip.number or '' + ' - ' + slip.employee_id.name or '' move_dict['narration'] += '\n' slip._process_journal_lines_grouped(line_ids, date, precision, slip_accounts) for line_id in line_ids: # Get the debit and credit sum. debit_sum += line_id['debit'] credit_sum += line_id['credit'] # The code below is called if there is an error in the balance between credit and debit sum. if float_compare(credit_sum, debit_sum, precision_digits=precision) == -1: acc_id = slip.journal_id.default_account_id.id if not acc_id: raise UserError( _('The Expense Journal "%s" has not properly configured the Default Account!') % ( slip.journal_id.name)) existing_adjustment_line = ( line_id for line_id in line_ids if line_id['name'] == _('Adjustment Entry') ) adjust_credit = next(existing_adjustment_line, False) if not adjust_credit: adjust_credit = { 'name': _('Adjustment Entry'), 'partner_id': False, 'account_id': acc_id, 'journal_id': slip.journal_id.id, 'date': date, 'debit': 0.0, 'credit': debit_sum - credit_sum, } line_ids.append(adjust_credit) else: adjust_credit['credit'] = debit_sum - credit_sum elif float_compare(debit_sum, credit_sum, precision_digits=precision) == -1: acc_id = slip.journal_id.default_account_id.id if not acc_id: raise UserError(_('The Expense Journal "%s" has not properly configured the Default Account!') % ( slip.journal_id.name)) existing_adjustment_line = ( line_id for line_id in line_ids if line_id['name'] == _('Adjustment Entry') ) adjust_debit = next(existing_adjustment_line, False) if not adjust_debit: adjust_debit = { 'name': _('Adjustment Entry'), 'partner_id': False, 'account_id': acc_id, 'journal_id': slip.journal_id.id, 'date': date, 'debit': credit_sum - debit_sum, 'credit': 0.0, } line_ids.append(adjust_debit) else: adjust_debit['debit'] = credit_sum - debit_sum # Add accounting lines in the move move_dict['line_ids'] = [(0, 0, line_vals) for line_vals in line_ids] move = self.env['account.move'].create(move_dict) slip.write({'move_id': move.id, 'date': date}) def _generate_move_original(self, slip_mapped_data, journal, slip_date, account_map): """ Odoo's original version. Fixed bug with 'matching' credit line """ precision = self.env['decimal.precision'].precision_get('Payroll') line_ids = [] debit_sum = 0.0 credit_sum = 0.0 date = slip_date move_dict = { 'narration': '', 'ref': date.strftime('%B %Y'), 'journal_id': journal.id, 'date': date, } for slip in slip_mapped_data[journal][slip_date]: slip_accounts = account_map[slip.contract_id.payroll_fiscal_position_id] move_dict['narration'] += slip.number or '' + ' - ' + slip.employee_id.name or '' move_dict['narration'] += '\n' for line in slip.line_ids.filtered(lambda l: l.category_id): amount = -line.total if slip.credit_note else line.total if line.code == 'NET': # Check if the line is the 'Net Salary'. for tmp_line in slip.line_ids.filtered(lambda l: l.category_id): if tmp_line.salary_rule_id.not_computed_in_net: # Check if the rule must be computed in the 'Net Salary' or not. if amount > 0: amount -= abs(tmp_line.total) elif amount < 0: amount += abs(tmp_line.total) if float_is_zero(amount, precision_digits=precision): continue debit_account = line.salary_rule_id.account_debit debit_account_id = slip_accounts[debit_account].id if debit_account else False credit_account = line.salary_rule_id.account_credit credit_account_id = slip_accounts[credit_account].id if credit_account else False if debit_account_id: # If the rule has a debit account. debit = amount if amount > 0.0 else 0.0 credit = -amount if amount < 0.0 else 0.0 existing_debit_lines = ( line_id for line_id in line_ids if line_id['name'] == line.name and line_id['account_id'] == debit_account_id and ((line_id['debit'] > 0 and credit <= 0) or (line_id['credit'] > 0 and debit <= 0))) debit_line = next(existing_debit_lines, False) if not debit_line: debit_line = { 'name': line.name, 'partner_id': False, 'account_id': debit_account_id, 'journal_id': slip.struct_id.journal_id.id, 'date': date, 'debit': debit, 'credit': credit, 'analytic_account_id': line.salary_rule_id.analytic_account_id.id or slip.contract_id.analytic_account_id.id, } line_ids.append(debit_line) else: debit_line['debit'] += debit debit_line['credit'] += credit if credit_account_id: # If the rule has a credit account. debit = -amount if amount < 0.0 else 0.0 credit = amount if amount > 0.0 else 0.0 existing_credit_line = ( line_id for line_id in line_ids if line_id['name'] == line.name and line_id['account_id'] == credit_account_id and ((line_id['debit'] > 0 and credit <= 0) or (line_id['credit'] > 0 and debit <= 0)) ) credit_line = next(existing_credit_line, False) if not credit_line: credit_line = { 'name': line.name, 'partner_id': False, 'account_id': credit_account_id, 'journal_id': slip.struct_id.journal_id.id, 'date': date, 'debit': debit, 'credit': credit, 'analytic_account_id': line.salary_rule_id.analytic_account_id.id or slip.contract_id.analytic_account_id.id, } line_ids.append(credit_line) else: credit_line['debit'] += debit credit_line['credit'] += credit for line_id in line_ids: # Get the debit and credit sum. debit_sum += line_id['debit'] credit_sum += line_id['credit'] # The code below is called if there is an error in the balance between credit and debit sum. if float_compare(credit_sum, debit_sum, precision_digits=precision) == -1: acc_id = slip.journal_id.default_account_id.id if not acc_id: raise UserError( _('The Expense Journal "%s" has not properly configured the Default Account!') % ( slip.journal_id.name)) existing_adjustment_line = ( line_id for line_id in line_ids if line_id['name'] == _('Adjustment Entry') ) adjust_credit = next(existing_adjustment_line, False) if not adjust_credit: adjust_credit = { 'name': _('Adjustment Entry'), 'partner_id': False, 'account_id': acc_id, 'journal_id': slip.journal_id.id, 'date': date, 'debit': 0.0, 'credit': debit_sum - credit_sum, } line_ids.append(adjust_credit) else: adjust_credit['credit'] = debit_sum - credit_sum elif float_compare(debit_sum, credit_sum, precision_digits=precision) == -1: acc_id = slip.journal_id.default_account_id.id if not acc_id: raise UserError(_('The Expense Journal "%s" has not properly configured the Default Account!') % ( slip.journal_id.name)) existing_adjustment_line = ( line_id for line_id in line_ids if line_id['name'] == _('Adjustment Entry') ) adjust_debit = next(existing_adjustment_line, False) if not adjust_debit: adjust_debit = { 'name': _('Adjustment Entry'), 'partner_id': False, 'account_id': acc_id, 'journal_id': slip.journal_id.id, 'date': date, 'debit': credit_sum - debit_sum, 'credit': 0.0, } line_ids.append(adjust_debit) else: adjust_debit['debit'] = credit_sum - debit_sum # Add accounting lines in the move move_dict['line_ids'] = [(0, 0, line_vals) for line_vals in line_ids] move = self.env['account.move'].create(move_dict) for slip in slip_mapped_data[journal][slip_date]: slip.write({'move_id': move.id, 'date': date})