Skip to content

Commit

Permalink
[IMP] account: vendor bill creation upon email reception
Browse files Browse the repository at this point in the history
This allows to set up a mail alias per purchase journal. Then share that email address to your supplier, or use it internally to forward the vendor bills received by mail, to automatically create an empty vendor bill with the mail attachments linked, and the partner might be filled if the source email matches a supplier.

Thanks to the document preview on the side, it's now super easy and super fast to copy the vendor bill info from the received PDF into the account.invoice object. This would eventually be improved later on (IAP).

Was task: 37703
Was PR #22158
  • Loading branch information
Sanjay Jamod authored and qdp-odoo committed Jun 6, 2018
1 parent a6b75d4 commit a4df9f8
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 22 deletions.
9 changes: 9 additions & 0 deletions addons/account/data/account_data.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
<odoo>
<data noupdate="1">

<!-- Open Settings from Purchase Journal to configure mail servers -->
<record id="action_open_settings" model="ir.actions.act_window">
<field name="name">Settings</field>
<field name="res_model">res.config.settings</field>
<field name="view_mode">form</field>
<field name="target">inline</field>
<field name="context" eval="{'module': 'general_settings'}"/>
</record>

<!-- TAGS FOR CASH FLOW STATEMENT -->

<record id="account_tag_operating" model="account.account.tag">
Expand Down
46 changes: 44 additions & 2 deletions addons/account/models/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import time
import math
import re

from odoo.osv import expression
from odoo.tools.float_utils import float_round as round
Expand Down Expand Up @@ -427,10 +428,21 @@ def _default_outbound_payment_methods(self):
bank_acc_number = fields.Char(related='bank_account_id.acc_number')
bank_id = fields.Many2one('res.bank', related='bank_account_id.bank_id')

# alias configuration for 'purchase' type journals
alias_id = fields.Many2one('mail.alias', string='Alias')
alias_domain = fields.Char('Alias domain', compute='_compute_alias_domain', default=lambda self: self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain"))
alias_name = fields.Char('Alias Name for Vendor Bills', related='alias_id.alias_name', help="It creates draft vendor bill by sending an email.")

_sql_constraints = [
('code_company_uniq', 'unique (code, name, company_id)', 'The code and name of the journal must be unique per company !'),
]

@api.multi
def _compute_alias_domain(self):
alias_domain = self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain")
for record in self:
record.alias_domain = alias_domain

@api.multi
# do not depend on 'sequence_id.date_range_ids', because
# sequence_id._get_current_sequence() may invalidate it!
Expand Down Expand Up @@ -509,13 +521,27 @@ def onchange_credit_account_id(self):
if not self.default_debit_account_id:
self.default_debit_account_id = self.default_credit_account_id

@api.multi
def _get_alias_values(self, alias_name=None):
if not alias_name:
alias_name = self.name
if self.company_id != self.env.ref('base.main_company'):
alias_name += '-' + str(self.company_id.name)
return {
'alias_defaults': {'type': 'in_invoice'},
'alias_user_id': self.env.user.id,
'alias_parent_thread_id': self.id,
'alias_name': re.sub(r'[^\w]+', '-', alias_name)
}

@api.multi
def unlink(self):
bank_accounts = self.env['res.partner.bank'].browse()
for bank_account in self.mapped('bank_account_id'):
accounts = self.search([('bank_account_id', '=', bank_account.id)])
if accounts <= self:
bank_accounts += bank_account
self.mapped('alias_id').unlink()
ret = super(AccountJournal, self).unlink()
bank_accounts.unlink()
return ret
Expand All @@ -529,6 +555,19 @@ def copy(self, default=None):
name=_("%s (copy)") % (self.name or ''))
return super(AccountJournal, self).copy(default)

def _update_mail_alias(self, vals):
self.ensure_one()
alias_values = self._get_alias_values(alias_name=vals.get('alias_name'))
if self.alias_id:
self.alias_id.write(alias_values)
else:
self.alias_id = self.env['mail.alias'].with_context(alias_model_name='account.invoice',
alias_parent_model_name='account.journal').create(alias_values)

if vals.get('alias_name'):
# remove alias_name to avoid useless write on alias
del(vals['alias_name'])

@api.multi
def write(self, vals):
for journal in self:
Expand Down Expand Up @@ -564,7 +603,8 @@ def write(self, vals):
bank_account = self.env['res.partner.bank'].browse(vals['bank_account_id'])
if bank_account.partner_id != company.partner_id:
raise UserError(_("The partners of the journal's company and the related bank account mismatch."))

if vals.get('type') == 'purchase':
self._update_mail_alias(vals)
result = super(AccountJournal, self).write(vals)

# Create the bank_account_id if necessary
Expand Down Expand Up @@ -676,8 +716,10 @@ def create(self, vals):
vals.update({'sequence_id': self.sudo()._create_sequence(vals).id})
if vals.get('type') in ('sale', 'purchase') and vals.get('refund_sequence') and not vals.get('refund_sequence_id'):
vals.update({'refund_sequence_id': self.sudo()._create_sequence(vals, refund=True).id})

journal = super(AccountJournal, self).create(vals)
if journal.type == 'purchase':
# create a mail alias for purchase journals (always, deactivated if alias_name isn't set)
journal._update_mail_alias(vals)

# Create the bank_account_id if necessary
if journal.type == 'bank' and not journal.bank_account_id and vals.get('bank_acc_number'):
Expand Down
104 changes: 98 additions & 6 deletions addons/account/models/account_invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from werkzeug.urls import url_encode

from odoo import api, exceptions, fields, models, _
from odoo.tools import float_is_zero, float_compare, pycompat
from odoo.tools import email_re, email_split, email_escape_char, float_is_zero, float_compare, pycompat
from odoo.tools.misc import formatLang

from odoo.exceptions import AccessError, UserError, RedirectWarning, ValidationError, Warning
Expand Down Expand Up @@ -241,7 +241,7 @@ def _compute_payments(self):
('in_invoice','Vendor Bill'),
('out_refund','Customer Credit Note'),
('in_refund','Vendor Credit Note'),
], readonly=True, index=True, change_default=True,
], readonly=True, states={'draft': [('readonly', False)]}, index=True, change_default=True,
default=lambda self: self._context.get('type', 'out_invoice'),
track_visibility='always')
access_token = fields.Char(
Expand Down Expand Up @@ -284,7 +284,7 @@ def _compute_payments(self):
"term is not set on the invoice. If you keep the Payment terms and the due date empty, it "
"means direct payment.")
partner_id = fields.Many2one('res.partner', string='Partner', change_default=True,
required=True, readonly=True, states={'draft': [('readonly', False)]},
readonly=True, states={'draft': [('readonly', False)]},
track_visibility='always')
vendor_bill_id = fields.Many2one('account.invoice', string='Vendor Bill',
help="Auto-complete from a past bill.")
Expand All @@ -299,7 +299,7 @@ def _compute_payments(self):
readonly=True, states={'draft': [('readonly', False)]})

account_id = fields.Many2one('account.account', string='Account',
required=True, readonly=True, states={'draft': [('readonly', False)]},
readonly=True, states={'draft': [('readonly', False)]},
domain=[('deprecated', '=', False)], help="The partner account used for this invoice.")
invoice_line_ids = fields.One2many('account.invoice.line', 'invoice_id', string='Invoice Lines', oldname='invoice_line',
readonly=True, states={'draft': [('readonly', False)]}, copy=True)
Expand Down Expand Up @@ -370,10 +370,24 @@ def _compute_payments(self):
sequence_number_next = fields.Char(string='Next Number', compute="_get_sequence_number_next", inverse="_set_sequence_next")
sequence_number_next_prefix = fields.Char(string='Next Number Prefix', compute="_get_sequence_prefix")

#fields related to vendor bills automated creation by email
source_email = fields.Char(string='Source Email', track_visibility='onchange')
vendor_display_name = fields.Char(compute='_get_vendor_display_info', store=True) # store=True to enable sorting on that column
invoice_icon = fields.Char(compute='_get_vendor_display_info', store=False)

_sql_constraints = [
('number_uniq', 'unique(number, company_id, journal_id, type)', 'Invoice Number must be unique per Company!'),
]

@api.depends('partner_id', 'source_email')
def _get_vendor_display_info(self):
for invoice in self:
vendor_display_name = invoice.partner_id.name
if not vendor_display_name and invoice.source_email:
vendor_display_name = _('From: ') + invoice.source_email
invoice.vendor_display_name = vendor_display_name
invoice.invoice_icon = invoice.source_email and '@' or ''

# Load all Vendor Bill lines
@api.onchange('vendor_bill_id')
def _onchange_vendor_bill(self):
Expand Down Expand Up @@ -479,8 +493,6 @@ def create(self, vals):
for field in changed_fields:
if field not in vals and invoice[field]:
vals[field] = invoice._fields[field].convert_to_write(invoice[field], invoice)
if not vals.get('account_id',False):
raise UserError(_('No account was found to create the invoice, be sure you have installed a chart of account.'))

invoice = super(AccountInvoice, self.with_context(mail_create_nolog=True)).create(vals)

Expand Down Expand Up @@ -611,6 +623,82 @@ def message_post(self, **kwargs):
self.filtered(lambda inv: not inv.sent).write({'sent': True})
return super(AccountInvoice, self.with_context(mail_post_autofollow=True)).message_post(**kwargs)

@api.model
def message_new(self, msg_dict, custom_values=None):
""" Overrides mail_thread message_new(), called by the mailgateway through message_process,
to complete values for vendor bills created by mails.
"""
# Split `From` and `CC` email address from received email to look for related partners to subscribe on the invoice
subscribed_emails = email_split((msg_dict.get('from') or '') + ',' + (msg_dict.get('cc') or ''))
subscribed_partner_ids = [pid for pid in self._find_partner_from_emails(subscribed_emails) if pid]

# Detection of the partner_id of the invoice:
# 1) check if the email_from correspond to a supplier
email_from = msg_dict.get('from') or ''
email_from = email_escape_char(email_split(email_from)[0])
partner_id = self._search_on_partner(email_from, extra_domain=[('supplier', '=', True)])

# 2) otherwise, if the email sender is from odoo internal users then it is likely that the vendor sent the bill
# by mail to the internal user who, inturn, forwarded that email to the alias to automatically generate the bill
# on behalf of the vendor.
if not partner_id:
user_partner_id = self._search_on_user(email_from)
if user_partner_id and user_partner_id in self.env.ref('base.group_user').users.mapped('partner_id').ids:
# In this case, we will look for the vendor's email address in email's body and assume if will come first
email_addresses = email_re.findall(msg_dict.get('body'))
if email_addresses:
partner_ids = [pid for pid in self._find_partner_from_emails([email_addresses[0]], force_create=False) if pid]
partner_id = partner_ids and partner_ids[0]
# otherwise, there's no fallback on the partner_id found for the regular author of the mail.message as we want
# the partner_id to stay empty

# If the partner_id can be found, subscribe it to the bill, otherwise it's left empty to be manually filled
if partner_id:
subscribed_partner_ids.append(partner_id)

# Find the right purchase journal based on the "TO" email address
destination_emails = email_split((msg_dict.get('to') or '') + ',' + (msg_dict.get('cc') or ''))
alias_names = [mail_to.split('@')[0] for mail_to in destination_emails]
journal = self.env['account.journal'].search([
('type', '=', 'purchase'), ('alias_name', 'in', alias_names)
], limit=1)

# Create the message and the bill.
values = dict(custom_values or {}, partner_id=partner_id, source_email=email_from)
if journal:
values['journal_id'] = journal.id
# Passing `type` in context so that _default_journal(...) can correctly set journal for new vendor bill
invoice = super(AccountInvoice, self.with_context(type=values.get('type'))).message_new(msg_dict, values)

# Subscribe people on the newly created bill
if subscribed_partner_ids:
invoice.message_subscribe(subscribed_partner_ids)
return invoice

@api.model
def complete_empty_list_help(self):
# add help message about email alias in vendor bills empty lists
Journal = self.env['account.journal']
journals = Journal.browse(self._context.get('default_journal_id')) or Journal.search([('type', '=', 'purchase')])

if journals:
links = ''
alias_count = 0
for journal in journals.filtered(lambda j: j.alias_domain and j.alias_id.alias_name):
email = format(journal.alias_id.alias_name) + "@" + format(journal.alias_domain)
links += "<a id='o_mail_test' href='mailto:{}'>{}</a>".format(email, email) + ", "
alias_count += 1
if links and alias_count == 1:
help_message = _('Or share the email %s to your vendors: bills will be created automatically upon mail reception.') % (links[:-2])
elif links:
help_message = _('Or share the emails %s to your vendors: bills will be created automatically upon mail reception.') % (links[:-2])
else:
help_message = _('''Or set an <a data-oe-id=%s data-oe-model="account.journal" href=#id=%s&model=account.journal>email alias</a> '''
'''to allow draft vendor bills to be created upon reception of an email.''') % (journals[0].id, journals[0].id)
else:
help_message = _('<p>You can control the invoice from your vendor based on what you purchased or received.</p>')
return help_message

@api.multi
def compute_taxes(self):
"""Function used in other module to compute the taxes on a fresh invoice created (onchanges did not applied)"""
Expand Down Expand Up @@ -799,10 +887,14 @@ def action_invoice_draft(self):
def action_invoice_open(self):
# lots of duplicate calls to action_invoice_open, so we remove those already open
to_open_invoices = self.filtered(lambda inv: inv.state != 'open')
for inv in to_open_invoices.filtered(lambda inv: not inv.partner_id):
raise UserError(_("The field Vendor is required, please complete it to validate the Vendor Bill."))
if to_open_invoices.filtered(lambda inv: inv.state != 'draft'):
raise UserError(_("Invoice must be in draft state in order to validate it."))
if to_open_invoices.filtered(lambda inv: inv.amount_total < 0):
raise UserError(_("You cannot validate an invoice with a negative total amount. You should create a credit note instead."))
if to_open_invoices.filtered(lambda inv: not inv.account_id):
raise UserError(_('No account was found to create the invoice, be sure you have installed a chart of account.'))
to_open_invoices.action_date_assign()
to_open_invoices.action_move_create()
return to_open_invoices.invoice_validate()
Expand Down
7 changes: 5 additions & 2 deletions addons/account/models/account_journal_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ def open_action(self):
elif self.type == 'sale':
action_name = 'action_invoice_tree1'
elif self.type == 'purchase':
action_name = 'action_invoice_tree2'
action_name = 'action_vendor_bill_template'
else:
action_name = 'action_move_journal_line'

Expand Down Expand Up @@ -354,11 +354,14 @@ def open_action(self):
action['context'] = ctx
action['domain'] = self._context.get('use_domain', [])
account_invoice_filter = self.env.ref('account.view_account_invoice_filter', False)
if action_name in ['action_invoice_tree1', 'action_invoice_tree2']:
if action_name in ['action_invoice_tree1', 'action_vendor_bill_template']:
action['search_view_id'] = account_invoice_filter and account_invoice_filter.id or False
if action_name in ['action_bank_statement_tree', 'action_view_bank_statement_tree']:
action['views'] = False
action['view_id'] = False
if self.type == 'purchase':
new_help = self.env['account.invoice'].with_context(ctx).complete_empty_list_help()
action.update({'help': action.get('help', '') + new_help})
return action

@api.multi
Expand Down
1 change: 0 additions & 1 deletion addons/account/static/src/css/account.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,3 @@
font-style: italic;
color: grey;
}

Loading

0 comments on commit a4df9f8

Please sign in to comment.