summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRohan Patel <rop@odoo.com>2018-01-10 12:54:39 +0000
committerYannick Tivisse <yti@odoo.com>2019-01-24 13:08:27 +0000
commit4ba8f51d87be394988e6e2889cf03f351afea1a0 (patch)
tree0d7a6c49f3c494789d0c99a4cb4ae7bb9b24bc98
parentaf1728fddf61c0bc35c4fd7a9c139ab7b1feefd0 (diff)
[IMP] hr_expense: improve mail gateaway
1/ Send mail notification if expense is registered successfully Before this commit, when user submits the expense via email, user is not getting information about whether expense is registered or not. After this commit user will get a confirmation email. Few other usability improvements, - Improved tooltip for the field 'default_code' in expense's form and in the 'Emails' config setting. - Added product's default code in product's kanban view - Filtered uom based on category of product's uom. 2/ Better parsing of expense mail subject Criteria for matching expense product from email subject is changed, - The product code should be the first word of the subject. - Only expense products are allowed, previously there was no filter and user was able to select any product in expense via mail. Currency support is added, - Now user can specify currency symbols and ISO code in expense mail subject. - If subject contains multiple numbers in that case, number with currency will get higher priory, and it selected as an amount in expense. - If multi currency is active, user can use any active currency symbol/ISO code in mail subject. Respective currency will be selected in registered expense. - If multi currency is not active, user can only pass company currency. Other currencies are ignored and expense will be registered as default company currency - If user pass non active currency in subject then that currency will be ignored and expense will be registered as default company currency Examples: PROD_CODE 2 foo $1205.91 baz Added python test cases for mail subject parsing. Related to task #35093 closes #22168 Co-authored-by: Lucas Lefèvre <lul@odoo.com>
-rw-r--r--addons/hr_expense/__manifest__.py1
-rw-r--r--addons/hr_expense/data/hr_expense_data.xml15
-rw-r--r--addons/hr_expense/data/hr_expense_demo.xml10
-rw-r--r--addons/hr_expense/data/mail_data.xml45
-rw-r--r--addons/hr_expense/models/hr_expense.py149
-rw-r--r--addons/hr_expense/tests/__init__.py1
-rw-r--r--addons/hr_expense/tests/test_expenses.py23
-rw-r--r--addons/hr_expense/tests/test_expenses_mail_subjects.py104
-rw-r--r--addons/hr_expense/views/hr_expense_views.xml6
-rw-r--r--addons/hr_expense/views/res_config_settings_views.xml2
-rw-r--r--addons/product/views/product_views.xml5
-rw-r--r--addons/sale_expense/__manifest__.py1
-rw-r--r--addons/sale_expense/data/sale_expense_data.xml10
-rw-r--r--addons/sale_expense/data/sale_expense_demo.xml5
14 files changed, 303 insertions, 74 deletions
diff --git a/addons/hr_expense/__manifest__.py b/addons/hr_expense/__manifest__.py
index 5a444c69b7f..1bbd090d7a8 100644
--- a/addons/hr_expense/__manifest__.py
+++ b/addons/hr_expense/__manifest__.py
@@ -31,7 +31,6 @@ This module also uses analytic accounting and is compatible with the invoice on
'security/hr_expense_security.xml',
'security/ir.model.access.csv',
'data/mail_data.xml',
- 'data/hr_expense_data.xml',
'data/hr_expense_sequence.xml',
'wizard/hr_expense_refuse_reason_views.xml',
'wizard/hr_expense_sheet_register_payment.xml',
diff --git a/addons/hr_expense/data/hr_expense_data.xml b/addons/hr_expense/data/hr_expense_data.xml
deleted file mode 100644
index 5a733714722..00000000000
--- a/addons/hr_expense/data/hr_expense_data.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
- <data noupdate="1">
-
- <record id="product_product_fixed_cost" model="product.product">
- <field name="name">Expenses</field>
- <field name="list_price">0.0</field>
- <field name="standard_price">1.0</field>
- <field name="type">service</field>
- <field name="default_code">EXP_GEN</field>
- <field name="categ_id" ref="product.cat_expense"/>
- <field name="can_be_expensed" eval="True"/>
- </record>
- </data>
-</odoo>
diff --git a/addons/hr_expense/data/hr_expense_demo.xml b/addons/hr_expense/data/hr_expense_demo.xml
index 975013c13db..b3ea6145ab7 100644
--- a/addons/hr_expense/data/hr_expense_demo.xml
+++ b/addons/hr_expense/data/hr_expense_demo.xml
@@ -59,6 +59,16 @@
<!-- ++++++++++++++ Expense sheet for Admin ++++++++++++++-->
+ <record id="product_product_fixed_cost" model="product.product">
+ <field name="name">Expenses</field>
+ <field name="list_price">0.0</field>
+ <field name="standard_price">1.0</field>
+ <field name="type">service</field>
+ <field name="default_code">EXP_GEN</field>
+ <field name="categ_id" ref="product.cat_expense"/>
+ <field name="can_be_expensed" eval="True"/>
+ </record>
+
<record id="screen_expense" model="hr.expense">
<field name="name">Screen</field>
<field name="employee_id" ref="hr.employee_admin"/>
diff --git a/addons/hr_expense/data/mail_data.xml b/addons/hr_expense/data/mail_data.xml
index bc095de21ca..8d216a0ddbf 100644
--- a/addons/hr_expense/data/mail_data.xml
+++ b/addons/hr_expense/data/mail_data.xml
@@ -44,5 +44,50 @@
</ul>
</template>
+ <template id="hr_expense_template_register">
+ <p>Dear <t t-esc="expense.employee_id.name"/>,</p>
+ <p>
+ Your expense has been successfully registered.
+ <t t-if="expense.employee_id.user_id">
+ You can now submit it to the manager from the following link.
+ </t>
+ </p>
+ <p t-if="expense.product_id">
+ Product: <t t-esc="expense.product_id.name"/>
+ </p>
+ <div t-else="">
+ <p>Product: not found</p>
+ <p>The first word of the email subject did not correspond to any product code. You'll have to set the product manually on the expense.</p>
+ </div>
+ <p>
+ Price: <t t-esc="expense.unit_amount"/><t t-esc="expense.currency_id.symbol"/>
+ </p>
+ <p t-if="expense.employee_id.user_id">
+ <br/>
+ <a t-att-href="'/web#id=%s&amp;model=hr.expense&amp;view_type=form' % (expense.id)" style="background-color: #9E588B; margin-top: 10px; padding: 10px; text-decoration: none; color: #fff; border-radius: 5px; font-size: 16px;">View Expense</a>
+ </p>
+ </template>
+
+ <template id="hr_expense_template_register_no_user">
+ <div style="background:#F0F0F0;color:#515166;padding:10px 0px;font-family:Arial,Helvetica,sans-serif;font-size:14px;">
+ <table style="width:600px;margin:5px auto;">
+ <tbody>
+ <tr>
+ <td><a href="/"><img src="/web/binary/company_logo" style="vertical-align:baseline;max-width:100px;" /></a></td>
+ </tr>
+ </tbody>
+ </table>
+ <table style="width:600px;margin:0px auto;background:white;border:1px solid #e1e1e1;">
+ <tbody>
+ <tr>
+ <td style="padding:15px 20px 10px 20px;">
+ <t t-call="hr_expense.hr_expense_template_register"/>
+ <p style="color:#9E588B;">Powered by <a target="_blank" href="https://www.odoo.com">Odoo</a>.</p>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </template>
</data>
</odoo>
diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py
index bc4ab4e4d99..1547ef369e4 100644
--- a/addons/hr_expense/models/hr_expense.py
+++ b/addons/hr_expense/models/hr_expense.py
@@ -47,8 +47,9 @@ class HrExpense(models.Model):
name = fields.Char('Description', readonly=True, required=True, states={'draft': [('readonly', False)], 'reported': [('readonly', False)], 'refused': [('readonly', False)]})
date = fields.Date(readonly=True, states={'draft': [('readonly', False)], 'reported': [('readonly', False)], 'refused': [('readonly', False)]}, default=fields.Date.context_today, string="Date")
employee_id = fields.Many2one('hr.employee', string="Employee", required=True, readonly=True, states={'draft': [('readonly', False)], 'reported': [('readonly', False)], 'refused': [('readonly', False)]}, default=_default_employee_id, domain=lambda self: self._get_employee_id_domain())
- product_id = fields.Many2one('product.product', string='Product', readonly=True, states={'draft': [('readonly', False)], 'reported': [('readonly', False)], 'refused': [('readonly', False)]}, domain=[('can_be_expensed', '=', True)], required=True)
- product_uom_id = fields.Many2one('uom.uom', string='Unit of Measure', required=True, readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, default=_default_product_uom_id)
+ # product_id not required to allow create an expense without product via mail alias, but should be required on the view.
+ product_id = fields.Many2one('product.product', string='Product', readonly=True, states={'draft': [('readonly', False)], 'reported': [('readonly', False)], 'refused': [('readonly', False)]}, domain=[('can_be_expensed', '=', True)])
+ product_uom_id = fields.Many2one('uom.uom', string='Unit of Measure', readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, default=_default_product_uom_id)
unit_amount = fields.Float("Unit Price", readonly=True, required=True, states={'draft': [('readonly', False)], 'reported': [('readonly', False)], 'refused': [('readonly', False)]}, digits=dp.get_precision('Product Price'))
quantity = fields.Float(required=True, readonly=True, states={'draft': [('readonly', False)], 'reported': [('readonly', False)], 'refused': [('readonly', False)]}, digits=dp.get_precision('Product Unit of Measure'), default=1)
tax_ids = fields.Many2many('account.tax', 'expense_tax', 'expense_id', 'tax_id', string='Taxes', states={'done': [('readonly', True)], 'post': [('readonly', True)]})
@@ -128,6 +129,9 @@ class HrExpense(models.Model):
account = self.product_id.product_tmpl_id._get_product_accounts()['expense']
if account:
self.account_id = account
+ return {'domain': {'product_uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)]}}
+ else:
+ return {'domain': {'product_uom_id': []}}
@api.onchange('product_uom_id')
def _onchange_product_uom_id(self):
@@ -412,9 +416,6 @@ class HrExpense(models.Model):
@api.model
def message_new(self, msg_dict, custom_values=None):
- if custom_values is None:
- custom_values = {}
-
email_address = email_split(msg_dict.get('email_from', False))[0]
employee = self.env['hr.employee'].search([
@@ -425,50 +426,116 @@ class HrExpense(models.Model):
expense_description = msg_dict.get('subject', '')
- # Match the first occurence of '[]' in the string and extract the content inside it
- # Example: '[foo] bar (baz)' becomes 'foo'. This is potentially the product code
- # of the product to encode on the expense. If not, take the default product instead
- # which is 'Fixed Cost'
- default_product = self.env.ref('hr_expense.product_product_fixed_cost')
- pattern = '\[([^)]*)\]'
- product_code = re.search(pattern, expense_description)
- if product_code is None:
- product = default_product
+ if employee.user_id:
+ currencies = employee.user_id.company_id.currency_id | employee.user_id.company_ids.mapped('currency_id')
else:
- expense_description = expense_description.replace(product_code.group(), '')
- products = self.env['product.product'].search([('default_code', 'ilike', product_code.group(1))]) or default_product
- product = products.filtered(lambda p: p.default_code == product_code.group(1)) or products[0]
- account = product.product_tmpl_id._get_product_accounts()['expense']
+ currencies = employee.company_id.currency_id
- pattern = '[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?'
- # Match the last occurence of a float in the string
- # Example: '[foo] 50.3 bar 34.5' becomes '34.5'. This is potentially the price
- # to encode on the expense. If not, take 1.0 instead
- expense_price = re.findall(pattern, expense_description)
- # TODO: International formatting
- if not expense_price:
- price = 1.0
- else:
- price = expense_price[-1][0]
- expense_description = expense_description.replace(price, '')
- try:
- price = float(price)
- except ValueError:
- price = 1.0
-
- custom_values.update({
- 'name': expense_description.strip(),
+ product, price, currency_id, expense_description = self._parse_expense_subject(expense_description, currencies)
+ vals = {
'employee_id': employee.id,
- 'product_id': product.id,
+ 'name': expense_description,
+ 'unit_amount': price,
+ 'product_id': product.id if product else None,
'product_uom_id': product.uom_id.id,
'tax_ids': [(4, tax.id, False) for tax in product.supplier_taxes_id],
'quantity': 1,
- 'unit_amount': price,
'company_id': employee.company_id.id,
- })
+ 'currency_id': currency_id.id
+ }
+
+ account = product.product_tmpl_id._get_product_accounts()['expense']
if account:
- custom_values['account_id'] = account.id
- return super(HrExpense, self).message_new(msg_dict, custom_values)
+ vals['account_id'] = account.id
+
+ expense = super(HrExpense, self).message_new(msg_dict, dict(custom_values or {}, **vals))
+ self._send_expense_success_mail(msg_dict, expense)
+ return expense
+
+
+ @api.model
+ def _parse_product(self, expense_description):
+ """
+ Parse the subject to find the product.
+ Product code should be the first word of expense_description
+ Return product.product and updated description
+ """
+
+ product_code = expense_description.split(' ')[0]
+ product = self.env['product.product'].search([('can_be_expensed', '=', True), ('default_code', '=ilike', product_code)], limit=1)
+ if product:
+ expense_description = expense_description.replace(product_code, '')
+
+ return product, expense_description
+
+ @api.model
+ def _parse_price(self, expense_description, currencies):
+ """ Return price, currency and updated description """
+ symbols, symbols_pattern, float_pattern = [], '', '[+-]?(\d+[.,]?\d*)'
+ price = 0.0
+ for currency in currencies:
+ symbols.append(re.escape(currency.symbol))
+ symbols.append(re.escape(currency.name))
+ symbols_pattern = '|'.join(symbols)
+ price_pattern = "((%s)?\s?%s\s?(%s)?)" % (symbols_pattern, float_pattern, symbols_pattern)
+ matches = re.findall(price_pattern, expense_description)
+ if matches:
+ match = max(matches, key=lambda match: len([group for group in match if group])) # get the longuest match. e.g. "2 chairs 120$" -> the price is 120$, not 2
+ full_str = match[0]
+ currency_str = match[1] or match[3]
+ price = match[2].replace(',', '.')
+
+ if currency_str:
+ currency = currencies.filtered(lambda c: currency_str in [c.symbol, c.name])[0]
+ currency = currency or currencies[0]
+ expense_description = expense_description.replace(full_str, ' ') # remove price from description
+ expense_description = re.sub(' +', ' ', expense_description.strip())
+
+ price = float(price)
+ return price, currency, expense_description
+
+ @api.model
+ def _parse_expense_subject(self, expense_description, currencies):
+ """ Fetch product, price and currency info from mail subject.
+
+ Product can be identified based on product name or product code.
+ It can be passed between [] or it can be placed at start.
+
+ When parsing, only consider currencies passed as parameter.
+ This will fetch currency in symbol($) or ISO name (USD).
+
+ Some valid examples:
+ Travel by Air [TICKET] USD 1205.91
+ TICKET $1205.91 Travel by Air
+ Extra expenses 29.10EUR [EXTRA]
+ """
+ product, expense_description = self._parse_product(expense_description)
+ price, currency_id, expense_description = self._parse_price(expense_description, currencies)
+
+ return product, price, currency_id, expense_description
+
+ # TODO: Make api.multi
+ def _send_expense_success_mail(self, msg_dict, expense):
+ mail_template_id = 'hr_expense.hr_expense_template_register' if expense.employee_id.user_id else 'hr_expense.hr_expense_template_register_no_user'
+ expense_template = self.env.ref(mail_template_id)
+ rendered_body = expense_template.render({'expense': expense}, engine='ir.qweb')
+ body = self.env['mail.thread']._replace_local_links(rendered_body)
+ if expense.employee_id.user_id.partner_id:
+ expense.message_post(
+ partner_ids=expense.employee_id.user_id.partner_id.ids,
+ subject='Re: %s' % msg_dict.get('subject', ''),
+ body=body,
+ subtype_id=self.env.ref('mail.mt_note').id,
+ notif_layout='mail.mail_notification_light',
+ )
+ else:
+ self.env['mail.mail'].create({
+ 'body_html': body,
+ 'subject': 'Re: %s' % msg_dict.get('subject', ''),
+ 'email_to': msg_dict.get('email_from', False),
+ 'auto_delete': True,
+ 'references': msg_dict.get('message_id'),
+ }).send()
class HrExpenseSheet(models.Model):
diff --git a/addons/hr_expense/tests/__init__.py b/addons/hr_expense/tests/__init__.py
index 0d18c30d3de..558af4cd575 100644
--- a/addons/hr_expense/tests/__init__.py
+++ b/addons/hr_expense/tests/__init__.py
@@ -3,3 +3,4 @@
from . import common
from . import test_expenses
+from . import test_expenses_mail_subjects
diff --git a/addons/hr_expense/tests/test_expenses.py b/addons/hr_expense/tests/test_expenses.py
index c2c2439ec80..62eb6367365 100644
--- a/addons/hr_expense/tests/test_expenses.py
+++ b/addons/hr_expense/tests/test_expenses.py
@@ -84,7 +84,7 @@ class TestAccountEntry(TestExpenseCommon):
message_parsed = {
'message_id': 'the-world-is-a-ghetto',
- 'subject': '[EXP_AF] 9876',
+ 'subject': 'EXP_AF 9876',
'email_from': 'mark.brown23@example.com',
'to': 'catchall@yourcompany.com',
'body': "Don't you know, that for me, and for you",
@@ -99,6 +99,27 @@ class TestAccountEntry(TestExpenseCommon):
self.assertEquals(expense.total_amount, 9876.0)
self.assertTrue(expense.employee_id in user_demo.employee_ids)
+ def test_expense_from_email_without_product(self):
+ user_demo = self.env.ref('base.user_demo')
+ self.tax.price_include = False
+
+ message_parsed = {
+ 'message_id': 'the-world-is-a-ghetto',
+ 'subject': 'no product code 9876',
+ 'email_from': 'mark.brown23@example.com',
+ 'to': 'catchall@yourcompany.com',
+ 'body': "Don't you know, that for me, and for you",
+ 'attachments': [],
+ }
+
+ expense = self.env['hr.expense'].message_new(message_parsed)
+
+ air_ticket = self.env.ref("hr_expense.air_ticket")
+ self.assertFalse(expense.product_id, "No product should be linked")
+ self.assertEquals(expense.tax_ids.ids, [])
+ self.assertEquals(expense.total_amount, 9876.0)
+ self.assertTrue(expense.employee_id in user_demo.employee_ids)
+
class TestExpenseRights(TestExpenseCommon):
@classmethod
diff --git a/addons/hr_expense/tests/test_expenses_mail_subjects.py b/addons/hr_expense/tests/test_expenses_mail_subjects.py
new file mode 100644
index 00000000000..134726e32cf
--- /dev/null
+++ b/addons/hr_expense/tests/test_expenses_mail_subjects.py
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.addons.hr_expense.tests.common import TestExpenseCommon
+
+
+class TestExpenseSubject(TestExpenseCommon):
+ """
+ Check subject parsing while registering expense via mail.
+ """
+
+ def setUp(self):
+ super(TestExpenseSubject, self).setUp()
+ self.product_expense = self.env['product.product'].create({
+ 'name': "Phone bill",
+ 'can_be_expensed': True,
+ 'standard_price': 700,
+ 'list_price': 700,
+ 'type': 'consu',
+ 'default_code': 'EXP-PHONE'
+ })
+
+ def test_expense_subjects(self):
+ employee = self.employee
+ employee.user_id = self.user_employee
+ parse_subject = self.env['hr.expense']._parse_expense_subject
+ company_currency = employee.company_id.currency_id
+ alternate_currency = self.env['res.currency'].create({'name': 'AAB', 'symbol': '#'})
+
+ # Without Multi currency access
+ subject = 'EXP-PHONE bar %s1205.91 electro wizard' % (company_currency.symbol,)
+
+ product, price, currency_id, expense_description = parse_subject(subject, company_currency)
+ self.assertEquals(expense_description, 'bar electro wizard', "Should be remove price and product from subject")
+ self.assertAlmostEquals(price, 1205.91, "Price is not fetched correctly")
+ self.assertEquals(currency_id, company_currency, "Should fetch currency correctly")
+ self.assertEquals(product, self.product_expense, "Should fetch product correctly")
+
+ # subject having other currency then company currency, it should ignore other currency then company currency
+ subject = 'foo bar %s1406.91 royal giant' % (alternate_currency.symbol,)
+ product, price, currency_id, expense_description = parse_subject(subject, company_currency)
+ self.assertEquals(expense_description, 'foo bar %s royal giant' % (alternate_currency.symbol,), "Should be remove price and product from subject but not currency symbol")
+ self.assertEquals(currency_id, company_currency, "Should fetch currency correctly")
+
+ # With Multi currency access
+ group_multi_currency = self.env.ref('base.group_multi_currency')
+ self.user_employee.write({
+ 'groups_id': [(4, group_multi_currency.id)],
+ })
+
+ subject = 'EXP-PHONE foo bar %s2205.92 elite barbarians' % (company_currency.symbol,) # with product code at start
+ product, price, currency_id, expense_description = parse_subject(subject, company_currency)
+ self.assertEquals(expense_description, 'foo bar elite barbarians', "Should be remove price and product from subject")
+ self.assertAlmostEquals(price, 2205.92, "Price is not fetched correctly")
+ self.assertEquals(currency_id, company_currency, "Should fetch currency correctly")
+ self.assertEquals(product, self.product_expense, "Should fetch product correctly")
+
+ # subject having other currency then company currency, it should accept other currency because multi currency is activated
+ subject = 'EXP-PHONE %s2510.90 chhota bheem' % (alternate_currency.symbol,)
+ product, price, currency_id, expense_description = parse_subject(subject, company_currency | alternate_currency)
+ self.assertEquals(expense_description, 'chhota bheem', "Should be remove price and product from subject but not currency symbol")
+ self.assertAlmostEquals(price, 2510.90, "Price is not fetched correctly")
+ self.assertEquals(currency_id, alternate_currency, "Should fetch currency correctly")
+ self.assertEquals(product, self.product_expense, "Should fetch product correctly")
+
+ # subject without product and currency, should take company currency and default product
+ subject = 'foo bar 109.96 spear goblins'
+ product, price, currency_id, expense_description = parse_subject(subject, company_currency | alternate_currency)
+ self.assertEquals(expense_description, 'foo bar spear goblins', "Should remove price")
+ self.assertAlmostEquals(price, 109.96, "Price is not fetched correctly")
+ self.assertIn(currency_id, company_currency | alternate_currency, "Should fetch company currency")
+ self.assertFalse(product, "Should not have parsed any product")
+
+ # subject with currency symbol at end
+ subject = 'EXP-PHONE foo bar 2910.94%s inferno dragon' % (company_currency.symbol,)
+ product, price, currency_id, expense_description = parse_subject(subject, company_currency | alternate_currency)
+ self.assertEquals(expense_description, 'foo bar inferno dragon', "Should be remove price and product from subject")
+ self.assertAlmostEquals(price, 2910.94, "Price is not fetched correctly")
+ self.assertEquals(currency_id, company_currency, "Should fetch currency correctly")
+ self.assertEquals(product, self.product_expense, "Should fetch product correctly")
+
+ # subject with no amount and product
+ subject = 'foo bar mega knight'
+ product, price, currency_id, expense_description = parse_subject(subject, company_currency | alternate_currency)
+ self.assertEquals(expense_description, 'foo bar mega knight', "Should be same as subject")
+ self.assertAlmostEquals(price, 0.0, "Price is not fetched correctly")
+ self.assertIn(currency_id, company_currency | alternate_currency, "Should fetch currency correctly")
+ self.assertFalse(product, "Should fetch product correctly")
+
+ # price with a comma
+ subject = 'foo bar 291,56%s mega knight' % (company_currency.symbol,)
+ product, price, currency_id, expense_description = parse_subject(subject, company_currency | alternate_currency)
+ self.assertAlmostEquals(price, 291.56, "Price is not fetched correctly")
+ self.assertEquals(currency_id, company_currency, "Should fetch currency correctly")
+
+ # price without decimals
+ subject = 'foo bar 291%s mega knight' % (company_currency.symbol,)
+ product, price, currency_id, expense_description = parse_subject(subject, company_currency | alternate_currency)
+ self.assertAlmostEquals(price, 291.0, "Price is not fetched correctly")
+
+ subject = 'EXP-PHONE 2 foo bar 291.5%s mega knight' % (company_currency.symbol,)
+ product, price, currency_id, expense_description = parse_subject(subject, company_currency | alternate_currency)
+ print(price)
+ self.assertAlmostEquals(price, 291.5, "Price is not fetched correctly")
diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml
index cfa59fbe47e..fa70f9c7fa1 100644
--- a/addons/hr_expense/views/hr_expense_views.xml
+++ b/addons/hr_expense/views/hr_expense_views.xml
@@ -67,12 +67,12 @@
</div>
<group>
<group>
- <field name="product_id" context="{'default_can_be_expensed': 1, 'tree_view_ref': 'hr_expense.product_product_expense_tree_view'}"/>
+ <field name="product_id" required="1" context="{'default_can_be_expensed': 1, 'tree_view_ref': 'hr_expense.product_product_expense_tree_view'}"/>
<field name="unit_amount" required="1" widget="monetary" options="{'currency_field': 'currency_id'}"/>
<label for="quantity"/>
<div>
<field name="quantity" class="oe_inline"/>
- <field name="product_uom_id" widget="selection" class="oe_inline" groups="uom.group_uom"/>
+ <field name="product_uom_id" required="1" widget="selection" class="oe_inline" groups="uom.group_uom"/>
</div>
<field name="tax_ids" domain="[('type_tax_use', '=', 'purchase')]" widget="many2many_tags" groups="account.group_account_user"/>
</group><group>
@@ -301,7 +301,7 @@
<label for="default_code"/>
<div>
<field name="default_code"/>
- <i class="text-muted oe_edit_only">Use [Reference] as a subject prefix for incoming receipts</i>
+ <i class="text-muted oe_edit_only">Use this reference as a subject prefix when submitting by email.</i>
</div>
</group>
<group string="Accounting">
diff --git a/addons/hr_expense/views/res_config_settings_views.xml b/addons/hr_expense/views/res_config_settings_views.xml
index 098c7637dbc..26140494651 100644
--- a/addons/hr_expense/views/res_config_settings_views.xml
+++ b/addons/hr_expense/views/res_config_settings_views.xml
@@ -11,7 +11,7 @@
<div class="app_settings_block" data-string="Expenses" string="Expenses" data-key="hr_expense" groups="hr_expense.group_hr_expense_manager">
<h2>Recording</h2>
<div class="row mt16 o_settings_container">
- <div class="col-12 col-lg-6 o_setting_box" title="You can setup a generic email alias to create incoming expenses easily. Write an email with the receipt in attachment to create an expense line in one click. If the mail subject contains the product's internal reference between brackets, the product will be set automatically. Type the expense amount in the mail subject to set it on the expense too.">
+ <div class="col-xs-12 col-md-6 o_setting_box" title="Send an email to this email alias with the receipt in attachment to create an expense in one click. If the first word of the mail subject contains the product's internal reference or the product name, the corresponding product will automatically be set. Type the expense amount in the mail subject to set it on the expense too.">
<div class="o_setting_left_pane">
<field name="use_mailgateway"/>
</div>
diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml
index 3d2a4306f66..20c099ecfb8 100644
--- a/addons/product/views/product_views.xml
+++ b/addons/product/views/product_views.xml
@@ -433,7 +433,10 @@
<img t-att-src="kanban_image('product.product', 'image_small', record.id.raw_value)" alt="Product"/>
</div>
<div class="oe_kanban_details">
- <strong class="o_kanban_record_title"><field name="name"/></strong>
+ <strong class="o_kanban_record_title">
+ <field name="name"/>
+ <small t-if="record.default_code.value">[<field name="default_code"/>]</small>
+ </strong>
<div class="o_kanban_tags_section">
<field name="attribute_value_ids" groups="product.group_product_variant"/>
</div>
diff --git a/addons/sale_expense/__manifest__.py b/addons/sale_expense/__manifest__.py
index 3a1f2e996da..a9150bee904 100644
--- a/addons/sale_expense/__manifest__.py
+++ b/addons/sale_expense/__manifest__.py
@@ -18,7 +18,6 @@ This module allow to reinvoice employee expense, by setting the SO directly on t
'data/digest_data.xml',
'security/ir.model.access.csv',
'security/sale_expense_security.xml',
- 'data/sale_expense_data.xml',
'views/product_view.xml',
'views/hr_expense_views.xml',
'views/sale_order_views.xml',
diff --git a/addons/sale_expense/data/sale_expense_data.xml b/addons/sale_expense/data/sale_expense_data.xml
deleted file mode 100644
index e8f85810f4b..00000000000
--- a/addons/sale_expense/data/sale_expense_data.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
- <data noupdate="1">
-
- <record id="hr_expense.product_product_fixed_cost" model="product.product">
- <field name="invoice_policy">delivery</field>
- <field name="expense_policy">cost</field>
- </record>
- </data>
-</odoo>
diff --git a/addons/sale_expense/data/sale_expense_demo.xml b/addons/sale_expense/data/sale_expense_demo.xml
index dbeb2f12f36..8660bbc49da 100644
--- a/addons/sale_expense/data/sale_expense_demo.xml
+++ b/addons/sale_expense/data/sale_expense_demo.xml
@@ -2,6 +2,11 @@
<odoo>
<data noupdate="1">
+ <record id="hr_expense.product_product_fixed_cost" model="product.product">
+ <field name="invoice_policy">delivery</field>
+ <field name="expense_policy">cost</field>
+ </record>
+
<record id="hr_expense.car_travel" model="product.product">
<field name="invoice_policy">delivery</field>
<field name="expense_policy">sales_price</field>