From a4df9f8c938cf8cd85878c91872159a987de5176 Mon Sep 17 00:00:00 2001
From: Sanjay Jamod <sja@odoo.com>
Date: Wed, 6 Jun 2018 10:01:14 +0200
Subject: [PATCH] [IMP] account: vendor bill creation upon email reception

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
---
 addons/account/data/account_data.xml          |   9 ++
 addons/account/models/account.py              |  46 +++++++-
 addons/account/models/account_invoice.py      | 104 +++++++++++++++++-
 .../models/account_journal_dashboard.py       |   7 +-
 addons/account/static/src/css/account.css     |   1 -
 addons/account/views/account_invoice_view.xml |  37 +++++--
 addons/account/views/account_view.xml         |  12 ++
 7 files changed, 194 insertions(+), 22 deletions(-)

diff --git a/addons/account/data/account_data.xml b/addons/account/data/account_data.xml
index 900e7fdead45..f7fa31bd517e 100644
--- a/addons/account/data/account_data.xml
+++ b/addons/account/data/account_data.xml
@@ -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">
diff --git a/addons/account/models/account.py b/addons/account/models/account.py
index 87b4ffc2765e..c8ec7e289ffe 100644
--- a/addons/account/models/account.py
+++ b/addons/account/models/account.py
@@ -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
@@ -427,10 +428,21 @@ class AccountJournal(models.Model):
     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!
@@ -509,6 +521,19 @@ class AccountJournal(models.Model):
         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()
@@ -516,6 +541,7 @@ class AccountJournal(models.Model):
             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
@@ -529,6 +555,19 @@ class AccountJournal(models.Model):
             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:
@@ -564,7 +603,8 @@ class AccountJournal(models.Model):
                     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
@@ -676,8 +716,10 @@ class AccountJournal(models.Model):
             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'):
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index a4407c557aff..3999980159aa 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -10,7 +10,7 @@ from dateutil.relativedelta import relativedelta
 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
@@ -241,7 +241,7 @@ class AccountInvoice(models.Model):
             ('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(
@@ -284,7 +284,7 @@ class AccountInvoice(models.Model):
              "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.")
@@ -299,7 +299,7 @@ class AccountInvoice(models.Model):
         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)
@@ -370,10 +370,24 @@ class AccountInvoice(models.Model):
     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):
@@ -479,8 +493,6 @@ class AccountInvoice(models.Model):
                 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)
 
@@ -611,6 +623,82 @@ class AccountInvoice(models.Model):
             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)"""
@@ -799,10 +887,14 @@ class AccountInvoice(models.Model):
     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()
diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py
index cb58e37df92c..af8e26b27ab1 100644
--- a/addons/account/models/account_journal_dashboard.py
+++ b/addons/account/models/account_journal_dashboard.py
@@ -324,7 +324,7 @@ class account_journal(models.Model):
             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'
 
@@ -354,11 +354,14 @@ class account_journal(models.Model):
         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
diff --git a/addons/account/static/src/css/account.css b/addons/account/static/src/css/account.css
index 923ea75efe31..217e74f75714 100644
--- a/addons/account/static/src/css/account.css
+++ b/addons/account/static/src/css/account.css
@@ -23,4 +23,3 @@
     font-style: italic;
     color: grey;
 }
-
diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml
index 0ce2d057a5bd..8345b94ac582 100644
--- a/addons/account/views/account_invoice_view.xml
+++ b/addons/account/views/account_invoice_view.xml
@@ -190,8 +190,11 @@
             <field name="name">account.invoice.supplier.tree</field>
             <field name="model">account.invoice</field>
             <field name="arch" type="xml">
-                <tree decoration-info="state == 'draft'" decoration-muted="state == 'cancel'" string="Invoice">
-                    <field name="partner_id" groups="base.group_user" string="Vendor"/>
+                <tree decoration-info="state == 'draft'" decoration-muted="state == 'cancel'" decoration-bf="not partner_id" string="Vendor Bill">
+                    <field name="partner_id" invisible="1"/>
+                    <field name="source_email" invisible="1"/>
+                    <field name="invoice_icon" string=" "/>
+                    <field name="vendor_display_name" groups="base.group_user" string="Vendor"/>
                     <field name="date_invoice" string="Bill Date"/>
                     <field name="number"/>
                     <field name="reference"/>
@@ -250,7 +253,7 @@
                         <group>
                             <field string="Vendor" name="partner_id"
                               context="{'default_customer': 0, 'search_default_supplier': 1, 'default_supplier': 1, 'default_company_type': 'company'}"
-                              domain="[('supplier', '=', True)]"/>
+                              domain="[('supplier', '=', True)]" required="1"/>
                             <field name="reference" string="Vendor Reference"/>
                             <field name="vendor_bill_id" attrs="{'invisible': [('state','not in',['draft'])]}"
                               domain="[('partner_id','child_of', [partner_id]), ('state','in',('open','paid')), ('type','=','in_invoice')]"
@@ -259,6 +262,7 @@
                         </group>
                         <group>
                             <field name="origin" attrs="{'invisible': [('origin', '=', False)]}"/>
+                            <field name="source_email" widget="email" groups="base.group_no_one" attrs="{'invisible': [('source_email', '=', False)]}"/>
                             <field name="date_invoice" string="Bill Date"/>
                             <field name="date_due" attrs="{'readonly': ['|',('payment_term_id','!=',False), ('state', 'in', ['open', 'paid'])]}" force_save="1"/>
                             <field name="move_name" invisible="1"/>
@@ -348,7 +352,7 @@
                         </page>
                     </notebook>
                 </sheet>
-                <div class="o_attachment_preview" attrs="{'invisible': ['|',('type', '!=', 'in_invoice'),('state', '!=', 'draft')]}"/>
+                <div class="o_attachment_preview" attrs="{'invisible': ['|',('type', '!=', 'in_invoice'),('state', '!=', 'draft')]}" options="{'preview_priority_type': 'pdf'}"/>
                 <div class="oe_chatter">
                     <field name="message_follower_ids" widget="mail_followers"/>
                     <field name="activity_ids" widget="mail_activity"/>
@@ -402,7 +406,7 @@
                             <field string="Customer" name="partner_id"
                                 context="{'search_default_customer':1, 'show_address': 1, 'default_company_type': 'company'}"
                                 options='{"always_reload": True, "no_quick_create": True}'
-                                domain="[('customer', '=', True)]"/>
+                                domain="[('customer', '=', True)]" required="1"/>
                             <field name="payment_term_id"/>
                             <field name="cash_rounding_id" groups="account.group_cash_rounding"/>
                         </group>
@@ -707,7 +711,7 @@
                   parent="menu_finance_receivables_documents"
                   sequence="1"/>
 
-        <record id="action_invoice_tree2" model="ir.actions.act_window">
+        <record id="action_vendor_bill_template" model="ir.actions.act_window">
             <field name="name">Vendor Bills</field>
             <field name="res_model">account.invoice</field>
             <field name="view_type">form</field>
@@ -719,25 +723,36 @@
             <field name="help" type="html">
               <p class="o_view_nocontent_smiling_face">
                 Record a new vendor bill
-              </p><p>
-                You can control the invoice from your vendor based on
-                what you purchased or received.
               </p>
             </field>
         </record>
+        <!--
+            server action opening the vendor bills and returning the right help tooltip
+        -->
+        <record id="action_invoice_tree2" model="ir.actions.server">
+            <field name="name">Vendor Bills</field>
+            <field name="model_id" ref="model_account_invoice"/>
+            <field name="state">code</field>
+            <field name="code">
+action_values = env.ref('account.action_vendor_bill_template').read()[0]
+new_help = model.complete_empty_list_help()
+action_values.update({'help': action_values.get('help', '') + new_help})
+action = action_values
+            </field>
+        </record>
 
         <record id="action_invoice_supplier_tree1_view1" model="ir.actions.act_window.view">
             <field eval="1" name="sequence"/>
             <field name="view_mode">tree</field>
             <field name="view_id" ref="invoice_supplier_tree"/>
-            <field name="act_window_id" ref="action_invoice_tree2"/>
+            <field name="act_window_id" ref="action_vendor_bill_template"/>
         </record>
 
         <record id="action_invoice__supplier_tree1_view2" model="ir.actions.act_window.view">
             <field eval="2" name="sequence"/>
             <field name="view_mode">form</field>
             <field name="view_id" ref="invoice_supplier_form"/>
-            <field name="act_window_id" ref="action_invoice_tree2"/>
+            <field name="act_window_id" ref="action_vendor_bill_template"/>
         </record>
 
         <menuitem action="action_invoice_tree2" id="menu_action_invoice_tree2" parent="menu_finance_payables_documents" sequence="1"/>
diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml
index 4d6a7e5a51b6..6e22ba8b77aa 100644
--- a/addons/account/views/account_view.xml
+++ b/addons/account/views/account_view.xml
@@ -333,6 +333,18 @@
                                         <field name="loss_account_id" attrs="{'invisible': [('type', '!=', 'cash')]}"/>
                                         <field name="show_on_dashboard" groups="base.group_no_one"/>
                                     </group>
+                                    <group name="group_alias" string="Email your Vendor Bills" attrs="{'invisible': [('type', '!=',  'purchase')]}">
+                                        <label string="Email Alias" attrs="{'invisible': [('alias_domain', '=', False)]}"/>
+                                        <div name="alias_def" attrs="{'invisible': [('alias_domain', '=', False)]}">
+                                            <field name="alias_id" class="oe_read_only oe_inline"/>
+                                            <div class="oe_edit_only oe_inline" name="edit_alias" style="display: inline;" >
+                                                <field name="alias_name" class="oe_inline"/>@<field name="alias_domain" class="oe_inline" readonly="1"/>
+                                            </div>
+                                        </div>
+                                        <div class="content-group" attrs="{'invisible': [('alias_domain', '!=', False)]}">
+                                            <a type='action' name='%(action_open_settings)d' class="btn btn-sm btn-link"><i class="fa fa-fw o_button_icon fa-arrow-right"/> Configure Email Servers</a>
+                                    </div>
+                                    </group>
                                 </group>
                             </page>
                             <page name="bank_account" string="Bank Account" attrs="{'invisible': [('type', '!=', 'bank')]}">
-- 
GitLab