[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:
@@ -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',
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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), '&', ('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>
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
Reference in New Issue
Block a user