diff options
21 files changed, 1091 insertions, 330 deletions
diff --git a/addons/hr_expense/__init__.py b/addons/hr_expense/__init__.py index d84fb182e8f..11529f98733 100644 --- a/addons/hr_expense/__init__.py +++ b/addons/hr_expense/__init__.py @@ -2,5 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import models -import report import wizard diff --git a/addons/hr_expense/__openerp__.py b/addons/hr_expense/__openerp__.py index e82a1c3c8e0..ad04f65520b 100644 --- a/addons/hr_expense/__openerp__.py +++ b/addons/hr_expense/__openerp__.py @@ -32,9 +32,10 @@ This module also uses analytic accounting and is compatible with the invoice on 'data/hr_expense_data.xml', 'data/hr_expense_sequence.xml', 'wizard/hr_expense_refuse_reason.xml', + 'wizard/hr_expense_register_payment.xml', 'views/hr_expense_views.xml', 'security/ir_rule.xml', - 'views/report_expense.xml', + 'report/report_expense_sheet.xml', 'views/hr_dashboard.xml', 'views/hr_expense.xml', 'views/tour_views.xml', diff --git a/addons/hr_expense/data/hr_expense_data.xml b/addons/hr_expense/data/hr_expense_data.xml index 8af41a40832..808f1d98d9d 100644 --- a/addons/hr_expense/data/hr_expense_data.xml +++ b/addons/hr_expense/data/hr_expense_data.xml @@ -21,31 +21,30 @@ <!-- Expense-related subtypes for messaging / Chatter --> <record id="mt_expense_approved" model="mail.message.subtype"> <field name="name">Approved</field> - <field name="res_model">hr.expense</field> - <field name="default" eval="False"/> + <field name="res_model">hr.expense.sheet</field> + <field name="default" eval="True"/> <field name="description">Expense approved</field> </record> <record id="mt_expense_refused" model="mail.message.subtype"> <field name="name">Refused</field> - <field name="res_model">hr.expense</field> + <field name="res_model">hr.expense.sheet</field> <field name="default" eval="True"/> <field name="description">Expense refused</field> </record> <record id="mt_expense_confirmed" model="mail.message.subtype"> <field name="name">To Approve</field> - <field name="res_model">hr.expense</field> + <field name="res_model">hr.expense.sheet</field> <field name="description">Expense confirmed, waiting confirmation</field> - <field name="default" eval="True"/> + <field name="default" eval="False"/> </record> <!-- Department (Parent) related subtypes for messaging / Chatter --> <record id="mt_department_expense_confirmed" model="mail.message.subtype"> <field name="name">Expenses To Approve</field> <field name="res_model">hr.department</field> - <field name="default" eval="True"/> + <field name="default" eval="False"/> <field name="parent_id" eval="ref('mt_expense_confirmed')"/> <field name="relation_field">department_id</field> <field name="sequence" eval="10"/> </record> - </data> </odoo> diff --git a/addons/hr_expense/data/hr_expense_demo.xml b/addons/hr_expense/data/hr_expense_demo.xml index 229653b1808..bea4c5b26db 100644 --- a/addons/hr_expense/data/hr_expense_demo.xml +++ b/addons/hr_expense/data/hr_expense_demo.xml @@ -3,7 +3,21 @@ <data noupdate="1"> <record id="hr.employee_mit" model="hr.employee"> - <field name="address_home_id" ref="base.res_partner_address_5"/> + <field name="address_home_id" ref="base.res_partner_address_3"/> + </record> + + <record id="base.res_partner_address_fp" model="res.partner"> + <field name="name">Pieter Parter's Farm</field> + <field name="parent_id" eval="ref('base.partner_root')"/> + <field name="supplier">1</field> + <field name="street">Chaussée de Namur, 40</field> + <field name="zip">1367</field> + <field name="city">Grand-Rosière-Hottomont</field> + <field name="country_id" ref="base.be"/> + </record> + + <record id="hr.employee_fp" model="hr.employee"> + <field name="address_home_id" ref="base.res_partner_address_fp"/> </record> <record id="car_travel" model="product.product"> diff --git a/addons/hr_expense/data/web_tip_data.xml b/addons/hr_expense/data/web_tip_data.xml new file mode 100644 index 00000000000..e45b329cd89 --- /dev/null +++ b/addons/hr_expense/data/web_tip_data.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + + <record id="expense_tip_1" model="web.tip"> + <field name="title"></field> + <field name="description">Select the lines you want to submit to be reimbursed.</field> + <field name="action_id" ref="hr_expense.hr_expense_actions_all"/> + <field name="model">hr.expense</field> + <field name="mode">list</field> + <field name="trigger_selector">input.oe_list_record_selector:visible</field> + <field name="highlight_selector">input.oe_list_record_selector:visible</field> + <field name="placement">bottom</field> + </record> + + <record id="expense_tip_2" model="web.tip"> + <field name="title"></field> + <field name="description">Click on Action to submit multiple expenses to your manager.</field> + <field name="action_id" ref="hr_expense.hr_expense_actions_all"/> + <field name="model">hr.expense</field> + <field name="mode">list</field> + <field name="trigger_selector">div.oe_sidebar.btn-group:visible</field> + <field name="highlight_selector">div.oe_sidebar.btn-group:visible</field> + <field name="placement">bottom</field> + </record> + + <record id="expense_tip_3" model="web.tip"> + <field name="title"></field> + <field name="description">Use the messaging tool to log a note and attach a document to the expense. (e.g. scan of the bill)</field> + <field name="action_id" ref="hr_expense.hr_expense_actions_all"/> + <field name="model">hr.expense</field> + <field name="mode">form</field> + <field name="trigger_selector">div.btn-group.btn-group-sm:visible > button</field> + <field name="highlight_selector">.o_timeline_compose_post.btn.btn-primary:visible</field> + <field name="placement">bottom</field> + </record> + +</odoo> diff --git a/addons/hr_expense/models/account_move_line.py b/addons/hr_expense/models/account_move_line.py index f7e24f65277..c81d8d753ef 100644 --- a/addons/hr_expense/models/account_move_line.py +++ b/addons/hr_expense/models/account_move_line.py @@ -12,6 +12,6 @@ class AccountMoveLine(models.Model): res = super(AccountMoveLine, self).reconcile(writeoff_acc_id=writeoff_acc_id, writeoff_journal_id=writeoff_journal_id) account_move_ids = [l.move_id.id for l in self if float_compare(l.move_id.matched_percentage, 1, precision_digits=5) == 0] if account_move_ids: - expenses = self.env['hr.expense'].search([('account_move_id', 'in', account_move_ids)]) - expenses.paid_expenses() + expense_sheets = self.env['hr.expense.sheet'].search([('account_move_id', 'in', account_move_ids)]) + expense_sheets.set_to_paid() return res diff --git a/addons/hr_expense/models/hr_department.py b/addons/hr_expense/models/hr_department.py index 45433dde58e..a9db1671105 100644 --- a/addons/hr_expense/models/hr_department.py +++ b/addons/hr_expense/models/hr_department.py @@ -7,10 +7,10 @@ from openerp import fields, models class HrDepartment(models.Model): _inherit = 'hr.department' - def _compute_expense_to_approve(self): - expense_data = self.env['hr.expense'].read_group([('department_id', 'in', self.ids), ('state', '=', 'submit')], ['department_id'], ['department_id']) - result = dict((data['department_id'][0], data['department_id_count']) for data in expense_data) + def _compute_expense_sheets_to_approve(self): + expense_sheet_data = self.env['hr.expense.sheet'].read_group([('department_id', 'in', self.ids), ('state', '=', 'submit')], ['department_id'], ['department_id']) + result = dict((data['department_id'][0], data['department_id_count']) for data in expense_sheet_data) for department in self: - department.expense_to_approve_count = result.get(department.id, 0) + department.expense_sheets_to_approve_count = result.get(department.id, 0) - expense_to_approve_count = fields.Integer(compute='_compute_expense_to_approve', string='Expenses to Approve') + expense_sheets_to_approve_count = fields.Integer(compute='_compute_expense_sheets_to_approve', string='Expenses Reports to Approve') diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index 15fc0473d3e..f8d6ec8be3a 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -12,11 +12,10 @@ class HrExpense(models.Model): _name = "hr.expense" _inherit = ['mail.thread', 'ir.needaction_mixin'] _description = "Expense" - _order = "date desc" + _order = "date desc, id desc" - name = fields.Char(string='Expense Description', readonly=True, required=True, states={'draft': [('readonly', False)]}) + name = fields.Char(string='Expense Description', readonly=True, required=True, states={'draft': [('readonly', False)]}, default=fields.Date.context_today) date = fields.Date(readonly=True, states={'draft': [('readonly', False)]}, default=fields.Date.context_today, string="Date") - accounting_date = fields.Date(readonly=True, states={'draft': [('readonly', False)]}, string='Accounting Date') employee_id = fields.Many2one('hr.employee', string="Employee", required=True, readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1)) product_id = fields.Many2one('product.product', string='Product', readonly=True, states={'draft': [('readonly', False)]}, domain=[('can_be_expensed', '=', True)], required=True) product_uom_id = fields.Many2one('product.uom', string='Unit of Measure', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env['product.uom'].search([], limit=1, order='id')) @@ -29,22 +28,29 @@ class HrExpense(models.Model): currency_id = fields.Many2one('res.currency', string='Currency', readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env.user.company_id.currency_id) analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', states={'post': [('readonly', True)], 'done': [('readonly', True)]}, oldname='analytic_account') account_id = fields.Many2one('account.account', string='Account', states={'post': [('readonly', True)], 'done': [('readonly', True)]}, default=lambda self: self.env['ir.property'].get('property_account_expense_categ_id', 'product.category')) - department_id = fields.Many2one('hr.department', string='Department', states={'post': [('readonly', True)], 'done': [('readonly', True)]}) description = fields.Text() payment_mode = fields.Selection([("own_account", "Employee (to reimburse)"), ("company_account", "Company")], default='own_account', states={'done': [('readonly', True)], 'post': [('readonly', True)]}, string="Payment By") - journal_id = fields.Many2one('account.journal', string='Expense Journal', states={'done': [('readonly', True)], 'post': [('readonly', True)]}, default=lambda self: self.env['account.journal'].search([('type', '=', 'purchase')], limit=1), help="The journal used when the expense is done.") - bank_journal_id = fields.Many2one('account.journal', string='Bank Journal', states={'done': [('readonly', True)], 'post': [('readonly', True)]}, default=lambda self: self.env['account.journal'].search([('type', 'in', ['case', 'bank'])], limit=1), help="The payment method used when the expense is paid by the company.") - account_move_id = fields.Many2one('account.move', string='Journal Entry', copy=False, track_visibility="onchange") attachment_number = fields.Integer(compute='_compute_attachment_number', string='Number of Attachments') - state = fields.Selection([('draft', 'To Submit'), - ('submit', 'Submitted'), - ('approve', 'Approved'), - ('post', 'Waiting Payment'), - ('done', 'Paid'), - ('cancel', 'Refused') - ], string='Status', index=True, readonly=True, track_visibility='onchange', copy=False, default='draft', required=True, - help='When the expense request is created the status is \'To Submit\'.\n It is submitted by the employee and request is sent to manager, the status is \'Submitted\'.\ - \nIf the manager approve it, the status is \'Approved\'.\n If the accountant genrate the accounting entries for the expense request, the status is \'Waiting Payment\'.') + state = fields.Selection([('draft', 'To Report'), + ('reported', 'Reported'), + ('done', 'Posted'), + ('refused', 'Refused') + ], compute='_compute_state', default="draft", string='Status', index=True, readonly=True, copy=False, required=True, store=True, + help="Status of the expense.") + sheet_id = fields.Many2one('hr.expense.sheet', string="Expense Report", readonly=True) + reference = fields.Char(string="Bill Reference") + + @api.depends('sheet_id', 'sheet_id.account_move_id', 'sheet_id.state') + def _compute_state(self): + for expense in self: + if not expense.sheet_id: + expense.state = "draft" + elif expense.sheet_id.state == "cancel": + expense.state = "refused" + elif not expense.sheet_id.account_move_id: + expense.state = "reported" + else: + expense.state = "done" @api.depends('quantity', 'unit_amount', 'tax_ids', 'currency_id') def _compute_amount(self): @@ -60,6 +66,13 @@ class HrExpense(models.Model): for expense in self: expense.attachment_number = attachment.get(expense.id, 0) + @api.multi + def action_get_attachment_view(self): + res = self.env['ir.actions.act_window'].for_xml_id('base', 'action_attachment') + res['domain'] = [('res_model', '=', 'hr.expense'), ('res_id', 'in', self.ids)] + res['context'] = {'default_res_model': 'hr.expense', 'default_res_id': self.id} + return res + @api.onchange('product_id') def _onchange_product_id(self): if self.product_id: @@ -77,76 +90,31 @@ class HrExpense(models.Model): if self.product_id and self.product_uom_id.category_id != self.product_id.uom_id.category_id: raise UserError(_('Selected Unit of Measure does not belong to the same category as the product Unit of Measure')) - @api.onchange('employee_id') - def _onchange_employee_id(self): - self.department_id = self.employee_id.department_id - - def _add_followers(self): - user_ids = [] - employee = self.employee_id - if employee.user_id: - user_ids.append(employee.user_id.id) - if employee.parent_id: - user_ids.append(employee.parent_id.user_id.id) - if employee.department_id and employee.department_id.manager_id and employee.parent_id != employee.department_id.manager_id: - user_ids.append(employee.department_id.manager_id.user_id.id) - self.message_subscribe_users(user_ids=user_ids) - - @api.model - def create(self, vals): - hr_expense = super(HrExpense, self).create(vals) - if vals.get('employee_id'): - hr_expense._add_followers() - return hr_expense - - @api.multi - def write(self, vals): - res = super(HrExpense, self).write(vals) - if vals.get('employee_id'): - self._add_followers() - return res - @api.multi - def unlink(self): - if any(expense.state not in ['draft', 'cancel'] for expense in self): - raise UserError(_('You can only delete draft or refused expenses!')) - return super(HrExpense, self).unlink() + def view_sheet(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'view_mode': 'form', + 'res_model': 'hr.expense.sheet', + 'target': 'current', + 'res_id': self.sheet_id.id + } @api.multi def submit_expenses(self): if any(expense.state != 'draft' for expense in self): - raise UserError(_("You can only submit draft expenses!")) - self.write({'state': 'submit'}) - - @api.multi - def approve_expenses(self): - self.write({'state': 'approve'}) - - @api.multi - def refuse_expenses(self, reason): - self.write({'state': 'cancel'}) - if self.employee_id.user_id: - body = (_("Your Expense %s has been refused.<br/><ul class=o_timeline_tracking_value_list><li>Reason<span> : </span><span class=o_timeline_tracking_value>%s</span></li></ul>") % (self.name, reason)) - self.message_post(body=body, partner_ids=[self.employee_id.user_id.partner_id.id]) - - @api.multi - def paid_expenses(self): - self.write({'state': 'done'}) - - @api.multi - def reset_expenses(self): - return self.write({'state': 'draft'}) - - @api.multi - def _track_subtype(self, init_values): - self.ensure_one() - if 'state' in init_values and self.state == 'approve': - return 'hr_expense.mt_expense_approved' - elif 'state' in init_values and self.state == 'submit': - return 'hr_expense.mt_expense_confirmed' - elif 'state' in init_values and self.state == 'cancel': - return 'hr_expense.mt_expense_refused' - return super(HrExpense, self)._track_subtype(init_values) + raise UserError(_("You cannot report twice the same line!")) + return { + 'type': 'ir.actions.act_window', + 'view_mode': 'form', + 'res_model': 'hr.expense.sheet', + 'target': 'current', + 'context': { + 'default_expense_line_ids': [line.id for line in self], + 'default_employee_id': self[0].employee_id.id, + } + } def _prepare_move_line(self, line): ''' @@ -157,15 +125,16 @@ class HrExpense(models.Model): 'date_maturity': line.get('date_maturity'), 'partner_id': partner_id, 'name': line['name'][:64], + 'date': self.sheet_id.accounting_date, 'debit': line['price'] > 0 and line['price'], - 'credit': line['price'] < 0 and -line['price'], + 'credit': line['price'] < 0 and - line['price'], 'account_id': line['account_id'], 'analytic_line_ids': line.get('analytic_line_ids'), - 'amount_currency': line['price'] > 0 and abs(line.get('amount_currency')) or -abs(line.get('amount_currency')), + 'amount_currency': line['price'] > 0 and abs(line.get('amount_currency')) or - abs(line.get('amount_currency')), 'currency_id': line.get('currency_id'), 'tax_line_id': line.get('tax_line_id'), 'ref': line.get('ref'), - 'quantity': line.get('quantity',1.00), + 'quantity': line.get('quantity', 1.00), 'product_id': line.get('product_id'), 'product_uom_id': line.get('uom_id'), 'analytic_account_id': line.get('analytic_account_id'), @@ -203,22 +172,10 @@ class HrExpense(models.Model): ''' main function that is called when trying to create the accounting entries related to an expense ''' - if any(expense.state != 'approve' for expense in self): - raise UserError(_("You can only generate accounting entry for approved expense(s).")) - - if any(expense.employee_id != self[0].employee_id for expense in self): - raise UserError(_("Expenses must belong to the same Employee.")) - - if any(not expense.journal_id for expense in self): - raise UserError(_("Expenses must have an expense journal specified to generate accounting entries.")) - journal_dict = {} - maxdate = False for expense in self: - acc_date = expense.accounting_date or expense.date - if acc_date > maxdate: - maxdate = acc_date - jrn = expense.bank_journal_id if expense.payment_mode == 'company_account' else expense.journal_id + acc_date = expense.sheet_id.accounting_date or fields.Date.context_today(self) + jrn = expense.sheet_id.bank_journal_id if expense.payment_mode == 'company_account' else expense.sheet_id.journal_id journal_dict.setdefault(jrn, []) journal_dict[jrn].append(expense) @@ -227,7 +184,7 @@ class HrExpense(models.Model): move = self.env['account.move'].create({ 'journal_id': journal.id, 'company_id': self.env.user.company_id.id, - 'date': maxdate, + 'date': acc_date, }) for expense in expense_list: company_currency = expense.company_id.currency_id @@ -236,11 +193,11 @@ class HrExpense(models.Model): move_lines = expense._move_line_get() #create one more move line, a counterline for the total on payable account - total, total_currency, move_lines = expense._compute_expense_totals(company_currency, move_lines, maxdate) + total, total_currency, move_lines = expense._compute_expense_totals(company_currency, move_lines, acc_date) if expense.payment_mode == 'company_account': - if not expense.bank_journal_id.default_credit_account_id: - raise UserError(_("No credit account found for the %s journal, please configure one.") % (expense.bank_journal_id.name)) - emp_account = expense.bank_journal_id.default_credit_account_id.id + if not expense.sheet_id.bank_journal_id.default_credit_account_id: + raise UserError(_("No credit account found for the %s journal, please configure one.") % (expense.sheet_id.bank_journal_id.name)) + emp_account = expense.sheet_id.bank_journal_id.default_credit_account_id.id else: if not expense.employee_id.address_home_id: raise UserError(_("No Home Address found for the employee %s, please configure one.") % (expense.employee_id.name)) @@ -251,18 +208,18 @@ class HrExpense(models.Model): 'name': expense.employee_id.name, 'price': total, 'account_id': emp_account, - 'date_maturity': expense.accounting_date or expense.date, + 'date_maturity': acc_date, 'amount_currency': diff_currency_p and total_currency or False, 'currency_id': diff_currency_p and expense.currency_id.id or False, 'ref': expense.employee_id.address_home_id.ref or False }) #convert eml into an osv-valid format - lines = map(lambda x:(0, 0, expense._prepare_move_line(x)), move_lines) + lines = map(lambda x: (0, 0, expense._prepare_move_line(x)), move_lines) move.write({'line_ids': lines}) - expense.write({'account_move_id': move.id, 'state': 'post'}) + expense.sheet_id.write({'account_move_id': move.id}) if expense.payment_mode == 'company_account': - expense.paid_expenses() + expense.sheet_id.paid_expense_sheets() move.post() return True @@ -270,8 +227,8 @@ class HrExpense(models.Model): def _move_line_get(self): account_move = [] for expense in self: - if expense.account_id: - account = expense.account_id + if expense.sheet_id and expense.sheet_id.account_id: + account = expense.sheet_id.account_id elif expense.product_id: account = expense.product_id.product_tmpl_id._get_product_accounts()['expense'] if not account: @@ -291,7 +248,7 @@ class HrExpense(models.Model): 'product_id': expense.product_id.id, 'uom_id': expense.product_uom_id.id, 'analytic_account_id': expense.analytic_account_id.id, - } + } account_move.append(move_line) # Calculate tax lines and adjust base line @@ -317,3 +274,175 @@ class HrExpense(models.Model): res['domain'] = [('res_model', '=', 'hr.expense'), ('res_id', 'in', self.ids)] res['context'] = {'default_res_model': 'hr.expense', 'default_res_id': self.id} return res + +class HrExpenseSheet(models.Model): + + _name = "hr.expense.sheet" + _inherit = ['mail.thread', 'ir.needaction_mixin'] + _description = "Expense Report" + _order = "accounting_date desc" + + name = fields.Char(string='Expense Report Summary', required=True) + expense_line_ids = fields.One2many('hr.expense', 'sheet_id', string='Expense Lines', states={'done': [('readonly', True)], 'post': [('readonly', True)]}, copy=False) + state = fields.Selection([('submit', 'Submitted'), + ('approve', 'Approved'), + ('post', 'Posted'), + ('done', 'Paid'), + ('cancel', 'Refused') + ], string='Status', index=True, readonly=True, track_visibility='onchange', copy=False, default='submit', required=True, + help='Expense Report State') + employee_id = fields.Many2one('hr.employee', string="Employee", required=True, readonly=True, states={'submit': [('readonly', False)]}, default=lambda self: self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1)) + address_id = fields.Many2one('res.partner', string="Employee Home Address") + payment_mode = fields.Selection([("own_account", "Employee (to reimburse)"), ("company_account", "Company")], related='expense_line_ids.payment_mode', default='own_account', readonly=True, string="Payment By") + responsible_id = fields.Many2one('res.users', 'Validation By', readonly=True, copy=False, states={'submit': [('readonly', False)], 'submit': [('readonly', False)]}) + total_amount = fields.Float(string='Total Amount', store=True, compute='_compute_amount', digits=dp.get_precision('Account')) + company_id = fields.Many2one('res.company', string='Company', readonly=True, states={'submit': [('readonly', False)]}, default=lambda self: self.env.user.company_id) + currency_id = fields.Many2one('res.currency', string='Currency', readonly=True, states={'submit': [('readonly', False)]}, default=lambda self: self.env.user.company_id.currency_id) + attachment_number = fields.Integer(compute='_compute_attachment_number', string='Number of Attachments') + journal_id = fields.Many2one('account.journal', string='Expense Journal', states={'done': [('readonly', True)], 'post': [('readonly', True)]}, + default=lambda self: self.env['ir.model.data'].xmlid_to_object('hr_expense.hr_expense_account_journal') or self.env['account.journal'].search([('type', '=', 'purchase')], limit=1), + help="The journal used when the expense is done.") + bank_journal_id = fields.Many2one('account.journal', string='Bank Journal', states={'done': [('readonly', True)], 'post': [('readonly', True)]}, default=lambda self: self.env['account.journal'].search([('type', 'in', ['case', 'bank'])], limit=1), help="The payment method used when the expense is paid by the company.") + accounting_date = fields.Date(string="Accounting Date") + account_id = fields.Many2one('account.account', string='Account', states={'post': [('readonly', True)], 'done': [('readonly', True)]}, default=lambda self: self.env['ir.property'].get('property_account_payable_id', 'account.chart.template')) + account_move_id = fields.Many2one('account.move', string='Journal Entry', copy=False, track_visibility="onchange") + department_id = fields.Many2one('hr.department', string='Department', states={'post': [('readonly', True)], 'done': [('readonly', True)]}) + + @api.multi + def check_consistency(self): + if any(sheet.employee_id != self[0].employee_id for sheet in self): + raise UserError(_("Expenses must belong to the same Employee.")) + + expense_lines = self.mapped('expense_line_ids') + if expense_lines and any(expense.payment_mode != expense_lines[0].payment_mode for expense in expense_lines): + raise UserError(_("Expenses must have been paid by the same entity (Company or employee)")) + + @api.model + def create(self, vals): + sheet = super(HrExpenseSheet, self).create(vals) + self.check_consistency() + if vals.get('employee_id'): + sheet._add_followers() + return sheet + + @api.multi + def write(self, vals): + res = super(HrExpenseSheet, self).write(vals) + self.check_consistency() + if vals.get('employee_id'): + self._add_followers() + return res + + @api.multi + def set_to_paid(self): + self.write({'state': 'done'}) + + @api.multi + def _track_subtype(self, init_values): + self.ensure_one() + if 'state' in init_values and self.state == 'approve': + return 'hr_expense.mt_expense_approved' + elif 'state' in init_values and self.state == 'submit': + return 'hr_expense.mt_expense_confirmed' + elif 'state' in init_values and self.state == 'cancel': + return 'hr_expense.mt_expense_refused' + return super(HrExpenseSheet, self)._track_subtype(init_values) + + def _add_followers(self): + user_ids = [] + employee = self.employee_id + if employee.user_id: + user_ids.append(employee.user_id.id) + if employee.parent_id: + user_ids.append(employee.parent_id.user_id.id) + if employee.department_id and employee.department_id.manager_id and employee.parent_id != employee.department_id.manager_id: + user_ids.append(employee.department_id.manager_id.user_id.id) + self.message_subscribe_users(user_ids=user_ids) + + @api.onchange('employee_id') + def _onchange_employee_id(self): + self.address_id = self.employee_id.address_home_id + self.department_id = self.employee_id.department_id + self.account_id = self.employee_id.address_home_id.id and self.employee_id.address_home_id.property_account_payable_id.id + + @api.onchange('payment_mode') + def _onchange_payment_mode(self): + if self.payment_mode == 'own_account': + self.account_id = self.employee_id.address_home_id.property_account_payable_id.id + else: + self.account_id = self.bank_journal_id.default_credit_account_id.id + + @api.one + @api.depends('expense_line_ids') + def _compute_amount(self): + self.total_amount = sum(self.expense_line_ids.mapped('total_amount')) + + # FIXME: A 4 command is missing to explicitly declare the one2many relation + # between the sheet and the lines when using 'default_expense_line_ids':[ids] + # in the context. A fix from chm-odoo should come since + # several saas versions but sadly I had to add this hack to avoid this + # issue + @api.model + def _add_missing_default_values(self, values): + values = super(HrExpenseSheet, self)._add_missing_default_values(values) + if self.env.context.get('default_expense_line_ids', False): + lines_to_add = [] + for line in values.get('expense_line_ids', []): + if line[0] == 1: + lines_to_add.append([4, line[1], False]) + values['expense_line_ids'] = lines_to_add + values['expense_line_ids'] + return values + + @api.one + def _compute_attachment_number(self): + self.attachment_number = sum(self.expense_line_ids.mapped('attachment_number')) + + @api.multi + def refuse_expenses(self, reason): + self.write({'state': 'cancel'}) + for sheet in self: + body = (_("Your Expense %s has been refused.<br/><ul class=o_timeline_tracking_value_list><li>Reason<span> : </span><span class=o_timeline_tracking_value>%s</span></li></ul>") % (sheet.name, reason)) + sheet.message_post(body=body) + + @api.multi + def approve_expense_sheets(self): + self.message_post(body=_("The expense has been validated by %s") % (self.env.user.name)) + self.write({'state': 'approve', 'responsible_id': self.env.user.id}) + + @api.multi + def refuse_expense_sheets(self, reason): + self.write({'state': 'cancel'}) + for sheet in self: + body = (_("Your Expense %s has been refused.<br/><ul class=o_timeline_tracking_value_list><li>Reason<span> : </span><span class=o_timeline_tracking_value>%s</span></li></ul>") % (sheet.name, reason)) + sheet.message_post(body=body) + + @api.multi + def paid_expense_sheets(self): + self.write({'state': 'done'}) + + @api.multi + def reset_expense_sheets(self): + return self.write({'state': 'submit'}) + + @api.multi + def action_sheet_move_create(self): + if any(sheet.state != 'approve' for sheet in self): + raise UserError(_("You can only generate accounting entry for approved expense(s).")) + + if any(not sheet.journal_id for sheet in self): + raise UserError(_("Expenses must have an expense journal specified to generate accounting entries.")) + + res = self.mapped('expense_line_ids').action_move_create() + + if not self.accounting_date: + self.accounting_date = self.account_move_id.date + + self.write({'state': 'post'}) + return res + + @api.multi + def action_get_attachment_view(self): + res = self.env['ir.actions.act_window'].for_xml_id('base', 'action_attachment') + res['domain'] = [('res_model', '=', 'hr.expense'), ('res_id', 'in', self.expense_line_ids.ids)] + res['context'] = {'default_res_model': 'hr.expense.sheet', 'default_res_id': self.id} + return res diff --git a/addons/hr_expense/report/__init__.py b/addons/hr_expense/report/__init__.py deleted file mode 100644 index 7f74fc905a4..00000000000 --- a/addons/hr_expense/report/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import hr_expense_report
\ No newline at end of file diff --git a/addons/hr_expense/report/hr_expense_report.py b/addons/hr_expense/report/hr_expense_report.py deleted file mode 100644 index 1422abf9aa5..00000000000 --- a/addons/hr_expense/report/hr_expense_report.py +++ /dev/null @@ -1,60 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import time -from openerp.osv import osv -from openerp.tools.misc import formatLang -from openerp.tools.translate import _ -from openerp.report import report_sxw -from openerp.exceptions import UserError - - -class report_expense(report_sxw.rml_parse): - - def set_context(self, objects, data, ids, report_type=None): - res = super(report_expense, self).set_context(objects, data, ids, report_type=report_type) - - hr_expense_obj = self.pool.get('hr.expense') - - context = {'lang': self.pool.get('res.users').browse(self.cr, self.uid, self.uid).lang} - state_field = hr_expense_obj.fields_get(self.cr, self.uid, 'state', context=context)['state']['selection'] - state_dict = {} - for state_tuple in state_field: - state_dict[state_tuple[0]] = state_tuple[1] - - expenses_info = {} - expenses = hr_expense_obj.search(self.cr, self.uid, [('id', 'in', ids)], order="employee_id, currency_id, state, date") - for expense in hr_expense_obj.browse(self.cr, self.uid, expenses): - key = expense.employee_id.name + '-' + expense.currency_id.name + '-' + expense.state - if expenses_info.get(key): - expenses_info[key]['lines'] += expense - expenses_info[key]['total_amount'] += expense.total_amount - else: - expenses_info[key] = { - 'employee_name': expense.employee_id.name, - 'total_amount': expense.total_amount, - 'lines': expense, - 'currency': expense.currency_id, - 'validator_name': expense.employee_id.parent_id.name, - 'notes': [], - 'notes_index': {}, - 'state': state_dict[expense.state], - } - if expense.description: - index = len(expenses_info[key]['notes']) + 1 - expenses_info[key]['notes'].append({'description': expense.description, 'index':index}) - expenses_info[key]['notes_index'][expense.id] = index - - # Qweb for-each do not work on dict, so we send a list and we sort it by the name of the employee - # that way if we have two sheet for the same employee they will follow in the report - self.localcontext.update({ - 'get_expenses': lambda : [v for k,v in sorted(expenses_info.items())], - }) - return res - - -class report_hr_expense(osv.AbstractModel): - _name = 'report.hr_expense.report_expense' - _inherit = 'report.abstract_report' - _template = 'hr_expense.report_expense' - _wrapped_report_class = report_expense diff --git a/addons/hr_expense/report/report_expense_sheet.xml b/addons/hr_expense/report/report_expense_sheet.xml new file mode 100644 index 00000000000..52d26268668 --- /dev/null +++ b/addons/hr_expense/report/report_expense_sheet.xml @@ -0,0 +1,103 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <template id="report_expense_sheet"> + <t t-call="report.html_container"> + <t t-foreach="docs" t-as="o"> + <t t-call="report.external_layout"> + <div class="page"> + <h2>Expenses Report</h2> + + <div class="row mt32 mb32"> + <div class="col-xs-2"> + <strong>Employee:</strong> + <p t-field="o.employee_id.name"/> + </div> + <div class="col-xs-2"> + <strong>Date:</strong> + <p t-field="o.accounting_date"/> + </div> + <div class="col-xs-3"> + <strong>Description:</strong> + <p t-field="o.name"/> + </div> + <div class="col-xs-2"> + <strong>Validated By:</strong> + <p t-field="o.responsible_id"/> + </div> + <div class="col-xs-3"> + <strong>Payment By:</strong> + <p t-field="o.payment_mode"/> + </div> + </div> + + <table class="table table-condensed"> + <thead> + <tr> + <th>Date</th> + <th>Name</th> + <th class="text-center">Ref.</th> + <th>Unit Price</th> + <th>Taxes</th> + <th class="text-center">Qty</th> + <th class="text-right">Price</th> + </tr> + </thead> + <tbody> + <tr t-foreach="o.expense_line_ids" t-as="line"> + <td><span t-field="line.date"/></td> + <td> + <span t-field="line.name"/> + <span t-field="line.description"/><br/> + <span t-field="line.analytic_account_id.name"/> + </td> + <td style="text-center"> + <span t-field="line.reference"/> + </td> + <td> + <span t-field="line.unit_amount"/> + </td> + <td> + <span t-field="line.tax_ids"/> + </td> + <td class="text-center"> + <span t-field="line.quantity"/> + </td> + <td class="text-right"> + <span t-field="line.total_amount" + t-field-options='{"widget": "monetary", "display_currency":"o.currency_id"}'/> + </td> + </tr> + </tbody> + </table> + + <div class="row"> + <div class="col-xs-4 pull-right"> + <table class="table table-condensed"> + <tr class="border-black"> + <td><strong>Total</strong></td> + <td class="text-right"> + <span t-field="o.total_amount" + t-field-options='{"widget": "monetary", "display_currency": "o.currency_id"}'/> + </td> + </tr> + </table> + </div> + </div> + + <p>Certified honest and conform,<br/>(Date and signature).<br/><br/></p> + </div> + </t> + </t> + </t> + </template> + + <report + id="action_report_hr_expense_sheet" + string="Expenses Report" + model="hr.expense.sheet" + report_type="qweb-pdf" + name="hr_expense.report_expense_sheet" + file="hr_expense.report_expense_sheet" + /> + +</odoo>
\ No newline at end of file diff --git a/addons/hr_expense/security/ir.model.access.csv b/addons/hr_expense/security/ir.model.access.csv index c1d8735ddcf..0e60e20c017 100644 --- a/addons/hr_expense/security/ir.model.access.csv +++ b/addons/hr_expense/security/ir.model.access.csv @@ -2,6 +2,8 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_hr_expense_user,hr.expense.user,model_hr_expense,base.group_hr_user,1,1,1,1
access_hr_expense_employee,hr.expense.employee,model_hr_expense,base.group_user,1,1,1,1
+access_hr_expense_sheet_user,hr.expense.sheet.user,model_hr_expense_sheet,base.group_hr_user,1,1,1,1
+access_hr_expense_sheet_employee,hr.expense.sheet.employee,model_hr_expense_sheet,base.group_user,1,1,1,1
access_product_product_hr_expense_user,product.product.hr.expense.user,product.model_product_product,base.group_hr_user,1,1,1,1
access_product_template_hr_expense_user,product.template.hr.expense.user,product.model_product_template,base.group_hr_user,1,1,1,1
access_product_uom_hr_expense_user,product.uom.hr.expense.user,product.model_product_uom,base.group_hr_user,1,1,1,1
diff --git a/addons/hr_expense/tests/__init__.py b/addons/hr_expense/tests/__init__.py index e69de29bb2d..fb87f7b9832 100644 --- a/addons/hr_expense/tests/__init__.py +++ b/addons/hr_expense/tests/__init__.py @@ -0,0 +1 @@ +from . import test_expenses
\ No newline at end of file diff --git a/addons/hr_expense/tests/test_expenses.py b/addons/hr_expense/tests/test_expenses.py new file mode 100644 index 00000000000..c1b53160702 --- /dev/null +++ b/addons/hr_expense/tests/test_expenses.py @@ -0,0 +1,74 @@ +from openerp.tests.common import TransactionCase +from openerp import workflow + + +class TestCheckJournalEntry(TransactionCase): + """ + Check journal entries when the expense product is having tax which is tax included. + """ + + def setUp(self): + super(TestCheckJournalEntry, self).setUp() + cr, uid = self.cr, self.uid + self.expense_obj = self.registry('hr.expense.sheet') + self.exp_line_obj = self.registry('hr.expense') + self.product_obj = self.registry('product.product') + self.tax_obj = self.registry('account.tax') + _, self.product_id = self.registry("ir.model.data").get_object_reference(cr, uid, "hr_expense", "air_ticket") + self.employee = self.registry("ir.model.data").xmlid_to_object(cr, uid, "hr.employee_mit") + self.tax_id = self.tax_obj.create(cr, uid, { + 'name': 'Expense 10%', + 'amount': 10, + 'amount_type': 'percent', + 'type_tax_use': 'purchase', + 'price_include': True, + }) + self.product_obj.write(cr, uid, self.product_id, {'supplier_taxes_id': [(6, 0, [self.tax_id])]}) + # Create payable account for the expense + user_type_id = self.registry('ir.model.data').get_object_reference(cr, uid, 'account', 'data_account_type_payable')[1] + account_payable_id = self.registry('account.account').create(cr, uid, {'code': 'X1111', 'name': 'HR Expense - Test Payable Account', 'user_type_id': user_type_id, 'reconcile': True}) + self.employee.address_home_id.property_account_payable_id = account_payable_id + # Create expenses account for the expense + user_type_id = self.registry('ir.model.data').get_object_reference(cr, uid, 'account', 'data_account_type_expenses')[1] + account_expense_id = self.registry('account.account').create(cr, uid, {'code': 'X2120', 'name': 'HR Expense - Test Purchase Account', 'user_type_id': user_type_id}) + # Assign it to the air ticket product + self.registry('product.product').write(cr, uid, self.product_id, {'property_account_expense_id': account_expense_id}) + # Create Sale Journal + company_id = self.registry('ir.model.data').get_object_reference(cr, uid, 'base', 'main_company')[1] + self.registry('account.journal').create(cr, uid, {'name': 'Purchase Journal - Test', 'code': 'HRTPJ', 'type': 'purchase', 'company_id': company_id}) + self.expense_id = self.expense_obj.create(cr, uid, { + 'name': 'Expense for John Smith', + 'employee_id': self.employee.id, + }) + self.expense_line = self.exp_line_obj.create(cr, uid, { + 'name': 'Car Travel Expenses', + 'employee_id': self.employee.id, + 'product_id': self.product_id, + 'unit_amount': 700.00, + 'tax_ids': [(6, 0, [self.tax_id])], + 'sheet_id': self.expense_id, + }) + + def test_journal_entry(self): + cr, uid = self.cr, self.uid + self.expense = self.expense_obj.browse(cr, uid, self.expense_id) + #Submitted to Manager + self.assertEquals(self.expense.state, 'submit', 'Expense is not in Reported state') + #Approve + self.expense.approve_expense_sheets() + self.assertEquals(self.expense.state, 'approve', 'Expense is not in Approved state') + #Create Expense Entries + self.expense.action_sheet_move_create() + self.assertEquals(self.expense.state, 'post', 'Expense is not in Waiting Payment state') + self.assertTrue(self.expense.account_move_id.id, 'Expense Journal Entry is not created') + + # [(line.debit, line.credit, line.tax_line_id.id) for line in self.expense.expense_line_ids.account_move_id.line_ids] + # should git this result [(0.0, 700.0, False), (63.64, 0.0, 179), (636.36, 0.0, False)] + for line in self.expense.account_move_id.line_ids: + if line.credit: + self.assertAlmostEquals(line.credit, 700.00) + else: + if not line.tax_line_id.id == self.tax_id: + self.assertAlmostEquals(line.debit, 636.36) + else: + self.assertAlmostEquals(line.debit, 63.64)
\ No newline at end of file diff --git a/addons/hr_expense/views/hr_dashboard.xml b/addons/hr_expense/views/hr_dashboard.xml index bf4afa7943a..26d049bfd62 100644 --- a/addons/hr_expense/views/hr_dashboard.xml +++ b/addons/hr_expense/views/hr_dashboard.xml @@ -7,18 +7,18 @@ <field name="arch" type="xml"> <data> <xpath expr="//templates" position="before"> - <field name="expense_to_approve_count"/> + <field name="expense_sheets_to_approve_count"/> </xpath> <xpath expr="//div[contains(@class, 'o_kanban_primary_right')]" position="inside"> - <div t-if="record.expense_to_approve_count.raw_value > 0" class="row"> + <div t-if="record.expense_sheets_to_approve_count.raw_value > 0" class="row"> <div class="col-xs-9"> - <a name="%(hr_expense_action_from_department)d" type="action"> - Expenses + <a name="%(action_hr_expense_sheet_department_to_approve)d" type="action"> + Expense Reports </a> </div> <div class="col-xs-3"> - <t t-esc="record.expense_to_approve_count.raw_value"/> + <t t-esc="record.expense_sheets_to_approve_count.raw_value"/> </div> </div> </xpath> @@ -26,8 +26,8 @@ <xpath expr="//div[contains(@class, 'o_kanban_manage_to_approve')]" position="inside"> <div class="row"> <div class="col-xs-12 text-left"> - <a name="%(hr_expense_action_from_department)d" type="action"> - <t t-esc="record.expense_to_approve_count.raw_value or 0"/> Expenses + <a name="%(action_hr_expense_sheet_department_to_approve)d" type="action"> + <t t-esc="record.expense_sheets_to_approve_count.raw_value or 0"/> Expense Reports </a> </div> </div> @@ -36,8 +36,8 @@ <xpath expr="//div[contains(@class, 'o_kanban_manage_reports')]" position="inside"> <div class="row"> <div class="col-xs-12 text-left"> - <a name="%(action_hr_expense_report_filtered)d" type="action"> - Expenses + <a name="%(action_hr_expense_sheet_department_filtered)d" type="action"> + Expense Reports </a> </div> </div> diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml index cdbc60bfa3d..d2a3c55f084 100644 --- a/addons/hr_expense/views/hr_expense_views.xml +++ b/addons/hr_expense/views/hr_expense_views.xml @@ -1,41 +1,21 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> - - <!-- Expenses --> - <record id="view_expenses_tree" model="ir.ui.view"> <field name="name">hr.expense.tree</field> <field name="model">hr.expense</field> <field name="arch" type="xml"> - <tree string="Expenses" decoration-warning="state=='draft'" decoration-bf="message_unread == True"> + <tree string="Expenses" decoration-bf="message_unread == True"> + <field name="currency_id" invisible="True"/> + <field name="attachment_number" invisible="True"/> <field name="date"/> <field name="name"/> <field name="employee_id"/> + <field name="sheet_id" invisible="not context.get('show_report', False)"/> <field name="analytic_account_id" groups="analytic.group_analytic_accounting"/> - <field name="total_amount" sum="Total Amount" widget="monetary"/> - <field name="state"/> - <field name="message_unread" invisible="1"/> - <button name="submit_expenses" states="draft" string="Submit to Manager" type="object" icon="fa-check"/> - <button name="approve_expenses" states="submit" string="Click here to approve" type="object" icon="fa-check" groups="base.group_hr_manager"/> - <button name="%(hr_expense.hr_expense_refuse_wizard_action)d" states="submit" string="Click here to refuse" type="action" icon="fa-times" groups="base.group_hr_manager"/> - </tree> - </field> - </record> - - <record id="hr_expense_my_tree" model="ir.ui.view"> - <field name="name">my.hr.expense.tree</field> - <field name="model">hr.expense</field> - <field name="arch" type="xml"> - <tree string="Expenses" decoration-warning="state=='draft'" decoration-bf="message_unread == True"> - <field name="date"/> - <field name="name"/> - <field name="analytic_account_id" groups="analytic.group_analytic_accounting"/> - <field name="total_amount" sum="Total Amount" widget="monetary"/> + <field name="total_amount" sum="Total Amount" widget="monetary" options="{'currency_field': 'currency_id'}"/> <field name="state"/> <field name="message_unread" invisible="1"/> - <button name="submit_expenses" states="draft" string="Submit to Manager" type="object" icon="fa-check"/> - <button name="approve_expenses" states="submit" string="Click here to approve" type="object" icon="fa-check" groups="base.group_hr_manager"/> - <button name="%(hr_expense.hr_expense_refuse_wizard_action)d" states="submit" string="Click here to refuse" type="action" icon="fa-times" groups="base.group_hr_manager"/> + <button name="action_get_attachment_view" string="View Attached Documents" type="object" icon="fa-paperclip" attrs="{'invisible': [('attachment_number', '=', 0)]}"/> </tree> </field> </record> @@ -48,12 +28,8 @@ <form string="Expenses"> <header> <button name="submit_expenses" states="draft" string="Submit to Manager" type="object" class="oe_highlight"/> - <button name="approve_expenses" states="submit" string="Approve" type="object" groups="base.group_hr_user" class="oe_highlight"/> - <button name="%(hr_expense.hr_expense_refuse_wizard_action)d" states="submit" string="Refuse" type="action" groups="base.group_hr_user" /> - <button name="%(hr_expense.hr_expense_refuse_wizard_action)d" states="approve" string="Refuse" type="action" groups="account.group_account_user" /> - <button name="reset_expenses" states="cancel" string="Set to Draft" type="object"/> - <button name="action_move_create" states="approve" string="Post Journal Entries" type="object" groups="account.group_account_user" class="oe_highlight"/> - <field name="state" widget="statusbar" statusbar_visible="draft,submit,approve,post,done" statusbar_colors='{"submit":"blue","cancel":"red"}'/> + <field name="state" widget="statusbar" statusbar_visible="draft,reported,done" statusbar_colors='{"reported":"blue"}'/> + <button name="view_sheet" type="object" string="View Report" class="oe_highlight" attrs="{'invisible': [('state', '=','draft')]}"/> </header> <sheet> <div class="oe_button_box"> @@ -67,7 +43,7 @@ <div class="oe_title"> <label for="name"/> <h1> - <field name="name" placeholder="e.g. Business lunch with X"/> + <field name="name" placeholder="e.g. Business Trip NY"/> </h1> </div> <group> @@ -79,27 +55,21 @@ <field name="quantity" class="oe_inline"/> <field name="product_uom_id" widget="selection" class="oe_inline" groups="product.group_uom"/> </div> - <field name="tax_ids" widget="many2many_tags" groups="account.group_account_user"/> + <field name="tax_ids" domain="[('type_tax_use', '=', 'purchase')]" widget="many2many_tags" groups="account.group_no_one"/> </group><group> + <field name="reference"/> <field name="date"/> - <field name="accounting_date"/> + <field name="account_id"/> <field name="employee_id"/> + <field name="sheet_id" invisible="1"/> <field name="currency_id" groups="base.group_multi_currency"/> - <field name="department_id" invisible="1"/> <field name="analytic_account_id" groups="analytic.group_analytic_accounting"/> <field name="company_id" groups="base.group_multi_company"/> - <field name="journal_id" domain="[('type', '=', 'purchase')]" groups="account.group_account_user" - options="{'no_open': True, 'no_create': True}"/> - <field name="account_id" groups="account.group_account_user"/> - <field name="account_move_id" domain="[('type', '=', 'purchase'])]" readonly="1"/> </group><group> <field name="total_amount" widget='monetary' options="{'currency_field': 'currency_id'}"/> <label for="payment_mode"/> <div> <field name="payment_mode" widget="radio"/> - <field name="bank_journal_id" - attrs="{'invisible': [('payment_mode', '=', 'own_account')], 'required': [('payment_mode', '=', 'company_account')]}" domain="[('type', 'in', ['cash', 'bank'])]" - widget="selection"/> </div> </group> @@ -187,21 +157,21 @@ <field name="name" string="Expenses"/> <field name="date"/> <field name="employee_id"/> - <field name="department_id" string="Department" context="{'invisible_department': False}"/> <field name="product_id"/> <field name="analytic_account_id" groups="analytic.group_analytic_accounting"/> <separator /> - <filter domain="[('state', '=', 'draft')]" string="New" help="New Expense"/> - <filter domain="[('state', '=', 'submit')]" string="To Approve" name="submitted" help="Confirmed Expenses"/> - <filter domain="['|', ('state', '=', 'approve'), ('state', '=', 'post')]" string="To Pay" name="approved" help="Expenses to Invoice"/> + <filter domain="[('state', '=', 'draft')]" string="To Report" name="to_report" help="New Expense"/> + <filter domain="[('state', '=', 'report')]" string="Reported" name="submitted" help="Confirmed Expenses"/> + <filter domain="[('state', '=', 'refused')]" string="Refused" name="refused" help="Refused Expenses"/> + <filter domain="[('state', '!=', 'cancel')]" string="Not Refused" name="uncancelled" help="Actual expense sheets, not the refused ones"/> <separator /> <filter string="My Team Expenses" domain="[('employee_id.parent_id.user_id', '=', uid)]" groups="base.group_hr_manager" help="Expenses of Your Team Member"/> <filter string="My Expenses" domain="[('employee_id.user_id', '=', uid)]"/> <separator /> <filter string="New Mail" name="message_unread" domain="[('message_unread', '=', True)]"/> <group expand="0" string="Group By"> + <filter string="Report" domain="[]" context="{'group_by': 'sheet_id'}"/> <filter string="Employee" domain="[]" context="{'group_by': 'employee_id'}"/> - <filter string="Department" domain="[]" context="{'group_by': 'department_id'}" groups="hr.group_multi_departments"/> <filter string="Company" domain="[]" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/> <filter string="Product" domain="[]" context="{'group_by': 'product_id'}"/> <filter string="Analytic Account" domain="[]" context="{'group_by': 'analytic_account_id'}" groups="analytic.group_analytic_accounting"/> @@ -214,12 +184,43 @@ </field> </record> - <record id="expense_all" model="ir.actions.act_window"> + <record id="hr_expense_actions_all" model="ir.actions.act_window"> + <field name="name">All Expenses</field> + <field name="res_model">hr.expense</field> + <field name="view_mode">tree,kanban,form,graph,pivot</field> + <field name="search_view_id" ref="view_hr_expense_filter"/> + <field name="help" type="html"> + <p class="oe_view_nocontent_create"> + Click here to create new expenses. + </p><p> + Once you have created your expense, submit it to your manager who will validate it. + </p> + </field> + </record> + + <record id="hr_expense_actions_my_all" model="ir.actions.act_window"> <field name="name">My Expenses</field> <field name="res_model">hr.expense</field> <field name="view_mode">tree,kanban,form,graph,pivot</field> <field name="search_view_id" ref="view_hr_expense_filter"/> <field name="domain">[('employee_id.user_id', '=', uid)]</field> + <field name="context">{'show_report': True}</field> + <field name="help" type="html"> + <p class="oe_view_nocontent_create"> + Click here to create new expenses. + </p><p> + Once you have created your expense, submit it to your manager who will validate it. + </p> + </field> + </record> + + <record id="hr_expense_actions_my_unsubmitted" model="ir.actions.act_window"> + <field name="name">My Expenses</field> + <field name="res_model">hr.expense</field> + <field name="view_mode">tree,kanban,form,graph,pivot</field> + <field name="search_view_id" ref="view_hr_expense_filter"/> + <field name="domain">[('employee_id.user_id', '=', uid)]</field> + <field name="context">{'search_default_to_report': 1}</field> <field name="help" type="html"> <p class="oe_view_nocontent_create"> Click here to create new expenses. @@ -232,22 +233,22 @@ <record id="hr_expense_tree_action" model="ir.actions.act_window.view"> <field name="sequence" eval="0"/> <field name="view_mode">tree</field> - <field name="view_id" ref="hr_expense_my_tree"/> - <field name="act_window_id" ref="expense_all"/> + <field name="view_id" ref="view_expenses_tree"/> + <field name="act_window_id" ref="hr_expense_actions_all"/> </record> <record id="hr_expense_kanban_action" model="ir.actions.act_window.view"> <field name="sequence" eval="1"/> <field name="view_mode">kanban</field> <field name="view_id" ref="hr_expense_kanban_view"/> - <field name="act_window_id" ref="expense_all"/> + <field name="act_window_id" ref="hr_expense_actions_all"/> </record> <record id="hr_expense_form_action" model="ir.actions.act_window.view"> <field name="sequence" eval="2"/> <field name="view_mode">form</field> <field name="view_id" ref="hr_expense_form_view"/> - <field name="act_window_id" ref="expense_all"/> + <field name="act_window_id" ref="hr_expense_actions_all"/> </record> <record id="view_product_hr_expense_form" model="ir.ui.view"> @@ -293,105 +294,428 @@ </field> </record> - <record id="action_request_approve_expense" model="ir.actions.act_window"> - <field name="name">Expenses to Approve</field> + <record id="action_approved_expense" model="ir.actions.act_window"> + <field name="name">Unsubmitted Expenses</field> <field name="res_model">hr.expense</field> <field name="view_type">form</field> <field name="view_mode">tree,kanban,form,pivot,graph</field> - <field name="context">{'search_default_submitted': 1, 'needaction_menu_ref': 'hr_expense.menu_expense_all'}</field> - <field name="search_view_id" ref="view_hr_expense_filter"/> <field name="view_id" ref="view_expenses_tree"/> + <field name="domain">[]</field> + <field name="context">{"search_default_unsubmitted": 1}</field> + <field name="help" type="html"> + <p> + From here the accountant will be able to approve as well as refuse the expenses which are verified by the HR Manager. + </p> + </field> + </record> + + <record id="hr_expense_action" model="ir.actions.act_window"> + <field name="name">Expenses Analysis</field> + <field name="res_model">hr.expense</field> + <field name="view_type">form</field> + <field name="view_mode">pivot,graph</field> + <field name="context">{ + 'search_default_uncancelled': 1, + } + </field> + </record> + + <record id="view_hr_expense_sheet_tree" model="ir.ui.view"> + <field name="name">hr.expense.sheet.tree</field> + <field name="model">hr.expense.sheet</field> + <field name="arch" type="xml"> + <tree string="Expense Reports" decoration-warning="state=='draft'" decoration-bf="message_unread == True"> + <field name="accounting_date"/> + <field name="name" string="Expense Sheet"/> + <field name="employee_id"/> + <field name="total_amount" sum="Total Amount" widget="monetary" options="{'currency_field': 'currency_id'}"/> + <field name="state"/> + <field name="message_unread" invisible="1"/> + </tree> + </field> + </record> + + <record id="view_hr_expense_sheet_form" model="ir.ui.view"> + <field name="name">hr.expense.sheet.form</field> + <field name="model">hr.expense.sheet</field> + <field eval="25" name="priority"/> + <field name="arch" type="xml"> + <form string="Expense Reports"> + <header> + <button name="approve_expense_sheets" states="submit" string="Approve" type="object" groups="base.group_hr_user" class="oe_highlight"/> + <button name="action_sheet_move_create" states="approve" string="Post Journal Entries" type="object" groups="account.group_account_user" class="oe_highlight"/> + <button name="%(hr_expense.hr_expense_register_payment_wizard_action)d" type="action" string="Register Payment" class="oe_highlight" attrs="{'invisible': [('state', '!=', 'post')]}" context="{'default_amount': total_amount, 'partner_id': address_id}" groups="account.group_account_user"/> + <button name="reset_expense_sheets" states="cancel" string="Resubmit" type="object"/> + <button name="%(hr_expense.hr_expense_refuse_wizard_action)d" states="submit" string="Refuse" type="action" groups="base.group_hr_user" /> + <button name="%(hr_expense.hr_expense_refuse_wizard_action)d" states="approve" string="Refuse" type="action" groups="account.group_account_user"/> + <field name="state" widget="statusbar" statusbar_visible="draft,submit,approve,post,done" statusbar_colors='{"submit":"blue","cancel":"red"}'/> + </header> + <sheet> + <div class="oe_button_box"> + <button name="action_get_attachment_view" + class="oe_stat_button" + icon="fa-book" + type="object"> + <field name="attachment_number" widget="statinfo" string="Documents"/> + </button> + </div> + <div class="oe_title"> + <label for="name" class="oe_edit_only"/> + <h1> + <field name="name" placeholder="e.g. Business lunch with X"/> + </h1> + </div> + <group> + <group> + <field name="employee_id"/> + <field name="payment_mode"/> + <field name="responsible_id" invisible="1"/> + <field name="total_amount" invisible="1"/> + <field name="currency_id" invisible="1"/> + <field name="address_id" invisible="1"/> + <field name="department_id" invisible="1"/> + <field name="company_id" groups="base.group_multi_company"/> + </group> + <group> + <field name="journal_id" domain="[('type', '=', 'purchase')]" groups="account.group_account_user" options="{'no_open': True, 'no_create': True}" attrs="{'invisible': [('payment_mode', '!=', 'own_account')]}"/> + <field name="bank_journal_id" groups="account.group_account_user" options="{'no_open': True, 'no_create': True}" attrs="{'invisible': [('payment_mode', '!=', 'company_account')]}"/> + <field name="account_id" groups="account.group_account_user"/> + <field name="accounting_date" groups="account.group_account_user" attrs="{'invisible': [('state', 'not in', ['approve', 'post', 'done'])]}"/> + <field name="account_move_id" attrs="{'invisible': [('state', 'not in', ['post', 'done'])]}" groups="account.group_account_user"/> + </group> + </group> + <field name="expense_line_ids" nolabel="1" widget="many2many" domain="[('state', '=', 'draft')]" options="{'no_delete': True}"> + <tree> + <field name="date"/> + <field name="name"/> + <field name="state" invisible="1"/> + <field name="analytic_account_id" groups="analytic.group_analytic_accounting"/> + <field name="total_amount" sum="Total Amount" widget="monetary"/> + <field name="message_unread" invisible="1"/> + <field name="attachment_number" invisible="1"/> + <field name="tax_ids" widget="many2many_tags"/> + <field name="attachment_number" string=" "/> + <button name="action_get_attachment_view" string="View Attachments" type="object" icon="fa-paperclip"/> + </tree> + </field> + </sheet> + <div class="oe_chatter"> + <field name="message_follower_ids" widget="mail_followers"/> + <field name="message_ids" widget="mail_thread"/> + </div> + </form> + </field> + </record> + + <record id="view_hr_expense_sheet_kanban" model="ir.ui.view"> + <field name="name">hr.expense.sheet.kanban</field> + <field name="model">hr.expense.sheet</field> + <field name="arch" type="xml"> + <kanban class="o_kanban_mobile"> + <field name="name"/> + <field name="employee_id"/> + <field name="total_amount"/> + <field name="accounting_date"/> + <field name="state"/> + <templates> + <t t-name="kanban-box"> + <div t-attf-class="oe_kanban_card oe_kanban_global_click"> + <div class="row"> + <div class="col-xs-6"> + <strong><span><t t-esc="record.name.value"/></span></strong> + </div> + <div class="col-xs-6"> + <strong><span class="pull-right text-right"><t t-esc="record.total_amount.value"/></span></strong> + </div> + </div> + <div class="row"> + <div class="col-xs-6 text-muted"> + <span><t t-esc="record.employee_id.value"/> <t t-esc="record.date.value"/></span> + </div> + <div class="col-xs-6"> + <span class="pull-right text-right"> + <field name="state" widget="kanban_label_selection" options="{'classes': {'draft': 'default', 'submit': 'default', 'cancel': 'danger', 'post': 'warning', + 'done': 'success'}}"/> + </span> + </div> + </div> + </div> + </t> + </templates> + </kanban> + </field> + </record> + + <record id="view_hr_expense_sheet_pivot" model="ir.ui.view"> + <field name="name">hr.expense.sheet.pivot</field> + <field name="model">hr.expense.sheet</field> + <field name="arch" type="xml"> + <pivot string="Expenses Analysis" disable_linking="True"> + <field name="employee_id" type="row"/> + <field name="accounting_date" interval="month" type="col"/> + <field name="total_amount" type="measure"/> + </pivot> + </field> + </record> + + <record id="view_hr_expense_sheet_graph" model="ir.ui.view"> + <field name="name">hr.expense.sheet.graph</field> + <field name="model">hr.expense.sheet</field> + <field name="arch" type="xml"> + <graph string="Expenses Analysis"> + <field name="employee_id" type="col"/> + <field name="accounting_date" interval="month" type="row"/> + <field name="total_amount" type="measure"/> + </graph> + </field> + </record> + + + <record id="view_hr_expense_sheet_filter" model="ir.ui.view"> + <field name="name">hr.expense.sheet.filter</field> + <field name="model">hr.expense.sheet</field> + <field name="arch" type="xml"> + <search string="Expense"> + <field name="name" string="Expenses"/> + <field name="accounting_date"/> + <field name="state"/> + <field name="employee_id"/> + <field name="department_id" string="Department"/> + <separator /> + <filter domain="[('state', '=', 'submit')]" string="To Approve" name="submitted" help="Confirmed Expenses"/> + <filter domain="[('state', '=', 'approve')]" string="To Post" name="to_post" help="Approved Expenses"/> + <filter domain="['|', ('state', '=', 'approve'), ('state', '=', 'post')]" string="To Pay" name="approved" help="Expenses to Invoice"/> + <separator/> + <filter string="My Team Reports" domain="[('employee_id.parent_id.user_id', '=', uid)]" groups="base.group_hr_manager" help="Expenses of Your Team Member"/> + <filter string="My Reports" domain="[('employee_id.user_id', '=', uid)]"/> + <separator /> + <filter string="New Mail" name="message_unread" domain="[('message_unread', '=', True)]"/> + <group expand="0" string="Group By"> + <filter string="Employee" domain="[]" context="{'group_by': 'employee_id'}"/> + <filter string="Department" domain="[]" context="{'group_by': 'department_id'}" groups="hr.group_multi_departments"/> + <filter string="Company" domain="[]" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/> + <filter string="Expenses Month" domain="[]" context="{'group_by': 'accounting_date'}" help="Expenses by Month"/> + </group> + <separator /> + <filter domain="[('employee_id.active', '=', False)]" string="Former Employees" name="inactive" groups="base.group_hr_user,base.group_hr_manager"/> + </search> + </field> + </record> + + <record id="action_hr_expense_sheet_my_to_approve" model="ir.actions.act_window"> + <field name="name">My Reports</field> + <field name="res_model">hr.expense.sheet</field> + <field name="view_mode">tree,kanban,form,pivot,graph</field> + <field name="search_view_id" ref="view_hr_expense_sheet_filter"/> + <field name="domain">[('employee_id.user_id', '=', uid)]</field> + <field name="context">{'search_default_submitted': 1}</field> <field name="help" type="html"> <p class="oe_view_nocontent_create"> - Click here to create new expenses. + Click here to create a new expense report. </p><p> Once you have created your expense, submit it to your manager who will validate it. </p> </field> </record> - <record id="hr_expense_submit_action_server" model="ir.actions.server"> - <field name="name">Submit Expenses</field> - <field name="type">ir.actions.server</field> - <field name="model_id" ref="model_hr_expense"/> - <field name="state">code</field> - <field name="code"> - self.submit_expenses(cr, uid, context.get('active_ids', []), context=context) + <record id="action_hr_expense_sheet_my_all" model="ir.actions.act_window"> + <field name="name">My Reports</field> + <field name="res_model">hr.expense.sheet</field> + <field name="view_mode">tree,kanban,form,pivot,graph</field> + <field name="search_view_id" ref="view_hr_expense_sheet_filter"/> + <field name="domain">[('employee_id.user_id', '=', uid)]</field> + <field name="context">{}</field> + <field name="help" type="html"> + <p class="oe_view_nocontent_create"> + Click here to create a new expense report. + </p><p> + Once you have created your expense, submit it to your manager who will validate it. + </p> </field> </record> - <record id="action_hr_expense_submit" model="ir.values"> - <field name="name">action_hr_expense_submit</field> - <field name="action_id" ref="hr_expense_submit_action_server" /> - <field name="value" eval="'ir.actions.server,' + str(ref('hr_expense_submit_action_server'))" /> - <field name="key">action</field> - <field name="model_id" ref="model_hr_expense"/> - <field name="model">hr.expense</field> - <field name="key2">client_action_multi</field> + <record id="action_hr_expense_sheet_all_to_approve" model="ir.actions.act_window"> + <field name="name">My Reports</field> + <field name="res_model">hr.expense.sheet</field> + <field name="view_mode">tree,kanban,form,pivot,graph</field> + <field name="search_view_id" ref="view_hr_expense_sheet_filter"/> + <field name="domain">[]</field> + <field name="context">{'search_default_submitted': 1}</field> + <field name="help" type="html"> + <p class="oe_view_nocontent_create"> + Click here to create a new expense report. + </p><p> + Once you have created your expense, submit it to your manager who will validate it. + </p> + </field> </record> - <record id="hr_expense_entry_action_server" model="ir.actions.server"> - <field name="name">Generate Accounting Entries</field> - <field name="type">ir.actions.server</field> - <field name="model_id" ref="model_hr_expense"/> - <field name="state">code</field> - <field name="code"> - self.action_move_create(cr, uid, context.get('active_ids', []), context=context) + <record id="action_hr_expense_sheet_all_to_post" model="ir.actions.act_window"> + <field name="name">My Reports</field> + <field name="res_model">hr.expense.sheet</field> + <field name="view_mode">tree,kanban,form,pivot,graph</field> + <field name="search_view_id" ref="view_hr_expense_sheet_filter"/> + <field name="domain">[]</field> + <field name="context">{'search_default_to_post': 1}</field> + <field name="help" type="html"> + <p class="oe_view_nocontent_create"> + Click here to create a new expense report. + </p><p> + Once you have created your expense, submit it to your manager who will validate it. + </p> </field> </record> - <record id="action_hr_expense_account_entry" model="ir.values"> - <field name="name">action_hr_expense_account_entry</field> - <field name="action_id" ref="hr_expense_entry_action_server" /> - <field name="value" eval="'ir.actions.server,' + str(ref('hr_expense_entry_action_server'))" /> - <field name="key">action</field> - <field name="model_id" ref="model_hr_expense"/> - <field name="model">hr.expense</field> - <field name="key2">client_action_multi</field> + <record id="action_hr_expense_sheet_all_to_pay" model="ir.actions.act_window"> + <field name="name">My Reports</field> + <field name="res_model">hr.expense.sheet</field> + <field name="view_mode">tree,kanban,form,pivot,graph</field> + <field name="search_view_id" ref="view_hr_expense_sheet_filter"/> + <field name="domain">[]</field> + <field name="context">{'search_default_approved': 1}</field> + <field name="help" type="html"> + <p class="oe_view_nocontent_create"> + Click here to create a new expense report. + </p><p> + Once you have created your expense, submit it to your manager who will validate it. + </p> + </field> </record> - <menuitem id="menu_expense_approved" parent="account.menu_finance_payables" - action="action_approved_expense" sequence="15"/> - <menuitem id="menu_hr_expense_root" name="Expenses" sequence="100" web_icon="hr_expense,static/description/icon.png"/> - <menuitem id="menu_hr_expense_configuration" name="Configuration" parent="menu_hr_expense_root" - sequence="100"/> - <menuitem id="menu_hr_product" name="Expense Products" parent="menu_hr_expense_configuration" - action="hr_expense_product" groups="base.group_hr_manager"/> - <menuitem id="menu_expense_all" action="expense_all" name="My Expenses" parent="menu_hr_expense_root"/> - <menuitem id="menu_expense_to_approve" action="action_request_approve_expense" name="To Approve" parent="menu_hr_expense_root" groups="base.group_hr_user"/> + <record id="action_hr_expense_sheet_all_all" model="ir.actions.act_window"> + <field name="name">My Reports</field> + <field name="res_model">hr.expense.sheet</field> + <field name="view_mode">tree,kanban,form,pivot,graph</field> + <field name="search_view_id" ref="view_hr_expense_sheet_filter"/> + <field name="domain">[]</field> + <field name="context">{}</field> + <field name="help" type="html"> + <p class="oe_view_nocontent_create"> + Click here to create a new expense report. + </p><p> + Once you have created your expense, submit it to your manager who will validate it. + </p> + </field> + </record> - <record id="hr_expense_action_from_department" model="ir.actions.act_window"> - <field name="name">Expenses to Approve</field> - <field name="res_model">hr.expense</field> + <record id="action_request_approve_expense_sheet" model="ir.actions.act_window"> + <field name="name">Reports to Approve</field> + <field name="res_model">hr.expense.sheet</field> <field name="view_type">form</field> <field name="view_mode">tree,kanban,form,pivot,graph</field> - <field name="search_view_id" ref="view_hr_expense_filter"/> + <field name="context">{'search_default_submitted': 1, 'needaction_menu_ref': 'hr_expense.menu_hr_expense_all'}</field> + <field name="search_view_id" ref="view_hr_expense_sheet_filter"/> + <field name="view_id" ref="view_hr_expense_sheet_tree"/> + <field name="help" type="html"> + <p class="oe_view_nocontent_create"> + Click here to create new expenses. + </p><p> + Once you have created your expense, submit it to your manager who will validate it. + </p> + </field> + </record> + + <record id="action_request_to_post_expense_sheet" model="ir.actions.act_window"> + <field name="name">Reports to Post</field> + <field name="res_model">hr.expense.sheet</field> + <field name="view_type">form</field> + <field name="view_mode">tree,kanban,form,pivot,graph</field> + <field name="context">{'search_default_to_post': 1, 'needaction_menu_ref': 'hr_expense.menu_hr_expense_all'}</field> + <field name="search_view_id" ref="view_hr_expense_sheet_filter"/> + <field name="view_id" ref="view_hr_expense_sheet_tree"/> + <field name="help" type="html"> + <p class="oe_view_nocontent_create"> + Click here to create new expenses. + </p><p> + Once you have created your expense, submit it to your manager who will validate it. + </p> + </field> + </record> + + <record id="action_request_to_pay_expense_sheet" model="ir.actions.act_window"> + <field name="name">Reports to Pay</field> + <field name="res_model">hr.expense.sheet</field> + <field name="view_type">form</field> + <field name="view_mode">tree,kanban,form,pivot,graph</field> + <field name="context">{'search_default_approved': 1, 'needaction_menu_ref': 'hr_expense.menu_hr_expense_all'}</field> + <field name="search_view_id" ref="view_hr_expense_sheet_filter"/> + <field name="view_id" ref="view_hr_expense_sheet_tree"/> + <field name="help" type="html"> + <p class="oe_view_nocontent_create"> + Click here to create new expenses. + </p><p> + Once you have created your expense, submit it to your manager who will validate it. + </p> + </field> + </record> + + <record id="action_hr_expense_sheet_department_to_approve" model="ir.actions.act_window"> + <field name="name">Expense Reports to Approve</field> + <field name="res_model">hr.expense.sheet</field> + <field name="view_type">form</field> + <field name="view_mode">tree,kanban,form,pivot,graph</field> + <field name="search_view_id" ref="view_hr_expense_sheet_filter"/> <field name="context">{ 'search_default_submitted': 1, 'search_default_department_id': [active_id], - 'default_department_id': active_id} + 'default_department_id': active_id + } </field> </record> - <record id="hr_expense_action" model="ir.actions.act_window"> - <field name="name">Expenses Analysis</field> - <field name="res_model">hr.expense</field> + <record id="action_hr_expense_sheet_department_filtered" model="ir.actions.act_window"> + <field name="name">Expense Reports Analysis</field> + <field name="res_model">hr.expense.sheet</field> <field name="view_type">form</field> - <field name="view_mode">pivot,graph,tree,form,kanban</field> + <field name="view_mode">pivot,graph</field> + <field name="context">{ + 'search_default_department_id': [active_id], + 'default_department_id': active_id} + </field> </record> - <menuitem id="menu_hr_expense" name="Reports" sequence="99" parent="menu_hr_expense_root" action="hr_expense_action"/> + <record id="hr_expense_submit_action_server" model="ir.actions.server"> + <field name="name">Submit To Manager</field> + <field name="type">ir.actions.server</field> + <field name="model_id" ref="model_hr_expense"/> + <field name="state">code</field> + <field name="code"> + if context.get('active_model') == 'hr.expense' and context.get('active_ids'): + action = env['hr.expense'].browse(context['active_ids']).submit_expenses() + </field> + </record> + <record id="action_hr_expense_submit" model="ir.values"> + <field name="name">Submit To Manager</field> + <field name="model_id" ref="model_hr_expense"/> + <field name="model">hr.expense</field> + <field name="key2">client_action_multi</field> + <field eval="'ir.actions.server,%d'%hr_expense_submit_action_server" name="value"/> + </record> + + <menuitem id="menu_hr_expense_root" name="Expenses" sequence="100" web_icon="hr_expense,static/description/icon.png"/> + + <menuitem id="menu_hr_expense_my_expenses" name="My Expenses" sequence="10" parent="menu_hr_expense_root"/> + <menuitem id="menu_hr_expense_my_unsubmitted" name="Unsubmitted Expenses" sequence="1" parent="menu_hr_expense_my_expenses" action="hr_expense_actions_my_unsubmitted"/> + <menuitem id="menu_hr_expense_my_all" name="All Expenses" sequence="2" parent="menu_hr_expense_my_expenses" + action="hr_expense_actions_my_all"/> - <record id="action_hr_expense_report_filtered" model="ir.actions.act_window"> - <field name="name">Expenses Analysis</field> - <field name="res_model">hr.expense</field> - <field name="view_type">form</field> - <field name="view_mode">pivot,graph</field> - <field name="context">{ - 'search_default_department_id': [active_id], - 'default_department_id': active_id} - </field> - </record> + <menuitem id="menu_hr_expense_sheet_my_reports" name="My Reports" sequence="15" parent="menu_hr_expense_root"/> + <menuitem id="menu_hr_expense_sheet_my_to_approve" name="To Approve" sequence="1" parent="menu_hr_expense_sheet_my_reports" action="action_hr_expense_sheet_my_to_approve"/> + <menuitem id="menu_hr_expense_sheet_my_all" name="All Reports" sequence="2" parent="menu_hr_expense_sheet_my_reports" action="action_hr_expense_sheet_my_all"/> + <menuitem id="menu_hr_expense_sheet_all" name="All Reports" sequence="20" parent="menu_hr_expense_root"/> + <menuitem id="menu_hr_expense_sheet_all_to_approve" name="To Approve" sequence="1" parent="menu_hr_expense_sheet_all" + action="action_hr_expense_sheet_all_to_approve" groups="base.group_hr_user"/> + <menuitem id="menu_hr_expense_sheet_all_to_post" name="To Post" sequence="2" parent="menu_hr_expense_sheet_all" + action="action_hr_expense_sheet_all_to_post" groups="base.group_hr_manager"/> + <menuitem id="menu_hr_expense_sheet_all_to_pay" name="To Pay" sequence="3" parent="menu_hr_expense_sheet_all" + action="action_hr_expense_sheet_all_to_pay" groups="base.group_hr_manager"/> + <menuitem id="menu_hr_expense_sheet_all_all" name="All" sequence="4" parent="menu_hr_expense_sheet_all" + action="action_hr_expense_sheet_all_all" groups="base.group_hr_manager"/> + + <menuitem id="menu_hr_expense_configuration" name="Configuration" parent="menu_hr_expense_root" + sequence="100"/> + <menuitem id="menu_hr_product" name="Expense Products" parent="menu_hr_expense_configuration" + action="hr_expense_product" groups="base.group_hr_manager" sequence="10"/> </odoo> diff --git a/addons/hr_expense/wizard/__init__.py b/addons/hr_expense/wizard/__init__.py index bc8a5c091e0..faafe22de6b 100644 --- a/addons/hr_expense/wizard/__init__.py +++ b/addons/hr_expense/wizard/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- import hr_expense_refuse_reason +import hr_expense_register_payment diff --git a/addons/hr_expense/wizard/hr_expense_refuse_reason.py b/addons/hr_expense/wizard/hr_expense_refuse_reason.py index c317c026fe5..c223d73a1a1 100644 --- a/addons/hr_expense/wizard/hr_expense_refuse_reason.py +++ b/addons/hr_expense/wizard/hr_expense_refuse_reason.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp import api, fields, models +from odoo import api, fields, models class HrExpenseRefuseWizard(models.TransientModel): @@ -17,6 +17,6 @@ class HrExpenseRefuseWizard(models.TransientModel): context = dict(self._context or {}) active_ids = context.get('active_ids', []) - expense = self.env['hr.expense'].browse(active_ids) - expense.refuse_expenses(self.description) + expense_sheet = self.env['hr.expense.sheet'].browse(active_ids) + expense_sheet.refuse_expenses(self.description) return {'type': 'ir.actions.act_window_close'} diff --git a/addons/hr_expense/wizard/hr_expense_register_payment.py b/addons/hr_expense/wizard/hr_expense_register_payment.py new file mode 100644 index 00000000000..05f0038a7da --- /dev/null +++ b/addons/hr_expense/wizard/hr_expense_register_payment.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ +from openerp.exceptions import ValidationError + + +class HrExpenseRegisterPaymentWizard(models.TransientModel): + + _name = "hr.expense.register.payment.wizard" + _description = "Hr Expense Register Payment wizard" + + @api.model + def _default_partner_id(self): + context = dict(self._context or {}) + active_ids = context.get('active_ids', []) + expense_sheet = self.env['hr.expense.sheet'].browse(active_ids) + return expense_sheet.address_id.id or expense_sheet.employee_id.id and expense_sheet.employee_id.address_home_id.id + + partner_id = fields.Many2one('res.partner', string='Partner', required=True, default=_default_partner_id) + journal_id = fields.Many2one('account.journal', string='Payment Method', required=True, domain=[('type', 'in', ('bank', 'cash'))]) + company_id = fields.Many2one('res.company', related='journal_id.company_id', string='Company', readonly=True, required=True) + payment_method_id = fields.Many2one('account.payment.method', string='Payment Type', required=True) + amount = fields.Monetary(string='Payment Amount', required=True) + currency_id = fields.Many2one('res.currency', string='Currency', required=True, default=lambda self: self.env.user.company_id.currency_id) + payment_date = fields.Date(string='Payment Date', default=fields.Date.context_today, required=True) + communication = fields.Char(string='Memo') + hide_payment_method = fields.Boolean(compute='_compute_hide_payment_method', + help="Technical field used to hide the payment method if the selected journal has only one available which is 'manual'") + + @api.one + @api.constrains('amount') + def _check_amount(self): + if not self.amount > 0.0: + raise ValidationError('The payment amount must be strictly positive.') + + @api.one + @api.depends('journal_id') + def _compute_hide_payment_method(self): + if not self.journal_id: + self.hide_payment_method = True + return + journal_payment_methods = self.journal_id.outbound_payment_method_ids + self.hide_payment_method = len(journal_payment_methods) == 1 and journal_payment_methods[0].code == 'manual' + + @api.onchange('journal_id') + def _onchange_journal(self): + if self.journal_id: + # Set default payment method (we consider the first to be the default one) + payment_methods = self.journal_id.outbound_payment_method_ids + self.payment_method_id = payment_methods and payment_methods[0] or False + # Set payment method domain (restrict to methods enabled for the journal and to selected payment type) + return {'domain': {'payment_method_id': [('payment_type', '=', 'outbound'), ('id', 'in', payment_methods.ids)]}} + return {} + + @api.multi + def expense_post_payment(self): + self.ensure_one() + context = dict(self._context or {}) + active_ids = context.get('active_ids', []) + expense_sheet = self.env['hr.expense.sheet'].browse(active_ids) + + # Create payment and post it + payment = self.env['account.payment'].create({ + 'partner_type': 'supplier', + 'payment_type': 'outbound', + 'partner_id': self.partner_id.id, + 'journal_id': self.journal_id.id, + 'company_id': self.company_id.id, + 'payment_method_id': self.payment_method_id.id, + 'amount': self.amount, + 'currency_id': self.currency_id.id, + 'payment_date': self.payment_date, + 'communication': self.communication + }) + payment.post() + + # Log the payment in the chatter + body = (_("A payment of %s %s with the reference %s related to your expense %s has been made.") % (payment.amount, payment.currency_id.symbol, payment.name, expense_sheet.name)) + expense_sheet.message_post(body=body) + + return {'type': 'ir.actions.act_window_close'} diff --git a/addons/hr_expense/wizard/hr_expense_register_payment.xml b/addons/hr_expense/wizard/hr_expense_register_payment.xml new file mode 100644 index 00000000000..d8ae5342555 --- /dev/null +++ b/addons/hr_expense/wizard/hr_expense_register_payment.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <record id="hr_expense_register_payment_view_form" model="ir.ui.view"> + <field name="name">hr.expense.register.payment.wizard.form</field> + <field name="model">hr.expense.register.payment.wizard</field> + <field name="arch" type="xml"> + <form string="Register Payment"> + <sheet> + <field name="id" invisible="1"/> + <div class="oe_title"> + <h1>Draft Payment</h1> + </div> + <group> + <group> + <field name="partner_id" required="1" context="{'default_is_company': True, 'default_supplier': True}"/> + <field name="journal_id" widget="selection"/> + <field name="hide_payment_method" invisible="1"/> + <field name="payment_method_id" widget="radio" attrs="{'invisible': [('hide_payment_method', '=', True)]}"/> + <label for="amount"/> + <div name="amount_div" class="o_row"> + <field name="amount"/> + <field name="currency_id" options="{'no_create': True, 'no_open': True}" groups="base.group_multi_currency"/> + </div> + </group> + <group> + <field name="payment_date"/> + <field name="communication"/> + </group> + </group> + </sheet> + <footer> + <button string='Validate' name="expense_post_payment" type="object" class="btn-primary"/> + <button string="Cancel" class="btn-default" special="cancel"/> + </footer> + </form> + </field> + </record> + + <!-- Expenses --> + <record id="hr_expense_register_payment_wizard_action" model="ir.actions.act_window"> + <field name="name">Register Payment</field> + <field name="res_model">hr.expense.register.payment.wizard</field> + <field name="view_type">form</field> + <field name="view_mode">form</field> + <field name="view_id" ref="hr_expense_register_payment_view_form"/> + <field name="target">new</field> + <field name="context">{'default_payment_type': 'inbound'}</field> + <field name="domain">[('partner_type', '=', 'customer')]</field> + </record> + +</odoo>
\ No newline at end of file diff --git a/addons/sale_expense/tests/test_sale_expense.py b/addons/sale_expense/tests/test_sale_expense.py index a0da6ae48cb..0152f4df845 100644 --- a/addons/sale_expense/tests/test_sale_expense.py +++ b/addons/sale_expense/tests/test_sale_expense.py @@ -27,20 +27,24 @@ class TestSaleExpense(TestSale): account_payable = self.env['account.account'].create({'code': 'X1111', 'name': 'HR Expense - Test Payable Account', 'user_type_id': self.env.ref('account.data_account_type_payable').id, 'reconcile': True}) employee = self.env['hr.employee'].create({'name': 'Test employee', 'user_id': self.user.id, 'address_home_id': self.user.partner_id.id}) self.user.partner_id.property_account_payable_id = account_payable.id + # Submit to Manager + sheet = self.env['hr.expense.sheet'].create({ + 'name': 'Expense for John Smith', + 'employee_id': employee.id, + 'journal_id': journal.id, + }) exp = self.env['hr.expense'].create({ 'name': 'Air Travel', 'product_id': prod_exp_1.id, 'analytic_account_id': so.project_id.id, 'unit_amount': 621.54, - 'journal_id': journal.id, 'employee_id': employee.id, + 'sheet_id': sheet.id }) - # Submit to Manager - exp.submit_expenses() # Approve - exp.approve_expenses() + sheet.approve_expense_sheets() # Create Expense Entries - exp.action_move_create() + sheet.action_sheet_move_create() # expense should now be in sales order self.assertTrue(prod_exp_1 in map(lambda so: so.product_id, so.order_line), 'Sale Expense: expense product should be in so') sol = so.order_line.filtered(lambda sol: sol.product_id.id == prod_exp_1.id) @@ -50,6 +54,12 @@ class TestSaleExpense(TestSale): # create some expense and validate it (expense at sales price) init_price = so.amount_total prod_exp_2 = self.env.ref('hr_expense.car_travel') + # Submit to Manager + sheet = self.env['hr.expense.sheet'].create({ + 'name': 'Expense for John Smith', + 'employee_id': employee.id, + 'journal_id': journal.id, + }) exp = self.env['hr.expense'].create({ 'name': 'Car Travel', 'product_id': prod_exp_2.id, @@ -57,15 +67,13 @@ class TestSaleExpense(TestSale): 'product_uom_id': self.env.ref('product.product_uom_km').id, 'unit_amount': 0.15, 'quantity': 100, - 'journal_id': journal.id, 'employee_id': employee.id, + 'sheet_id': sheet.id }) - # Submit to Manager - exp.submit_expenses() # Approve - exp.approve_expenses() + sheet.approve_expense_sheets() # Create Expense Entries - exp.action_move_create() + sheet.action_sheet_move_create() # expense should now be in sales order self.assertTrue(prod_exp_2 in map(lambda so: so.product_id, so.order_line), 'Sale Expense: expense product should be in so') sol = so.order_line.filtered(lambda sol: sol.product_id.id == prod_exp_2.id) |
