mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
[IMP] sale_payment_web: migrate from 13.0 to 14.0
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
'name': 'Sale Payment Web',
|
'name': 'Sale Payment Web',
|
||||||
'author': 'Hibou Corp. <hello@hibou.io>',
|
'author': 'Hibou Corp. <hello@hibou.io>',
|
||||||
'category': 'Sales',
|
'category': 'Sales',
|
||||||
'version': '13.0.1.0.0',
|
'version': '14.0.1.0.0',
|
||||||
'description':
|
'description':
|
||||||
"""
|
"""
|
||||||
Sale Payment Web
|
Sale Payment Web
|
||||||
@@ -21,7 +21,8 @@ Electronic payments will create transactions and automatically reconcile on the
|
|||||||
'data': [
|
'data': [
|
||||||
'security/sale_security.xml',
|
'security/sale_security.xml',
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'wizard/account_payment_register_views.xml',
|
'views/payment_views.xml',
|
||||||
'views/sale_views.xml',
|
'views/sale_views.xml',
|
||||||
|
'wizard/account_payment_register_views.xml',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ class SaleOrder(models.Model):
|
|||||||
'name': _('Register Payment'),
|
'name': _('Register Payment'),
|
||||||
'res_model': 'account.payment.register',
|
'res_model': 'account.payment.register',
|
||||||
'view_mode': 'form',
|
'view_mode': 'form',
|
||||||
'view_id': self.env.ref('account.view_account_payment_form_multi').id,
|
|
||||||
'context': {'active_ids': self.ids, 'active_model': 'sale.order'},
|
'context': {'active_ids': self.ids, 'active_model': 'sale.order'},
|
||||||
'target': 'new',
|
'target': 'new',
|
||||||
'type': 'ir.actions.act_window',
|
'type': 'ir.actions.act_window',
|
||||||
|
|||||||
@@ -1,42 +1,49 @@
|
|||||||
from odoo.addons.sale.tests.test_sale_to_invoice import TestSaleToInvoice
|
from odoo.addons.sale.tests.test_sale_to_invoice import TestSaleToInvoice
|
||||||
from odoo.exceptions import UserError
|
from odoo.exceptions import UserError
|
||||||
|
from odoo.tests import tagged
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install')
|
||||||
class TestSalePayment(TestSaleToInvoice):
|
class TestSalePayment(TestSaleToInvoice):
|
||||||
|
|
||||||
def setUp(self):
|
def _sale_context(self):
|
||||||
super(TestSalePayment, self).setUp()
|
return {
|
||||||
self.context = {
|
|
||||||
'active_model': 'sale.order',
|
'active_model': 'sale.order',
|
||||||
'active_ids': [self.sale_order.id],
|
'active_ids': [self.sale_order.id],
|
||||||
'active_id': self.sale_order.id,
|
'active_id': self.sale_order.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_payment(self):
|
def test_00_payment(self):
|
||||||
self.sale_order.action_confirm()
|
self.sale_order.action_confirm()
|
||||||
|
|
||||||
payment_wizard = self.env['account.payment.register'].with_context(self.context).create({'amount': -15})
|
payment_wizard = self.env['account.payment.register'].with_context(self._sale_context()).create({'amount': -15})
|
||||||
self.assertTrue(payment_wizard.journal_id)
|
self.assertTrue(payment_wizard.journal_id)
|
||||||
with self.assertRaises(UserError):
|
with self.assertRaises(UserError):
|
||||||
payment_wizard.create_payments()
|
payment_wizard._create_payments()
|
||||||
|
|
||||||
payment_wizard = self.env['account.payment.register'].with_context(self.context).create({'amount': 0})
|
payment_wizard = self.env['account.payment.register'].with_context(self._sale_context()).create({'amount': 0})
|
||||||
self.assertTrue(payment_wizard.journal_id)
|
self.assertTrue(payment_wizard.journal_id)
|
||||||
with self.assertRaises(UserError):
|
with self.assertRaises(UserError):
|
||||||
payment_wizard.create_payments()
|
payment_wizard._create_payments()
|
||||||
|
|
||||||
payment_wizard = self.env['account.payment.register'].with_context(self.context).create({})
|
payment_wizard = self.env['account.payment.register'].with_context(self._sale_context()).create({})
|
||||||
self.assertTrue(payment_wizard.journal_id)
|
self.assertTrue(payment_wizard.journal_id)
|
||||||
|
|
||||||
payment_action = payment_wizard.create_payments()
|
payment = payment_wizard._create_payments()
|
||||||
self.assertTrue(isinstance(payment_action, dict))
|
|
||||||
payment = self.env[payment_action['res_model']].browse(payment_action['res_id'])
|
|
||||||
self.assertTrue(payment.exists())
|
|
||||||
self.assertEqual(payment.amount, self.sale_order.amount_total)
|
self.assertEqual(payment.amount, self.sale_order.amount_total)
|
||||||
|
|
||||||
self.assertEqual(payment.sale_order_id, self.sale_order)
|
self.assertEqual(payment.sale_order_id, self.sale_order)
|
||||||
|
self.assertEqual(self.sale_order.manual_amount_remaining, 0.0)
|
||||||
|
|
||||||
payment_wizard = self.env['account.payment.register'].with_context(self.context).create({'amount': 15})
|
payment_wizard = self.env['account.payment.register'].with_context(self._sale_context()).create({'amount': 15})
|
||||||
self.assertTrue(payment_wizard.journal_id)
|
self.assertTrue(payment_wizard.journal_id)
|
||||||
with self.assertRaises(UserError):
|
with self.assertRaises(UserError):
|
||||||
payment_wizard.create_payments()
|
payment_wizard._create_payments()
|
||||||
|
|
||||||
|
def test_10_partial_payment(self):
|
||||||
|
payment_wizard = self.env['account.payment.register'].with_context(self._sale_context()).create({'amount': 50})
|
||||||
|
self.assertTrue(payment_wizard.journal_id)
|
||||||
|
|
||||||
|
payment = payment_wizard._create_payments()
|
||||||
|
self.assertEqual(payment.amount, 50)
|
||||||
|
self.assertEqual(payment.sale_order_id, self.sale_order)
|
||||||
|
self.assertEqual(self.sale_order.manual_amount_remaining, self.sale_order.amount_total - 50)
|
||||||
|
|||||||
15
sale_payment_web/views/payment_views.xml
Normal file
15
sale_payment_web/views/payment_views.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_account_payment_form_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">account.payment.form.inherit</field>
|
||||||
|
<field name="model">account.payment</field>
|
||||||
|
<field name="inherit_id" ref="account.view_account_payment_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='move_id']" position="after">
|
||||||
|
<field name="sale_order_id" attrs="{'invisible': [('state', '!=', 'draft'), ('sale_order_id', '=', False)], 'readonly': [('state', '!=', 'draft')]}" groups="account.group_account_readonly"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
@@ -8,7 +8,10 @@
|
|||||||
<field name="groups_id" eval="[(4, ref('group_payment_web'))]"/>
|
<field name="groups_id" eval="[(4, ref('group_payment_web'))]"/>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//button[@name='action_draft']" position="after">
|
<xpath expr="//button[@name='action_draft']" position="after">
|
||||||
<button name="action_payment_register" type="object" string="Register Payment"/>
|
<button name="action_payment_register"
|
||||||
|
type="object"
|
||||||
|
context="{'dont_redirect_to_payments': True}"
|
||||||
|
string="Register Payment"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//field[@name='amount_total']" position="after">
|
<xpath expr="//field[@name='amount_total']" position="after">
|
||||||
<field name="manual_amount_remaining" widget="monetary"/>
|
<field name="manual_amount_remaining" widget="monetary"/>
|
||||||
|
|||||||
@@ -6,129 +6,124 @@ class AccountPaymentRegister(models.TransientModel):
|
|||||||
_inherit = 'account.payment.register'
|
_inherit = 'account.payment.register'
|
||||||
|
|
||||||
sale_order_id = fields.Many2one('sale.order', string='Sale Order')
|
sale_order_id = fields.Many2one('sale.order', string='Sale Order')
|
||||||
payment_token_id = fields.Many2one('payment.token', string="Saved payment token",
|
|
||||||
domain="[('acquirer_id.capture_manually', '=', False), ('company_id', '=', company_id)]",
|
|
||||||
help="Note that tokens from acquirers set to only authorize transactions "
|
|
||||||
"(instead of capturing the amount) are not available.")
|
|
||||||
company_id = fields.Many2one('res.company') # technical requirement for acquirer domain
|
|
||||||
partner_id = fields.Many2one('res.partner') # technical payment mode/token domain
|
|
||||||
|
|
||||||
currency_id = fields.Many2one(related='journal_id.currency_id')
|
|
||||||
amount = fields.Monetary('Amount')
|
|
||||||
so_amount_registered_payment = fields.Monetary(related='sale_order_id.manual_amount_registered_payment')
|
|
||||||
so_amount_remaining = fields.Monetary(related='sale_order_id.manual_amount_remaining')
|
so_amount_remaining = fields.Monetary(related='sale_order_id.manual_amount_remaining')
|
||||||
so_amount_over = fields.Boolean()
|
so_amount_over = fields.Boolean()
|
||||||
|
|
||||||
@api.onchange('amount')
|
@api.onchange('amount')
|
||||||
def _compute_amount_remaining(self):
|
def _compute_amount_remaining(self):
|
||||||
self.so_amount_over = self.amount > self.so_amount_remaining
|
self.so_amount_over = self.sale_order_id and self.amount > self.so_amount_remaining
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def default_get(self, fields):
|
def default_get(self, fields):
|
||||||
# super implementation checks active_ids, but not active_model
|
# super implementation checks active_ids, but not active_model
|
||||||
active_ids = self._context.get('active_ids')
|
active_ids = self._context.get('active_ids')
|
||||||
if self._context.get('active_model') != 'sale.order' or not active_ids:
|
if self._context.get('active_model') == 'sale.order' and 'line_ids' in fields:
|
||||||
return super(AccountPaymentRegister, self).default_get(fields)
|
fields.remove('line_ids')
|
||||||
|
rec = super(AccountPaymentRegister, self).default_get(fields)
|
||||||
rec = super(AccountPaymentRegister, self.with_context(active_ids=None)).default_get(fields)
|
if self._context.get('active_model') != 'sale.order':
|
||||||
|
return rec
|
||||||
sale_order = self.env['sale.order'].browse(active_ids)
|
sale_order = self.env['sale.order'].browse(active_ids)
|
||||||
if len(sale_order) != 1:
|
if len(sale_order) != 1:
|
||||||
raise UserError('Register payment must be called on a sale order.')
|
raise UserError('Register payment must be called on a sale order.')
|
||||||
|
|
||||||
if 'sale_order_id' not in rec:
|
if 'sale_order_id' not in rec:
|
||||||
rec['sale_order_id'] = sale_order.id
|
rec['sale_order_id'] = sale_order.id
|
||||||
rec['so_amount_registered_payment'] = sale_order.manual_amount_registered_payment
|
return rec
|
||||||
rec['so_amount_remaining'] = sale_order.manual_amount_remaining
|
|
||||||
if 'amount' not in rec:
|
def _compute_from_lines(self):
|
||||||
rec['amount'] = sale_order.manual_amount_remaining
|
wizards = self.filtered('sale_order_id')
|
||||||
if 'company_id' not in rec:
|
super(AccountPaymentRegister, self - wizards)._compute_from_lines()
|
||||||
rec['company_id'] = sale_order.company_id.id
|
for wizard in wizards:
|
||||||
if 'partner_id' not in rec:
|
sale_order = wizard.sale_order_id
|
||||||
rec['partner_id'] = sale_order.partner_id.id
|
|
||||||
account_receivable = sale_order.partner_id.property_account_receivable_id
|
account_receivable = sale_order.partner_id.property_account_receivable_id
|
||||||
if not account_receivable:
|
if not account_receivable:
|
||||||
raise UserError('Your partner must have an Account Receivable setup.')
|
raise UserError('Your partner must have an Account Receivable setup.')
|
||||||
if 'journal_id' not in rec:
|
company = sale_order.company_id
|
||||||
rec['journal_id'] = self.env['account.journal'].search([('company_id', '=', sale_order.company_id.id), ('type', 'in', ('bank', 'cash'))], limit=1).id
|
amount_currency = sale_order.manual_amount_remaining
|
||||||
if 'payment_method_id' not in rec:
|
if sale_order.currency_id == company.currency_id:
|
||||||
domain = [('payment_type', '=', 'inbound')]
|
amount = amount_currency
|
||||||
rec['payment_method_id'] = self.env['account.payment.method'].search(domain, limit=1).id
|
else:
|
||||||
return rec
|
amount = sale_order.currency_id._convert(amount_currency, company.currency_id, company, fields.Date.context_today(self))
|
||||||
|
wizard.update({
|
||||||
@api.onchange('partner_id', 'payment_method_id', 'journal_id')
|
'company_id': company.id,
|
||||||
def _onchange_set_payment_token_id(self):
|
'partner_id': sale_order.partner_id.id,
|
||||||
res = {}
|
'partner_type': 'customer',
|
||||||
|
'payment_type': 'inbound',
|
||||||
if not self.payment_method_id.code == 'electronic' or not self.partner_id or not self.journal_id:
|
'source_currency_id': sale_order.currency_id.id,
|
||||||
self.payment_token_id = False
|
'source_amount': amount,
|
||||||
return res
|
'source_amount_currency': amount_currency,
|
||||||
|
'can_edit_wizard': True,
|
||||||
partners = self.partner_id | self.partner_id.commercial_partner_id | self.partner_id.commercial_partner_id.child_ids
|
'can_group_payments': False,
|
||||||
domain = [('partner_id', 'in', partners.ids), ('acquirer_id.journal_id', '=', self.journal_id.id)]
|
})
|
||||||
self.payment_token_id = self.env['payment.token'].search(domain, limit=1)
|
|
||||||
|
def _compute_communication(self):
|
||||||
res['domain'] = {'payment_token_id': domain}
|
# The communication can't be computed in '_compute_from_lines' because
|
||||||
return res
|
# it's a compute editable field and then, should be computed in a separated method.
|
||||||
|
wizards = self.filtered('sale_order_id')
|
||||||
@api.onchange('journal_id', 'invoice_ids', 'sale_order_id')
|
super(AccountPaymentRegister, self - wizards)._compute_communication()
|
||||||
def _onchange_journal(self):
|
for wizard in wizards:
|
||||||
active_ids = self._context.get('active_ids')
|
so = wizard.sale_order_id
|
||||||
if self._context.get('active_model') != 'sale.order' or not active_ids:
|
wizards.communication = so.reference or so.name
|
||||||
return super(AccountPaymentRegister, self)._onchange_journal()
|
|
||||||
journal = self.journal_id
|
def _compute_group_payment(self):
|
||||||
if journal:
|
wizards = self.filtered('sale_order_id')
|
||||||
domain_payment = [('payment_type', '=', 'inbound'), ('id', 'in', journal.inbound_payment_method_ids.ids)]
|
super(AccountPaymentRegister, self - wizards)._compute_group_payment()
|
||||||
return {'domain': {'payment_method_id': domain_payment}}
|
wizards.write({'group_payment': False})
|
||||||
|
|
||||||
def _prepare_communication_sale_orders(self, sale_order):
|
def _compute_journal_id(self):
|
||||||
return " ".join(o.reference or o.name for o in sale_order)
|
wizards = self.filtered('sale_order_id')
|
||||||
|
super(AccountPaymentRegister, self - wizards)._compute_journal_id()
|
||||||
def _prepare_payment_vals_sale_orders(self, sale_order):
|
for wizard in wizards:
|
||||||
'''Create the payment values.
|
wizard.journal_id = self.env['account.journal'].search([
|
||||||
|
('type', 'in', ('bank', 'cash')),
|
||||||
:param sale_order: The sale orders to pay. In case of multiple
|
('company_id', '=', wizard.company_id.id),
|
||||||
documents.
|
], limit=1)
|
||||||
:return: The payment values as a dictionary.
|
|
||||||
'''
|
def _compute_available_partner_bank_ids(self):
|
||||||
|
wizards = self.filtered('sale_order_id')
|
||||||
|
super(AccountPaymentRegister, self - wizards)._compute_available_partner_bank_ids()
|
||||||
|
for wizard in wizards:
|
||||||
|
wizard.available_partner_bank_ids = wizard.journal_id.bank_account_id
|
||||||
|
|
||||||
|
def _compute_partner_bank_id(self):
|
||||||
|
wizards = self.filtered('sale_order_id')
|
||||||
|
super(AccountPaymentRegister, self - wizards)._compute_partner_bank_id()
|
||||||
|
wizards.write({'partner_bank_id': None})
|
||||||
|
|
||||||
|
def _compute_suitable_payment_token_partner_ids(self):
|
||||||
|
""" Override from `payment_fix_register_token` """
|
||||||
|
wizards = self.filtered('sale_order_id')
|
||||||
|
super(AccountPaymentRegister, self - wizards)._compute_suitable_payment_token_partner_ids()
|
||||||
|
for wizard in wizards:
|
||||||
|
partners = wizard.sale_order_id.partner_id
|
||||||
|
commercial_partners = partners.commercial_partner_id
|
||||||
|
children_partners = commercial_partners.child_ids
|
||||||
|
wizard.suitable_payment_token_partner_ids = (partners + commercial_partners + children_partners)._origin
|
||||||
|
|
||||||
|
def _create_payment_vals_from_sale_order(self):
|
||||||
if self.amount <= 0:
|
if self.amount <= 0:
|
||||||
raise UserError("You must enter a positive amount.")
|
raise UserError("You must enter a positive amount.")
|
||||||
elif self.amount > self.so_amount_remaining:
|
elif self.amount > self.so_amount_remaining:
|
||||||
raise UserError("You cannot make a payment for more than the difference of the total amount and existing "
|
raise UserError("You cannot make a payment for more than the difference of the total amount and existing "
|
||||||
"payments: %.2f" % self.so_amount_remaining)
|
"payments: %.2f" % self.so_amount_remaining)
|
||||||
values = {
|
return {
|
||||||
'journal_id': self.journal_id.id,
|
'date': self.payment_date,
|
||||||
'payment_method_id': self.payment_method_id.id,
|
|
||||||
'payment_date': self.payment_date,
|
|
||||||
'communication': self._prepare_communication_sale_orders(sale_order),
|
|
||||||
# TODO sale orders need to get to transactions somehow
|
|
||||||
# 'invoice_ids': [(6, 0, invoices.ids)],
|
|
||||||
'payment_type': 'inbound', # if amount can be negative we need to allow this to be outbound
|
|
||||||
'amount': self.amount,
|
'amount': self.amount,
|
||||||
'currency_id': sale_order.currency_id.id,
|
'payment_type': self.payment_type,
|
||||||
'partner_id': sale_order.partner_id.id,
|
'partner_type': self.partner_type,
|
||||||
'partner_type': 'customer',
|
'ref': self.communication,
|
||||||
'payment_token_id': self.payment_token_id.id,
|
'journal_id': self.journal_id.id,
|
||||||
|
'currency_id': self.currency_id.id,
|
||||||
|
'partner_id': self.partner_id.id,
|
||||||
|
'partner_bank_id': self.partner_bank_id.id,
|
||||||
|
'payment_method_id': self.payment_method_id.id,
|
||||||
|
'sale_order_id': self.sale_order_id.id,
|
||||||
}
|
}
|
||||||
return values
|
|
||||||
|
def _create_payments(self):
|
||||||
def get_payments_vals(self):
|
self.ensure_one()
|
||||||
if self.sale_order_id:
|
if not self.sale_order_id:
|
||||||
return [self._prepare_payment_vals_sale_orders(self.sale_order_id)]
|
return super(AccountPaymentRegister, self)._create_payments()
|
||||||
return super(AccountPaymentRegister, self).get_payments_vals()
|
payments = self.env['account.payment'].create(self._create_payment_vals_from_sale_order())
|
||||||
|
payments.action_post()
|
||||||
def create_payments(self):
|
return payments
|
||||||
if self.sale_order_id:
|
|
||||||
# user may not be able to create payment
|
|
||||||
res = super(AccountPaymentRegister, self.sudo()).create_payments()
|
|
||||||
else:
|
|
||||||
res = super(AccountPaymentRegister, self).create_payments()
|
|
||||||
if res and 'res_id' in res and self.sale_order_id:
|
|
||||||
payment = self.env['account.payment'].browse(res['res_id'])
|
|
||||||
self.sale_order_id.manual_payment_ids += payment
|
|
||||||
if payment.name: # if we don't have a name, then it started a transaction and that will be in chatter
|
|
||||||
payment_link = payment._get_payment_chatter_link()
|
|
||||||
for order in self.sale_order_id:
|
|
||||||
order.message_post(body=_('A %s payment has been registered: %s') % (payment.payment_method_code, payment_link))
|
|
||||||
# return a 'dummy' action like object for tests
|
|
||||||
return {'res_id': payment.id, 'res_model': payment._name}
|
|
||||||
return res
|
|
||||||
|
|||||||
@@ -1,29 +1,28 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<odoo>
|
<odoo>
|
||||||
|
|
||||||
<record id="view_account_payment_form_multi_inherit" model="ir.ui.view">
|
<record id="view_account_payment_register_form_inherit" model="ir.ui.view">
|
||||||
<field name="name">account.payment.form.multi.inherit</field>
|
<field name="name">account.payment.form.multi.inherit</field>
|
||||||
<field name="model">account.payment.register</field>
|
<field name="model">account.payment.register</field>
|
||||||
<field name="inherit_id" ref="account.view_account_payment_form_multi"/>
|
<field name="inherit_id" ref="account.view_account_payment_register_form"/>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//group/group[1]" position="inside">
|
<xpath expr="//field[@name='company_currency_id']" position="after">
|
||||||
<field name="company_id" invisible="1" />
|
<field name="sale_order_id" invisible="1"/>
|
||||||
<field name="partner_id" invisible="1" />
|
|
||||||
<field name="payment_token_id" nocreate="1" />
|
|
||||||
</xpath>
|
|
||||||
<xpath expr="//field[@name='payment_date']" position="after">
|
|
||||||
<field name="currency_id" invisible="True"/>
|
|
||||||
<field name="amount"/>
|
|
||||||
<field name="so_amount_remaining" invisible="1"/>
|
|
||||||
<field name="so_amount_over" invisible="1"/>
|
<field name="so_amount_over" invisible="1"/>
|
||||||
<div attrs="{'invisible':[('so_amount_over', '=', False)]}" class="text-danger" colspan="4">
|
<field name="so_amount_remaining" invisible="1"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='payment_difference_handling']" position="attributes">
|
||||||
|
<attribute name="attrs">{'invisible': [('sale_order_id', '!=', False)]}</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//group[@name='group3']" position="after">
|
||||||
|
<group name="group_so_amount_over" attrs="{'invisible':[('so_amount_over', '=', False)]}" class="text-danger" colspan="4">
|
||||||
<p>
|
<p>
|
||||||
You cannot make a payment for more than the difference of the total amount and existing payments:
|
You cannot make a payment for more than the difference of the total amount and existing payments:
|
||||||
<field name="so_amount_remaining"/>
|
<field name="so_amount_remaining"/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</group>
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
Reference in New Issue
Block a user