[IMP] account{,fleet,lock}: add quick invoice encoding
This commit: - adds a fiduciary mode which allows fast encoding of invoices. This is an Accounting setting which must be activated. This fiduciary mode allows one to edit directly the invoice sequence and enter the total amount of the invoice (by creating a single line which contains the base price, the tax amount, and the most frequent account) such that we match this 'quick total amount'. - adds a new test for this feature - adds a 'quick edit total amount' in the tax computation which verifies that the 'quick edit total amount' correctly matches sum of the base and the tax (in case of fiduciary mode) - sorts and filters the accounts suggestions in an invoice line. The most frequent accounts for a particular partner rank higher while filtering on the type of account suggested (this is disabled if the user searches itself for the account) - selects by default the most frequent account for a particular partner when creating a new invoice line - adds the option to hide some columns (label & quantity) - moves the car column after the account column in case the Fleet app is installed Task-id: 2857253 Community PR: odoo#91938 Enterprise PR: odoo/enterprise#27605 Part-of: odoo/odoo#91938
This commit is contained in:
committed by
qdp-odoo
parent
f07439fbc8
commit
17d44d6617
@@ -442,8 +442,40 @@ class AccountAccount(models.Model):
|
||||
contextual_self = self.with_context(default_name=default_name, default_code=default_code)
|
||||
return super(AccountAccount, contextual_self).default_get(default_fields)
|
||||
|
||||
@api.model
|
||||
def _order_by_frequency_per_partner(self, company_id, partner_id, move_type=None, limit=None):
|
||||
"""
|
||||
Returns the accounts ordered from most used to least used for a given partner
|
||||
and filtered according to the move type.
|
||||
"""
|
||||
limit = "LIMIT 1" if limit is not None else ""
|
||||
where_internal_group = ""
|
||||
if move_type in self.env['account.move'].get_inbound_types(include_receipts=True):
|
||||
where_internal_group = "AND account.internal_group = 'income'"
|
||||
elif move_type in self.env['account.move'].get_outbound_types(include_receipts=True):
|
||||
where_internal_group = "AND account.internal_group = 'expense'"
|
||||
self._cr.execute(f"""
|
||||
SELECT account.id
|
||||
FROM account_account account
|
||||
LEFT JOIN account_move_line aml
|
||||
ON aml.account_id = account.id
|
||||
AND aml.partner_id = %s
|
||||
AND account.deprecated = FALSE
|
||||
AND account.company_id = aml.company_id
|
||||
AND aml.date >= now() - interval '2 years'
|
||||
WHERE account.company_id = %s
|
||||
{where_internal_group}
|
||||
GROUP BY account.id
|
||||
ORDER BY COUNT(aml.id) DESC, account.code
|
||||
{limit}
|
||||
""", [partner_id, company_id])
|
||||
return [r[0] for r in self._cr.fetchall()]
|
||||
|
||||
@api.model
|
||||
def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None):
|
||||
if not name and self._context.get('partner_id') and self._context.get('move_type'):
|
||||
return self._order_by_frequency_per_partner(
|
||||
self.env.company.id, self._context.get('partner_id'), self._context.get('move_type'))
|
||||
args = args or []
|
||||
domain = []
|
||||
if name:
|
||||
|
||||
@@ -385,6 +385,12 @@ class AccountMove(models.Model):
|
||||
readonly=True, states={'draft': [('readonly', False)]},
|
||||
help='Defines the smallest coinage of the currency that can be used to pay by cash.')
|
||||
|
||||
# Fiduciary mode fields
|
||||
quick_edit_mode = fields.Boolean(compute='_compute_quick_edit_mode')
|
||||
quick_edit_total_amount = fields.Monetary(string='Total (Tax inc.)',
|
||||
help='Use this field to encode the total amount of the invoice. '
|
||||
'Odoo will automatically create one invoice line with default values to match it.')
|
||||
|
||||
# ==== Display purpose fields ====
|
||||
invoice_filter_type_domain = fields.Char(compute='_compute_invoice_filter_type_domain',
|
||||
help="Technical field used to have a dynamic domain on journal / taxes in the form view.")
|
||||
@@ -540,7 +546,7 @@ class AccountMove(models.Model):
|
||||
if new_currency != self.currency_id:
|
||||
self.currency_id = new_currency
|
||||
self._onchange_currency()
|
||||
if self.state == 'draft' and self._get_last_sequence() and self.name and self.name != '/':
|
||||
if self.state == 'draft' and self.name and self.name != '/' and not self.quick_edit_mode and self._get_last_sequence():
|
||||
self.name = '/'
|
||||
|
||||
@api.onchange('partner_id')
|
||||
@@ -637,6 +643,121 @@ class AccountMove(models.Model):
|
||||
self.invoice_vendor_bill_id = False
|
||||
self._recompute_dynamic_lines()
|
||||
|
||||
@api.model
|
||||
def _get_frequent_account_and_taxes(self, company_id, partner_id, move_type):
|
||||
"""
|
||||
Returns the most used accounts and taxes for a given partner and company,
|
||||
eventually filtered according to the move type.
|
||||
"""
|
||||
where_internal_group = ""
|
||||
if move_type in self.env['account.move'].get_inbound_types(include_receipts=True):
|
||||
where_internal_group = "AND account.internal_group = 'income'"
|
||||
elif move_type in self.env['account.move'].get_outbound_types(include_receipts=True):
|
||||
where_internal_group = "AND account.internal_group = 'expense'"
|
||||
self._cr.execute(f"""
|
||||
SELECT
|
||||
COUNT(foo.id), foo.account_id, foo.taxes
|
||||
FROM
|
||||
(
|
||||
SELECT
|
||||
account.id AS account_id,
|
||||
account.code,
|
||||
aml.id,
|
||||
ARRAY_AGG(tax_rel.account_tax_id) AS taxes
|
||||
FROM account_account account
|
||||
LEFT JOIN account_move_line aml
|
||||
ON (account.id = aml.account_id
|
||||
AND aml.partner_id = %s
|
||||
AND aml.date >= now() - interval '2 years')
|
||||
LEFT JOIN account_move_line_account_tax_rel tax_rel ON (aml.id = tax_rel.account_move_line_id)
|
||||
WHERE
|
||||
account.company_id = %s
|
||||
AND account.deprecated = FALSE
|
||||
{where_internal_group}
|
||||
GROUP BY account.id, account.code, aml.id
|
||||
) AS foo
|
||||
GROUP BY foo.account_id, foo.code, foo.taxes
|
||||
ORDER BY COUNT(foo.id) DESC, foo.code
|
||||
LIMIT 1
|
||||
""", [partner_id, company_id])
|
||||
return self._cr.fetchone()
|
||||
|
||||
@api.model
|
||||
def _get_quick_edit_suggestions(self, amount_total, quick_edit_total_amount, move_type, journal_id, partner_id, fiscal_position_id):
|
||||
"""
|
||||
Returns a dictionnary containing the suggested values when creating a new
|
||||
line with the quick_edit_total_amount set. We will compute the price_unit
|
||||
that has to be set with the correct that in order to match this total amount.
|
||||
If the vendor/customer is set, we will suggest the most frequently used account
|
||||
for that partner as the default one, otherwise the default of the journal.
|
||||
"""
|
||||
if not quick_edit_total_amount:
|
||||
return False
|
||||
journal = self.env['account.journal'].browse(journal_id)
|
||||
count, account_id, tax_ids = self._get_frequent_account_and_taxes(journal.company_id.id, partner_id, move_type)
|
||||
if count:
|
||||
taxes = self.env['account.tax'].browse(tax_ids)
|
||||
else:
|
||||
account_id = journal.default_account_id.id
|
||||
if journal.default_account_id.tax_ids:
|
||||
taxes = journal.default_account_id.tax_ids
|
||||
else:
|
||||
taxes = journal.company_id.account_sale_tax_id if journal.type == 'sale' else journal.company_id.account_purchase_tax_id
|
||||
taxes = self.env['account.fiscal.position'].browse(fiscal_position_id).map_tax(taxes)
|
||||
price_untaxed = taxes.with_context(force_price_include=True).compute_all(quick_edit_total_amount - amount_total)['total_excluded']
|
||||
return {'account_id': account_id, 'tax_ids': taxes.ids, 'price_unit': price_untaxed}
|
||||
|
||||
@api.onchange('quick_edit_total_amount')
|
||||
def _onchange_quick_edit_total_amount(self):
|
||||
"""
|
||||
Creates a new line with the suggested values (for the account, the price_unit,
|
||||
and the tax) such that the total amount matches the quick total amount.
|
||||
"""
|
||||
if not self.quick_edit_total_amount or not self.quick_edit_mode or len(self.invoice_line_ids) > 0:
|
||||
return
|
||||
self.line_ids = [Command.clear()]
|
||||
suggestions = self._get_quick_edit_suggestions(
|
||||
self.amount_total,
|
||||
self.quick_edit_total_amount,
|
||||
self.move_type,
|
||||
self.journal_id.id,
|
||||
self.partner_id.id,
|
||||
self.fiscal_position_id.id,
|
||||
)
|
||||
self.env['account.move.line'].new({
|
||||
'move_id': self.id,
|
||||
'partner_id': self.partner_id,
|
||||
'account_id': suggestions['account_id'],
|
||||
'currency_id': self.currency_id.id,
|
||||
'price_unit': suggestions['price_unit'],
|
||||
'tax_ids': [Command.set(suggestions['tax_ids'])],
|
||||
})
|
||||
self.line_ids._onchange_price_subtotal()
|
||||
self._onchange_invoice_line_ids()
|
||||
|
||||
def _check_quick_edit_total_amount(self):
|
||||
"""
|
||||
Verifies that the total amount corresponds to the quick total amount chosen as some
|
||||
rounding errors may appear. In such a case, we round up the tax such that the total
|
||||
is equal to the quick total amount set
|
||||
E.g.: 100€ including 21% tax: base = 82.64, tax = 17.35, total = 99.99
|
||||
The tax will be set to 17.36 in order to have a total of 100.00
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not self.quick_edit_total_amount or not self.quick_edit_mode:
|
||||
return
|
||||
totals = json.loads(self.tax_totals_json)
|
||||
tax_amount_rounding_error = self.quick_edit_total_amount - self.amount_total
|
||||
if not float_compare(abs(tax_amount_rounding_error), self.currency_id.rounding, precision_rounding=self.currency_id.rounding) > 0:
|
||||
try:
|
||||
totals['groups_by_subtotal']['Untaxed Amount'][0]['tax_group_amount'] += tax_amount_rounding_error
|
||||
totals['groups_by_subtotal']['Untaxed Amount'][0]['formatted_tax_group_amount'] = formatLang(self.env, totals['groups_by_subtotal']['Untaxed Amount'][0]['tax_group_amount'], currency_obj=self.currency_id)
|
||||
totals['amount_total'] = self.quick_edit_total_amount
|
||||
totals['formatted_amount_total'] = formatLang(self.env, self.quick_edit_total_amount, currency_obj=self.currency_id)
|
||||
self.tax_totals_json = json.dumps(totals)
|
||||
self._onchange_tax_totals_json()
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@api.onchange('invoice_line_ids')
|
||||
def _onchange_invoice_line_ids(self):
|
||||
@@ -646,6 +767,7 @@ class AccountMove(models.Model):
|
||||
others_lines[0].recompute_tax_line = True
|
||||
self.line_ids = others_lines + self.invoice_line_ids
|
||||
self._onchange_recompute_dynamic_lines()
|
||||
self._check_quick_edit_total_amount()
|
||||
|
||||
@api.onchange('line_ids', 'invoice_payment_term_id', 'invoice_date_due', 'invoice_cash_rounding_id', 'invoice_vendor_bill_id')
|
||||
def _onchange_recompute_dynamic_lines(self):
|
||||
@@ -1183,6 +1305,9 @@ class AccountMove(models.Model):
|
||||
# In the form view, we need to compute a default sequence so that the user can edit
|
||||
# it. We only check the first move as an approximation (enough for new in form view)
|
||||
pass
|
||||
elif (not move.name or move.name == '/') and move.quick_edit_mode:
|
||||
# We suggest the next sequence as the default name of the new move
|
||||
pass
|
||||
elif (move.name and move.name != '/') or move.state != 'posted':
|
||||
try:
|
||||
if not move.posted_before:
|
||||
@@ -1237,6 +1362,17 @@ class AccountMove(models.Model):
|
||||
for record in self:
|
||||
record.highest_name = record._get_last_sequence()
|
||||
|
||||
@api.depends('journal_id.type', 'company_id.quick_edit_mode', 'quick_edit_total_amount')
|
||||
def _compute_quick_edit_mode(self):
|
||||
for move in self:
|
||||
quick_edit_mode = move.company_id.quick_edit_mode
|
||||
if move.journal_id.type == 'sale':
|
||||
move.quick_edit_mode = quick_edit_mode in ('out_invoices', 'out_and_in_invoices')
|
||||
elif move.journal_id.type == 'purchase':
|
||||
move.quick_edit_mode = quick_edit_mode in ('in_invoices', 'out_and_in_invoices')
|
||||
else:
|
||||
move.quick_edit_mode = False
|
||||
|
||||
@api.depends('name', 'journal_id')
|
||||
def _compute_made_sequence_hole(self):
|
||||
self.env.cr.execute("""
|
||||
@@ -2177,7 +2313,7 @@ class AccountMove(models.Model):
|
||||
|
||||
if move.journal_id.sequence_override_regex and vals.get('name') and vals['name'] != '/' and not re.match(move.journal_id.sequence_override_regex, vals['name']):
|
||||
if not self.env.user.has_group('account.group_account_manager'):
|
||||
raise UserError(_('The Journal Entry sequence is not conform to the current format. Only the Advisor can change it.'))
|
||||
raise UserError(_('The Journal Entry sequence is not conform to the current format. Only the Accountant can change it.'))
|
||||
move.journal_id.sequence_override_regex = False
|
||||
|
||||
if self._move_autocomplete_invoice_lines_write(vals):
|
||||
@@ -2833,6 +2969,10 @@ class AccountMove(models.Model):
|
||||
if not self.env.su and not self.env.user.has_group('account.group_account_invoice'):
|
||||
raise AccessError(_("You don't have the access rights to post an invoice."))
|
||||
for move in to_post:
|
||||
if move.quick_edit_mode and move.quick_edit_total_amount and move.quick_edit_total_amount != move.amount_total:
|
||||
computed_total = formatLang(self.env, move.amount_total, currency_obj=move.currency_id)
|
||||
encoded_total = formatLang(self.env, move.quick_edit_total_amount, currency_obj=move.currency_id)
|
||||
raise UserError(_("The current total is %s but the expected total is %s. In order to post the invoice/bill, you can adjust its lines or the expected Total (tax inc.).", computed_total, encoded_total))
|
||||
if move.partner_bank_id and not move.partner_bank_id.active:
|
||||
raise UserError(_("The recipient bank account link to this invoice is archived.\nSo you cannot confirm the invoice."))
|
||||
if move.state == 'posted':
|
||||
@@ -4391,6 +4531,28 @@ class AccountMoveLine(models.Model):
|
||||
for record in self:
|
||||
record.is_same_currency = record.currency_id == record.company_currency_id
|
||||
|
||||
@api.onchange('tax_ids')
|
||||
def _onchange_quick_suggest_price_unit(self):
|
||||
"""When a new line is created, we suggest a price_unit according to the quick suggestions"""
|
||||
# TODO Using an onchange on tax_ids to do this is a huge hack, we should instead give default values in default_get
|
||||
# but the current implementation of the invoice lines is sooo messy that it's overly complicated to success doing so
|
||||
for line in self:
|
||||
if line.price_unit != 0 or not line.move_id.quick_edit_mode:
|
||||
continue
|
||||
suggestions = line.move_id._get_quick_edit_suggestions(
|
||||
line.move_id.amount_total,
|
||||
line.move_id.quick_edit_total_amount,
|
||||
line.move_id.move_type,
|
||||
line.move_id.journal_id.id,
|
||||
line.partner_id.id,
|
||||
line.move_id.fiscal_position_id.id)
|
||||
if suggestions:
|
||||
line.price_unit = suggestions['price_unit']
|
||||
line.tax_ids = suggestions['tax_ids']
|
||||
line.exclude_from_invoice_tab = False
|
||||
line._onchange_price_subtotal()
|
||||
line.move_id._onchange_invoice_line_ids()
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# CONSTRAINT METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -4765,8 +4927,15 @@ class AccountMoveLine(models.Model):
|
||||
and self._context.get('default_move_type') in ('out_invoice', 'out_refund', 'in_invoice', 'in_refund', 'out_receipt', 'in_receipt'):
|
||||
# Fill missing 'account_id'.
|
||||
journal = self.env['account.journal'].browse(self._context.get('default_journal_id') or self._context['journal_id'])
|
||||
values['account_id'] = journal.default_account_id.id
|
||||
if values['partner_id']:
|
||||
# Get most used account of the partner as the default.
|
||||
account_id = self.env['account.account']._order_by_frequency_per_partner(
|
||||
journal.company_id.id, values['partner_id'], self._context.get("default_move_type"), limit=1)[0]
|
||||
else:
|
||||
account_id = journal.default_account_id
|
||||
values['account_id'] = account_id
|
||||
elif self._context.get('line_ids') and any(field_name in default_fields for field_name in ('debit', 'credit', 'account_id', 'partner_id')):
|
||||
# MISC JOURNAL ENTRY ENCODING
|
||||
move = self.env['account.move'].new({'line_ids': self._context['line_ids']})
|
||||
|
||||
# Suggest default value for debit / credit to balance the journal entry.
|
||||
|
||||
@@ -172,6 +172,14 @@ class ResCompany(models.Model):
|
||||
# Storno Accounting
|
||||
account_storno = fields.Boolean(string="Storno accounting", readonly=False)
|
||||
|
||||
# Fiduciary mode
|
||||
quick_edit_mode = fields.Selection(
|
||||
selection=[
|
||||
('out_invoices', 'Customer Invoices'),
|
||||
('in_invoices', 'Vendor Bills'),
|
||||
('out_and_in_invoices', 'Customer Invoices and Vendor Bills')],
|
||||
string="Quick encoding")
|
||||
|
||||
@api.constrains('account_opening_move_id', 'fiscalyear_last_day', 'fiscalyear_last_month')
|
||||
def _check_fiscalyear_last_day(self):
|
||||
# if the user explicitly chooses the 29th of February we allow it:
|
||||
|
||||
@@ -159,6 +159,9 @@ class ResConfigSettings(models.TransientModel):
|
||||
# Allows for the use of a different delivery address
|
||||
group_sale_delivery_address = fields.Boolean("Customer Addresses", implied_group='account.group_delivery_invoice_address')
|
||||
|
||||
# Quick encoding (fiduciary mode)
|
||||
quick_edit_mode = fields.Selection(string="Quick encoding", readonly=False, related='company_id.quick_edit_mode')
|
||||
|
||||
def set_values(self):
|
||||
super().set_values()
|
||||
# install a chart of accounts for the given company (if required)
|
||||
|
||||
@@ -3452,3 +3452,48 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
|
||||
|
||||
# Tags should be empty since the tax has been removed from the invoice line
|
||||
self.assertRecordValues(invoice.line_ids, [{'tax_tag_ids': []}, {'tax_tag_ids': []}])
|
||||
|
||||
def test_quick_edit_total_amount(self):
|
||||
move_form = Form(self.env['account.move'].with_context(default_move_type='out_invoice'))
|
||||
move_form.invoice_date = fields.Date.from_string('2022-01-01')
|
||||
move_form.partner_id = self.partner_a
|
||||
|
||||
# Quick edit total amount not activated yet
|
||||
move_form.quick_edit_total_amount = 100.0
|
||||
invoice = move_form.save()
|
||||
self.assertEqual(invoice.amount_total, 0.0)
|
||||
self.assertEqual(len(invoice.invoice_line_ids), 0)
|
||||
|
||||
# Quick edit total amount activated
|
||||
self.env.company.quick_edit_mode = "out_and_in_invoices"
|
||||
self.env.company.account_sale_tax_id = self.env['account.tax'].create({
|
||||
'name': '21%',
|
||||
'amount': 21,
|
||||
'type_tax_use': 'sale',
|
||||
}) # 21% tax of a total amount may create a rounding error (99.99 or 100.01)
|
||||
|
||||
# Let's make sure it does not (the rounding cent should be on the tax)
|
||||
move_form.quick_edit_total_amount = 100.0
|
||||
invoice = move_form.save()
|
||||
self.assertEqual(invoice.amount_total, 100)
|
||||
self.assertEqual(invoice.amount_untaxed, 82.64)
|
||||
self.assertEqual(invoice.amount_tax, 17.36)
|
||||
self.assertEqual(len(invoice.invoice_line_ids), 1)
|
||||
|
||||
# Modify one invoice line
|
||||
with move_form.invoice_line_ids.edit(0) as line_form:
|
||||
line_form.price_unit = 50
|
||||
invoice = move_form.save()
|
||||
self.assertEqual(invoice.amount_total, 60.5)
|
||||
self.assertEqual(invoice.amount_untaxed, 50)
|
||||
self.assertEqual(invoice.amount_tax, 10.5)
|
||||
self.assertEqual(len(invoice.invoice_line_ids), 1)
|
||||
|
||||
# Suggest the new amount such that the total is equal to the quick amount
|
||||
with move_form.invoice_line_ids.new() as line_form:
|
||||
self.assertEqual(line_form.price_unit, 32.64)
|
||||
invoice = move_form.save()
|
||||
self.assertEqual(invoice.amount_total, 100)
|
||||
self.assertEqual(invoice.amount_untaxed, 82.64)
|
||||
self.assertEqual(invoice.amount_tax, 17.36)
|
||||
self.assertEqual(len(invoice.invoice_line_ids), 2)
|
||||
|
||||
@@ -739,21 +739,20 @@
|
||||
<field name="payment_id" invisible="1"/>
|
||||
<field name="tax_country_id" invisible="1"/>
|
||||
<field name="tax_cash_basis_created_move_ids" invisible="1"/>
|
||||
<field name="quick_edit_mode" invisible="1"/>
|
||||
<field name="hide_post_button" invisible="1"/>
|
||||
|
||||
<div class="oe_title">
|
||||
<!-- Invoice draft header -->
|
||||
<span class="o_form_label"><field name="move_type" attrs="{'invisible': [('move_type', '=', 'entry')]}" readonly="1" nolabel="1"/></span>
|
||||
<h1 attrs="{'invisible': ['|', ('state', '!=', 'draft'), ('name', '!=', '/')]}">
|
||||
<span>Draft</span>
|
||||
</h1>
|
||||
|
||||
<!-- Number -->
|
||||
<span class="text-warning" attrs="{'invisible': [('show_name_warning', '=', False)]}">The current highest number is <field name="highest_name"/>. You might want to put a higher number here.</span>
|
||||
<h1 class="mt0" attrs="{'invisible':[('name', '=', '/'), ('posted_before', '=', False)]}">
|
||||
<field name="name" attrs="{'readonly': [('state', '!=', 'draft')]}" placeholder="JRNL/2016/00001"/>
|
||||
</h1>
|
||||
|
||||
<h1>
|
||||
<field name="name" attrs="{'invisible':[('name', '=', '/'), ('posted_before', '=', False), ('quick_edit_mode', '=', False)],
|
||||
'readonly': [('state', '!=', 'draft')]}" placeholder="Draft"/>
|
||||
|
||||
<span attrs="{'invisible': ['|', '|', ('state', '!=', 'draft'), ('name', '!=', '/'), ('quick_edit_mode', '=', True)]}">Draft</span>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group id="header_left_group">
|
||||
@@ -772,17 +771,13 @@
|
||||
<field name="partner_shipping_id"
|
||||
groups="account.group_delivery_invoice_address"
|
||||
attrs="{'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund', 'out_receipt'))], 'readonly': [('state', '!=', 'draft')]}"/>
|
||||
<field name="quick_edit_total_amount"
|
||||
attrs="{'invisible': ['|', ('move_type', '=', 'entry'), ('quick_edit_mode', '=', False)], 'readonly': [('state', '!=', 'draft')]}"/>
|
||||
<label for="ref" string="Bill Reference"
|
||||
attrs="{'invisible':[('move_type', 'not in', ('in_invoice', 'in_receipt', 'in_refund'))]}" />
|
||||
<field name="ref" nolabel="1" attrs="{'invisible':[('move_type', 'not in', ('in_invoice', 'in_receipt', 'in_refund'))]}" />
|
||||
<field name="ref" attrs="{'invisible':[('move_type', 'in', ('in_invoice', 'in_receipt', 'in_refund', 'out_invoice', 'out_refund'))]}"/>
|
||||
<field name="tax_cash_basis_origin_move_id" attrs="{'invisible': [('tax_cash_basis_origin_move_id', '=', False)]}"/>
|
||||
<field name="payment_reference"
|
||||
attrs="{'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund', 'out_receipt', 'in_receipt'))], 'readonly': [('state', '!=', 'draft')]}"/>
|
||||
<field name="partner_bank_id"
|
||||
context="{'default_partner_id': bank_partner_id}"
|
||||
domain="[('partner_id', '=', bank_partner_id)]"
|
||||
attrs="{'invisible': [('move_type', 'not in', ('in_invoice', 'in_refund', 'in_receipt'))], 'readonly': [('state', '!=', 'draft')]}"/>
|
||||
<label name="invoice_vendor_bill_id_label" for="invoice_vendor_bill_id" string="Auto-Complete" class="oe_edit_only"
|
||||
attrs="{'invisible': ['|', ('state', '!=', 'draft'), ('move_type', '!=', 'in_invoice')]}"/>
|
||||
<field name="invoice_vendor_bill_id" nolabel="1" class="oe_edit_only"
|
||||
@@ -804,9 +799,15 @@
|
||||
|
||||
<field name="date" string="Accounting Date"
|
||||
attrs="{
|
||||
'invisible': [('move_type', 'in', ('out_invoice', 'out_refund', 'out_receipt'))],
|
||||
'invisible': [('move_type', 'in', ('out_invoice', 'out_refund', 'out_receipt')), ('quick_edit_mode', '=', False)],
|
||||
'readonly': [('state', '!=', 'draft')],
|
||||
}"/>
|
||||
<field name="payment_reference"
|
||||
attrs="{'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund', 'out_receipt', 'in_receipt'))], 'readonly': [('state', '!=', 'draft')]}"/>
|
||||
<field name="partner_bank_id"
|
||||
context="{'default_partner_id': bank_partner_id}"
|
||||
domain="[('partner_id', '=', bank_partner_id)]"
|
||||
attrs="{'invisible': [('move_type', 'not in', ('in_invoice', 'in_refund', 'in_receipt'))], 'readonly': [('state', '!=', 'draft')]}"/>
|
||||
|
||||
<!-- Invoice payment terms (only invoices) + due date (only invoices / receipts) -->
|
||||
<label for="invoice_payment_term_id" string="Due Date"
|
||||
@@ -827,12 +828,9 @@
|
||||
<div name="journal_div" class="d-flex" invisible="context.get('default_journal_id') and context.get('move_type', 'entry') != 'entry'">
|
||||
<field name="journal_id"
|
||||
groups="account.group_account_readonly"
|
||||
options="{'no_create': True}"
|
||||
options="{'no_create': True, 'no_open': True}"
|
||||
attrs="{'readonly': [('posted_before', '=', True)]}"/>
|
||||
<span class="oe_inline o_form_label mx-3 oe_read_only"
|
||||
groups="base.group_multi_currency" attrs="{'invisible': [('move_type', '=', 'entry')]}"> in </span>
|
||||
<!-- Bigger margin on the left because in edit mode the external link button covers the text -->
|
||||
<span class="oe_inline o_form_label mr-3 ml-5 oe_edit_only"
|
||||
<span class="oe_inline o_form_label mx-3"
|
||||
groups="base.group_multi_currency" attrs="{'invisible': [('move_type', '=', 'entry')]}"> in </span>
|
||||
<field name="currency_id"
|
||||
groups="base.group_multi_currency"
|
||||
@@ -865,8 +863,9 @@
|
||||
and [('sale_ok', '=', True), '|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]
|
||||
or [('purchase_ok', '=', True), '|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]
|
||||
"/>
|
||||
<field name="name" widget="section_and_note_text" attrs="{'required': [('parent.move_type', 'not in', ('in_invoice', 'in_refund','in_receipt'))]}"/>
|
||||
<field name="name" widget="section_and_note_text" optional="show"/>
|
||||
<field name="account_id"
|
||||
context="{'partner_id': partner_id, 'move_type': parent.move_type}"
|
||||
groups="account.group_account_readonly"
|
||||
options="{'no_create': True}"
|
||||
domain="[('deprecated', '=', False), ('user_type_id.type', 'not in', ('receivable', 'payable')), ('company_id', '=', parent.company_id), ('is_off_balance', '=', False)]"
|
||||
@@ -880,7 +879,7 @@
|
||||
groups="analytic.group_analytic_tags"
|
||||
optional="show"
|
||||
widget="many2many_tags"/>
|
||||
<field name="quantity"/>
|
||||
<field name="quantity" optional="show"/>
|
||||
<field name="product_uom_category_id" invisible="1"/>
|
||||
<field name="product_uom_id" string="UoM" groups="uom.group_uom" optional="show"/>
|
||||
<field name="price_unit" string="Price"/>
|
||||
@@ -1017,7 +1016,7 @@
|
||||
</group>
|
||||
<group>
|
||||
<field name="analytic_tag_ids" groups="analytic.group_analytic_tags" widget="many2many_tags"/>
|
||||
<field name="account_id" options="{'no_create': True}" domain="[('company_id', '=', company_id)]" />
|
||||
<field name="account_id" options="{'no_create': True}" domain="[('company_id', '=', company_id)]" context="{'partner_id': partner_id, 'move_type': parent.move_type}"/>
|
||||
<field name="tax_ids" widget="many2many_tags"/>
|
||||
<field name="analytic_account_id" groups="analytic.group_analytic_accounting"/>
|
||||
</group>
|
||||
|
||||
@@ -691,6 +691,23 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Accounting Firms mode</h2>
|
||||
<div class="row mt16 o_settings_container" id="quick_edit_mode">
|
||||
<div class="col-12 col-lg-12 o_setting_box">
|
||||
<div class="text-muted">
|
||||
<p style="margin-bottom: 0">Accounting firm mode will change invoice/bill encoding:</p>
|
||||
<p style="margin-bottom: 0"> - The sequence’s document becomes manageable for each document.</p>
|
||||
<p style="margin-bottom: 0"> - A new field « Total (tax inc.) » to speed up and control the encoding by automating line creation with the right account & tax.</p>
|
||||
</div>
|
||||
<div class="o_setting_right_pane mt16">
|
||||
<label for="quick_edit_mode"/>
|
||||
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." aria-label="Values set here are company-specific." groups="base.group_multi_company" role="img"/>
|
||||
<div>
|
||||
<field name="quick_edit_mode" placeholder="Disabled"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account.view_move_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='line_ids']//field[@name='name']" position="after">
|
||||
<xpath expr="//field[@name='line_ids']//field[@name='account_id']" position="after">
|
||||
<field name='need_vehicle' invisible='1'/>
|
||||
<field name='vehicle_id' attrs="{'required': [('need_vehicle', '=', True), ('parent.move_type', 'in', ('in_invoice', 'in_refund'))], 'column_invisible': [('parent.move_type', 'not in', ('in_invoice', 'in_refund'))]}" optional='hidden'/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='invoice_line_ids']//field[@name='name']" position="after">
|
||||
<xpath expr="//field[@name='invoice_line_ids']//field[@name='account_id']" position="after">
|
||||
<field name='need_vehicle' invisible='1'/>
|
||||
<field name='vehicle_id' attrs="{'required': [('need_vehicle', '=', True), ('parent.move_type', 'in', ('in_invoice', 'in_refund'))], 'column_invisible': [('parent.move_type', 'not in', ('in_invoice', 'in_refund'))]}" optional='hidden'/>
|
||||
</xpath>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
'description': """
|
||||
Make the lock date irreversible:
|
||||
|
||||
* You cannot set stricter restrictions on advisors than on users. Therefore, the All Users Lock Date must be anterior (or equal) to the Invoice/Bills Lock Date.
|
||||
* You cannot set stricter restrictions on accountants than on users. Therefore, the All Users Lock Date must be anterior (or equal) to the Invoice/Bills Lock Date.
|
||||
* You cannot lock a period that has not yet ended. Therefore, the All Users Lock Date must be anterior (or equal) to the last day of the previous month.
|
||||
* Any new All Users Lock Date must be posterior (or equal) to the previous one.
|
||||
""",
|
||||
|
||||
@@ -13,7 +13,7 @@ class ResCompany(models.Model):
|
||||
def _autorise_lock_date_changes(self, vals):
|
||||
'''Check the lock dates for the current companies. This can't be done in a api.constrains because we need
|
||||
to perform some comparison between new/old values. This method forces the lock dates to be irreversible.
|
||||
* You cannot set stricter restrictions on advisors than on users.
|
||||
* You cannot set stricter restrictions on accountants than on users.
|
||||
Therefore, the All Users Lock Date must be anterior (or equal) to the Invoice/Bills Lock Date.
|
||||
* You cannot lock a period that has not yet ended.
|
||||
Therefore, the All Users Lock Date must be anterior (or equal) to the last day of the previous month.
|
||||
@@ -48,11 +48,11 @@ class ResCompany(models.Model):
|
||||
if tax_lock_date and tax_lock_date > previous_month:
|
||||
raise UserError(_('You cannot lock a period that has not yet ended. Therefore, the tax lock date must be anterior (or equal) to the last day of the previous month.'))
|
||||
|
||||
# The user attempts to remove the lock date for advisors
|
||||
# The user attempts to remove the lock date for accountants
|
||||
if old_fiscalyear_lock_date and not fiscalyear_lock_date and 'fiscalyear_lock_date' in vals:
|
||||
raise UserError(_('The lock date for advisors is irreversible and can\'t be removed.'))
|
||||
raise UserError(_('The lock date for accountants is irreversible and can\'t be removed.'))
|
||||
|
||||
# The user attempts to set a lock date for advisors prior to the previous one
|
||||
# The user attempts to set a lock date for accountants prior to the previous one
|
||||
if old_fiscalyear_lock_date and fiscalyear_lock_date and fiscalyear_lock_date < old_fiscalyear_lock_date:
|
||||
raise UserError(_('Any new All Users Lock Date must be posterior (or equal) to the previous one.'))
|
||||
|
||||
@@ -61,7 +61,7 @@ class ResCompany(models.Model):
|
||||
if not fiscalyear_lock_date:
|
||||
continue
|
||||
|
||||
# The user attempts to set a lock date for advisors prior to the last day of previous month
|
||||
# The user attempts to set a lock date for accountants prior to the last day of previous month
|
||||
if fiscalyear_lock_date > previous_month:
|
||||
raise UserError(_('You cannot lock a period that has not yet ended. Therefore, the All Users Lock Date must be anterior (or equal) to the last day of the previous month.'))
|
||||
|
||||
@@ -70,9 +70,9 @@ class ResCompany(models.Model):
|
||||
if not period_lock_date:
|
||||
continue
|
||||
|
||||
# The user attempts to set a lock date for advisors prior to the lock date for users
|
||||
# The user attempts to set a lock date for accountants prior to the lock date for users
|
||||
if period_lock_date < fiscalyear_lock_date:
|
||||
raise UserError(_('You cannot set stricter restrictions on advisors than on users. Therefore, the All Users Lock Date must be anterior (or equal) to the Invoice/Bills Lock Date.'))
|
||||
raise UserError(_('You cannot set stricter restrictions on accountants than on users. Therefore, the All Users Lock Date must be anterior (or equal) to the Invoice/Bills Lock Date.'))
|
||||
|
||||
def write(self, vals):
|
||||
# fiscalyear_lock_date can't be set to a prior date
|
||||
|
||||
Reference in New Issue
Block a user