diff --git a/hr_expense_change/__init__.py b/hr_expense_change/__init__.py new file mode 100644 index 00000000..40272379 --- /dev/null +++ b/hr_expense_change/__init__.py @@ -0,0 +1 @@ +from . import wizard diff --git a/hr_expense_change/__manifest__.py b/hr_expense_change/__manifest__.py new file mode 100644 index 00000000..2bab2f06 --- /dev/null +++ b/hr_expense_change/__manifest__.py @@ -0,0 +1,31 @@ +{ + 'name': 'HR Expense Change', + 'author': 'Hibou Corp. ', + 'version': '14.0.1.0.0', + 'category': 'Employees', + 'sequence': 95, + 'summary': 'Technical foundation for changing expenses.', + 'description': """ +Technical foundation for changing expenses. + +Creates wizard and permissions for making expense changes that can be +handled by other individual modules. + +This module implements, as examples, how to change the Date fields. + +Abstractly, individual 'changes' should come from specific 'fields' or capability +modules that handle the consequences of changing that field in whatever state the +the invoice is currently in. + + """, + 'website': 'https://hibou.io/', + 'depends': [ + 'hr_expense', + ], + 'data': [ + 'security/ir.model.access.csv', + 'wizard/expense_change_views.xml', + ], + 'installable': True, + 'application': False, +} diff --git a/hr_expense_change/security/ir.model.access.csv b/hr_expense_change/security/ir.model.access.csv new file mode 100644 index 00000000..f392cd8d --- /dev/null +++ b/hr_expense_change/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_hr_expense_change,access_hr_expense_change,model_hr_expense_change,hr_expense.group_hr_expense_manager,1,1,1,1 diff --git a/hr_expense_change/tests/__init__.py b/hr_expense_change/tests/__init__.py new file mode 100644 index 00000000..431d3587 --- /dev/null +++ b/hr_expense_change/tests/__init__.py @@ -0,0 +1 @@ +from . import test_expense_change diff --git a/hr_expense_change/tests/test_expense_change.py b/hr_expense_change/tests/test_expense_change.py new file mode 100644 index 00000000..7c4cc156 --- /dev/null +++ b/hr_expense_change/tests/test_expense_change.py @@ -0,0 +1,154 @@ +from odoo.addons.hr_expense.tests import test_expenses +from odoo.tests import tagged +from odoo import fields + + +@tagged('-at_install', 'post_install') +class TestAccountEntry(test_expenses.TestExpenses): + + def test_expense_values(self): + """ Checking accounting move entries and analytic entries when submitting expense """ + + # The expense employee is able to a create an expense sheet. + # The total should be 1725.0 because: + # - first line: 1000.0 (unit amount) + 150.0 (tax) = 1150.0 + # - second line: (1500.0 (unit amount) + 225.0 (tax)) * 1/3 (rate) = 575.0. + + expense_sheet = self.env['hr.expense.sheet'].create({ + 'name': 'First Expense for employee', + 'employee_id': self.expense_employee.id, + 'journal_id': self.company_data['default_journal_purchase'].id, + 'accounting_date': '2017-01-01', + 'expense_line_ids': [ + (0, 0, { + # Expense without foreign currency. + 'name': 'expense_1', + 'date': '2016-01-01', + 'product_id': self.product_a.id, + 'unit_amount': 1000.0, + 'tax_ids': [(6, 0, self.company_data['default_tax_purchase'].ids)], + 'analytic_account_id': self.analytic_account_1.id, + 'employee_id': self.expense_employee.id, + }), + (0, 0, { + # Expense with foreign currency (rate 1:3). + 'name': 'expense_1', + 'date': '2016-01-01', + 'product_id': self.product_b.id, + 'unit_amount': 1500.0, + 'tax_ids': [(6, 0, self.company_data['default_tax_purchase'].ids)], + 'analytic_account_id': self.analytic_account_2.id, + 'currency_id': self.currency_data['currency'].id, + 'employee_id': self.expense_employee.id, + }), + ], + }) + + # Check expense sheet values. + self.assertRecordValues(expense_sheet, [{'state': 'draft', 'total_amount': 1725.0}]) + + expense_sheet.action_submit_sheet() + expense_sheet.approve_expense_sheets() + expense_sheet.action_sheet_move_create() + + # Check expense sheet journal entry values. + self.assertRecordValues(expense_sheet.account_move_id.line_ids.sorted('balance'), [ + # Receivable line (company currency): + { + 'debit': 0.0, + 'credit': 1150.0, + 'amount_currency': -1150.0, + 'account_id': self.company_data['default_account_payable'].id, + 'product_id': False, + 'currency_id': self.company_data['currency'].id, + 'tax_line_id': False, + 'analytic_account_id': False, + }, + # Receivable line (foreign currency): + { + 'debit': 0.0, + 'credit': 862.5, + 'amount_currency': -1725.0, + 'account_id': self.company_data['default_account_payable'].id, + 'product_id': False, + 'currency_id': self.currency_data['currency'].id, + 'tax_line_id': False, + 'analytic_account_id': False, + }, + # Tax line (foreign currency): + { + 'debit': 112.5, + 'credit': 0.0, + 'amount_currency': 225.0, + 'account_id': self.company_data['default_account_tax_purchase'].id, + 'product_id': False, + 'currency_id': self.currency_data['currency'].id, + 'tax_line_id': self.company_data['default_tax_purchase'].id, + 'analytic_account_id': False, + }, + # Tax line (company currency): + { + 'debit': 150.0, + 'credit': 0.0, + 'amount_currency': 150.0, + 'account_id': self.company_data['default_account_tax_purchase'].id, + 'product_id': False, + 'currency_id': self.company_data['currency'].id, + 'tax_line_id': self.company_data['default_tax_purchase'].id, + 'analytic_account_id': False, + }, + # Product line (foreign currency): + { + 'debit': 750.0, + 'credit': 0.0, + 'amount_currency': 1500.0, + 'account_id': self.company_data['default_account_expense'].id, + 'product_id': self.product_b.id, + 'currency_id': self.currency_data['currency'].id, + 'tax_line_id': False, + 'analytic_account_id': self.analytic_account_2.id, + }, + # Product line (company currency): + { + 'debit': 1000.0, + 'credit': 0.0, + 'amount_currency': 1000.0, + 'account_id': self.company_data['default_account_expense'].id, + 'product_id': self.product_a.id, + 'currency_id': self.company_data['currency'].id, + 'tax_line_id': False, + 'analytic_account_id': self.analytic_account_1.id, + }, + ]) + + # Check expense analytic lines. + self.assertRecordValues(expense_sheet.account_move_id.line_ids.analytic_line_ids.sorted('amount'), [ + { + 'amount': -1000.0, + 'date': fields.Date.from_string('2017-01-01'), + 'account_id': self.analytic_account_1.id, + 'currency_id': self.company_data['currency'].id, + }, + { + 'amount': -750.0, + 'date': fields.Date.from_string('2017-01-01'), + 'account_id': self.analytic_account_2.id, + 'currency_id': self.company_data['currency'].id, + }, + ]) + + # actual test + self.assertEqual(expense_sheet.accounting_date, expense_sheet.account_move_id.date) + + expense = expense_sheet.expense_line_ids[0] + + ctx = {'active_model': 'hr.expense', 'active_ids': expense.ids} + change = self.env['hr.expense.change'].sudo(flag=True).with_context(ctx).create({}) + self.assertEqual(change.date, expense.date) + + change_date = '2018-01-01' + change.write({'date': change_date}) + + change.affect_change() + self.assertEqual(change_date, str(expense.date)) + self.assertEqual(change_date, str(expense_sheet.account_move_id.date)) diff --git a/hr_expense_change/wizard/__init__.py b/hr_expense_change/wizard/__init__.py new file mode 100644 index 00000000..72a7c8d3 --- /dev/null +++ b/hr_expense_change/wizard/__init__.py @@ -0,0 +1 @@ +from . import expense_change diff --git a/hr_expense_change/wizard/expense_change.py b/hr_expense_change/wizard/expense_change.py new file mode 100644 index 00000000..d3250352 --- /dev/null +++ b/hr_expense_change/wizard/expense_change.py @@ -0,0 +1,54 @@ +from odoo import api, fields, models, _ +from odoo.exceptions import UserError + + +class ExpenseChangeWizard(models.TransientModel): + _name = 'hr.expense.change' + _description = 'Expense Change' + + expense_id = fields.Many2one('hr.expense', string='Expense', readonly=True, required=True) + expense_company_id = fields.Many2one('res.company', related='expense_id.company_id') + date = fields.Date(string='Expense Date') + + @api.model + def default_get(self, fields): + rec = super(ExpenseChangeWizard, self).default_get(fields) + context = dict(self._context or {}) + active_model = context.get('active_model') + active_ids = context.get('active_ids') + + # Checks on context parameters + if not active_model or not active_ids: + raise UserError( + _("Programmation error: wizard action executed without active_model or active_ids in context.")) + if active_model != 'hr.expense': + raise UserError(_( + "Programmation error: the expected model for this action is 'hr.expense'. The provided one is '%d'.") % active_model) + + # Checks on received expense records + expense = self.env[active_model].browse(active_ids) + if len(expense) != 1: + raise UserError(_("Expense Change expects only one expense.")) + rec.update({ + 'expense_id': expense.id, + 'date': expense.date, + }) + return rec + + def _new_expense_vals(self): + vals = {} + if self.expense_id.date != self.date: + vals['date'] = self.date + return vals + + def affect_change(self): + self.ensure_one() + vals = self._new_expense_vals() + old_date = self.expense_id.date + if vals: + self.expense_id.write(vals) + if 'date' in vals and self.expense_id.sheet_id.account_move_id: + self.expense_id.sheet_id.account_move_id.write({'date': vals['date']}) + self.expense_id.sheet_id.account_move_id.line_ids\ + .filtered(lambda l: l.date_maturity == old_date).write({'date_maturity': vals['date']}) + return True diff --git a/hr_expense_change/wizard/expense_change_views.xml b/hr_expense_change/wizard/expense_change_views.xml new file mode 100644 index 00000000..95250bc6 --- /dev/null +++ b/hr_expense_change/wizard/expense_change_views.xml @@ -0,0 +1,65 @@ + + + + + Expense Change + hr.expense.change + +
+ + + + + + + + +
+
+ +
+
+ + + Expense Change Wizard + ir.actions.act_window + hr.expense.change + form + new + + + + + hr.expense.form.inherit + hr.expense + + + +