2
0

Merge PR #1740 into 16.0

Signed-off-by kittiu
This commit is contained in:
OCA-git-bot 2024-03-06 07:24:43 +00:00
commit 253b117b97
44 changed files with 9441 additions and 0 deletions

View File

@ -0,0 +1,229 @@
===================
Cost-Revenue Spread
===================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:560d87ede92fd18ca4a929595b6a9fd9f558f6b577e8c9bad1f8e7236ad4175d
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--financial--tools-lightgray.png?logo=github
:target: https://github.com/OCA/account-financial-tools/tree/16.0/account_spread_cost_revenue
:alt: OCA/account-financial-tools
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/account-financial-tools-16-0/account-financial-tools-16-0-account_spread_cost_revenue
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/account-financial-tools&target_branch=16.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
Allows to spread costs or revenues over a customizable periods, to even out cost or invoice spikes.
**Table of contents**
.. contents::
:local:
Configuration
=============
To be able to access the full spreading features, the user must belong to *Show Full Accounting Features* group.
On the form view of the company, in the *Account Spread* tab, you can configure
the journals in which the spread journal items will be generated by default:
* the *Default Spread Journal for Revenues*,
* the *Default Spread Journal for Expenses*.
In the same *Account Spread* tab, you can also configure the Spread Balance Sheet Accounts used by default:
* the *Default Spread Account for Revenues*,
* the *Default Spread Account for Expenses*.
This module by default allows the spreading even before the receipt of the invoice or when the invoice is still draft,
so that it is possible to work on the plan of the cost/revenue spreading. To disable this feature, on the form view of
the company disable the *Allow Spread Planning* option.
In Spread Template, there is also option to *Auto assign template on invoice validate*, based on the preset invoice line criteria.
On the form view of the company, the *Auto-post spread lines* option forces the account moves created
during the cost/revenue spreading to be automatically posted. When this option is false, the user can
enable/disable the automatic posting by the flag *Auto-post lines* present in the spread board.
On the form view of the company, enable the *Auto-archive spread* option if you want the
cron job to automatically archive the spreads when all lines are posted.
Usage
=====
Define Spread Costs/Revenues Board
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Under Invoicing -> Accounting -> Journals -> Spread Costs/Revenues, create a new spread board.
Complete the definition of the spreading criteria, by setting the the fields:
* *Debit Account*
* *Credit Account*
* *Estimated Amount* (The total amount to spread)
* *Number of Repetitions*
* *Period Type* (Duration of each period)
* *Start date*
* *Journal*
.. figure:: https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/spread.png
:alt: Create a new spread board
Click on the "Recalculate unposted lines" button on the top-left to calculate the spread lines.
.. figure:: https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/create_spread.png
:alt: The spreading board is defined
A cron job will automatically create the accounting moves for all the lines having date previous that the current day (today).
.. figure:: https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/update_spread.png
:alt: The spreading board is updated by the cron job
By default, the status of the created accounting moves is posted.
To disable the automatic posting of the accounting moves, set the flag *Auto-post lines* to False.
This flag is only available when the *Auto-post spread lines* option, present on the form view of the company, is disabled.
Click on button *Recalculate entire spread* button in the spread board to force the recalculation of the spread lines:
this will also reset all the journal entries previously created.
Link Invoice to Spread Costs/Revenues Board
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Create an invoice or vendor bill in draft. On its lines, the spreading right-arrow icon are displayed in dark-grey color.
.. figure:: https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/invoice_line_1.png
:alt: On the invoice line the spreading icon is displayed
Click on the spreading right-arrow icon. A wizard prompts to enter a *Spread Action Type*:
- *Link to existing spread board*
- *Create from spread template*
- *Create new spread board*
Select *Link to existing spread board* and enter the previously generated Spread Board. Click on Confirm button:
the selected Spread Board will be automatically displayed.
Go back to the draft invoice/bill. The spreading functionality is now enabled on the invoice line:
the spreading right-arrow icon is now displayed in green color.
.. figure:: https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/invoice_line_2.png
:alt: On the invoice line the spreading icon is displayed in green color
Validate the invoice/bill. Click on the spreading (green) right-arrow icon to open the spread board, then click
on the smart button *Posted entries* to see the moves of the spread lines together with the move of the invoice line.
In case the Subtotal Price of the invoice line is different than the *Estimated Amount* of the spread board, the spread
lines (not yet posted) will be recalculated when validating the invoice/bill.
Define Spread Costs/Revenues Template
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Under Invoicing -> Configuration -> Accounting -> Spread Templates, create a new spread template.
* *Spread Type*
* *Spread Balance Sheet Account*
* *Expense/Revenue Account* This option visible if invoice line account is balance sheet account, user need to specify this too.
* *Journal*
* *Auto assign template on invoice validate*
When creating a new Spread Costs/Revenues Board, select the right template.
This way the above fields will be copied to the Spread Board.
If *Auto assign template on invoice validate* is checked, this template will be used to auto create spread, if the underlining invoice match the preset product/account/analytic criteria.
Changelog
=========
13.0.1.0.0
~~~~~~~~~~
* [MIG] Port account_spread_cost_revenue to V13.
12.0.2.0.0
~~~~~~~~~~
* [ENH] In spread template, add option to auto create spread on invoice validation
12.0.1.1.0
~~~~~~~~~~
* [ENH] Add optional Expense/Revenue Account in Chart Template, which can be used
in place of account from invoice line to set Expense/Revenue account in the spread
12.0.1.0.0
~~~~~~~~~~
* [MIG] Port account_spread_cost_revenue to V12.
11.0.1.0.0
~~~~~~~~~~
* [ADD] Module account_spread_cost_revenue.
(`#715 <https://github.com/OCA/account-financial-tools/pull/715>`_)
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/account-financial-tools/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/account-financial-tools/issues/new?body=module:%20account_spread_cost_revenue%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Onestein
Contributors
~~~~~~~~~~~~
* Andrea Stirpe <a.stirpe@onestein.nl>
* Kitti U. <kittiu@ecosoft.co.th>
* Saran Lim. <saranl@ecosoft.co.th>
Other credits
~~~~~~~~~~~~~
Part of the code in this module (in particular the computation of the spread lines)
is highly inspired by the Assets Management module from the standard
Odoo 11.0 Community developed by Odoo SA.
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/account-financial-tools <https://github.com/OCA/account-financial-tools/tree/16.0/account_spread_cost_revenue>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@ -0,0 +1,4 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import models
from . import wizards

View File

@ -0,0 +1,25 @@
# Copyright 2016-2020 Onestein (<https://www.onestein.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Cost-Revenue Spread",
"summary": "Spread costs and revenues over a custom period",
"version": "16.0.1.0.0",
"development_status": "Beta",
"author": "Onestein,Odoo Community Association (OCA)",
"license": "AGPL-3",
"website": "https://github.com/OCA/account-financial-tools",
"category": "Accounting & Finance",
"depends": ["account"],
"data": [
"security/ir.model.access.csv",
"security/account_spread_security.xml",
"views/account_spread.xml",
"views/account_move.xml",
"views/res_company.xml",
"views/account_spread_template.xml",
"wizards/account_spread_invoice_line_link_wizard.xml",
"data/spread_cron.xml",
],
"installable": True,
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="ir_cron_spread_create_entries" forcecreate="True" model="ir.cron">
<field name="name">Cost/revenue Spread: Create Entries</field>
<field name="active" eval="True" />
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False" />
<field name="model_id" ref="model_account_spread_line" />
<field name="state">code</field>
<field name="code">model._create_entries()</field>
</record>
</odoo>

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

View File

@ -0,0 +1,8 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import account_move
from . import account_move_line
from . import account_spread_line
from . import account_spread
from . import account_spread_template
from . import res_company

View File

@ -0,0 +1,39 @@
# Copyright 2016-2020 Onestein (<https://www.onestein.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, models
class AccountMove(models.Model):
_inherit = "account.move"
def action_post(self):
"""Invoked when validating the invoices."""
self.mapped("invoice_line_ids").create_auto_spread()
res = super().action_post()
spreads = self.mapped("invoice_line_ids.spread_id")
spreads.compute_spread_board()
# On posting of spread moves. Find their related spreads to reconcile
move_spreads = self.env["account.spread"].search(
[("line_ids.move_id", "in", self.ids)]
)
spreads += move_spreads
spreads.reconcile_spread_moves()
return res
def button_cancel(self):
"""Cancel the spread lines and their related moves when
the invoice is canceled."""
spread_lines = self.mapped("invoice_line_ids.spread_id.line_ids")
moves = spread_lines.mapped("move_id")
moves.line_ids.remove_move_reconcile()
moves.filtered(lambda move: move.state == "posted").button_draft()
moves.with_context(force_delete=True).unlink()
spread_lines.unlink()
res = super().button_cancel()
return res
@api.constrains("name", "journal_id", "state")
def _check_unique_sequence_number(self):
if not self.env.context.get("skip_unique_sequence_number"):
return super()._check_unique_sequence_number()

View File

@ -0,0 +1,177 @@
# Copyright 2016-2020 Onestein (<https://www.onestein.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
class AccountMoveLine(models.Model):
_inherit = "account.move.line"
spread_id = fields.Many2one("account.spread", string="Spread Board", copy=False)
spread_check = fields.Selection(
[
("linked", "Linked"),
("unlinked", "Unlinked"),
("unavailable", "Unavailable"),
],
compute="_compute_spread_check",
)
@api.depends("spread_id", "move_id.state")
def _compute_spread_check(self):
for line in self:
if line.spread_id:
line.spread_check = "linked"
elif line.move_id.state == "draft":
line.spread_check = "unlinked"
else:
line.spread_check = "unavailable"
def spread_details(self):
"""Button on the invoice lines tree view of the invoice
form to show the spread form view."""
if not self:
# In case the widget clicked before the creation of the line
return
if self.spread_id:
return {
"name": _("Spread Details"),
"view_mode": "form",
"res_model": "account.spread",
"type": "ir.actions.act_window",
"target": "current",
"readonly": False,
"res_id": self.spread_id.id,
}
# In case no spread board is linked to the invoice line
# open the wizard to link them
ctx = dict(
self.env.context,
default_invoice_line_id=self.id,
default_company_id=self.move_id.company_id.id,
allow_spread_planning=self.move_id.company_id.allow_spread_planning,
)
return {
"name": _("Link Invoice Line with Spread Board"),
"view_mode": "form",
"res_model": "account.spread.invoice.line.link.wizard",
"type": "ir.actions.act_window",
"target": "new",
"context": ctx,
}
@api.constrains("spread_id", "account_id")
def _check_spread_account_balance_sheet(self):
for line in self:
if not line.spread_id:
pass
elif line.move_id.move_type in ("out_invoice", "in_refund"):
if line.account_id != line.spread_id.debit_account_id:
raise ValidationError(
_(
"The account of the invoice line does not correspond "
"to the Balance Sheet (debit account) of the spread"
)
)
elif line.move_id.move_type in ("in_invoice", "out_refund"):
if line.account_id != line.spread_id.credit_account_id:
raise ValidationError(
_(
"The account of the invoice line does not correspond "
"to the Balance Sheet (credit account) of the spread"
)
)
def write(self, vals):
if vals.get("spread_id"):
spread = self.env["account.spread"].browse(vals.get("spread_id"))
if spread.invoice_type in ["out_invoice", "in_refund"]:
vals["account_id"] = spread.debit_account_id.id
else:
vals["account_id"] = spread.credit_account_id.id
return super().write(vals)
def _check_spread_reconcile_validity(self):
# Improve error messages of standard Odoo
reconciled_lines = self.filtered(lambda l: l.reconciled)
msg_line = _(
"Move line: %(line_id)s (%(line_name)s), account code: %(account_code)s\n"
)
if reconciled_lines:
msg = _("Cannot reconcile entries that are already reconciled:\n")
for line in reconciled_lines:
msg += msg_line % {
"line_id": line.id,
"line_name": line.name,
"account_code": line.account_id.code,
}
raise ValidationError(msg)
if len(self.mapped("account_id").ids) > 1:
msg = _("Some entries are not from the same account:\n")
for line in self:
msg += msg_line % {
"line_id": line.id,
"line_name": line.name,
"account_code": line.account_id.code,
}
raise ValidationError(msg)
def create_auto_spread(self):
"""Create auto spread table for each invoice line, when needed"""
def _filter_line(aline, iline):
"""Find matching template auto line with invoice line"""
if aline.product_id and iline.product_id != aline.product_id:
return False
if aline.account_id and iline.account_id != aline.account_id:
return False
if (
aline.analytic_distribution
and iline.analytic_distribution != aline.analytic_distribution
):
return False
return True
# Skip create new template when create move on spread lines
if self.env.context.get("skip_create_template"):
return
for line in self:
if line.spread_check == "linked":
continue
spread_type = (
"sale"
if line.move_id.move_type in ["out_invoice", "out_refund"]
else "purchase"
)
spread_auto = self.env["account.spread.template.auto"].search(
[
("template_id.auto_spread", "=", True),
("template_id.spread_type", "=", spread_type),
]
)
matched = spread_auto.filtered(lambda a, i=line: _filter_line(a, i))
template = matched.mapped("template_id")
if not template:
continue
elif len(template) > 1:
raise UserError(
_(
"Too many auto spread templates (%(len_template)s) matched with the "
"invoice line, %(line_name)s"
)
% {"len_template": len(template), "line_name": line.display_name}
)
# Found auto spread template for this invoice line, create it
wizard = self.env["account.spread.invoice.line.link.wizard"].new(
{
"invoice_line_id": line.id,
"company_id": line.company_id.id,
"spread_action_type": "template",
"template_id": template.id,
}
)
wizard.confirm()

View File

@ -0,0 +1,614 @@
# Copyright 2018-2020 Onestein (<https://www.onestein.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import calendar
import time
from dateutil.relativedelta import relativedelta
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools import float_is_zero
class AccountSpread(models.Model):
_name = "account.spread"
_description = "Account Spread"
_inherit = ["mail.thread", "analytic.mixin"]
_check_company_auto = True
name = fields.Char(required=True)
template_id = fields.Many2one("account.spread.template", string="Spread Template")
invoice_type = fields.Selection(
[
("out_invoice", "Customer Invoice"),
("in_invoice", "Vendor Bill"),
("out_refund", "Customer Credit Note"),
("in_refund", "Vendor Credit Note"),
],
required=True,
)
spread_type = fields.Selection(
[("sale", "Customer"), ("purchase", "Supplier")],
compute="_compute_spread_type",
required=True,
)
period_number = fields.Integer(
string="Number of Repetitions",
default=12,
help="Define the number of spread lines",
required=True,
)
period_type = fields.Selection(
[("month", "Month"), ("quarter", "Quarter"), ("year", "Year")],
default="month",
help="Period length for the entries",
required=True,
)
days_calc = fields.Boolean(
string="Calculate by days",
default=False,
help="Use number of days to calculate amount",
)
use_invoice_line_account = fields.Boolean()
credit_account_id = fields.Many2one(
"account.account",
compute="_compute_credit_account_id",
readonly=False,
store=True,
required=True,
)
debit_account_id = fields.Many2one(
"account.account",
compute="_compute_debit_account_id",
readonly=False,
store=True,
required=True,
)
is_credit_account_deprecated = fields.Boolean(
compute="_compute_deprecated_accounts"
)
is_debit_account_deprecated = fields.Boolean(compute="_compute_deprecated_accounts")
unspread_amount = fields.Float(
digits="Account",
compute="_compute_amounts",
)
unposted_amount = fields.Float(
digits="Account",
compute="_compute_amounts",
)
posted_amount = fields.Float(
digits="Account",
compute="_compute_amounts",
)
total_amount = fields.Float(
digits="Account",
compute="_compute_amounts",
)
all_posted = fields.Boolean(compute="_compute_all_posted", store=True)
line_ids = fields.One2many(
"account.spread.line", "spread_id", string="Spread Lines"
)
spread_date = fields.Date(
string="Start Date", default=time.strftime("%Y-01-01"), required=True
)
journal_id = fields.Many2one(
"account.journal",
compute="_compute_journal_id",
readonly=False,
precompute=True,
store=True,
required=True,
check_company=True,
domain="[('id', 'in', suitable_journal_ids)]",
)
suitable_journal_ids = fields.Many2many(
"account.journal",
compute="_compute_suitable_journal_ids",
)
invoice_line_ids = fields.One2many(
"account.move.line", "spread_id", copy=False, string="Invoice Lines"
)
invoice_line_id = fields.Many2one(
"account.move.line",
string="Invoice line",
compute="_compute_invoice_line",
inverse="_inverse_invoice_line",
store=True,
)
invoice_id = fields.Many2one(
related="invoice_line_id.move_id",
readonly=True,
store=True,
)
estimated_amount = fields.Float(digits="Account")
company_id = fields.Many2one(
"res.company", default=lambda self: self.env.company, required=True
)
currency_id = fields.Many2one(
"res.currency",
required=True,
default=lambda self: self.env.company.currency_id.id,
)
move_line_auto_post = fields.Boolean("Auto-post lines", default=True)
display_create_all_moves = fields.Boolean(
compute="_compute_display_create_all_moves",
)
display_recompute_buttons = fields.Boolean(
compute="_compute_display_recompute_buttons",
)
display_move_line_auto_post = fields.Boolean(
compute="_compute_display_move_line_auto_post",
string="Display Button Auto-post lines",
)
active = fields.Boolean(default=True)
@api.model
def default_journal(self, company_id):
domain = [("type", "=", "general"), ("company_id", "=", company_id)]
return self.env["account.journal"].search(domain, limit=1)
@api.model
def default_get(self, fields):
res = super().default_get(fields)
if "journal_id" not in res:
company_id = res.get("company_id", self.env.company.id)
default_journal = self.default_journal(company_id)
if default_journal:
res["journal_id"] = default_journal.id
return res
@api.depends("company_id")
def _compute_suitable_journal_ids(self):
for spread in self:
domain = [("company_id", "=", spread.company_id.id)]
spread.suitable_journal_ids = self.env["account.journal"].search(domain)
@api.depends("invoice_type")
def _compute_spread_type(self):
for spread in self:
if spread.invoice_type in ["out_invoice", "out_refund"]:
spread.spread_type = "sale"
else:
spread.spread_type = "purchase"
@api.depends("invoice_line_ids", "invoice_line_ids.move_id")
def _compute_invoice_line(self):
for spread in self:
invoice_lines = spread.invoice_line_ids
spread.invoice_line_id = invoice_lines and invoice_lines[0] or False
def _inverse_invoice_line(self):
for spread in self:
invoice_line = spread.invoice_line_id
spread.write({"invoice_line_ids": [(6, 0, [invoice_line.id])]})
@api.depends(
"estimated_amount",
"currency_id",
"company_id",
"invoice_line_id.price_subtotal",
"invoice_line_id.currency_id",
"line_ids.amount",
"line_ids.move_id.state",
)
def _compute_amounts(self):
for spread in self:
lines_move = spread.line_ids.filtered(lambda l: l.move_id)
moves_amount = sum(spread_line.amount for spread_line in lines_move)
lines_posted = lines_move.filtered(lambda l: l.move_id.state == "posted")
posted_amount = sum(spread_line.amount for spread_line in lines_posted)
total_amount = spread.estimated_amount
if spread.invoice_line_id:
total_amount = spread.invoice_line_id.currency_id._convert(
spread.invoice_line_id.balance,
spread.currency_id,
spread.company_id,
spread.invoice_id.date,
)
spread.unspread_amount = total_amount - moves_amount
spread.unposted_amount = total_amount - posted_amount
spread.posted_amount = posted_amount
spread.total_amount = total_amount
@api.depends("unposted_amount")
def _compute_all_posted(self):
for spread in self:
rounding = self.currency_id.rounding
unposted = spread.unposted_amount
spread.all_posted = float_is_zero(unposted, precision_rounding=rounding)
def _compute_display_create_all_moves(self):
for spread in self:
any_not_move = any(not line.move_id for line in spread.line_ids)
spread.display_create_all_moves = any_not_move
def _compute_display_recompute_buttons(self):
for spread in self:
spread.display_recompute_buttons = True
if not spread.company_id.allow_spread_planning:
if spread.invoice_id.state == "draft":
spread.display_recompute_buttons = False
@api.depends("company_id.force_move_auto_post")
def _compute_display_move_line_auto_post(self):
for spread in self:
auto_post = spread.company_id.force_move_auto_post
spread.display_move_line_auto_post = not auto_post
def _get_spread_entry_name(self, seq):
"""Use this method to customise the name of the accounting entry."""
self.ensure_one()
return (self.name or "") + "/" + str(seq)
@api.onchange("template_id")
def onchange_template(self):
if self.template_id:
if self.template_id.spread_type == "sale":
if self.invoice_type in ["in_invoice", "in_refund"]:
self.invoice_type = "out_invoice"
else:
if self.invoice_type in ["out_invoice", "out_refund"]:
self.invoice_type = "in_invoice"
if self.template_id.period_number:
self.period_number = self.template_id.period_number
if self.template_id.period_type:
self.period_type = self.template_id.period_type
if self.template_id.start_date:
self.spread_date = self.template_id.start_date
if self.template_id.analytic_distribution:
self.analytic_distribution = self.template_id.analytic_distribution
self.days_calc = self.template_id.days_calc
@api.depends("invoice_type", "company_id")
def _compute_journal_id(self):
if not self.env.context.get("default_journal_id"):
for spread in self:
journal = spread.company_id.default_spread_expense_journal_id
if spread.invoice_type in ("out_invoice", "in_refund"):
journal = spread.company_id.default_spread_revenue_journal_id
if not journal:
journal = self.default_journal(spread.company_id.id)
spread.journal_id = journal
@api.depends("invoice_type", "company_id")
def _compute_debit_account_id(self):
if not self.env.context.get("default_debit_account_id"):
invoice_types = ("out_invoice", "in_refund")
for spread in self.filtered(lambda s: s.invoice_type in invoice_types):
debit_account = spread.company_id.default_spread_revenue_account_id
spread.debit_account_id = debit_account
@api.depends("invoice_type", "company_id")
def _compute_credit_account_id(self):
if not self.env.context.get("default_credit_account_id"):
invoice_types = ("in_invoice", "out_refund")
for spread in self.filtered(lambda s: s.invoice_type in invoice_types):
credit_account = spread.company_id.default_spread_expense_account_id
spread.credit_account_id = credit_account
@api.constrains("invoice_id", "invoice_type")
def _check_invoice_type(self):
if self.filtered(
lambda s: s.invoice_id and s.invoice_type != s.invoice_id.move_type
):
raise ValidationError(
_("The Invoice Type does not correspond to the Invoice")
)
@api.constrains("journal_id")
def _check_journal(self):
for spread in self:
moves = spread.mapped("line_ids.move_id").filtered("journal_id")
if any(move.journal_id != spread.journal_id for move in moves):
err_msg = _("The Journal is not consistent with the account moves.")
raise ValidationError(err_msg)
@api.constrains("template_id", "invoice_type")
def _check_template_invoice_type(self):
for spread in self.filtered(lambda s: s.template_id.spread_type == "sale"):
if spread.invoice_type in ["in_invoice", "in_refund"]:
err_msg = _(
"The Spread Template (Sales) is not compatible "
"with selected invoice type"
)
raise ValidationError(err_msg)
for spread in self.filtered(lambda s: s.template_id.spread_type == "purchase"):
if spread.invoice_type in ["out_invoice", "out_refund"]:
err_msg = _(
"The Spread Template (Purchases) is not compatible "
"with selected invoice type"
)
raise ValidationError(err_msg)
def _get_spread_period_duration(self):
"""Converts the selected period_type to number of months."""
self.ensure_one()
if self.period_type == "year":
return 12
elif self.period_type == "quarter":
return 3
return 1
def _init_line_date(self, posted_line_ids):
"""Calculates the initial spread date. This method
is used by "def _compute_spread_board()" method.
"""
self.ensure_one()
if posted_line_ids:
# if we already have some previous validated entries,
# starting date is last entry + method period
last_date = posted_line_ids[-1].date
months = self._get_spread_period_duration()
spread_date = last_date + relativedelta(months=months)
else:
spread_date = self.spread_date
return spread_date
def _next_line_date(self, month_day, date):
"""Calculates the next spread date. This method
is used by "def _compute_spread_board()" method.
"""
self.ensure_one()
months = self._get_spread_period_duration()
date = date + relativedelta(months=months)
# get the last day of the month
if month_day > 28:
max_day_in_month = calendar.monthrange(date.year, date.month)[1]
date = date.replace(day=min(max_day_in_month, month_day))
return date
def _compute_spread_board(self):
"""Creates the spread lines. This method is highly inspired
from method compute_depreciation_board() present in standard
Odoo 11.0 "account_asset" module, developed by Odoo SA.
"""
self.ensure_one()
posted_line_ids = self.line_ids.filtered(
lambda x: x.move_id.state == "posted"
).sorted(key=lambda l: l.date)
unposted_line_ids = self.line_ids.filtered(
lambda x: not x.move_id.state == "posted"
)
# Remove old unposted spread lines.
commands = [(2, line_id.id, False) for line_id in unposted_line_ids]
if self.unposted_amount != 0.0:
unposted_amount = self.unposted_amount
spread_date = self._init_line_date(posted_line_ids)
month_day = spread_date.day
number_of_periods = self._get_number_of_periods(month_day)
for x in range(len(posted_line_ids), number_of_periods):
sequence = x + 1
date = self._get_last_day_of_month(spread_date)
amount = self._compute_board_amount(
sequence, unposted_amount, number_of_periods, date
)
amount = self.currency_id.round(amount)
rounding = self.currency_id.rounding
if float_is_zero(amount, precision_rounding=rounding):
continue
unposted_amount -= amount
vals = {
"amount": amount,
"spread_id": self.id,
"name": self._get_spread_entry_name(sequence),
"date": date,
}
commands.append((0, False, vals))
spread_date = self._next_line_date(month_day, spread_date)
self.write({"line_ids": commands})
invoice_type_selection = dict(
self.fields_get(allfields=["invoice_type"])["invoice_type"]["selection"]
)[self.invoice_type]
msg_body = _("Spread table '%s' created.") % invoice_type_selection
self.message_post(body=msg_body)
def _get_number_of_periods(self, month_day):
"""Calculates the number of spread lines."""
self.ensure_one()
return self.period_number + 1 if month_day != 1 else self.period_number
@staticmethod
def _get_first_day_of_month(spread_date):
return spread_date + relativedelta(day=1)
@staticmethod
def _get_last_day_of_month(spread_date):
return spread_date + relativedelta(day=31)
def _get_spread_start_date(self, period_type, spread_end_date):
self.ensure_one()
spread_start_date = spread_end_date + relativedelta(days=1)
if period_type == "month":
spread_start_date = spread_end_date + relativedelta(day=1)
elif period_type == "quarter":
spread_start_date = spread_start_date - relativedelta(months=3)
elif period_type == "year":
spread_start_date = spread_start_date - relativedelta(years=1)
spread_start_date = self._get_first_day_of_month(spread_start_date)
spread_start_date = max(spread_start_date, self.spread_date)
return spread_start_date
def _get_spread_end_date(self, period_type, period_number, spread_start_date):
self.ensure_one()
spread_end_date = spread_start_date
number_of_periods = (
period_number if spread_start_date.day != 1 else period_number - 1
)
if period_type == "month":
spread_end_date = spread_start_date + relativedelta(
months=number_of_periods
)
elif period_type == "quarter":
months = number_of_periods * 3
spread_end_date = spread_start_date + relativedelta(months=months)
elif period_type == "year":
spread_end_date = spread_start_date + relativedelta(years=number_of_periods)
# calculate by days and not first day of month should compute residual day only
if self.days_calc and spread_end_date.day != 1:
spread_end_date = spread_end_date - relativedelta(days=1)
else:
spread_end_date = self._get_last_day_of_month(spread_end_date)
return spread_end_date
def _get_amount_per_day(self, amount):
self.ensure_one()
spread_start_date = self.spread_date
spread_end_date = self._get_spread_end_date(
self.period_type, self.period_number, spread_start_date
)
number_of_days = (spread_end_date - spread_start_date).days + 1
return amount / number_of_days
def _compute_board_amount(
self, sequence, amount, number_of_periods, spread_end_date
):
"""Calculates the amount for the spread lines."""
self.ensure_one()
amount_to_spread = self.total_amount
period = self.period_number
if sequence != number_of_periods:
amount = amount_to_spread / period
if sequence == 1:
date = self.spread_date
month_days = calendar.monthrange(date.year, date.month)[1]
days = month_days - date.day + 1
amount = (amount_to_spread / period) / month_days * days
if self.days_calc:
spread_start_date = self._get_spread_start_date(
self.period_type, spread_end_date
)
days = (spread_end_date - spread_start_date).days + 1
amount = self._get_amount_per_day(amount_to_spread) * days
return amount
def compute_spread_board(self):
"""Checks whether the spread lines should be calculated.
In case checks pass, invoke "def _compute_spread_board()" method.
"""
for spread in self.filtered(lambda s: s.total_amount):
spread._compute_spread_board()
def action_recalculate_spread(self):
"""Recalculate spread"""
self.ensure_one()
spread_lines = self.mapped("line_ids").filtered("move_id")
spread_lines.unlink_move()
self.compute_spread_board()
self.env["account.spread.line"]._create_entries()
def action_undo_spread(self):
"""Undo spreading: Remove all created moves"""
self.ensure_one()
self.mapped("line_ids").filtered("move_id").unlink_move()
self.mapped("line_ids").unlink()
def action_unlink_invoice_line(self):
"""Unlink the invoice line from the spread board"""
self.ensure_one()
if self.invoice_id.state != "draft":
msg = _("Cannot unlink invoice lines if the invoice is validated")
raise UserError(msg)
self._action_unlink_invoice_line()
def _action_unlink_invoice_line(self):
self.mapped("line_ids.move_id.line_ids").remove_move_reconcile()
self._message_post_unlink_invoice_line()
self.write({"invoice_line_ids": [(5, 0, 0)]})
def _message_post_unlink_invoice_line(self):
for spread in self:
inv_link = (
"<a href=# data-oe-model=account.move "
"data-oe-id=%d>%s</a>" % (spread.invoice_id.id, _("Invoice"))
)
msg_body = _(
"Unlinked invoice line '%(spread_line_name)s' (view %(inv_link)s)."
) % {
"spread_line_name": spread.invoice_line_id.name,
"inv_link": inv_link,
}
spread.message_post(body=msg_body)
spread_link = (
"<a href=# data-oe-model=account.spread "
"data-oe-id=%d>%s</a>" % (spread.id, _("Spread"))
)
msg_body = _("Unlinked '%(spread_link)s' (invoice line %(inv_line)s).") % {
"spread_link": spread_link,
"inv_line": spread.invoice_line_id.name,
}
spread.invoice_id.message_post(body=msg_body)
def unlink(self):
if self.filtered(lambda s: s.invoice_line_id):
err_msg = _("Cannot delete spread(s) that are linked to an invoice line.")
raise UserError(err_msg)
if self.mapped("line_ids.move_id").filtered(lambda m: m.state == "posted"):
err_msg = _("Cannot delete spread(s): there are posted Journal Entries.")
raise ValidationError(err_msg)
return super().unlink()
def reconcile_spread_moves(self):
for spread in self:
spread._reconcile_spread_moves()
def _reconcile_spread_moves(self, created_moves=False):
"""Reconcile spread moves if possible"""
self.ensure_one()
spread_mls = self.line_ids.mapped("move_id.line_ids")
if created_moves:
spread_mls |= created_moves.mapped("line_ids")
account = self.invoice_line_id.account_id
mls_to_reconcile = spread_mls.filtered(lambda l: l.account_id == account)
if mls_to_reconcile:
do_reconcile = mls_to_reconcile + self.invoice_line_id
do_reconcile.remove_move_reconcile()
for line in do_reconcile:
line.reconciled = False
# ensure to reconcile only posted items
do_reconcile = do_reconcile.filtered(lambda l: l.move_id.state == "posted")
do_reconcile._check_spread_reconcile_validity()
do_reconcile.reconcile()
def create_all_moves(self):
for line in self.mapped("line_ids").filtered(lambda l: not l.move_id):
line.create_move()
def _post_spread_moves(self, moves):
self.ensure_one()
moves = moves.filtered(lambda l: l.state != "posted")
if not moves:
return
ctx = dict(self.env.context, skip_unique_sequence_number=True)
if self.company_id.force_move_auto_post or self.move_line_auto_post:
moves.with_context(**ctx).action_post()
@api.depends("debit_account_id.deprecated", "credit_account_id.deprecated")
def _compute_deprecated_accounts(self):
for spread in self:
spread.is_debit_account_deprecated = spread.debit_account_id.deprecated
spread.is_credit_account_deprecated = spread.credit_account_id.deprecated
def open_posted_view(self):
action_name = "account_spread_cost_revenue.action_account_moves_all_spread"
action = self.env["ir.actions.act_window"]._for_xml_id(action_name)
action["domain"] = [("id", "in", [])]
spread_mls = self.line_ids.mapped("move_id.line_ids")
if self.env.context.get("show_reconciled_only"):
spread_mls = spread_mls.filtered(lambda m: m.reconciled)
if spread_mls:
domain = [("id", "in", spread_mls.ids + [self.invoice_line_id.id])]
action["domain"] = domain
return action

View File

@ -0,0 +1,173 @@
# Copyright 2016-2020 Onestein (<https://www.onestein.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class AccountInvoiceSpreadLine(models.Model):
_name = "account.spread.line"
_description = "Account Spread Lines"
_order = "date"
name = fields.Char("Description", readonly=True)
amount = fields.Float(digits="Account", required=True)
date = fields.Date(required=True)
spread_id = fields.Many2one("account.spread", ondelete="cascade")
move_id = fields.Many2one("account.move", string="Journal Entry", readonly=True)
def create_and_reconcile_moves(self):
grouped_lines = {}
for spread_line in self:
spread = spread_line.spread_id
spread_line_list = grouped_lines.get(
spread, self.env["account.spread.line"]
)
grouped_lines.update({spread: spread_line_list + spread_line})
for spread in grouped_lines:
created_moves = grouped_lines[spread]._create_moves()
if created_moves:
post_msg = _("Created move(s) ")
post_msg += ", ".join(
"<a href=# data-oe-model=account.move data-oe-id=%d"
">%s</a>" % (move.id, move.name)
for move in created_moves
)
spread.message_post(body=post_msg)
spread._post_spread_moves(created_moves)
def create_move(self):
"""Button to manually create a move from a spread line entry."""
self.ensure_one()
self.with_context(
skip_create_template=True,
).create_and_reconcile_moves()
def _create_moves(self):
if self.filtered(lambda l: l.move_id):
raise UserError(
_(
"This spread line is already linked to a "
"journal entry! Please post or delete it."
)
)
created_moves = self.env["account.move"]
for line in self:
move_vals = line._prepare_move()
move = self.env["account.move"].create(move_vals)
line.move_id = move
created_moves += move
return created_moves
def _prepare_move(self):
self.ensure_one()
spread_date = self.env.context.get("spread_date") or self.date
spread = self.spread_id
analytic_distribution = spread.analytic_distribution
company_currency = spread.company_id.currency_id
current_currency = spread.currency_id
amount = current_currency._convert(
self.amount, company_currency, spread.company_id, spread_date
)
debit_credit = spread.invoice_type in ["in_invoice", "out_refund"]
line_ids = [
(
0,
0,
{
"name": spread.name,
"account_id": spread.debit_account_id.id
if debit_credit
else spread.credit_account_id.id,
"debit": amount if amount > 0.0 else 0.0,
"credit": -amount if amount < 0.0 else 0.0,
"partner_id": self.spread_id.invoice_id.partner_id.id,
"journal_id": self.spread_id.journal_id.id,
"analytic_distribution": analytic_distribution,
"date": self.date,
},
),
(
0,
0,
{
"name": spread.name,
"account_id": spread.credit_account_id.id
if debit_credit
else spread.debit_account_id.id,
"credit": amount if amount > 0.0 else 0.0,
"debit": -amount if amount < 0.0 else 0.0,
"partner_id": self.spread_id.invoice_id.partner_id.id,
"journal_id": self.spread_id.journal_id.id,
"analytic_distribution": analytic_distribution,
"date": self.date,
},
),
]
return {
"name": False,
"ref": self.name,
"date": spread_date,
"invoice_date": spread_date,
"journal_id": spread.journal_id.id,
"line_ids": line_ids,
"company_id": spread.company_id.id,
"partner_id": spread.invoice_id.partner_id.id,
}
def open_move(self):
"""Used by a button to manually view a move from a spread line entry."""
self.ensure_one()
return {
"name": _("Journal Entry"),
"view_mode": "form",
"res_model": "account.move",
"view_id": False,
"type": "ir.actions.act_window",
"res_id": self.move_id.id,
}
def unlink_move(self):
"""Used by a button to manually unlink a move from a spread line entry."""
for line in self:
move = line.move_id
if move.state == "posted":
move.button_cancel()
move.line_ids.remove_move_reconcile()
post_msg = _("Deleted move %s") % line.move_id.id
move.with_context(force_delete=True).unlink()
line.move_id = False
line.spread_id.message_post(body=post_msg)
@api.model
def _create_entries(self):
"""Find spread line entries where date is in the past and
create moves for them. Method also called by the cron job.
"""
lines = self.search(
[("date", "<=", fields.Date.today()), ("move_id", "=", False)]
)
lines.create_and_reconcile_moves()
unposted_moves = (
self.search([("move_id", "!=", False)])
.mapped("move_id")
.filtered(lambda m: m.state != "posted")
)
unposted_moves.filtered(
lambda m: m.company_id.force_move_auto_post
).action_post()
spreads_to_archive = (
self.env["account.spread"]
.search([("all_posted", "=", True)])
.filtered(lambda s: s.company_id.auto_archive_spread)
)
spreads_to_archive.write({"active": False})

View File

@ -0,0 +1,179 @@
# Copyright 2018-2020 Onestein (<https://www.onestein.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class AccountSpreadTemplate(models.Model):
_name = "account.spread.template"
_inherit = "analytic.mixin"
_description = "Account Spread Template"
name = fields.Char(required=True)
spread_type = fields.Selection(
[("sale", "Customer"), ("purchase", "Supplier")], default="sale", required=True
)
company_id = fields.Many2one(
"res.company", default=lambda self: self.env.company, required=True
)
spread_journal_id = fields.Many2one(
"account.journal",
string="Journal",
compute="_compute_spread_journal",
readonly=False,
store=True,
required=True,
)
use_invoice_line_account = fields.Boolean(
string="Invoice account as spread account",
help="Use invoice line's account as Balance sheet / spread account.\n"
"In this case, user need to select expense/revenue account too.",
)
spread_account_id = fields.Many2one(
"account.account",
string="Spread Balance Sheet Account",
compute="_compute_spread_account",
readonly=False,
store=True,
required=False,
)
exp_rev_account_id = fields.Many2one(
"account.account",
string="Expense/Revenue Account",
help="Optional account to overwrite the existing expense/revenue account",
)
period_number = fields.Integer(
string="Number of Repetitions", help="Define the number of spread lines"
)
period_type = fields.Selection(
[("month", "Month"), ("quarter", "Quarter"), ("year", "Year")],
help="Period length for the entries",
)
start_date = fields.Date()
days_calc = fields.Boolean(
string="Calculate by days",
default=False,
help="Use number of days to calculate amount",
)
auto_spread = fields.Boolean(
string="Auto assign template on invoice validate",
help="If checked, provide option to auto create spread during "
"invoice validation, based on product/account/analytic in invoice line.",
)
auto_spread_ids = fields.One2many(
comodel_name="account.spread.template.auto",
string="Auto Spread On",
inverse_name="template_id",
)
@api.model
def default_get(self, fields):
res = super().default_get(fields)
if not res.get("company_id"):
res["company_id"] = self.env.company.id
if "spread_journal_id" not in res:
default_journal = self.env["account.spread"].default_journal(
res["company_id"]
)
if default_journal:
res["spread_journal_id"] = default_journal.id
return res
@api.constrains("auto_spread", "auto_spread_ids")
def _check_product_account(self):
for rec in self.filtered("auto_spread"):
for line in rec.auto_spread_ids:
if not line.product_id and not line.account_id:
raise UserError(
_(
"Please select product and/or account "
"on auto spread options"
)
)
@api.depends("spread_type", "company_id")
def _compute_spread_journal(self):
for spread in self:
company = spread.company_id
if spread.spread_type == "sale":
journal = company.default_spread_revenue_journal_id
else:
journal = company.default_spread_expense_journal_id
if journal:
spread.spread_journal_id = journal
@api.depends("spread_type", "company_id")
def _compute_spread_account(self):
for spread in self:
company = spread.company_id
if spread.spread_type == "sale":
account = company.default_spread_revenue_account_id
else:
account = company.default_spread_expense_account_id
if account:
spread.spread_account_id = account
@api.onchange("use_invoice_line_account")
def _onchange_user_invoice_line_account(self):
self.exp_rev_account_id = False
def _prepare_spread_from_template(self, spread_account_id=False):
self.ensure_one()
company = self.company_id
spread_vals = {
"name": self.name,
"template_id": self.id,
"journal_id": self.spread_journal_id.id,
"use_invoice_line_account": self.use_invoice_line_account,
"days_calc": self.days_calc,
"company_id": company.id,
}
account_id = spread_account_id or self.spread_account_id.id
if self.spread_type == "sale":
invoice_type = "out_invoice"
spread_vals["debit_account_id"] = account_id
else:
invoice_type = "in_invoice"
spread_vals["credit_account_id"] = account_id
if self.period_number:
spread_vals["period_number"] = self.period_number
if self.period_type:
spread_vals["period_type"] = self.period_type
if self.start_date:
spread_vals["spread_date"] = self.start_date
spread_vals["invoice_type"] = invoice_type
return spread_vals
class AccountSpreadTemplateAuto(models.Model):
_name = "account.spread.template.auto"
_inherit = "analytic.mixin"
_description = "Auto create spread, based on product/account/analytic"
template_id = fields.Many2one(
comodel_name="account.spread.template",
string="Spread Template",
required=True,
ondelete="cascade",
index=True,
)
company_id = fields.Many2one(
related="template_id.company_id",
store=True,
)
name = fields.Char(
required=True,
default="/",
)
product_id = fields.Many2one(
comodel_name="product.product",
string="Product",
)
account_id = fields.Many2one(
comodel_name="account.account",
string="Account",
)

View File

@ -0,0 +1,40 @@
# Copyright 2018-2020 Onestein (<https://www.onestein.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class ResCompany(models.Model):
_inherit = "res.company"
default_spread_revenue_account_id = fields.Many2one(
"account.account", string="Revenue Spread Account"
)
default_spread_expense_account_id = fields.Many2one(
"account.account", string="Expense Spread Account"
)
default_spread_revenue_journal_id = fields.Many2one(
"account.journal", string="Revenue Spread Journal"
)
default_spread_expense_journal_id = fields.Many2one(
"account.journal", string="Expense Spread Journal"
)
allow_spread_planning = fields.Boolean(
default=True,
help="Disable this option if you do not want to allow the "
"spreading before the invoice is validated.",
)
force_move_auto_post = fields.Boolean(
"Auto-post spread lines",
help="Enable this option if you want to post automatically the "
"accounting moves of all the spreads.",
)
auto_archive_spread = fields.Boolean(
"Auto-archive spread",
help="Enable this option if you want the cron job to automatically "
"archive the spreads when all lines are posted.",
)

View File

@ -0,0 +1,25 @@
To be able to access the full spreading features, the user must belong to *Show Full Accounting Features* group.
On the form view of the company, in the *Account Spread* tab, you can configure
the journals in which the spread journal items will be generated by default:
* the *Default Spread Journal for Revenues*,
* the *Default Spread Journal for Expenses*.
In the same *Account Spread* tab, you can also configure the Spread Balance Sheet Accounts used by default:
* the *Default Spread Account for Revenues*,
* the *Default Spread Account for Expenses*.
This module by default allows the spreading even before the receipt of the invoice or when the invoice is still draft,
so that it is possible to work on the plan of the cost/revenue spreading. To disable this feature, on the form view of
the company disable the *Allow Spread Planning* option.
In Spread Template, there is also option to *Auto assign template on invoice validate*, based on the preset invoice line criteria.
On the form view of the company, the *Auto-post spread lines* option forces the account moves created
during the cost/revenue spreading to be automatically posted. When this option is false, the user can
enable/disable the automatic posting by the flag *Auto-post lines* present in the spread board.
On the form view of the company, enable the *Auto-archive spread* option if you want the
cron job to automatically archive the spreads when all lines are posted.

View File

@ -0,0 +1,3 @@
* Andrea Stirpe <a.stirpe@onestein.nl>
* Kitti U. <kittiu@ecosoft.co.th>
* Saran Lim. <saranl@ecosoft.co.th>

View File

@ -0,0 +1,3 @@
Part of the code in this module (in particular the computation of the spread lines)
is highly inspired by the Assets Management module from the standard
Odoo 11.0 Community developed by Odoo SA.

View File

@ -0,0 +1 @@
Allows to spread costs or revenues over a customizable periods, to even out cost or invoice spikes.

View File

@ -0,0 +1,28 @@
13.0.1.0.0
~~~~~~~~~~
* [MIG] Port account_spread_cost_revenue to V13.
12.0.2.0.0
~~~~~~~~~~
* [ENH] In spread template, add option to auto create spread on invoice validation
12.0.1.1.0
~~~~~~~~~~
* [ENH] Add optional Expense/Revenue Account in Chart Template, which can be used
in place of account from invoice line to set Expense/Revenue account in the spread
12.0.1.0.0
~~~~~~~~~~
* [MIG] Port account_spread_cost_revenue to V12.
11.0.1.0.0
~~~~~~~~~~
* [ADD] Module account_spread_cost_revenue.
(`#715 <https://github.com/OCA/account-financial-tools/pull/715>`_)

View File

@ -0,0 +1,79 @@
Define Spread Costs/Revenues Board
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Under Invoicing -> Accounting -> Journals -> Spread Costs/Revenues, create a new spread board.
Complete the definition of the spreading criteria, by setting the the fields:
* *Debit Account*
* *Credit Account*
* *Estimated Amount* (The total amount to spread)
* *Number of Repetitions*
* *Period Type* (Duration of each period)
* *Start date*
* *Journal*
.. figure:: https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/spread.png
:alt: Create a new spread board
Click on the "Recalculate unposted lines" button on the top-left to calculate the spread lines.
.. figure:: https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/create_spread.png
:alt: The spreading board is defined
A cron job will automatically create the accounting moves for all the lines having date previous that the current day (today).
.. figure:: https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/update_spread.png
:alt: The spreading board is updated by the cron job
By default, the status of the created accounting moves is posted.
To disable the automatic posting of the accounting moves, set the flag *Auto-post lines* to False.
This flag is only available when the *Auto-post spread lines* option, present on the form view of the company, is disabled.
Click on button *Recalculate entire spread* button in the spread board to force the recalculation of the spread lines:
this will also reset all the journal entries previously created.
Link Invoice to Spread Costs/Revenues Board
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Create an invoice or vendor bill in draft. On its lines, the spreading right-arrow icon are displayed in dark-grey color.
.. figure:: https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/invoice_line_1.png
:alt: On the invoice line the spreading icon is displayed
Click on the spreading right-arrow icon. A wizard prompts to enter a *Spread Action Type*:
- *Link to existing spread board*
- *Create from spread template*
- *Create new spread board*
Select *Link to existing spread board* and enter the previously generated Spread Board. Click on Confirm button:
the selected Spread Board will be automatically displayed.
Go back to the draft invoice/bill. The spreading functionality is now enabled on the invoice line:
the spreading right-arrow icon is now displayed in green color.
.. figure:: https://raw.githubusercontent.com/OCA/account-financial-tools/16.0/account_spread_cost_revenue/static/description/invoice_line_2.png
:alt: On the invoice line the spreading icon is displayed in green color
Validate the invoice/bill. Click on the spreading (green) right-arrow icon to open the spread board, then click
on the smart button *Posted entries* to see the moves of the spread lines together with the move of the invoice line.
In case the Subtotal Price of the invoice line is different than the *Estimated Amount* of the spread board, the spread
lines (not yet posted) will be recalculated when validating the invoice/bill.
Define Spread Costs/Revenues Template
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Under Invoicing -> Configuration -> Accounting -> Spread Templates, create a new spread template.
* *Spread Type*
* *Spread Balance Sheet Account*
* *Expense/Revenue Account* This option visible if invoice line account is balance sheet account, user need to specify this too.
* *Journal*
* *Auto assign template on invoice validate*
When creating a new Spread Costs/Revenues Board, select the right template.
This way the above fields will be copied to the Spread Board.
If *Auto assign template on invoice validate* is checked, this template will be used to auto create spread, if the underlining invoice match the preset product/account/analytic criteria.

View File

@ -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>

View File

@ -0,0 +1,10 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_account_spread_cost_revenue_full,Full access on account.spread,model_account_spread,account.group_account_manager,1,1,1,1
access_account_spread_cost_revenue_read,Read access on account.spread,model_account_spread,account.group_account_invoice,1,0,0,0
access_account_spread_cost_revenue_line_full,Full access on account.spread.line,model_account_spread_line,account.group_account_manager,1,1,1,1
access_account_spread_cost_revenue_line_read,Read access on account.spread.line,model_account_spread_line,account.group_account_invoice,1,0,0,0
access_account_spread_cost_revenue_template_full,Full access on account.spread.template,model_account_spread_template,account.group_account_manager,1,1,1,1
access_account_spread_cost_revenue_template_read,Read access on account.spread.template,model_account_spread_template,account.group_account_invoice,1,0,0,0
access_account_spread_cost_revenue_template_auto_full,Full access on account.spread.template.auto,model_account_spread_template_auto,account.group_account_manager,1,1,1,1
access_account_spread_cost_revenue_template_auto_read,Read access on account.spread.template.auto,model_account_spread_template_auto,account.group_account_invoice,1,0,0,0
access_account_spread_invoice_line_link_wizard,access_account_spread_invoice_line_link_wizard,model_account_spread_invoice_line_link_wizard,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_account_spread_cost_revenue_full Full access on account.spread model_account_spread account.group_account_manager 1 1 1 1
3 access_account_spread_cost_revenue_read Read access on account.spread model_account_spread account.group_account_invoice 1 0 0 0
4 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
5 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
6 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
7 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
8 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
9 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
10 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

View File

@ -0,0 +1,575 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Cost-Revenue Spread</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="cost-revenue-spread">
<h1 class="title">Cost-Revenue Spread</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:560d87ede92fd18ca4a929595b6a9fd9f558f6b577e8c9bad1f8e7236ad4175d
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/account-financial-tools/tree/16.0/account_spread_cost_revenue"><img alt="OCA/account-financial-tools" src="https://img.shields.io/badge/github-OCA%2Faccount--financial--tools-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/account-financial-tools-16-0/account-financial-tools-16-0-account_spread_cost_revenue"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/account-financial-tools&amp;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 -&gt; Accounting -&gt; Journals -&gt; 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 -&gt; Configuration -&gt; Accounting -&gt; 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 &lt;<a class="reference external" href="mailto:a.stirpe&#64;onestein.nl">a.stirpe&#64;onestein.nl</a>&gt;</li>
<li>Kitti U. &lt;<a class="reference external" href="mailto:kittiu&#64;ecosoft.co.th">kittiu&#64;ecosoft.co.th</a>&gt;</li>
<li>Saran Lim. &lt;<a class="reference external" href="mailto:saranl&#64;ecosoft.co.th">saranl&#64;ecosoft.co.th</a>&gt;</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

View File

@ -0,0 +1,6 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import test_account_spread_cost_revenue
from . import test_compute_spread_board
from . import test_account_invoice_spread
from . import test_account_invoice_auto_spread

View File

@ -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)

View File

@ -0,0 +1,809 @@
# Copyright 2018-2020 Onestein (<https://www.onestein.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import datetime
from odoo import fields
from odoo.exceptions import UserError, ValidationError
from odoo.tests import Form, common
class TestAccountInvoiceSpread(common.TransactionCase):
def create_account_invoice(self, invoice_type, quantity=1.0, price_unit=1000.0):
"""Create an invoice as in a view by triggering its onchange methods"""
invoice_form = Form(
self.env["account.move"].with_context(default_move_type=invoice_type)
)
invoice_form.partner_id = self.env["res.partner"].create(
{"name": "Partner Name"}
)
with invoice_form.invoice_line_ids.new() as line:
line.name = "product that costs " + str(price_unit)
line.quantity = quantity
line.price_unit = price_unit
return invoice_form.save()
def setUp(self):
super().setUp()
# Define minimal accounting data to run
a_expense = self.env["account.account"].create(
{
"code": "X2120",
"name": "Expenses - (test)",
"account_type": "expense",
}
)
a_sale = self.env["account.account"].create(
{
"code": "X2020",
"name": "Product Sales - (test)",
"account_type": "expense_direct_cost",
}
)
self.expenses_journal = self.env["account.journal"].create(
{
"name": "Vendor Bills - Test",
"code": "TEXJ",
"type": "purchase",
"default_account_id": a_expense.id,
"refund_sequence": True,
}
)
self.sales_journal = self.env["account.journal"].create(
{
"name": "Customer Invoices - Test",
"code": "TINV",
"type": "sale",
"default_account_id": a_sale.id,
"refund_sequence": True,
}
)
self.account_payable = self.env["account.account"].create(
{
"name": "Test account payable",
"code": "321spread",
"account_type": "income_other",
"reconcile": True,
}
)
self.account_receivable = self.env["account.account"].create(
{
"name": "Test account receivable",
"code": "322spread",
"account_type": "income_other",
"reconcile": True,
}
)
spread_account_payable = self.env["account.account"].create(
{
"name": "test spread account_payable",
"code": "765spread",
"account_type": "income_other",
"reconcile": True,
}
)
spread_account_receivable = self.env["account.account"].create(
{
"name": "test spread account_receivable",
"code": "766spread",
"account_type": "income_other",
"reconcile": True,
}
)
# Invoices
self.vendor_bill = self.create_account_invoice("in_invoice")
self.vendor_bill.invoice_date = fields.Date.today()
self.sale_invoice = self.create_account_invoice("out_invoice")
self.sale_invoice.invoice_date = fields.Date.today()
self.vendor_bill_line = self.vendor_bill.invoice_line_ids[0]
self.invoice_line = self.sale_invoice.invoice_line_ids[0]
# Set accounts to reconcilable
self.vendor_bill_line.account_id.reconcile = True
self.invoice_line.account_id.reconcile = True
analytic_plan = self.env["account.analytic.plan"].create(
{"name": "Plan Test", "company_id": False}
)
self.analytic_account = self.env["account.analytic.account"].create(
{"name": "test account", "plan_id": analytic_plan.id}
)
self.distribution = self.env["account.analytic.distribution.model"].create(
{
"partner_id": self.vendor_bill.partner_id.id,
"analytic_distribution": {self.analytic_account.id: 100},
}
)
self.spread = (
self.env["account.spread"]
.with_context(mail_create_nosubscribe=True)
.create(
[
{
"name": "test",
"debit_account_id": spread_account_payable.id,
"credit_account_id": self.account_payable.id,
"period_number": 12,
"period_type": "month",
"spread_date": datetime.date(2017, 2, 1),
"estimated_amount": 1000.0,
"journal_id": self.vendor_bill.journal_id.id,
"invoice_type": "in_invoice",
"analytic_distribution": self.distribution._get_distribution(
{
"partner_id": self.vendor_bill.partner_id.id,
}
),
}
]
)
)
self.spread2 = self.env["account.spread"].create(
[
{
"name": "test2",
"debit_account_id": spread_account_receivable.id,
"credit_account_id": self.account_receivable.id,
"period_number": 12,
"period_type": "month",
"spread_date": datetime.date(2017, 2, 1),
"estimated_amount": 1000.0,
"journal_id": self.sale_invoice.journal_id.id,
"invoice_type": "out_invoice",
}
]
)
def test_01_wizard_defaults(self):
Wizard = self.env["account.spread.invoice.line.link.wizard"]
wizard1 = Wizard.with_context(
default_invoice_line_id=self.vendor_bill_line.id,
default_company_id=self.env.company.id,
allow_spread_planning=True,
).create({})
self.assertEqual(wizard1.invoice_line_id, self.vendor_bill_line)
self.assertEqual(wizard1.invoice_line_id.move_id, self.vendor_bill)
self.assertEqual(wizard1.invoice_type, "in_invoice")
self.assertFalse(wizard1.spread_id)
self.assertEqual(wizard1.company_id, self.env.company)
self.assertEqual(wizard1.spread_action_type, "link")
self.assertFalse(wizard1.spread_account_id)
self.assertFalse(wizard1.spread_journal_id)
wizard2 = Wizard.with_context(
default_invoice_line_id=self.invoice_line.id,
default_company_id=self.env.company.id,
).create({})
self.assertEqual(wizard2.invoice_line_id, self.invoice_line)
self.assertEqual(wizard2.invoice_line_id.move_id, self.sale_invoice)
self.assertEqual(wizard2.invoice_type, "out_invoice")
self.assertFalse(wizard2.spread_id)
self.assertEqual(wizard2.company_id, self.env.company)
self.assertEqual(wizard2.spread_action_type, "template")
self.assertFalse(wizard2.spread_account_id)
self.assertFalse(wizard2.spread_journal_id)
def test_02_wizard_defaults(self):
Wizard = self.env["account.spread.invoice.line.link.wizard"]
self.env.company.default_spread_revenue_account_id = self.account_receivable
self.env.company.default_spread_expense_account_id = self.account_payable
self.env.company.default_spread_revenue_journal_id = self.sales_journal
self.env.company.default_spread_expense_journal_id = self.expenses_journal
self.assertTrue(self.env.company.default_spread_revenue_account_id)
self.assertTrue(self.env.company.default_spread_expense_account_id)
self.assertTrue(self.env.company.default_spread_revenue_journal_id)
self.assertTrue(self.env.company.default_spread_expense_journal_id)
wizard1 = Wizard.with_context(
default_invoice_line_id=self.vendor_bill_line.id,
default_company_id=self.env.company.id,
allow_spread_planning=True,
).create({})
self.assertEqual(wizard1.invoice_line_id, self.vendor_bill_line)
self.assertEqual(wizard1.invoice_line_id.move_id, self.vendor_bill)
self.assertEqual(wizard1.invoice_type, "in_invoice")
self.assertFalse(wizard1.spread_id)
self.assertEqual(wizard1.company_id, self.env.company)
self.assertEqual(wizard1.spread_action_type, "link")
self.assertTrue(wizard1.spread_account_id)
self.assertTrue(wizard1.spread_journal_id)
self.assertEqual(wizard1.spread_account_id, self.account_payable)
self.assertEqual(wizard1.spread_journal_id.id, self.expenses_journal.id)
self.assertTrue(wizard1.spread_invoice_type_domain_ids)
wizard2 = Wizard.with_context(
default_invoice_line_id=self.invoice_line.id,
default_company_id=self.env.company.id,
).create({})
self.assertEqual(wizard2.invoice_line_id, self.invoice_line)
self.assertEqual(wizard2.invoice_line_id.move_id, self.sale_invoice)
self.assertEqual(wizard2.invoice_type, "out_invoice")
self.assertFalse(wizard2.spread_id)
self.assertEqual(wizard2.company_id, self.env.company)
self.assertEqual(wizard2.spread_action_type, "template")
self.assertTrue(wizard2.spread_account_id)
self.assertTrue(wizard2.spread_journal_id)
self.assertEqual(wizard2.spread_account_id, self.account_receivable)
self.assertEqual(wizard2.spread_journal_id.id, self.sales_journal.id)
self.assertTrue(wizard2.spread_invoice_type_domain_ids)
def test_03_link_invoice_line_with_spread_sheet(self):
self.env.user.write(
{
"groups_id": [
(4, self.env.ref("analytic.group_analytic_accounting").id),
],
}
)
Wizard = self.env["account.spread.invoice.line.link.wizard"]
wizard1 = Wizard.with_context(
default_invoice_line_id=self.vendor_bill_line.id,
default_company_id=self.env.company.id,
allow_spread_planning=True,
).create({})
self.assertEqual(wizard1.spread_action_type, "link")
wizard1.spread_account_id = self.account_receivable
wizard1.spread_journal_id = self.expenses_journal
wizard1.spread_id = self.spread
res_action = wizard1.confirm()
self.assertTrue(isinstance(res_action, dict))
self.assertTrue(res_action.get("res_id"))
self.assertEqual(res_action.get("res_id"), self.spread.id)
self.assertTrue(self.spread.invoice_line_id)
self.assertEqual(self.spread.invoice_line_id, self.vendor_bill_line)
self.assertFalse(any(line.move_id for line in self.spread.line_ids))
self.spread.compute_spread_board()
spread_lines = self.spread.line_ids
for line in spread_lines:
line.create_move()
self.assertTrue(line.move_id)
for ml in line.move_id.line_ids:
self.assertEqual(
ml.analytic_distribution, self.spread.analytic_distribution
)
self.spread.invoice_id.button_cancel()
self.assertTrue(self.spread.invoice_line_id)
with self.assertRaises(UserError):
self.spread.unlink()
with self.assertRaises(UserError):
self.spread.action_unlink_invoice_line()
self.assertTrue(self.spread.invoice_line_id)
def test_04_new_spread_sheet(self):
Wizard = self.env["account.spread.invoice.line.link.wizard"]
spread_journal_id = self.expenses_journal
wizard1 = Wizard.with_context(
default_invoice_line_id=self.vendor_bill_line.id,
default_company_id=self.env.company.id,
).create({"spread_action_type": "new"})
self.assertEqual(wizard1.spread_action_type, "new")
wizard1.write(
{
"spread_account_id": self.account_receivable.id,
"spread_journal_id": spread_journal_id,
}
)
res_action = wizard1.confirm()
self.assertTrue(isinstance(res_action, dict))
self.assertFalse(res_action.get("res_id"))
self.assertTrue(res_action.get("context"))
res_context = res_action.get("context")
self.assertTrue(res_context.get("default_name"))
self.assertTrue(res_context.get("default_invoice_type"))
self.assertTrue(res_context.get("default_invoice_line_id"))
self.assertTrue(res_context.get("default_debit_account_id"))
self.assertTrue(res_context.get("default_credit_account_id"))
self.assertFalse(any(line.move_id for line in self.spread.line_ids))
self.spread.compute_spread_board()
spread_lines = self.spread.line_ids
for line in spread_lines:
line.create_move()
self.assertTrue(line.move_id)
wizard2 = Wizard.with_context(
default_invoice_line_id=self.invoice_line.id,
default_company_id=self.env.company.id,
).create({"spread_action_type": "new"})
self.assertEqual(wizard2.spread_action_type, "new")
wizard2.write(
{
"spread_account_id": self.account_receivable.id,
"spread_journal_id": spread_journal_id,
}
)
res_action = wizard2.confirm()
self.assertTrue(isinstance(res_action, dict))
self.assertFalse(res_action.get("res_id"))
self.assertTrue(res_action.get("context"))
res_context = res_action.get("context")
self.assertTrue(res_context.get("default_name"))
self.assertTrue(res_context.get("default_invoice_type"))
self.assertTrue(res_context.get("default_invoice_line_id"))
self.assertTrue(res_context.get("default_debit_account_id"))
self.assertTrue(res_context.get("default_credit_account_id"))
self.assertFalse(any(line.move_id for line in self.spread2.line_ids))
self.spread2.compute_spread_board()
for line in self.spread2.line_ids:
line.create_move()
self.assertTrue(line.move_id)
def test_05_new_spread_sheet_from_template(self):
Wizard = self.env["account.spread.invoice.line.link.wizard"]
spread_account = self.account_payable
self.assertTrue(spread_account)
spread_journal_id = self.expenses_journal.id
template = self.env["account.spread.template"].create(
{
"name": "test",
"spread_type": "purchase",
"period_number": 5,
"period_type": "month",
"spread_account_id": spread_account.id,
"spread_journal_id": spread_journal_id,
}
)
wizard1 = Wizard.with_context(
default_invoice_line_id=self.vendor_bill_line.id,
default_company_id=self.env.company.id,
).create({"spread_action_type": "template", "template_id": template.id})
self.assertEqual(wizard1.spread_action_type, "template")
res_action = wizard1.confirm()
self.assertTrue(isinstance(res_action, dict))
self.assertTrue(res_action.get("res_id"))
new_spread = self.env["account.spread"].browse(res_action["res_id"])
new_spread.compute_spread_board()
self.assertFalse(any(line.move_id for line in new_spread.line_ids))
for line in new_spread.line_ids:
line.create_move()
self.assertTrue(line.move_id)
wizard2 = Wizard.with_context(
default_invoice_line_id=self.invoice_line.id,
default_company_id=self.env.company.id,
).create({"spread_action_type": "new"})
self.assertEqual(wizard2.spread_action_type, "new")
wizard2.write(
{
"spread_account_id": spread_account.id,
"spread_journal_id": spread_journal_id,
}
)
res_action = wizard2.confirm()
self.assertTrue(isinstance(res_action, dict))
self.assertFalse(res_action.get("res_id"))
self.assertTrue(res_action.get("context"))
res_context = res_action.get("context")
self.assertTrue(res_context.get("default_name"))
self.assertTrue(res_context.get("default_invoice_type"))
self.assertTrue(res_context.get("default_invoice_line_id"))
self.assertTrue(res_context.get("default_debit_account_id"))
self.assertTrue(res_context.get("default_credit_account_id"))
self.assertFalse(any(line.move_id for line in self.spread2.line_ids))
self.spread2.compute_spread_board()
for line in self.spread2.line_ids:
line.create_move()
self.assertTrue(line.move_id)
def test_06_open_wizard(self):
res_action = self.vendor_bill_line.spread_details()
self.assertTrue(isinstance(res_action, dict))
self.assertFalse(res_action.get("res_id"))
self.assertTrue(res_action.get("context"))
def test_07_unlink_invoice_line_and_spread_sheet(self):
self.assertFalse(self.spread.invoice_line_id)
self.vendor_bill_line.spread_id = self.spread
self.assertTrue(self.spread.invoice_line_id)
self.spread.action_unlink_invoice_line()
self.assertFalse(self.spread.invoice_line_id)
self.assertFalse(self.spread2.invoice_line_id)
self.invoice_line.spread_id = self.spread2
self.assertTrue(self.spread2.invoice_line_id)
self.spread2.action_unlink_invoice_line()
self.assertFalse(self.spread2.invoice_line_id)
def test_08_invoice_multi_line(self):
invoice_form = Form(self.vendor_bill)
with invoice_form.invoice_line_ids.new() as line:
line.name = "new test line"
line.quantity = 1.0
line.price_unit = 1000.0
self.invoice = invoice_form.save()
self.assertEqual(len(self.vendor_bill.invoice_line_ids), 2)
self.vendor_bill_line.spread_id = self.spread
self.assertTrue(self.spread.invoice_id.invoice_line_ids[0])
self.assertEqual(
self.spread.invoice_id.invoice_line_ids[0], self.vendor_bill_line
)
self.assertTrue(self.vendor_bill_line.spread_id)
self.assertEqual(self.vendor_bill_line.spread_check, "linked")
self.assertFalse(self.vendor_bill.invoice_line_ids[1].spread_id)
self.assertEqual(self.vendor_bill.invoice_line_ids[1].spread_check, "unlinked")
self.assertFalse(any(line.move_id for line in self.spread.line_ids))
self.spread.compute_spread_board()
spread_lines = self.spread.line_ids
for line in spread_lines:
line.create_move()
self.assertTrue(line.move_id)
# Validate invoice
self.vendor_bill.action_post()
self.assertTrue(self.vendor_bill_line.spread_id)
self.assertEqual(self.vendor_bill_line.spread_check, "linked")
self.assertFalse(self.vendor_bill.invoice_line_ids[1].spread_id)
self.assertEqual(
self.vendor_bill.invoice_line_ids[1].spread_check, "unavailable"
)
def test_09_no_link_invoice(self):
balance_sheet = self.spread.credit_account_id
# Validate invoice
self.vendor_bill.action_post()
invoice_mls = self.vendor_bill.invoice_line_ids
self.assertTrue(invoice_mls)
for invoice_ml in invoice_mls:
self.assertNotEqual(invoice_ml.account_id, balance_sheet)
def test_10_link_vendor_bill_line_with_spread_sheet(self):
invoice_form = Form(self.vendor_bill)
with invoice_form.invoice_line_ids.new() as line:
line.name = "new test line"
line.quantity = 1.0
line.price_unit = 1000.0
self.invoice = invoice_form.save()
self.spread.write(
{
"estimated_amount": 1000.0,
"period_number": 12,
"period_type": "month",
"spread_date": datetime.date(2017, 1, 7),
"invoice_line_id": self.vendor_bill_line.id,
"move_line_auto_post": False,
}
)
self.assertFalse(any(line.move_id for line in self.spread.line_ids))
self.spread.compute_spread_board()
spread_lines = self.spread.line_ids
for line in spread_lines:
line.create_move()
self.assertTrue(line.move_id)
expense_account = self.spread.debit_account_id
balance_sheet = self.spread.credit_account_id
self.assertTrue(balance_sheet.reconcile)
spread_mls = self.spread.line_ids.mapped("move_id.line_ids")
self.assertTrue(spread_mls)
for spread_ml in spread_mls:
if spread_ml.debit:
self.assertEqual(spread_ml.account_id, expense_account)
if spread_ml.credit:
self.assertEqual(spread_ml.account_id, balance_sheet)
# Validate invoice
self.vendor_bill.action_post()
count_balance_sheet = len(
self.vendor_bill.line_ids.filtered(lambda x: x.account_id == balance_sheet)
)
self.assertEqual(count_balance_sheet, 1)
self.spread.line_ids.create_and_reconcile_moves()
spread_mls = self.spread.line_ids.mapped("move_id.line_ids")
self.assertTrue(spread_mls)
for spread_ml in spread_mls:
if spread_ml.debit:
self.assertFalse(spread_ml.full_reconcile_id)
if spread_ml.credit:
self.assertFalse(spread_ml.full_reconcile_id)
action_posted_view = self.spread2.open_posted_view()
self.assertTrue(isinstance(action_posted_view, dict))
self.assertFalse(action_posted_view.get("domain")[0][2])
self.assertTrue(action_posted_view.get("context"))
def test_11_link_vendor_bill_line_with_spread_sheet(self):
invoice_form = Form(self.vendor_bill)
with invoice_form.invoice_line_ids.new() as line:
line.name = "new test line"
line.quantity = 1.0
line.price_unit = 1000.0
self.invoice = invoice_form.save()
self.spread.write(
{
"estimated_amount": 1000.0,
"period_number": 12,
"period_type": "month",
"spread_date": datetime.date(2017, 1, 7),
"invoice_line_id": self.vendor_bill_line.id,
"move_line_auto_post": False,
}
)
self.assertFalse(any(line.move_id for line in self.spread.line_ids))
self.spread.compute_spread_board()
spread_lines = self.spread.line_ids
for line in spread_lines:
line.create_move()
self.assertTrue(line.move_id)
expense_account = self.spread.debit_account_id
balance_sheet = self.spread.credit_account_id
self.assertTrue(balance_sheet.reconcile)
spread_mls = self.spread.line_ids.mapped("move_id.line_ids")
self.assertTrue(spread_mls)
for spread_ml in spread_mls:
if spread_ml.debit:
self.assertEqual(spread_ml.account_id, expense_account)
if spread_ml.credit:
self.assertEqual(spread_ml.account_id, balance_sheet)
# Validate invoice
self.vendor_bill.action_post()
invoice_mls = self.vendor_bill.line_ids
self.assertTrue(invoice_mls)
count_balance_sheet = len(
invoice_mls.filtered(lambda x: x.account_id == balance_sheet)
)
self.assertEqual(count_balance_sheet, 1)
self.spread.company_id.force_move_auto_post = True
self.spread.line_ids.create_and_reconcile_moves()
spread_mls = self.spread.line_ids.mapped("move_id.line_ids")
self.assertTrue(spread_mls)
for spread_ml in spread_mls:
if spread_ml.credit:
self.assertEqual(spread_ml.account_id, balance_sheet)
self.assertTrue(spread_ml.full_reconcile_id)
if spread_ml.debit:
self.assertEqual(spread_ml.account_id, expense_account)
self.assertFalse(spread_ml.full_reconcile_id)
action_posted_view = self.spread.open_posted_view()
self.assertTrue(isinstance(action_posted_view, dict))
self.assertTrue(action_posted_view.get("domain")[0][2])
self.assertTrue(action_posted_view.get("context"))
action_spread_details = self.vendor_bill_line.spread_details()
self.assertTrue(isinstance(action_spread_details, dict))
self.assertTrue(action_spread_details.get("res_id"))
def test_12_link_invoice_line_with_spread_sheet_full_reconcile(self):
# Validate invoice
self.sale_invoice.action_post()
self.spread2.write(
{
"estimated_amount": 1000.0,
"period_number": 12,
"period_type": "month",
"spread_date": datetime.date(2017, 1, 7),
"invoice_line_id": self.invoice_line.id,
"move_line_auto_post": False,
}
)
self.assertFalse(any(line.move_id for line in self.spread2.line_ids))
self.spread2.compute_spread_board()
spread_lines = self.spread2.line_ids
for line in spread_lines:
line.create_move()
self.assertTrue(line.move_id)
payable_account = self.spread2.credit_account_id
balance_sheet = self.spread2.debit_account_id
self.assertTrue(balance_sheet.reconcile)
spread_mls = self.spread2.line_ids.mapped("move_id.line_ids")
self.assertTrue(spread_mls)
for spread_ml in spread_mls:
if spread_ml.debit:
self.assertEqual(spread_ml.account_id, balance_sheet)
self.assertFalse(spread_ml.reconciled)
self.assertFalse(spread_ml.full_reconcile_id)
if spread_ml.credit:
self.assertEqual(spread_ml.account_id, payable_account)
self.assertFalse(spread_ml.reconciled)
self.assertFalse(spread_ml.full_reconcile_id)
invoice_mls = self.sale_invoice.invoice_line_ids
self.assertTrue(invoice_mls)
for invoice_ml in invoice_mls:
self.assertEqual(invoice_ml.account_id, balance_sheet)
action_posted_view = self.spread2.open_posted_view()
self.assertTrue(isinstance(action_posted_view, dict))
self.assertTrue(action_posted_view.get("domain")[0][2])
self.assertFalse(action_posted_view.get("res_id"))
self.assertTrue(action_posted_view.get("context"))
action_spread_details = self.invoice_line.spread_details()
self.assertTrue(isinstance(action_spread_details, dict))
self.assertTrue(action_spread_details.get("res_id"))
def test_13_link_invoice_line_with_spread_sheet_partial_reconcile(self):
self.spread2.write(
{
"estimated_amount": 1000.0,
"period_number": 12,
"period_type": "month",
"spread_date": datetime.date(2017, 1, 7),
}
)
self.spread2.compute_spread_board()
spread_lines = self.spread2.line_ids
self.assertEqual(len(spread_lines), 13)
self.assertFalse(any(line.move_id for line in spread_lines))
spread_lines[0].create_and_reconcile_moves()
spread_lines[1].create_and_reconcile_moves()
spread_lines[2].create_and_reconcile_moves()
spread_lines[3].create_and_reconcile_moves()
self.assertEqual(spread_lines[0].move_id.state, "posted")
self.assertEqual(spread_lines[1].move_id.state, "posted")
self.assertEqual(spread_lines[2].move_id.state, "posted")
self.assertEqual(spread_lines[3].move_id.state, "posted")
self.assertNotEqual(spread_lines[4].move_id.state, "posted")
spread_mls = spread_lines[0].move_id.line_ids
self.assertTrue(spread_mls)
for spread_ml in spread_mls:
if spread_ml.debit:
self.assertFalse(spread_ml.matched_debit_ids)
self.assertFalse(spread_ml.matched_credit_ids)
self.assertFalse(spread_ml.full_reconcile_id)
if spread_ml.credit:
self.assertFalse(spread_ml.matched_debit_ids)
self.assertFalse(spread_ml.matched_credit_ids)
self.assertFalse(spread_ml.full_reconcile_id)
balance_sheet = self.spread2.debit_account_id
self.assertTrue(balance_sheet.reconcile)
# Assing invoice line to spread
self.spread2.invoice_line_id = self.invoice_line
self.assertEqual(self.invoice_line.spread_id, self.spread2)
# Validate invoice
self.sale_invoice.action_post()
invoice_mls = self.sale_invoice.invoice_line_ids
self.assertTrue(invoice_mls)
for invoice_ml in invoice_mls:
self.assertEqual(invoice_ml.account_id, balance_sheet)
spread_lines = self.spread2.line_ids
spread_lines[4].create_and_reconcile_moves()
self.assertEqual(spread_lines[4].move_id.state, "posted")
spread_mls = spread_lines[4].move_id.line_ids
self.assertTrue(spread_mls)
for spread_ml in spread_mls:
if spread_ml.debit:
self.assertFalse(spread_ml.matched_debit_ids)
self.assertTrue(spread_ml.matched_credit_ids)
self.assertFalse(spread_ml.full_reconcile_id)
if spread_ml.credit:
self.assertFalse(spread_ml.matched_debit_ids)
self.assertFalse(spread_ml.matched_credit_ids)
self.assertFalse(spread_ml.full_reconcile_id)
other_journal = self.env["account.journal"].create(
{"name": "Other Journal", "type": "general", "code": "test2"}
)
with self.assertRaises(ValidationError):
self.spread2.journal_id = other_journal
with self.assertRaises(UserError):
self.spread2.unlink()
def test_14_create_all_moves(self):
self.spread.compute_spread_board()
self.assertEqual(len(self.spread.line_ids), 12)
self.assertFalse(any(line.move_id for line in self.spread.line_ids))
# create moves for all the spread lines
self.spread.create_all_moves()
self.assertTrue(all(line.move_id for line in self.spread.line_ids))
with self.assertRaises(ValidationError):
self.spread.unlink()
def test_15_invoice_refund(self):
self.vendor_bill_line.spread_id = self.spread
# Validate invoice
self.vendor_bill.action_post()
self.assertTrue(self.vendor_bill.invoice_line_ids.mapped("spread_id"))
# Create a refund for invoice.
move_reversal = (
self.env["account.move.reversal"]
.with_context(active_model="account.move", active_ids=self.vendor_bill.ids)
.create(
{
"date": fields.Date.today(),
"reason": "no reason",
"refund_method": "refund",
"journal_id": self.vendor_bill.journal_id.id,
}
)
)
reversal = move_reversal.reverse_moves()
refund = self.env["account.move"].browse(reversal["res_id"])
self.assertFalse(refund.invoice_line_ids.mapped("spread_id"))

View File

@ -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)

View File

@ -0,0 +1,725 @@
# Copyright 2017-2020 Onestein (<https://www.onestein.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import datetime
from odoo.exceptions import UserError
from odoo.tests import Form, common
class TestComputeSpreadBoard(common.TransactionCase):
def setUp(self):
super().setUp()
journal = self.env["account.journal"].create(
{"name": "Test", "type": "general", "code": "test"}
)
self.receivable_account = self.env["account.account"].create(
{
"name": "test_account_receivable",
"code": "123",
"account_type": "asset_receivable",
"reconcile": True,
}
)
self.expense_account = self.env["account.account"].create(
{
"name": "test account_expenses",
"code": "765",
"account_type": "expense",
"reconcile": True,
}
)
self.spread_account = self.env["account.account"].create(
{
"name": "test spread account_expenses",
"code": "321",
"account_type": "expense",
"reconcile": True,
}
)
self.spread = self.env["account.spread"].create(
{
"name": "test",
"debit_account_id": self.spread_account.id,
"credit_account_id": self.expense_account.id,
"period_number": 12,
"period_type": "month",
"spread_date": "2017-02-01",
"estimated_amount": 1000.0,
"journal_id": journal.id,
"invoice_type": "in_invoice",
}
)
self.spread2 = self.env["account.spread"].create(
{
"name": "test2",
"debit_account_id": self.spread_account.id,
"credit_account_id": self.expense_account.id,
"period_number": 12,
"period_type": "month",
"spread_date": "2017-02-01",
"estimated_amount": 1000.0,
"journal_id": journal.id,
"invoice_type": "out_invoice",
}
)
self.spread3 = self.env["account.spread"].create(
{
"name": "test by cal days",
"debit_account_id": self.spread_account.id,
"credit_account_id": self.expense_account.id,
"period_number": 12,
"period_type": "month",
"spread_date": "2017-02-01",
"estimated_amount": 12000.0,
"journal_id": journal.id,
"invoice_type": "out_invoice",
"days_calc": True,
}
)
self.template = self.env["account.spread.template"].create(
{
"name": "test",
"spread_type": "purchase",
"period_number": 5,
"period_type": "month",
"start_date": "2017-01-01",
"spread_account_id": self.spread_account.id,
"spread_journal_id": journal.id,
"days_calc": True,
}
)
def test_01_supplier_invoice(self):
self.spread.compute_spread_board()
spread_lines = self.spread.line_ids
self.assertEqual(len(spread_lines), 12)
self.assertEqual(83.33, spread_lines[0].amount)
self.assertEqual(83.33, spread_lines[1].amount)
self.assertEqual(83.33, spread_lines[2].amount)
self.assertEqual(83.33, spread_lines[3].amount)
self.assertEqual(83.33, spread_lines[4].amount)
self.assertEqual(83.33, spread_lines[5].amount)
self.assertEqual(83.33, spread_lines[6].amount)
self.assertEqual(83.33, spread_lines[7].amount)
self.assertEqual(83.33, spread_lines[8].amount)
self.assertEqual(83.33, spread_lines[9].amount)
self.assertEqual(83.33, spread_lines[10].amount)
self.assertEqual(83.37, spread_lines[11].amount)
self.assertEqual(datetime.date(2017, 2, 28), spread_lines[0].date)
self.assertEqual(datetime.date(2017, 3, 31), spread_lines[1].date)
self.assertEqual(datetime.date(2017, 4, 30), spread_lines[2].date)
self.assertEqual(datetime.date(2017, 5, 31), spread_lines[3].date)
self.assertEqual(datetime.date(2017, 6, 30), spread_lines[4].date)
self.assertEqual(datetime.date(2017, 7, 31), spread_lines[5].date)
self.assertEqual(datetime.date(2017, 8, 31), spread_lines[6].date)
self.assertEqual(datetime.date(2017, 9, 30), spread_lines[7].date)
self.assertEqual(datetime.date(2017, 10, 31), spread_lines[8].date)
self.assertEqual(datetime.date(2017, 11, 30), spread_lines[9].date)
self.assertEqual(datetime.date(2017, 12, 31), spread_lines[10].date)
self.assertEqual(datetime.date(2018, 1, 31), spread_lines[11].date)
for line in spread_lines:
self.assertFalse(line.move_id)
self.assertEqual(self.spread.unspread_amount, 1000.0)
self.assertEqual(self.spread.unposted_amount, 1000.0)
for line in spread_lines:
line.create_move()
self.assertTrue(line.move_id)
self.spread.action_recalculate_spread()
spread_lines = self.spread.line_ids
for line in spread_lines:
self.assertTrue(line.move_id)
def test_02_supplier_invoice(self):
# spread date set
self.spread.write(
{
"period_number": 12,
"period_type": "month",
"spread_date": datetime.date(2017, 1, 7),
}
)
self.spread_account.reconcile = True
self.assertTrue(self.spread_account.reconcile)
self.spread.compute_spread_board()
spread_lines = self.spread.line_ids
self.assertEqual(len(spread_lines), 13)
self.assertEqual(67.20, spread_lines[0].amount)
self.assertEqual(83.33, spread_lines[1].amount)
self.assertEqual(83.33, spread_lines[2].amount)
self.assertEqual(83.33, spread_lines[3].amount)
self.assertEqual(83.33, spread_lines[4].amount)
self.assertEqual(83.33, spread_lines[5].amount)
self.assertEqual(83.33, spread_lines[6].amount)
self.assertEqual(83.33, spread_lines[7].amount)
self.assertEqual(83.33, spread_lines[8].amount)
self.assertEqual(83.33, spread_lines[9].amount)
self.assertEqual(83.33, spread_lines[10].amount)
self.assertEqual(83.33, spread_lines[11].amount)
self.assertEqual(16.17, spread_lines[12].amount)
self.assertEqual(datetime.date(2017, 1, 31), spread_lines[0].date)
self.assertEqual(datetime.date(2017, 2, 28), spread_lines[1].date)
self.assertEqual(datetime.date(2017, 3, 31), spread_lines[2].date)
self.assertEqual(datetime.date(2017, 4, 30), spread_lines[3].date)
self.assertEqual(datetime.date(2017, 5, 31), spread_lines[4].date)
self.assertEqual(datetime.date(2017, 6, 30), spread_lines[5].date)
self.assertEqual(datetime.date(2017, 7, 31), spread_lines[6].date)
self.assertEqual(datetime.date(2017, 8, 31), spread_lines[7].date)
self.assertEqual(datetime.date(2017, 9, 30), spread_lines[8].date)
self.assertEqual(datetime.date(2017, 10, 31), spread_lines[9].date)
self.assertEqual(datetime.date(2017, 11, 30), spread_lines[10].date)
self.assertEqual(datetime.date(2017, 12, 31), spread_lines[11].date)
self.assertEqual(datetime.date(2018, 1, 31), spread_lines[12].date)
for line in spread_lines:
self.assertFalse(line.move_id)
self.assertEqual(self.spread.unspread_amount, 1000.0)
self.assertEqual(self.spread.unposted_amount, 1000.0)
self.spread.line_ids.create_and_reconcile_moves()
for line in self.spread.line_ids:
self.assertTrue(line.move_id)
def test_03_supplier_invoice(self):
# spread date set
self.spread.write(
{
"period_number": 12,
"period_type": "month",
"spread_date": datetime.date(2017, 1, 31),
"move_line_auto_post": False,
}
)
self.spread.compute_spread_board()
spread_lines = self.spread.line_ids
self.assertEqual(len(spread_lines), 13)
self.assertEqual(2.69, spread_lines[0].amount)
self.assertEqual(83.33, spread_lines[1].amount)
self.assertEqual(83.33, spread_lines[2].amount)
self.assertEqual(83.33, spread_lines[3].amount)
self.assertEqual(83.33, spread_lines[4].amount)
self.assertEqual(83.33, spread_lines[5].amount)
self.assertEqual(83.33, spread_lines[6].amount)
self.assertEqual(83.33, spread_lines[7].amount)
self.assertEqual(83.33, spread_lines[8].amount)
self.assertEqual(83.33, spread_lines[9].amount)
self.assertEqual(83.33, spread_lines[10].amount)
self.assertEqual(83.33, spread_lines[11].amount)
self.assertEqual(80.68, spread_lines[12].amount)
self.assertEqual(datetime.date(2017, 1, 31), spread_lines[0].date)
self.assertEqual(datetime.date(2017, 2, 28), spread_lines[1].date)
self.assertEqual(datetime.date(2017, 3, 31), spread_lines[2].date)
self.assertEqual(datetime.date(2017, 4, 30), spread_lines[3].date)
self.assertEqual(datetime.date(2017, 5, 31), spread_lines[4].date)
self.assertEqual(datetime.date(2017, 6, 30), spread_lines[5].date)
self.assertEqual(datetime.date(2017, 7, 31), spread_lines[6].date)
self.assertEqual(datetime.date(2017, 8, 31), spread_lines[7].date)
self.assertEqual(datetime.date(2017, 9, 30), spread_lines[8].date)
self.assertEqual(datetime.date(2017, 10, 31), spread_lines[9].date)
self.assertEqual(datetime.date(2017, 11, 30), spread_lines[10].date)
self.assertEqual(datetime.date(2017, 12, 31), spread_lines[11].date)
self.assertEqual(datetime.date(2018, 1, 31), spread_lines[12].date)
for line in spread_lines:
self.assertFalse(line.move_id)
self.assertEqual(self.spread.unspread_amount, 1000.0)
self.assertEqual(self.spread.unposted_amount, 1000.0)
spread_lines[0].create_move()
spread_lines[1].create_move()
spread_lines[2].create_move()
self.assertTrue(any(line.move_id for line in spread_lines))
self.assertTrue(any(not line.move_id for line in spread_lines))
self.spread._compute_amounts()
self.assertEqual(self.spread.unspread_amount, 830.65)
self.assertEqual(self.spread.unposted_amount, 1000.0)
self.spread.compute_spread_board()
spread_lines = self.spread.line_ids
self.assertEqual(len(spread_lines), 13)
self.assertEqual(2.69, spread_lines[0].amount)
self.assertEqual(83.33, spread_lines[1].amount)
self.assertEqual(83.33, spread_lines[2].amount)
self.assertEqual(83.33, spread_lines[3].amount)
self.assertEqual(83.33, spread_lines[4].amount)
self.assertEqual(83.33, spread_lines[5].amount)
self.assertEqual(83.33, spread_lines[6].amount)
self.assertEqual(83.33, spread_lines[7].amount)
self.assertEqual(83.33, spread_lines[8].amount)
self.assertEqual(83.33, spread_lines[9].amount)
self.assertEqual(83.33, spread_lines[10].amount)
self.assertEqual(83.33, spread_lines[11].amount)
self.assertEqual(80.68, spread_lines[12].amount)
self.assertEqual(datetime.date(2017, 1, 31), spread_lines[0].date)
self.assertEqual(datetime.date(2017, 2, 28), spread_lines[1].date)
self.assertEqual(datetime.date(2017, 3, 31), spread_lines[2].date)
self.assertEqual(datetime.date(2017, 4, 30), spread_lines[3].date)
self.assertEqual(datetime.date(2017, 5, 31), spread_lines[4].date)
self.assertEqual(datetime.date(2017, 6, 30), spread_lines[5].date)
self.assertEqual(datetime.date(2017, 7, 31), spread_lines[6].date)
self.assertEqual(datetime.date(2017, 8, 31), spread_lines[7].date)
self.assertEqual(datetime.date(2017, 9, 30), spread_lines[8].date)
self.assertEqual(datetime.date(2017, 10, 31), spread_lines[9].date)
self.assertEqual(datetime.date(2017, 11, 30), spread_lines[10].date)
self.assertEqual(datetime.date(2017, 12, 31), spread_lines[11].date)
self.assertEqual(datetime.date(2018, 1, 31), spread_lines[12].date)
def test_04_supplier_invoice(self):
self.spread.write(
{
"credit_account_id": self.expense_account.id,
"debit_account_id": self.spread_account.id,
"period_number": 3,
"period_type": "year",
"spread_date": datetime.date(2018, 10, 24),
}
)
# change the state of invoice to open by clicking Validate button
self.spread.compute_spread_board()
spread_lines = self.spread.line_ids
self.assertEqual(len(spread_lines), 4)
self.assertEqual(333.33, spread_lines[1].amount)
self.assertEqual(333.33, spread_lines[2].amount)
first_amount = spread_lines[0].amount
last_amount = spread_lines[3].amount
remaining_amount = first_amount + last_amount
self.assertAlmostEqual(remaining_amount, 333.34, places=2)
total_line_amount = 0.0
for line in spread_lines:
total_line_amount += line.amount
self.assertAlmostEqual(total_line_amount, 1000.0, places=2)
for line in spread_lines:
self.assertFalse(line.move_id)
self.assertEqual(self.spread.unspread_amount, 1000.0)
self.assertEqual(self.spread.unposted_amount, 1000.0)
def test_05_supplier_invoice(self):
# spread date set
self.spread.write(
{
"period_number": 12,
"period_type": "month",
"spread_date": datetime.date(2017, 2, 1),
}
)
self.spread.compute_spread_board()
# create moves for all the spread lines and open them
self.spread.line_ids.create_and_reconcile_moves()
for spread_line in self.spread.line_ids:
attrs = spread_line.open_move()
self.assertTrue(isinstance(attrs, dict))
# unlink all created moves
self.spread.line_ids.unlink_move()
for spread_line in self.spread.line_ids:
self.assertFalse(spread_line.move_id)
self.assertEqual(self.spread.unspread_amount, 1000.0)
self.assertEqual(self.spread.unposted_amount, 1000.0)
def test_06_supplier_invoice(self):
# spread date set
self.spread.write(
{"period_number": 3, "period_type": "quarter", "move_line_auto_post": False}
)
self.spread.compute_spread_board()
# create moves for all the spread lines and open them
self.spread.line_ids.create_and_reconcile_moves()
# check move lines
for spread_line in self.spread.line_ids:
for move_line in spread_line.move_id.line_ids:
spread_account = self.spread.debit_account_id
if move_line.account_id == spread_account:
debit = move_line.debit
self.assertAlmostEqual(debit, spread_line.amount)
for line in self.spread.line_ids:
self.assertTrue(line.move_id)
self.assertFalse(line.move_id.state == "posted")
self.assertEqual(self.spread.unspread_amount, 0.0)
self.assertEqual(self.spread.unposted_amount, 1000.0)
# try to create move lines again: an error is raised
for line in self.spread.line_ids:
with self.assertRaises(UserError):
line.create_move()
self.spread.write({"move_line_auto_post": True})
self.spread.action_recalculate_spread()
for line in self.spread.line_ids:
self.assertTrue(line.move_id)
self.assertTrue(line.move_id.state == "posted")
self.assertEqual(self.spread.unspread_amount, 0.0)
self.assertEqual(self.spread.unposted_amount, 0.0)
def test_07_supplier_invoice(self):
self.spread.write(
{
"period_number": 3,
"period_type": "month",
"spread_date": datetime.date(2017, 1, 1),
"estimated_amount": 345.96,
}
)
self.spread.compute_spread_board()
spread_lines = self.spread.line_ids
self.assertEqual(len(spread_lines), 3)
self.assertAlmostEqual(115.32, spread_lines[0].amount)
self.assertAlmostEqual(115.32, spread_lines[1].amount)
self.assertAlmostEqual(115.32, spread_lines[2].amount)
self.assertEqual(datetime.date(2017, 1, 31), spread_lines[0].date)
self.assertEqual(datetime.date(2017, 2, 28), spread_lines[1].date)
self.assertEqual(datetime.date(2017, 3, 31), spread_lines[2].date)
for line in spread_lines:
self.assertFalse(line.move_id)
self.assertEqual(self.spread.unspread_amount, 345.96)
self.assertEqual(self.spread.unposted_amount, 345.96)
def test_08_supplier_invoice(self):
# spread date set
self.spread.write(
{
"period_number": 12,
"period_type": "month",
"spread_date": datetime.date(2017, 2, 1),
}
)
self.spread.compute_spread_board()
self.assertTrue(self.spread.line_ids)
self.spread.action_undo_spread()
self.assertFalse(self.spread.line_ids)
self.assertEqual(self.spread.unspread_amount, 1000.0)
self.assertEqual(self.spread.unposted_amount, 1000.0)
def test_09_supplier_invoice(self):
# spread date set
self.spread.write(
{
"period_number": 12,
"period_type": "month",
"spread_date": datetime.date(2017, 2, 1),
}
)
self.spread.compute_spread_board()
for line in self.spread.line_ids:
line.create_move()
self.assertTrue(line.move_id)
action = line.open_move()
self.assertTrue(action)
self.spread.line_ids.unlink_move()
for line in self.spread.line_ids:
self.assertFalse(line.move_id)
self.assertTrue(self.spread.line_ids)
self.assertEqual(self.spread.unspread_amount, 1000.0)
self.assertEqual(self.spread.unposted_amount, 1000.0)
def test_10_create_entries(self):
self.env["account.spread.line"]._create_entries()
self.assertFalse(self.spread.line_ids)
self.spread.compute_spread_board()
self.env["account.spread.line"]._create_entries()
self.assertTrue(self.spread.line_ids)
for line in self.spread.line_ids:
self.assertTrue(line.move_id)
def test_11_create_move_sale_invoice(self):
self.spread2.move_line_auto_post = False
self.spread2.compute_spread_board()
for line in self.spread2.line_ids:
self.assertFalse(line.move_id)
line.create_move()
self.assertTrue(line.move_id)
self.assertFalse(line.move_id.state == "posted")
self.spread2.action_undo_spread()
for line in self.spread2.line_ids:
self.assertFalse(line.move_id)
self.spread2.action_recalculate_spread()
for line in self.spread2.line_ids:
self.assertTrue(line.move_id)
self.assertTrue(line.move_id)
self.assertFalse(line.move_id.state == "posted")
# try to create move lines again: an error is raised
with self.assertRaises(UserError):
line.create_move()
def test_12_supplier_invoice_auto_post(self):
# spread date set
self.spread.write(
{"period_number": 8, "period_type": "month", "move_line_auto_post": True}
)
self.spread.compute_spread_board()
# create moves for all the spread lines and open them
self.spread.line_ids.create_and_reconcile_moves()
# check move lines
for spread_line in self.spread.line_ids:
for move_line in spread_line.move_id.line_ids:
spread_account = self.spread.debit_account_id
if move_line.account_id == spread_account:
debit = move_line.debit
self.assertAlmostEqual(debit, spread_line.amount)
self.assertTrue(self.spread.move_line_auto_post)
for line in self.spread.line_ids:
self.assertTrue(line.move_id)
self.assertTrue(line.move_id.state == "posted")
self.assertEqual(self.spread.unspread_amount, 0.0)
self.assertEqual(self.spread.unposted_amount, 0.0)
def test_13_create_move_in_invoice_auto_post(self):
self.spread2.write({"period_number": 4, "move_line_auto_post": True})
self.spread_account.reconcile = True
self.assertTrue(self.spread_account.reconcile)
self.spread2.compute_spread_board()
for line in self.spread2.line_ids:
self.assertFalse(line.move_id)
line.create_move()
self.assertTrue(line.move_id)
self.assertTrue(line.move_id.state == "posted")
self.assertEqual(self.spread.unspread_amount, 1000.0)
self.assertEqual(self.spread.unposted_amount, 1000.0)
def test_14_negative_amount(self):
# spread date set
self.spread.write(
{
"estimated_amount": -1000.0,
"period_number": 12,
"period_type": "month",
"spread_date": datetime.date(2017, 1, 7),
}
)
self.spread.compute_spread_board()
spread_lines = self.spread.line_ids
self.assertTrue(spread_lines)
def test_15_compute_spread_board_line_account_deprecated(self):
self.spread.debit_account_id.deprecated = True
self.assertTrue(self.spread.debit_account_id.deprecated)
self.assertTrue(self.spread.is_debit_account_deprecated)
self.spread.compute_spread_board()
self.assertEqual(self.spread.unspread_amount, 1000.0)
self.assertEqual(self.spread.unposted_amount, 1000.0)
def test_16_compute_spread_board_line_account_deprecated(self):
self.spread.credit_account_id.deprecated = True
self.assertTrue(self.spread.credit_account_id.deprecated)
self.assertTrue(self.spread.is_credit_account_deprecated)
self.spread.compute_spread_board()
self.assertEqual(self.spread.unspread_amount, 1000.0)
self.assertEqual(self.spread.unposted_amount, 1000.0)
def test_17_compute_spread_board_line_account_deprecated(self):
self.spread.compute_spread_board()
self.spread.debit_account_id.deprecated = True
self.assertTrue(self.spread.debit_account_id.deprecated)
for line in self.spread.line_ids:
self.assertFalse(line.move_id)
with self.assertRaises(UserError):
line.create_move()
self.assertEqual(self.spread.unspread_amount, 1000.0)
self.assertEqual(self.spread.unposted_amount, 1000.0)
def test_18_supplier_invoice(self):
# spread date set
self.spread.write(
{
"period_number": 12,
"period_type": "month",
"spread_date": datetime.date(2017, 1, 7),
}
)
self.spread_account.reconcile = True
self.assertTrue(self.spread_account.reconcile)
self.spread.compute_spread_board()
spread_lines = self.spread.line_ids
self.assertEqual(len(spread_lines), 13)
for line in spread_lines:
self.assertFalse(line.move_id)
spread_lines[0]._create_moves().action_post()
spread_lines[1]._create_moves().action_post()
spread_lines[2]._create_moves().action_post()
spread_lines[3]._create_moves().action_post()
self.assertEqual(spread_lines[0].move_id.state, "posted")
self.assertEqual(spread_lines[1].move_id.state, "posted")
self.assertEqual(spread_lines[2].move_id.state, "posted")
self.assertEqual(spread_lines[3].move_id.state, "posted")
self.assertAlmostEqual(self.spread.unspread_amount, 682.81)
self.assertAlmostEqual(self.spread.unposted_amount, 682.81)
self.spread.compute_spread_board()
spread_lines = self.spread.line_ids
self.assertEqual(len(spread_lines), 13)
self.assertEqual(67.20, spread_lines[0].amount)
self.assertEqual(83.33, spread_lines[1].amount)
self.assertEqual(83.33, spread_lines[2].amount)
self.assertEqual(83.33, spread_lines[3].amount)
self.assertEqual(83.33, spread_lines[4].amount)
self.assertEqual(83.33, spread_lines[5].amount)
self.assertEqual(83.33, spread_lines[6].amount)
self.assertEqual(83.33, spread_lines[7].amount)
self.assertEqual(83.33, spread_lines[8].amount)
self.assertEqual(83.33, spread_lines[9].amount)
self.assertEqual(83.33, spread_lines[10].amount)
self.assertEqual(83.33, spread_lines[11].amount)
self.assertEqual(16.17, spread_lines[12].amount)
self.assertEqual(datetime.date(2017, 1, 31), spread_lines[0].date)
self.assertEqual(datetime.date(2017, 2, 28), spread_lines[1].date)
self.assertEqual(datetime.date(2017, 3, 31), spread_lines[2].date)
self.assertEqual(datetime.date(2017, 4, 30), spread_lines[3].date)
self.assertEqual(datetime.date(2017, 5, 31), spread_lines[4].date)
self.assertEqual(datetime.date(2017, 6, 30), spread_lines[5].date)
self.assertEqual(datetime.date(2017, 7, 31), spread_lines[6].date)
self.assertEqual(datetime.date(2017, 8, 31), spread_lines[7].date)
self.assertEqual(datetime.date(2017, 9, 30), spread_lines[8].date)
self.assertEqual(datetime.date(2017, 10, 31), spread_lines[9].date)
self.assertEqual(datetime.date(2017, 11, 30), spread_lines[10].date)
self.assertEqual(datetime.date(2017, 12, 31), spread_lines[11].date)
self.assertEqual(datetime.date(2018, 1, 31), spread_lines[12].date)
self.assertAlmostEqual(self.spread.unspread_amount, 682.81)
self.assertAlmostEqual(self.spread.unposted_amount, 682.81)
def test_19_supplier_invoice_calc_day(self):
self.assertTrue(self.spread3.days_calc)
self.spread3.compute_spread_board()
spread_lines = self.spread3.line_ids
self.assertEqual(len(spread_lines), 12)
# Calculate by day has formula:
# (amount spread cost / all spread cost day) * day of <period_type>
self.assertAlmostEqual(920.55, spread_lines[0].amount)
self.assertAlmostEqual(1019.18, spread_lines[1].amount)
self.assertAlmostEqual(986.30, spread_lines[2].amount)
self.assertAlmostEqual(1019.18, spread_lines[3].amount)
self.assertAlmostEqual(986.30, spread_lines[4].amount)
self.assertAlmostEqual(1019.18, spread_lines[5].amount)
self.assertAlmostEqual(1019.18, spread_lines[6].amount)
self.assertAlmostEqual(986.30, spread_lines[7].amount)
self.assertAlmostEqual(1019.18, spread_lines[8].amount)
self.assertAlmostEqual(986.30, spread_lines[9].amount)
self.assertAlmostEqual(1019.18, spread_lines[10].amount)
self.assertAlmostEqual(1019.17, spread_lines[11].amount) # total left
self.assertEqual(datetime.date(2017, 2, 28), spread_lines[0].date)
self.assertEqual(datetime.date(2017, 3, 31), spread_lines[1].date)
self.assertEqual(datetime.date(2017, 4, 30), spread_lines[2].date)
self.assertEqual(datetime.date(2017, 5, 31), spread_lines[3].date)
self.assertEqual(datetime.date(2017, 6, 30), spread_lines[4].date)
self.assertEqual(datetime.date(2017, 7, 31), spread_lines[5].date)
self.assertEqual(datetime.date(2017, 8, 31), spread_lines[6].date)
self.assertEqual(datetime.date(2017, 9, 30), spread_lines[7].date)
self.assertEqual(datetime.date(2017, 10, 31), spread_lines[8].date)
self.assertEqual(datetime.date(2017, 11, 30), spread_lines[9].date)
self.assertEqual(datetime.date(2017, 12, 31), spread_lines[10].date)
self.assertEqual(datetime.date(2018, 1, 31), spread_lines[11].date)
# Period Type is 'Quarter'
self.spread3.period_type = "quarter"
self.spread3.compute_spread_board()
spread_lines = self.spread3.line_ids
self.assertEqual(len(spread_lines), 12)
self.assertAlmostEqual(325.27, spread_lines[0].amount)
self.assertAlmostEqual(1068.73, spread_lines[1].amount)
self.assertAlmostEqual(1068.73, spread_lines[2].amount)
self.assertAlmostEqual(1057.12, spread_lines[3].amount)
self.assertAlmostEqual(1045.50, spread_lines[4].amount)
self.assertAlmostEqual(1068.73, spread_lines[5].amount)
self.assertAlmostEqual(1068.73, spread_lines[6].amount)
self.assertAlmostEqual(1057.12, spread_lines[7].amount)
self.assertAlmostEqual(1045.50, spread_lines[8].amount)
self.assertAlmostEqual(1068.73, spread_lines[9].amount)
self.assertAlmostEqual(1068.73, spread_lines[10].amount)
self.assertAlmostEqual(1057.11, spread_lines[11].amount) # total left
# Period Type is 'Year' and spread date is not first month
self.spread3.period_type = "year"
self.spread3.spread_date = "2017-02-02"
self.spread3.compute_spread_board()
spread_lines = self.spread3.line_ids
self.assertEqual(len(spread_lines), 13)
self.assertAlmostEqual(73.92, spread_lines[0].amount)
self.assertAlmostEqual(999.32, spread_lines[1].amount)
self.assertAlmostEqual(999.32, spread_lines[2].amount)
self.assertAlmostEqual(1002.05, spread_lines[3].amount)
self.assertAlmostEqual(999.32, spread_lines[4].amount)
self.assertAlmostEqual(999.32, spread_lines[5].amount)
self.assertAlmostEqual(999.32, spread_lines[6].amount)
self.assertAlmostEqual(1002.05, spread_lines[7].amount)
self.assertAlmostEqual(999.32, spread_lines[8].amount)
self.assertAlmostEqual(999.32, spread_lines[9].amount)
self.assertAlmostEqual(999.32, spread_lines[10].amount)
self.assertAlmostEqual(1002.05, spread_lines[11].amount)
self.assertAlmostEqual(925.37, spread_lines[12].amount)
def test_20_supplier_invoice_template(self):
"""Test onchange template"""
self.assertEqual(self.spread3.invoice_type, "out_invoice")
with Form(self.spread3) as sp:
sp.template_id = self.template
sp.credit_account_id = self.expense_account
sp.save()
self.assertEqual(self.spread3.invoice_type, "in_invoice")

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_invoice_spread" model="ir.ui.view">
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form" />
<field name="arch" type="xml">
<xpath
expr="//field[@name='invoice_line_ids']/tree/field[@name='quantity']"
position="before"
>
<field
name="spread_check"
invisible="1"
groups="account.group_account_user,account.group_account_manager"
/>
<button
name="spread_details"
type="object"
class="btn btn-link"
icon="fa-arrow-circle-right"
title="Not linked to any spread board"
attrs="{'invisible': [('spread_check', '!=', 'unlinked')]}"
groups="account.group_account_user,account.group_account_manager"
/>
<button
name="spread_details"
type="object"
class="btn btn-success"
icon="fa-arrow-circle-right"
title="Linked to spread board"
attrs="{'invisible': [('spread_check', '!=', 'linked')]}"
groups="account.group_account_user,account.group_account_manager"
/>
</xpath>
<xpath
expr="//field[@name='line_ids']/tree//button[@name='action_automatic_entry']"
position="after"
>
<field
name="spread_check"
invisible="1"
groups="account.group_account_user,account.group_account_manager"
/>
<button
name="spread_details"
type="object"
class="btn btn-success"
icon="fa-arrow-circle-right"
title="Linked to spread board"
groups="account.group_account_user,account.group_account_manager"
attrs="{'invisible': [('spread_check', '!=', 'linked')], 'column_invisible': ['|', ('parent.move_type', '=', 'entry'), ('parent.state', '!=', 'posted')]}"
/>
</xpath>
</field>
</record>
<record id="action_account_moves_all_spread" model="ir.actions.act_window">
<field
name="context"
>{'journal_type':'general', 'search_default_group_by_move': 0, 'search_default_posted':1, 'name_groupby':1}</field>
<field name="name">Journal Items</field>
<field name="res_model">account.move.line</field>
<field
name="domain"
>[('display_type', 'not in', ('line_section', 'line_note'))]</field>
<field name="view_id" ref="account.view_move_line_tree" />
<field name="view_mode">tree,pivot,graph,form,kanban</field>
</record>
</odoo>

View File

@ -0,0 +1,386 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_account_spread" model="ir.ui.view">
<field name="model">account.spread</field>
<field name="arch" type="xml">
<form>
<header>
<button
name="compute_spread_board"
type="object"
string="Recalculate unposted lines"
class="oe_highlight"
attrs="{'invisible': ['|',('debit_account_id', '=', False),('display_recompute_buttons', '=', False)]}"
/>
<button
name="action_recalculate_spread"
type="object"
string="Recalculate entire spread"
attrs="{'invisible': ['|',('debit_account_id', '=', False),('display_recompute_buttons', '=', False)]}"
groups="account.group_account_manager"
/>
<button
name="action_undo_spread"
type="object"
string="Undo spread"
attrs="{'invisible': [('line_ids', '=', [])]}"
groups="account.group_account_manager"
/>
<button
name="action_unlink_invoice_line"
type="object"
string="Unlink Invoice Line"
attrs="{'invisible': [('invoice_line_id', '=', False)]}"
groups="account.group_account_manager"
/>
</header>
<sheet>
<div name="button_box" class="oe_button_box">
<button
name="open_posted_view"
class="oe_stat_button"
icon="fa-bars"
type="object"
string="Posted entries"
>
</button>
</div>
<widget
name="web_ribbon"
title="Archived"
bg_color="bg-danger"
attrs="{'invisible': [('active', '=', True)]}"
/>
<div class="oe_title">
<label for="name" string="Spread Board Name" />
<h1>
<field
name="name"
placeholder="e.g. One year offices cleaning contract"
/>
</h1>
</div>
<group name="header_info">
<group name="spread_definitions">
<field name="template_id" />
<field
name="invoice_type"
attrs="{'readonly':[('invoice_line_id','!=',False)]}"
/>
<field name="display_recompute_buttons" invisible="1" />
<field name="display_move_line_auto_post" invisible="1" />
<field name="all_posted" invisible="1" />
<field name="active" invisible="1" />
<field name="use_invoice_line_account" invisible="1" />
</group>
</group>
<group name="accounts">
<group name="debits">
<field name="is_debit_account_deprecated" invisible="1" />
<label
for="debit_account_id"
colspan="3"
string="Balance sheet account / Spread account"
attrs="{'invisible':[('invoice_type','not in',('out_invoice','in_refund'))]}"
/>
<label
for="debit_account_id"
colspan="3"
string="Expense account"
attrs="{'invisible':[('invoice_type','not in',('in_invoice','out_refund'))]}"
/>
<div
attrs="{'invisible': [('use_invoice_line_account', '=', True)]}"
colspan="3"
>
<span
class="help-block"
colspan="2"
attrs="{'invisible':[('invoice_type','not in',('out_invoice','in_refund'))]}"
>
The Balance Sheet account used for the spreading.<br
/>This account is the counterpart of the account in the invoice line.
</span>
<span
class="help-block"
colspan="2"
attrs="{'invisible':[('invoice_type','not in',('in_invoice','out_refund'))]}"
>
The Expense account in the vendor bill line.<br
/>Usually the same account of the vendor bill line.
</span>
</div>
<div
attrs="{'invisible': [('use_invoice_line_account', '!=', True)]}"
colspan="3"
>
<span
class="help-block"
colspan="2"
attrs="{'invisible':[('invoice_type','not in',('out_invoice','in_refund'))]}"
>
The Balance Sheet account.<br
/>This is the account in the invoice line.
</span>
<span
class="help-block"
colspan="2"
attrs="{'invisible':[('invoice_type','not in',('in_invoice','out_refund'))]}"
>
The Expense account used for the spreading.<br
/>This account is the counterpart of the account of the vendor bill line.
</span>
</div>
<field
name="debit_account_id"
required="1"
domain="[('deprecated', '=', False), ('account_type', 'not in', ('asset_cash','liability_credit_card')), ('internal_group', '!=', 'off_balance')]"
attrs="{'readonly':[('invoice_line_id','!=',False)]}"
/>
<span
class="help-block text-danger"
colspan="2"
attrs="{'invisible':[('is_debit_account_deprecated','!=',True)]}"
>
This account in deprecated! The reconciliation will be NOT possible.
</span>
</group>
<group name="credits">
<field name="is_credit_account_deprecated" invisible="1" />
<label
for="credit_account_id"
colspan="3"
string="Revenue account"
attrs="{'invisible':[('invoice_type','not in',('out_invoice','in_refund'))]}"
/>
<label
for="credit_account_id"
colspan="3"
string="Balance sheet account / Spread account"
attrs="{'invisible':[('invoice_type','not in',('in_invoice','out_refund'))]}"
/>
<div
attrs="{'invisible': [('use_invoice_line_account', '=', True)]}"
colspan="3"
>
<span
class="help-block"
colspan="2"
attrs="{'invisible':[('invoice_type','not in',('out_invoice','in_refund'))]}"
>
The Revenue account in the invoice line.<br
/>Usually the same account of the invoice line.
</span>
<span
class="help-block"
colspan="2"
attrs="{'invisible':[('invoice_type','not in',('in_invoice','out_refund'))]}"
>
The Balance Sheet account used for the spreading.<br
/>This account is the counterpart of the account in the vendor bill line.
</span>
</div>
<div
attrs="{'invisible': [('use_invoice_line_account', '!=', True)]}"
colspan="3"
>
<span
class="help-block"
colspan="2"
attrs="{'invisible':[('invoice_type','not in',('out_invoice','in_refund'))]}"
>
The Revenue account used for the spreading.<br
/>This account is the counterpart of the account of the invoice line.
</span>
<span
class="help-block"
colspan="2"
attrs="{'invisible':[('invoice_type','not in',('in_invoice','out_refund'))]}"
>
The Balance Sheet account.<br
/>This is the account in the vendor bill line.
</span>
</div>
<field
name="credit_account_id"
required="1"
domain="[('deprecated', '=', False), ('account_type', 'not in', ('asset_cash','liability_credit_card')), ('internal_group', '!=', 'off_balance')]"
attrs="{'readonly':[('invoice_line_id','!=',False)]}"
/>
<span
class="help-block text-danger"
colspan="2"
attrs="{'invisible':[('is_credit_account_deprecated','!=',True)]}"
>
This account in deprecated! The reconciliation will be NOT possible.
</span>
</group>
</group>
<group name="main_info">
<group>
<field
name="invoice_id"
attrs="{'invisible':[('invoice_id','=',False)]}"
/>
<field
name="invoice_line_id"
readonly="1"
attrs="{'invisible':[('invoice_line_id','=',False)]}"
/>
<field
name="estimated_amount"
attrs="{'readonly':[('invoice_line_id','!=',False)],'invisible':[('estimated_amount','=',0.0),('invoice_line_id','!=',False)]}"
/>
<field
name="total_amount"
attrs="{'invisible':[('invoice_line_id','=',False)]}"
/>
<field
name="move_line_auto_post"
attrs="{'invisible':[('display_move_line_auto_post','=',False)]}"
/>
</group>
<group>
<field name="period_number" />
<field name="period_type" />
<field name="spread_date" />
<field name="days_calc" />
<field name="suitable_journal_ids" invisible="1" />
<field name="journal_id" widget="selection" />
</group>
</group>
<notebook name="notebook">
<page name="spread_lines" string="Spread lines">
<field name="line_ids" readonly="1">
<tree>
<field name="name" readonly="1" />
<field
name="amount"
attrs="{'readonly':[('move_id','!=',False)]}"
sum="Total"
/>
<field name="date" readonly="1" />
<field name="move_id" readonly="1" />
<button
name="create_move"
icon="fa-play"
string="Create Move"
type="object"
groups="account.group_account_manager"
attrs="{'invisible':[('move_id','!=',False)]}"
/>
<button
name="open_move"
icon="fa-plus-square-o"
string="View Move"
type="object"
attrs="{'invisible':[('move_id','=',False)]}"
/>
<button
name="unlink_move"
icon="fa-times"
string="Delete Move"
type="object"
confirm="This will delete the move. Are you sure ?"
groups="account.group_account_manager"
attrs="{'invisible':[('move_id','=',False)]}"
/>
</tree>
</field>
<group name="extension">
<group name="extension_left">
</group>
<group name="extension_right">
<field
name="display_create_all_moves"
invisible="1"
/>
<button
name="create_all_moves"
string="Create All Moves"
type="object"
icon="fa-play"
colspan="2"
attrs="{'invisible':[('display_create_all_moves','!=',True)]}"
/>
<field
name="unspread_amount"
attrs="{'invisible': [('unspread_amount', '=', 0)]}"
/>
<field
name="unposted_amount"
attrs="{'invisible': [('unposted_amount', '=', 0)]}"
/>
<field
name="posted_amount"
attrs="{'invisible': [('posted_amount', '=', 0)]}"
/>
</group>
</group>
</page>
<page name="details" string="Details">
<group name="extra_details">
<group>
<field
name="company_id"
groups="base.group_multi_company"
options="{'no_create': True}"
/>
<field
name="currency_id"
groups="base.group_multi_currency"
/>
</group>
<group>
<field
name="analytic_distribution"
groups="analytic.group_analytic_accounting"
widget="analytic_distribution"
/>
</group>
</group>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers" />
<field name="message_ids" widget="mail_thread" />
</div>
</form>
</field>
</record>
<record id="view_account_spread_tree" model="ir.ui.view">
<field name="model">account.spread</field>
<field name="arch" type="xml">
<tree>
<field name="name" />
<field name="company_id" groups="base.group_multi_company" />
</tree>
</field>
</record>
<record id="view_account_spread_search" model="ir.ui.view">
<field name="model">account.spread</field>
<field name="arch" type="xml">
<search>
<field name="name" string="Spread" />
<filter
string="Archived"
name="inactive"
domain="[('active','=',False)]"
/>
</search>
</field>
</record>
<record id="action_account_spread_form" model="ir.actions.act_window">
<field name="name">Spread Costs/Revenues</field>
<field name="res_model">account.spread</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_account_spread_tree" />
</record>
<menuitem
id="menu_action_account_spread_form"
parent="account.menu_finance_entries_accounting_miscellaneous"
action="action_account_spread_form"
groups="account.group_account_user,account.group_account_manager"
/>
</odoo>

View File

@ -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>

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_company_form" model="ir.ui.view">
<field name="model">res.company</field>
<field name="inherit_id" ref="base.view_company_form" />
<field name="arch" type="xml">
<xpath expr="//notebook">
<page
name="account_spread_cost_revenue"
string="Account Spread"
groups="account.group_account_manager"
>
<group>
<group
string="Default Spread Accounts"
name="default_spread_accounts"
>
<field name="default_spread_revenue_account_id" />
<field name="default_spread_expense_account_id" />
</group>
<group
string="Default Spread Journals"
name="default_spread_journals"
>
<field name="default_spread_revenue_journal_id" />
<field name="default_spread_expense_journal_id" />
</group>
</group>
<group name="spreading_options">
<group name="spreading_options_left">
<field name="allow_spread_planning" />
</group>
<group name="spreading_options_right">
<field name="force_move_auto_post" />
<field name="auto_archive_spread" />
</group>
</group>
</page>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,3 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import account_spread_invoice_line_link_wizard

View File

@ -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,
}

View File

@ -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>

View File

@ -0,0 +1 @@
../../../../account_spread_cost_revenue

View File

@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)