commit
253b117b97
229
account_spread_cost_revenue/README.rst
Normal file
229
account_spread_cost_revenue/README.rst
Normal file
@ -0,0 +1,229 @@
|
||||
===================
|
||||
Cost-Revenue Spread
|
||||
===================
|
||||
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:560d87ede92fd18ca4a929595b6a9fd9f558f6b577e8c9bad1f8e7236ad4175d
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
|
||||
:target: https://odoo-community.org/page/development-status
|
||||
:alt: Beta
|
||||
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
|
||||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||
:alt: License: AGPL-3
|
||||
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--financial--tools-lightgray.png?logo=github
|
||||
:target: https://github.com/OCA/account-financial-tools/tree/16.0/account_spread_cost_revenue
|
||||
:alt: OCA/account-financial-tools
|
||||
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
|
||||
:target: https://translation.odoo-community.org/projects/account-financial-tools-16-0/account-financial-tools-16-0-account_spread_cost_revenue
|
||||
:alt: Translate me on Weblate
|
||||
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
|
||||
:target: https://runboat.odoo-community.org/builds?repo=OCA/account-financial-tools&target_branch=16.0
|
||||
:alt: Try me on Runboat
|
||||
|
||||
|badge1| |badge2| |badge3| |badge4| |badge5|
|
||||
|
||||
Allows to spread costs or revenues over a customizable periods, to even out cost or invoice spikes.
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
To be able to access the full spreading features, the user must belong to *Show Full Accounting Features* group.
|
||||
|
||||
On the form view of the company, in the *Account Spread* tab, you can configure
|
||||
the journals in which the spread journal items will be generated by default:
|
||||
|
||||
* the *Default Spread Journal for Revenues*,
|
||||
* the *Default Spread Journal for Expenses*.
|
||||
|
||||
In the same *Account Spread* tab, you can also configure the Spread Balance Sheet Accounts used by default:
|
||||
|
||||
* the *Default Spread Account for Revenues*,
|
||||
* the *Default Spread Account for Expenses*.
|
||||
|
||||
This module by default allows the spreading even before the receipt of the invoice or when the invoice is still draft,
|
||||
so that it is possible to work on the plan of the cost/revenue spreading. To disable this feature, on the form view of
|
||||
the company disable the *Allow Spread Planning* option.
|
||||
|
||||
In Spread Template, there is also option to *Auto assign template on invoice validate*, based on the preset invoice line criteria.
|
||||
|
||||
On the form view of the company, the *Auto-post spread lines* option forces the account moves created
|
||||
during the cost/revenue spreading to be automatically posted. When this option is false, the user can
|
||||
enable/disable the automatic posting by the flag *Auto-post lines* present in the spread board.
|
||||
|
||||
On the form view of the company, enable the *Auto-archive spread* option if you want the
|
||||
cron job to automatically archive the spreads when all lines are posted.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
Define Spread Costs/Revenues Board
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Under Invoicing -> Accounting -> Journals -> Spread Costs/Revenues, create a new spread board.
|
||||
|
||||
Complete the definition of the spreading criteria, by setting the the fields:
|
||||
|
||||
* *Debit Account*
|
||||
* *Credit Account*
|
||||
* *Estimated Amount* (The total amount to spread)
|
||||
* *Number of Repetitions*
|
||||
* *Period Type* (Duration of each period)
|
||||
* *Start date*
|
||||
* *Journal*
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/spread.png
|
||||
:alt: Create a new spread board
|
||||
|
||||
Click on the "Recalculate unposted lines" button on the top-left to calculate the spread lines.
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/create_spread.png
|
||||
:alt: The spreading board is defined
|
||||
|
||||
A cron job will automatically create the accounting moves for all the lines having date previous that the current day (today).
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/update_spread.png
|
||||
:alt: The spreading board is updated by the cron job
|
||||
|
||||
By default, the status of the created accounting moves is posted.
|
||||
To disable the automatic posting of the accounting moves, set the flag *Auto-post lines* to False.
|
||||
This flag is only available when the *Auto-post spread lines* option, present on the form view of the company, is disabled.
|
||||
|
||||
Click on button *Recalculate entire spread* button in the spread board to force the recalculation of the spread lines:
|
||||
this will also reset all the journal entries previously created.
|
||||
|
||||
Link Invoice to Spread Costs/Revenues Board
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Create an invoice or vendor bill in draft. On its lines, the spreading right-arrow icon are displayed in dark-grey color.
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/invoice_line_1.png
|
||||
:alt: On the invoice line the spreading icon is displayed
|
||||
|
||||
Click on the spreading right-arrow icon. A wizard prompts to enter a *Spread Action Type*:
|
||||
|
||||
- *Link to existing spread board*
|
||||
- *Create from spread template*
|
||||
- *Create new spread board*
|
||||
|
||||
Select *Link to existing spread board* and enter the previously generated Spread Board. Click on Confirm button:
|
||||
the selected Spread Board will be automatically displayed.
|
||||
|
||||
Go back to the draft invoice/bill. The spreading functionality is now enabled on the invoice line:
|
||||
the spreading right-arrow icon is now displayed in green color.
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/invoice_line_2.png
|
||||
:alt: On the invoice line the spreading icon is displayed in green color
|
||||
|
||||
Validate the invoice/bill. Click on the spreading (green) right-arrow icon to open the spread board, then click
|
||||
on the smart button *Posted entries* to see the moves of the spread lines together with the move of the invoice line.
|
||||
|
||||
In case the Subtotal Price of the invoice line is different than the *Estimated Amount* of the spread board, the spread
|
||||
lines (not yet posted) will be recalculated when validating the invoice/bill.
|
||||
|
||||
Define Spread Costs/Revenues Template
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Under Invoicing -> Configuration -> Accounting -> Spread Templates, create a new spread template.
|
||||
|
||||
* *Spread Type*
|
||||
* *Spread Balance Sheet Account*
|
||||
* *Expense/Revenue Account* This option visible if invoice line account is balance sheet account, user need to specify this too.
|
||||
* *Journal*
|
||||
* *Auto assign template on invoice validate*
|
||||
|
||||
When creating a new Spread Costs/Revenues Board, select the right template.
|
||||
This way the above fields will be copied to the Spread Board.
|
||||
|
||||
If *Auto assign template on invoice validate* is checked, this template will be used to auto create spread, if the underlining invoice match the preset product/account/analytic criteria.
|
||||
|
||||
Changelog
|
||||
=========
|
||||
|
||||
13.0.1.0.0
|
||||
~~~~~~~~~~
|
||||
|
||||
* [MIG] Port account_spread_cost_revenue to V13.
|
||||
|
||||
12.0.2.0.0
|
||||
~~~~~~~~~~
|
||||
|
||||
* [ENH] In spread template, add option to auto create spread on invoice validation
|
||||
|
||||
12.0.1.1.0
|
||||
~~~~~~~~~~
|
||||
|
||||
* [ENH] Add optional Expense/Revenue Account in Chart Template, which can be used
|
||||
in place of account from invoice line to set Expense/Revenue account in the spread
|
||||
|
||||
|
||||
12.0.1.0.0
|
||||
~~~~~~~~~~
|
||||
|
||||
* [MIG] Port account_spread_cost_revenue to V12.
|
||||
|
||||
|
||||
11.0.1.0.0
|
||||
~~~~~~~~~~
|
||||
|
||||
* [ADD] Module account_spread_cost_revenue.
|
||||
(`#715 <https://github.com/OCA/account-financial-tools/pull/715>`_)
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/account-financial-tools/issues>`_.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/OCA/account-financial-tools/issues/new?body=module:%20account_spread_cost_revenue%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
|
||||
Do not contact contributors directly about support or help with technical issues.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Authors
|
||||
~~~~~~~
|
||||
|
||||
* Onestein
|
||||
|
||||
Contributors
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* Andrea Stirpe <a.stirpe@onestein.nl>
|
||||
* Kitti U. <kittiu@ecosoft.co.th>
|
||||
* Saran Lim. <saranl@ecosoft.co.th>
|
||||
|
||||
Other credits
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Part of the code in this module (in particular the computation of the spread lines)
|
||||
is highly inspired by the Assets Management module from the standard
|
||||
Odoo 11.0 Community developed by Odoo SA.
|
||||
|
||||
Maintainers
|
||||
~~~~~~~~~~~
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.
|
||||
|
||||
This module is part of the `OCA/account-financial-tools <https://github.com/OCA/account-financial-tools/tree/16.0/account_spread_cost_revenue>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
4
account_spread_cost_revenue/__init__.py
Normal file
4
account_spread_cost_revenue/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import models
|
||||
from . import wizards
|
25
account_spread_cost_revenue/__manifest__.py
Normal file
25
account_spread_cost_revenue/__manifest__.py
Normal file
@ -0,0 +1,25 @@
|
||||
# Copyright 2016-2020 Onestein (<https://www.onestein.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
"name": "Cost-Revenue Spread",
|
||||
"summary": "Spread costs and revenues over a custom period",
|
||||
"version": "16.0.1.0.0",
|
||||
"development_status": "Beta",
|
||||
"author": "Onestein,Odoo Community Association (OCA)",
|
||||
"license": "AGPL-3",
|
||||
"website": "https://github.com/OCA/account-financial-tools",
|
||||
"category": "Accounting & Finance",
|
||||
"depends": ["account"],
|
||||
"data": [
|
||||
"security/ir.model.access.csv",
|
||||
"security/account_spread_security.xml",
|
||||
"views/account_spread.xml",
|
||||
"views/account_move.xml",
|
||||
"views/res_company.xml",
|
||||
"views/account_spread_template.xml",
|
||||
"wizards/account_spread_invoice_line_link_wizard.xml",
|
||||
"data/spread_cron.xml",
|
||||
],
|
||||
"installable": True,
|
||||
}
|
15
account_spread_cost_revenue/data/spread_cron.xml
Normal file
15
account_spread_cost_revenue/data/spread_cron.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record id="ir_cron_spread_create_entries" forcecreate="True" model="ir.cron">
|
||||
<field name="name">Cost/revenue Spread: Create Entries</field>
|
||||
<field name="active" eval="True" />
|
||||
<field name="user_id" ref="base.user_root" />
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False" />
|
||||
<field name="model_id" ref="model_account_spread_line" />
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._create_entries()</field>
|
||||
</record>
|
||||
</odoo>
|
1087
account_spread_cost_revenue/i18n/account_spread_cost_revenue.pot
Normal file
1087
account_spread_cost_revenue/i18n/account_spread_cost_revenue.pot
Normal file
File diff suppressed because it is too large
Load Diff
1117
account_spread_cost_revenue/i18n/fr.po
Normal file
1117
account_spread_cost_revenue/i18n/fr.po
Normal file
File diff suppressed because it is too large
Load Diff
1000
account_spread_cost_revenue/i18n/nl.po
Normal file
1000
account_spread_cost_revenue/i18n/nl.po
Normal file
File diff suppressed because it is too large
Load Diff
1086
account_spread_cost_revenue/i18n/pt.po
Normal file
1086
account_spread_cost_revenue/i18n/pt.po
Normal file
File diff suppressed because it is too large
Load Diff
8
account_spread_cost_revenue/models/__init__.py
Normal file
8
account_spread_cost_revenue/models/__init__.py
Normal file
@ -0,0 +1,8 @@
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import account_move
|
||||
from . import account_move_line
|
||||
from . import account_spread_line
|
||||
from . import account_spread
|
||||
from . import account_spread_template
|
||||
from . import res_company
|
39
account_spread_cost_revenue/models/account_move.py
Normal file
39
account_spread_cost_revenue/models/account_move.py
Normal file
@ -0,0 +1,39 @@
|
||||
# Copyright 2016-2020 Onestein (<https://www.onestein.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = "account.move"
|
||||
|
||||
def action_post(self):
|
||||
"""Invoked when validating the invoices."""
|
||||
self.mapped("invoice_line_ids").create_auto_spread()
|
||||
res = super().action_post()
|
||||
spreads = self.mapped("invoice_line_ids.spread_id")
|
||||
spreads.compute_spread_board()
|
||||
# On posting of spread moves. Find their related spreads to reconcile
|
||||
move_spreads = self.env["account.spread"].search(
|
||||
[("line_ids.move_id", "in", self.ids)]
|
||||
)
|
||||
spreads += move_spreads
|
||||
spreads.reconcile_spread_moves()
|
||||
return res
|
||||
|
||||
def button_cancel(self):
|
||||
"""Cancel the spread lines and their related moves when
|
||||
the invoice is canceled."""
|
||||
spread_lines = self.mapped("invoice_line_ids.spread_id.line_ids")
|
||||
moves = spread_lines.mapped("move_id")
|
||||
moves.line_ids.remove_move_reconcile()
|
||||
moves.filtered(lambda move: move.state == "posted").button_draft()
|
||||
moves.with_context(force_delete=True).unlink()
|
||||
spread_lines.unlink()
|
||||
res = super().button_cancel()
|
||||
return res
|
||||
|
||||
@api.constrains("name", "journal_id", "state")
|
||||
def _check_unique_sequence_number(self):
|
||||
if not self.env.context.get("skip_unique_sequence_number"):
|
||||
return super()._check_unique_sequence_number()
|
177
account_spread_cost_revenue/models/account_move_line.py
Normal file
177
account_spread_cost_revenue/models/account_move_line.py
Normal file
@ -0,0 +1,177 @@
|
||||
# Copyright 2016-2020 Onestein (<https://www.onestein.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
_inherit = "account.move.line"
|
||||
|
||||
spread_id = fields.Many2one("account.spread", string="Spread Board", copy=False)
|
||||
spread_check = fields.Selection(
|
||||
[
|
||||
("linked", "Linked"),
|
||||
("unlinked", "Unlinked"),
|
||||
("unavailable", "Unavailable"),
|
||||
],
|
||||
compute="_compute_spread_check",
|
||||
)
|
||||
|
||||
@api.depends("spread_id", "move_id.state")
|
||||
def _compute_spread_check(self):
|
||||
for line in self:
|
||||
if line.spread_id:
|
||||
line.spread_check = "linked"
|
||||
elif line.move_id.state == "draft":
|
||||
line.spread_check = "unlinked"
|
||||
else:
|
||||
line.spread_check = "unavailable"
|
||||
|
||||
def spread_details(self):
|
||||
"""Button on the invoice lines tree view of the invoice
|
||||
form to show the spread form view."""
|
||||
if not self:
|
||||
# In case the widget clicked before the creation of the line
|
||||
return
|
||||
|
||||
if self.spread_id:
|
||||
return {
|
||||
"name": _("Spread Details"),
|
||||
"view_mode": "form",
|
||||
"res_model": "account.spread",
|
||||
"type": "ir.actions.act_window",
|
||||
"target": "current",
|
||||
"readonly": False,
|
||||
"res_id": self.spread_id.id,
|
||||
}
|
||||
|
||||
# In case no spread board is linked to the invoice line
|
||||
# open the wizard to link them
|
||||
ctx = dict(
|
||||
self.env.context,
|
||||
default_invoice_line_id=self.id,
|
||||
default_company_id=self.move_id.company_id.id,
|
||||
allow_spread_planning=self.move_id.company_id.allow_spread_planning,
|
||||
)
|
||||
return {
|
||||
"name": _("Link Invoice Line with Spread Board"),
|
||||
"view_mode": "form",
|
||||
"res_model": "account.spread.invoice.line.link.wizard",
|
||||
"type": "ir.actions.act_window",
|
||||
"target": "new",
|
||||
"context": ctx,
|
||||
}
|
||||
|
||||
@api.constrains("spread_id", "account_id")
|
||||
def _check_spread_account_balance_sheet(self):
|
||||
for line in self:
|
||||
if not line.spread_id:
|
||||
pass
|
||||
elif line.move_id.move_type in ("out_invoice", "in_refund"):
|
||||
if line.account_id != line.spread_id.debit_account_id:
|
||||
raise ValidationError(
|
||||
_(
|
||||
"The account of the invoice line does not correspond "
|
||||
"to the Balance Sheet (debit account) of the spread"
|
||||
)
|
||||
)
|
||||
elif line.move_id.move_type in ("in_invoice", "out_refund"):
|
||||
if line.account_id != line.spread_id.credit_account_id:
|
||||
raise ValidationError(
|
||||
_(
|
||||
"The account of the invoice line does not correspond "
|
||||
"to the Balance Sheet (credit account) of the spread"
|
||||
)
|
||||
)
|
||||
|
||||
def write(self, vals):
|
||||
if vals.get("spread_id"):
|
||||
spread = self.env["account.spread"].browse(vals.get("spread_id"))
|
||||
if spread.invoice_type in ["out_invoice", "in_refund"]:
|
||||
vals["account_id"] = spread.debit_account_id.id
|
||||
else:
|
||||
vals["account_id"] = spread.credit_account_id.id
|
||||
return super().write(vals)
|
||||
|
||||
def _check_spread_reconcile_validity(self):
|
||||
# Improve error messages of standard Odoo
|
||||
reconciled_lines = self.filtered(lambda l: l.reconciled)
|
||||
msg_line = _(
|
||||
"Move line: %(line_id)s (%(line_name)s), account code: %(account_code)s\n"
|
||||
)
|
||||
if reconciled_lines:
|
||||
msg = _("Cannot reconcile entries that are already reconciled:\n")
|
||||
for line in reconciled_lines:
|
||||
msg += msg_line % {
|
||||
"line_id": line.id,
|
||||
"line_name": line.name,
|
||||
"account_code": line.account_id.code,
|
||||
}
|
||||
raise ValidationError(msg)
|
||||
if len(self.mapped("account_id").ids) > 1:
|
||||
msg = _("Some entries are not from the same account:\n")
|
||||
for line in self:
|
||||
msg += msg_line % {
|
||||
"line_id": line.id,
|
||||
"line_name": line.name,
|
||||
"account_code": line.account_id.code,
|
||||
}
|
||||
raise ValidationError(msg)
|
||||
|
||||
def create_auto_spread(self):
|
||||
"""Create auto spread table for each invoice line, when needed"""
|
||||
|
||||
def _filter_line(aline, iline):
|
||||
"""Find matching template auto line with invoice line"""
|
||||
if aline.product_id and iline.product_id != aline.product_id:
|
||||
return False
|
||||
if aline.account_id and iline.account_id != aline.account_id:
|
||||
return False
|
||||
if (
|
||||
aline.analytic_distribution
|
||||
and iline.analytic_distribution != aline.analytic_distribution
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
# Skip create new template when create move on spread lines
|
||||
if self.env.context.get("skip_create_template"):
|
||||
return
|
||||
|
||||
for line in self:
|
||||
if line.spread_check == "linked":
|
||||
continue
|
||||
spread_type = (
|
||||
"sale"
|
||||
if line.move_id.move_type in ["out_invoice", "out_refund"]
|
||||
else "purchase"
|
||||
)
|
||||
spread_auto = self.env["account.spread.template.auto"].search(
|
||||
[
|
||||
("template_id.auto_spread", "=", True),
|
||||
("template_id.spread_type", "=", spread_type),
|
||||
]
|
||||
)
|
||||
matched = spread_auto.filtered(lambda a, i=line: _filter_line(a, i))
|
||||
template = matched.mapped("template_id")
|
||||
if not template:
|
||||
continue
|
||||
elif len(template) > 1:
|
||||
raise UserError(
|
||||
_(
|
||||
"Too many auto spread templates (%(len_template)s) matched with the "
|
||||
"invoice line, %(line_name)s"
|
||||
)
|
||||
% {"len_template": len(template), "line_name": line.display_name}
|
||||
)
|
||||
# Found auto spread template for this invoice line, create it
|
||||
wizard = self.env["account.spread.invoice.line.link.wizard"].new(
|
||||
{
|
||||
"invoice_line_id": line.id,
|
||||
"company_id": line.company_id.id,
|
||||
"spread_action_type": "template",
|
||||
"template_id": template.id,
|
||||
}
|
||||
)
|
||||
wizard.confirm()
|
614
account_spread_cost_revenue/models/account_spread.py
Normal file
614
account_spread_cost_revenue/models/account_spread.py
Normal file
@ -0,0 +1,614 @@
|
||||
# Copyright 2018-2020 Onestein (<https://www.onestein.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
import calendar
|
||||
import time
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools import float_is_zero
|
||||
|
||||
|
||||
class AccountSpread(models.Model):
|
||||
_name = "account.spread"
|
||||
_description = "Account Spread"
|
||||
_inherit = ["mail.thread", "analytic.mixin"]
|
||||
_check_company_auto = True
|
||||
|
||||
name = fields.Char(required=True)
|
||||
template_id = fields.Many2one("account.spread.template", string="Spread Template")
|
||||
invoice_type = fields.Selection(
|
||||
[
|
||||
("out_invoice", "Customer Invoice"),
|
||||
("in_invoice", "Vendor Bill"),
|
||||
("out_refund", "Customer Credit Note"),
|
||||
("in_refund", "Vendor Credit Note"),
|
||||
],
|
||||
required=True,
|
||||
)
|
||||
spread_type = fields.Selection(
|
||||
[("sale", "Customer"), ("purchase", "Supplier")],
|
||||
compute="_compute_spread_type",
|
||||
required=True,
|
||||
)
|
||||
period_number = fields.Integer(
|
||||
string="Number of Repetitions",
|
||||
default=12,
|
||||
help="Define the number of spread lines",
|
||||
required=True,
|
||||
)
|
||||
period_type = fields.Selection(
|
||||
[("month", "Month"), ("quarter", "Quarter"), ("year", "Year")],
|
||||
default="month",
|
||||
help="Period length for the entries",
|
||||
required=True,
|
||||
)
|
||||
days_calc = fields.Boolean(
|
||||
string="Calculate by days",
|
||||
default=False,
|
||||
help="Use number of days to calculate amount",
|
||||
)
|
||||
use_invoice_line_account = fields.Boolean()
|
||||
credit_account_id = fields.Many2one(
|
||||
"account.account",
|
||||
compute="_compute_credit_account_id",
|
||||
readonly=False,
|
||||
store=True,
|
||||
required=True,
|
||||
)
|
||||
debit_account_id = fields.Many2one(
|
||||
"account.account",
|
||||
compute="_compute_debit_account_id",
|
||||
readonly=False,
|
||||
store=True,
|
||||
required=True,
|
||||
)
|
||||
is_credit_account_deprecated = fields.Boolean(
|
||||
compute="_compute_deprecated_accounts"
|
||||
)
|
||||
is_debit_account_deprecated = fields.Boolean(compute="_compute_deprecated_accounts")
|
||||
unspread_amount = fields.Float(
|
||||
digits="Account",
|
||||
compute="_compute_amounts",
|
||||
)
|
||||
unposted_amount = fields.Float(
|
||||
digits="Account",
|
||||
compute="_compute_amounts",
|
||||
)
|
||||
posted_amount = fields.Float(
|
||||
digits="Account",
|
||||
compute="_compute_amounts",
|
||||
)
|
||||
total_amount = fields.Float(
|
||||
digits="Account",
|
||||
compute="_compute_amounts",
|
||||
)
|
||||
all_posted = fields.Boolean(compute="_compute_all_posted", store=True)
|
||||
line_ids = fields.One2many(
|
||||
"account.spread.line", "spread_id", string="Spread Lines"
|
||||
)
|
||||
spread_date = fields.Date(
|
||||
string="Start Date", default=time.strftime("%Y-01-01"), required=True
|
||||
)
|
||||
journal_id = fields.Many2one(
|
||||
"account.journal",
|
||||
compute="_compute_journal_id",
|
||||
readonly=False,
|
||||
precompute=True,
|
||||
store=True,
|
||||
required=True,
|
||||
check_company=True,
|
||||
domain="[('id', 'in', suitable_journal_ids)]",
|
||||
)
|
||||
suitable_journal_ids = fields.Many2many(
|
||||
"account.journal",
|
||||
compute="_compute_suitable_journal_ids",
|
||||
)
|
||||
invoice_line_ids = fields.One2many(
|
||||
"account.move.line", "spread_id", copy=False, string="Invoice Lines"
|
||||
)
|
||||
invoice_line_id = fields.Many2one(
|
||||
"account.move.line",
|
||||
string="Invoice line",
|
||||
compute="_compute_invoice_line",
|
||||
inverse="_inverse_invoice_line",
|
||||
store=True,
|
||||
)
|
||||
invoice_id = fields.Many2one(
|
||||
related="invoice_line_id.move_id",
|
||||
readonly=True,
|
||||
store=True,
|
||||
)
|
||||
estimated_amount = fields.Float(digits="Account")
|
||||
company_id = fields.Many2one(
|
||||
"res.company", default=lambda self: self.env.company, required=True
|
||||
)
|
||||
currency_id = fields.Many2one(
|
||||
"res.currency",
|
||||
required=True,
|
||||
default=lambda self: self.env.company.currency_id.id,
|
||||
)
|
||||
move_line_auto_post = fields.Boolean("Auto-post lines", default=True)
|
||||
display_create_all_moves = fields.Boolean(
|
||||
compute="_compute_display_create_all_moves",
|
||||
)
|
||||
display_recompute_buttons = fields.Boolean(
|
||||
compute="_compute_display_recompute_buttons",
|
||||
)
|
||||
display_move_line_auto_post = fields.Boolean(
|
||||
compute="_compute_display_move_line_auto_post",
|
||||
string="Display Button Auto-post lines",
|
||||
)
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
@api.model
|
||||
def default_journal(self, company_id):
|
||||
domain = [("type", "=", "general"), ("company_id", "=", company_id)]
|
||||
return self.env["account.journal"].search(domain, limit=1)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super().default_get(fields)
|
||||
if "journal_id" not in res:
|
||||
company_id = res.get("company_id", self.env.company.id)
|
||||
default_journal = self.default_journal(company_id)
|
||||
if default_journal:
|
||||
res["journal_id"] = default_journal.id
|
||||
return res
|
||||
|
||||
@api.depends("company_id")
|
||||
def _compute_suitable_journal_ids(self):
|
||||
for spread in self:
|
||||
domain = [("company_id", "=", spread.company_id.id)]
|
||||
spread.suitable_journal_ids = self.env["account.journal"].search(domain)
|
||||
|
||||
@api.depends("invoice_type")
|
||||
def _compute_spread_type(self):
|
||||
for spread in self:
|
||||
if spread.invoice_type in ["out_invoice", "out_refund"]:
|
||||
spread.spread_type = "sale"
|
||||
else:
|
||||
spread.spread_type = "purchase"
|
||||
|
||||
@api.depends("invoice_line_ids", "invoice_line_ids.move_id")
|
||||
def _compute_invoice_line(self):
|
||||
for spread in self:
|
||||
invoice_lines = spread.invoice_line_ids
|
||||
spread.invoice_line_id = invoice_lines and invoice_lines[0] or False
|
||||
|
||||
def _inverse_invoice_line(self):
|
||||
for spread in self:
|
||||
invoice_line = spread.invoice_line_id
|
||||
spread.write({"invoice_line_ids": [(6, 0, [invoice_line.id])]})
|
||||
|
||||
@api.depends(
|
||||
"estimated_amount",
|
||||
"currency_id",
|
||||
"company_id",
|
||||
"invoice_line_id.price_subtotal",
|
||||
"invoice_line_id.currency_id",
|
||||
"line_ids.amount",
|
||||
"line_ids.move_id.state",
|
||||
)
|
||||
def _compute_amounts(self):
|
||||
for spread in self:
|
||||
lines_move = spread.line_ids.filtered(lambda l: l.move_id)
|
||||
moves_amount = sum(spread_line.amount for spread_line in lines_move)
|
||||
lines_posted = lines_move.filtered(lambda l: l.move_id.state == "posted")
|
||||
posted_amount = sum(spread_line.amount for spread_line in lines_posted)
|
||||
total_amount = spread.estimated_amount
|
||||
if spread.invoice_line_id:
|
||||
total_amount = spread.invoice_line_id.currency_id._convert(
|
||||
spread.invoice_line_id.balance,
|
||||
spread.currency_id,
|
||||
spread.company_id,
|
||||
spread.invoice_id.date,
|
||||
)
|
||||
|
||||
spread.unspread_amount = total_amount - moves_amount
|
||||
spread.unposted_amount = total_amount - posted_amount
|
||||
spread.posted_amount = posted_amount
|
||||
spread.total_amount = total_amount
|
||||
|
||||
@api.depends("unposted_amount")
|
||||
def _compute_all_posted(self):
|
||||
for spread in self:
|
||||
rounding = self.currency_id.rounding
|
||||
unposted = spread.unposted_amount
|
||||
spread.all_posted = float_is_zero(unposted, precision_rounding=rounding)
|
||||
|
||||
def _compute_display_create_all_moves(self):
|
||||
for spread in self:
|
||||
any_not_move = any(not line.move_id for line in spread.line_ids)
|
||||
spread.display_create_all_moves = any_not_move
|
||||
|
||||
def _compute_display_recompute_buttons(self):
|
||||
for spread in self:
|
||||
spread.display_recompute_buttons = True
|
||||
if not spread.company_id.allow_spread_planning:
|
||||
if spread.invoice_id.state == "draft":
|
||||
spread.display_recompute_buttons = False
|
||||
|
||||
@api.depends("company_id.force_move_auto_post")
|
||||
def _compute_display_move_line_auto_post(self):
|
||||
for spread in self:
|
||||
auto_post = spread.company_id.force_move_auto_post
|
||||
spread.display_move_line_auto_post = not auto_post
|
||||
|
||||
def _get_spread_entry_name(self, seq):
|
||||
"""Use this method to customise the name of the accounting entry."""
|
||||
self.ensure_one()
|
||||
return (self.name or "") + "/" + str(seq)
|
||||
|
||||
@api.onchange("template_id")
|
||||
def onchange_template(self):
|
||||
if self.template_id:
|
||||
if self.template_id.spread_type == "sale":
|
||||
if self.invoice_type in ["in_invoice", "in_refund"]:
|
||||
self.invoice_type = "out_invoice"
|
||||
else:
|
||||
if self.invoice_type in ["out_invoice", "out_refund"]:
|
||||
self.invoice_type = "in_invoice"
|
||||
if self.template_id.period_number:
|
||||
self.period_number = self.template_id.period_number
|
||||
if self.template_id.period_type:
|
||||
self.period_type = self.template_id.period_type
|
||||
if self.template_id.start_date:
|
||||
self.spread_date = self.template_id.start_date
|
||||
if self.template_id.analytic_distribution:
|
||||
self.analytic_distribution = self.template_id.analytic_distribution
|
||||
self.days_calc = self.template_id.days_calc
|
||||
|
||||
@api.depends("invoice_type", "company_id")
|
||||
def _compute_journal_id(self):
|
||||
if not self.env.context.get("default_journal_id"):
|
||||
for spread in self:
|
||||
journal = spread.company_id.default_spread_expense_journal_id
|
||||
if spread.invoice_type in ("out_invoice", "in_refund"):
|
||||
journal = spread.company_id.default_spread_revenue_journal_id
|
||||
if not journal:
|
||||
journal = self.default_journal(spread.company_id.id)
|
||||
spread.journal_id = journal
|
||||
|
||||
@api.depends("invoice_type", "company_id")
|
||||
def _compute_debit_account_id(self):
|
||||
if not self.env.context.get("default_debit_account_id"):
|
||||
invoice_types = ("out_invoice", "in_refund")
|
||||
for spread in self.filtered(lambda s: s.invoice_type in invoice_types):
|
||||
debit_account = spread.company_id.default_spread_revenue_account_id
|
||||
spread.debit_account_id = debit_account
|
||||
|
||||
@api.depends("invoice_type", "company_id")
|
||||
def _compute_credit_account_id(self):
|
||||
if not self.env.context.get("default_credit_account_id"):
|
||||
invoice_types = ("in_invoice", "out_refund")
|
||||
for spread in self.filtered(lambda s: s.invoice_type in invoice_types):
|
||||
credit_account = spread.company_id.default_spread_expense_account_id
|
||||
spread.credit_account_id = credit_account
|
||||
|
||||
@api.constrains("invoice_id", "invoice_type")
|
||||
def _check_invoice_type(self):
|
||||
if self.filtered(
|
||||
lambda s: s.invoice_id and s.invoice_type != s.invoice_id.move_type
|
||||
):
|
||||
raise ValidationError(
|
||||
_("The Invoice Type does not correspond to the Invoice")
|
||||
)
|
||||
|
||||
@api.constrains("journal_id")
|
||||
def _check_journal(self):
|
||||
for spread in self:
|
||||
moves = spread.mapped("line_ids.move_id").filtered("journal_id")
|
||||
if any(move.journal_id != spread.journal_id for move in moves):
|
||||
err_msg = _("The Journal is not consistent with the account moves.")
|
||||
raise ValidationError(err_msg)
|
||||
|
||||
@api.constrains("template_id", "invoice_type")
|
||||
def _check_template_invoice_type(self):
|
||||
for spread in self.filtered(lambda s: s.template_id.spread_type == "sale"):
|
||||
if spread.invoice_type in ["in_invoice", "in_refund"]:
|
||||
err_msg = _(
|
||||
"The Spread Template (Sales) is not compatible "
|
||||
"with selected invoice type"
|
||||
)
|
||||
raise ValidationError(err_msg)
|
||||
for spread in self.filtered(lambda s: s.template_id.spread_type == "purchase"):
|
||||
if spread.invoice_type in ["out_invoice", "out_refund"]:
|
||||
err_msg = _(
|
||||
"The Spread Template (Purchases) is not compatible "
|
||||
"with selected invoice type"
|
||||
)
|
||||
raise ValidationError(err_msg)
|
||||
|
||||
def _get_spread_period_duration(self):
|
||||
"""Converts the selected period_type to number of months."""
|
||||
self.ensure_one()
|
||||
if self.period_type == "year":
|
||||
return 12
|
||||
elif self.period_type == "quarter":
|
||||
return 3
|
||||
return 1
|
||||
|
||||
def _init_line_date(self, posted_line_ids):
|
||||
"""Calculates the initial spread date. This method
|
||||
is used by "def _compute_spread_board()" method.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if posted_line_ids:
|
||||
# if we already have some previous validated entries,
|
||||
# starting date is last entry + method period
|
||||
last_date = posted_line_ids[-1].date
|
||||
months = self._get_spread_period_duration()
|
||||
spread_date = last_date + relativedelta(months=months)
|
||||
else:
|
||||
spread_date = self.spread_date
|
||||
return spread_date
|
||||
|
||||
def _next_line_date(self, month_day, date):
|
||||
"""Calculates the next spread date. This method
|
||||
is used by "def _compute_spread_board()" method.
|
||||
"""
|
||||
self.ensure_one()
|
||||
months = self._get_spread_period_duration()
|
||||
date = date + relativedelta(months=months)
|
||||
# get the last day of the month
|
||||
if month_day > 28:
|
||||
max_day_in_month = calendar.monthrange(date.year, date.month)[1]
|
||||
date = date.replace(day=min(max_day_in_month, month_day))
|
||||
return date
|
||||
|
||||
def _compute_spread_board(self):
|
||||
"""Creates the spread lines. This method is highly inspired
|
||||
from method compute_depreciation_board() present in standard
|
||||
Odoo 11.0 "account_asset" module, developed by Odoo SA.
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
posted_line_ids = self.line_ids.filtered(
|
||||
lambda x: x.move_id.state == "posted"
|
||||
).sorted(key=lambda l: l.date)
|
||||
unposted_line_ids = self.line_ids.filtered(
|
||||
lambda x: not x.move_id.state == "posted"
|
||||
)
|
||||
|
||||
# Remove old unposted spread lines.
|
||||
commands = [(2, line_id.id, False) for line_id in unposted_line_ids]
|
||||
|
||||
if self.unposted_amount != 0.0:
|
||||
unposted_amount = self.unposted_amount
|
||||
|
||||
spread_date = self._init_line_date(posted_line_ids)
|
||||
|
||||
month_day = spread_date.day
|
||||
number_of_periods = self._get_number_of_periods(month_day)
|
||||
|
||||
for x in range(len(posted_line_ids), number_of_periods):
|
||||
sequence = x + 1
|
||||
date = self._get_last_day_of_month(spread_date)
|
||||
amount = self._compute_board_amount(
|
||||
sequence, unposted_amount, number_of_periods, date
|
||||
)
|
||||
amount = self.currency_id.round(amount)
|
||||
rounding = self.currency_id.rounding
|
||||
if float_is_zero(amount, precision_rounding=rounding):
|
||||
continue
|
||||
unposted_amount -= amount
|
||||
vals = {
|
||||
"amount": amount,
|
||||
"spread_id": self.id,
|
||||
"name": self._get_spread_entry_name(sequence),
|
||||
"date": date,
|
||||
}
|
||||
commands.append((0, False, vals))
|
||||
|
||||
spread_date = self._next_line_date(month_day, spread_date)
|
||||
|
||||
self.write({"line_ids": commands})
|
||||
invoice_type_selection = dict(
|
||||
self.fields_get(allfields=["invoice_type"])["invoice_type"]["selection"]
|
||||
)[self.invoice_type]
|
||||
msg_body = _("Spread table '%s' created.") % invoice_type_selection
|
||||
self.message_post(body=msg_body)
|
||||
|
||||
def _get_number_of_periods(self, month_day):
|
||||
"""Calculates the number of spread lines."""
|
||||
self.ensure_one()
|
||||
return self.period_number + 1 if month_day != 1 else self.period_number
|
||||
|
||||
@staticmethod
|
||||
def _get_first_day_of_month(spread_date):
|
||||
return spread_date + relativedelta(day=1)
|
||||
|
||||
@staticmethod
|
||||
def _get_last_day_of_month(spread_date):
|
||||
return spread_date + relativedelta(day=31)
|
||||
|
||||
def _get_spread_start_date(self, period_type, spread_end_date):
|
||||
self.ensure_one()
|
||||
spread_start_date = spread_end_date + relativedelta(days=1)
|
||||
if period_type == "month":
|
||||
spread_start_date = spread_end_date + relativedelta(day=1)
|
||||
elif period_type == "quarter":
|
||||
spread_start_date = spread_start_date - relativedelta(months=3)
|
||||
elif period_type == "year":
|
||||
spread_start_date = spread_start_date - relativedelta(years=1)
|
||||
spread_start_date = self._get_first_day_of_month(spread_start_date)
|
||||
spread_start_date = max(spread_start_date, self.spread_date)
|
||||
return spread_start_date
|
||||
|
||||
def _get_spread_end_date(self, period_type, period_number, spread_start_date):
|
||||
self.ensure_one()
|
||||
spread_end_date = spread_start_date
|
||||
number_of_periods = (
|
||||
period_number if spread_start_date.day != 1 else period_number - 1
|
||||
)
|
||||
if period_type == "month":
|
||||
spread_end_date = spread_start_date + relativedelta(
|
||||
months=number_of_periods
|
||||
)
|
||||
elif period_type == "quarter":
|
||||
months = number_of_periods * 3
|
||||
spread_end_date = spread_start_date + relativedelta(months=months)
|
||||
elif period_type == "year":
|
||||
spread_end_date = spread_start_date + relativedelta(years=number_of_periods)
|
||||
# calculate by days and not first day of month should compute residual day only
|
||||
if self.days_calc and spread_end_date.day != 1:
|
||||
spread_end_date = spread_end_date - relativedelta(days=1)
|
||||
else:
|
||||
spread_end_date = self._get_last_day_of_month(spread_end_date)
|
||||
return spread_end_date
|
||||
|
||||
def _get_amount_per_day(self, amount):
|
||||
self.ensure_one()
|
||||
spread_start_date = self.spread_date
|
||||
spread_end_date = self._get_spread_end_date(
|
||||
self.period_type, self.period_number, spread_start_date
|
||||
)
|
||||
number_of_days = (spread_end_date - spread_start_date).days + 1
|
||||
return amount / number_of_days
|
||||
|
||||
def _compute_board_amount(
|
||||
self, sequence, amount, number_of_periods, spread_end_date
|
||||
):
|
||||
"""Calculates the amount for the spread lines."""
|
||||
self.ensure_one()
|
||||
amount_to_spread = self.total_amount
|
||||
period = self.period_number
|
||||
if sequence != number_of_periods:
|
||||
amount = amount_to_spread / period
|
||||
if sequence == 1:
|
||||
date = self.spread_date
|
||||
month_days = calendar.monthrange(date.year, date.month)[1]
|
||||
days = month_days - date.day + 1
|
||||
amount = (amount_to_spread / period) / month_days * days
|
||||
if self.days_calc:
|
||||
spread_start_date = self._get_spread_start_date(
|
||||
self.period_type, spread_end_date
|
||||
)
|
||||
days = (spread_end_date - spread_start_date).days + 1
|
||||
amount = self._get_amount_per_day(amount_to_spread) * days
|
||||
return amount
|
||||
|
||||
def compute_spread_board(self):
|
||||
"""Checks whether the spread lines should be calculated.
|
||||
In case checks pass, invoke "def _compute_spread_board()" method.
|
||||
"""
|
||||
for spread in self.filtered(lambda s: s.total_amount):
|
||||
spread._compute_spread_board()
|
||||
|
||||
def action_recalculate_spread(self):
|
||||
"""Recalculate spread"""
|
||||
self.ensure_one()
|
||||
spread_lines = self.mapped("line_ids").filtered("move_id")
|
||||
spread_lines.unlink_move()
|
||||
self.compute_spread_board()
|
||||
self.env["account.spread.line"]._create_entries()
|
||||
|
||||
def action_undo_spread(self):
|
||||
"""Undo spreading: Remove all created moves"""
|
||||
self.ensure_one()
|
||||
self.mapped("line_ids").filtered("move_id").unlink_move()
|
||||
self.mapped("line_ids").unlink()
|
||||
|
||||
def action_unlink_invoice_line(self):
|
||||
"""Unlink the invoice line from the spread board"""
|
||||
self.ensure_one()
|
||||
if self.invoice_id.state != "draft":
|
||||
msg = _("Cannot unlink invoice lines if the invoice is validated")
|
||||
raise UserError(msg)
|
||||
self._action_unlink_invoice_line()
|
||||
|
||||
def _action_unlink_invoice_line(self):
|
||||
self.mapped("line_ids.move_id.line_ids").remove_move_reconcile()
|
||||
self._message_post_unlink_invoice_line()
|
||||
self.write({"invoice_line_ids": [(5, 0, 0)]})
|
||||
|
||||
def _message_post_unlink_invoice_line(self):
|
||||
for spread in self:
|
||||
inv_link = (
|
||||
"<a href=# data-oe-model=account.move "
|
||||
"data-oe-id=%d>%s</a>" % (spread.invoice_id.id, _("Invoice"))
|
||||
)
|
||||
msg_body = _(
|
||||
"Unlinked invoice line '%(spread_line_name)s' (view %(inv_link)s)."
|
||||
) % {
|
||||
"spread_line_name": spread.invoice_line_id.name,
|
||||
"inv_link": inv_link,
|
||||
}
|
||||
spread.message_post(body=msg_body)
|
||||
spread_link = (
|
||||
"<a href=# data-oe-model=account.spread "
|
||||
"data-oe-id=%d>%s</a>" % (spread.id, _("Spread"))
|
||||
)
|
||||
msg_body = _("Unlinked '%(spread_link)s' (invoice line %(inv_line)s).") % {
|
||||
"spread_link": spread_link,
|
||||
"inv_line": spread.invoice_line_id.name,
|
||||
}
|
||||
spread.invoice_id.message_post(body=msg_body)
|
||||
|
||||
def unlink(self):
|
||||
if self.filtered(lambda s: s.invoice_line_id):
|
||||
err_msg = _("Cannot delete spread(s) that are linked to an invoice line.")
|
||||
raise UserError(err_msg)
|
||||
if self.mapped("line_ids.move_id").filtered(lambda m: m.state == "posted"):
|
||||
err_msg = _("Cannot delete spread(s): there are posted Journal Entries.")
|
||||
raise ValidationError(err_msg)
|
||||
return super().unlink()
|
||||
|
||||
def reconcile_spread_moves(self):
|
||||
for spread in self:
|
||||
spread._reconcile_spread_moves()
|
||||
|
||||
def _reconcile_spread_moves(self, created_moves=False):
|
||||
"""Reconcile spread moves if possible"""
|
||||
self.ensure_one()
|
||||
|
||||
spread_mls = self.line_ids.mapped("move_id.line_ids")
|
||||
if created_moves:
|
||||
spread_mls |= created_moves.mapped("line_ids")
|
||||
|
||||
account = self.invoice_line_id.account_id
|
||||
mls_to_reconcile = spread_mls.filtered(lambda l: l.account_id == account)
|
||||
|
||||
if mls_to_reconcile:
|
||||
do_reconcile = mls_to_reconcile + self.invoice_line_id
|
||||
do_reconcile.remove_move_reconcile()
|
||||
for line in do_reconcile:
|
||||
line.reconciled = False
|
||||
# ensure to reconcile only posted items
|
||||
do_reconcile = do_reconcile.filtered(lambda l: l.move_id.state == "posted")
|
||||
do_reconcile._check_spread_reconcile_validity()
|
||||
do_reconcile.reconcile()
|
||||
|
||||
def create_all_moves(self):
|
||||
for line in self.mapped("line_ids").filtered(lambda l: not l.move_id):
|
||||
line.create_move()
|
||||
|
||||
def _post_spread_moves(self, moves):
|
||||
self.ensure_one()
|
||||
moves = moves.filtered(lambda l: l.state != "posted")
|
||||
if not moves:
|
||||
return
|
||||
ctx = dict(self.env.context, skip_unique_sequence_number=True)
|
||||
if self.company_id.force_move_auto_post or self.move_line_auto_post:
|
||||
moves.with_context(**ctx).action_post()
|
||||
|
||||
@api.depends("debit_account_id.deprecated", "credit_account_id.deprecated")
|
||||
def _compute_deprecated_accounts(self):
|
||||
for spread in self:
|
||||
spread.is_debit_account_deprecated = spread.debit_account_id.deprecated
|
||||
spread.is_credit_account_deprecated = spread.credit_account_id.deprecated
|
||||
|
||||
def open_posted_view(self):
|
||||
action_name = "account_spread_cost_revenue.action_account_moves_all_spread"
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(action_name)
|
||||
action["domain"] = [("id", "in", [])]
|
||||
spread_mls = self.line_ids.mapped("move_id.line_ids")
|
||||
if self.env.context.get("show_reconciled_only"):
|
||||
spread_mls = spread_mls.filtered(lambda m: m.reconciled)
|
||||
if spread_mls:
|
||||
domain = [("id", "in", spread_mls.ids + [self.invoice_line_id.id])]
|
||||
action["domain"] = domain
|
||||
return action
|
173
account_spread_cost_revenue/models/account_spread_line.py
Normal file
173
account_spread_cost_revenue/models/account_spread_line.py
Normal file
@ -0,0 +1,173 @@
|
||||
# Copyright 2016-2020 Onestein (<https://www.onestein.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class AccountInvoiceSpreadLine(models.Model):
|
||||
_name = "account.spread.line"
|
||||
_description = "Account Spread Lines"
|
||||
_order = "date"
|
||||
|
||||
name = fields.Char("Description", readonly=True)
|
||||
amount = fields.Float(digits="Account", required=True)
|
||||
date = fields.Date(required=True)
|
||||
spread_id = fields.Many2one("account.spread", ondelete="cascade")
|
||||
move_id = fields.Many2one("account.move", string="Journal Entry", readonly=True)
|
||||
|
||||
def create_and_reconcile_moves(self):
|
||||
grouped_lines = {}
|
||||
for spread_line in self:
|
||||
spread = spread_line.spread_id
|
||||
spread_line_list = grouped_lines.get(
|
||||
spread, self.env["account.spread.line"]
|
||||
)
|
||||
grouped_lines.update({spread: spread_line_list + spread_line})
|
||||
for spread in grouped_lines:
|
||||
created_moves = grouped_lines[spread]._create_moves()
|
||||
|
||||
if created_moves:
|
||||
post_msg = _("Created move(s) ")
|
||||
post_msg += ", ".join(
|
||||
"<a href=# data-oe-model=account.move data-oe-id=%d"
|
||||
">%s</a>" % (move.id, move.name)
|
||||
for move in created_moves
|
||||
)
|
||||
spread.message_post(body=post_msg)
|
||||
spread._post_spread_moves(created_moves)
|
||||
|
||||
def create_move(self):
|
||||
"""Button to manually create a move from a spread line entry."""
|
||||
self.ensure_one()
|
||||
self.with_context(
|
||||
skip_create_template=True,
|
||||
).create_and_reconcile_moves()
|
||||
|
||||
def _create_moves(self):
|
||||
if self.filtered(lambda l: l.move_id):
|
||||
raise UserError(
|
||||
_(
|
||||
"This spread line is already linked to a "
|
||||
"journal entry! Please post or delete it."
|
||||
)
|
||||
)
|
||||
|
||||
created_moves = self.env["account.move"]
|
||||
for line in self:
|
||||
move_vals = line._prepare_move()
|
||||
move = self.env["account.move"].create(move_vals)
|
||||
line.move_id = move
|
||||
created_moves += move
|
||||
return created_moves
|
||||
|
||||
def _prepare_move(self):
|
||||
self.ensure_one()
|
||||
|
||||
spread_date = self.env.context.get("spread_date") or self.date
|
||||
spread = self.spread_id
|
||||
analytic_distribution = spread.analytic_distribution
|
||||
|
||||
company_currency = spread.company_id.currency_id
|
||||
current_currency = spread.currency_id
|
||||
amount = current_currency._convert(
|
||||
self.amount, company_currency, spread.company_id, spread_date
|
||||
)
|
||||
|
||||
debit_credit = spread.invoice_type in ["in_invoice", "out_refund"]
|
||||
|
||||
line_ids = [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": spread.name,
|
||||
"account_id": spread.debit_account_id.id
|
||||
if debit_credit
|
||||
else spread.credit_account_id.id,
|
||||
"debit": amount if amount > 0.0 else 0.0,
|
||||
"credit": -amount if amount < 0.0 else 0.0,
|
||||
"partner_id": self.spread_id.invoice_id.partner_id.id,
|
||||
"journal_id": self.spread_id.journal_id.id,
|
||||
"analytic_distribution": analytic_distribution,
|
||||
"date": self.date,
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": spread.name,
|
||||
"account_id": spread.credit_account_id.id
|
||||
if debit_credit
|
||||
else spread.debit_account_id.id,
|
||||
"credit": amount if amount > 0.0 else 0.0,
|
||||
"debit": -amount if amount < 0.0 else 0.0,
|
||||
"partner_id": self.spread_id.invoice_id.partner_id.id,
|
||||
"journal_id": self.spread_id.journal_id.id,
|
||||
"analytic_distribution": analytic_distribution,
|
||||
"date": self.date,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
return {
|
||||
"name": False,
|
||||
"ref": self.name,
|
||||
"date": spread_date,
|
||||
"invoice_date": spread_date,
|
||||
"journal_id": spread.journal_id.id,
|
||||
"line_ids": line_ids,
|
||||
"company_id": spread.company_id.id,
|
||||
"partner_id": spread.invoice_id.partner_id.id,
|
||||
}
|
||||
|
||||
def open_move(self):
|
||||
"""Used by a button to manually view a move from a spread line entry."""
|
||||
self.ensure_one()
|
||||
return {
|
||||
"name": _("Journal Entry"),
|
||||
"view_mode": "form",
|
||||
"res_model": "account.move",
|
||||
"view_id": False,
|
||||
"type": "ir.actions.act_window",
|
||||
"res_id": self.move_id.id,
|
||||
}
|
||||
|
||||
def unlink_move(self):
|
||||
"""Used by a button to manually unlink a move from a spread line entry."""
|
||||
for line in self:
|
||||
move = line.move_id
|
||||
if move.state == "posted":
|
||||
move.button_cancel()
|
||||
move.line_ids.remove_move_reconcile()
|
||||
post_msg = _("Deleted move %s") % line.move_id.id
|
||||
move.with_context(force_delete=True).unlink()
|
||||
line.move_id = False
|
||||
line.spread_id.message_post(body=post_msg)
|
||||
|
||||
@api.model
|
||||
def _create_entries(self):
|
||||
"""Find spread line entries where date is in the past and
|
||||
create moves for them. Method also called by the cron job.
|
||||
"""
|
||||
lines = self.search(
|
||||
[("date", "<=", fields.Date.today()), ("move_id", "=", False)]
|
||||
)
|
||||
lines.create_and_reconcile_moves()
|
||||
|
||||
unposted_moves = (
|
||||
self.search([("move_id", "!=", False)])
|
||||
.mapped("move_id")
|
||||
.filtered(lambda m: m.state != "posted")
|
||||
)
|
||||
unposted_moves.filtered(
|
||||
lambda m: m.company_id.force_move_auto_post
|
||||
).action_post()
|
||||
|
||||
spreads_to_archive = (
|
||||
self.env["account.spread"]
|
||||
.search([("all_posted", "=", True)])
|
||||
.filtered(lambda s: s.company_id.auto_archive_spread)
|
||||
)
|
||||
spreads_to_archive.write({"active": False})
|
179
account_spread_cost_revenue/models/account_spread_template.py
Normal file
179
account_spread_cost_revenue/models/account_spread_template.py
Normal file
@ -0,0 +1,179 @@
|
||||
# Copyright 2018-2020 Onestein (<https://www.onestein.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class AccountSpreadTemplate(models.Model):
|
||||
_name = "account.spread.template"
|
||||
_inherit = "analytic.mixin"
|
||||
_description = "Account Spread Template"
|
||||
|
||||
name = fields.Char(required=True)
|
||||
spread_type = fields.Selection(
|
||||
[("sale", "Customer"), ("purchase", "Supplier")], default="sale", required=True
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
"res.company", default=lambda self: self.env.company, required=True
|
||||
)
|
||||
spread_journal_id = fields.Many2one(
|
||||
"account.journal",
|
||||
string="Journal",
|
||||
compute="_compute_spread_journal",
|
||||
readonly=False,
|
||||
store=True,
|
||||
required=True,
|
||||
)
|
||||
use_invoice_line_account = fields.Boolean(
|
||||
string="Invoice account as spread account",
|
||||
help="Use invoice line's account as Balance sheet / spread account.\n"
|
||||
"In this case, user need to select expense/revenue account too.",
|
||||
)
|
||||
spread_account_id = fields.Many2one(
|
||||
"account.account",
|
||||
string="Spread Balance Sheet Account",
|
||||
compute="_compute_spread_account",
|
||||
readonly=False,
|
||||
store=True,
|
||||
required=False,
|
||||
)
|
||||
exp_rev_account_id = fields.Many2one(
|
||||
"account.account",
|
||||
string="Expense/Revenue Account",
|
||||
help="Optional account to overwrite the existing expense/revenue account",
|
||||
)
|
||||
period_number = fields.Integer(
|
||||
string="Number of Repetitions", help="Define the number of spread lines"
|
||||
)
|
||||
period_type = fields.Selection(
|
||||
[("month", "Month"), ("quarter", "Quarter"), ("year", "Year")],
|
||||
help="Period length for the entries",
|
||||
)
|
||||
start_date = fields.Date()
|
||||
days_calc = fields.Boolean(
|
||||
string="Calculate by days",
|
||||
default=False,
|
||||
help="Use number of days to calculate amount",
|
||||
)
|
||||
auto_spread = fields.Boolean(
|
||||
string="Auto assign template on invoice validate",
|
||||
help="If checked, provide option to auto create spread during "
|
||||
"invoice validation, based on product/account/analytic in invoice line.",
|
||||
)
|
||||
auto_spread_ids = fields.One2many(
|
||||
comodel_name="account.spread.template.auto",
|
||||
string="Auto Spread On",
|
||||
inverse_name="template_id",
|
||||
)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super().default_get(fields)
|
||||
if not res.get("company_id"):
|
||||
res["company_id"] = self.env.company.id
|
||||
if "spread_journal_id" not in res:
|
||||
default_journal = self.env["account.spread"].default_journal(
|
||||
res["company_id"]
|
||||
)
|
||||
if default_journal:
|
||||
res["spread_journal_id"] = default_journal.id
|
||||
return res
|
||||
|
||||
@api.constrains("auto_spread", "auto_spread_ids")
|
||||
def _check_product_account(self):
|
||||
for rec in self.filtered("auto_spread"):
|
||||
for line in rec.auto_spread_ids:
|
||||
if not line.product_id and not line.account_id:
|
||||
raise UserError(
|
||||
_(
|
||||
"Please select product and/or account "
|
||||
"on auto spread options"
|
||||
)
|
||||
)
|
||||
|
||||
@api.depends("spread_type", "company_id")
|
||||
def _compute_spread_journal(self):
|
||||
for spread in self:
|
||||
company = spread.company_id
|
||||
if spread.spread_type == "sale":
|
||||
journal = company.default_spread_revenue_journal_id
|
||||
else:
|
||||
journal = company.default_spread_expense_journal_id
|
||||
if journal:
|
||||
spread.spread_journal_id = journal
|
||||
|
||||
@api.depends("spread_type", "company_id")
|
||||
def _compute_spread_account(self):
|
||||
for spread in self:
|
||||
company = spread.company_id
|
||||
if spread.spread_type == "sale":
|
||||
account = company.default_spread_revenue_account_id
|
||||
else:
|
||||
account = company.default_spread_expense_account_id
|
||||
if account:
|
||||
spread.spread_account_id = account
|
||||
|
||||
@api.onchange("use_invoice_line_account")
|
||||
def _onchange_user_invoice_line_account(self):
|
||||
self.exp_rev_account_id = False
|
||||
|
||||
def _prepare_spread_from_template(self, spread_account_id=False):
|
||||
self.ensure_one()
|
||||
company = self.company_id
|
||||
spread_vals = {
|
||||
"name": self.name,
|
||||
"template_id": self.id,
|
||||
"journal_id": self.spread_journal_id.id,
|
||||
"use_invoice_line_account": self.use_invoice_line_account,
|
||||
"days_calc": self.days_calc,
|
||||
"company_id": company.id,
|
||||
}
|
||||
|
||||
account_id = spread_account_id or self.spread_account_id.id
|
||||
if self.spread_type == "sale":
|
||||
invoice_type = "out_invoice"
|
||||
spread_vals["debit_account_id"] = account_id
|
||||
else:
|
||||
invoice_type = "in_invoice"
|
||||
spread_vals["credit_account_id"] = account_id
|
||||
|
||||
if self.period_number:
|
||||
spread_vals["period_number"] = self.period_number
|
||||
if self.period_type:
|
||||
spread_vals["period_type"] = self.period_type
|
||||
if self.start_date:
|
||||
spread_vals["spread_date"] = self.start_date
|
||||
|
||||
spread_vals["invoice_type"] = invoice_type
|
||||
return spread_vals
|
||||
|
||||
|
||||
class AccountSpreadTemplateAuto(models.Model):
|
||||
_name = "account.spread.template.auto"
|
||||
_inherit = "analytic.mixin"
|
||||
_description = "Auto create spread, based on product/account/analytic"
|
||||
|
||||
template_id = fields.Many2one(
|
||||
comodel_name="account.spread.template",
|
||||
string="Spread Template",
|
||||
required=True,
|
||||
ondelete="cascade",
|
||||
index=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
related="template_id.company_id",
|
||||
store=True,
|
||||
)
|
||||
name = fields.Char(
|
||||
required=True,
|
||||
default="/",
|
||||
)
|
||||
product_id = fields.Many2one(
|
||||
comodel_name="product.product",
|
||||
string="Product",
|
||||
)
|
||||
account_id = fields.Many2one(
|
||||
comodel_name="account.account",
|
||||
string="Account",
|
||||
)
|
40
account_spread_cost_revenue/models/res_company.py
Normal file
40
account_spread_cost_revenue/models/res_company.py
Normal file
@ -0,0 +1,40 @@
|
||||
# Copyright 2018-2020 Onestein (<https://www.onestein.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = "res.company"
|
||||
|
||||
default_spread_revenue_account_id = fields.Many2one(
|
||||
"account.account", string="Revenue Spread Account"
|
||||
)
|
||||
|
||||
default_spread_expense_account_id = fields.Many2one(
|
||||
"account.account", string="Expense Spread Account"
|
||||
)
|
||||
|
||||
default_spread_revenue_journal_id = fields.Many2one(
|
||||
"account.journal", string="Revenue Spread Journal"
|
||||
)
|
||||
|
||||
default_spread_expense_journal_id = fields.Many2one(
|
||||
"account.journal", string="Expense Spread Journal"
|
||||
)
|
||||
|
||||
allow_spread_planning = fields.Boolean(
|
||||
default=True,
|
||||
help="Disable this option if you do not want to allow the "
|
||||
"spreading before the invoice is validated.",
|
||||
)
|
||||
force_move_auto_post = fields.Boolean(
|
||||
"Auto-post spread lines",
|
||||
help="Enable this option if you want to post automatically the "
|
||||
"accounting moves of all the spreads.",
|
||||
)
|
||||
auto_archive_spread = fields.Boolean(
|
||||
"Auto-archive spread",
|
||||
help="Enable this option if you want the cron job to automatically "
|
||||
"archive the spreads when all lines are posted.",
|
||||
)
|
25
account_spread_cost_revenue/readme/CONFIGURE.rst
Normal file
25
account_spread_cost_revenue/readme/CONFIGURE.rst
Normal file
@ -0,0 +1,25 @@
|
||||
To be able to access the full spreading features, the user must belong to *Show Full Accounting Features* group.
|
||||
|
||||
On the form view of the company, in the *Account Spread* tab, you can configure
|
||||
the journals in which the spread journal items will be generated by default:
|
||||
|
||||
* the *Default Spread Journal for Revenues*,
|
||||
* the *Default Spread Journal for Expenses*.
|
||||
|
||||
In the same *Account Spread* tab, you can also configure the Spread Balance Sheet Accounts used by default:
|
||||
|
||||
* the *Default Spread Account for Revenues*,
|
||||
* the *Default Spread Account for Expenses*.
|
||||
|
||||
This module by default allows the spreading even before the receipt of the invoice or when the invoice is still draft,
|
||||
so that it is possible to work on the plan of the cost/revenue spreading. To disable this feature, on the form view of
|
||||
the company disable the *Allow Spread Planning* option.
|
||||
|
||||
In Spread Template, there is also option to *Auto assign template on invoice validate*, based on the preset invoice line criteria.
|
||||
|
||||
On the form view of the company, the *Auto-post spread lines* option forces the account moves created
|
||||
during the cost/revenue spreading to be automatically posted. When this option is false, the user can
|
||||
enable/disable the automatic posting by the flag *Auto-post lines* present in the spread board.
|
||||
|
||||
On the form view of the company, enable the *Auto-archive spread* option if you want the
|
||||
cron job to automatically archive the spreads when all lines are posted.
|
3
account_spread_cost_revenue/readme/CONTRIBUTORS.rst
Normal file
3
account_spread_cost_revenue/readme/CONTRIBUTORS.rst
Normal file
@ -0,0 +1,3 @@
|
||||
* Andrea Stirpe <a.stirpe@onestein.nl>
|
||||
* Kitti U. <kittiu@ecosoft.co.th>
|
||||
* Saran Lim. <saranl@ecosoft.co.th>
|
3
account_spread_cost_revenue/readme/CREDITS.rst
Normal file
3
account_spread_cost_revenue/readme/CREDITS.rst
Normal file
@ -0,0 +1,3 @@
|
||||
Part of the code in this module (in particular the computation of the spread lines)
|
||||
is highly inspired by the Assets Management module from the standard
|
||||
Odoo 11.0 Community developed by Odoo SA.
|
1
account_spread_cost_revenue/readme/DESCRIPTION.rst
Normal file
1
account_spread_cost_revenue/readme/DESCRIPTION.rst
Normal file
@ -0,0 +1 @@
|
||||
Allows to spread costs or revenues over a customizable periods, to even out cost or invoice spikes.
|
28
account_spread_cost_revenue/readme/HISTORY.rst
Normal file
28
account_spread_cost_revenue/readme/HISTORY.rst
Normal file
@ -0,0 +1,28 @@
|
||||
13.0.1.0.0
|
||||
~~~~~~~~~~
|
||||
|
||||
* [MIG] Port account_spread_cost_revenue to V13.
|
||||
|
||||
12.0.2.0.0
|
||||
~~~~~~~~~~
|
||||
|
||||
* [ENH] In spread template, add option to auto create spread on invoice validation
|
||||
|
||||
12.0.1.1.0
|
||||
~~~~~~~~~~
|
||||
|
||||
* [ENH] Add optional Expense/Revenue Account in Chart Template, which can be used
|
||||
in place of account from invoice line to set Expense/Revenue account in the spread
|
||||
|
||||
|
||||
12.0.1.0.0
|
||||
~~~~~~~~~~
|
||||
|
||||
* [MIG] Port account_spread_cost_revenue to V12.
|
||||
|
||||
|
||||
11.0.1.0.0
|
||||
~~~~~~~~~~
|
||||
|
||||
* [ADD] Module account_spread_cost_revenue.
|
||||
(`#715 <https://github.com/OCA/account-financial-tools/pull/715>`_)
|
79
account_spread_cost_revenue/readme/USAGE.rst
Normal file
79
account_spread_cost_revenue/readme/USAGE.rst
Normal file
@ -0,0 +1,79 @@
|
||||
Define Spread Costs/Revenues Board
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Under Invoicing -> Accounting -> Journals -> Spread Costs/Revenues, create a new spread board.
|
||||
|
||||
Complete the definition of the spreading criteria, by setting the the fields:
|
||||
|
||||
* *Debit Account*
|
||||
* *Credit Account*
|
||||
* *Estimated Amount* (The total amount to spread)
|
||||
* *Number of Repetitions*
|
||||
* *Period Type* (Duration of each period)
|
||||
* *Start date*
|
||||
* *Journal*
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/spread.png
|
||||
:alt: Create a new spread board
|
||||
|
||||
Click on the "Recalculate unposted lines" button on the top-left to calculate the spread lines.
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/create_spread.png
|
||||
:alt: The spreading board is defined
|
||||
|
||||
A cron job will automatically create the accounting moves for all the lines having date previous that the current day (today).
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/update_spread.png
|
||||
:alt: The spreading board is updated by the cron job
|
||||
|
||||
By default, the status of the created accounting moves is posted.
|
||||
To disable the automatic posting of the accounting moves, set the flag *Auto-post lines* to False.
|
||||
This flag is only available when the *Auto-post spread lines* option, present on the form view of the company, is disabled.
|
||||
|
||||
Click on button *Recalculate entire spread* button in the spread board to force the recalculation of the spread lines:
|
||||
this will also reset all the journal entries previously created.
|
||||
|
||||
Link Invoice to Spread Costs/Revenues Board
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Create an invoice or vendor bill in draft. On its lines, the spreading right-arrow icon are displayed in dark-grey color.
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/invoice_line_1.png
|
||||
:alt: On the invoice line the spreading icon is displayed
|
||||
|
||||
Click on the spreading right-arrow icon. A wizard prompts to enter a *Spread Action Type*:
|
||||
|
||||
- *Link to existing spread board*
|
||||
- *Create from spread template*
|
||||
- *Create new spread board*
|
||||
|
||||
Select *Link to existing spread board* and enter the previously generated Spread Board. Click on Confirm button:
|
||||
the selected Spread Board will be automatically displayed.
|
||||
|
||||
Go back to the draft invoice/bill. The spreading functionality is now enabled on the invoice line:
|
||||
the spreading right-arrow icon is now displayed in green color.
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/invoice_line_2.png
|
||||
:alt: On the invoice line the spreading icon is displayed in green color
|
||||
|
||||
Validate the invoice/bill. Click on the spreading (green) right-arrow icon to open the spread board, then click
|
||||
on the smart button *Posted entries* to see the moves of the spread lines together with the move of the invoice line.
|
||||
|
||||
In case the Subtotal Price of the invoice line is different than the *Estimated Amount* of the spread board, the spread
|
||||
lines (not yet posted) will be recalculated when validating the invoice/bill.
|
||||
|
||||
Define Spread Costs/Revenues Template
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Under Invoicing -> Configuration -> Accounting -> Spread Templates, create a new spread template.
|
||||
|
||||
* *Spread Type*
|
||||
* *Spread Balance Sheet Account*
|
||||
* *Expense/Revenue Account* This option visible if invoice line account is balance sheet account, user need to specify this too.
|
||||
* *Journal*
|
||||
* *Auto assign template on invoice validate*
|
||||
|
||||
When creating a new Spread Costs/Revenues Board, select the right template.
|
||||
This way the above fields will be copied to the Spread Board.
|
||||
|
||||
If *Auto assign template on invoice validate* is checked, this template will be used to auto create spread, if the underlining invoice match the preset product/account/analytic criteria.
|
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record id="account_spread_multi_company_rule" model="ir.rule">
|
||||
<field name="name">Account Spread multi-company</field>
|
||||
<field ref="model_account_spread" name="model_id" />
|
||||
<field eval="True" name="global" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
<record id="account_spread_template_multi_company_rule" model="ir.rule">
|
||||
<field name="name">Account Spread Template multi-company</field>
|
||||
<field ref="model_account_spread_template" name="model_id" />
|
||||
<field eval="True" name="global" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
<record id="account_spread_template_auto_multi_company_rule" model="ir.rule">
|
||||
<field name="name">Account Spread Tempalte Auto multi-company</field>
|
||||
<field ref="model_account_spread_template_auto" name="model_id" />
|
||||
<field eval="True" name="global" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
</odoo>
|
10
account_spread_cost_revenue/security/ir.model.access.csv
Normal file
10
account_spread_cost_revenue/security/ir.model.access.csv
Normal file
@ -0,0 +1,10 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_account_spread_cost_revenue_full,Full access on account.spread,model_account_spread,account.group_account_manager,1,1,1,1
|
||||
access_account_spread_cost_revenue_read,Read access on account.spread,model_account_spread,account.group_account_invoice,1,0,0,0
|
||||
access_account_spread_cost_revenue_line_full,Full access on account.spread.line,model_account_spread_line,account.group_account_manager,1,1,1,1
|
||||
access_account_spread_cost_revenue_line_read,Read access on account.spread.line,model_account_spread_line,account.group_account_invoice,1,0,0,0
|
||||
access_account_spread_cost_revenue_template_full,Full access on account.spread.template,model_account_spread_template,account.group_account_manager,1,1,1,1
|
||||
access_account_spread_cost_revenue_template_read,Read access on account.spread.template,model_account_spread_template,account.group_account_invoice,1,0,0,0
|
||||
access_account_spread_cost_revenue_template_auto_full,Full access on account.spread.template.auto,model_account_spread_template_auto,account.group_account_manager,1,1,1,1
|
||||
access_account_spread_cost_revenue_template_auto_read,Read access on account.spread.template.auto,model_account_spread_template_auto,account.group_account_invoice,1,0,0,0
|
||||
access_account_spread_invoice_line_link_wizard,access_account_spread_invoice_line_link_wizard,model_account_spread_invoice_line_link_wizard,base.group_user,1,1,1,1
|
|
BIN
account_spread_cost_revenue/static/description/create_spread.png
Normal file
BIN
account_spread_cost_revenue/static/description/create_spread.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 60 KiB |
BIN
account_spread_cost_revenue/static/description/icon.png
Normal file
BIN
account_spread_cost_revenue/static/description/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.6 KiB |
575
account_spread_cost_revenue/static/description/index.html
Normal file
575
account_spread_cost_revenue/static/description/index.html
Normal file
@ -0,0 +1,575 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
|
||||
<title>Cost-Revenue Spread</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
|
||||
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
|
||||
customize this style sheet.
|
||||
*/
|
||||
|
||||
/* used to remove borders from tables and images */
|
||||
.borderless, table.borderless td, table.borderless th {
|
||||
border: 0 }
|
||||
|
||||
table.borderless td, table.borderless th {
|
||||
/* Override padding for "table.docutils td" with "! important".
|
||||
The right padding separates the table cells. */
|
||||
padding: 0 0.5em 0 0 ! important }
|
||||
|
||||
.first {
|
||||
/* Override more specific margin styles with "! important". */
|
||||
margin-top: 0 ! important }
|
||||
|
||||
.last, .with-subtitle {
|
||||
margin-bottom: 0 ! important }
|
||||
|
||||
.hidden {
|
||||
display: none }
|
||||
|
||||
.subscript {
|
||||
vertical-align: sub;
|
||||
font-size: smaller }
|
||||
|
||||
.superscript {
|
||||
vertical-align: super;
|
||||
font-size: smaller }
|
||||
|
||||
a.toc-backref {
|
||||
text-decoration: none ;
|
||||
color: black }
|
||||
|
||||
blockquote.epigraph {
|
||||
margin: 2em 5em ; }
|
||||
|
||||
dl.docutils dd {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||
dl.docutils dt {
|
||||
font-weight: bold }
|
||||
*/
|
||||
|
||||
div.abstract {
|
||||
margin: 2em 5em }
|
||||
|
||||
div.abstract p.topic-title {
|
||||
font-weight: bold ;
|
||||
text-align: center }
|
||||
|
||||
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||
div.hint, div.important, div.note, div.tip, div.warning {
|
||||
margin: 2em ;
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||
div.important p.admonition-title, div.note p.admonition-title,
|
||||
div.tip p.admonition-title {
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||
div.danger p.admonition-title, div.error p.admonition-title,
|
||||
div.warning p.admonition-title, .code .error {
|
||||
color: red ;
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||
compound paragraphs.
|
||||
div.compound .compound-first, div.compound .compound-middle {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
div.compound .compound-last, div.compound .compound-middle {
|
||||
margin-top: 0.5em }
|
||||
*/
|
||||
|
||||
div.dedication {
|
||||
margin: 2em 5em ;
|
||||
text-align: center ;
|
||||
font-style: italic }
|
||||
|
||||
div.dedication p.topic-title {
|
||||
font-weight: bold ;
|
||||
font-style: normal }
|
||||
|
||||
div.figure {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
div.footer, div.header {
|
||||
clear: both;
|
||||
font-size: smaller }
|
||||
|
||||
div.line-block {
|
||||
display: block ;
|
||||
margin-top: 1em ;
|
||||
margin-bottom: 1em }
|
||||
|
||||
div.line-block div.line-block {
|
||||
margin-top: 0 ;
|
||||
margin-bottom: 0 ;
|
||||
margin-left: 1.5em }
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em ;
|
||||
border: medium outset ;
|
||||
padding: 1em ;
|
||||
background-color: #ffffee ;
|
||||
width: 40% ;
|
||||
float: right ;
|
||||
clear: right }
|
||||
|
||||
div.sidebar p.rubric {
|
||||
font-family: sans-serif ;
|
||||
font-size: medium }
|
||||
|
||||
div.system-messages {
|
||||
margin: 5em }
|
||||
|
||||
div.system-messages h1 {
|
||||
color: red }
|
||||
|
||||
div.system-message {
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.system-message p.system-message-title {
|
||||
color: red ;
|
||||
font-weight: bold }
|
||||
|
||||
div.topic {
|
||||
margin: 2em }
|
||||
|
||||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||
margin-top: 0.4em }
|
||||
|
||||
h1.title {
|
||||
text-align: center }
|
||||
|
||||
h2.subtitle {
|
||||
text-align: center }
|
||||
|
||||
hr.docutils {
|
||||
width: 75% }
|
||||
|
||||
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||
clear: left ;
|
||||
float: left ;
|
||||
margin-right: 1em }
|
||||
|
||||
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||
clear: right ;
|
||||
float: right ;
|
||||
margin-left: 1em }
|
||||
|
||||
img.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left }
|
||||
|
||||
.align-center {
|
||||
clear: both ;
|
||||
text-align: center }
|
||||
|
||||
.align-right {
|
||||
text-align: right }
|
||||
|
||||
/* reset inner alignment in figures */
|
||||
div.align-right {
|
||||
text-align: inherit }
|
||||
|
||||
/* div.align-center * { */
|
||||
/* text-align: left } */
|
||||
|
||||
.align-top {
|
||||
vertical-align: top }
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle }
|
||||
|
||||
.align-bottom {
|
||||
vertical-align: bottom }
|
||||
|
||||
ol.simple, ul.simple {
|
||||
margin-bottom: 1em }
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal }
|
||||
|
||||
ol.loweralpha {
|
||||
list-style: lower-alpha }
|
||||
|
||||
ol.upperalpha {
|
||||
list-style: upper-alpha }
|
||||
|
||||
ol.lowerroman {
|
||||
list-style: lower-roman }
|
||||
|
||||
ol.upperroman {
|
||||
list-style: upper-roman }
|
||||
|
||||
p.attribution {
|
||||
text-align: right ;
|
||||
margin-left: 50% }
|
||||
|
||||
p.caption {
|
||||
font-style: italic }
|
||||
|
||||
p.credits {
|
||||
font-style: italic ;
|
||||
font-size: smaller }
|
||||
|
||||
p.label {
|
||||
white-space: nowrap }
|
||||
|
||||
p.rubric {
|
||||
font-weight: bold ;
|
||||
font-size: larger ;
|
||||
color: maroon ;
|
||||
text-align: center }
|
||||
|
||||
p.sidebar-title {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold ;
|
||||
font-size: larger }
|
||||
|
||||
p.sidebar-subtitle {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
p.topic-title {
|
||||
font-weight: bold }
|
||||
|
||||
pre.address {
|
||||
margin-bottom: 0 ;
|
||||
margin-top: 0 ;
|
||||
font: inherit }
|
||||
|
||||
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
pre.code .ln { color: grey; } /* line numbers */
|
||||
pre.code, code { background-color: #eeeeee }
|
||||
pre.code .comment, code .comment { color: #5C6576 }
|
||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||
|
||||
span.classifier {
|
||||
font-family: sans-serif ;
|
||||
font-style: oblique }
|
||||
|
||||
span.classifier-delimiter {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
span.interpreted {
|
||||
font-family: sans-serif }
|
||||
|
||||
span.option {
|
||||
white-space: nowrap }
|
||||
|
||||
span.pre {
|
||||
white-space: pre }
|
||||
|
||||
span.problematic {
|
||||
color: red }
|
||||
|
||||
span.section-subtitle {
|
||||
/* font-size relative to parent (h1..h6 element) */
|
||||
font-size: 80% }
|
||||
|
||||
table.citation {
|
||||
border-left: solid 1px gray;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docinfo {
|
||||
margin: 2em 4em }
|
||||
|
||||
table.docutils {
|
||||
margin-top: 0.5em ;
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
table.footnote {
|
||||
border-left: solid 1px black;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docutils td, table.docutils th,
|
||||
table.docinfo td, table.docinfo th {
|
||||
padding-left: 0.5em ;
|
||||
padding-right: 0.5em ;
|
||||
vertical-align: top }
|
||||
|
||||
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||
font-weight: bold ;
|
||||
text-align: left ;
|
||||
white-space: nowrap ;
|
||||
padding-left: 0 }
|
||||
|
||||
/* "booktabs" style (no vertical lines) */
|
||||
table.docutils.booktabs {
|
||||
border: 0px;
|
||||
border-top: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.docutils.booktabs * {
|
||||
border: 0px;
|
||||
}
|
||||
table.docutils.booktabs th {
|
||||
border-bottom: thin solid;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||
font-size: 100% }
|
||||
|
||||
ul.auto-toc {
|
||||
list-style-type: none }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document" id="cost-revenue-spread">
|
||||
<h1 class="title">Cost-Revenue Spread</h1>
|
||||
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:560d87ede92fd18ca4a929595b6a9fd9f558f6b577e8c9bad1f8e7236ad4175d
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/account-financial-tools/tree/16.0/account_spread_cost_revenue"><img alt="OCA/account-financial-tools" src="https://img.shields.io/badge/github-OCA%2Faccount--financial--tools-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/account-financial-tools-16-0/account-financial-tools-16-0-account_spread_cost_revenue"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/account-financial-tools&target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
|
||||
<p>Allows to spread costs or revenues over a customizable periods, to even out cost or invoice spikes.</p>
|
||||
<p><strong>Table of contents</strong></p>
|
||||
<div class="contents local topic" id="contents">
|
||||
<ul class="simple">
|
||||
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a></li>
|
||||
<li><a class="reference internal" href="#usage" id="toc-entry-2">Usage</a><ul>
|
||||
<li><a class="reference internal" href="#define-spread-costs-revenues-board" id="toc-entry-3">Define Spread Costs/Revenues Board</a></li>
|
||||
<li><a class="reference internal" href="#link-invoice-to-spread-costs-revenues-board" id="toc-entry-4">Link Invoice to Spread Costs/Revenues Board</a></li>
|
||||
<li><a class="reference internal" href="#define-spread-costs-revenues-template" id="toc-entry-5">Define Spread Costs/Revenues Template</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#changelog" id="toc-entry-6">Changelog</a><ul>
|
||||
<li><a class="reference internal" href="#section-1" id="toc-entry-7">13.0.1.0.0</a></li>
|
||||
<li><a class="reference internal" href="#section-2" id="toc-entry-8">12.0.2.0.0</a></li>
|
||||
<li><a class="reference internal" href="#section-3" id="toc-entry-9">12.0.1.1.0</a></li>
|
||||
<li><a class="reference internal" href="#section-4" id="toc-entry-10">12.0.1.0.0</a></li>
|
||||
<li><a class="reference internal" href="#section-5" id="toc-entry-11">11.0.1.0.0</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-12">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="toc-entry-13">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="toc-entry-14">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="toc-entry-15">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#other-credits" id="toc-entry-16">Other credits</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-17">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="configuration">
|
||||
<h1><a class="toc-backref" href="#toc-entry-1">Configuration</a></h1>
|
||||
<p>To be able to access the full spreading features, the user must belong to <em>Show Full Accounting Features</em> group.</p>
|
||||
<p>On the form view of the company, in the <em>Account Spread</em> tab, you can configure
|
||||
the journals in which the spread journal items will be generated by default:</p>
|
||||
<ul class="simple">
|
||||
<li>the <em>Default Spread Journal for Revenues</em>,</li>
|
||||
<li>the <em>Default Spread Journal for Expenses</em>.</li>
|
||||
</ul>
|
||||
<p>In the same <em>Account Spread</em> tab, you can also configure the Spread Balance Sheet Accounts used by default:</p>
|
||||
<ul class="simple">
|
||||
<li>the <em>Default Spread Account for Revenues</em>,</li>
|
||||
<li>the <em>Default Spread Account for Expenses</em>.</li>
|
||||
</ul>
|
||||
<p>This module by default allows the spreading even before the receipt of the invoice or when the invoice is still draft,
|
||||
so that it is possible to work on the plan of the cost/revenue spreading. To disable this feature, on the form view of
|
||||
the company disable the <em>Allow Spread Planning</em> option.</p>
|
||||
<p>In Spread Template, there is also option to <em>Auto assign template on invoice validate</em>, based on the preset invoice line criteria.</p>
|
||||
<p>On the form view of the company, the <em>Auto-post spread lines</em> option forces the account moves created
|
||||
during the cost/revenue spreading to be automatically posted. When this option is false, the user can
|
||||
enable/disable the automatic posting by the flag <em>Auto-post lines</em> present in the spread board.</p>
|
||||
<p>On the form view of the company, enable the <em>Auto-archive spread</em> option if you want the
|
||||
cron job to automatically archive the spreads when all lines are posted.</p>
|
||||
</div>
|
||||
<div class="section" id="usage">
|
||||
<h1><a class="toc-backref" href="#toc-entry-2">Usage</a></h1>
|
||||
<div class="section" id="define-spread-costs-revenues-board">
|
||||
<h2><a class="toc-backref" href="#toc-entry-3">Define Spread Costs/Revenues Board</a></h2>
|
||||
<p>Under Invoicing -> Accounting -> Journals -> Spread Costs/Revenues, create a new spread board.</p>
|
||||
<p>Complete the definition of the spreading criteria, by setting the the fields:</p>
|
||||
<ul class="simple">
|
||||
<li><em>Debit Account</em></li>
|
||||
<li><em>Credit Account</em></li>
|
||||
<li><em>Estimated Amount</em> (The total amount to spread)</li>
|
||||
<li><em>Number of Repetitions</em></li>
|
||||
<li><em>Period Type</em> (Duration of each period)</li>
|
||||
<li><em>Start date</em></li>
|
||||
<li><em>Journal</em></li>
|
||||
</ul>
|
||||
<div class="figure">
|
||||
<img alt="Create a new spread board" src="https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/spread.png" />
|
||||
</div>
|
||||
<p>Click on the “Recalculate unposted lines” button on the top-left to calculate the spread lines.</p>
|
||||
<div class="figure">
|
||||
<img alt="The spreading board is defined" src="https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/create_spread.png" />
|
||||
</div>
|
||||
<p>A cron job will automatically create the accounting moves for all the lines having date previous that the current day (today).</p>
|
||||
<div class="figure">
|
||||
<img alt="The spreading board is updated by the cron job" src="https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/update_spread.png" />
|
||||
</div>
|
||||
<p>By default, the status of the created accounting moves is posted.
|
||||
To disable the automatic posting of the accounting moves, set the flag <em>Auto-post lines</em> to False.
|
||||
This flag is only available when the <em>Auto-post spread lines</em> option, present on the form view of the company, is disabled.</p>
|
||||
<p>Click on button <em>Recalculate entire spread</em> button in the spread board to force the recalculation of the spread lines:
|
||||
this will also reset all the journal entries previously created.</p>
|
||||
</div>
|
||||
<div class="section" id="link-invoice-to-spread-costs-revenues-board">
|
||||
<h2><a class="toc-backref" href="#toc-entry-4">Link Invoice to Spread Costs/Revenues Board</a></h2>
|
||||
<p>Create an invoice or vendor bill in draft. On its lines, the spreading right-arrow icon are displayed in dark-grey color.</p>
|
||||
<div class="figure">
|
||||
<img alt="On the invoice line the spreading icon is displayed" src="https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/invoice_line_1.png" />
|
||||
</div>
|
||||
<p>Click on the spreading right-arrow icon. A wizard prompts to enter a <em>Spread Action Type</em>:</p>
|
||||
<ul class="simple">
|
||||
<li><em>Link to existing spread board</em></li>
|
||||
<li><em>Create from spread template</em></li>
|
||||
<li><em>Create new spread board</em></li>
|
||||
</ul>
|
||||
<p>Select <em>Link to existing spread board</em> and enter the previously generated Spread Board. Click on Confirm button:
|
||||
the selected Spread Board will be automatically displayed.</p>
|
||||
<p>Go back to the draft invoice/bill. The spreading functionality is now enabled on the invoice line:
|
||||
the spreading right-arrow icon is now displayed in green color.</p>
|
||||
<div class="figure">
|
||||
<img alt="On the invoice line the spreading icon is displayed in green color" src="https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/invoice_line_2.png" />
|
||||
</div>
|
||||
<p>Validate the invoice/bill. Click on the spreading (green) right-arrow icon to open the spread board, then click
|
||||
on the smart button <em>Posted entries</em> to see the moves of the spread lines together with the move of the invoice line.</p>
|
||||
<p>In case the Subtotal Price of the invoice line is different than the <em>Estimated Amount</em> of the spread board, the spread
|
||||
lines (not yet posted) will be recalculated when validating the invoice/bill.</p>
|
||||
</div>
|
||||
<div class="section" id="define-spread-costs-revenues-template">
|
||||
<h2><a class="toc-backref" href="#toc-entry-5">Define Spread Costs/Revenues Template</a></h2>
|
||||
<p>Under Invoicing -> Configuration -> Accounting -> Spread Templates, create a new spread template.</p>
|
||||
<ul class="simple">
|
||||
<li><em>Spread Type</em></li>
|
||||
<li><em>Spread Balance Sheet Account</em></li>
|
||||
<li><em>Expense/Revenue Account</em> This option visible if invoice line account is balance sheet account, user need to specify this too.</li>
|
||||
<li><em>Journal</em></li>
|
||||
<li><em>Auto assign template on invoice validate</em></li>
|
||||
</ul>
|
||||
<p>When creating a new Spread Costs/Revenues Board, select the right template.
|
||||
This way the above fields will be copied to the Spread Board.</p>
|
||||
<p>If <em>Auto assign template on invoice validate</em> is checked, this template will be used to auto create spread, if the underlining invoice match the preset product/account/analytic criteria.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="changelog">
|
||||
<h1><a class="toc-backref" href="#toc-entry-6">Changelog</a></h1>
|
||||
<div class="section" id="section-1">
|
||||
<h2><a class="toc-backref" href="#toc-entry-7">13.0.1.0.0</a></h2>
|
||||
<ul class="simple">
|
||||
<li>[MIG] Port account_spread_cost_revenue to V13.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-2">
|
||||
<h2><a class="toc-backref" href="#toc-entry-8">12.0.2.0.0</a></h2>
|
||||
<ul class="simple">
|
||||
<li>[ENH] In spread template, add option to auto create spread on invoice validation</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-3">
|
||||
<h2><a class="toc-backref" href="#toc-entry-9">12.0.1.1.0</a></h2>
|
||||
<ul class="simple">
|
||||
<li>[ENH] Add optional Expense/Revenue Account in Chart Template, which can be used
|
||||
in place of account from invoice line to set Expense/Revenue account in the spread</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-4">
|
||||
<h2><a class="toc-backref" href="#toc-entry-10">12.0.1.0.0</a></h2>
|
||||
<ul class="simple">
|
||||
<li>[MIG] Port account_spread_cost_revenue to V12.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-5">
|
||||
<h2><a class="toc-backref" href="#toc-entry-11">11.0.1.0.0</a></h2>
|
||||
<ul class="simple">
|
||||
<li>[ADD] Module account_spread_cost_revenue.
|
||||
(<a class="reference external" href="https://github.com/OCA/account-financial-tools/pull/715">#715</a>)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h1><a class="toc-backref" href="#toc-entry-12">Bug Tracker</a></h1>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/account-financial-tools/issues">GitHub Issues</a>.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
<a class="reference external" href="https://github.com/OCA/account-financial-tools/issues/new?body=module:%20account_spread_cost_revenue%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
||||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||
</div>
|
||||
<div class="section" id="credits">
|
||||
<h1><a class="toc-backref" href="#toc-entry-13">Credits</a></h1>
|
||||
<div class="section" id="authors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-14">Authors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Onestein</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="contributors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-15">Contributors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Andrea Stirpe <<a class="reference external" href="mailto:a.stirpe@onestein.nl">a.stirpe@onestein.nl</a>></li>
|
||||
<li>Kitti U. <<a class="reference external" href="mailto:kittiu@ecosoft.co.th">kittiu@ecosoft.co.th</a>></li>
|
||||
<li>Saran Lim. <<a class="reference external" href="mailto:saranl@ecosoft.co.th">saranl@ecosoft.co.th</a>></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="other-credits">
|
||||
<h2><a class="toc-backref" href="#toc-entry-16">Other credits</a></h2>
|
||||
<p>Part of the code in this module (in particular the computation of the spread lines)
|
||||
is highly inspired by the Assets Management module from the standard
|
||||
Odoo 11.0 Community developed by Odoo SA.</p>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h2><a class="toc-backref" href="#toc-entry-17">Maintainers</a></h2>
|
||||
<p>This module is maintained by the OCA.</p>
|
||||
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
|
||||
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.</p>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/account-financial-tools/tree/16.0/account_spread_cost_revenue">OCA/account-financial-tools</a> project on GitHub.</p>
|
||||
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
account_spread_cost_revenue/static/description/spread.png
Normal file
BIN
account_spread_cost_revenue/static/description/spread.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 50 KiB |
BIN
account_spread_cost_revenue/static/description/update_spread.png
Normal file
BIN
account_spread_cost_revenue/static/description/update_spread.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 65 KiB |
6
account_spread_cost_revenue/tests/__init__.py
Normal file
6
account_spread_cost_revenue/tests/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import test_account_spread_cost_revenue
|
||||
from . import test_compute_spread_board
|
||||
from . import test_account_invoice_spread
|
||||
from . import test_account_invoice_auto_spread
|
@ -0,0 +1,103 @@
|
||||
# Copyright 2018-2019 Onestein (<https://www.onestein.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
from .test_account_invoice_spread import TestAccountInvoiceSpread
|
||||
|
||||
|
||||
class TestAccountInvoiceAutoSpread(TestAccountInvoiceSpread):
|
||||
def test_01_no_auto_spread_sheet(self):
|
||||
|
||||
self.env["account.spread.template"].create(
|
||||
{
|
||||
"name": "test",
|
||||
"spread_type": "purchase",
|
||||
"period_number": 5,
|
||||
"period_type": "month",
|
||||
"spread_account_id": self.account_payable.id,
|
||||
"spread_journal_id": self.expenses_journal.id,
|
||||
"auto_spread": False, # Auto Spread = False
|
||||
"auto_spread_ids": [
|
||||
(0, 0, {"account_id": self.vendor_bill_line.account_id.id})
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
self.assertFalse(self.vendor_bill_line.spread_id)
|
||||
self.vendor_bill.action_post()
|
||||
self.assertFalse(self.vendor_bill_line.spread_id)
|
||||
|
||||
def test_02_new_auto_spread_sheet_purchase(self):
|
||||
|
||||
self.env["account.spread.template"].create(
|
||||
{
|
||||
"name": "test 1",
|
||||
"spread_type": "purchase",
|
||||
"period_number": 5,
|
||||
"period_type": "month",
|
||||
"spread_account_id": self.account_payable.id,
|
||||
"spread_journal_id": self.expenses_journal.id,
|
||||
"auto_spread": True, # Auto Spread
|
||||
"auto_spread_ids": [
|
||||
(0, 0, {"account_id": self.vendor_bill_line.account_id.id})
|
||||
],
|
||||
}
|
||||
)
|
||||
template2 = self.env["account.spread.template"].create(
|
||||
{
|
||||
"name": "test 2",
|
||||
"spread_type": "purchase",
|
||||
"period_number": 5,
|
||||
"period_type": "month",
|
||||
"spread_account_id": self.account_payable.id,
|
||||
"spread_journal_id": self.expenses_journal.id,
|
||||
"auto_spread": True, # Auto Spread
|
||||
"auto_spread_ids": [
|
||||
(0, 0, {"account_id": self.vendor_bill_line.account_id.id})
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
self.assertFalse(self.vendor_bill_line.spread_id)
|
||||
with self.assertRaises(UserError): # too many auto_spread_ids matched
|
||||
self.vendor_bill.action_post()
|
||||
|
||||
template2.auto_spread = False # Do not use this template
|
||||
self.vendor_bill.action_post()
|
||||
self.assertTrue(self.vendor_bill_line.spread_id)
|
||||
|
||||
spread_lines = self.vendor_bill_line.spread_id.line_ids
|
||||
self.assertTrue(spread_lines)
|
||||
|
||||
for line in spread_lines:
|
||||
line.create_move()
|
||||
self.assertTrue(line.move_id)
|
||||
|
||||
def test_03_new_auto_spread_sheet_sale(self):
|
||||
|
||||
self.env["account.spread.template"].create(
|
||||
{
|
||||
"name": "test",
|
||||
"spread_type": "sale",
|
||||
"period_number": 5,
|
||||
"period_type": "month",
|
||||
"spread_account_id": self.account_receivable.id,
|
||||
"spread_journal_id": self.sales_journal.id,
|
||||
"auto_spread": True, # Auto Spread
|
||||
"auto_spread_ids": [
|
||||
(0, 0, {"account_id": self.invoice_line.account_id.id})
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
self.assertFalse(self.invoice_line.spread_id)
|
||||
self.sale_invoice.action_post()
|
||||
self.assertTrue(self.invoice_line.spread_id)
|
||||
|
||||
spread_lines = self.invoice_line.spread_id.line_ids
|
||||
self.assertTrue(spread_lines)
|
||||
|
||||
for line in spread_lines:
|
||||
line.create_move()
|
||||
self.assertTrue(line.move_id)
|
809
account_spread_cost_revenue/tests/test_account_invoice_spread.py
Normal file
809
account_spread_cost_revenue/tests/test_account_invoice_spread.py
Normal file
@ -0,0 +1,809 @@
|
||||
# Copyright 2018-2020 Onestein (<https://www.onestein.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
import datetime
|
||||
|
||||
from odoo import fields
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tests import Form, common
|
||||
|
||||
|
||||
class TestAccountInvoiceSpread(common.TransactionCase):
|
||||
def create_account_invoice(self, invoice_type, quantity=1.0, price_unit=1000.0):
|
||||
"""Create an invoice as in a view by triggering its onchange methods"""
|
||||
|
||||
invoice_form = Form(
|
||||
self.env["account.move"].with_context(default_move_type=invoice_type)
|
||||
)
|
||||
invoice_form.partner_id = self.env["res.partner"].create(
|
||||
{"name": "Partner Name"}
|
||||
)
|
||||
with invoice_form.invoice_line_ids.new() as line:
|
||||
line.name = "product that costs " + str(price_unit)
|
||||
line.quantity = quantity
|
||||
line.price_unit = price_unit
|
||||
|
||||
return invoice_form.save()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
# Define minimal accounting data to run
|
||||
a_expense = self.env["account.account"].create(
|
||||
{
|
||||
"code": "X2120",
|
||||
"name": "Expenses - (test)",
|
||||
"account_type": "expense",
|
||||
}
|
||||
)
|
||||
a_sale = self.env["account.account"].create(
|
||||
{
|
||||
"code": "X2020",
|
||||
"name": "Product Sales - (test)",
|
||||
"account_type": "expense_direct_cost",
|
||||
}
|
||||
)
|
||||
|
||||
self.expenses_journal = self.env["account.journal"].create(
|
||||
{
|
||||
"name": "Vendor Bills - Test",
|
||||
"code": "TEXJ",
|
||||
"type": "purchase",
|
||||
"default_account_id": a_expense.id,
|
||||
"refund_sequence": True,
|
||||
}
|
||||
)
|
||||
self.sales_journal = self.env["account.journal"].create(
|
||||
{
|
||||
"name": "Customer Invoices - Test",
|
||||
"code": "TINV",
|
||||
"type": "sale",
|
||||
"default_account_id": a_sale.id,
|
||||
"refund_sequence": True,
|
||||
}
|
||||
)
|
||||
|
||||
self.account_payable = self.env["account.account"].create(
|
||||
{
|
||||
"name": "Test account payable",
|
||||
"code": "321spread",
|
||||
"account_type": "income_other",
|
||||
"reconcile": True,
|
||||
}
|
||||
)
|
||||
|
||||
self.account_receivable = self.env["account.account"].create(
|
||||
{
|
||||
"name": "Test account receivable",
|
||||
"code": "322spread",
|
||||
"account_type": "income_other",
|
||||
"reconcile": True,
|
||||
}
|
||||
)
|
||||
|
||||
spread_account_payable = self.env["account.account"].create(
|
||||
{
|
||||
"name": "test spread account_payable",
|
||||
"code": "765spread",
|
||||
"account_type": "income_other",
|
||||
"reconcile": True,
|
||||
}
|
||||
)
|
||||
|
||||
spread_account_receivable = self.env["account.account"].create(
|
||||
{
|
||||
"name": "test spread account_receivable",
|
||||
"code": "766spread",
|
||||
"account_type": "income_other",
|
||||
"reconcile": True,
|
||||
}
|
||||
)
|
||||
|
||||
# Invoices
|
||||
|
||||
self.vendor_bill = self.create_account_invoice("in_invoice")
|
||||
self.vendor_bill.invoice_date = fields.Date.today()
|
||||
self.sale_invoice = self.create_account_invoice("out_invoice")
|
||||
self.sale_invoice.invoice_date = fields.Date.today()
|
||||
self.vendor_bill_line = self.vendor_bill.invoice_line_ids[0]
|
||||
self.invoice_line = self.sale_invoice.invoice_line_ids[0]
|
||||
|
||||
# Set accounts to reconcilable
|
||||
self.vendor_bill_line.account_id.reconcile = True
|
||||
self.invoice_line.account_id.reconcile = True
|
||||
|
||||
analytic_plan = self.env["account.analytic.plan"].create(
|
||||
{"name": "Plan Test", "company_id": False}
|
||||
)
|
||||
self.analytic_account = self.env["account.analytic.account"].create(
|
||||
{"name": "test account", "plan_id": analytic_plan.id}
|
||||
)
|
||||
self.distribution = self.env["account.analytic.distribution.model"].create(
|
||||
{
|
||||
"partner_id": self.vendor_bill.partner_id.id,
|
||||
"analytic_distribution": {self.analytic_account.id: 100},
|
||||
}
|
||||
)
|
||||
self.spread = (
|
||||
self.env["account.spread"]
|
||||
.with_context(mail_create_nosubscribe=True)
|
||||
.create(
|
||||
[
|
||||
{
|
||||
"name": "test",
|
||||
"debit_account_id": spread_account_payable.id,
|
||||
"credit_account_id": self.account_payable.id,
|
||||
"period_number": 12,
|
||||
"period_type": "month",
|
||||
"spread_date": datetime.date(2017, 2, 1),
|
||||
"estimated_amount": 1000.0,
|
||||
"journal_id": self.vendor_bill.journal_id.id,
|
||||
"invoice_type": "in_invoice",
|
||||
"analytic_distribution": self.distribution._get_distribution(
|
||||
{
|
||||
"partner_id": self.vendor_bill.partner_id.id,
|
||||
}
|
||||
),
|
||||
}
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
self.spread2 = self.env["account.spread"].create(
|
||||
[
|
||||
{
|
||||
"name": "test2",
|
||||
"debit_account_id": spread_account_receivable.id,
|
||||
"credit_account_id": self.account_receivable.id,
|
||||
"period_number": 12,
|
||||
"period_type": "month",
|
||||
"spread_date": datetime.date(2017, 2, 1),
|
||||
"estimated_amount": 1000.0,
|
||||
"journal_id": self.sale_invoice.journal_id.id,
|
||||
"invoice_type": "out_invoice",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
def test_01_wizard_defaults(self):
|
||||
Wizard = self.env["account.spread.invoice.line.link.wizard"]
|
||||
|
||||
wizard1 = Wizard.with_context(
|
||||
default_invoice_line_id=self.vendor_bill_line.id,
|
||||
default_company_id=self.env.company.id,
|
||||
allow_spread_planning=True,
|
||||
).create({})
|
||||
|
||||
self.assertEqual(wizard1.invoice_line_id, self.vendor_bill_line)
|
||||
self.assertEqual(wizard1.invoice_line_id.move_id, self.vendor_bill)
|
||||
self.assertEqual(wizard1.invoice_type, "in_invoice")
|
||||
self.assertFalse(wizard1.spread_id)
|
||||
self.assertEqual(wizard1.company_id, self.env.company)
|
||||
self.assertEqual(wizard1.spread_action_type, "link")
|
||||
self.assertFalse(wizard1.spread_account_id)
|
||||
self.assertFalse(wizard1.spread_journal_id)
|
||||
|
||||
wizard2 = Wizard.with_context(
|
||||
default_invoice_line_id=self.invoice_line.id,
|
||||
default_company_id=self.env.company.id,
|
||||
).create({})
|
||||
|
||||
self.assertEqual(wizard2.invoice_line_id, self.invoice_line)
|
||||
self.assertEqual(wizard2.invoice_line_id.move_id, self.sale_invoice)
|
||||
self.assertEqual(wizard2.invoice_type, "out_invoice")
|
||||
self.assertFalse(wizard2.spread_id)
|
||||
self.assertEqual(wizard2.company_id, self.env.company)
|
||||
self.assertEqual(wizard2.spread_action_type, "template")
|
||||
self.assertFalse(wizard2.spread_account_id)
|
||||
self.assertFalse(wizard2.spread_journal_id)
|
||||
|
||||
def test_02_wizard_defaults(self):
|
||||
Wizard = self.env["account.spread.invoice.line.link.wizard"]
|
||||
|
||||
self.env.company.default_spread_revenue_account_id = self.account_receivable
|
||||
self.env.company.default_spread_expense_account_id = self.account_payable
|
||||
self.env.company.default_spread_revenue_journal_id = self.sales_journal
|
||||
self.env.company.default_spread_expense_journal_id = self.expenses_journal
|
||||
|
||||
self.assertTrue(self.env.company.default_spread_revenue_account_id)
|
||||
self.assertTrue(self.env.company.default_spread_expense_account_id)
|
||||
self.assertTrue(self.env.company.default_spread_revenue_journal_id)
|
||||
self.assertTrue(self.env.company.default_spread_expense_journal_id)
|
||||
|
||||
wizard1 = Wizard.with_context(
|
||||
default_invoice_line_id=self.vendor_bill_line.id,
|
||||
default_company_id=self.env.company.id,
|
||||
allow_spread_planning=True,
|
||||
).create({})
|
||||
|
||||
self.assertEqual(wizard1.invoice_line_id, self.vendor_bill_line)
|
||||
self.assertEqual(wizard1.invoice_line_id.move_id, self.vendor_bill)
|
||||
self.assertEqual(wizard1.invoice_type, "in_invoice")
|
||||
self.assertFalse(wizard1.spread_id)
|
||||
self.assertEqual(wizard1.company_id, self.env.company)
|
||||
self.assertEqual(wizard1.spread_action_type, "link")
|
||||
self.assertTrue(wizard1.spread_account_id)
|
||||
self.assertTrue(wizard1.spread_journal_id)
|
||||
self.assertEqual(wizard1.spread_account_id, self.account_payable)
|
||||
self.assertEqual(wizard1.spread_journal_id.id, self.expenses_journal.id)
|
||||
self.assertTrue(wizard1.spread_invoice_type_domain_ids)
|
||||
|
||||
wizard2 = Wizard.with_context(
|
||||
default_invoice_line_id=self.invoice_line.id,
|
||||
default_company_id=self.env.company.id,
|
||||
).create({})
|
||||
|
||||
self.assertEqual(wizard2.invoice_line_id, self.invoice_line)
|
||||
self.assertEqual(wizard2.invoice_line_id.move_id, self.sale_invoice)
|
||||
self.assertEqual(wizard2.invoice_type, "out_invoice")
|
||||
self.assertFalse(wizard2.spread_id)
|
||||
self.assertEqual(wizard2.company_id, self.env.company)
|
||||
self.assertEqual(wizard2.spread_action_type, "template")
|
||||
self.assertTrue(wizard2.spread_account_id)
|
||||
self.assertTrue(wizard2.spread_journal_id)
|
||||
self.assertEqual(wizard2.spread_account_id, self.account_receivable)
|
||||
self.assertEqual(wizard2.spread_journal_id.id, self.sales_journal.id)
|
||||
self.assertTrue(wizard2.spread_invoice_type_domain_ids)
|
||||
|
||||
def test_03_link_invoice_line_with_spread_sheet(self):
|
||||
|
||||
self.env.user.write(
|
||||
{
|
||||
"groups_id": [
|
||||
(4, self.env.ref("analytic.group_analytic_accounting").id),
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
Wizard = self.env["account.spread.invoice.line.link.wizard"]
|
||||
wizard1 = Wizard.with_context(
|
||||
default_invoice_line_id=self.vendor_bill_line.id,
|
||||
default_company_id=self.env.company.id,
|
||||
allow_spread_planning=True,
|
||||
).create({})
|
||||
self.assertEqual(wizard1.spread_action_type, "link")
|
||||
|
||||
wizard1.spread_account_id = self.account_receivable
|
||||
wizard1.spread_journal_id = self.expenses_journal
|
||||
wizard1.spread_id = self.spread
|
||||
res_action = wizard1.confirm()
|
||||
self.assertTrue(isinstance(res_action, dict))
|
||||
self.assertTrue(res_action.get("res_id"))
|
||||
self.assertEqual(res_action.get("res_id"), self.spread.id)
|
||||
self.assertTrue(self.spread.invoice_line_id)
|
||||
self.assertEqual(self.spread.invoice_line_id, self.vendor_bill_line)
|
||||
|
||||
self.assertFalse(any(line.move_id for line in self.spread.line_ids))
|
||||
|
||||
self.spread.compute_spread_board()
|
||||
spread_lines = self.spread.line_ids
|
||||
for line in spread_lines:
|
||||
line.create_move()
|
||||
self.assertTrue(line.move_id)
|
||||
for ml in line.move_id.line_ids:
|
||||
self.assertEqual(
|
||||
ml.analytic_distribution, self.spread.analytic_distribution
|
||||
)
|
||||
|
||||
self.spread.invoice_id.button_cancel()
|
||||
|
||||
self.assertTrue(self.spread.invoice_line_id)
|
||||
with self.assertRaises(UserError):
|
||||
self.spread.unlink()
|
||||
with self.assertRaises(UserError):
|
||||
self.spread.action_unlink_invoice_line()
|
||||
self.assertTrue(self.spread.invoice_line_id)
|
||||
|
||||
def test_04_new_spread_sheet(self):
|
||||
|
||||
Wizard = self.env["account.spread.invoice.line.link.wizard"]
|
||||
|
||||
spread_journal_id = self.expenses_journal
|
||||
|
||||
wizard1 = Wizard.with_context(
|
||||
default_invoice_line_id=self.vendor_bill_line.id,
|
||||
default_company_id=self.env.company.id,
|
||||
).create({"spread_action_type": "new"})
|
||||
self.assertEqual(wizard1.spread_action_type, "new")
|
||||
|
||||
wizard1.write(
|
||||
{
|
||||
"spread_account_id": self.account_receivable.id,
|
||||
"spread_journal_id": spread_journal_id,
|
||||
}
|
||||
)
|
||||
|
||||
res_action = wizard1.confirm()
|
||||
self.assertTrue(isinstance(res_action, dict))
|
||||
self.assertFalse(res_action.get("res_id"))
|
||||
self.assertTrue(res_action.get("context"))
|
||||
res_context = res_action.get("context")
|
||||
self.assertTrue(res_context.get("default_name"))
|
||||
self.assertTrue(res_context.get("default_invoice_type"))
|
||||
self.assertTrue(res_context.get("default_invoice_line_id"))
|
||||
self.assertTrue(res_context.get("default_debit_account_id"))
|
||||
self.assertTrue(res_context.get("default_credit_account_id"))
|
||||
|
||||
self.assertFalse(any(line.move_id for line in self.spread.line_ids))
|
||||
|
||||
self.spread.compute_spread_board()
|
||||
spread_lines = self.spread.line_ids
|
||||
for line in spread_lines:
|
||||
line.create_move()
|
||||
self.assertTrue(line.move_id)
|
||||
|
||||
wizard2 = Wizard.with_context(
|
||||
default_invoice_line_id=self.invoice_line.id,
|
||||
default_company_id=self.env.company.id,
|
||||
).create({"spread_action_type": "new"})
|
||||
self.assertEqual(wizard2.spread_action_type, "new")
|
||||
|
||||
wizard2.write(
|
||||
{
|
||||
"spread_account_id": self.account_receivable.id,
|
||||
"spread_journal_id": spread_journal_id,
|
||||
}
|
||||
)
|
||||
|
||||
res_action = wizard2.confirm()
|
||||
self.assertTrue(isinstance(res_action, dict))
|
||||
self.assertFalse(res_action.get("res_id"))
|
||||
self.assertTrue(res_action.get("context"))
|
||||
res_context = res_action.get("context")
|
||||
self.assertTrue(res_context.get("default_name"))
|
||||
self.assertTrue(res_context.get("default_invoice_type"))
|
||||
self.assertTrue(res_context.get("default_invoice_line_id"))
|
||||
self.assertTrue(res_context.get("default_debit_account_id"))
|
||||
self.assertTrue(res_context.get("default_credit_account_id"))
|
||||
|
||||
self.assertFalse(any(line.move_id for line in self.spread2.line_ids))
|
||||
|
||||
self.spread2.compute_spread_board()
|
||||
for line in self.spread2.line_ids:
|
||||
line.create_move()
|
||||
self.assertTrue(line.move_id)
|
||||
|
||||
def test_05_new_spread_sheet_from_template(self):
|
||||
|
||||
Wizard = self.env["account.spread.invoice.line.link.wizard"]
|
||||
|
||||
spread_account = self.account_payable
|
||||
self.assertTrue(spread_account)
|
||||
spread_journal_id = self.expenses_journal.id
|
||||
|
||||
template = self.env["account.spread.template"].create(
|
||||
{
|
||||
"name": "test",
|
||||
"spread_type": "purchase",
|
||||
"period_number": 5,
|
||||
"period_type": "month",
|
||||
"spread_account_id": spread_account.id,
|
||||
"spread_journal_id": spread_journal_id,
|
||||
}
|
||||
)
|
||||
|
||||
wizard1 = Wizard.with_context(
|
||||
default_invoice_line_id=self.vendor_bill_line.id,
|
||||
default_company_id=self.env.company.id,
|
||||
).create({"spread_action_type": "template", "template_id": template.id})
|
||||
self.assertEqual(wizard1.spread_action_type, "template")
|
||||
|
||||
res_action = wizard1.confirm()
|
||||
self.assertTrue(isinstance(res_action, dict))
|
||||
self.assertTrue(res_action.get("res_id"))
|
||||
|
||||
new_spread = self.env["account.spread"].browse(res_action["res_id"])
|
||||
new_spread.compute_spread_board()
|
||||
|
||||
self.assertFalse(any(line.move_id for line in new_spread.line_ids))
|
||||
|
||||
for line in new_spread.line_ids:
|
||||
line.create_move()
|
||||
self.assertTrue(line.move_id)
|
||||
|
||||
wizard2 = Wizard.with_context(
|
||||
default_invoice_line_id=self.invoice_line.id,
|
||||
default_company_id=self.env.company.id,
|
||||
).create({"spread_action_type": "new"})
|
||||
self.assertEqual(wizard2.spread_action_type, "new")
|
||||
|
||||
wizard2.write(
|
||||
{
|
||||
"spread_account_id": spread_account.id,
|
||||
"spread_journal_id": spread_journal_id,
|
||||
}
|
||||
)
|
||||
|
||||
res_action = wizard2.confirm()
|
||||
self.assertTrue(isinstance(res_action, dict))
|
||||
self.assertFalse(res_action.get("res_id"))
|
||||
self.assertTrue(res_action.get("context"))
|
||||
res_context = res_action.get("context")
|
||||
self.assertTrue(res_context.get("default_name"))
|
||||
self.assertTrue(res_context.get("default_invoice_type"))
|
||||
self.assertTrue(res_context.get("default_invoice_line_id"))
|
||||
self.assertTrue(res_context.get("default_debit_account_id"))
|
||||
self.assertTrue(res_context.get("default_credit_account_id"))
|
||||
|
||||
self.assertFalse(any(line.move_id for line in self.spread2.line_ids))
|
||||
|
||||
self.spread2.compute_spread_board()
|
||||
for line in self.spread2.line_ids:
|
||||
line.create_move()
|
||||
self.assertTrue(line.move_id)
|
||||
|
||||
def test_06_open_wizard(self):
|
||||
|
||||
res_action = self.vendor_bill_line.spread_details()
|
||||
self.assertTrue(isinstance(res_action, dict))
|
||||
self.assertFalse(res_action.get("res_id"))
|
||||
self.assertTrue(res_action.get("context"))
|
||||
|
||||
def test_07_unlink_invoice_line_and_spread_sheet(self):
|
||||
|
||||
self.assertFalse(self.spread.invoice_line_id)
|
||||
|
||||
self.vendor_bill_line.spread_id = self.spread
|
||||
self.assertTrue(self.spread.invoice_line_id)
|
||||
self.spread.action_unlink_invoice_line()
|
||||
self.assertFalse(self.spread.invoice_line_id)
|
||||
|
||||
self.assertFalse(self.spread2.invoice_line_id)
|
||||
self.invoice_line.spread_id = self.spread2
|
||||
self.assertTrue(self.spread2.invoice_line_id)
|
||||
self.spread2.action_unlink_invoice_line()
|
||||
self.assertFalse(self.spread2.invoice_line_id)
|
||||
|
||||
def test_08_invoice_multi_line(self):
|
||||
invoice_form = Form(self.vendor_bill)
|
||||
with invoice_form.invoice_line_ids.new() as line:
|
||||
line.name = "new test line"
|
||||
line.quantity = 1.0
|
||||
line.price_unit = 1000.0
|
||||
self.invoice = invoice_form.save()
|
||||
self.assertEqual(len(self.vendor_bill.invoice_line_ids), 2)
|
||||
|
||||
self.vendor_bill_line.spread_id = self.spread
|
||||
self.assertTrue(self.spread.invoice_id.invoice_line_ids[0])
|
||||
self.assertEqual(
|
||||
self.spread.invoice_id.invoice_line_ids[0], self.vendor_bill_line
|
||||
)
|
||||
self.assertTrue(self.vendor_bill_line.spread_id)
|
||||
self.assertEqual(self.vendor_bill_line.spread_check, "linked")
|
||||
self.assertFalse(self.vendor_bill.invoice_line_ids[1].spread_id)
|
||||
self.assertEqual(self.vendor_bill.invoice_line_ids[1].spread_check, "unlinked")
|
||||
|
||||
self.assertFalse(any(line.move_id for line in self.spread.line_ids))
|
||||
|
||||
self.spread.compute_spread_board()
|
||||
spread_lines = self.spread.line_ids
|
||||
for line in spread_lines:
|
||||
line.create_move()
|
||||
self.assertTrue(line.move_id)
|
||||
|
||||
# Validate invoice
|
||||
self.vendor_bill.action_post()
|
||||
|
||||
self.assertTrue(self.vendor_bill_line.spread_id)
|
||||
self.assertEqual(self.vendor_bill_line.spread_check, "linked")
|
||||
self.assertFalse(self.vendor_bill.invoice_line_ids[1].spread_id)
|
||||
self.assertEqual(
|
||||
self.vendor_bill.invoice_line_ids[1].spread_check, "unavailable"
|
||||
)
|
||||
|
||||
def test_09_no_link_invoice(self):
|
||||
|
||||
balance_sheet = self.spread.credit_account_id
|
||||
|
||||
# Validate invoice
|
||||
self.vendor_bill.action_post()
|
||||
|
||||
invoice_mls = self.vendor_bill.invoice_line_ids
|
||||
self.assertTrue(invoice_mls)
|
||||
for invoice_ml in invoice_mls:
|
||||
self.assertNotEqual(invoice_ml.account_id, balance_sheet)
|
||||
|
||||
def test_10_link_vendor_bill_line_with_spread_sheet(self):
|
||||
invoice_form = Form(self.vendor_bill)
|
||||
with invoice_form.invoice_line_ids.new() as line:
|
||||
line.name = "new test line"
|
||||
line.quantity = 1.0
|
||||
line.price_unit = 1000.0
|
||||
self.invoice = invoice_form.save()
|
||||
|
||||
self.spread.write(
|
||||
{
|
||||
"estimated_amount": 1000.0,
|
||||
"period_number": 12,
|
||||
"period_type": "month",
|
||||
"spread_date": datetime.date(2017, 1, 7),
|
||||
"invoice_line_id": self.vendor_bill_line.id,
|
||||
"move_line_auto_post": False,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertFalse(any(line.move_id for line in self.spread.line_ids))
|
||||
|
||||
self.spread.compute_spread_board()
|
||||
spread_lines = self.spread.line_ids
|
||||
for line in spread_lines:
|
||||
line.create_move()
|
||||
self.assertTrue(line.move_id)
|
||||
|
||||
expense_account = self.spread.debit_account_id
|
||||
balance_sheet = self.spread.credit_account_id
|
||||
self.assertTrue(balance_sheet.reconcile)
|
||||
|
||||
spread_mls = self.spread.line_ids.mapped("move_id.line_ids")
|
||||
self.assertTrue(spread_mls)
|
||||
for spread_ml in spread_mls:
|
||||
if spread_ml.debit:
|
||||
self.assertEqual(spread_ml.account_id, expense_account)
|
||||
if spread_ml.credit:
|
||||
self.assertEqual(spread_ml.account_id, balance_sheet)
|
||||
|
||||
# Validate invoice
|
||||
self.vendor_bill.action_post()
|
||||
|
||||
count_balance_sheet = len(
|
||||
self.vendor_bill.line_ids.filtered(lambda x: x.account_id == balance_sheet)
|
||||
)
|
||||
self.assertEqual(count_balance_sheet, 1)
|
||||
|
||||
self.spread.line_ids.create_and_reconcile_moves()
|
||||
|
||||
spread_mls = self.spread.line_ids.mapped("move_id.line_ids")
|
||||
self.assertTrue(spread_mls)
|
||||
for spread_ml in spread_mls:
|
||||
if spread_ml.debit:
|
||||
self.assertFalse(spread_ml.full_reconcile_id)
|
||||
if spread_ml.credit:
|
||||
self.assertFalse(spread_ml.full_reconcile_id)
|
||||
|
||||
action_posted_view = self.spread2.open_posted_view()
|
||||
self.assertTrue(isinstance(action_posted_view, dict))
|
||||
self.assertFalse(action_posted_view.get("domain")[0][2])
|
||||
self.assertTrue(action_posted_view.get("context"))
|
||||
|
||||
def test_11_link_vendor_bill_line_with_spread_sheet(self):
|
||||
invoice_form = Form(self.vendor_bill)
|
||||
with invoice_form.invoice_line_ids.new() as line:
|
||||
line.name = "new test line"
|
||||
line.quantity = 1.0
|
||||
line.price_unit = 1000.0
|
||||
self.invoice = invoice_form.save()
|
||||
self.spread.write(
|
||||
{
|
||||
"estimated_amount": 1000.0,
|
||||
"period_number": 12,
|
||||
"period_type": "month",
|
||||
"spread_date": datetime.date(2017, 1, 7),
|
||||
"invoice_line_id": self.vendor_bill_line.id,
|
||||
"move_line_auto_post": False,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertFalse(any(line.move_id for line in self.spread.line_ids))
|
||||
|
||||
self.spread.compute_spread_board()
|
||||
spread_lines = self.spread.line_ids
|
||||
for line in spread_lines:
|
||||
line.create_move()
|
||||
self.assertTrue(line.move_id)
|
||||
|
||||
expense_account = self.spread.debit_account_id
|
||||
balance_sheet = self.spread.credit_account_id
|
||||
self.assertTrue(balance_sheet.reconcile)
|
||||
|
||||
spread_mls = self.spread.line_ids.mapped("move_id.line_ids")
|
||||
self.assertTrue(spread_mls)
|
||||
for spread_ml in spread_mls:
|
||||
if spread_ml.debit:
|
||||
self.assertEqual(spread_ml.account_id, expense_account)
|
||||
if spread_ml.credit:
|
||||
self.assertEqual(spread_ml.account_id, balance_sheet)
|
||||
|
||||
# Validate invoice
|
||||
self.vendor_bill.action_post()
|
||||
|
||||
invoice_mls = self.vendor_bill.line_ids
|
||||
self.assertTrue(invoice_mls)
|
||||
|
||||
count_balance_sheet = len(
|
||||
invoice_mls.filtered(lambda x: x.account_id == balance_sheet)
|
||||
)
|
||||
self.assertEqual(count_balance_sheet, 1)
|
||||
|
||||
self.spread.company_id.force_move_auto_post = True
|
||||
self.spread.line_ids.create_and_reconcile_moves()
|
||||
|
||||
spread_mls = self.spread.line_ids.mapped("move_id.line_ids")
|
||||
self.assertTrue(spread_mls)
|
||||
for spread_ml in spread_mls:
|
||||
if spread_ml.credit:
|
||||
self.assertEqual(spread_ml.account_id, balance_sheet)
|
||||
self.assertTrue(spread_ml.full_reconcile_id)
|
||||
if spread_ml.debit:
|
||||
self.assertEqual(spread_ml.account_id, expense_account)
|
||||
self.assertFalse(spread_ml.full_reconcile_id)
|
||||
|
||||
action_posted_view = self.spread.open_posted_view()
|
||||
self.assertTrue(isinstance(action_posted_view, dict))
|
||||
self.assertTrue(action_posted_view.get("domain")[0][2])
|
||||
self.assertTrue(action_posted_view.get("context"))
|
||||
|
||||
action_spread_details = self.vendor_bill_line.spread_details()
|
||||
self.assertTrue(isinstance(action_spread_details, dict))
|
||||
self.assertTrue(action_spread_details.get("res_id"))
|
||||
|
||||
def test_12_link_invoice_line_with_spread_sheet_full_reconcile(self):
|
||||
|
||||
# Validate invoice
|
||||
self.sale_invoice.action_post()
|
||||
|
||||
self.spread2.write(
|
||||
{
|
||||
"estimated_amount": 1000.0,
|
||||
"period_number": 12,
|
||||
"period_type": "month",
|
||||
"spread_date": datetime.date(2017, 1, 7),
|
||||
"invoice_line_id": self.invoice_line.id,
|
||||
"move_line_auto_post": False,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertFalse(any(line.move_id for line in self.spread2.line_ids))
|
||||
|
||||
self.spread2.compute_spread_board()
|
||||
spread_lines = self.spread2.line_ids
|
||||
for line in spread_lines:
|
||||
line.create_move()
|
||||
self.assertTrue(line.move_id)
|
||||
|
||||
payable_account = self.spread2.credit_account_id
|
||||
balance_sheet = self.spread2.debit_account_id
|
||||
self.assertTrue(balance_sheet.reconcile)
|
||||
|
||||
spread_mls = self.spread2.line_ids.mapped("move_id.line_ids")
|
||||
self.assertTrue(spread_mls)
|
||||
for spread_ml in spread_mls:
|
||||
if spread_ml.debit:
|
||||
self.assertEqual(spread_ml.account_id, balance_sheet)
|
||||
self.assertFalse(spread_ml.reconciled)
|
||||
self.assertFalse(spread_ml.full_reconcile_id)
|
||||
if spread_ml.credit:
|
||||
self.assertEqual(spread_ml.account_id, payable_account)
|
||||
self.assertFalse(spread_ml.reconciled)
|
||||
self.assertFalse(spread_ml.full_reconcile_id)
|
||||
|
||||
invoice_mls = self.sale_invoice.invoice_line_ids
|
||||
self.assertTrue(invoice_mls)
|
||||
for invoice_ml in invoice_mls:
|
||||
self.assertEqual(invoice_ml.account_id, balance_sheet)
|
||||
|
||||
action_posted_view = self.spread2.open_posted_view()
|
||||
self.assertTrue(isinstance(action_posted_view, dict))
|
||||
self.assertTrue(action_posted_view.get("domain")[0][2])
|
||||
self.assertFalse(action_posted_view.get("res_id"))
|
||||
self.assertTrue(action_posted_view.get("context"))
|
||||
|
||||
action_spread_details = self.invoice_line.spread_details()
|
||||
self.assertTrue(isinstance(action_spread_details, dict))
|
||||
self.assertTrue(action_spread_details.get("res_id"))
|
||||
|
||||
def test_13_link_invoice_line_with_spread_sheet_partial_reconcile(self):
|
||||
|
||||
self.spread2.write(
|
||||
{
|
||||
"estimated_amount": 1000.0,
|
||||
"period_number": 12,
|
||||
"period_type": "month",
|
||||
"spread_date": datetime.date(2017, 1, 7),
|
||||
}
|
||||
)
|
||||
|
||||
self.spread2.compute_spread_board()
|
||||
spread_lines = self.spread2.line_ids
|
||||
self.assertEqual(len(spread_lines), 13)
|
||||
self.assertFalse(any(line.move_id for line in spread_lines))
|
||||
|
||||
spread_lines[0].create_and_reconcile_moves()
|
||||
spread_lines[1].create_and_reconcile_moves()
|
||||
spread_lines[2].create_and_reconcile_moves()
|
||||
spread_lines[3].create_and_reconcile_moves()
|
||||
|
||||
self.assertEqual(spread_lines[0].move_id.state, "posted")
|
||||
self.assertEqual(spread_lines[1].move_id.state, "posted")
|
||||
self.assertEqual(spread_lines[2].move_id.state, "posted")
|
||||
self.assertEqual(spread_lines[3].move_id.state, "posted")
|
||||
self.assertNotEqual(spread_lines[4].move_id.state, "posted")
|
||||
|
||||
spread_mls = spread_lines[0].move_id.line_ids
|
||||
self.assertTrue(spread_mls)
|
||||
for spread_ml in spread_mls:
|
||||
if spread_ml.debit:
|
||||
self.assertFalse(spread_ml.matched_debit_ids)
|
||||
self.assertFalse(spread_ml.matched_credit_ids)
|
||||
self.assertFalse(spread_ml.full_reconcile_id)
|
||||
if spread_ml.credit:
|
||||
self.assertFalse(spread_ml.matched_debit_ids)
|
||||
self.assertFalse(spread_ml.matched_credit_ids)
|
||||
self.assertFalse(spread_ml.full_reconcile_id)
|
||||
|
||||
balance_sheet = self.spread2.debit_account_id
|
||||
self.assertTrue(balance_sheet.reconcile)
|
||||
|
||||
# Assing invoice line to spread
|
||||
self.spread2.invoice_line_id = self.invoice_line
|
||||
self.assertEqual(self.invoice_line.spread_id, self.spread2)
|
||||
|
||||
# Validate invoice
|
||||
self.sale_invoice.action_post()
|
||||
invoice_mls = self.sale_invoice.invoice_line_ids
|
||||
self.assertTrue(invoice_mls)
|
||||
for invoice_ml in invoice_mls:
|
||||
self.assertEqual(invoice_ml.account_id, balance_sheet)
|
||||
|
||||
spread_lines = self.spread2.line_ids
|
||||
spread_lines[4].create_and_reconcile_moves()
|
||||
|
||||
self.assertEqual(spread_lines[4].move_id.state, "posted")
|
||||
|
||||
spread_mls = spread_lines[4].move_id.line_ids
|
||||
self.assertTrue(spread_mls)
|
||||
for spread_ml in spread_mls:
|
||||
if spread_ml.debit:
|
||||
self.assertFalse(spread_ml.matched_debit_ids)
|
||||
self.assertTrue(spread_ml.matched_credit_ids)
|
||||
self.assertFalse(spread_ml.full_reconcile_id)
|
||||
if spread_ml.credit:
|
||||
self.assertFalse(spread_ml.matched_debit_ids)
|
||||
self.assertFalse(spread_ml.matched_credit_ids)
|
||||
self.assertFalse(spread_ml.full_reconcile_id)
|
||||
|
||||
other_journal = self.env["account.journal"].create(
|
||||
{"name": "Other Journal", "type": "general", "code": "test2"}
|
||||
)
|
||||
with self.assertRaises(ValidationError):
|
||||
self.spread2.journal_id = other_journal
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
self.spread2.unlink()
|
||||
|
||||
def test_14_create_all_moves(self):
|
||||
self.spread.compute_spread_board()
|
||||
|
||||
self.assertEqual(len(self.spread.line_ids), 12)
|
||||
self.assertFalse(any(line.move_id for line in self.spread.line_ids))
|
||||
|
||||
# create moves for all the spread lines
|
||||
self.spread.create_all_moves()
|
||||
self.assertTrue(all(line.move_id for line in self.spread.line_ids))
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
self.spread.unlink()
|
||||
|
||||
def test_15_invoice_refund(self):
|
||||
|
||||
self.vendor_bill_line.spread_id = self.spread
|
||||
|
||||
# Validate invoice
|
||||
self.vendor_bill.action_post()
|
||||
self.assertTrue(self.vendor_bill.invoice_line_ids.mapped("spread_id"))
|
||||
|
||||
# Create a refund for invoice.
|
||||
move_reversal = (
|
||||
self.env["account.move.reversal"]
|
||||
.with_context(active_model="account.move", active_ids=self.vendor_bill.ids)
|
||||
.create(
|
||||
{
|
||||
"date": fields.Date.today(),
|
||||
"reason": "no reason",
|
||||
"refund_method": "refund",
|
||||
"journal_id": self.vendor_bill.journal_id.id,
|
||||
}
|
||||
)
|
||||
)
|
||||
reversal = move_reversal.reverse_moves()
|
||||
refund = self.env["account.move"].browse(reversal["res_id"])
|
||||
self.assertFalse(refund.invoice_line_ids.mapped("spread_id"))
|
@ -0,0 +1,351 @@
|
||||
# Copyright 2018-2020 Onestein (<https://www.onestein.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
import datetime
|
||||
|
||||
from psycopg2.errors import NotNullViolation
|
||||
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests import Form, common
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
class TestAccountSpreadCostRevenue(common.TransactionCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.sales_journal = self.env["account.journal"].create(
|
||||
{"name": "Customer Invoices - Test", "code": "TEST1", "type": "sale"}
|
||||
)
|
||||
|
||||
self.expenses_journal = self.env["account.journal"].create(
|
||||
{"name": "Vendor Bills - Test", "code": "TEST2", "type": "purchase"}
|
||||
)
|
||||
|
||||
self.credit_account = self.env["account.account"].create(
|
||||
{
|
||||
"name": "test_account_receivable",
|
||||
"code": "123",
|
||||
"account_type": "asset_receivable",
|
||||
"reconcile": True,
|
||||
}
|
||||
)
|
||||
|
||||
self.debit_account = self.env["account.account"].create(
|
||||
{
|
||||
"name": "test account_expenses",
|
||||
"code": "765",
|
||||
"account_type": "expense",
|
||||
"reconcile": True,
|
||||
}
|
||||
)
|
||||
|
||||
self.account_payable = self.env["account.account"].create(
|
||||
{
|
||||
"name": "test_account_payable",
|
||||
"code": "321",
|
||||
"account_type": "liability_payable",
|
||||
"reconcile": True,
|
||||
}
|
||||
)
|
||||
|
||||
self.account_revenue = self.env["account.account"].create(
|
||||
{
|
||||
"name": "test_account_revenue",
|
||||
"code": "864",
|
||||
"account_type": "asset_receivable",
|
||||
"reconcile": True,
|
||||
}
|
||||
)
|
||||
|
||||
def test_01_account_spread_defaults(self):
|
||||
this_year = datetime.date.today().year
|
||||
spread_template = self.env["account.spread.template"].create(
|
||||
{"name": "test", "spread_account_id": self.debit_account.id}
|
||||
)
|
||||
self.assertEqual(spread_template.spread_type, "sale")
|
||||
self.assertTrue(spread_template.spread_journal_id)
|
||||
|
||||
spread = self.env["account.spread"].create(
|
||||
{
|
||||
"name": "test",
|
||||
"invoice_type": "out_invoice",
|
||||
"debit_account_id": self.debit_account.id,
|
||||
"credit_account_id": self.credit_account.id,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertTrue(spread)
|
||||
self.assertFalse(spread.line_ids)
|
||||
self.assertFalse(spread.invoice_line_ids)
|
||||
self.assertFalse(spread.invoice_line_id)
|
||||
self.assertFalse(spread.invoice_id)
|
||||
self.assertFalse(spread.analytic_distribution)
|
||||
self.assertTrue(spread.move_line_auto_post)
|
||||
self.assertEqual(spread.name, "test")
|
||||
self.assertEqual(spread.invoice_type, "out_invoice")
|
||||
self.assertEqual(spread.company_id, self.env.company)
|
||||
self.assertEqual(spread.currency_id, self.env.company.currency_id)
|
||||
self.assertEqual(spread.period_number, 12)
|
||||
self.assertEqual(spread.period_type, "month")
|
||||
self.assertEqual(spread.debit_account_id, self.debit_account)
|
||||
self.assertEqual(spread.credit_account_id, self.credit_account)
|
||||
self.assertEqual(spread.unspread_amount, 0.0)
|
||||
self.assertEqual(spread.unposted_amount, 0.0)
|
||||
self.assertEqual(spread.total_amount, 0.0)
|
||||
self.assertEqual(spread.estimated_amount, 0.0)
|
||||
self.assertEqual(spread.spread_date, datetime.date(this_year, 1, 1))
|
||||
self.assertTrue(spread.journal_id)
|
||||
self.assertEqual(spread.journal_id.type, "general")
|
||||
|
||||
self.assertFalse(spread.display_create_all_moves)
|
||||
self.assertTrue(spread.display_recompute_buttons)
|
||||
self.assertTrue(spread.display_move_line_auto_post)
|
||||
|
||||
def test_02_config_defaults(self):
|
||||
self.assertFalse(self.env.company.default_spread_revenue_account_id)
|
||||
self.assertFalse(self.env.company.default_spread_expense_account_id)
|
||||
self.assertFalse(self.env.company.default_spread_revenue_journal_id)
|
||||
self.assertFalse(self.env.company.default_spread_expense_journal_id)
|
||||
|
||||
@mute_logger("odoo.sql_db")
|
||||
def test_03_no_defaults(self):
|
||||
with self.assertRaises(NotNullViolation):
|
||||
self.env["account.spread"].create({"name": "test"})
|
||||
with self.assertRaises(NotNullViolation):
|
||||
self.env["account.spread"].create(
|
||||
{"name": "test", "invoice_type": "out_invoice"}
|
||||
)
|
||||
|
||||
@mute_logger("odoo.sql_db")
|
||||
def test_04_no_defaults(self):
|
||||
with self.assertRaises(NotNullViolation):
|
||||
self.env["account.spread"].create(
|
||||
{
|
||||
"name": "test",
|
||||
"debit_account_id": self.debit_account.id,
|
||||
"credit_account_id": self.credit_account.id,
|
||||
}
|
||||
)
|
||||
with self.assertRaises(NotNullViolation):
|
||||
self.env["account.spread"].create(
|
||||
{
|
||||
"name": "test",
|
||||
"credit_account_id": self.credit_account.id,
|
||||
}
|
||||
)
|
||||
|
||||
def test_05_config_settings(self):
|
||||
self.env.company.default_spread_revenue_account_id = self.account_revenue
|
||||
self.env.company.default_spread_expense_account_id = self.account_payable
|
||||
self.env.company.default_spread_revenue_journal_id = self.sales_journal
|
||||
self.env.company.default_spread_expense_journal_id = self.expenses_journal
|
||||
|
||||
self.assertTrue(self.env.company.default_spread_revenue_account_id)
|
||||
self.assertTrue(self.env.company.default_spread_expense_account_id)
|
||||
self.assertTrue(self.env.company.default_spread_revenue_journal_id)
|
||||
self.assertTrue(self.env.company.default_spread_expense_journal_id)
|
||||
|
||||
self.env.user.groups_id += self.env.ref("base.group_multi_company")
|
||||
|
||||
spread_form = Form(self.env["account.spread"])
|
||||
spread_form.name = "test"
|
||||
spread_form.invoice_type = "in_invoice"
|
||||
spread_form.debit_account_id = self.debit_account
|
||||
spread_form.credit_account_id = self.credit_account
|
||||
spread = spread_form.save()
|
||||
|
||||
self.assertTrue(spread)
|
||||
self.assertFalse(spread.line_ids)
|
||||
self.assertFalse(spread.invoice_line_ids)
|
||||
self.assertFalse(spread.invoice_line_id)
|
||||
self.assertFalse(spread.invoice_id)
|
||||
self.assertFalse(spread.analytic_distribution)
|
||||
self.assertTrue(spread.move_line_auto_post)
|
||||
|
||||
defaults = self.env["account.spread"].default_get(["company_id", "currency_id"])
|
||||
|
||||
self.assertEqual(defaults["company_id"], self.env.company.id)
|
||||
self.assertEqual(defaults["currency_id"], self.env.company.currency_id.id)
|
||||
|
||||
spread_form = Form(spread)
|
||||
spread_form.invoice_type = "out_invoice"
|
||||
spread_form.company_id = self.env.company
|
||||
spread = spread_form.save()
|
||||
self.assertEqual(spread.debit_account_id, self.account_revenue)
|
||||
self.assertFalse(spread.is_debit_account_deprecated)
|
||||
self.assertEqual(spread.journal_id, self.sales_journal)
|
||||
self.assertEqual(spread.spread_type, "sale")
|
||||
|
||||
spread_form = Form(spread)
|
||||
spread_form.invoice_type = "in_invoice"
|
||||
spread = spread_form.save()
|
||||
self.assertEqual(spread.credit_account_id, self.account_payable)
|
||||
self.assertFalse(spread.is_credit_account_deprecated)
|
||||
self.assertEqual(spread.journal_id, self.expenses_journal)
|
||||
self.assertEqual(spread.spread_type, "purchase")
|
||||
|
||||
def test_07_create_spread_template(self):
|
||||
spread_template = self.env["account.spread.template"].create(
|
||||
{
|
||||
"name": "test",
|
||||
"spread_type": "sale",
|
||||
"spread_account_id": self.account_revenue.id,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(spread_template.company_id, self.env.company)
|
||||
self.assertTrue(spread_template.spread_journal_id)
|
||||
|
||||
self.env.company.default_spread_revenue_account_id = self.account_revenue
|
||||
self.env.company.default_spread_expense_account_id = self.account_payable
|
||||
self.env.company.default_spread_revenue_journal_id = self.sales_journal
|
||||
self.env.company.default_spread_expense_journal_id = self.expenses_journal
|
||||
|
||||
spread_template.spread_type = "purchase"
|
||||
self.assertTrue(spread_template.spread_journal_id)
|
||||
self.assertTrue(spread_template.spread_account_id)
|
||||
self.assertEqual(spread_template.spread_account_id, self.account_payable)
|
||||
self.assertEqual(spread_template.spread_journal_id, self.expenses_journal)
|
||||
|
||||
spread_vals = spread_template._prepare_spread_from_template()
|
||||
self.assertTrue(spread_vals["name"])
|
||||
self.assertTrue(spread_vals["template_id"])
|
||||
self.assertTrue(spread_vals["journal_id"])
|
||||
self.assertTrue(spread_vals["company_id"])
|
||||
self.assertTrue(spread_vals["invoice_type"])
|
||||
self.assertTrue(spread_vals["credit_account_id"])
|
||||
|
||||
spread_template.spread_type = "sale"
|
||||
self.assertTrue(spread_template.spread_journal_id)
|
||||
self.assertTrue(spread_template.spread_account_id)
|
||||
self.assertEqual(spread_template.spread_account_id, self.account_revenue)
|
||||
self.assertEqual(spread_template.spread_journal_id, self.sales_journal)
|
||||
|
||||
spread_vals = spread_template._prepare_spread_from_template()
|
||||
self.assertTrue(spread_vals["name"])
|
||||
self.assertTrue(spread_vals["template_id"])
|
||||
self.assertTrue(spread_vals["journal_id"])
|
||||
self.assertTrue(spread_vals["company_id"])
|
||||
self.assertTrue(spread_vals["invoice_type"])
|
||||
self.assertTrue(spread_vals["debit_account_id"])
|
||||
|
||||
def test_08_check_template_invoice_type(self):
|
||||
template_sale = self.env["account.spread.template"].create(
|
||||
{
|
||||
"name": "test",
|
||||
"spread_type": "sale",
|
||||
"spread_account_id": self.account_revenue.id,
|
||||
}
|
||||
)
|
||||
template_purchase = self.env["account.spread.template"].create(
|
||||
{
|
||||
"name": "test",
|
||||
"spread_type": "purchase",
|
||||
"spread_account_id": self.account_payable.id,
|
||||
}
|
||||
)
|
||||
spread = self.env["account.spread"].create(
|
||||
{
|
||||
"name": "test",
|
||||
"invoice_type": "out_invoice",
|
||||
"debit_account_id": self.debit_account.id,
|
||||
"credit_account_id": self.credit_account.id,
|
||||
}
|
||||
)
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
spread.template_id = template_purchase
|
||||
|
||||
spread.template_id = template_sale
|
||||
self.assertEqual(spread.template_id, template_sale)
|
||||
|
||||
self.assertFalse(spread.display_create_all_moves)
|
||||
self.assertTrue(spread.display_recompute_buttons)
|
||||
self.assertTrue(spread.display_move_line_auto_post)
|
||||
|
||||
spread = self.env["account.spread"].create(
|
||||
{
|
||||
"name": "test",
|
||||
"invoice_type": "in_invoice",
|
||||
"debit_account_id": self.debit_account.id,
|
||||
"credit_account_id": self.credit_account.id,
|
||||
}
|
||||
)
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
spread.template_id = template_sale
|
||||
|
||||
spread.template_id = template_purchase
|
||||
self.assertEqual(spread.template_id, template_purchase)
|
||||
|
||||
self.assertFalse(spread.display_create_all_moves)
|
||||
self.assertTrue(spread.display_recompute_buttons)
|
||||
self.assertTrue(spread.display_move_line_auto_post)
|
||||
|
||||
def test_10_account_spread_unlink(self):
|
||||
spread = self.env["account.spread"].create(
|
||||
{
|
||||
"name": "test",
|
||||
"invoice_type": "out_invoice",
|
||||
"debit_account_id": self.debit_account.id,
|
||||
"credit_account_id": self.credit_account.id,
|
||||
}
|
||||
)
|
||||
spread.unlink()
|
||||
|
||||
def test_11_compute_display_fields(self):
|
||||
spread = self.env["account.spread"].create(
|
||||
{
|
||||
"name": "test",
|
||||
"invoice_type": "out_invoice",
|
||||
"debit_account_id": self.debit_account.id,
|
||||
"credit_account_id": self.credit_account.id,
|
||||
}
|
||||
)
|
||||
spread.company_id.allow_spread_planning = True
|
||||
self.assertFalse(spread.display_create_all_moves)
|
||||
self.assertTrue(spread.display_recompute_buttons)
|
||||
self.assertTrue(spread.display_move_line_auto_post)
|
||||
|
||||
def test_12_compute_display_fields(self):
|
||||
spread = self.env["account.spread"].create(
|
||||
{
|
||||
"name": "test",
|
||||
"invoice_type": "out_invoice",
|
||||
"debit_account_id": self.debit_account.id,
|
||||
"credit_account_id": self.credit_account.id,
|
||||
}
|
||||
)
|
||||
spread.company_id.allow_spread_planning = False
|
||||
self.assertFalse(spread.display_create_all_moves)
|
||||
self.assertTrue(spread.display_recompute_buttons)
|
||||
self.assertTrue(spread.display_move_line_auto_post)
|
||||
|
||||
def test_13_compute_display_fields(self):
|
||||
spread = self.env["account.spread"].create(
|
||||
{
|
||||
"name": "test",
|
||||
"invoice_type": "out_invoice",
|
||||
"debit_account_id": self.debit_account.id,
|
||||
"credit_account_id": self.credit_account.id,
|
||||
}
|
||||
)
|
||||
spread.company_id.force_move_auto_post = True
|
||||
self.assertFalse(spread.display_create_all_moves)
|
||||
self.assertTrue(spread.display_recompute_buttons)
|
||||
self.assertFalse(spread.display_move_line_auto_post)
|
||||
|
||||
def test_14_compute_display_fields(self):
|
||||
spread = self.env["account.spread"].create(
|
||||
{
|
||||
"name": "test",
|
||||
"invoice_type": "out_invoice",
|
||||
"debit_account_id": self.debit_account.id,
|
||||
"credit_account_id": self.credit_account.id,
|
||||
}
|
||||
)
|
||||
spread.company_id.force_move_auto_post = False
|
||||
self.assertFalse(spread.display_create_all_moves)
|
||||
self.assertTrue(spread.display_recompute_buttons)
|
||||
self.assertTrue(spread.display_move_line_auto_post)
|
725
account_spread_cost_revenue/tests/test_compute_spread_board.py
Normal file
725
account_spread_cost_revenue/tests/test_compute_spread_board.py
Normal file
@ -0,0 +1,725 @@
|
||||
# Copyright 2017-2020 Onestein (<https://www.onestein.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
import datetime
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import Form, common
|
||||
|
||||
|
||||
class TestComputeSpreadBoard(common.TransactionCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
journal = self.env["account.journal"].create(
|
||||
{"name": "Test", "type": "general", "code": "test"}
|
||||
)
|
||||
|
||||
self.receivable_account = self.env["account.account"].create(
|
||||
{
|
||||
"name": "test_account_receivable",
|
||||
"code": "123",
|
||||
"account_type": "asset_receivable",
|
||||
"reconcile": True,
|
||||
}
|
||||
)
|
||||
|
||||
self.expense_account = self.env["account.account"].create(
|
||||
{
|
||||
"name": "test account_expenses",
|
||||
"code": "765",
|
||||
"account_type": "expense",
|
||||
"reconcile": True,
|
||||
}
|
||||
)
|
||||
|
||||
self.spread_account = self.env["account.account"].create(
|
||||
{
|
||||
"name": "test spread account_expenses",
|
||||
"code": "321",
|
||||
"account_type": "expense",
|
||||
"reconcile": True,
|
||||
}
|
||||
)
|
||||
|
||||
self.spread = self.env["account.spread"].create(
|
||||
{
|
||||
"name": "test",
|
||||
"debit_account_id": self.spread_account.id,
|
||||
"credit_account_id": self.expense_account.id,
|
||||
"period_number": 12,
|
||||
"period_type": "month",
|
||||
"spread_date": "2017-02-01",
|
||||
"estimated_amount": 1000.0,
|
||||
"journal_id": journal.id,
|
||||
"invoice_type": "in_invoice",
|
||||
}
|
||||
)
|
||||
|
||||
self.spread2 = self.env["account.spread"].create(
|
||||
{
|
||||
"name": "test2",
|
||||
"debit_account_id": self.spread_account.id,
|
||||
"credit_account_id": self.expense_account.id,
|
||||
"period_number": 12,
|
||||
"period_type": "month",
|
||||
"spread_date": "2017-02-01",
|
||||
"estimated_amount": 1000.0,
|
||||
"journal_id": journal.id,
|
||||
"invoice_type": "out_invoice",
|
||||
}
|
||||
)
|
||||
|
||||
self.spread3 = self.env["account.spread"].create(
|
||||
{
|
||||
"name": "test by cal days",
|
||||
"debit_account_id": self.spread_account.id,
|
||||
"credit_account_id": self.expense_account.id,
|
||||
"period_number": 12,
|
||||
"period_type": "month",
|
||||
"spread_date": "2017-02-01",
|
||||
"estimated_amount": 12000.0,
|
||||
"journal_id": journal.id,
|
||||
"invoice_type": "out_invoice",
|
||||
"days_calc": True,
|
||||
}
|
||||
)
|
||||
|
||||
self.template = self.env["account.spread.template"].create(
|
||||
{
|
||||
"name": "test",
|
||||
"spread_type": "purchase",
|
||||
"period_number": 5,
|
||||
"period_type": "month",
|
||||
"start_date": "2017-01-01",
|
||||
"spread_account_id": self.spread_account.id,
|
||||
"spread_journal_id": journal.id,
|
||||
"days_calc": True,
|
||||
}
|
||||
)
|
||||
|
||||
def test_01_supplier_invoice(self):
|
||||
self.spread.compute_spread_board()
|
||||
spread_lines = self.spread.line_ids
|
||||
self.assertEqual(len(spread_lines), 12)
|
||||
|
||||
self.assertEqual(83.33, spread_lines[0].amount)
|
||||
self.assertEqual(83.33, spread_lines[1].amount)
|
||||
self.assertEqual(83.33, spread_lines[2].amount)
|
||||
self.assertEqual(83.33, spread_lines[3].amount)
|
||||
self.assertEqual(83.33, spread_lines[4].amount)
|
||||
self.assertEqual(83.33, spread_lines[5].amount)
|
||||
self.assertEqual(83.33, spread_lines[6].amount)
|
||||
self.assertEqual(83.33, spread_lines[7].amount)
|
||||
self.assertEqual(83.33, spread_lines[8].amount)
|
||||
self.assertEqual(83.33, spread_lines[9].amount)
|
||||
self.assertEqual(83.33, spread_lines[10].amount)
|
||||
self.assertEqual(83.37, spread_lines[11].amount)
|
||||
|
||||
self.assertEqual(datetime.date(2017, 2, 28), spread_lines[0].date)
|
||||
self.assertEqual(datetime.date(2017, 3, 31), spread_lines[1].date)
|
||||
self.assertEqual(datetime.date(2017, 4, 30), spread_lines[2].date)
|
||||
self.assertEqual(datetime.date(2017, 5, 31), spread_lines[3].date)
|
||||
self.assertEqual(datetime.date(2017, 6, 30), spread_lines[4].date)
|
||||
self.assertEqual(datetime.date(2017, 7, 31), spread_lines[5].date)
|
||||
self.assertEqual(datetime.date(2017, 8, 31), spread_lines[6].date)
|
||||
self.assertEqual(datetime.date(2017, 9, 30), spread_lines[7].date)
|
||||
self.assertEqual(datetime.date(2017, 10, 31), spread_lines[8].date)
|
||||
self.assertEqual(datetime.date(2017, 11, 30), spread_lines[9].date)
|
||||
self.assertEqual(datetime.date(2017, 12, 31), spread_lines[10].date)
|
||||
self.assertEqual(datetime.date(2018, 1, 31), spread_lines[11].date)
|
||||
|
||||
for line in spread_lines:
|
||||
self.assertFalse(line.move_id)
|
||||
|
||||
self.assertEqual(self.spread.unspread_amount, 1000.0)
|
||||
self.assertEqual(self.spread.unposted_amount, 1000.0)
|
||||
|
||||
for line in spread_lines:
|
||||
line.create_move()
|
||||
self.assertTrue(line.move_id)
|
||||
|
||||
self.spread.action_recalculate_spread()
|
||||
spread_lines = self.spread.line_ids
|
||||
for line in spread_lines:
|
||||
self.assertTrue(line.move_id)
|
||||
|
||||
def test_02_supplier_invoice(self):
|
||||
# spread date set
|
||||
self.spread.write(
|
||||
{
|
||||
"period_number": 12,
|
||||
"period_type": "month",
|
||||
"spread_date": datetime.date(2017, 1, 7),
|
||||
}
|
||||
)
|
||||
self.spread_account.reconcile = True
|
||||
self.assertTrue(self.spread_account.reconcile)
|
||||
|
||||
self.spread.compute_spread_board()
|
||||
spread_lines = self.spread.line_ids
|
||||
self.assertEqual(len(spread_lines), 13)
|
||||
|
||||
self.assertEqual(67.20, spread_lines[0].amount)
|
||||
self.assertEqual(83.33, spread_lines[1].amount)
|
||||
self.assertEqual(83.33, spread_lines[2].amount)
|
||||
self.assertEqual(83.33, spread_lines[3].amount)
|
||||
self.assertEqual(83.33, spread_lines[4].amount)
|
||||
self.assertEqual(83.33, spread_lines[5].amount)
|
||||
self.assertEqual(83.33, spread_lines[6].amount)
|
||||
self.assertEqual(83.33, spread_lines[7].amount)
|
||||
self.assertEqual(83.33, spread_lines[8].amount)
|
||||
self.assertEqual(83.33, spread_lines[9].amount)
|
||||
self.assertEqual(83.33, spread_lines[10].amount)
|
||||
self.assertEqual(83.33, spread_lines[11].amount)
|
||||
self.assertEqual(16.17, spread_lines[12].amount)
|
||||
|
||||
self.assertEqual(datetime.date(2017, 1, 31), spread_lines[0].date)
|
||||
self.assertEqual(datetime.date(2017, 2, 28), spread_lines[1].date)
|
||||
self.assertEqual(datetime.date(2017, 3, 31), spread_lines[2].date)
|
||||
self.assertEqual(datetime.date(2017, 4, 30), spread_lines[3].date)
|
||||
self.assertEqual(datetime.date(2017, 5, 31), spread_lines[4].date)
|
||||
self.assertEqual(datetime.date(2017, 6, 30), spread_lines[5].date)
|
||||
self.assertEqual(datetime.date(2017, 7, 31), spread_lines[6].date)
|
||||
self.assertEqual(datetime.date(2017, 8, 31), spread_lines[7].date)
|
||||
self.assertEqual(datetime.date(2017, 9, 30), spread_lines[8].date)
|
||||
self.assertEqual(datetime.date(2017, 10, 31), spread_lines[9].date)
|
||||
self.assertEqual(datetime.date(2017, 11, 30), spread_lines[10].date)
|
||||
self.assertEqual(datetime.date(2017, 12, 31), spread_lines[11].date)
|
||||
self.assertEqual(datetime.date(2018, 1, 31), spread_lines[12].date)
|
||||
|
||||
for line in spread_lines:
|
||||
self.assertFalse(line.move_id)
|
||||
|
||||
self.assertEqual(self.spread.unspread_amount, 1000.0)
|
||||
self.assertEqual(self.spread.unposted_amount, 1000.0)
|
||||
|
||||
self.spread.line_ids.create_and_reconcile_moves()
|
||||
for line in self.spread.line_ids:
|
||||
self.assertTrue(line.move_id)
|
||||
|
||||
def test_03_supplier_invoice(self):
|
||||
# spread date set
|
||||
self.spread.write(
|
||||
{
|
||||
"period_number": 12,
|
||||
"period_type": "month",
|
||||
"spread_date": datetime.date(2017, 1, 31),
|
||||
"move_line_auto_post": False,
|
||||
}
|
||||
)
|
||||
|
||||
self.spread.compute_spread_board()
|
||||
spread_lines = self.spread.line_ids
|
||||
self.assertEqual(len(spread_lines), 13)
|
||||
self.assertEqual(2.69, spread_lines[0].amount)
|
||||
self.assertEqual(83.33, spread_lines[1].amount)
|
||||
self.assertEqual(83.33, spread_lines[2].amount)
|
||||
self.assertEqual(83.33, spread_lines[3].amount)
|
||||
self.assertEqual(83.33, spread_lines[4].amount)
|
||||
self.assertEqual(83.33, spread_lines[5].amount)
|
||||
self.assertEqual(83.33, spread_lines[6].amount)
|
||||
self.assertEqual(83.33, spread_lines[7].amount)
|
||||
self.assertEqual(83.33, spread_lines[8].amount)
|
||||
self.assertEqual(83.33, spread_lines[9].amount)
|
||||
self.assertEqual(83.33, spread_lines[10].amount)
|
||||
self.assertEqual(83.33, spread_lines[11].amount)
|
||||
self.assertEqual(80.68, spread_lines[12].amount)
|
||||
|
||||
self.assertEqual(datetime.date(2017, 1, 31), spread_lines[0].date)
|
||||
self.assertEqual(datetime.date(2017, 2, 28), spread_lines[1].date)
|
||||
self.assertEqual(datetime.date(2017, 3, 31), spread_lines[2].date)
|
||||
self.assertEqual(datetime.date(2017, 4, 30), spread_lines[3].date)
|
||||
self.assertEqual(datetime.date(2017, 5, 31), spread_lines[4].date)
|
||||
self.assertEqual(datetime.date(2017, 6, 30), spread_lines[5].date)
|
||||
self.assertEqual(datetime.date(2017, 7, 31), spread_lines[6].date)
|
||||
self.assertEqual(datetime.date(2017, 8, 31), spread_lines[7].date)
|
||||
self.assertEqual(datetime.date(2017, 9, 30), spread_lines[8].date)
|
||||
self.assertEqual(datetime.date(2017, 10, 31), spread_lines[9].date)
|
||||
self.assertEqual(datetime.date(2017, 11, 30), spread_lines[10].date)
|
||||
self.assertEqual(datetime.date(2017, 12, 31), spread_lines[11].date)
|
||||
self.assertEqual(datetime.date(2018, 1, 31), spread_lines[12].date)
|
||||
|
||||
for line in spread_lines:
|
||||
self.assertFalse(line.move_id)
|
||||
|
||||
self.assertEqual(self.spread.unspread_amount, 1000.0)
|
||||
self.assertEqual(self.spread.unposted_amount, 1000.0)
|
||||
|
||||
spread_lines[0].create_move()
|
||||
spread_lines[1].create_move()
|
||||
spread_lines[2].create_move()
|
||||
self.assertTrue(any(line.move_id for line in spread_lines))
|
||||
self.assertTrue(any(not line.move_id for line in spread_lines))
|
||||
|
||||
self.spread._compute_amounts()
|
||||
self.assertEqual(self.spread.unspread_amount, 830.65)
|
||||
self.assertEqual(self.spread.unposted_amount, 1000.0)
|
||||
|
||||
self.spread.compute_spread_board()
|
||||
spread_lines = self.spread.line_ids
|
||||
self.assertEqual(len(spread_lines), 13)
|
||||
self.assertEqual(2.69, spread_lines[0].amount)
|
||||
self.assertEqual(83.33, spread_lines[1].amount)
|
||||
self.assertEqual(83.33, spread_lines[2].amount)
|
||||
self.assertEqual(83.33, spread_lines[3].amount)
|
||||
self.assertEqual(83.33, spread_lines[4].amount)
|
||||
self.assertEqual(83.33, spread_lines[5].amount)
|
||||
self.assertEqual(83.33, spread_lines[6].amount)
|
||||
self.assertEqual(83.33, spread_lines[7].amount)
|
||||
self.assertEqual(83.33, spread_lines[8].amount)
|
||||
self.assertEqual(83.33, spread_lines[9].amount)
|
||||
self.assertEqual(83.33, spread_lines[10].amount)
|
||||
self.assertEqual(83.33, spread_lines[11].amount)
|
||||
self.assertEqual(80.68, spread_lines[12].amount)
|
||||
|
||||
self.assertEqual(datetime.date(2017, 1, 31), spread_lines[0].date)
|
||||
self.assertEqual(datetime.date(2017, 2, 28), spread_lines[1].date)
|
||||
self.assertEqual(datetime.date(2017, 3, 31), spread_lines[2].date)
|
||||
self.assertEqual(datetime.date(2017, 4, 30), spread_lines[3].date)
|
||||
self.assertEqual(datetime.date(2017, 5, 31), spread_lines[4].date)
|
||||
self.assertEqual(datetime.date(2017, 6, 30), spread_lines[5].date)
|
||||
self.assertEqual(datetime.date(2017, 7, 31), spread_lines[6].date)
|
||||
self.assertEqual(datetime.date(2017, 8, 31), spread_lines[7].date)
|
||||
self.assertEqual(datetime.date(2017, 9, 30), spread_lines[8].date)
|
||||
self.assertEqual(datetime.date(2017, 10, 31), spread_lines[9].date)
|
||||
self.assertEqual(datetime.date(2017, 11, 30), spread_lines[10].date)
|
||||
self.assertEqual(datetime.date(2017, 12, 31), spread_lines[11].date)
|
||||
self.assertEqual(datetime.date(2018, 1, 31), spread_lines[12].date)
|
||||
|
||||
def test_04_supplier_invoice(self):
|
||||
self.spread.write(
|
||||
{
|
||||
"credit_account_id": self.expense_account.id,
|
||||
"debit_account_id": self.spread_account.id,
|
||||
"period_number": 3,
|
||||
"period_type": "year",
|
||||
"spread_date": datetime.date(2018, 10, 24),
|
||||
}
|
||||
)
|
||||
|
||||
# change the state of invoice to open by clicking Validate button
|
||||
self.spread.compute_spread_board()
|
||||
spread_lines = self.spread.line_ids
|
||||
self.assertEqual(len(spread_lines), 4)
|
||||
self.assertEqual(333.33, spread_lines[1].amount)
|
||||
self.assertEqual(333.33, spread_lines[2].amount)
|
||||
first_amount = spread_lines[0].amount
|
||||
last_amount = spread_lines[3].amount
|
||||
remaining_amount = first_amount + last_amount
|
||||
self.assertAlmostEqual(remaining_amount, 333.34, places=2)
|
||||
total_line_amount = 0.0
|
||||
for line in spread_lines:
|
||||
total_line_amount += line.amount
|
||||
self.assertAlmostEqual(total_line_amount, 1000.0, places=2)
|
||||
|
||||
for line in spread_lines:
|
||||
self.assertFalse(line.move_id)
|
||||
|
||||
self.assertEqual(self.spread.unspread_amount, 1000.0)
|
||||
self.assertEqual(self.spread.unposted_amount, 1000.0)
|
||||
|
||||
def test_05_supplier_invoice(self):
|
||||
# spread date set
|
||||
self.spread.write(
|
||||
{
|
||||
"period_number": 12,
|
||||
"period_type": "month",
|
||||
"spread_date": datetime.date(2017, 2, 1),
|
||||
}
|
||||
)
|
||||
|
||||
self.spread.compute_spread_board()
|
||||
|
||||
# create moves for all the spread lines and open them
|
||||
self.spread.line_ids.create_and_reconcile_moves()
|
||||
for spread_line in self.spread.line_ids:
|
||||
attrs = spread_line.open_move()
|
||||
self.assertTrue(isinstance(attrs, dict))
|
||||
|
||||
# unlink all created moves
|
||||
self.spread.line_ids.unlink_move()
|
||||
for spread_line in self.spread.line_ids:
|
||||
self.assertFalse(spread_line.move_id)
|
||||
|
||||
self.assertEqual(self.spread.unspread_amount, 1000.0)
|
||||
self.assertEqual(self.spread.unposted_amount, 1000.0)
|
||||
|
||||
def test_06_supplier_invoice(self):
|
||||
# spread date set
|
||||
self.spread.write(
|
||||
{"period_number": 3, "period_type": "quarter", "move_line_auto_post": False}
|
||||
)
|
||||
|
||||
self.spread.compute_spread_board()
|
||||
|
||||
# create moves for all the spread lines and open them
|
||||
self.spread.line_ids.create_and_reconcile_moves()
|
||||
|
||||
# check move lines
|
||||
for spread_line in self.spread.line_ids:
|
||||
for move_line in spread_line.move_id.line_ids:
|
||||
spread_account = self.spread.debit_account_id
|
||||
if move_line.account_id == spread_account:
|
||||
debit = move_line.debit
|
||||
self.assertAlmostEqual(debit, spread_line.amount)
|
||||
|
||||
for line in self.spread.line_ids:
|
||||
self.assertTrue(line.move_id)
|
||||
self.assertFalse(line.move_id.state == "posted")
|
||||
|
||||
self.assertEqual(self.spread.unspread_amount, 0.0)
|
||||
self.assertEqual(self.spread.unposted_amount, 1000.0)
|
||||
|
||||
# try to create move lines again: an error is raised
|
||||
for line in self.spread.line_ids:
|
||||
with self.assertRaises(UserError):
|
||||
line.create_move()
|
||||
|
||||
self.spread.write({"move_line_auto_post": True})
|
||||
self.spread.action_recalculate_spread()
|
||||
|
||||
for line in self.spread.line_ids:
|
||||
self.assertTrue(line.move_id)
|
||||
self.assertTrue(line.move_id.state == "posted")
|
||||
|
||||
self.assertEqual(self.spread.unspread_amount, 0.0)
|
||||
self.assertEqual(self.spread.unposted_amount, 0.0)
|
||||
|
||||
def test_07_supplier_invoice(self):
|
||||
self.spread.write(
|
||||
{
|
||||
"period_number": 3,
|
||||
"period_type": "month",
|
||||
"spread_date": datetime.date(2017, 1, 1),
|
||||
"estimated_amount": 345.96,
|
||||
}
|
||||
)
|
||||
|
||||
self.spread.compute_spread_board()
|
||||
spread_lines = self.spread.line_ids
|
||||
self.assertEqual(len(spread_lines), 3)
|
||||
self.assertAlmostEqual(115.32, spread_lines[0].amount)
|
||||
self.assertAlmostEqual(115.32, spread_lines[1].amount)
|
||||
self.assertAlmostEqual(115.32, spread_lines[2].amount)
|
||||
self.assertEqual(datetime.date(2017, 1, 31), spread_lines[0].date)
|
||||
self.assertEqual(datetime.date(2017, 2, 28), spread_lines[1].date)
|
||||
self.assertEqual(datetime.date(2017, 3, 31), spread_lines[2].date)
|
||||
|
||||
for line in spread_lines:
|
||||
self.assertFalse(line.move_id)
|
||||
|
||||
self.assertEqual(self.spread.unspread_amount, 345.96)
|
||||
self.assertEqual(self.spread.unposted_amount, 345.96)
|
||||
|
||||
def test_08_supplier_invoice(self):
|
||||
# spread date set
|
||||
self.spread.write(
|
||||
{
|
||||
"period_number": 12,
|
||||
"period_type": "month",
|
||||
"spread_date": datetime.date(2017, 2, 1),
|
||||
}
|
||||
)
|
||||
|
||||
self.spread.compute_spread_board()
|
||||
self.assertTrue(self.spread.line_ids)
|
||||
self.spread.action_undo_spread()
|
||||
self.assertFalse(self.spread.line_ids)
|
||||
|
||||
self.assertEqual(self.spread.unspread_amount, 1000.0)
|
||||
self.assertEqual(self.spread.unposted_amount, 1000.0)
|
||||
|
||||
def test_09_supplier_invoice(self):
|
||||
# spread date set
|
||||
self.spread.write(
|
||||
{
|
||||
"period_number": 12,
|
||||
"period_type": "month",
|
||||
"spread_date": datetime.date(2017, 2, 1),
|
||||
}
|
||||
)
|
||||
|
||||
self.spread.compute_spread_board()
|
||||
for line in self.spread.line_ids:
|
||||
line.create_move()
|
||||
self.assertTrue(line.move_id)
|
||||
action = line.open_move()
|
||||
self.assertTrue(action)
|
||||
|
||||
self.spread.line_ids.unlink_move()
|
||||
for line in self.spread.line_ids:
|
||||
self.assertFalse(line.move_id)
|
||||
self.assertTrue(self.spread.line_ids)
|
||||
|
||||
self.assertEqual(self.spread.unspread_amount, 1000.0)
|
||||
self.assertEqual(self.spread.unposted_amount, 1000.0)
|
||||
|
||||
def test_10_create_entries(self):
|
||||
self.env["account.spread.line"]._create_entries()
|
||||
self.assertFalse(self.spread.line_ids)
|
||||
|
||||
self.spread.compute_spread_board()
|
||||
self.env["account.spread.line"]._create_entries()
|
||||
self.assertTrue(self.spread.line_ids)
|
||||
for line in self.spread.line_ids:
|
||||
self.assertTrue(line.move_id)
|
||||
|
||||
def test_11_create_move_sale_invoice(self):
|
||||
self.spread2.move_line_auto_post = False
|
||||
self.spread2.compute_spread_board()
|
||||
for line in self.spread2.line_ids:
|
||||
self.assertFalse(line.move_id)
|
||||
line.create_move()
|
||||
self.assertTrue(line.move_id)
|
||||
self.assertFalse(line.move_id.state == "posted")
|
||||
|
||||
self.spread2.action_undo_spread()
|
||||
for line in self.spread2.line_ids:
|
||||
self.assertFalse(line.move_id)
|
||||
|
||||
self.spread2.action_recalculate_spread()
|
||||
for line in self.spread2.line_ids:
|
||||
self.assertTrue(line.move_id)
|
||||
self.assertTrue(line.move_id)
|
||||
self.assertFalse(line.move_id.state == "posted")
|
||||
# try to create move lines again: an error is raised
|
||||
with self.assertRaises(UserError):
|
||||
line.create_move()
|
||||
|
||||
def test_12_supplier_invoice_auto_post(self):
|
||||
# spread date set
|
||||
self.spread.write(
|
||||
{"period_number": 8, "period_type": "month", "move_line_auto_post": True}
|
||||
)
|
||||
|
||||
self.spread.compute_spread_board()
|
||||
|
||||
# create moves for all the spread lines and open them
|
||||
self.spread.line_ids.create_and_reconcile_moves()
|
||||
|
||||
# check move lines
|
||||
for spread_line in self.spread.line_ids:
|
||||
for move_line in spread_line.move_id.line_ids:
|
||||
spread_account = self.spread.debit_account_id
|
||||
if move_line.account_id == spread_account:
|
||||
debit = move_line.debit
|
||||
self.assertAlmostEqual(debit, spread_line.amount)
|
||||
|
||||
self.assertTrue(self.spread.move_line_auto_post)
|
||||
for line in self.spread.line_ids:
|
||||
self.assertTrue(line.move_id)
|
||||
self.assertTrue(line.move_id.state == "posted")
|
||||
|
||||
self.assertEqual(self.spread.unspread_amount, 0.0)
|
||||
self.assertEqual(self.spread.unposted_amount, 0.0)
|
||||
|
||||
def test_13_create_move_in_invoice_auto_post(self):
|
||||
self.spread2.write({"period_number": 4, "move_line_auto_post": True})
|
||||
self.spread_account.reconcile = True
|
||||
self.assertTrue(self.spread_account.reconcile)
|
||||
|
||||
self.spread2.compute_spread_board()
|
||||
for line in self.spread2.line_ids:
|
||||
self.assertFalse(line.move_id)
|
||||
line.create_move()
|
||||
self.assertTrue(line.move_id)
|
||||
self.assertTrue(line.move_id.state == "posted")
|
||||
|
||||
self.assertEqual(self.spread.unspread_amount, 1000.0)
|
||||
self.assertEqual(self.spread.unposted_amount, 1000.0)
|
||||
|
||||
def test_14_negative_amount(self):
|
||||
# spread date set
|
||||
self.spread.write(
|
||||
{
|
||||
"estimated_amount": -1000.0,
|
||||
"period_number": 12,
|
||||
"period_type": "month",
|
||||
"spread_date": datetime.date(2017, 1, 7),
|
||||
}
|
||||
)
|
||||
self.spread.compute_spread_board()
|
||||
|
||||
spread_lines = self.spread.line_ids
|
||||
self.assertTrue(spread_lines)
|
||||
|
||||
def test_15_compute_spread_board_line_account_deprecated(self):
|
||||
self.spread.debit_account_id.deprecated = True
|
||||
self.assertTrue(self.spread.debit_account_id.deprecated)
|
||||
|
||||
self.assertTrue(self.spread.is_debit_account_deprecated)
|
||||
self.spread.compute_spread_board()
|
||||
|
||||
self.assertEqual(self.spread.unspread_amount, 1000.0)
|
||||
self.assertEqual(self.spread.unposted_amount, 1000.0)
|
||||
|
||||
def test_16_compute_spread_board_line_account_deprecated(self):
|
||||
self.spread.credit_account_id.deprecated = True
|
||||
self.assertTrue(self.spread.credit_account_id.deprecated)
|
||||
|
||||
self.assertTrue(self.spread.is_credit_account_deprecated)
|
||||
self.spread.compute_spread_board()
|
||||
|
||||
self.assertEqual(self.spread.unspread_amount, 1000.0)
|
||||
self.assertEqual(self.spread.unposted_amount, 1000.0)
|
||||
|
||||
def test_17_compute_spread_board_line_account_deprecated(self):
|
||||
self.spread.compute_spread_board()
|
||||
self.spread.debit_account_id.deprecated = True
|
||||
self.assertTrue(self.spread.debit_account_id.deprecated)
|
||||
|
||||
for line in self.spread.line_ids:
|
||||
self.assertFalse(line.move_id)
|
||||
with self.assertRaises(UserError):
|
||||
line.create_move()
|
||||
|
||||
self.assertEqual(self.spread.unspread_amount, 1000.0)
|
||||
self.assertEqual(self.spread.unposted_amount, 1000.0)
|
||||
|
||||
def test_18_supplier_invoice(self):
|
||||
# spread date set
|
||||
self.spread.write(
|
||||
{
|
||||
"period_number": 12,
|
||||
"period_type": "month",
|
||||
"spread_date": datetime.date(2017, 1, 7),
|
||||
}
|
||||
)
|
||||
self.spread_account.reconcile = True
|
||||
self.assertTrue(self.spread_account.reconcile)
|
||||
|
||||
self.spread.compute_spread_board()
|
||||
spread_lines = self.spread.line_ids
|
||||
self.assertEqual(len(spread_lines), 13)
|
||||
|
||||
for line in spread_lines:
|
||||
self.assertFalse(line.move_id)
|
||||
|
||||
spread_lines[0]._create_moves().action_post()
|
||||
spread_lines[1]._create_moves().action_post()
|
||||
spread_lines[2]._create_moves().action_post()
|
||||
spread_lines[3]._create_moves().action_post()
|
||||
|
||||
self.assertEqual(spread_lines[0].move_id.state, "posted")
|
||||
self.assertEqual(spread_lines[1].move_id.state, "posted")
|
||||
self.assertEqual(spread_lines[2].move_id.state, "posted")
|
||||
self.assertEqual(spread_lines[3].move_id.state, "posted")
|
||||
|
||||
self.assertAlmostEqual(self.spread.unspread_amount, 682.81)
|
||||
self.assertAlmostEqual(self.spread.unposted_amount, 682.81)
|
||||
|
||||
self.spread.compute_spread_board()
|
||||
spread_lines = self.spread.line_ids
|
||||
self.assertEqual(len(spread_lines), 13)
|
||||
|
||||
self.assertEqual(67.20, spread_lines[0].amount)
|
||||
self.assertEqual(83.33, spread_lines[1].amount)
|
||||
self.assertEqual(83.33, spread_lines[2].amount)
|
||||
self.assertEqual(83.33, spread_lines[3].amount)
|
||||
self.assertEqual(83.33, spread_lines[4].amount)
|
||||
self.assertEqual(83.33, spread_lines[5].amount)
|
||||
self.assertEqual(83.33, spread_lines[6].amount)
|
||||
self.assertEqual(83.33, spread_lines[7].amount)
|
||||
self.assertEqual(83.33, spread_lines[8].amount)
|
||||
self.assertEqual(83.33, spread_lines[9].amount)
|
||||
self.assertEqual(83.33, spread_lines[10].amount)
|
||||
self.assertEqual(83.33, spread_lines[11].amount)
|
||||
self.assertEqual(16.17, spread_lines[12].amount)
|
||||
|
||||
self.assertEqual(datetime.date(2017, 1, 31), spread_lines[0].date)
|
||||
self.assertEqual(datetime.date(2017, 2, 28), spread_lines[1].date)
|
||||
self.assertEqual(datetime.date(2017, 3, 31), spread_lines[2].date)
|
||||
self.assertEqual(datetime.date(2017, 4, 30), spread_lines[3].date)
|
||||
self.assertEqual(datetime.date(2017, 5, 31), spread_lines[4].date)
|
||||
self.assertEqual(datetime.date(2017, 6, 30), spread_lines[5].date)
|
||||
self.assertEqual(datetime.date(2017, 7, 31), spread_lines[6].date)
|
||||
self.assertEqual(datetime.date(2017, 8, 31), spread_lines[7].date)
|
||||
self.assertEqual(datetime.date(2017, 9, 30), spread_lines[8].date)
|
||||
self.assertEqual(datetime.date(2017, 10, 31), spread_lines[9].date)
|
||||
self.assertEqual(datetime.date(2017, 11, 30), spread_lines[10].date)
|
||||
self.assertEqual(datetime.date(2017, 12, 31), spread_lines[11].date)
|
||||
self.assertEqual(datetime.date(2018, 1, 31), spread_lines[12].date)
|
||||
|
||||
self.assertAlmostEqual(self.spread.unspread_amount, 682.81)
|
||||
self.assertAlmostEqual(self.spread.unposted_amount, 682.81)
|
||||
|
||||
def test_19_supplier_invoice_calc_day(self):
|
||||
self.assertTrue(self.spread3.days_calc)
|
||||
self.spread3.compute_spread_board()
|
||||
spread_lines = self.spread3.line_ids
|
||||
self.assertEqual(len(spread_lines), 12)
|
||||
# Calculate by day has formula:
|
||||
# (amount spread cost / all spread cost day) * day of <period_type>
|
||||
self.assertAlmostEqual(920.55, spread_lines[0].amount)
|
||||
self.assertAlmostEqual(1019.18, spread_lines[1].amount)
|
||||
self.assertAlmostEqual(986.30, spread_lines[2].amount)
|
||||
self.assertAlmostEqual(1019.18, spread_lines[3].amount)
|
||||
self.assertAlmostEqual(986.30, spread_lines[4].amount)
|
||||
self.assertAlmostEqual(1019.18, spread_lines[5].amount)
|
||||
self.assertAlmostEqual(1019.18, spread_lines[6].amount)
|
||||
self.assertAlmostEqual(986.30, spread_lines[7].amount)
|
||||
self.assertAlmostEqual(1019.18, spread_lines[8].amount)
|
||||
self.assertAlmostEqual(986.30, spread_lines[9].amount)
|
||||
self.assertAlmostEqual(1019.18, spread_lines[10].amount)
|
||||
self.assertAlmostEqual(1019.17, spread_lines[11].amount) # total left
|
||||
|
||||
self.assertEqual(datetime.date(2017, 2, 28), spread_lines[0].date)
|
||||
self.assertEqual(datetime.date(2017, 3, 31), spread_lines[1].date)
|
||||
self.assertEqual(datetime.date(2017, 4, 30), spread_lines[2].date)
|
||||
self.assertEqual(datetime.date(2017, 5, 31), spread_lines[3].date)
|
||||
self.assertEqual(datetime.date(2017, 6, 30), spread_lines[4].date)
|
||||
self.assertEqual(datetime.date(2017, 7, 31), spread_lines[5].date)
|
||||
self.assertEqual(datetime.date(2017, 8, 31), spread_lines[6].date)
|
||||
self.assertEqual(datetime.date(2017, 9, 30), spread_lines[7].date)
|
||||
self.assertEqual(datetime.date(2017, 10, 31), spread_lines[8].date)
|
||||
self.assertEqual(datetime.date(2017, 11, 30), spread_lines[9].date)
|
||||
self.assertEqual(datetime.date(2017, 12, 31), spread_lines[10].date)
|
||||
self.assertEqual(datetime.date(2018, 1, 31), spread_lines[11].date)
|
||||
|
||||
# Period Type is 'Quarter'
|
||||
self.spread3.period_type = "quarter"
|
||||
self.spread3.compute_spread_board()
|
||||
spread_lines = self.spread3.line_ids
|
||||
self.assertEqual(len(spread_lines), 12)
|
||||
self.assertAlmostEqual(325.27, spread_lines[0].amount)
|
||||
self.assertAlmostEqual(1068.73, spread_lines[1].amount)
|
||||
self.assertAlmostEqual(1068.73, spread_lines[2].amount)
|
||||
self.assertAlmostEqual(1057.12, spread_lines[3].amount)
|
||||
self.assertAlmostEqual(1045.50, spread_lines[4].amount)
|
||||
self.assertAlmostEqual(1068.73, spread_lines[5].amount)
|
||||
self.assertAlmostEqual(1068.73, spread_lines[6].amount)
|
||||
self.assertAlmostEqual(1057.12, spread_lines[7].amount)
|
||||
self.assertAlmostEqual(1045.50, spread_lines[8].amount)
|
||||
self.assertAlmostEqual(1068.73, spread_lines[9].amount)
|
||||
self.assertAlmostEqual(1068.73, spread_lines[10].amount)
|
||||
self.assertAlmostEqual(1057.11, spread_lines[11].amount) # total left
|
||||
|
||||
# Period Type is 'Year' and spread date is not first month
|
||||
self.spread3.period_type = "year"
|
||||
self.spread3.spread_date = "2017-02-02"
|
||||
self.spread3.compute_spread_board()
|
||||
spread_lines = self.spread3.line_ids
|
||||
self.assertEqual(len(spread_lines), 13)
|
||||
self.assertAlmostEqual(73.92, spread_lines[0].amount)
|
||||
self.assertAlmostEqual(999.32, spread_lines[1].amount)
|
||||
self.assertAlmostEqual(999.32, spread_lines[2].amount)
|
||||
self.assertAlmostEqual(1002.05, spread_lines[3].amount)
|
||||
self.assertAlmostEqual(999.32, spread_lines[4].amount)
|
||||
self.assertAlmostEqual(999.32, spread_lines[5].amount)
|
||||
self.assertAlmostEqual(999.32, spread_lines[6].amount)
|
||||
self.assertAlmostEqual(1002.05, spread_lines[7].amount)
|
||||
self.assertAlmostEqual(999.32, spread_lines[8].amount)
|
||||
self.assertAlmostEqual(999.32, spread_lines[9].amount)
|
||||
self.assertAlmostEqual(999.32, spread_lines[10].amount)
|
||||
self.assertAlmostEqual(1002.05, spread_lines[11].amount)
|
||||
self.assertAlmostEqual(925.37, spread_lines[12].amount)
|
||||
|
||||
def test_20_supplier_invoice_template(self):
|
||||
"""Test onchange template"""
|
||||
self.assertEqual(self.spread3.invoice_type, "out_invoice")
|
||||
with Form(self.spread3) as sp:
|
||||
sp.template_id = self.template
|
||||
sp.credit_account_id = self.expense_account
|
||||
sp.save()
|
||||
self.assertEqual(self.spread3.invoice_type, "in_invoice")
|
68
account_spread_cost_revenue/views/account_move.xml
Normal file
68
account_spread_cost_revenue/views/account_move.xml
Normal file
@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="view_invoice_spread" model="ir.ui.view">
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account.view_move_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath
|
||||
expr="//field[@name='invoice_line_ids']/tree/field[@name='quantity']"
|
||||
position="before"
|
||||
>
|
||||
<field
|
||||
name="spread_check"
|
||||
invisible="1"
|
||||
groups="account.group_account_user,account.group_account_manager"
|
||||
/>
|
||||
<button
|
||||
name="spread_details"
|
||||
type="object"
|
||||
class="btn btn-link"
|
||||
icon="fa-arrow-circle-right"
|
||||
title="Not linked to any spread board"
|
||||
attrs="{'invisible': [('spread_check', '!=', 'unlinked')]}"
|
||||
groups="account.group_account_user,account.group_account_manager"
|
||||
/>
|
||||
<button
|
||||
name="spread_details"
|
||||
type="object"
|
||||
class="btn btn-success"
|
||||
icon="fa-arrow-circle-right"
|
||||
title="Linked to spread board"
|
||||
attrs="{'invisible': [('spread_check', '!=', 'linked')]}"
|
||||
groups="account.group_account_user,account.group_account_manager"
|
||||
/>
|
||||
</xpath>
|
||||
<xpath
|
||||
expr="//field[@name='line_ids']/tree//button[@name='action_automatic_entry']"
|
||||
position="after"
|
||||
>
|
||||
<field
|
||||
name="spread_check"
|
||||
invisible="1"
|
||||
groups="account.group_account_user,account.group_account_manager"
|
||||
/>
|
||||
<button
|
||||
name="spread_details"
|
||||
type="object"
|
||||
class="btn btn-success"
|
||||
icon="fa-arrow-circle-right"
|
||||
title="Linked to spread board"
|
||||
groups="account.group_account_user,account.group_account_manager"
|
||||
attrs="{'invisible': [('spread_check', '!=', 'linked')], 'column_invisible': ['|', ('parent.move_type', '=', 'entry'), ('parent.state', '!=', 'posted')]}"
|
||||
/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_account_moves_all_spread" model="ir.actions.act_window">
|
||||
<field
|
||||
name="context"
|
||||
>{'journal_type':'general', 'search_default_group_by_move': 0, 'search_default_posted':1, 'name_groupby':1}</field>
|
||||
<field name="name">Journal Items</field>
|
||||
<field name="res_model">account.move.line</field>
|
||||
<field
|
||||
name="domain"
|
||||
>[('display_type', 'not in', ('line_section', 'line_note'))]</field>
|
||||
<field name="view_id" ref="account.view_move_line_tree" />
|
||||
<field name="view_mode">tree,pivot,graph,form,kanban</field>
|
||||
</record>
|
||||
</odoo>
|
386
account_spread_cost_revenue/views/account_spread.xml
Normal file
386
account_spread_cost_revenue/views/account_spread.xml
Normal file
@ -0,0 +1,386 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="view_account_spread" model="ir.ui.view">
|
||||
<field name="model">account.spread</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<button
|
||||
name="compute_spread_board"
|
||||
type="object"
|
||||
string="Recalculate unposted lines"
|
||||
class="oe_highlight"
|
||||
attrs="{'invisible': ['|',('debit_account_id', '=', False),('display_recompute_buttons', '=', False)]}"
|
||||
/>
|
||||
<button
|
||||
name="action_recalculate_spread"
|
||||
type="object"
|
||||
string="Recalculate entire spread"
|
||||
attrs="{'invisible': ['|',('debit_account_id', '=', False),('display_recompute_buttons', '=', False)]}"
|
||||
groups="account.group_account_manager"
|
||||
/>
|
||||
<button
|
||||
name="action_undo_spread"
|
||||
type="object"
|
||||
string="Undo spread"
|
||||
attrs="{'invisible': [('line_ids', '=', [])]}"
|
||||
groups="account.group_account_manager"
|
||||
/>
|
||||
<button
|
||||
name="action_unlink_invoice_line"
|
||||
type="object"
|
||||
string="Unlink Invoice Line"
|
||||
attrs="{'invisible': [('invoice_line_id', '=', False)]}"
|
||||
groups="account.group_account_manager"
|
||||
/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div name="button_box" class="oe_button_box">
|
||||
<button
|
||||
name="open_posted_view"
|
||||
class="oe_stat_button"
|
||||
icon="fa-bars"
|
||||
type="object"
|
||||
string="Posted entries"
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
<widget
|
||||
name="web_ribbon"
|
||||
title="Archived"
|
||||
bg_color="bg-danger"
|
||||
attrs="{'invisible': [('active', '=', True)]}"
|
||||
/>
|
||||
<div class="oe_title">
|
||||
<label for="name" string="Spread Board Name" />
|
||||
<h1>
|
||||
<field
|
||||
name="name"
|
||||
placeholder="e.g. One year offices cleaning contract"
|
||||
/>
|
||||
</h1>
|
||||
</div>
|
||||
<group name="header_info">
|
||||
<group name="spread_definitions">
|
||||
<field name="template_id" />
|
||||
<field
|
||||
name="invoice_type"
|
||||
attrs="{'readonly':[('invoice_line_id','!=',False)]}"
|
||||
/>
|
||||
<field name="display_recompute_buttons" invisible="1" />
|
||||
<field name="display_move_line_auto_post" invisible="1" />
|
||||
<field name="all_posted" invisible="1" />
|
||||
<field name="active" invisible="1" />
|
||||
<field name="use_invoice_line_account" invisible="1" />
|
||||
</group>
|
||||
</group>
|
||||
<group name="accounts">
|
||||
<group name="debits">
|
||||
<field name="is_debit_account_deprecated" invisible="1" />
|
||||
<label
|
||||
for="debit_account_id"
|
||||
colspan="3"
|
||||
string="Balance sheet account / Spread account"
|
||||
attrs="{'invisible':[('invoice_type','not in',('out_invoice','in_refund'))]}"
|
||||
/>
|
||||
<label
|
||||
for="debit_account_id"
|
||||
colspan="3"
|
||||
string="Expense account"
|
||||
attrs="{'invisible':[('invoice_type','not in',('in_invoice','out_refund'))]}"
|
||||
/>
|
||||
<div
|
||||
attrs="{'invisible': [('use_invoice_line_account', '=', True)]}"
|
||||
colspan="3"
|
||||
>
|
||||
<span
|
||||
class="help-block"
|
||||
colspan="2"
|
||||
attrs="{'invisible':[('invoice_type','not in',('out_invoice','in_refund'))]}"
|
||||
>
|
||||
The Balance Sheet account used for the spreading.<br
|
||||
/>This account is the counterpart of the account in the invoice line.
|
||||
</span>
|
||||
<span
|
||||
class="help-block"
|
||||
colspan="2"
|
||||
attrs="{'invisible':[('invoice_type','not in',('in_invoice','out_refund'))]}"
|
||||
>
|
||||
The Expense account in the vendor bill line.<br
|
||||
/>Usually the same account of the vendor bill line.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
attrs="{'invisible': [('use_invoice_line_account', '!=', True)]}"
|
||||
colspan="3"
|
||||
>
|
||||
<span
|
||||
class="help-block"
|
||||
colspan="2"
|
||||
attrs="{'invisible':[('invoice_type','not in',('out_invoice','in_refund'))]}"
|
||||
>
|
||||
The Balance Sheet account.<br
|
||||
/>This is the account in the invoice line.
|
||||
</span>
|
||||
<span
|
||||
class="help-block"
|
||||
colspan="2"
|
||||
attrs="{'invisible':[('invoice_type','not in',('in_invoice','out_refund'))]}"
|
||||
>
|
||||
The Expense account used for the spreading.<br
|
||||
/>This account is the counterpart of the account of the vendor bill line.
|
||||
</span>
|
||||
</div>
|
||||
<field
|
||||
name="debit_account_id"
|
||||
required="1"
|
||||
domain="[('deprecated', '=', False), ('account_type', 'not in', ('asset_cash','liability_credit_card')), ('internal_group', '!=', 'off_balance')]"
|
||||
attrs="{'readonly':[('invoice_line_id','!=',False)]}"
|
||||
/>
|
||||
<span
|
||||
class="help-block text-danger"
|
||||
colspan="2"
|
||||
attrs="{'invisible':[('is_debit_account_deprecated','!=',True)]}"
|
||||
>
|
||||
This account in deprecated! The reconciliation will be NOT possible.
|
||||
</span>
|
||||
</group>
|
||||
<group name="credits">
|
||||
<field name="is_credit_account_deprecated" invisible="1" />
|
||||
<label
|
||||
for="credit_account_id"
|
||||
colspan="3"
|
||||
string="Revenue account"
|
||||
attrs="{'invisible':[('invoice_type','not in',('out_invoice','in_refund'))]}"
|
||||
/>
|
||||
<label
|
||||
for="credit_account_id"
|
||||
colspan="3"
|
||||
string="Balance sheet account / Spread account"
|
||||
attrs="{'invisible':[('invoice_type','not in',('in_invoice','out_refund'))]}"
|
||||
/>
|
||||
<div
|
||||
attrs="{'invisible': [('use_invoice_line_account', '=', True)]}"
|
||||
colspan="3"
|
||||
>
|
||||
<span
|
||||
class="help-block"
|
||||
colspan="2"
|
||||
attrs="{'invisible':[('invoice_type','not in',('out_invoice','in_refund'))]}"
|
||||
>
|
||||
The Revenue account in the invoice line.<br
|
||||
/>Usually the same account of the invoice line.
|
||||
</span>
|
||||
<span
|
||||
class="help-block"
|
||||
colspan="2"
|
||||
attrs="{'invisible':[('invoice_type','not in',('in_invoice','out_refund'))]}"
|
||||
>
|
||||
The Balance Sheet account used for the spreading.<br
|
||||
/>This account is the counterpart of the account in the vendor bill line.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
attrs="{'invisible': [('use_invoice_line_account', '!=', True)]}"
|
||||
colspan="3"
|
||||
>
|
||||
<span
|
||||
class="help-block"
|
||||
colspan="2"
|
||||
attrs="{'invisible':[('invoice_type','not in',('out_invoice','in_refund'))]}"
|
||||
>
|
||||
The Revenue account used for the spreading.<br
|
||||
/>This account is the counterpart of the account of the invoice line.
|
||||
</span>
|
||||
<span
|
||||
class="help-block"
|
||||
colspan="2"
|
||||
attrs="{'invisible':[('invoice_type','not in',('in_invoice','out_refund'))]}"
|
||||
>
|
||||
The Balance Sheet account.<br
|
||||
/>This is the account in the vendor bill line.
|
||||
</span>
|
||||
</div>
|
||||
<field
|
||||
name="credit_account_id"
|
||||
required="1"
|
||||
domain="[('deprecated', '=', False), ('account_type', 'not in', ('asset_cash','liability_credit_card')), ('internal_group', '!=', 'off_balance')]"
|
||||
attrs="{'readonly':[('invoice_line_id','!=',False)]}"
|
||||
/>
|
||||
<span
|
||||
class="help-block text-danger"
|
||||
colspan="2"
|
||||
attrs="{'invisible':[('is_credit_account_deprecated','!=',True)]}"
|
||||
>
|
||||
This account in deprecated! The reconciliation will be NOT possible.
|
||||
</span>
|
||||
</group>
|
||||
</group>
|
||||
<group name="main_info">
|
||||
<group>
|
||||
<field
|
||||
name="invoice_id"
|
||||
attrs="{'invisible':[('invoice_id','=',False)]}"
|
||||
/>
|
||||
<field
|
||||
name="invoice_line_id"
|
||||
readonly="1"
|
||||
attrs="{'invisible':[('invoice_line_id','=',False)]}"
|
||||
/>
|
||||
<field
|
||||
name="estimated_amount"
|
||||
attrs="{'readonly':[('invoice_line_id','!=',False)],'invisible':[('estimated_amount','=',0.0),('invoice_line_id','!=',False)]}"
|
||||
/>
|
||||
<field
|
||||
name="total_amount"
|
||||
attrs="{'invisible':[('invoice_line_id','=',False)]}"
|
||||
/>
|
||||
<field
|
||||
name="move_line_auto_post"
|
||||
attrs="{'invisible':[('display_move_line_auto_post','=',False)]}"
|
||||
/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="period_number" />
|
||||
<field name="period_type" />
|
||||
<field name="spread_date" />
|
||||
<field name="days_calc" />
|
||||
<field name="suitable_journal_ids" invisible="1" />
|
||||
<field name="journal_id" widget="selection" />
|
||||
</group>
|
||||
</group>
|
||||
<notebook name="notebook">
|
||||
<page name="spread_lines" string="Spread lines">
|
||||
<field name="line_ids" readonly="1">
|
||||
<tree>
|
||||
<field name="name" readonly="1" />
|
||||
<field
|
||||
name="amount"
|
||||
attrs="{'readonly':[('move_id','!=',False)]}"
|
||||
sum="Total"
|
||||
/>
|
||||
<field name="date" readonly="1" />
|
||||
<field name="move_id" readonly="1" />
|
||||
<button
|
||||
name="create_move"
|
||||
icon="fa-play"
|
||||
string="Create Move"
|
||||
type="object"
|
||||
groups="account.group_account_manager"
|
||||
attrs="{'invisible':[('move_id','!=',False)]}"
|
||||
/>
|
||||
<button
|
||||
name="open_move"
|
||||
icon="fa-plus-square-o"
|
||||
string="View Move"
|
||||
type="object"
|
||||
attrs="{'invisible':[('move_id','=',False)]}"
|
||||
/>
|
||||
<button
|
||||
name="unlink_move"
|
||||
icon="fa-times"
|
||||
string="Delete Move"
|
||||
type="object"
|
||||
confirm="This will delete the move. Are you sure ?"
|
||||
groups="account.group_account_manager"
|
||||
attrs="{'invisible':[('move_id','=',False)]}"
|
||||
/>
|
||||
</tree>
|
||||
</field>
|
||||
<group name="extension">
|
||||
<group name="extension_left">
|
||||
</group>
|
||||
<group name="extension_right">
|
||||
<field
|
||||
name="display_create_all_moves"
|
||||
invisible="1"
|
||||
/>
|
||||
<button
|
||||
name="create_all_moves"
|
||||
string="Create All Moves"
|
||||
type="object"
|
||||
icon="fa-play"
|
||||
colspan="2"
|
||||
attrs="{'invisible':[('display_create_all_moves','!=',True)]}"
|
||||
/>
|
||||
<field
|
||||
name="unspread_amount"
|
||||
attrs="{'invisible': [('unspread_amount', '=', 0)]}"
|
||||
/>
|
||||
<field
|
||||
name="unposted_amount"
|
||||
attrs="{'invisible': [('unposted_amount', '=', 0)]}"
|
||||
/>
|
||||
<field
|
||||
name="posted_amount"
|
||||
attrs="{'invisible': [('posted_amount', '=', 0)]}"
|
||||
/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
<page name="details" string="Details">
|
||||
<group name="extra_details">
|
||||
<group>
|
||||
<field
|
||||
name="company_id"
|
||||
groups="base.group_multi_company"
|
||||
options="{'no_create': True}"
|
||||
/>
|
||||
<field
|
||||
name="currency_id"
|
||||
groups="base.group_multi_currency"
|
||||
/>
|
||||
</group>
|
||||
<group>
|
||||
<field
|
||||
name="analytic_distribution"
|
||||
groups="analytic.group_analytic_accounting"
|
||||
widget="analytic_distribution"
|
||||
/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" widget="mail_followers" />
|
||||
<field name="message_ids" widget="mail_thread" />
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_account_spread_tree" model="ir.ui.view">
|
||||
<field name="model">account.spread</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name" />
|
||||
<field name="company_id" groups="base.group_multi_company" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_account_spread_search" model="ir.ui.view">
|
||||
<field name="model">account.spread</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name" string="Spread" />
|
||||
<filter
|
||||
string="Archived"
|
||||
name="inactive"
|
||||
domain="[('active','=',False)]"
|
||||
/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_account_spread_form" model="ir.actions.act_window">
|
||||
<field name="name">Spread Costs/Revenues</field>
|
||||
<field name="res_model">account.spread</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="view_account_spread_tree" />
|
||||
</record>
|
||||
<menuitem
|
||||
id="menu_action_account_spread_form"
|
||||
parent="account.menu_finance_entries_accounting_miscellaneous"
|
||||
action="action_account_spread_form"
|
||||
groups="account.group_account_user,account.group_account_manager"
|
||||
/>
|
||||
</odoo>
|
@ -0,0 +1,98 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="view_account_spread_template" model="ir.ui.view">
|
||||
<field name="model">account.spread.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name" string="Spread Template Name" />
|
||||
<h1>
|
||||
<field
|
||||
name="name"
|
||||
placeholder="e.g. Template cleaning contract"
|
||||
/>
|
||||
</h1>
|
||||
</div>
|
||||
<group name="main_info">
|
||||
<group>
|
||||
<field name="spread_type" />
|
||||
<field
|
||||
name="company_id"
|
||||
groups="base.group_multi_company"
|
||||
/>
|
||||
<field name="period_number" />
|
||||
<field name="period_type" />
|
||||
<field name="start_date" />
|
||||
<field name="days_calc" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="use_invoice_line_account" />
|
||||
<field
|
||||
name="spread_account_id"
|
||||
domain="[('deprecated', '=', False), ('account_type', 'not in', ('asset_cash','liability_credit_card')), ('internal_group', '!=', 'off_balance')]"
|
||||
options="{'no_create': True}"
|
||||
attrs="{'required': [('use_invoice_line_account', '!=', True)], 'invisible': [('use_invoice_line_account', '=', True)]}"
|
||||
/>
|
||||
<field
|
||||
name="exp_rev_account_id"
|
||||
attrs="{'invisible': [('use_invoice_line_account', '=', False)], 'required': [('use_invoice_line_account', '=', True)]}"
|
||||
domain="[('deprecated', '=', False)]"
|
||||
options="{'no_create': True}"
|
||||
/>
|
||||
<field name="spread_journal_id" widget="selection" />
|
||||
<field
|
||||
name="analytic_distribution"
|
||||
groups="analytic.group_analytic_accounting"
|
||||
widget="analytic_distribution"
|
||||
/>
|
||||
</group>
|
||||
</group>
|
||||
<div>
|
||||
<field name="auto_spread" />
|
||||
<label for="auto_spread" />
|
||||
</div>
|
||||
<p attrs="{'invisible': [('auto_spread', '!=', True)]}">
|
||||
Automatically use this spread template on invoice validation for invoice lines using below product and/or account and/or analytic,
|
||||
</p>
|
||||
<field
|
||||
name="auto_spread_ids"
|
||||
attrs="{'invisible': [('auto_spread', '!=', True)]}"
|
||||
nolabel="1"
|
||||
>
|
||||
<tree editable="bottom">
|
||||
<field name="name" />
|
||||
<field name="product_id" />
|
||||
<field name="account_id" />
|
||||
</tree>
|
||||
</field>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_account_spread_template_tree" model="ir.ui.view">
|
||||
<field name="model">account.spread.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name" />
|
||||
<field name="company_id" groups="base.group_multi_company" />
|
||||
<field name="spread_account_id" />
|
||||
<field name="spread_journal_id" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_account_spread_template_form" model="ir.actions.act_window">
|
||||
<field name="name">Spread Templates</field>
|
||||
<field name="res_model">account.spread.template</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="view_account_spread_template_tree" />
|
||||
</record>
|
||||
<menuitem
|
||||
id="menu_action_account_spread_template_form"
|
||||
parent="account.account_account_menu"
|
||||
action="action_account_spread_template_form"
|
||||
groups="account.group_account_manager"
|
||||
/>
|
||||
</odoo>
|
42
account_spread_cost_revenue/views/res_company.xml
Normal file
42
account_spread_cost_revenue/views/res_company.xml
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="view_company_form" model="ir.ui.view">
|
||||
<field name="model">res.company</field>
|
||||
<field name="inherit_id" ref="base.view_company_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//notebook">
|
||||
<page
|
||||
name="account_spread_cost_revenue"
|
||||
string="Account Spread"
|
||||
groups="account.group_account_manager"
|
||||
>
|
||||
<group>
|
||||
<group
|
||||
string="Default Spread Accounts"
|
||||
name="default_spread_accounts"
|
||||
>
|
||||
<field name="default_spread_revenue_account_id" />
|
||||
<field name="default_spread_expense_account_id" />
|
||||
</group>
|
||||
<group
|
||||
string="Default Spread Journals"
|
||||
name="default_spread_journals"
|
||||
>
|
||||
<field name="default_spread_revenue_journal_id" />
|
||||
<field name="default_spread_expense_journal_id" />
|
||||
</group>
|
||||
</group>
|
||||
<group name="spreading_options">
|
||||
<group name="spreading_options_left">
|
||||
<field name="allow_spread_planning" />
|
||||
</group>
|
||||
<group name="spreading_options_right">
|
||||
<field name="force_move_auto_post" />
|
||||
<field name="auto_archive_spread" />
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
3
account_spread_cost_revenue/wizards/__init__.py
Normal file
3
account_spread_cost_revenue/wizards/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import account_spread_invoice_line_link_wizard
|
@ -0,0 +1,231 @@
|
||||
# Copyright 2018-2020 Onestein (<https://www.onestein.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
class AccountSpreadInvoiceLineLinkWizard(models.TransientModel):
|
||||
_name = "account.spread.invoice.line.link.wizard"
|
||||
_description = "Account Spread Invoice Line Link Wizard"
|
||||
|
||||
def _selection_spread_action_type(self):
|
||||
base_selection = [
|
||||
("template", _("Create from spread template")),
|
||||
("new", _("Create new spread board")),
|
||||
]
|
||||
if not self.env.context.get("allow_spread_planning"):
|
||||
return base_selection
|
||||
|
||||
link_selection = [
|
||||
("link", _("Link to existing spread board")),
|
||||
]
|
||||
return link_selection + base_selection
|
||||
|
||||
def _selection_default_spread_action_type(self):
|
||||
if not self.env.context.get("allow_spread_planning"):
|
||||
return "template"
|
||||
return "link"
|
||||
|
||||
invoice_line_id = fields.Many2one(
|
||||
"account.move.line", readonly=True, required=True, ondelete="cascade"
|
||||
)
|
||||
invoice_id = fields.Many2one(related="invoice_line_id.move_id", readonly=True)
|
||||
invoice_type = fields.Selection(
|
||||
[
|
||||
("out_invoice", "Customer Invoice"),
|
||||
("in_invoice", "Vendor Bill"),
|
||||
("out_refund", "Customer Credit Note"),
|
||||
("in_refund", "Vendor Credit Note"),
|
||||
],
|
||||
compute="_compute_invoice_type",
|
||||
store=True,
|
||||
)
|
||||
spread_type = fields.Selection(
|
||||
[("sale", "Customer"), ("purchase", "Supplier")],
|
||||
compute="_compute_invoice_type",
|
||||
store=True,
|
||||
)
|
||||
spread_invoice_type_domain_ids = fields.One2many(
|
||||
"account.spread",
|
||||
compute="_compute_spread_invoice_type_domain",
|
||||
)
|
||||
spread_id = fields.Many2one(
|
||||
"account.spread",
|
||||
string="Spread Board",
|
||||
domain="[('id', 'in', spread_invoice_type_domain_ids)]",
|
||||
)
|
||||
company_id = fields.Many2one("res.company", required=True)
|
||||
spread_action_type = fields.Selection(
|
||||
selection=_selection_spread_action_type,
|
||||
default=_selection_default_spread_action_type,
|
||||
)
|
||||
template_id = fields.Many2one("account.spread.template", string="Spread Template")
|
||||
use_invoice_line_account = fields.Boolean(
|
||||
help="Use invoice line's account as Balance sheet / spread account.\n"
|
||||
"In this case, user need to select expense/revenue account too.",
|
||||
)
|
||||
spread_account_id = fields.Many2one(
|
||||
"account.account",
|
||||
string="Balance sheet account / Spread account",
|
||||
compute="_compute_spread_account",
|
||||
readonly=False,
|
||||
store=True,
|
||||
)
|
||||
exp_rev_account_id = fields.Many2one(
|
||||
"account.account",
|
||||
string="Expense/revenue account",
|
||||
help="Optional account to overwrite the existing expense/revenue account",
|
||||
)
|
||||
spread_journal_id = fields.Many2one(
|
||||
"account.journal",
|
||||
string="Spread Journal",
|
||||
compute="_compute_spread_journal",
|
||||
readonly=False,
|
||||
store=True,
|
||||
)
|
||||
|
||||
@api.depends("invoice_line_id")
|
||||
def _compute_invoice_type(self):
|
||||
for wizard in self:
|
||||
invoice = wizard.invoice_line_id.move_id
|
||||
wizard.invoice_type = invoice.move_type
|
||||
if invoice.is_sale_document(include_receipts=True):
|
||||
wizard.spread_type = "sale"
|
||||
else:
|
||||
wizard.spread_type = "purchase"
|
||||
|
||||
@api.depends("invoice_type", "company_id")
|
||||
def _compute_spread_journal(self):
|
||||
for wizard in self:
|
||||
journal_revenue = wizard.company_id.default_spread_revenue_journal_id
|
||||
journal_expense = wizard.company_id.default_spread_expense_journal_id
|
||||
if wizard.invoice_type in ("out_invoice", "in_refund"):
|
||||
wizard.spread_journal_id = journal_revenue
|
||||
else:
|
||||
wizard.spread_journal_id = journal_expense
|
||||
|
||||
@api.depends("invoice_type", "company_id")
|
||||
def _compute_spread_account(self):
|
||||
for wizard in self:
|
||||
acc_revenue = wizard.company_id.default_spread_revenue_account_id
|
||||
acc_expense = wizard.company_id.default_spread_expense_account_id
|
||||
if wizard.invoice_type in ("out_invoice", "in_refund"):
|
||||
wizard.spread_account_id = acc_revenue
|
||||
else:
|
||||
wizard.spread_account_id = acc_expense
|
||||
|
||||
def _inverse_spread_journal_account(self):
|
||||
"""Keep this for making the fields editable"""
|
||||
|
||||
@api.depends("company_id", "invoice_type")
|
||||
def _compute_spread_invoice_type_domain(self):
|
||||
for wizard in self:
|
||||
spreads = self.env["account.spread"].search(
|
||||
[
|
||||
("invoice_id", "=", False),
|
||||
("invoice_type", "=", wizard.invoice_type),
|
||||
("company_id", "=", wizard.company_id.id),
|
||||
]
|
||||
)
|
||||
wizard.spread_invoice_type_domain_ids = spreads
|
||||
|
||||
@api.onchange("use_invoice_line_account")
|
||||
def _onchange_user_invoice_line_account(self):
|
||||
self.spread_account_id = (
|
||||
self.use_invoice_line_account and self.invoice_line_id.account_id or False
|
||||
)
|
||||
self.exp_rev_account_id = False
|
||||
|
||||
def confirm(self):
|
||||
self.ensure_one()
|
||||
|
||||
if self.spread_action_type == "link":
|
||||
if not self.invoice_line_id.spread_id:
|
||||
self.invoice_line_id.spread_id = self.spread_id
|
||||
|
||||
return {
|
||||
"name": _("Spread Details"),
|
||||
"view_mode": "form",
|
||||
"res_model": "account.spread",
|
||||
"type": "ir.actions.act_window",
|
||||
"target": "current",
|
||||
"readonly": False,
|
||||
"res_id": self.invoice_line_id.spread_id.id,
|
||||
}
|
||||
elif self.spread_action_type == "new":
|
||||
debit_account = credit_account = self.spread_account_id
|
||||
if self.invoice_type in ("out_invoice", "in_refund"):
|
||||
credit_account = (
|
||||
self.exp_rev_account_id or self.invoice_line_id.account_id
|
||||
)
|
||||
else:
|
||||
debit_account = (
|
||||
self.exp_rev_account_id or self.invoice_line_id.account_id
|
||||
)
|
||||
|
||||
analytic_distribution = self.invoice_line_id.analytic_distribution
|
||||
date_invoice = self.invoice_id.invoice_date or fields.Date.today()
|
||||
|
||||
return {
|
||||
"name": _("New Spread Board"),
|
||||
"view_type": "form",
|
||||
"view_mode": "form",
|
||||
"res_model": "account.spread",
|
||||
"type": "ir.actions.act_window",
|
||||
"target": "current",
|
||||
"readonly": False,
|
||||
"context": {
|
||||
"default_name": self.invoice_line_id.name,
|
||||
"default_invoice_type": self.invoice_type,
|
||||
"default_invoice_line_id": self.invoice_line_id.id,
|
||||
"default_use_invoice_line_account": self.use_invoice_line_account,
|
||||
"default_debit_account_id": debit_account.id,
|
||||
"default_credit_account_id": credit_account.id,
|
||||
"default_journal_id": self.spread_journal_id.id,
|
||||
"default_analytic_distribution": analytic_distribution,
|
||||
"default_spread_date": date_invoice,
|
||||
},
|
||||
}
|
||||
elif self.spread_action_type == "template":
|
||||
if not self.invoice_line_id.spread_id:
|
||||
account = self.invoice_line_id.account_id
|
||||
spread_account_id = False
|
||||
if self.template_id.use_invoice_line_account:
|
||||
account = self.template_id.exp_rev_account_id
|
||||
spread_account_id = self.invoice_line_id.account_id.id
|
||||
|
||||
spread_vals = self.template_id._prepare_spread_from_template(
|
||||
spread_account_id=spread_account_id
|
||||
)
|
||||
date_invoice = self.invoice_id.invoice_date
|
||||
date_invoice = date_invoice or self.template_id.start_date
|
||||
date_invoice = date_invoice or fields.Date.today()
|
||||
spread_vals["spread_date"] = date_invoice
|
||||
|
||||
spread_vals["name"] = ("%s %s") % (
|
||||
spread_vals["name"],
|
||||
self.invoice_line_id.name,
|
||||
)
|
||||
|
||||
if spread_vals["invoice_type"] == "out_invoice":
|
||||
spread_vals["credit_account_id"] = account.id
|
||||
else:
|
||||
spread_vals["debit_account_id"] = account.id
|
||||
|
||||
analytic_distribution = self.invoice_line_id.analytic_distribution
|
||||
spread_vals["analytic_distribution"] = analytic_distribution
|
||||
|
||||
spread_vals["currency_id"] = self.invoice_id.currency_id.id
|
||||
|
||||
spread = self.env["account.spread"].create(spread_vals)
|
||||
|
||||
self.invoice_line_id.spread_id = spread
|
||||
return {
|
||||
"name": _("Spread Details"),
|
||||
"view_mode": "form",
|
||||
"res_model": "account.spread",
|
||||
"type": "ir.actions.act_window",
|
||||
"target": "current",
|
||||
"readonly": False,
|
||||
"res_id": self.invoice_line_id.spread_id.id,
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="view_account_spread_invoice_line_link_wizard" model="ir.ui.view">
|
||||
<field name="model">account.spread.invoice.line.link.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group name="main_info">
|
||||
<group>
|
||||
<field
|
||||
name="company_id"
|
||||
readonly="1"
|
||||
groups="base.group_multi_company"
|
||||
/>
|
||||
<field name="invoice_type" readonly="1" />
|
||||
<field name="spread_type" invisible="1" />
|
||||
<field name="invoice_id" readonly="1" />
|
||||
<field name="invoice_line_id" readonly="1" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="spread_action_type" widget="radio" />
|
||||
<field
|
||||
name="spread_id"
|
||||
attrs="{'invisible': [('spread_action_type', '!=', 'link')],'required': [('spread_action_type', '=', 'link')]}"
|
||||
domain="[('invoice_type', '=', invoice_type)]"
|
||||
options="{'no_create': True}"
|
||||
/>
|
||||
<field
|
||||
name="template_id"
|
||||
attrs="{'invisible': [('spread_action_type', '!=', 'template')],'required': [('spread_action_type', '=', 'template')]}"
|
||||
domain="[('spread_type', '=', spread_type)]"
|
||||
options="{'no_create': True}"
|
||||
/>
|
||||
<field
|
||||
name="use_invoice_line_account"
|
||||
attrs="{'invisible': [('spread_action_type', '!=', 'new')]}"
|
||||
/>
|
||||
<field
|
||||
name="spread_account_id"
|
||||
attrs="{'invisible': [('spread_action_type', '!=', 'new')],'required': [('spread_action_type', '=', 'new')]}"
|
||||
domain="[('deprecated', '=', False), ('account_type', 'not in', ('asset_cash','liability_credit_card')), ('internal_group', '!=', 'off_balance')]"
|
||||
options="{'no_create': True}"
|
||||
/>
|
||||
<field
|
||||
name="exp_rev_account_id"
|
||||
attrs="{'invisible': ['|', ('use_invoice_line_account', '=', False), ('spread_action_type', '!=', 'new')], 'required': [('use_invoice_line_account', '=', True)]}"
|
||||
domain="[('deprecated', '=', False)]"
|
||||
options="{'no_create': True}"
|
||||
/>
|
||||
<field
|
||||
name="spread_journal_id"
|
||||
attrs="{'invisible': [('spread_action_type', '!=', 'new')],'required': [('spread_action_type', '=', 'new')]}"
|
||||
options="{'no_create': True}"
|
||||
/>
|
||||
</group>
|
||||
</group>
|
||||
<footer>
|
||||
<button
|
||||
string="Confirm"
|
||||
type="object"
|
||||
name="confirm"
|
||||
class="btn-primary"
|
||||
/>
|
||||
<button string="Cancel" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
@ -0,0 +1 @@
|
||||
../../../../account_spread_cost_revenue
|
6
setup/account_spread_cost_revenue/setup.py
Normal file
6
setup/account_spread_cost_revenue/setup.py
Normal file
@ -0,0 +1,6 @@
|
||||
import setuptools
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['setuptools-odoo'],
|
||||
odoo_addon=True,
|
||||
)
|
Loading…
Reference in New Issue
Block a user