[IMP] account: payment scamming protection

Odoo does not have much in terms of payment protection; you can create
payments to any account you want without many checks.

This changes it by requiring users with the right groups to trust an
account before allowing it to be used for payment methods requiring.
Also adds some warnings on the bank account view, and checks at the time
of creating a payment.

Task id # 3210415

closes odoo/odoo#114278

Related: odoo/enterprise#37789
Related: odoo/documentation#3737
Related: odoo/upgrade#4397
Signed-off-by: Florian Gilbert (flg) <flg@odoo.com>
This commit is contained in:
Nicolas (vin)
2023-03-07 01:59:51 +01:00
parent 2541cf636b
commit c25341ed0c
13 changed files with 275 additions and 33 deletions
+1
View File
@@ -321,6 +321,7 @@ class AccountMove(models.Model):
"otherwise a Partner bank account number.",
check_company=True,
tracking=True,
ondelete='restrict',
)
fiscal_position_id = fields.Many2one(
'account.fiscal.position',
+9 -1
View File
@@ -30,7 +30,9 @@ class AccountPayment(models.Model):
readonly=False, store=True, tracking=True,
compute='_compute_partner_bank_id',
domain="[('id', 'in', available_partner_bank_ids)]",
check_company=True)
check_company=True,
ondelete='restrict',
)
is_internal_transfer = fields.Boolean(string="Internal Transfer",
readonly=False, store=True,
tracking=True,
@@ -528,6 +530,7 @@ class AccountPayment(models.Model):
for pay in self:
if pay.state in ('draft', 'posted') \
and pay.partner_bank_id \
and pay.partner_bank_id.allow_out_payment \
and pay.payment_method_line_id.code == 'manual' \
and pay.payment_type == 'outbound' \
and pay.currency_id:
@@ -921,6 +924,11 @@ class AccountPayment(models.Model):
def action_post(self):
''' draft -> posted '''
# Do not allow to post if the account is required but not trusted
for payment in self:
if payment.require_partner_bank_account and not payment.partner_bank_id.allow_out_payment:
raise UserError(_('To record payments with %s, the recipient bank account must be manually validated. You should go on the partner bank account in order to validate it.', self.payment_method_line_id.name))
self.move_id._post(soft=False)
self.filtered(
+78 -1
View File
@@ -16,6 +16,21 @@ class ResPartnerBank(models.Model):
journal_id = fields.One2many(
'account.journal', 'bank_account_id', domain=[('type', '=', 'bank')], string='Account Journal', readonly=True,
help="The accounting journal corresponding to this bank account.")
has_iban_warning = fields.Boolean(
compute='_compute_display_account_warning',
help='Technical field used to display a warning if the IBAN country is different than the holder country.',
store=True,
)
partner_country_name = fields.Char(related='partner_id.country_id.name')
has_money_transfer_warning = fields.Boolean(
compute='_compute_display_account_warning',
help='Technical field used to display a warning if the account is a transfer service account.',
store=True,
)
money_transfer_service = fields.Char(compute='_compute_money_transfer_service_name')
partner_supplier_rank = fields.Integer(related='partner_id.supplier_rank')
partner_customer_rank = fields.Integer(related='partner_id.customer_rank')
related_moves = fields.One2many('account.move', inverse_name='partner_bank_id')
# Add tracking to the base fields
bank_id = fields.Many2one(tracking=True)
@@ -23,7 +38,13 @@ class ResPartnerBank(models.Model):
acc_number = fields.Char(tracking=True)
acc_holder_name = fields.Char(tracking=True)
partner_id = fields.Many2one(tracking=True)
allow_out_payment = fields.Boolean(tracking=True)
user_has_group_validate_bank_account = fields.Boolean(compute='_compute_user_has_group_validate_bank_account')
allow_out_payment = fields.Boolean(
tracking=True,
help='Sending fake invoices with a fraudulent account number is a common phishing practice. '
'To protect yourself, always verify new bank account numbers, preferably by calling the vendor, as phishing '
'usually happens when their emails are compromised. Once verified, you can activate the ability to send money.'
)
currency_id = fields.Many2one(tracking=True)
@api.constrains('journal_id')
@@ -32,6 +53,49 @@ class ResPartnerBank(models.Model):
if len(bank.journal_id) > 1:
raise ValidationError(_('A bank account can belong to only one journal.'))
@api.constrains('allow_out_payment')
def _check_allow_out_payment(self):
""" Block enabling the setting, but it can be set to false without the group. (For example, at creation) """
for bank in self:
if bank.allow_out_payment:
if not self.user_has_groups('account.group_validate_bank_account'):
raise ValidationError(_('You do not have the right to trust or un-trust a bank account.'))
@api.depends('partner_id.country_id', 'sanitized_acc_number', 'allow_out_payment', 'acc_type')
def _compute_display_account_warning(self):
for bank in self:
if bank.allow_out_payment or not bank.sanitized_acc_number or bank.acc_type != 'iban':
bank.has_iban_warning = False
bank.has_money_transfer_warning = False
continue
bank_country = bank.sanitized_acc_number[:2]
bank.has_iban_warning = bank.partner_id.country_id and bank_country != bank.partner_id.country_id.code
bank_institution_code = bank.sanitized_acc_number[4:7]
bank.has_money_transfer_warning = bank_institution_code in bank._get_money_transfer_services()
@api.depends('sanitized_acc_number', 'allow_out_payment')
def _compute_money_transfer_service_name(self):
for bank in self:
if bank.sanitized_acc_number:
bank_institution_code = bank.sanitized_acc_number[4:7]
bank.money_transfer_service = bank._get_money_transfer_services().get(bank_institution_code, False)
else:
bank.money_transfer_service = False
def _get_money_transfer_services(self):
return {
'967': 'Wise',
'977': 'Paynovate',
'974': 'PPS EU SA',
}
@api.depends('acc_number')
def _compute_user_has_group_validate_bank_account(self):
user_has_group_validate_bank_account = self.user_has_groups('account.group_validate_bank_account')
for bank in self:
bank.user_has_group_validate_bank_account = user_has_group_validate_bank_account
def _build_qr_code_vals(self, amount, free_communication, structured_communication, currency, debtor_partner, qr_method=None, silent_errors=True):
""" Returns the QR-code vals needed to generate the QR-code report link to pay this account with the given parameters,
or None if no QR-code could be generated.
@@ -215,3 +279,16 @@ class ResPartnerBank(models.Model):
msg = _("Bank Account %s with number %s deleted", account._get_html_link(title=f"#{account.id}"), account.acc_number)
account.partner_id._message_log(body=msg)
return super().unlink()
def name_get(self):
res = super().name_get()
if self.env.context.get('display_account_trust'):
res = []
for acc in self:
trusted_label = _('trusted') if acc.allow_out_payment else _('untrusted')
if acc.bank_id:
name = '{} - {} ({})'.format(acc.acc_number, acc.bank_id.name, trusted_label)
else:
name = '{} ({})'.format(acc.acc_number, trusted_label)
res.append((acc.id, name))
return res
@@ -82,6 +82,12 @@
<field name="category_id" ref="base.module_category_hidden"/>
</record>
<record id="group_validate_bank_account" model="res.groups">
<field name="name">Validate bank account</field>
<field name="category_id" ref="base.module_category_usability"/>
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
</record>
</data>
<data noupdate="1">
@@ -20,6 +20,7 @@
<menuitem id="menu_action_move_in_refund_type" action="action_move_in_refund_type" sequence="2"/>
<menuitem id="menu_action_move_in_receipt_type" action="action_move_in_receipt_type" groups="account.group_purchase_receipts" sequence="3"/>
<menuitem id="menu_action_account_payments_payable" action="action_account_payments_payable" sequence="20"/>
<menuitem id="menu_account_supplier_accounts" name="Bank Accounts" action="action_account_supplier_accounts" sequence="80"/>
<menuitem id="product_product_menu_purchasable" name="Products" action="product_product_action_purchasable" sequence="100"/>
<menuitem id="menu_account_supplier" name="Vendors" action="account.res_partner_action_supplier" sequence="200"/>
</menuitem>
+6 -6
View File
@@ -595,7 +595,7 @@
<button name="action_register_payment" id="account_invoice_payment_btn"
type="object" class="oe_highlight"
attrs="{'invisible': ['|', '|', ('state', '!=', 'posted'), ('payment_state', 'not in', ('not_paid', 'partial')), ('move_type', 'not in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund', 'out_receipt', 'in_receipt'))]}"
context="{'dont_redirect_to_payments': True}"
context="{'dont_redirect_to_payments': True, 'display_account_trust': True}"
string="Register Payment" data-hotkey="g"
groups="account.group_account_invoice"/>
<!-- Preview (only customer invoices) -->
@@ -815,7 +815,7 @@
<field name="payment_reference"
attrs="{'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund', 'out_receipt', 'in_receipt'))]}"/>
<field name="partner_bank_id"
context="{'default_partner_id': bank_partner_id}"
context="{'default_partner_id': bank_partner_id, 'display_account_trust': True}"
domain="[('partner_id', '=', bank_partner_id)]"
attrs="{'invisible': [('move_type', 'not in', ('in_invoice', 'in_refund', 'in_receipt'))]}"/>
@@ -1193,7 +1193,7 @@
<field name="invoice_user_id" domain="[('share', '=', False)]" widget="many2one_avatar_user"/>
<field name="invoice_origin" string="Source Document" force_save="1" invisible="1"/>
<field name="partner_bank_id"
context="{'default_partner_id': bank_partner_id}"
context="{'default_partner_id': bank_partner_id, 'display_account_trust': True}"
domain="[('partner_id', '=', bank_partner_id)]"
attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<field name="qr_code_method"
@@ -1495,7 +1495,7 @@
<field name="view_id" ref="view_out_credit_note_tree"/>
<field name="search_view_id" ref="view_account_invoice_filter"/>
<field name="domain">[('move_type', '=', 'out_refund')]</field>
<field name="context">{'default_move_type': 'out_refund'}</field>
<field name="context">{'default_move_type': 'out_refund', 'display_account_trust': True}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a credit note
@@ -1513,7 +1513,7 @@
<field name="view_id" ref="view_in_invoice_bill_tree"/>
<field name="search_view_id" ref="view_account_invoice_filter"/>
<field name="domain">[('move_type', '=', 'in_invoice')]</field>
<field name="context">{'default_move_type': 'in_invoice'}</field>
<field name="context">{'default_move_type': 'in_invoice', 'display_account_trust': True}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a vendor bill
@@ -1565,7 +1565,7 @@
<field name="view_id" ref="view_in_invoice_receipt_tree"/>
<field name="search_view_id" ref="view_account_invoice_filter"/>
<field name="domain">[('move_type', '=', 'in_receipt')]</field>
<field name="context">{'default_move_type': 'in_receipt'}</field>
<field name="context">{'default_move_type': 'in_receipt', 'display_account_trust': True}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Register a new purchase receipt
@@ -282,14 +282,14 @@
attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<field name="partner_bank_id" string="Customer Bank Account"
context="{'default_partner_id': partner_id, 'default_allow_out_payment': True}"
context="{'default_partner_id': partner_id, 'display_account_trust': True}"
attrs="{
'invisible': ['|', '|', '|', ('show_partner_bank_account', '=', False), ('partner_type','!=','customer'), ('is_internal_transfer', '=', True), ('payment_type', '=', 'inbound')],
'required': [('require_partner_bank_account', '=', True), ('is_internal_transfer', '=', False)],
}"/>
<field name="partner_bank_id" string="Vendor Bank Account"
context="{'default_partner_id': partner_id, 'default_allow_out_payment': True}"
context="{'default_partner_id': partner_id, 'display_account_trust': True}"
attrs="{
'invisible': ['|', '|', '|', ('show_partner_bank_account', '=', False), ('partner_type','!=','supplier'), ('is_internal_transfer', '=', True), ('payment_type', '=', 'inbound')],
'required': [('require_partner_bank_account', '=', True), ('is_internal_transfer', '=', False)],
@@ -298,7 +298,7 @@
<!-- This field should always be readonly but using readonly="1" overrides the other partner_bank_id
fields readonly condition in the framework, preventing the modification of these fields -->
<field name="partner_bank_id" string="Company Bank Account"
context="{'default_partner_id': partner_id, 'default_allow_out_payment': True}"
context="{'default_partner_id': partner_id, 'display_account_trust': True}"
attrs="{
'invisible': ['|', '|', ('show_partner_bank_account', '=', False), ('is_internal_transfer', '=', True), ('payment_type', '=', 'outbound')],
'required': [('require_partner_bank_account', '=', True), ('is_internal_transfer', '=', False)],
@@ -349,6 +349,7 @@
'default_partner_type': 'customer',
'search_default_inbound_filter': 1,
'default_move_journal_types': ('bank', 'cash'),
'display_account_trust': True,
}</field>
<field name="view_id" ref="view_account_payment_tree"/>
<field name="help" type="html">
@@ -369,6 +370,7 @@
'default_partner_type': 'supplier',
'search_default_outbound_filter': 1,
'default_move_journal_types': ('bank', 'cash'),
'display_account_trust': True,
}</field>
<field name="view_id" ref="view_account_supplier_payment_tree"/>
<field name="help" type="html">
@@ -384,7 +386,7 @@
<field name="name">Internal Transfers</field>
<field name="res_model">account.payment</field>
<field name="view_mode">tree,kanban,form,graph</field>
<field name="context">{'default_payment_type': 'outbound', 'search_default_transfers_filter': 1}</field>
<field name="context">{'default_payment_type': 'outbound', 'search_default_transfers_filter': 1, 'display_account_trust': True}</field>
<field name="domain">[]</field>
<field name="view_id" ref="view_account_supplier_payment_tree"/>
<field name="help" type="html">
+4 -10
View File
@@ -208,21 +208,15 @@
<field name="show_credit_limit" invisible="1"/>
<group>
<group string="Bank Accounts" name="banks" groups="account.group_account_invoice,account.group_account_readonly">
<field name="bank_ids" nolabel="1" colspan="2" context="{'default_allow_out_payment': True}">
<tree editable="bottom">
<field name="bank_ids" nolabel="1" colspan="2" context="{'default_partner_id': active_id}">
<tree>
<field name="sequence" widget="handle"/>
<field name="bank_id"/>
<field name="acc_number"/>
<field name="allow_out_payment" widget="boolean_toggle"/>
<field name="bank_id"/>
<field name="allow_out_payment"/>
<field name="acc_holder_name" invisible="1"/>
</tree>
</field>
<button type="action" class="btn-link"
name="%(base.action_res_partner_bank_account_form)d"
context="{'search_default_partner_id': active_id, 'default_partner_id': active_id}"
string="View accounts detail"
colspan="2"
/>
</group>
<group string="Accounting Entries" name="accounting_entries" groups="account.group_account_readonly">
<field name="currency_id" invisible="1"/>
@@ -15,6 +15,67 @@
<field name="activity_ids" widget="mail_activity"/>
</div>
</xpath>
<xpath expr="//field[@name='acc_number']" position="attributes">
<attribute name="attrs">{'readonly': [('allow_out_payment', '=', True)]}</attribute>
</xpath>
<xpath expr="//field[@name='partner_id']" position="attributes">
<attribute name="attrs">{'readonly': [('allow_out_payment', '=', True)]}</attribute>
</xpath>
<xpath expr="//field[@name='allow_out_payment']" position="replace">
<field name="user_has_group_validate_bank_account" invisible="1"/>
<label for="allow_out_payment"/>
<div class="d-flex gap-2">
<field name="allow_out_payment" widget="boolean_toggle" attrs="{'readonly': [('user_has_group_validate_bank_account', '=', False)]}"/>
<span class="d-flex gap-2 w-100">
<span attrs="{'invisible': [('allow_out_payment', '=', True)]}" class="text-muted">Untrusted</span>
<span attrs="{'invisible': [('allow_out_payment', '=', False)]}" class="text-success">Trusted</span>
</span>
</div>
<div attrs="{'invisible': [('allow_out_payment', '=', True)]}" colspan="2">
<field name="has_money_transfer_warning" invisible="1"/>
<div attrs="{'invisible': [('has_money_transfer_warning', '=', False)]}" class="mb-1">
<span class="text-danger">High risk</span>: <field name="money_transfer_service" nolabel="1" class="oe_inline"/> is a money transfer service and not a bank.
Double check if the account can be trusted by calling the vendor.<br/>
<a href="https://www.odoo.com/documentation/master/applications/finance/accounting/payables/pay/trusted_accounts.html">Check why.</a>
</div>
<field name="has_iban_warning" invisible="1"/>
<div attrs="{'invisible': [('has_iban_warning', '=', False)]}">
<span class="text-warning">Medium risk</span>: Iban <field name="sanitized_acc_number" nolabel="1" class="oe_inline fw-bold"/>
is not from the same country as the partner (<field name="partner_country_name" nolabel="1" class="oe_inline"/>).<br/>
<a href="https://www.odoo.com/documentation/master/applications/finance/accounting/payables/pay/trusted_accounts.html">Check why it's risky.</a>
</div>
</div>
</xpath>
</field>
</record>
<record id="view_partner_bank_search_inherit" model="ir.ui.view">
<field name="name">res.partner.bank.search.inherit</field>
<field name="model">res.partner.bank</field>
<field name="inherit_id" ref="base.view_partner_bank_search"/>
<field name="arch" type="xml">
<xpath expr="//search" position="replace">
<search string="Bank Accounts">
<filter string="Trusted" name="trusted" domain="[('allow_out_payment', '=', True)]"/>
<filter string="Untrusted" name="untrusted" domain="[('allow_out_payment', '=', False)]"/>
<filter string="To validate" name="to_validate" domain="[('allow_out_payment', '=', False), ('related_moves', '!=', False)]"/>
<separator/>
<filter string="Customers" name="customer" domain="[('partner_customer_rank','>', 0)]"/>
<filter string="Vendors" name="supplier" domain="[('partner_supplier_rank','>', 0)]"/>
<separator/>
<filter string="Phishing risk: High" name="high_phishing_risk" domain="[('has_money_transfer_warning', '!=', False)]"/>
<filter string="Phishing risk: Medium" name="medium_phishing_risk" domain="[('has_iban_warning', '!=', False)]"/>
<separator/>
<filter string="Created On" name="create_date" date="create_date"/>
<group expand="1" string="Group By">
<filter string="Created On" name="groupby_create_date" context="{'group_by': 'create_date'}"/>
<filter string="Created By" name="groupby_create_by" context="{'group_by': 'create_uid'}"/>
</group>
</search>
</xpath>
</field>
</record>
@@ -28,5 +89,18 @@ action = model.setting_init_bank_account_action()
<field name="groups_id" eval="[(4, ref('account.group_account_manager'))]"/>
</record>
<record id="action_account_supplier_accounts" model="ir.actions.act_window">
<field name="name">Bank Accounts</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">res.partner.bank</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="base.view_partner_bank_tree"/>
<field name="search_view_id" ref="view_partner_bank_search_inherit"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new bank account
</p>
</field>
</record>
</data>
</odoo>
@@ -126,6 +126,11 @@ class AccountPaymentRegister(models.TransientModel):
compute='_compute_show_require_partner_bank') # used to know whether the field `partner_bank_id` should be required
country_code = fields.Char(related='company_id.account_fiscal_country_id.code', readonly=True)
# == trust check ==
untrusted_bank_ids = fields.Many2many('res.partner.bank', compute='_compute_trust_values')
total_payments_amount = fields.Integer(compute='_compute_trust_values')
untrusted_payments_count = fields.Integer(compute='_compute_trust_values')
# -------------------------------------------------------------------------
# HELPERS
# -------------------------------------------------------------------------
@@ -323,6 +328,30 @@ class AccountPaymentRegister(models.TransientModel):
# COMPUTE METHODS
# -------------------------------------------------------------------------
@api.depends('payment_method_line_id', 'line_ids', 'group_payment')
def _compute_trust_values(self):
for wizard in self:
batches = wizard._get_batches()
total_payment_count = 0
untrusted_payments_count = 0
untrusted_accounts = self.env['res.partner.bank']
# Validate batches; if require_partner_bank_account and the account isn't trusted, we do not allow the payment
for batch in batches:
payment_count = 1 if wizard.group_payment else len(batch['lines'])
total_payment_count += payment_count
batch_account = wizard._get_batch_account(batch)
if wizard.require_partner_bank_account and not batch_account.allow_out_payment:
untrusted_payments_count += payment_count
untrusted_accounts |= batch_account
wizard.update({
'total_payments_amount': total_payment_count,
'untrusted_payments_count': untrusted_payments_count,
'untrusted_bank_ids': untrusted_accounts or False,
})
@api.depends('line_ids')
def _compute_from_lines(self):
''' Load initial values from the account.moves passed through the context. '''
@@ -834,7 +863,18 @@ class AccountPaymentRegister(models.TransientModel):
def _create_payments(self):
self.ensure_one()
batches = self._get_batches()
all_batches = self._get_batches()
batches = []
# Skip batches that are not valid (bank account not trusted but required)
for batch in all_batches:
batch_account = self._get_batch_account(batch)
if self.require_partner_bank_account and not batch_account.allow_out_payment:
continue
batches.append(batch)
if not batches:
raise UserError(_('To record payments with %s, the recipient bank account must be manually validated. You should go on the partner bank account in order to validate it.', self.payment_method_line_id.name))
first_batch_result = batches[0]
edit_mode = self.can_edit_wizard and (len(first_batch_result['lines']) == 1 or self.group_payment)
to_process = []
@@ -897,3 +937,32 @@ class AccountPaymentRegister(models.TransientModel):
'domain': [('id', 'in', payments.ids)],
})
return action
def _get_batch_account(self, batch_result):
# Get the batch bank account
partner_bank_id = batch_result['payment_values']['partner_bank_id']
available_partner_banks = self._get_batch_available_partner_banks(batch_result, self.journal_id)
if partner_bank_id and partner_bank_id in available_partner_banks.ids:
return self.env['res.partner.bank'].browse(partner_bank_id)
else:
return available_partner_banks[:1]
def action_open_untrusted_bank_accounts(self):
self.ensure_one()
if len(self.untrusted_bank_ids) == 1:
action = {
"view_mode": "form",
"res_model": "res.partner.bank",
"type": "ir.actions.act_window",
"res_id": self.untrusted_bank_ids.id,
"views": [[self.env.ref("account.view_partner_bank_form_inherit_account").id, "form"]],
}
else:
action = {
"type": "ir.actions.act_window",
"res_model": "res.partner.bank",
"views": [[False, "tree"], [self.env.ref("account.view_partner_bank_form_inherit_account").id, "form"]],
"domain": [["id", "in", self.untrusted_bank_ids.ids]],
}
return action
@@ -29,10 +29,14 @@
<field name="available_partner_bank_ids" invisible="1"/>
<field name="company_currency_id" invisible="1"/>
<field name="hide_writeoff_section" invisible="1"/>
<field name="untrusted_bank_ids" invisible="1"/>
<div role="alert" class="alert alert-info" attrs="{'invisible': [('hide_writeoff_section', '=', False)]}">
<p><b>Early Payment Discount applied.</b></p>
</div>
<div role="alert" class="alert alert-warning" attrs="{'invisible': [('untrusted_payments_count', '=', 0)]}">
<span class="fw-bold"><field name="untrusted_payments_count" class="oe_inline"/> out of <field name="total_payments_amount" class="oe_inline"/> payments will be skipped due to <button class="oe_link p-0 align-baseline" type="object" name="action_open_untrusted_bank_accounts">untrusted bank accounts</button>.</span>
</div>
<group>
<group name="group1">
<field name="journal_id" options="{'no_open': True, 'no_create': True}" required="1"/>
@@ -41,7 +45,7 @@
<field name="partner_bank_id"
attrs="{'invisible': ['|', ('show_partner_bank_account', '=', False), '|', ('can_edit_wizard', '=', False), '&amp;', ('can_group_payments', '=', True), ('group_payment', '=', False)],
'required': [('require_partner_bank_account', '=', True), ('can_edit_wizard', '=', True), '|', ('can_group_payments', '=', False), ('group_payment', '=', False)], 'readonly': [('payment_type', '=', 'inbound')]}"
context="{'default_allow_out_payment': True}"/>
context="{'display_account_trust': True}"/>
<field name="group_payment"
attrs="{'invisible': [('can_group_payments', '=', False)]}"/>
</group>
@@ -79,8 +83,9 @@
</group>
</group>
<footer>
<button string="Create Payment" name="action_create_payments" type="object" class="oe_highlight" data-hotkey="q"/>
<button string="Cancel" class="btn btn-secondary" special="cancel" data-hotkey="z"/>
<button string="Create Payments" name="action_create_payments" type="object" class="oe_highlight" data-hotkey="q" attrs="{'invisible': [('total_payments_amount', '=', 1)]}"/>
<button string="Create Payment" name="action_create_payments" type="object" class="oe_highlight" data-hotkey="q" attrs="{'invisible': [('total_payments_amount', '!=', 1)]}"/>
<button string="Discard" class="btn btn-secondary" special="cancel" data-hotkey="z"/>
</footer>
</form>
</field>
+7 -2
View File
@@ -76,7 +76,7 @@ class ResPartnerBank(models.Model):
acc_type = fields.Selection(selection=lambda x: x.env['res.partner.bank'].get_supported_account_types(), compute='_compute_acc_type', string='Type', help='Bank account type: Normal or IBAN. Inferred from the bank account number.')
acc_number = fields.Char('Account Number', required=True)
sanitized_acc_number = fields.Char(compute='_compute_sanitized_acc_number', string='Sanitized Account Number', readonly=True, store=True)
acc_holder_name = fields.Char(string='Account Holder Name', help="Account holder name, in case it is different than the name of the Account Holder")
acc_holder_name = fields.Char(string='Account Holder Name', help="Account holder name, in case it is different than the name of the Account Holder", compute='_compute_account_holder_name', readonly=False, store=True)
partner_id = fields.Many2one('res.partner', 'Account Holder', ondelete='cascade', index=True, domain=['|', ('is_company', '=', True), ('parent_id', '=', False)], required=True)
allow_out_payment = fields.Boolean('Send Money', help='This account can be used for outgoing payments', default=False, copy=False, readonly=False)
bank_id = fields.Many2one('res.bank', string='Bank')
@@ -102,12 +102,17 @@ class ResPartnerBank(models.Model):
for bank in self:
bank.acc_type = self.retrieve_acc_type(bank.acc_number)
@api.depends('partner_id')
def _compute_account_holder_name(self):
for bank in self:
bank.acc_holder_name = bank.partner_id.name
@api.model
def retrieve_acc_type(self, acc_number):
""" To be overridden by subclasses in order to support other account_types.
"""
return 'bank'
def name_get(self):
return [(acc.id, '{} - {}'.format(acc.acc_number, acc.bank_id.name) if acc.bank_id else acc.acc_number)
for acc in self]
+5 -5
View File
@@ -87,12 +87,12 @@
<field name="sequence" invisible="1"/>
<field name="acc_type" invisible="1"/>
<field name="acc_number"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
<field name="partner_id"/>
<field name="bank_id"/>
<field name="acc_holder_name"/>
<field name="partner_id"/>
</group>
<group>
<field name="bank_id"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
<field name="currency_id" groups="base.group_multi_currency" options="{'no_create': True}"/>
<field name="allow_out_payment" widget="boolean_toggle"/>
<field name="active" invisible="1"/>
@@ -107,10 +107,10 @@
<field name="name">res.partner.bank.tree</field>
<field name="model">res.partner.bank</field>
<field name="arch" type="xml">
<tree string="Bank Accounts">
<tree string="Bank Accounts" multi_edit="1">
<field name="sequence" widget="handle"/>
<field name="acc_number"/>
<field name="bank_name"/>
<field name="bank_name" string="Bank"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="partner_id"/>
<field name="acc_holder_name" invisible="1"/>