# Part of Odoo. See LICENSE file for full copyright and licensing details. from markupsafe import Markup from odoo.addons.iap.tools import iap_tools from odoo import api, fields, models, _ from odoo.tools import is_html_empty from odoo.tools.misc import DEFAULT_SERVER_DATE_FORMAT import time OCR_VERSION = 133 class HrExpense(models.Model): _name = 'hr.expense' _inherit = ['extract.mixin', 'hr.expense'] # We want to see the records that are just processed by OCR at the top of the list _order = "extract_state_processed desc, date desc, id desc" sample = fields.Boolean(help='Expenses created from sample receipt') @api.depends('state') def _compute_is_in_extractable_state(self): for expense in self: expense.is_in_extractable_state = expense.state == 'draft' and not expense.split_expense_origin_id @api.depends('extract_state', 'state') def _compute_extract_state_processed(self): # Overrides 'iap_extract' for expense in self: expense.extract_state_processed = expense.extract_state == 'waiting_extraction' and expense.state == 'draft' @api.model def _contact_iap_extract(self, pathinfo, params): params['version'] = OCR_VERSION params['account_token'] = self._get_iap_account().sudo().account_token endpoint = self.env['ir.config_parameter'].sudo().get_param('iap_extract_endpoint', 'https://extract.api.odoo.com') return iap_tools.iap_jsonrpc(endpoint + '/api/extract/expense/2/' + pathinfo, params=params) def _filter_preprocess_skipped_ocr(self): """ If the hr_expense_stripe module is installed we want to skip the OCR """ if 'is_card_expense' not in self._fields: return self stripe_expenses = self.filtered('is_card_expense') stripe_expenses.write({'extract_state': 'done'}) for expense in stripe_expenses: expense.message_post(body=self.env._("Receipt not digitized: the expense already includes Stripe data.")) return self - stripe_expenses def _upload_to_extract(self): to_extract = self._filter_preprocess_skipped_ocr() if to_extract: super(HrExpense, to_extract)._upload_to_extract() def _autosend_for_digitization(self): to_extract = self.filtered(lambda e: e.company_id.expense_extract_show_ocr_option_selection == 'auto_send' and e.extract_can_show_send_button )._filter_preprocess_skipped_ocr() if to_extract: to_extract._send_batch_for_digitization() def _message_set_main_attachment_id(self, attachments, force=False, filter_xml=True): super()._message_set_main_attachment_id(attachments, force=force, filter_xml=filter_xml) if not self.sample and attachments: self._autosend_for_digitization() def _get_validation(self, field): text_to_send = {} if field == "total": text_to_send["content"] = self.price_unit elif field == "date": text_to_send["content"] = str(self.date) if self.date else False elif field == "description": text_to_send["content"] = self.name elif field == "currency": text_to_send["content"] = self.currency_id.name return text_to_send def action_submit(self, **kwargs): self._validate_ocr() res = super().action_submit(**kwargs) return res def _fill_document_with_results(self, ocr_results): if ocr_results is not None and self.state == "draft": vals = {} description_ocr = self._get_ocr_selected_value(ocr_results, 'description', "") total_ocr = self._get_ocr_selected_value(ocr_results, 'total', 0.0) date_ocr = self._get_ocr_selected_value(ocr_results, 'date', fields.Date.context_today(self).strftime(DEFAULT_SERVER_DATE_FORMAT)) currency_ocr = self._get_ocr_selected_value(ocr_results, 'currency', self.env.company.currency_id.name) # We need to set the user to ensure it will be translated in the same language user_id = self.employee_id.user_id if self.employee_id.user_id else self.env.user default_receipt_name = self.env['hr.expense'].with_context(lang=user_id.lang)._get_untitled_expense_name("").strip() if default_receipt_name in self.name: predicted_product_id = self._predict_product(description_ocr) if predicted_product_id: vals['product_id'] = predicted_product_id or self.product_id.id vals['name'] = description_ocr # We need to set the name after the product change as changing the product may change the name context_create_date = fields.Date.context_today(self, self.create_date) if not self.date or self.date == context_create_date: vals['date'] = date_ocr product_id = vals.get('product_id', self.product_id.id) product_price = product_id and self.env['product.product'].with_company(self.company_id).browse(product_id).standard_price if product_price: vals['price_unit'] = product_price vals['total_amount_currency'] = product_price vals['total_amount'] = product_price else: vals['total_amount_currency'] = total_ocr vals['quantity'] = 1 # Always the case for expense that are not using a flat rate vals['price_unit'] = total_ocr current_currency = self.currency_id if not self.currency_id or self.currency_id == self.env.company.currency_id: for comparison in ['=ilike', 'ilike']: matched_currency = self.env["res.currency"].with_context(active_test=False).search([ '|', '|', ('currency_unit_label', comparison, currency_ocr), ('name', comparison, currency_ocr), ('symbol', comparison, currency_ocr), ]) if len(matched_currency) == 1: current_currency = matched_currency vals['currency_id'] = current_currency.id if current_currency and current_currency != self.company_currency_id: vals['total_amount'] = current_currency._convert( total_ocr, self.company_currency_id, company=self.company_id, date=vals.get('date', self.date), ) else: vals['total_amount'] = total_ocr self.write(vals) @api.model def get_empty_list_help(self, help_message): if self.env.user.has_group('base.group_user'): has_expenses = bool(self.search_count([('employee_id', 'in', self.env.user.employee_ids.ids)])) if is_html_empty(help_message): help_message = Markup("""
""").format(title=_("Upload or drop an expense receipt")) # add hint for extract if not already present and user might now have already used it extract_txt = _("try a sample receipt") if not has_expenses and extract_txt not in help_message: action_id = self.env.ref('hr_expense_extract.action_expense_sample_receipt').id help_message += Markup( "" ) % { 'action_id': action_id, 'extract_txt': extract_txt, } return super().get_empty_list_help(help_message) def _get_ocr_module_name(self): return 'hr_expense_extract' def _get_ocr_option_can_extract(self): ocr_option = self.env.company.expense_extract_show_ocr_option_selection return ocr_option and ocr_option != 'no_send' def _get_validation_fields(self): return ['total', 'date', 'description', 'currency'] def _get_user_error_invalid_state_message(self): return _("You cannot send a expense that is not in draft state!") def _upload_to_extract_success_callback(self): super()._upload_to_extract_success_callback() if 'isMobile' in self.env.context and self.env.context['isMobile']: for record in self: timer = 0 while record.extract_state != 'waiting_validation' and timer < 10: timer += 1 time.sleep(1) record._check_ocr_status() @api.ondelete(at_uninstall=False) def _unlink_except_approved(self): super(HrExpense, self - self.filtered('sample'))._unlink_except_approved() def _do_approve(self, check=True): # If we're dealing with sample expenses (demo data) then we should NEVER create any account.move # EXTENDS account today = fields.Date.context_today(self) samples = self.filtered('sample') for expense in samples.filtered(lambda s: s.state in {'submitted', 'draft'}): expense.write( { 'approval_state': 'approved', 'manager_id': (expense.manager_id or self.env.user).id, 'approval_date': today, } ) expense.update_activities_and_mails() return super(HrExpense, self - samples)._do_approve(check) def action_post(self): # Add an override for sample expenses # EXTENDS account samples = self.filtered('sample') for expense in samples: expense.state = 'paid' return super(HrExpense, self - samples).action_post()