commit
253b117b97
|
@ -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.
|
|
@ -0,0 +1,4 @@
|
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import models
|
||||
from . import wizards
|
|
@ -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,
|
||||
}
|
|
@ -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>
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -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
|
|
@ -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()
|
|
@ -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()
|
|
@ -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
|
|
@ -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})
|
|
@ -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",
|
||||
)
|
|
@ -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.",
|
||||
)
|
|
@ -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.
|
|
@ -0,0 +1,3 @@
|
|||
* Andrea Stirpe <a.stirpe@onestein.nl>
|
||||
* Kitti U. <kittiu@ecosoft.co.th>
|
||||
* Saran Lim. <saranl@ecosoft.co.th>
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
|||
Allows to spread costs or revenues over a customizable periods, to even out cost or invoice spikes.
|
|
@ -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>`_)
|
|
@ -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>
|
|
@ -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
|
|
Binary file not shown.
After Width: | Height: | Size: 60 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.6 KiB |
|
@ -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 |
Binary file not shown.
After Width: | Height: | Size: 50 KiB |
Binary file not shown.
After Width: | Height: | Size: 65 KiB |
|
@ -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)
|
|
@ -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)
|
|
@ -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")
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
import setuptools
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['setuptools-odoo'],
|
||||
odoo_addon=True,
|
||||
)
|
Loading…
Reference in New Issue